充电和脱离
This commit is contained in:
@@ -70,6 +70,26 @@ export interface RosCar {
|
||||
liftHeight?: number;
|
||||
/** 升降柱状态 */
|
||||
lift?: RosLift;
|
||||
/** 充电点 X 坐标 */
|
||||
dockX?: number;
|
||||
/** 充电点 Y 坐标 */
|
||||
dockY?: number;
|
||||
/** 充电点朝向 */
|
||||
dockYaw?: number;
|
||||
/** 预对接点 X 坐标 */
|
||||
preDockX?: number;
|
||||
/** 预对接点 Y 坐标 */
|
||||
preDockY?: number;
|
||||
/** 预对接点朝向 */
|
||||
preDockYaw?: number;
|
||||
/** 是否开启自动回充 */
|
||||
autoRechargeEnabled?: boolean;
|
||||
/** 自动回充低电量阈值(%) */
|
||||
lowBatteryThreshold?: number;
|
||||
/** 自动回充状态文本 */
|
||||
dockStatus?: string;
|
||||
/** 自动回充是否进行中 */
|
||||
dockActive?: boolean;
|
||||
/** 创建时间 */
|
||||
createTime?: string;
|
||||
/** 更新时间 */
|
||||
@@ -293,3 +313,61 @@ export function updateRosCarMapUrls(data: {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 小车充电点与自动回充配置更新参数
|
||||
*/
|
||||
export interface RosCarDockConfigUpdate {
|
||||
/** 主键id */
|
||||
id: string;
|
||||
/** 充电点 X 坐标 */
|
||||
dockX?: number;
|
||||
/** 充电点 Y 坐标 */
|
||||
dockY?: number;
|
||||
/** 充电点朝向 */
|
||||
dockYaw?: number;
|
||||
/** 预对接点 X 坐标 */
|
||||
preDockX?: number;
|
||||
/** 预对接点 Y 坐标 */
|
||||
preDockY?: number;
|
||||
/** 预对接点朝向 */
|
||||
preDockYaw?: number;
|
||||
/** 是否开启自动回充 */
|
||||
autoRechargeEnabled?: boolean;
|
||||
/** 自动回充低电量阈值(%) */
|
||||
lowBatteryThreshold?: number;
|
||||
}
|
||||
|
||||
// 更新小车充电点与自动回充配置
|
||||
export function updateRosCarDockConfig(data: RosCarDockConfigUpdate) {
|
||||
return request<ApiResult<null>>({
|
||||
url: "/admin/rosCar/updateDockConfig",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发回充结果,对应后端 com.haizhiyustc.ros.dto.RosChargingResultDTO
|
||||
*/
|
||||
export interface RosChargingResult {
|
||||
/** 小车是否接受本次触发 */
|
||||
accepted?: boolean;
|
||||
/** 触发结果说明 */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// 手动触发小车自动回充
|
||||
export function startRosCharging(carId: string) {
|
||||
return request<ApiResult<RosChargingResult>>({
|
||||
url: "/admin/rosCar/startCharging",
|
||||
params: { carId },
|
||||
});
|
||||
}
|
||||
|
||||
// 手动触发小车脱离充电桩
|
||||
export function undockRosCharging(carId: string) {
|
||||
return request<ApiResult<RosChargingResult>>({
|
||||
url: "/admin/rosCar/undockCharging",
|
||||
params: { carId },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2655,6 +2655,58 @@
|
||||
color: #ffd9d5;
|
||||
}
|
||||
|
||||
.inspection-action-btn.charging {
|
||||
border-color: rgba(126, 255, 196, 0.32);
|
||||
background: linear-gradient(90deg, rgba(20, 104, 82, 0.96), rgba(10, 48, 44, 0.98));
|
||||
color: #d5ffee;
|
||||
}
|
||||
|
||||
.inspection-action-btn.undock {
|
||||
border-color: rgba(255, 205, 126, 0.32);
|
||||
background: linear-gradient(90deg, rgba(122, 78, 18, 0.96), rgba(70, 42, 12, 0.98));
|
||||
color: #ffeccd;
|
||||
}
|
||||
|
||||
.charging-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.charging-status-dot {
|
||||
width: 8px;
|
||||
min-width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(140, 180, 200, 0.5);
|
||||
box-shadow: 0 0 0 3px rgba(140, 180, 200, 0.12);
|
||||
}
|
||||
|
||||
.charging-status-dot.active {
|
||||
background: #7dffb0;
|
||||
box-shadow: 0 0 0 3px rgba(125, 255, 176, 0.18), 0 0 12px rgba(125, 255, 176, 0.6);
|
||||
animation: charging-dot-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.charging-status-text {
|
||||
color: rgba(190, 232, 248, 0.88);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@keyframes charging-dot-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
.inspection-action-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(102, 235, 255, 0.72);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
getRosCarList,
|
||||
getRosInspectionTaskDetail,
|
||||
saveRosInspectionTask,
|
||||
updateRosCarDockConfig,
|
||||
updateRosCarMapUrls,
|
||||
uploadFile,
|
||||
} from "@/api/rosCar";
|
||||
@@ -19,7 +20,7 @@ type RecognitionActionCode =
|
||||
| "TUNNEL_WATER"
|
||||
| "WALL_CRACK";
|
||||
type PatrolTaskMode = "定时" | "周期" | "临时";
|
||||
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint";
|
||||
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint" | "dock" | "preDock";
|
||||
|
||||
interface RosCarRecord {
|
||||
id?: string;
|
||||
@@ -42,6 +43,14 @@ interface RosCarRecord {
|
||||
liftHeight?: number;
|
||||
mapPgmUrl?: string;
|
||||
mapYamlUrl?: string;
|
||||
dockX?: number;
|
||||
dockY?: number;
|
||||
dockYaw?: number;
|
||||
preDockX?: number;
|
||||
preDockY?: number;
|
||||
preDockYaw?: number;
|
||||
autoRechargeEnabled?: boolean;
|
||||
lowBatteryThreshold?: number;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
@@ -67,6 +76,17 @@ interface DeviceRecord {
|
||||
infraredUrl?: string;
|
||||
mapPgmUrl?: string;
|
||||
mapYamlUrl?: string;
|
||||
poseX?: number;
|
||||
poseY?: number;
|
||||
poseYaw?: number;
|
||||
dockX?: number;
|
||||
dockY?: number;
|
||||
dockYaw?: number;
|
||||
preDockX?: number;
|
||||
preDockY?: number;
|
||||
preDockYaw?: number;
|
||||
autoRechargeEnabled?: boolean;
|
||||
lowBatteryThreshold?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -190,6 +210,17 @@ interface PatrolTask {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface DockConfigState {
|
||||
dockX?: number;
|
||||
dockY?: number;
|
||||
dockYaw?: number;
|
||||
preDockX?: number;
|
||||
preDockY?: number;
|
||||
preDockYaw?: number;
|
||||
autoRechargeEnabled: boolean;
|
||||
lowBatteryThreshold: number;
|
||||
}
|
||||
|
||||
interface DeviceConfigState {
|
||||
maps: MapRecord[];
|
||||
activeMapId: string;
|
||||
@@ -201,6 +232,7 @@ interface DeviceConfigState {
|
||||
points: PatrolPoint[];
|
||||
selectedPointIds: string[];
|
||||
plans: PatrolTask[];
|
||||
dock: DockConfigState;
|
||||
}
|
||||
|
||||
interface PlanFormState {
|
||||
@@ -243,6 +275,8 @@ let patrolActionIdSeed = 0;
|
||||
const editorModes: Array<{ id: EditorMode; label: string }> = [
|
||||
{ id: "normal", label: "新增业务点" },
|
||||
{ id: "waypoint", label: "新增途经点" },
|
||||
{ id: "dock", label: "设置充电点" },
|
||||
{ id: "preDock", label: "设置预对接点" },
|
||||
];
|
||||
|
||||
const devices = ref<DeviceRecord[]>([]);
|
||||
@@ -371,6 +405,7 @@ const rosMapImage = new Image();
|
||||
const mapImageUploading = ref(false);
|
||||
const mapConfigUploading = ref(false);
|
||||
const mapUploadMessage = ref("");
|
||||
const dockConfigMessage = ref("");
|
||||
const mapImageInputRef = ref<HTMLInputElement | null>(null);
|
||||
const mapConfigInputRef = ref<HTMLInputElement | null>(null);
|
||||
rosMapImage.onload = () => {
|
||||
@@ -1126,6 +1161,20 @@ function buildDevicePlans(device: DeviceRecord): PatrolTask[] {
|
||||
];
|
||||
}
|
||||
|
||||
// 回充配置默认值与 charging_dock 节点保持一致:默认开启自动回充,阈值 30%
|
||||
function buildDockConfig(device: DeviceRecord): DockConfigState {
|
||||
return {
|
||||
dockX: device.dockX,
|
||||
dockY: device.dockY,
|
||||
dockYaw: device.dockYaw,
|
||||
preDockX: device.preDockX,
|
||||
preDockY: device.preDockY,
|
||||
preDockYaw: device.preDockYaw,
|
||||
autoRechargeEnabled: device.autoRechargeEnabled ?? true,
|
||||
lowBatteryThreshold: device.lowBatteryThreshold ?? 30,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
|
||||
const maps = buildDeviceMaps(device);
|
||||
const points = buildDevicePoints();
|
||||
@@ -1142,6 +1191,7 @@ function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
|
||||
points,
|
||||
selectedPointIds: [],
|
||||
plans,
|
||||
dock: buildDockConfig(device),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1173,6 +1223,17 @@ function normalizeDeviceRecord(car: RosCarRecord, index: number): DeviceRecord {
|
||||
infraredUrl: car.cameraWebrtcUrl2 || "",
|
||||
mapPgmUrl: car.mapPgmUrl || "",
|
||||
mapYamlUrl: car.mapYamlUrl || "",
|
||||
poseX: car.poseX,
|
||||
poseY: car.poseY,
|
||||
poseYaw: car.poseYaw,
|
||||
dockX: car.dockX,
|
||||
dockY: car.dockY,
|
||||
dockYaw: car.dockYaw,
|
||||
preDockX: car.preDockX,
|
||||
preDockY: car.preDockY,
|
||||
preDockYaw: car.preDockYaw,
|
||||
autoRechargeEnabled: car.autoRechargeEnabled,
|
||||
lowBatteryThreshold: car.lowBatteryThreshold,
|
||||
createdAt: car.createTime || "--",
|
||||
};
|
||||
}
|
||||
@@ -1841,6 +1902,23 @@ function renderMapCanvas() {
|
||||
context.restore();
|
||||
});
|
||||
|
||||
drawDockMarker(
|
||||
context,
|
||||
rect,
|
||||
currentConfig.value?.dock.dockX,
|
||||
currentConfig.value?.dock.dockY,
|
||||
"#7dffb0",
|
||||
"充",
|
||||
);
|
||||
drawDockMarker(
|
||||
context,
|
||||
rect,
|
||||
currentConfig.value?.dock.preDockX,
|
||||
currentConfig.value?.dock.preDockY,
|
||||
"#8fb4ff",
|
||||
"预",
|
||||
);
|
||||
|
||||
if (selectionBox.active) {
|
||||
const left = mapFrame.left + Math.min(selectionBox.startX, selectionBox.endX) * unitX;
|
||||
const top = mapFrame.top + Math.min(selectionBox.startY, selectionBox.endY) * unitY;
|
||||
@@ -2359,6 +2437,144 @@ function buildNextPlanId() {
|
||||
return `${currentConfiguredDevice.value?.id ?? "ROBOT"}-TASK${String(maxIndex + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// 巡检点使用 0-100 百分比坐标,充电点使用 ROS 米制坐标,这里做双向换算
|
||||
function percentToRosPosition(xPercent: number, yPercent: number) {
|
||||
const resolution = rosMapMeta.resolution || 0.05;
|
||||
const width = rosMapMeta.width || 0;
|
||||
const height = rosMapMeta.height || 0;
|
||||
|
||||
return {
|
||||
x: (xPercent / 100) * width * resolution + rosMapMeta.origin[0],
|
||||
y: ((100 - yPercent) / 100) * height * resolution + rosMapMeta.origin[1],
|
||||
};
|
||||
}
|
||||
|
||||
function rosToPercentPosition(xRos: number, yRos: number) {
|
||||
const resolution = rosMapMeta.resolution || 0.05;
|
||||
const width = rosMapMeta.width || 1;
|
||||
const height = rosMapMeta.height || 1;
|
||||
|
||||
return {
|
||||
x: ((xRos - rosMapMeta.origin[0]) / resolution / width) * 100,
|
||||
y: 100 - ((yRos - rosMapMeta.origin[1]) / resolution / height) * 100,
|
||||
};
|
||||
}
|
||||
|
||||
function roundDockValue(value: number) {
|
||||
return Number(value.toFixed(4));
|
||||
}
|
||||
|
||||
// 在地图上点选充电点/预对接点,换算成 ROS 坐标后写回回充配置
|
||||
function applyMapPickToDockPose(mode: "dock" | "preDock", position: { x: number; y: number }) {
|
||||
if (!currentConfig.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rosPosition = percentToRosPosition(position.x, position.y);
|
||||
const x = roundDockValue(rosPosition.x);
|
||||
const y = roundDockValue(rosPosition.y);
|
||||
|
||||
if (mode === "dock") {
|
||||
currentConfig.value.dock.dockX = x;
|
||||
currentConfig.value.dock.dockY = y;
|
||||
dockConfigMessage.value = `充电点已更新为 x=${x}, y=${y},朝向可手填或使用当前位姿`;
|
||||
} else {
|
||||
currentConfig.value.dock.preDockX = x;
|
||||
currentConfig.value.dock.preDockY = y;
|
||||
dockConfigMessage.value = `预对接点已更新为 x=${x}, y=${y},朝向可手填或使用当前位姿`;
|
||||
}
|
||||
|
||||
scheduleMapCanvasRender();
|
||||
}
|
||||
|
||||
// 用小车最近上报的当前位姿填充充电点/预对接点
|
||||
function fillDockPoseFromRobot(mode: "dock" | "preDock") {
|
||||
if (!currentConfig.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = currentConfiguredDevice.value;
|
||||
if (
|
||||
typeof device?.poseX !== "number" ||
|
||||
typeof device?.poseY !== "number" ||
|
||||
typeof device?.poseYaw !== "number"
|
||||
) {
|
||||
dockConfigMessage.value = "未获取到小车当前位姿,无法使用当前位姿填充";
|
||||
return;
|
||||
}
|
||||
|
||||
const x = roundDockValue(device.poseX);
|
||||
const y = roundDockValue(device.poseY);
|
||||
const yaw = roundDockValue(device.poseYaw);
|
||||
|
||||
if (mode === "dock") {
|
||||
currentConfig.value.dock.dockX = x;
|
||||
currentConfig.value.dock.dockY = y;
|
||||
currentConfig.value.dock.dockYaw = yaw;
|
||||
dockConfigMessage.value = `充电点已填入小车当前位姿 x=${x}, y=${y}, yaw=${yaw}`;
|
||||
} else {
|
||||
currentConfig.value.dock.preDockX = x;
|
||||
currentConfig.value.dock.preDockY = y;
|
||||
currentConfig.value.dock.preDockYaw = yaw;
|
||||
dockConfigMessage.value = `预对接点已填入小车当前位姿 x=${x}, y=${y}, yaw=${yaw}`;
|
||||
}
|
||||
|
||||
scheduleMapCanvasRender();
|
||||
}
|
||||
|
||||
// 保存充电点与自动回充配置;后端保存后会同步下发给在线小车
|
||||
async function saveDockConfig() {
|
||||
const device = currentConfiguredDevice.value;
|
||||
const config = currentConfig.value;
|
||||
if (!device || !config) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRosCarDockConfig({
|
||||
id: device.id,
|
||||
...config.dock,
|
||||
});
|
||||
}
|
||||
|
||||
// 在地图上绘制充电点/预对接点标记
|
||||
function drawDockMarker(
|
||||
context: CanvasRenderingContext2D,
|
||||
rect: DOMRect,
|
||||
xRos: number | undefined,
|
||||
yRos: number | undefined,
|
||||
color: string,
|
||||
label: string,
|
||||
) {
|
||||
if (typeof xRos !== "number" || typeof yRos !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
const percentPosition = rosToPercentPosition(xRos, yRos);
|
||||
if (!Number.isFinite(percentPosition.x) || !Number.isFinite(percentPosition.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pixelPoint = percentToStagePixels(percentPosition.x, percentPosition.y, rect);
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.arc(pixelPoint.x, pixelPoint.y, 11, 0, Math.PI * 2);
|
||||
context.fillStyle = "rgba(5, 18, 38, 0.94)";
|
||||
context.fill();
|
||||
context.lineWidth = 2;
|
||||
context.strokeStyle = color;
|
||||
context.shadowColor = color;
|
||||
context.shadowBlur = 12;
|
||||
context.stroke();
|
||||
|
||||
context.shadowBlur = 0;
|
||||
context.fillStyle = color;
|
||||
context.font = '700 10px "Microsoft YaHei", sans-serif';
|
||||
context.textAlign = "center";
|
||||
context.fillText(label, pixelPoint.x, pixelPoint.y + 0.5);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function handleMapStageClick(event: MouseEvent) {
|
||||
if (suppressStageClick.value) {
|
||||
window.setTimeout(() => {
|
||||
@@ -2376,6 +2592,11 @@ function handleMapStageClick(event: MouseEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mapEditorMode.value === "dock" || mapEditorMode.value === "preDock") {
|
||||
applyMapPickToDockPose(mapEditorMode.value, position);
|
||||
return;
|
||||
}
|
||||
|
||||
const hitPoint = findHitPoint(event);
|
||||
if (hitPoint) {
|
||||
handlePointClick(hitPoint.id, event);
|
||||
@@ -2562,6 +2783,7 @@ async function saveConfigDialog() {
|
||||
|
||||
try {
|
||||
await saveCurrentMapUrls();
|
||||
await saveDockConfig();
|
||||
await saveRosInspectionTask(
|
||||
buildInspectionTaskPayload(currentConfiguredDevice.value, currentConfig.value),
|
||||
);
|
||||
@@ -3117,6 +3339,86 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-side-card dock-config-board">
|
||||
<div class="dock-config-head">
|
||||
<span class="dock-config-title">充电回充配置</span>
|
||||
<div class="dock-switch-row">
|
||||
<label class="dock-switch">
|
||||
<input v-model="currentConfig.dock.autoRechargeEnabled" type="checkbox" />
|
||||
<span>自动回充</span>
|
||||
</label>
|
||||
<label class="dock-threshold">
|
||||
<span>阈值</span>
|
||||
<input v-model.number="currentConfig.dock.lowBatteryThreshold" max="99" min="1" step="1"
|
||||
type="number" />
|
||||
<span>%</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dock-pose-block">
|
||||
<div class="dock-pose-head">
|
||||
<span class="dock-pose-title">充电点</span>
|
||||
<div class="dock-pose-actions">
|
||||
<button :class="{ primary: mapEditorMode === 'dock' }" class="page-action-btn ghost" type="button"
|
||||
@click="mapEditorMode = 'dock'">
|
||||
地图点选
|
||||
</button>
|
||||
<button class="page-action-btn ghost" type="button" @click="fillDockPoseFromRobot('dock')">
|
||||
当前位姿
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dock-pose-grid">
|
||||
<label class="config-field">
|
||||
<span>X</span>
|
||||
<input v-model.number="currentConfig.dock.dockX" step="0.001" type="number" />
|
||||
</label>
|
||||
<label class="config-field">
|
||||
<span>Y</span>
|
||||
<input v-model.number="currentConfig.dock.dockY" step="0.001" type="number" />
|
||||
</label>
|
||||
<label class="config-field">
|
||||
<span>Yaw</span>
|
||||
<input v-model.number="currentConfig.dock.dockYaw" step="0.001" type="number" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dock-pose-block">
|
||||
<div class="dock-pose-head">
|
||||
<span class="dock-pose-title">预对接点</span>
|
||||
<div class="dock-pose-actions">
|
||||
<button :class="{ primary: mapEditorMode === 'preDock' }" class="page-action-btn ghost" type="button"
|
||||
@click="mapEditorMode = 'preDock'">
|
||||
地图点选
|
||||
</button>
|
||||
<button class="page-action-btn ghost" type="button" @click="fillDockPoseFromRobot('preDock')">
|
||||
当前位姿
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dock-pose-grid">
|
||||
<label class="config-field">
|
||||
<span>X</span>
|
||||
<input v-model.number="currentConfig.dock.preDockX" step="0.001" type="number" />
|
||||
</label>
|
||||
<label class="config-field">
|
||||
<span>Y</span>
|
||||
<input v-model.number="currentConfig.dock.preDockY" step="0.001" type="number" />
|
||||
</label>
|
||||
<label class="config-field">
|
||||
<span>Yaw</span>
|
||||
<input v-model.number="currentConfig.dock.preDockYaw" step="0.001" type="number" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="dockConfigMessage" class="dock-config-message">
|
||||
{{ dockConfigMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -3347,7 +3649,7 @@ watch(
|
||||
}
|
||||
|
||||
.redesigned-side-panel {
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel-head.modern {
|
||||
@@ -3387,6 +3689,123 @@ watch(
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dock-config-board {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.dock-config-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dock-config-title {
|
||||
color: #f0fbff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dock-switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dock-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: rgba(200, 240, 255, 0.86);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dock-switch input {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
accent-color: #4fd1ff;
|
||||
}
|
||||
|
||||
.dock-threshold {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: rgba(173, 224, 246, 0.78);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dock-threshold input {
|
||||
width: 54px;
|
||||
padding: 3px 6px;
|
||||
border: 1px solid rgba(85, 198, 255, 0.18);
|
||||
background: rgba(8, 31, 62, 0.72);
|
||||
color: #eefcff;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dock-pose-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(85, 198, 255, 0.14);
|
||||
background: rgba(7, 26, 50, 0.6);
|
||||
}
|
||||
|
||||
.dock-pose-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dock-pose-title {
|
||||
color: rgba(210, 244, 255, 0.92);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dock-pose-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dock-pose-actions .page-action-btn {
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.dock-pose-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dock-pose-grid .config-field {
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dock-pose-grid .config-field input {
|
||||
padding: 3px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dock-config-message {
|
||||
color: rgba(150, 240, 200, 0.86);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -4030,8 +4449,9 @@ watch(
|
||||
|
||||
.rounds-board {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
/* 新增充电回充卡片后,轮次配置不再设为 0 基准高度,避免被压缩到看不见 */
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
getCurrentRosInspection,
|
||||
getRosCarList,
|
||||
getRosInspectionTaskDetail,
|
||||
startRosCharging,
|
||||
stopDispatchRosInspectionTask,
|
||||
undockRosCharging,
|
||||
type RosChargingResult,
|
||||
} from "@/api/rosCar";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
||||
@@ -42,6 +45,8 @@ interface RosCarRecord {
|
||||
cameraWebrtcUrl2?: string;
|
||||
liftHeight?: number;
|
||||
liftDeviceStatusDescription?: string;
|
||||
dockStatus?: string;
|
||||
dockActive?: boolean;
|
||||
}
|
||||
|
||||
interface RosCarListResult {
|
||||
@@ -69,6 +74,8 @@ interface CarStatusPushData {
|
||||
mapYamlUrl?: string;
|
||||
status?: number;
|
||||
signal?: string;
|
||||
dockStatus?: string;
|
||||
dockActive?: boolean;
|
||||
}
|
||||
|
||||
interface CarVelocityPushData {
|
||||
@@ -115,6 +122,8 @@ interface DeviceItem {
|
||||
mapYamlUrl: string;
|
||||
whiteLightUrl: string;
|
||||
infraredUrl: string;
|
||||
dockStatus?: string;
|
||||
dockActive?: boolean;
|
||||
}
|
||||
|
||||
interface ProjectItem {
|
||||
@@ -195,6 +204,11 @@ interface RosDispatchResultResponse {
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
interface RosChargingResultResponse {
|
||||
data?: RosChargingResult;
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
interface AlertItem {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -286,6 +300,8 @@ function normalizeDeviceRecord(car: RosCarRecord, index: number): DeviceItem {
|
||||
mapYamlUrl: car.mapYamlUrl || "",
|
||||
whiteLightUrl: car.cameraWebrtcUrl || "",
|
||||
infraredUrl: car.cameraWebrtcUrl2 || "",
|
||||
dockStatus: car.dockStatus,
|
||||
dockActive: car.dockActive ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -494,6 +510,8 @@ const inspectionTaskId = ref("");
|
||||
const currentInspection = ref<RosInspectionRecord | null>(null);
|
||||
const inspectionLoading = ref(false);
|
||||
const inspectionActionLoading = ref(false);
|
||||
const chargingActionLoading = ref(false);
|
||||
const undockingActionLoading = ref(false);
|
||||
const lastDispatchTime = ref("");
|
||||
const alertLoading = ref(false);
|
||||
const alertError = ref("");
|
||||
@@ -614,6 +632,27 @@ const currentScenario = computed(() => navScenario);
|
||||
const hasRobotPose = computed(
|
||||
() => typeof currentRobot.value.poseX === "number" && typeof currentRobot.value.poseY === "number",
|
||||
);
|
||||
// 回充状态文案:由 charging_dock 节点的 /dock_status 上报,未上报时给出中性提示
|
||||
const chargingStatusText = computed(() => {
|
||||
const device = currentRobot.value;
|
||||
|
||||
if (device.dockActive) {
|
||||
return device.dockStatus || "自动回充进行中";
|
||||
}
|
||||
return device.dockStatus || "回充状态未上报";
|
||||
});
|
||||
const chargingButtonText = computed(() => {
|
||||
if (currentRobot.value.dockActive) {
|
||||
return "回充中...";
|
||||
}
|
||||
return chargingActionLoading.value ? "处理中..." : "自动回充";
|
||||
});
|
||||
const undockingButtonText = computed(() => {
|
||||
if (currentRobot.value.dockActive) {
|
||||
return "处理中...";
|
||||
}
|
||||
return undockingActionLoading.value ? "处理中..." : "充电脱离";
|
||||
});
|
||||
|
||||
// 将 ROS 米制坐标转换成地图图片像素坐标,路线和机器人定位共用同一套转换
|
||||
function rosPoseToMapPoint(poseX: number, poseY: number, poseYaw = 0): RobotMapPoint | null {
|
||||
@@ -861,6 +900,74 @@ function confirmStopInspection() {
|
||||
showConfirmDialog("停止巡检", "确认停止当前巡检任务吗?", () => runInspectionAction("stop"));
|
||||
}
|
||||
|
||||
// 手动触发一次自动回充;需要小车在线且车端已启动回充节点
|
||||
async function runStartCharging() {
|
||||
const carId = currentRobot.value.carId;
|
||||
|
||||
if (!carId) {
|
||||
window.$message.warning("未获取到小车ID,请先在设备管理中确认小车已保存");
|
||||
return;
|
||||
}
|
||||
|
||||
chargingActionLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = (await startRosCharging(carId)) as RosChargingResultResponse;
|
||||
const accepted = response.data?.accepted ?? false;
|
||||
const message = response.data?.message
|
||||
|| response.msg
|
||||
|| (accepted ? "已触发自动回充" : "触发自动回充失败");
|
||||
|
||||
if (accepted) {
|
||||
window.$message.success(message);
|
||||
} else {
|
||||
window.$message.warning(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
chargingActionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmStartCharging() {
|
||||
showConfirmDialog("自动回充", "确认让小车执行自动回充吗?", () => runStartCharging());
|
||||
}
|
||||
|
||||
// 手动触发一次脱离充电桩;用于充电完成后手动让小车离开充电桩
|
||||
async function runUndockCharging() {
|
||||
const carId = currentRobot.value.carId;
|
||||
|
||||
if (!carId) {
|
||||
window.$message.warning("未获取到小车ID,请先在设备管理中确认小车已保存");
|
||||
return;
|
||||
}
|
||||
|
||||
undockingActionLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = (await undockRosCharging(carId)) as RosChargingResultResponse;
|
||||
const accepted = response.data?.accepted ?? false;
|
||||
const message = response.data?.message
|
||||
|| response.msg
|
||||
|| (accepted ? "已触发脱离充电桩" : "触发脱离充电桩失败");
|
||||
|
||||
if (accepted) {
|
||||
window.$message.success(message);
|
||||
} else {
|
||||
window.$message.warning(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
undockingActionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmUndockCharging() {
|
||||
showConfirmDialog("充电脱离", "确认让小车脱离充电桩吗?", () => runUndockCharging());
|
||||
}
|
||||
|
||||
// 切换设备时将当前设备已有升降柱高度回显到目标高度输入框
|
||||
function syncLiftTargetFromDevice(device?: DeviceItem) {
|
||||
if (!device || device.liftHeight === "--") {
|
||||
@@ -921,6 +1028,8 @@ function applyCarStatusPush(data: CarStatusPushData) {
|
||||
poseYaw: typeof data.poseYaw === "number" ? data.poseYaw : currentDevice.poseYaw,
|
||||
mapPgmUrl: data.mapPgmUrl || currentDevice.mapPgmUrl,
|
||||
mapYamlUrl: data.mapYamlUrl || currentDevice.mapYamlUrl,
|
||||
dockStatus: typeof data.dockStatus === "string" ? data.dockStatus : currentDevice.dockStatus,
|
||||
dockActive: typeof data.dockActive === "boolean" ? data.dockActive : currentDevice.dockActive,
|
||||
};
|
||||
|
||||
projectDevices.value[0].devices.splice(deviceIndex, 1, nextDevice);
|
||||
@@ -1868,6 +1977,26 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
>
|
||||
{{ inspectionActionLoading ? "处理中..." : "下发巡检" }}
|
||||
</button>
|
||||
<div class="charging-status-row">
|
||||
<span :class="['charging-status-dot', { active: currentRobot.dockActive }]"></span>
|
||||
<span class="charging-status-text">{{ chargingStatusText }}</span>
|
||||
</div>
|
||||
<button
|
||||
class="inspection-action-btn charging"
|
||||
type="button"
|
||||
:disabled="chargingActionLoading || currentRobot.dockActive"
|
||||
@click="confirmStartCharging"
|
||||
>
|
||||
{{ chargingButtonText }}
|
||||
</button>
|
||||
<button
|
||||
class="inspection-action-btn undock"
|
||||
type="button"
|
||||
:disabled="undockingActionLoading || currentRobot.dockActive"
|
||||
@click="confirmUndockCharging"
|
||||
>
|
||||
{{ undockingButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
"poseY": 6.789,
|
||||
"poseYaw": 1.57,
|
||||
"status": 1,
|
||||
"signal": "-58dBm"
|
||||
"signal": "-58dBm",
|
||||
"dockStatus": "自动回充待机中",
|
||||
"dockActive": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -42,6 +44,8 @@
|
||||
| `data.poseYaw` | 否 | 当前朝向 |
|
||||
| `data.status` | 否 | 小车状态:`0` 空闲,`1` 巡检,`2` 离线,`3` 充电中 |
|
||||
| `data.signal` | 否 | 小车信号 |
|
||||
| `data.dockStatus` | 否 | 自动回充状态文本,取自 `/dock_status` |
|
||||
| `data.dockActive` | 否 | 回充或脱离充电桩是否进行中,取自 `/auto_docking_active` 与 `/auto_undocking_active` |
|
||||
|
||||
说明:
|
||||
|
||||
@@ -77,7 +81,9 @@
|
||||
"poseY": 6.789,
|
||||
"poseYaw": 1.57,
|
||||
"status": 1,
|
||||
"signal": "-58dBm"
|
||||
"signal": "-58dBm",
|
||||
"dockStatus": "自动回充待机中",
|
||||
"dockActive": false
|
||||
},
|
||||
"timestamp": 1782702000010
|
||||
}
|
||||
@@ -86,10 +92,12 @@
|
||||
顶层字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| ---------------- | ---------------------------------------- |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| `type` | 固定为 `car_status` |
|
||||
| `data.deviceKey` | 小车设备标识 |
|
||||
| `data.liftHeight` | 升降柱高度,单位 mm |
|
||||
| `data.dockStatus` | 自动回充状态文本;用于前端回显当前回充阶段 |
|
||||
| `data.dockActive` | 回充或脱离充电桩是否进行中;为 `true` 时前端禁用「自动回充」「充电脱离」按钮 |
|
||||
| `timestamp` | 后端推送时间戳,毫秒 |
|
||||
|
||||
|
||||
|
||||
@@ -165,6 +165,74 @@ uint16 height
|
||||
| `3` | 停止 | `0` |
|
||||
| `4` | 运行到指定高度 | 目标高度,单位 mm |
|
||||
|
||||
## 充电点与回充配置
|
||||
|
||||
ROS 包参考:`charging_dock_interfaces/srv/SetDockPose`、`charging_dock_interfaces/srv/SetDockConfig`
|
||||
|
||||
消息类型:`dock_config`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "dock_config",
|
||||
"requestId": "dock-1",
|
||||
"data": {
|
||||
"deviceKey": "ROBOT-001",
|
||||
"dockX": 1.235,
|
||||
"dockY": -0.42,
|
||||
"dockYaw": 0.0,
|
||||
"preDockX": 1.8,
|
||||
"preDockY": -0.42,
|
||||
"preDockYaw": 0.0,
|
||||
"autoRechargeEnabled": true,
|
||||
"lowBatteryThreshold": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `dockX` / `dockY` / `dockYaw` | 充电点(对接点)位姿,`map` 坐标系,单位米 / 弧度 |
|
||||
| `preDockX` / `preDockY` / `preDockYaw` | 预对接点位姿,同上 |
|
||||
| `autoRechargeEnabled` | 是否开启低电量自动回充 |
|
||||
| `lowBatteryThreshold` | 自动回充触发的电量阈值(%) |
|
||||
|
||||
说明:
|
||||
|
||||
- 三组字段互相独立:ROS 端只对**同时提供完整三元组**的点位调用 `/charging_dock/set_dock_pose` 或 `/charging_dock/set_pre_dock_pose`,缺任一字段则跳过;`autoRechargeEnabled` 与 `lowBatteryThreshold` 任一缺失时跳过 `/charging_dock/set_config`。
|
||||
- 该消息由后端 `POST /admin/rosCar/updateDockConfig` 保存成功后下发,浏览器端不直接发送。
|
||||
- 小车离线时后端只落库、不下发,需要重新保存一次才能同步到车端。
|
||||
- 服务不可用时 ROS 端只记录告警,不回执失败。
|
||||
|
||||
## 回充与脱离充电桩
|
||||
|
||||
消息类型:`dock_start`(触发自动回充)、`dock_undock`(脱离充电桩)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "dock_undock",
|
||||
"requestId": "dock-undock-1",
|
||||
"data": {
|
||||
"deviceKey": "ROBOT-001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `deviceKey` | 控制目标机器人设备标识 |
|
||||
|
||||
说明:
|
||||
|
||||
- 两条消息都由后端接口下发,浏览器端不直接发送:回充 `POST /admin/rosCar/startCharging`,脱离 `POST /admin/rosCar/undockCharging`,参数都是 `carId`。
|
||||
- 车端分别调用 `/charging_dock/start` 与 `/charging_dock/undock`(均为 `std_srvs/Trigger`)。
|
||||
- `accepted=true` 只代表车端已受理,机械动作在车端异步执行;执行期间 `dockActive` 为 `true`,进度由 `dockStatus` 文本回显(`/auto_docking_active`、`/auto_undocking_active` 合并上报)。
|
||||
- 脱离动作:车端切到横移模式后向左侧退出约 `undock_distance`(默认 0.30 m)离开充电面板,完成后恢复阿克曼模式。
|
||||
- 充电完成自动脱离:车端电量升到 `undock_battery_threshold`(默认 95%),且本次到桩时电量低于(阈值 - `undock_battery_start_margin`,默认 3%)时,自动触发一次脱离。
|
||||
|
||||
## 机器人端收到的消息
|
||||
|
||||
设备端连接:
|
||||
|
||||
@@ -36,7 +36,7 @@ export default defineConfig(({ command }: ConfigEnv) => {
|
||||
rewrite: (path: string) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/ws': {
|
||||
target: `ws://192.168.3.110:16003/`,
|
||||
target: `ws://127.0.0.1:16003/`,
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user