优化 map中实时车悬浮的提醒框子,优化已完成任务提醒哪个设备 优化页面 设备类型显示 和 在线状态显示 预设路线分组显示 优化路径规划中的问题
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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: '障碍点',
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user