diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 5e877e6..2414f43 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -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,
${t('systemSetting.status')}: ${status.text} -
`; +`; } else { tooltip.innerHTML = `
📍 ${t('systemSetting.longitude')}:${lonStr}
@@ -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"; diff --git a/src/components/devices/DeviceParamModal.tsx b/src/components/devices/DeviceParamModal.tsx index c413674..97e209f 100644 --- a/src/components/devices/DeviceParamModal.tsx +++ b/src/components/devices/DeviceParamModal.tsx @@ -115,14 +115,9 @@ const DeviceParamModal: React.FC = ({ 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 = ({ 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'); diff --git a/src/components/devices/DeviceStatusPage.tsx b/src/components/devices/DeviceStatusPage.tsx index eea692d..0573971 100644 --- a/src/components/devices/DeviceStatusPage.tsx +++ b/src/components/devices/DeviceStatusPage.tsx @@ -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)} /> - {isUAV ? t('constants.uav') : device?.deviceType ? device?.deviceType : t('devices.robot')} + {isUAV ? t('constants.uav') : getDeviceType(device)} {isDeviceOnline ? t('devices.online') : t('devices.offline')} diff --git a/src/components/devices/MapEditorView.tsx b/src/components/devices/MapEditorView.tsx index eba6f01..0b3569d 100644 --- a/src/components/devices/MapEditorView.tsx +++ b/src/components/devices/MapEditorView.tsx @@ -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([]); const [obstaclePoints, setObstaclePoints] = useState([]); const [selectedRobotId, setSelectedRobotId] = useState(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([]); const [selectedRouteId, setSelectedRouteId] = useState(null); + // 预设路径列表的模式筛选(all=全部 / coverage=覆盖 / single_point=单点 / predefined_path=预定义) + const [presetModeFilter, setPresetModeFilter] = useState<'all' | 'coverage' | 'single_point' | 'predefined_path'>('all'); const [selectedObstaclePointId, setSelectedObstaclePointId] = useState(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(
- {workMode === 'predefined_path' && ( + {/*{workMode === 'predefined_path' && (
- )} + )}*/} {/* 路径点列表 */}
@@ -1458,25 +1494,40 @@ export const MapEditorView = React.forwardRef(function MapEditorView( - {t('devices.presetRoutes')} - - {t('devices.mapEditor.totalCountWithUnit', { count: presetRoutes.length })} +
+ + {t('devices.presetRoutes')} + + {t('devices.mapEditor.totalCountWithUnit', { count: filteredPresetRoutes.length })} + - + 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' }, + ]} + /> + +
} variant="borderless" styles={{ body: { padding: 12 } }} style={{ flex: '0 0 auto' }} > - {presetRoutes.length === 0 ? ( + {filteredPresetRoutes.length === 0 ? ( ) : (
- {presetRoutes.map((route: any) => ( + {filteredPresetRoutes.map((route: any) => (
handlePreviewRoute(route)} diff --git a/src/components/devices/TaskPlanningView.tsx b/src/components/devices/TaskPlanningView.tsx index 71e1637..0221b9f 100644 --- a/src/components/devices/TaskPlanningView.tsx +++ b/src/components/devices/TaskPlanningView.tsx @@ -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) ? ( ) : ( -
- {routeList.map(route => { - const checked = dispatchRouteIds.includes(route.id); +
+ {groupRoutesByMode(routeList, t).map(group => ( +
+
+ + {group.label} + ({group.items.length}) +
+
+ {group.items.map(route => { + const checked = dispatchRouteIds.includes(route.id); - return ( - { - // 按路线下发:单选 - setDispatchRouteIds(nextChecked ? [route.id] : []); + return ( + { + // 按路线下发:单选 + setDispatchRouteIds(nextChecked ? [route.id] : []); - if (nextChecked) { - // 选择下发路径时,取消上方任务规划的选中 - setEditorType(null); - flyToRoute(route.id); - } - }} - > - - {route.workName || route.name} - - - ); - })} + if (nextChecked) { + // 选择下发路径时,取消上方任务规划的选中 + setEditorType(null); + flyToRoute(route.id); + } + }} + > + + {route.workName || route.name} + + + ); + })} +
+
+ ))}
)}
@@ -2412,109 +2464,120 @@ export function TaskPlanningView({ selectedMachineSn, embedded = false, onStatsC
{/* 路线选择卡片 */} -
- {regionAvailableRoutes.map((route, index) => { - const checked = dispatchRouteIds.includes(route.id); - - const regionColors = [ - '#1677ff', - '#13b879', - '#fa8c16', - '#a33cff', - ]; - - const color = regionColors[index % regionColors.length]; - - return ( - { - setDispatchRouteIds(prev => - nextChecked - ? [...prev, route.id] - : prev.filter(id => id !== route.id) - ); - - if (nextChecked) { - // 选择下发路径时,取消上方任务规划的选中 - setEditorType(null); - flyToRoute(route.id); - } - }} +
+ {groupRoutesByMode(regionAvailableRoutes, t).map(group => ( +
+
+ + {group.label} + ({group.items.length}) +
+
- {/* 自定义复选框 */} - - {checked ? '✓' : ''} - + {group.items.map((route, index) => { + const checked = dispatchRouteIds.includes(route.id); - {/* 路线定位图标 */} - + const regionColors = [ + '#1677ff', + '#13b879', + '#fa8c16', + '#a33cff', + ]; - {/* 路线名称 */} - - {route.workName || route.name} - - - ); - })} + const color = regionColors[index % regionColors.length]; + + return ( + { + 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', + }} + > + {/* 自定义复选框 */} + + {checked ? '✓' : ''} + + + {/* 路线定位图标 */} + + + {/* 路线名称 */} + + {route.workName || route.name} + + + ); + })} +
+
+ ))}
)} diff --git a/src/components/devices/deviceController.js b/src/components/devices/deviceController.js index 8bf656b..4fe9e3d 100644 --- a/src/components/devices/deviceController.js +++ b/src/components/devices/deviceController.js @@ -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); } diff --git a/src/index.css b/src/index.css index e941a8a..b8f2051 100644 --- a/src/index.css +++ b/src/index.css @@ -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; + } +} + + diff --git a/src/locales/en/index.ts b/src/locales/en/index.ts index 7350b78..45ab018 100644 --- a/src/locales/en/index.ts +++ b/src/locales/en/index.ts @@ -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', diff --git a/src/locales/zh/index.ts b/src/locales/zh/index.ts index b1550f8..67cf632 100644 --- a/src/locales/zh/index.ts +++ b/src/locales/zh/index.ts @@ -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: '障碍点', diff --git a/src/pages/TaskMonitorPage.tsx b/src/pages/TaskMonitorPage.tsx index 45a6db2..7879c2f 100644 --- a/src/pages/TaskMonitorPage.tsx +++ b/src/pages/TaskMonitorPage.tsx @@ -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 = () => {
当前状态{selectedRealtime.status === '1' ? '进行中' : selectedRealtime.status === '2' ? '待命' : '离线'}
-
剩余电量{selectedBattery ?? 85}%
+ {isDeviceOnline(selectedDevice) && ( +
剩余电量{selectedBattery ?? 85}%
+ )}
当前任务{selectedRealtime.workName || "无"}
{/*
设备类型{selectedDevice.deviceTypeName || selectedDevice.deviceType || '车辆'}
*/}
任务进度{selectedRealtime.progress ?? 0}%
@@ -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 ( { >
: } style={{ background: '#f0f7ff', color: '#1890ff' }} /> -
-
- {item.deviceAlias || item.deviceName} +
+
+ + + {item.deviceAlias || item.deviceName} + + {rt.status == '1' ? '进行中' : rt.status == '2' ? '待命' : '离线'} -
+
{ minWidth: 0, }} > - 任务: {rt.workName || '无'} - - - {battery ?? 85}% - + {/* 任务名称增加溢出省略 */} + + + 任务: {rt.workName || '无'} + + + {online && ( + + + {battery ?? 85}% + + )}
- + {/* Progress 关键:minWidth:0,禁止撑出去 */} +
+
); @@ -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; }