优化 map中实时车悬浮的提醒框子,优化已完成任务提醒哪个设备 优化页面 设备类型显示 和 在线状态显示 预设路线分组显示 优化路径规划中的问题

This commit is contained in:
mmc
2026-11-10 17:03:52 +08:00
parent 6401327522
commit ce9f52fc52
10 changed files with 575 additions and 327 deletions

View File

@@ -17,7 +17,7 @@ import * as Cesium from 'cesium';
import 'cesium/Build/Cesium/Widgets/widgets.css';
const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], markedPoints = [], onPathChange, from, onClearDraw, onMarkPoint, routeMode = '', polyPoints, mapLayer, activeType, overlayRefreshKey = 0, focusOverlay = null, onObstacleRadiusChange, obstacleShapes = [], onObstacleRectResize, siteMapInfoId, detectReportInfo }, ref) => {
const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, finishPoint, drawRealTime, realtimePosition, realtimeType, realtimeDevice, pathPoints = [], devices = [], onCameraClick, focusLocation, points = [], markedPoints = [], onPathChange, from, onClearDraw, onMarkPoint, routeMode = '', polyPoints, mapLayer, activeType, overlayRefreshKey = 0, focusOverlay = null, onObstacleRadiusChange, obstacleShapes = [], onObstacleRectResize, siteMapInfoId, detectReportInfo }, ref) => {
const containerRef = useRef(null);
const viewerRef = useRef(null);
const tooltipRef = useRef(null);
@@ -37,8 +37,13 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
const onMarkPointRef = useRef(onMarkPoint); // 总是指向最新的 onMarkPoint,避免闭包滞后
const onCameraClickRef = useRef(onCameraClick); // 同样处理,避免闭包滞后
const lastHoverPositionRef = useRef(null);
// 实时车辆图标(realtimeCarEntity)对应的设备信息,用于悬浮提示
const realtimeDeviceRef = useRef(null);
const { t } = useTranslation();
// 实时模式下车图标为独立实体,没有 properties.robot,这里记录其设备信息
realtimeDeviceRef.current = realtimeDevice || (devices && devices[0]) || null;
// 场站已配置的返航点 / 分区叠加显示
const stationId = useSelector((state) => state?.station?.stationId || state?.station?.currentStation?.id);
const [stationOverlay, setStationOverlay] = useState({ returnPoints: [], regions: [], tempObstaclePoints: [] });
@@ -1112,7 +1117,13 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
function resolveDeviceStatus(device) {
const fsRaw = device?.feStatus;
const stRaw = device?.status;
if (device?.onlineStatus == 0) return 'offline';
//if (device?.onlineStatus == 0) return 'offline';
if (stRaw !== undefined && stRaw !== null && stRaw !== '') {
const st = Number(stRaw);
if (st === 1) return 'working';
if (st === 2) return 'idle';
if (st === 3) return 'offline';
}
// feStatus:0=离线,1=工作中,2=空闲
if (fsRaw !== undefined && fsRaw !== null && fsRaw !== '') {
@@ -1196,7 +1207,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
<div>${t('systemSetting.status')}:
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${status.color};margin-right:4px;"></span>
<span style="color:${status.color};font-weight:bold">${status.text}</span>
</div> `;
</div>`;
} else {
tooltip.innerHTML = `
<div style = "font-weight:600;margin-bottom:4px;" >📍 ${t('systemSetting.longitude')}:${lonStr}</div >
@@ -1204,6 +1215,15 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
`;
}
}
// 判断拾取到的实体是否为实时车图标或其光圈(光圈实体没有 properties.robot,需单独识别)
const isRealtimeCarPick = (picked) => {
if (!Cesium.defined(picked) || !picked.id) return false;
const target = picked.id;
if (realtimeCarEntityRef.current && target === realtimeCarEntityRef.current) return true;
const entityId = typeof target.id === 'string' ? target.id : '';
return entityId.startsWith('realtime_car_halo');
};
const handleFlyToDevice = () => {
let validPositions = [];
const viewer = viewerRef.current;
@@ -2787,7 +2807,7 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
device?.type === "plane" ? planeImg : // 飞机
device?.type === "drone" ? planeImg : // 无人机
device?.type === "mower" ? carImg : // 割草
device?.type === "robot" ? droneInspectImg : // 巡检
device?.type === "robot" ? carImg : // 巡检
carImg; // 默认车
width =
@@ -2954,11 +2974,14 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
const pickedNow = viewer.scene.pick(hoverPos);
const pickedReturnPointNow = (Cesium.defined(pickedNow) && pickedNow.id?.returnPointData) ? pickedNow.id.returnPointData : null;
const pickedRobotNow = (Cesium.defined(pickedNow) && pickedNow.id?.properties?.robot) ? pickedNow.id.properties.robot.getValue() : null;
// 实时车图标及其光圈(无 properties.robot)悬浮时,用当前实时设备信息补全
const pickedIsRealtimeCarNow = isRealtimeCarPick(pickedNow);
const robotNow = pickedRobotNow || (pickedIsRealtimeCarNow ? realtimeDeviceRef.current : null);
if (pickedReturnPointNow) {
showReturnPointTooltip(tooltipRef.current, hoverPos, pickedReturnPointNow);
} else if (pickedRobotNow) {
} else if (robotNow) {
const hoverLngLat = getCursorLngLat(viewer, hoverPos);
showCoordinateTooltip(tooltipRef.current, hoverPos, t, hoverLngLat.lon, hoverLngLat.lat, pickedRobotNow);
showCoordinateTooltip(tooltipRef.current, hoverPos, t, hoverLngLat.lon, hoverLngLat.lat, robotNow);
}
}
@@ -3122,13 +3145,18 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine,
const picked = viewer.scene.pick(movement.endPosition);
const pickedReturnPoint = (Cesium.defined(picked) && picked.id?.returnPointData) ? picked.id.returnPointData : null;
const pickedRobot = (Cesium.defined(picked) && picked.id?.properties?.robot) ? picked.id.properties.robot.getValue() : null;
// 实时模式下的车图标及光圈是独立实体(无 properties.robot),用当前实时设备信息补全悬浮提示
const pickedIsRealtimeCar = isRealtimeCarPick(picked);
// 返航点优先显示「别名 + 绑定机器」,设备显示设备信息,空白区域隐藏
if (pickedReturnPoint) {
lastHoverPositionRef.current = movement.endPosition;
showReturnPointTooltip(tooltipRef.current, movement.endPosition, pickedReturnPoint);
} else if (pickedRobot) {
} else if (pickedRobot || (pickedIsRealtimeCar && realtimeDeviceRef.current)) {
lastHoverPositionRef.current = movement.endPosition;
const robot = pickedRobot || realtimeDeviceRef.current;
const { lon, lat } = getCursorLngLat(viewer, movement.endPosition);
showCoordinateTooltip(tooltipRef.current, movement.endPosition, t, lon, lat, pickedRobot);
showCoordinateTooltip(tooltipRef.current, movement.endPosition, t, lon, lat, robot);
} else {
lastHoverPositionRef.current = null;
tooltipRef.current.style.display = "none";

View File

@@ -115,14 +115,9 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
try {
const res = await deviceRunParamSelectByDeviceId({ deviceId: id });
if (res.code == 200) {
if (res.data) {
setParamData(res.data);
paramForm.setFieldsValue({ ...res.data });
} else {
utils.message.error(t('devices.fetchParamFail'));
setParamData(null);
paramForm.resetFields();
}
setParamData(res.data);
paramForm.setFieldsValue({ ...res.data });
} else {
utils.message.error(t('devices.fetchParamFail'));
}
@@ -164,127 +159,127 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
onCancel?.();
};
// ========== 按你后端Java实体顺序重新整理字段配置 ==========
// 枚举取值参考设备参数规范:设备类型 M/I/U、车轮 W/T、动力 E/H、RTK N/B、发动机 H/L/Z 等
const paramFieldConfigs: any[] = [
// 【可编辑】增益滑块 + runSpeed
//{ key: 'leftForwardGain', label: t('devices.paramLeftForwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'leftBackwardGain', label: t('devices.paramLeftBackwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'rightForwardGain', label: t('devices.paramRightForwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'rightBackwardGain', label: t('devices.paramRightBackwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'runSpeed', label: t('devices.paramRunSpeed'), type: 'InputNumber', editable: true },
// ========== 按你后端Java实体顺序重新整理字段配置 ==========
// 枚举取值参考设备参数规范:设备类型 M/I/U、车轮 W/T、动力 E/H、RTK N/B、发动机 H/L/Z 等
const paramFieldConfigs: any[] = [
// 【可编辑】增益滑块 + runSpeed
//{ key: 'leftForwardGain', label: t('devices.paramLeftForwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'leftBackwardGain', label: t('devices.paramLeftBackwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'rightForwardGain', label: t('devices.paramRightForwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'rightBackwardGain', label: t('devices.paramRightBackwardGain'), type: 'GainSlider', editable: true, min: 0.1, max: 1, step: 0.01 },
//{ key: 'runSpeed', label: t('devices.paramRunSpeed'), type: 'InputNumber', editable: true },
// 下面全部只读,和后端Excel实体顺序对齐
{ key: 'deviceId', label: t('devices.deviceId'), type: 'Input', editable: isAdmin },
{
key: 'productModel', label: t('devices.productModel'), type: 'Select', editable: isAdmin,
options: [
{ value: 'ST', label: 'ST' },
{ value: 'PL', label: 'PL' },
{ value: 'PR', label: 'PR' },
{ value: 'AI', label: 'AI' },
],
},
{
key: 'deviceType', label: t('devices.deviceType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'M', label: t('devices.paramEnum.deviceTypeM') },
{ value: 'I', label: t('devices.paramEnum.deviceTypeI') },
{ value: 'U', label: t('devices.paramEnum.deviceTypeU') },
],
},
{
key: 'rtkMode', label: t('devices.rtkMode'), type: 'Select', editable: isAdmin,
options: [
{ value: 'N', label: t('devices.paramEnum.rtkModeN') },
{ value: 'B', label: t('devices.paramEnum.rtkModeB') },
],
},
{ key: 'cameraNum', label: t('devices.cameraNum'), type: 'InputNumber', editable: isAdmin, min: 0, max: 4, step: 1 },
{
key: 'powerType', label: t('devices.powerType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'E', label: t('devices.paramEnum.powerTypeE') },
{ value: 'H', label: t('devices.paramEnum.powerTypeH') },
],
},
{
key: 'wheelType', label: t('devices.wheelType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'W', label: t('devices.paramEnum.wheelTypeW') },
{ value: 'T', label: t('devices.paramEnum.wheelTypeT') },
],
},
{
key: 'engineType', label: t('devices.engineType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'H', label: t('devices.paramEnum.engineTypeH') },
{ value: 'L', label: t('devices.paramEnum.engineTypeL') },
{ value: 'Z', label: t('devices.paramEnum.engineTypeZ') },
],
},
{
key: 'hasHost', label: t('devices.hasHost'), type: 'Select', editable: isAdmin,
options: [
{ value: 0, label: t('devices.paramEnum.hasHostNo') },
{ value: 1, label: t('devices.paramEnum.hasHostYes') },
],
},
{
key: 'batteryType', label: t('devices.batteryType'), type: 'Select', editable: isAdmin,
options: [
{ value: '铅酸', label: t('devices.paramEnum.batteryLeadAcid') },
{ value: '锂电', label: t('devices.paramEnum.batteryLithium') },
],
},
{
key: 'walkingDriveType', label: t('devices.walkingDriveType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'FDK', label: 'FDK' },
{ value: 'SdBLdc', label: 'SdBLdc' },
{ value: 'SdFoc', label: 'SdFoc' },
],
},
{
key: 'cutterDriveType', label: t('devices.cutterDriveType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'XLD', label: 'XLD' },
{ value: 'LH', label: 'LH' },
],
},
{
key: 'rtkAntennasNum', label: t('devices.rtkAntennasNum'), type: 'Select', editable: isAdmin,
options: [
{ value: 1, label: t('devices.paramEnum.rtkAntennaSingle') },
{ value: 2, label: t('devices.paramEnum.rtkAntennaDual') },
],
},
{
key: 'handleType', label: t('devices.handleType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'SBUS', label: t('devices.paramEnum.handleSbus') },
{ value: '自定义', label: t('devices.paramEnum.handleCustom') },
],
},
{ key: 'pcbVersion', label: t('devices.pcbVersion'), type: 'Input', editable: isAdmin },
{ key: 'dimensions', label: t('devices.dimensions'), type: 'Input', editable: isAdmin },
{ key: 'bladeWidth', label: t('devices.bladeWidth'), type: 'Input', editable: isAdmin },
{ key: 'tyreSize', label: t('devices.tyreSize'), type: 'Input', editable: isAdmin },
{ key: 'speedRatio', label: t('devices.speedRatio'), type: 'Input', editable: isAdmin },
{ key: 'cellularInfo', label: t('devices.cellularInfo'), type: 'Input', editable: isAdmin },
{ key: 'wifiName', label: t('devices.paramWifiName'), type: 'Input', editable: isAdmin },
{ key: 'bluetoothName', label: t('devices.bluetoothName'), type: 'Input', editable: isAdmin },
{ key: 'hardwareIdentifiers', label: t('devices.hardwareIdentifiers'), type: 'Input', editable: isAdmin },
{ key: 'imuModel', label: t('devices.imuModel'), type: 'Input', editable: isAdmin },
{ key: 'tempHumiditySensor', label: t('devices.tempHumiditySensor'), type: 'Input', editable: isAdmin },
{ key: 'barometerPresent', label: t('devices.barometerPresent'), type: 'Input', editable: isAdmin },
{ key: 'ultrasonicPresent', label: t('devices.ultrasonicPresent'), type: 'Input', editable: isAdmin },
{ key: 'dacPresent', label: t('devices.dacPresent'), type: 'Input', editable: isAdmin },
{ key: 'operator', label: t('devices.operator'), type: 'Input', editable: isAdmin },
{ key: 'inspectionNo', label: t('devices.inspectionNo'), type: 'Input', editable: isAdmin },
{ key: 'firmwareVersion', label: t('devices.paramFirmwareVersion'), type: 'Input', editable: isAdmin },
{ key: 'dealer', label: t('devices.dealer'), type: 'Input', editable: isAdmin },
];
// 下面全部只读,和后端Excel实体顺序对齐
{ key: 'deviceId', label: t('devices.deviceId'), type: 'Input', editable: isAdmin },
{
key: 'productModel', label: t('devices.productModel'), type: 'Select', editable: isAdmin,
options: [
{ value: 'ST', label: 'ST' },
{ value: 'PL', label: 'PL' },
{ value: 'PR', label: 'PR' },
{ value: 'AI', label: 'AI' },
],
},
{
key: 'deviceType', label: t('devices.deviceType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'M', label: t('devices.paramEnum.deviceTypeM') },
{ value: 'I', label: t('devices.paramEnum.deviceTypeI') },
{ value: 'U', label: t('devices.paramEnum.deviceTypeU') },
],
},
{
key: 'rtkMode', label: t('devices.rtkMode'), type: 'Select', editable: isAdmin,
options: [
{ value: 'N', label: t('devices.paramEnum.rtkModeN') },
{ value: 'B', label: t('devices.paramEnum.rtkModeB') },
],
},
{ key: 'cameraNum', label: t('devices.cameraNum'), type: 'InputNumber', editable: isAdmin, min: 0, max: 4, step: 1 },
{
key: 'powerType', label: t('devices.powerType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'E', label: t('devices.paramEnum.powerTypeE') },
{ value: 'H', label: t('devices.paramEnum.powerTypeH') },
],
},
{
key: 'wheelType', label: t('devices.wheelType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'W', label: t('devices.paramEnum.wheelTypeW') },
{ value: 'T', label: t('devices.paramEnum.wheelTypeT') },
],
},
{
key: 'engineType', label: t('devices.engineType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'H', label: t('devices.paramEnum.engineTypeH') },
{ value: 'L', label: t('devices.paramEnum.engineTypeL') },
{ value: 'Z', label: t('devices.paramEnum.engineTypeZ') },
],
},
{
key: 'hasHost', label: t('devices.hasHost'), type: 'Select', editable: isAdmin,
options: [
{ value: 0, label: t('devices.paramEnum.hasHostNo') },
{ value: 1, label: t('devices.paramEnum.hasHostYes') },
],
},
{
key: 'batteryType', label: t('devices.batteryType'), type: 'Select', editable: isAdmin,
options: [
{ value: '铅酸', label: t('devices.paramEnum.batteryLeadAcid') },
{ value: '锂电', label: t('devices.paramEnum.batteryLithium') },
],
},
{
key: 'walkingDriveType', label: t('devices.walkingDriveType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'FDK', label: 'FDK' },
{ value: 'SdBLdc', label: 'SdBLdc' },
{ value: 'SdFoc', label: 'SdFoc' },
],
},
{
key: 'cutterDriveType', label: t('devices.cutterDriveType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'XLD', label: 'XLD' },
{ value: 'LH', label: 'LH' },
],
},
{
key: 'rtkAntennasNum', label: t('devices.rtkAntennasNum'), type: 'Select', editable: isAdmin,
options: [
{ value: 1, label: t('devices.paramEnum.rtkAntennaSingle') },
{ value: 2, label: t('devices.paramEnum.rtkAntennaDual') },
],
},
{
key: 'handleType', label: t('devices.handleType'), type: 'Select', editable: isAdmin,
options: [
{ value: 'SBUS', label: t('devices.paramEnum.handleSbus') },
{ value: '自定义', label: t('devices.paramEnum.handleCustom') },
],
},
{ key: 'pcbVersion', label: t('devices.pcbVersion'), type: 'Input', editable: isAdmin },
{ key: 'dimensions', label: t('devices.dimensions'), type: 'Input', editable: isAdmin },
{ key: 'bladeWidth', label: t('devices.bladeWidth'), type: 'Input', editable: isAdmin },
{ key: 'tyreSize', label: t('devices.tyreSize'), type: 'Input', editable: isAdmin },
{ key: 'speedRatio', label: t('devices.speedRatio'), type: 'Input', editable: isAdmin },
{ key: 'cellularInfo', label: t('devices.cellularInfo'), type: 'Input', editable: isAdmin },
{ key: 'wifiName', label: t('devices.paramWifiName'), type: 'Input', editable: isAdmin },
{ key: 'bluetoothName', label: t('devices.bluetoothName'), type: 'Input', editable: isAdmin },
{ key: 'hardwareIdentifiers', label: t('devices.hardwareIdentifiers'), type: 'Input', editable: isAdmin },
{ key: 'imuModel', label: t('devices.imuModel'), type: 'Input', editable: isAdmin },
{ key: 'tempHumiditySensor', label: t('devices.tempHumiditySensor'), type: 'Input', editable: isAdmin },
{ key: 'barometerPresent', label: t('devices.barometerPresent'), type: 'Input', editable: isAdmin },
{ key: 'ultrasonicPresent', label: t('devices.ultrasonicPresent'), type: 'Input', editable: isAdmin },
{ key: 'dacPresent', label: t('devices.dacPresent'), type: 'Input', editable: isAdmin },
{ key: 'operator', label: t('devices.operator'), type: 'Input', editable: isAdmin },
{ key: 'inspectionNo', label: t('devices.inspectionNo'), type: 'Input', editable: isAdmin },
{ key: 'firmwareVersion', label: t('devices.paramFirmwareVersion'), type: 'Input', editable: isAdmin },
{ key: 'dealer', label: t('devices.dealer'), type: 'Input', editable: isAdmin },
];
// 区分增益滑块组 和 其余普通字段组
const gainFields = paramFieldConfigs.filter(f => f.type === 'GainSlider');

View File

@@ -469,6 +469,7 @@ export default function DeviceStatusPage() {
if (hasWsData || hasLocData) {
return true;
}
return false;
//// 2. 检查 runningStatus 里是否有有效经纬度或实时状态字段
//const runningLon = runningStatus?.longitude;
//const runningLat = runningStatus?.latitude;
@@ -989,6 +990,17 @@ export default function DeviceStatusPage() {
return resultObj; // ✅返回
}
};
const getDeviceType = (device) => {
if (!device) return '';
if (device?.deviceType === 'I') {
return t("devices.paramEnum.deviceTypeI");
} else if (device?.deviceType === 'M') {
return t("devices.paramEnum.deviceTypeM");
} else if (device?.deviceType === 'U') {
return t("devices.paramEnum.deviceTypeU");
} return t("devices.robot");
}
// 打开保存模态框
const handleOpenSaveModal = () => {
//if (!completeFlag) {
@@ -1059,7 +1071,7 @@ export default function DeviceStatusPage() {
].filter(group => group?.options?.length > 0)}
/>
<Tag color={isUAV ? 'cyan' : 'blue'}>
{isUAV ? t('constants.uav') : device?.deviceType ? device?.deviceType : t('devices.robot')}
{isUAV ? t('constants.uav') : getDeviceType(device)}
</Tag>
<Tag color={isDeviceOnline ? 'green' : 'default'}>
{isDeviceOnline ? t('devices.online') : t('devices.offline')}

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, useMemo, useCallback, useImperativeHandle } from 'react';
import { Card, Button, Space, Tag, Typography, Input, Tooltip, Select, message, Modal, App, Radio, Empty, Form } from 'antd';
import { Card, Button, Space, Tag, Typography, Input, Tooltip, Select, message, Modal, App, Radio, Empty, Form, Segmented } from 'antd';
import { SaveOutlined, DeleteOutlined, UndoOutlined, ClearOutlined } from '@ant-design/icons';
import { Navigation, MapPin, MapPinned, Route, X } from 'lucide-react';
import { getRobotRouteList, getSingleRouteList, getSiteDevice, deleteRoute, addRoute } from '../../api/device';
@@ -64,7 +64,7 @@ type MapEditorViewProps = {
};
export const MapEditorView = React.forwardRef(function MapEditorView(
{ selectedRobotSn, embedded = false, extraMapProps, activeType: propsActiveType, onActiveTypeChange },
{ selectedRobotSn, embedded = false, extraMapProps, activeType: propsActiveType, onActiveTypeChange, onSave },
ref
) {
const { t } = useTranslation();
@@ -91,6 +91,9 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
const [routePoints, setRoutePoints] = useState<MarkedPoint[]>([]);
const [obstaclePoints, setObstaclePoints] = useState<MarkedPoint[]>([]);
const [selectedRobotId, setSelectedRobotId] = useState<string | undefined>(selectedRobotSn);
const [polyPoints, setPolyPoints] = useState([]);
// 与页面顶部统一机器选择保持同步
useEffect(() => {
if (selectedRobotSn && selectedRobotSn !== selectedRobotId) {
@@ -98,6 +101,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedRobotSn]);
// 已选机器人的实时经纬度(来自 WebSocket)
const [robotRealtimePos, setRobotRealtimePos] = useState<{ lng?: number; lat?: number } | null>(null);
const [robotPosConnected, setRobotPosConnected] = useState(false);
@@ -152,6 +156,8 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
// 预设路径(供分区的 bindRoutes / defaultRoute 选择)
const [presetRoutes, setPresetRoutes] = useState<any[]>([]);
const [selectedRouteId, setSelectedRouteId] = useState<string | number | null>(null);
// 预设路径列表的模式筛选(all=全部 / coverage=覆盖 / single_point=单点 / predefined_path=预定义)
const [presetModeFilter, setPresetModeFilter] = useState<'all' | 'coverage' | 'single_point' | 'predefined_path'>('all');
const [selectedObstaclePointId, setSelectedObstaclePointId] = useState<string | null>(null);
@@ -162,6 +168,34 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
const isRouteGen = activeType === 'routeGen';
// 按模式筛选后的预设路径(coverage_area 视为覆盖模式)
const filteredPresetRoutes = useMemo(() => {
if (presetModeFilter === 'all') return presetRoutes;
return presetRoutes.filter((route: any) => {
const mode = route?.mode;
if (presetModeFilter === 'coverage') return mode === 'coverage' || mode === 'coverage_area';
return mode === presetModeFilter;
});
}, [presetRoutes, presetModeFilter]);
const selectRobot = useMemo(() => {
if (!Array.isArray(robots)) return [];
return robots.filter(
robot => robot.serialNumber == selectedRobotId
);
}, [selectedRobotId, robots]);
useEffect(() => {
if (extraMapProps.polyPoints?.length > 0) {
setPolyPoints(extraMapProps.polyPoints);
}
}, [extraMapProps.polyPoints]);
useEffect(() => {
if (previewPolyPoints?.length > 0) {
setPolyPoints(previewPolyPoints);
}
}, [previewPolyPoints]);
// 当前展示的点位列表
const currentList = useMemo(() => {
//returnPointConfigList
@@ -771,7 +805,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
}
},
});
};
};
// ========== 删除预设路径 ==========
@@ -787,7 +821,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
messageApi.success(t('common.deleteSuccess'));
// 刷新预设路径列表
const list = await getRobotRouteList({ siteId: stationId });
if (list?.code === 200 && list.data) setPresetRoutes(list.data);
if (list?.code === 200 && list.data) { setPresetRoutes(list.data); onSave() };
} else {
messageApi.error(res?.msg || t('common.saveFail'));
}
@@ -854,7 +888,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
jsonData: resultObj,
mode: workMode,
routeType: routeForm.routeType,
...(workMode === 'predefined_path' ? { ifStart } : {}),
//...(workMode === 'predefined_path' ? { ifStart } : {}),
};
formData.append('workRecord', new Blob([JSON.stringify(workRecord)], { type: 'application/json' }));
@@ -872,6 +906,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
getRobotRouteList({ siteId: stationId }).then((r: any) => {
if (r?.code === 200 && r.data) setPresetRoutes(r.data);
});
onSave();
} else {
messageApi.error(res?.msg || t('common.saveFail'));
}
@@ -992,7 +1027,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
<div style={{ flex: 1, minHeight: 360, position: 'relative' }}>
<CesiumMap
from="mapEditor"
devices={robots}
devices={selectRobot}
markedPoints={drawingPoints}
mapLayer={currentStation?.siteMapName}
onMarkPoint={handleMarkPoint}
@@ -1000,15 +1035,16 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
ref={mapRef}
points={(extraMapProps?.points?.length ? extraMapProps.points : points) as any}
activeType={activeType ?? undefined}
polyPoints={extraMapProps?.polyPoints?.length ? extraMapProps.polyPoints : previewPolyPoints}
routeMode={extraMapProps?.routeMode || previewRouteMode}
polyPoints={polyPoints} routeMode={extraMapProps?.routeMode || previewRouteMode}
overlayRefreshKey={overlayRefreshKey}
focusOverlay={(focusOverlay as any)?.id ? focusOverlay : (extraMapProps ? (extraMapProps as any)?.focusOverlay || focusOverlay : focusOverlay)}
realtimePosition={extraMapProps?.realtimePosition}
realtimeDevice={extraMapProps?.realtimeDevice}
realtimeType={extraMapProps?.realtimeType || 'robot'}
focusLocation={extraMapProps?.focusLocation}
ifClearNavLine={extraMapProps?.ifClearNavLine}
ifClearPlanLine={extraMapProps?.ifClearPlanLine}
drawRealTime={extraMapProps?.execute}
onObstacleRadiusChange={handleObstacleRadiusChange}
onObstacleRectResize={handleObstacleRectResize}
@@ -1225,7 +1261,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
]}
/>
</div>
{workMode === 'predefined_path' && (
{/*{workMode === 'predefined_path' && (
<div>
<label style={{ display: 'block', marginBottom: 6, fontSize: 13, color: '#333' }}>
{t('devices.startMachine')}
@@ -1240,7 +1276,7 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
]}
/>
</div>
)}
)}*/}
{/* 路径点列表 */}
<div style={{ borderTop: '1px solid #f0f0f0', paddingTop: 10 }}>
@@ -1458,25 +1494,40 @@ export const MapEditorView = React.forwardRef(function MapEditorView(
<Card
size="small"
title={
<span style={{ fontSize: 14, fontWeight: 600 }}>
{t('devices.presetRoutes')}
<span style={{ marginLeft: 8, fontSize: 12, color: '#999', fontWeight: 400 }}>
{t('devices.mapEditor.totalCountWithUnit', { count: presetRoutes.length })}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<span style={{ fontSize: 14, fontWeight: 600 }}>
{t('devices.presetRoutes')}
<span style={{ marginLeft: 8, fontSize: 12, color: '#999', fontWeight: 400 }}>
{t('devices.mapEditor.totalCountWithUnit', { count: filteredPresetRoutes.length })}
</span>
</span>
</span>
<Segmented
className="map-editor-segmented"
size="small"
value={presetModeFilter}
onChange={(v) => setPresetModeFilter(v as string)}
options={[
{ label: t('devices.mapEditor.filterAll'), value: 'all' },
{ label: t('devices.mapEditor.filterCoverage'), value: 'coverage' },
{ label: t('devices.mapEditor.filterSingle'), value: 'single_point' },
{ label: t('devices.mapEditor.filterPredefined'), value: 'predefined_path' },
]}
/>
</div>
}
variant="borderless"
styles={{ body: { padding: 12 } }}
style={{ flex: '0 0 auto' }}
>
{presetRoutes.length === 0 ? (
{filteredPresetRoutes.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('devices.mapEditor.noPresetRoutes')}
description={presetModeFilter === 'all' ? t('devices.mapEditor.noPresetRoutes') : t('devices.mapEditor.noMatchedPresetRoutes')}
/>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{presetRoutes.map((route: any) => (
{filteredPresetRoutes.map((route: any) => (
<div
key={route.id}
onClick={() => handlePreviewRoute(route)}

View File

@@ -21,6 +21,26 @@ import type { ViewStats } from './WayLineView';
const { Text, Title } = Typography;
const { RangePicker } = DatePicker;
// 预设路径按模式分组(覆盖 / 单点 / 预定义),用于任务下发时区分展示
const ROUTE_MODE_GROUPS = [
{ key: 'coverage', labelKey: 'devices.coverageMode', match: (m) => m === 'coverage' || m === 'coverage_area', color: '#13b879' },
{ key: 'single_point', labelKey: 'devices.singleMode', match: (m) => m === 'single_point', color: '#1677ff' },
{ key: 'predefined_path', labelKey: 'devices.predefinedPathMode', match: (m) => m === 'predefined_path', color: '#fa8c16' },
];
const groupRoutesByMode = (routes, t) => {
const groups = ROUTE_MODE_GROUPS.map((g) => ({ ...g, label: t(g.labelKey), items: [] }));
const others = { key: 'other', label: t('devices.otherMode'), color: '#8c8c8c', items: [] };
(routes || []).forEach((r) => {
const g = groups.find((x) => x.match(r.mode));
if (g) g.items.push(r);
else others.items.push(r);
});
return [...groups, others].filter((g) => g.items.length > 0);
};
export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsChange }: { selectedMachineSn?: string; embedded?: boolean; onStatsChange?: (stats: ViewStats) => void }) {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
@@ -208,7 +228,7 @@ export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsC
console.log('任务完成回调', data);
setExecute(false);
setShowVideo(false); // 任务完成关闭视频
utils.message.success(t('devices.fixed2TaskSuspended2Fixed2CompletedFixed2'));
utils.message.success(t('devices.fixed2TaskSuspended2Fixed2CompletedFixed2', { deviceId: data.deviceId }));
setClearNavLine(clearNavLine + 1);
setSelectedRobotForExecute(null);
@@ -1012,7 +1032,16 @@ export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsC
{
title: t('devices.device'),
dataIndex: 'deviceId',
width: 100,
render: (_, record) => {
return record.deviceAlias ? record.deviceAlias : record.deviceId;
}
},
{
title: t('systemSetting.s6'),
dataIndex: 'deviceId',
width: 180,
},
{
title: t('devices.workName'),
@@ -1123,7 +1152,16 @@ export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsC
{
title: t('devices.device'),
dataIndex: 'deviceId',
width: 100,
render: (_, record) => {
return record.deviceAlias ? record.deviceAlias : record.deviceId;
}
},
{
title: t('systemSetting.s6'),
dataIndex: 'deviceId',
width: 220,
},
{
title: t('devices.workName'),
@@ -1661,13 +1699,16 @@ export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsC
polyPoints,
routeMode,
realtimePosition,
realtimeDevice: targetRobot,
realtimeType: 'robot',
ifClearNavLine: clearNavLine,
ifClearPlanLine: clearPlanLine,
focusOverlay,
focusLocation,
detectReportInfo,
execute
}}
onSave={() => fetchRouteList()}
/>
{/* 机器人状态悬浮面板 - 绝对定位左上角 */}
@@ -2183,49 +2224,60 @@ export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsC
{(!routeList || routeList.length === 0) ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, maxHeight: 250, overflowY: "auto", }}>
{routeList.map(route => {
const checked = dispatchRouteIds.includes(route.id);
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, maxHeight: 280, overflowY: 'auto' }}>
{groupRoutesByMode(routeList, t).map(group => (
<div key={group.key}>
<div style={{ fontSize: 12, fontWeight: 600, color: group.color, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: group.color }} />
{group.label}
<span style={{ color: '#999', fontWeight: 400, fontSize: 11 }}>({group.items.length})</span>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{group.items.map(route => {
const checked = dispatchRouteIds.includes(route.id);
return (
<Tag.CheckableTag
key={route.id}
checked={checked}
style={{
display: 'inline-flex',
alignItems: 'center',
border: `1px solid ${checked ? '#1677ff' : '#dce5f2'}`,
background: checked ? '#f0f7ff' : '#fff',
color: '#344054',
borderRadius: 4,
padding: '3px 12px',
margin: 0,
fontSize: 12,
lineHeight: '20px',
whiteSpace: 'nowrap',
}}
onChange={nextChecked => {
// 按路线下发:单选
setDispatchRouteIds(nextChecked ? [route.id] : []);
return (
<Tag.CheckableTag
key={route.id}
checked={checked}
style={{
display: 'inline-flex',
alignItems: 'center',
border: `1px solid ${checked ? '#1677ff' : '#dce5f2'}`,
background: checked ? '#f0f7ff' : '#fff',
color: '#344054',
borderRadius: 4,
padding: '3px 12px',
margin: 0,
fontSize: 12,
lineHeight: '20px',
whiteSpace: 'nowrap',
}}
onChange={nextChecked => {
// 按路线下发:单选
setDispatchRouteIds(nextChecked ? [route.id] : []);
if (nextChecked) {
// 选择下发路径时,取消上方任务规划的选中
setEditorType(null);
flyToRoute(route.id);
}
}}
>
<span
style={{
color: '#344054',
display: 'inline-block',
}}
>
{route.workName || route.name}
</span>
</Tag.CheckableTag>
);
})}
if (nextChecked) {
// 选择下发路径时,取消上方任务规划的选中
setEditorType(null);
flyToRoute(route.id);
}
}}
>
<span
style={{
color: '#344054',
display: 'inline-block',
}}
>
{route.workName || route.name}
</span>
</Tag.CheckableTag>
);
})}
</div>
</div>
))}
</div>
)}
</div>
@@ -2412,109 +2464,120 @@ export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsC
</div>
{/* 路线选择卡片 */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gap: 8,
width: '100%',
}}
>
{regionAvailableRoutes.map((route, index) => {
const checked = dispatchRouteIds.includes(route.id);
const regionColors = [
'#1677ff',
'#13b879',
'#fa8c16',
'#a33cff',
];
const color = regionColors[index % regionColors.length];
return (
<Tag.CheckableTag
key={route.id}
checked={checked}
onChange={nextChecked => {
setDispatchRouteIds(prev =>
nextChecked
? [...prev, route.id]
: prev.filter(id => id !== route.id)
);
if (nextChecked) {
// 选择下发路径时,取消上方任务规划的选中
setEditorType(null);
flyToRoute(route.id);
}
}}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, width: '100%' }}>
{groupRoutesByMode(regionAvailableRoutes, t).map(group => (
<div key={group.key}>
<div style={{ fontSize: 12, fontWeight: 600, color: group.color, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: group.color }} />
{group.label}
<span style={{ color: '#999', fontWeight: 400, fontSize: 11 }}>({group.items.length})</span>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 5,
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gap: 8,
width: '100%',
minWidth: 0,
height: 40,
boxSizing: 'border-box',
margin: 0,
padding: '0 8px',
color: '#344054',
background: '#fff',
border: '1px solid #dce5f2',
borderRadius: 4,
fontSize: 12,
lineHeight: 'normal',
cursor: 'pointer',
overflow: 'hidden',
}}
>
{/* 自定义复选框 */}
<span
style={{
display: 'inline-flex',
flex: '0 0 12px',
width: 12,
height: 12,
boxSizing: 'border-box',
alignItems: 'center',
justifyContent: 'center',
border: `1px solid ${checked ? color : '#cbd5e1'}`,
borderRadius: 3,
background: checked ? color : '#fff',
color: '#fff',
fontSize: 10,
fontWeight: 700,
}}
>
{checked ? '✓' : ''}
</span>
{group.items.map((route, index) => {
const checked = dispatchRouteIds.includes(route.id);
{/* 路线定位图标 */}
<EnvironmentFilled
style={{
flex: '0 0 auto',
color,
fontSize: 13,
}}
/>
const regionColors = [
'#1677ff',
'#13b879',
'#fa8c16',
'#a33cff',
];
{/* 路线名称 */}
<span
style={{
flex: 1,
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: '#344054',
}}
>
{route.workName || route.name}
</span>
</Tag.CheckableTag>
);
})}
const color = regionColors[index % regionColors.length];
return (
<Tag.CheckableTag
key={route.id}
checked={checked}
onChange={nextChecked => {
setDispatchRouteIds(prev =>
nextChecked
? [...prev, route.id]
: prev.filter(id => id !== route.id)
);
if (nextChecked) {
// 选择下发路径时,取消上方任务规划的选中
setEditorType(null);
flyToRoute(route.id);
}
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 5,
width: '100%',
minWidth: 0,
height: 40,
boxSizing: 'border-box',
margin: 0,
padding: '0 8px',
color: '#344054',
background: '#fff',
border: '1px solid #dce5f2',
borderRadius: 4,
fontSize: 12,
lineHeight: 'normal',
cursor: 'pointer',
overflow: 'hidden',
}}
>
{/* 自定义复选框 */}
<span
style={{
display: 'inline-flex',
flex: '0 0 12px',
width: 12,
height: 12,
boxSizing: 'border-box',
alignItems: 'center',
justifyContent: 'center',
border: `1px solid ${checked ? color : '#cbd5e1'}`,
borderRadius: 3,
background: checked ? color : '#fff',
color: '#fff',
fontSize: 10,
fontWeight: 700,
}}
>
{checked ? '✓' : ''}
</span>
{/* 路线定位图标 */}
<EnvironmentFilled
style={{
flex: '0 0 auto',
color,
fontSize: 13,
}}
/>
{/* 路线名称 */}
<span
style={{
flex: 1,
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: '#344054',
}}
>
{route.workName || route.name}
</span>
</Tag.CheckableTag>
);
})}
</div>
</div>
))}
</div>
</div>
)}

