- {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 = () => {