View File

@@ -158,7 +158,7 @@ export default class DeviceController {
if (msg?.type == 'device_task_change') {
if (msg.status == "FINISH") {
console.log('task finish');
this.onIsFinished?.(true);
this.onIsFinished?.(msg);
}
if (msg.status == "EXECUTING" || msg.status == "CANCELLED" || msg.status == "FAILED") {
this.onIsExecuting?.(true);
@@ -174,7 +174,8 @@ export default class DeviceController {
console.log('route_post');
this.onRoutePost?.(msg);
}
if (msg?.type == 'detectionReport') {
if (msg?.type
== 'detectionReport') {
console.log('detectionReport');
this.onDetectionReport?.(msg);
}

View File

@@ -175,3 +175,27 @@ h4, .ant-typography h4 { font-size: clamp(1rem, 1.5vw, 1.25rem) !important; }
100% { transform: translateY(12px); opacity: 0.4; }
}
.map-editor-segmented {
background-color: #f4f6f9;
padding: 4px;
border-radius: 12px;
.ant-segmented-item {
border-radius: 8px;
transition: all 0.2s ease;
color: #595959;
font-size: 12px;
}
.ant-segmented-item-selected {
background: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.07);
color: #1677ff;
font-weight: 500;
}
.ant-segmented-item:hover:not(.ant-segmented-item-selected) {
background-color: rgba(22, 119, 255, 0.05);
color: #1677ff;
}
}

View File

@@ -787,6 +787,7 @@
optionalEmpty: 'Optional Empty',
otherWebControlling: 'Other Web Controlling',
platformRequestControl: "{{platform}} platformRequestControl",
otherMode: 'Other',
coverageMode: 'coverageMode',
singleMode: 'singleMode',
predefinedPathMode: 'predefinedPathMode',
@@ -1007,10 +1008,17 @@
defaultRoutePointName: 'Route Point-{{n}}',
defaultObstaclePointName: 'Obstacle-{{n}}',
totalCountWithUnit: '{{count}} total',
// preset route mode filter
filterAll: 'All',
filterCoverage: 'Coverage',
filterSingle: 'Single',
filterPredefined: 'Predefined',
noPresetRoutes: 'No preset routes',
noMatchedPresetRoutes: 'No matching preset routes',
obstaclePointLabel: 'Obstacle',
saveRegion: 'Save Region',
editRegion: 'Edit Region',
regionName: 'Region Name',
@@ -1022,7 +1030,8 @@
selectRoutePlaceholder: 'Select default route',
bindRoutes: 'Bind Routes',
selectRoutesPlaceholder: 'Select routes to bind',
currentScopePoints: 'Current region scope points',}, turnSpeed: 'Turn Speed',
currentScopePoints: 'Current region scope points',
}, turnSpeed: 'Turn Speed',
realtimeVideoMonitor: 'Realtime Video Monitor',
rechargeBtn: 'Recharge Btn',
rechargeCommandSent: 'Recharge Command Sent',
@@ -1569,7 +1578,7 @@
videoFixed2: ' Airport video',
fixed2TaskSuspended2Fixed2CompletedFixed2: 'Task Completed ',
fixed2TaskSuspended2Fixed2CompletedFixed2: 'Task Completed device:{{deviceId}} ',
deletedSuccessfullyFixed2: '航线 Deleted successfully Fixed2',

View File

@@ -551,6 +551,7 @@ const zh = {
countUnit: '数量单位',
coverClosed: '已关闭',
coverOpen: '打开',
otherMode: '其他',
coverageMode: '覆盖模式',
singleMode: '单点模式',
predefinedPathMode: '预定义模式',
@@ -1420,7 +1421,7 @@ const zh = {
taskId: "任务ID",
routeId: "路线ID",
fixed2TaskSuspended2Fixed2CompletedFixed2: '任务已完成',
fixed2TaskSuspended2Fixed2CompletedFixed2: '任务已完成 ,设备:{{deviceId}}',
deletedSuccessfullyFixed2: '航线删除成功',
@@ -1544,7 +1545,7 @@ const zh = {
handleCustom: '自定义',
},
taskTypeRadio: {
scheduled: 'd定时任务',
scheduled: '定时任务',
scheduledDescription: '设置开始时间和重复周期',
immediate: '立即任务',
immediateDescription: '下发后机器人立即执行',
@@ -1664,6 +1665,12 @@ const zh = {
defaultRoutePointName: '路径点-{{n}}',
defaultObstaclePointName: '障碍物点-{{n}}',
totalCountWithUnit: '共 {{count}} 条',
// 预设路径模式筛选
filterAll: '全部',
filterCoverage: '覆盖',
filterSingle: '单点',
filterPredefined: '预定义',
noMatchedPresetRoutes: '暂无匹配的预设路径',
obstaclePointLabel: '障碍点',

View File

@@ -2,7 +2,8 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
import {
Row, Col, Card, Typography, Space, Table, Tag, Progress, Badge,
List, Avatar, Button, Statistic, Timeline, Empty, message,
Select
Select,
Tooltip
} from 'antd';
import {
LayoutGrid, PlayCircle, CheckCircle, AlertCircle,
@@ -111,6 +112,13 @@ const TaskMonitorPage: React.FC = () => {
return undefined;
};
// 判断设备是否在线:feStatus 优先(0 离线 / 1 工作中 / 2 空闲),status 兜底(1 工作中 / 2 空闲 / 3 离线)
const isDeviceOnline = (device: any): boolean => {
const rt = pickRealtimeRecord(realtimeMap, device) || {};
const status = String(rt.feStatus ?? rt.status ?? device?.feStatus ?? device?.status ?? '');
return status === '1' || status === '2';
};
// 供 WebSocket 回调始终调用最新的 fetchData
const fetchDataRef = useRef<() => any>(() => { });
@@ -245,7 +253,6 @@ const TaskMonitorPage: React.FC = () => {
console.log(realtimeMap, "realtimeMap")
// 告警车辆数量以实时数据里的 hasAlarm 为准
const alarmDevices = devices.filter(d => !!pickRealtimeRecord(realtimeMap, d)?.hasAlarm);
console.log(alarmDevices, "alarmDevices")
const alarm = alarmDevices.length;
// 与 DeviceStatusPage 一致:有实时数据即在线,兜底接口 onlineStatus
const alarmOnline = alarmDevices.filter(d => pickRealtimeRecord(realtimeMap, d) || d?.onlineStatus == 1).length;
@@ -486,7 +493,9 @@ const TaskMonitorPage: React.FC = () => {
</div>
<div className="vehicle-map-popup__body">
<div className="vehicle-map-popup__row"><span>当前状态</span><strong className={selectedRealtime.status === '1' ? 'is-running' : ''}>{selectedRealtime.status === '1' ? '进行中' : selectedRealtime.status === '2' ? '待命' : '离线'}</strong></div>
<div className="vehicle-map-popup__row"><span>剩余电量</span><strong className={(selectedBattery ?? 85) < 20 ? 'is-low-battery' : ''}>{selectedBattery ?? 85}%</strong></div>
{isDeviceOnline(selectedDevice) && (
<div className="vehicle-map-popup__row"><span>剩余电量</span><strong className={(selectedBattery ?? 85) < 20 ? 'is-low-battery' : ''}>{selectedBattery ?? 85}%</strong></div>
)}
<div className="vehicle-map-popup__row"><span>当前任务</span><strong>{selectedRealtime.workName || "无"}</strong></div>
{/*<div className="vehicle-map-popup__row"><span>设备类型</span><strong>{selectedDevice.deviceTypeName || selectedDevice.deviceType || '车辆'}</strong></div>*/}
<div className="vehicle-map-popup__row"><span>任务进度</span><strong>{selectedRealtime.progress ?? 0}%</strong></div>
@@ -523,6 +532,7 @@ const TaskMonitorPage: React.FC = () => {
renderItem={(item: any) => {
const rt = realtimeMap[item.serialNumber] || {};
const battery = getDeviceBattery(item);
const online = isDeviceOnline(item);
return (
<List.Item
@@ -550,17 +560,41 @@ const TaskMonitorPage: React.FC = () => {
>
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
<Avatar shape="square" size={44} icon={item.deviceType === 'UAV' ? <RocketOutlined /> : <RobotOutlined />} style={{ background: '#f0f7ff', color: '#1890ff' }} />
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontWeight: 600, fontSize: 14, color: '#262626' }}>{item.deviceAlias || item.deviceName}</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
minWidth: 0,
}}
>
<Tooltip title={item.deviceAlias || item.deviceName}>
<span
style={{
fontWeight: 600,
fontSize: 14,
color: '#262626',
display: 'inline-block',
maxWidth: '60%',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
verticalAlign: 'middle',
}}
>
{item.deviceAlias || item.deviceName}
</span>
</Tooltip>
<Tag
color={rt.status === '1' ? 'blue' : rt.status === '2' ? 'green' : 'default'}
style={{ margin: 0, borderRadius: 4 }}
style={{ margin: 0, borderRadius: 4, flexShrink: 0 }}
>
{rt.status == '1' ? '进行中' : rt.status == '2' ? '待命' : '离线'}
</Tag>
</div>
<div
style={{
display: 'flex',
@@ -572,25 +606,48 @@ const TaskMonitorPage: React.FC = () => {
minWidth: 0,
}}
>
<span>任务: {rt.workName || '无'}</span>
<span
style={{
color: (battery ?? 85) < 20 ? '#ff4d4f' : '#faad14',
display: 'inline-flex',
alignItems: 'center',
gap: 2,
whiteSpace: 'nowrap',
flexShrink: 0,
lineHeight: 1,
}}
>
<Zap size={10} style={{ flexShrink: 0 }} />
{battery ?? 85}%
</span>
{/* 任务名称增加溢出省略 */}
<Tooltip title={rt.workName || '无'}>
<span
style={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
flex: 1,
minWidth: 0,
}}
>
任务: {rt.workName || '无'}
</span>
</Tooltip>
{online && (
<span
style={{
color: (battery ?? 85) < 20 ? '#ff4d4f' : '#faad14',
display: 'inline-flex',
alignItems: 'center',
gap: 2,
whiteSpace: 'nowrap',
flexShrink: 0,
lineHeight: 1,
}}
>
<Zap size={10} style={{ flexShrink: 0 }} />
{battery ?? 85}%
</span>
)}
</div>
<Progress percent={rt.progress || 0} size="small" showInfo={false} strokeColor="#52c41a" style={{ marginTop: 6 }} />
{/* Progress 关键:minWidth:0,禁止撑出去 */}
<Progress
percent={rt.progress || 0}
size="small"
showInfo={false}
strokeColor="#52c41a"
style={{ marginTop: 6, width: '100%', minWidth: 0 }}
/>
</div>
</div>
</List.Item>
);
@@ -677,9 +734,10 @@ const TaskMonitorPage: React.FC = () => {
animation: vehiclePopupIn 180ms ease-out;
}
.vehicle-map-popup__header { display:flex; align-items:center; justify-content:space-between; gap:8px; padding: 12px 14px; border-bottom:1px solid #e6f4ff; background:linear-gradient(100deg,#f0f9ff,#fff); }
.vehicle-map-popup__identity { display:flex; align-items:center; gap:10px; min-width:0; }
.vehicle-map-popup__identity { display:flex; align-items:center; gap:10px; min-width:0; flex:1; }
.vehicle-map-popup__identity > div { min-width:0; flex:1; }
.vehicle-map-popup__title { color:#087bd0; font-size:16px; font-weight:700; overflow-wrap:anywhere; }
.vehicle-map-popup__serial { color:#8a9daf; font-size:11px; margin-top:2px; }
.vehicle-map-popup__serial { color:#8a9daf; font-size:11px; margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
.vehicle-map-popup__body { padding: 8px 14px 12px; }
.vehicle-map-popup__row { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:8px 0; border-bottom:1px dashed #e6f4ff; font-size:13px; }
.vehicle-map-popup__row:last-child { border-bottom:0; }