Files
web-Iot/src/components/devices/RobotTaskPage.tsx
2026-09-08 17:17:59 +08:00

2204 lines
67 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef, useMemo } from 'react';
import {
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker,
Modal, InputNumber,
TimePicker,
Popconfirm,
App,
Radio
} from 'antd';
import {
PlusOutlined, ImportOutlined, NodeIndexOutlined,
SaveOutlined, PlayCircleOutlined, EyeOutlined,
DeleteOutlined,
CloseOutlined,
ReloadOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons';
import {
Car, Navigation, Wind, Thermometer, Droplets, Satellite,
LayoutGrid, Clock, CheckCircle2, AlertCircle, Zap, Waves,
Calendar, User, TrendingUp, Maximize2, Play, Square, X, MapPin, RotateCw
} from 'lucide-react';
import {
getRobotRouteList,
getSingleRouteList,
saveDeviceTask, getRobotTaskList,
deleteDeviceTask,
getSiteDevice,
excuetRoute,
cancelRoute,
pauseRoute,
recoverRoute,
getDeviceTastPool,
getSiteDeviceTaskHistory,
getRouteByTask,
deleteRoute,
getTaskPath,
} from '../../api/device';
import dayjs from 'dayjs';
import DraggableBox from '../Draggable';
import utils, { getLocationQuality } from '../../lib/utils';
import CesiumMap from '../Map';
import { fix2, getVal } from '../../lib/utils';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import envConfig from '../../../env';
import DeviceController from './DeviceController';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import { MowerAllView } from './mowerAllView.jsx';
import { useWebRTCPlayer } from '../useWebRTCStream.js';
import { useTranslation } from 'react-i18next';
const { Text, Title } = Typography;
const { RangePicker } = DatePicker;
export default function RobotTaskPage() {
const { t } = useTranslation();
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
const [excuteStatus, setExcuteStatus] = useState("stop"); //"stop" "running" "pause"
// 机器人实时位置
const [robotRealtimePosition, setRobotRealtimePosition] = useState(null);
const [execute, setExecute] = useState(false); // 是否执行任务
const [taskList, setTaskList] = useState([]);
const [arrivedPoint, setArrivedPoint] = useState(null);//完成点
const { modal } = App.useApp();
const [polyPoints, setPolyPoints] = useState([]);
// 设备数据
const [devices, setDevices] = useState([]);
const [deleteLoading, setDeleteLoading] = useState(false);
const [focusLocation, setFocusLocation] = useState(null);
const [points, setPoints] = useState([]);
const [routeMode, setRouteMode] = useState('');
const [clearNavLine, setClearNavLine] = useState(0);
const [clearPlanLine, setClearPlanLine] = useState(0);
const [routeSearch, setRouteSearch] = useState('');
const [routeList, setRouteList] = useState([]);
// 执行任务弹窗
const [executeModalVisible, setExecuteModalVisible] = useState(false);
const [selectedRouteForExecute, setSelectedRouteForExecute] = useState(null);
const [selectedRobotForExecute, setSelectedRobotForExecute] = useState(null);
const [runningPoolTask, setRunningPoolTask] = useState(null);//正在执行的任务
// 当前执行任务的机器人
const [executingRobot, setExecutingRobot] = useState(null);
const [showVideo, setShowVideo] = useState(false); // 控制视频组件显示
// 任务池和历史任务数据
const [taskPoolList, setTaskPoolList] = useState([]);
const [taskHistoryList, setTaskHistoryList] = useState([]);
// 当前激活的Tab
const [activeTab, setActiveTab] = useState('pool'); // 'task' for 任务列表, 'pool' for 任务池, 'history' for 历史任务
// 任务池和历史任务筛选表单
const [poolFilterForm, setPoolFilterForm] = useState({
taskStatus: null,
robot: null,
planId: null,
});
const { video: robotVideo, error, ready } = useWebRTCPlayer(
executingRobot?.serialNumber ? executingRobot?.serialNumber : '',
`webrtc://1.95.137.212/live/livestream/${executingRobot?.serialNumber}`,
);
const [historyFilterForm, setHistoryFilterForm] = useState({
taskStatus: null,
robot: null,
dateRange: null,
});
// 取消任务模式 normal普通取消 force强制取消
const [cancelMode, setCancelMode] = useState<'normal' | 'force'>('normal');
const fontSmall = 'clamp(10px, 0.7vw, 11px)';
const fontNormal = 'clamp(12px, 0.75vw, 13px)';
const fontTitle = 'clamp(14px, 0.85vw, 16px)';
const fontLarge = 'clamp(18px, 1.1vw, 24px)';
const cardStyle = { borderRadius: 12, border: 'none', boxShadow: '0 2px 12px rgba(0,0,0,0.04)' };
// 任务列表 独立筛选状态
// 统一用 form 管理筛选条件
const [filterForm, setFilterForm] = useState({
dateRange: null, // 时间范围
taskStatus: null, // 任务状态
robot: null // 机器人设备
});
const [messageApi, contextHolder] = message.useMessage();
const controllerRef = useRef<DeviceController | null>(null);
const isFirstConnect = useRef(true);
const fixStatusMap: Record<string, string> = {
'no_fix': t('devices.noFix'),
'single': t('devices.single'),
'dgnss': t('devices.dgnss'),
'ins': t('devices.ins'),
'rtk_fixed': t('devices.rtkFixed'),
'rtk_float': t('devices.rtkFloat'),
'single_no_heading': t('devices.singleNoHeading'),
'dgnss_no_heading': t('devices.dgnssNoHeading'),
'rtk_fixed_no_heading': t('devices.rtkFixedNoHeading'),
'rtk_float_no_heading': t('devices.rtkFloatNoHeading'),
}
const handleFinish = (data) => {
setExecute(false);
setShowVideo(false); // 任务完成关闭视频
utils.message.success(t('devices.fixed2TaskSuspended2Fixed2CompletedFixed2'));
setClearNavLine(clearNavLine + 1);
setSelectedRobotForExecute(null);
setSelectedRouteForExecute(null);
fetchTaskList();
fetchTaskHistoryList();
fetchTaskPoolList();
}
const handleFinishEdPoint = (point) => {
setArrivedPoint(point);
};
// 根据任务ID获取路径并显示在地图上
const loadTaskPath = async (taskId) => {
//return;
if (!taskId) return;
try {
const res = await getRouteByTask({ taskId });
if (res?.code === 200 && res?.data?.jsonData?.path) {
let pathPoints = [];
try {
const data = res.data;
pathPoints = data.jsonData.path || [];
setRouteMode(data?.mode || 'coverage'); // 设置路线模式
} catch (err) {
console.error('解析任务路径数据失败:', err);
}
setPolyPoints(pathPoints);
}
} catch (err) {
console.error('获取任务路径失败:', err);
}
};
const handleDeleteRoute = async (route) => {
console.log('删除航线:', route);
// 防重复点击拦截
if (deleteLoading) return;
modal.confirm({
title: t('systemSetting.fixed2ConfirmFixed2DeleteFixed2'),
content: `确定要删除航线「${route.workName}」吗?删除后数据无法恢复!`,
icon: <ExclamationCircleOutlined style={{ color: '#FF7D00' }} />,
okText: t('devices.fixed2OkFixed2DeleteFixed2'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true, loading: deleteLoading },
onOk: async () => {
setDeleteLoading(true);
try {
const res = await deleteRoute(route.workName);
if (res.code === 200) {
message.success(t('devices.deletedSuccessfullyFixed2'));
fetchRouteList();
} else {
utils.message.error(res.msg || '删除失败,请稍后重试');
}
} catch (err) {
console.error('删除航线接口报错:', err);
utils.message.error(t('devices.fixed2DeleteFailedAbnormalFixed2'));
} finally {
setDeleteLoading(false);
}
},
});
};
// 任务池项点击事件
const handlePoolTaskClick = (record) => {
setRunningPoolTask(record);
loadTaskPath(record.id);
// 查找对应的设备
const targetDevice = devices?.find(d => d.serialNumber === record.deviceId);
if (!targetDevice) {
console.warn('未找到任务对应的设备');
return;
}
// 判断是否是新设备(避免重复连接)
const isSameDevice = executingRobot?.serialNumber === targetDevice.serialNumber;
if (isSameDevice) {
console.log('✅ 任务使用相同设备,无需重新连接');
return;
}
// 切换设备连接
console.log('🔄 点击任务切换设备连接', targetDevice);
setExecute(true);
setExecutingRobot(targetDevice);
setClearNavLine(v => v + 1); // 清空地图轨迹
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(targetDevice); } else { controllerRef.current?.switchDevice(targetDevice); }
setShowVideo(true);
};
useEffect(() => {
const controller = new DeviceController(userInfo, messageApi);
controller.onLocationInfoUpdate = setRobotRealtimePosition;
controller.onIsFinished = handleFinish;
controller.handleArrivedPoint = handleFinishEdPoint;
// 处理路线点回调:taskId必须和当前选中任务一致
controller.onRoutePost = (msg) => {
console.log('收到 route_post 消息:', msg);
const { taskId, data } = msg;
// 1. 先在任务池中查找该taskId对应的任务
const matchedTask = taskPoolList.find(task => String(task.id) === String(taskId));
// 2. 两种情况处理:
// a) 当前没有选中任务,且匹配到了任务池中的任务 → 自动选中该任务
// b) 当前已选中任务,且选中任务的id === 传来的taskId → 更新数据
const isSameAsSelected = runningPoolTask && String(runningPoolTask.id) === String(taskId);
const shouldSelectNewTask = !runningPoolTask && matchedTask;
if (isSameAsSelected || shouldSelectNewTask) {
console.log('taskId匹配成功,处理路线数据', {
taskId,
runningPoolTaskId: runningPoolTask?.id,
matchedTask
});
// 如果是匹配到新任务就设置选中状态
if (shouldSelectNewTask) {
setRunningPoolTask(matchedTask);
}
// 先直接用推送的data设置点(实时性)
if (data) {
setPoints(data);
}
} else {
console.log('taskId不匹配,忽略本次推送', {
receivedTaskId: taskId,
currentSelectedTaskId: runningPoolTask?.id,
matchedTaskFound: !!matchedTask
});
}
};
controller.init();
controllerRef.current = controller;
return () => {
if (controllerRef.current) {
controllerRef.current.destroy?.();
}
controllerRef.current = null;
};
}, []);
// 监听任务列表和设备列表变化,自动连接正在执行中的机器人(仅在没有选择任务时)
useEffect(() => {
if (!taskPoolList || taskPoolList.length == 0) {
setClearNavLine(v => v + 1); // 清空地图轨迹
setClearPlanLine(v => v + 1);
setRunningPoolTask(null);
return;
}
if (!devices) return;
// 如果已经有选择的任务(runningPoolTask),则不自动连接,让用户手动选择
if (runningPoolTask) return;
// 查找正在执行中的任务
const runningTask = taskPoolList.find(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus));
if (!runningTask) return;
//fetchRouteDetail(runningTask?.id);
// 找到对应的设备
const targetDevice = devices.find(d => d.serialNumber === runningTask.deviceId);
if (!targetDevice) return;
// 判断是否是新设备(避免重复连接)
const isSameDevice = executingRobot?.serialNumber === targetDevice.serialNumber;
if (isSameDevice) return;
// 切换设备
console.log('🔄 检测到正在执行任务的新设备,开始连接', targetDevice);
setRunningPoolTask(runningTask); // 设置当前任务
setExecute(true);
setExecutingRobot(targetDevice);
setClearNavLine(v => v + 1); // 清空地图轨迹
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(targetDevice); } else { controllerRef.current?.switchDevice(targetDevice); }
setShowVideo(true);
}, [devices, taskPoolList, executingRobot, runningPoolTask]);
const realtimePosition = useMemo(() => {
const data = {
lon: robotRealtimePosition?.longitude || 0,
lat: robotRealtimePosition?.latitude || 0,
heading: robotRealtimePosition?.yaw || 0,
}
return data;
}, [robotRealtimePosition])
//useEffect(() => {
// if (robotRealtimePosition && execute) {
// let warningMsg = '';
// if (robotRealtimePosition?.controlMode == "1") {
// warningMsg = `请切换到远程模式 `;
// } else if (robotRealtimePosition?.qual != "4") {
// warningMsg = '定位未固定';
// } else if (robotRealtimePosition?.headingStatus != "1") {
// warningMsg = '请先初始化航向角';
// }
// if (warningMsg) {
// utils.message.destroy();
// utils.message.warning(warningMsg);
// }
// }
//}, [robotRealtimePosition, execute])
// 获取数据
const fetchAllData = async () => {
if (!stationId) return;
try {
const deviceRes = await getSiteDevice({ siteId: stationId, pageSize: 9999, pageNum: 1, orgId: currentStation?.orgId });
const devData = deviceRes?.code === 200 ? (deviceRes.rows || []) : [];
setDevices(devData);
} catch (err) {
console.error('数据请求失败', err);
}
};
useEffect(() => {
fetchAllData();
}, [userInfo, stationId]);
// 新建任务弹窗状态
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
const [selectedPathWork, setSelectedPathWork] = useState(null);
// 获取任务列表数据
const fetchTaskList = async () => {
try {
const params = {
siteId: String(stationId), pageNum: 1,
pageSize: 99999999
};
// 时间范围
if (filterForm.dateRange && filterForm.dateRange.length === 2) {
params.startTime = filterForm.dateRange[0].format('YYYY-MM-DD');
params.endTime = filterForm.dateRange[1].format('YYYY-MM-DD');
}
// 设备
if (filterForm.robot?.serialNumber) {
params.deviceId = filterForm.robot.serialNumber;
}
// 状态
if (filterForm.taskStatus !== null && filterForm.taskStatus !== undefined) {
params.taskStaus = filterForm.taskStatus;
}
const res = await getRobotTaskList(params);
if (res.code == 200) {
setTaskList(res.data);
}
} catch (err) {
message.error(t('devices.taskListRefreshFail'));
}
};
useEffect(() => {
//console.log('获取任务列表', selectedPathWork?.id);
if (!selectedRobotForExecute?.id || !execute) {
setPoints([]);
return;
}
}, [selectedRobotForExecute?.id]);
// 监听任务池列表变化,自动加载第一个任务的路径
useEffect(() => {
if (taskPoolList && taskPoolList.length > 0) {
setRunningPoolTask(taskPoolList[0]);
}
}, [taskPoolList]);
const getRouteOut = async () => {
try {
const res = await getSingleRouteList({ id: selectedPathWork?.id });
// coverage(区域覆盖)绘制为多边形,其余为折线/单点
const mode = res?.data?.mode;
setRouteMode(mode);
if (res?.code === 200 && res?.data?.jsonData) {
let pathPoints = [];
try {
// 尝试解析 JSON
pathPoints = res?.data?.jsonData?.path || [];
} catch (err) {
console.error('解析路线数据失败:', err);
}
if (pathPoints.length === 0) {
utils.message.warning(t('devices.route2'));
}
setPolyPoints(pathPoints);
} else {
setPolyPoints([]);
setRouteMode('');
}
} catch (err) {
console.error('获取路线详情失败:', err);
setPolyPoints([]);
setRouteMode('');
}
}
useEffect(() => {
getRouteOut();
}, [selectedPathWork?.id])
// 调用接口获取路线详情
const fetchRouteDetail = async (id) => {
console.log(id, "获取正在执行任务的id")
try {
const params = {
taskId: id,
}
const res = await getTaskPath(params)
//const res = await getSingleRouteList({ id: selectedPathWork.id });
if (res?.code === 200 && res?.data) {
let pathPoints = [];
try {
// 尝试解析 JSON
pathPoints = res.data || [];
} catch (err) {
console.error('解析路线数据失败:', err);
}
if (pathPoints.length === 0) {
utils.message.warning(t('devices.route2'));
}
setPoints(pathPoints);
} else {
setPoints([]);
}
} catch (err) {
console.error('获取路线详情失败:', err);
setPoints([]);
}
};
useEffect(() => {
if (!runningPoolTask?.id) return;
loadTaskPath(runningPoolTask.id);
fetchRouteDetail(runningPoolTask?.id);
// 只有当任务是执行状态,并且没有连接的设备时,才自动连接
if ((runningPoolTask.taskStaus == 'EXECUTING' || runningPoolTask.taskStaus == 'PAUSE') && !executingRobot) {
const targetDevice = devices?.find(d => d.serialNumber === runningPoolTask.deviceId);
if (targetDevice) {
setExecute(true);
setExecutingRobot(targetDevice);
setClearNavLine(v => v + 1); // 清空地图轨迹
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(targetDevice); } else { controllerRef.current?.switchDevice(targetDevice); }
setShowVideo(true);
}
}
}, [runningPoolTask, devices, executingRobot])
// 获取路线列表
const fetchRouteList = async () => {
if (!stationId) return;
getRobotRouteList({ siteId: stationId }).then(res => {
if (res.code == 200 && res.data) {
setRouteList(res.data);
} else {
setRouteList([]);
}
}).catch(() => setRouteList([]));
};
const handleDelete = (id) => {
deleteDeviceTask({ id }).then(res => {
if (res.code == 200) {
utils.message.success(t('common.deleteSuccess'));
} else {
utils.message.error(res.msg);
}
}).catch(() => message.error(t('common.deleteFail'))).finally(() => {
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
});
}
// 清空所有筛选项并刷新任务列表
// 清空所有筛选项并刷新 —— 现在超级简单!
const resetFiltersAndRefresh = () => {
setFilterForm({
dateRange: null,
taskStatus: null,
robot: null
});
};
const handleTaskexcuted = async (route) => {
try {
console.log(route);
setSelectedRouteForExecute(route);
setExecuteModalVisible(true);
setSelectedRobotForExecute(null);
} catch (err) {
console.error('检查路线失败:', err);
utils.message.error(t('devices.checkRouteFail'));
}
}
const handleExecuteConfirm = async () => {
if (!selectedRobotForExecute) {
message.error(t('devices.selectExecuteDevice'));
return;
}
try {
const params = {
deviceId: selectedRobotForExecute.serialNumber,
routeId: selectedRouteForExecute.id,
siteId: stationId,
...(currentStation?.orgId ? { orgId: currentStation.orgId } : {}),
};
const res = await excuetRoute(params);
if (res.code === 200) {
setExecute(true);
utils.message.success(t('devices.taskExecSuccess'));
setExecuteModalVisible(false);
setExecutingRobot(selectedRobotForExecute);
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(selectedRobotForExecute); } else { controllerRef.current?.switchDevice(selectedRobotForExecute); }
setShowVideo(true); // 连接成功后自动打开视频
} else {
setExecute(false);
utils.message.error(res.msg || '任务执行失败');
}
} catch (err) {
setExecute(false);
console.error('执行任务失败:', err);
utils.message.error(t('devices.taskExecuteException'));
}
}
const handleTaskCreated = () => {
resetFiltersAndRefresh();
};
// 获取任务池数据
const fetchTaskPoolList = async () => {
if (!stationId) return;
try {
const params = {
userId: userInfo?.userId,
siteId: stationId,
orgId: currentStation?.orgId,
pageNum: 1,
pageSize: 99999999
};
if (poolFilterForm.taskStatus) {
params.taskStaus = poolFilterForm.taskStatus;
}
if (poolFilterForm.robot?.serialNumber) {
params.deviceId = poolFilterForm.robot.serialNumber;
}
if (poolFilterForm.planId) {
params.planId = poolFilterForm.planId;
}
const res = await getDeviceTastPool(params);
if (res.code === 200) {
const list = res.rows || [];
list.sort((a, b) => {
const aEmpty = !a.planId;
const bEmpty = !b.planId;
return aEmpty ? -1 : bEmpty ? 1 : 0;
});
setTaskPoolList(list);
}
} catch (err) {
console.error('获取任务池失败:', err);
}
};
// 获取历史任务数据
const fetchTaskHistoryList = async () => {
if (!stationId) return;
try {
const params = {
siteId: stationId,
orgId: currentStation?.orgId,
pageNum: 1,
pageSize: 99999999
};
if (historyFilterForm.taskStatus) {
params.taskStaus = historyFilterForm.taskStatus;
}
if (historyFilterForm.robot?.serialNumber) {
params.deviceId = historyFilterForm.robot.serialNumber;
}
if (historyFilterForm.dateRange && historyFilterForm.dateRange.length === 2) {
params.startTime = historyFilterForm.dateRange[0].format('YYYY-MM-DD HH:mm:ss');
params.endTime = historyFilterForm.dateRange[1].format('YYYY-MM-DD HH:mm:ss');
}
const res = await getSiteDeviceTaskHistory(params);
if (res.code === 200) {
setTaskHistoryList(res.rows || []);
}
} catch (err) {
console.error('获取历史任务失败:', err);
}
};
// 取消任务
const handleCancelTask = async (task, mode) => {
try {
const params = {
taskId: task.id,
//deviceId: task.deviceId,
siteId: stationId,
orgId: task.orgId,
forceCancel: mode === 'force' ? true : false,
};
const res = await cancelRoute(params);
if (res.code === 200 && res.data == true) {
utils.message.success(t('devices.fixed2TaskSuspended2Fixed2CancelledFixed2'));
fetchTaskPoolList();
fetchTaskHistoryList();
setExecute(false);
//if (controllerRef.current) {
// controllerRef.current.destroy?.();
//}
setSelectedRobotForExecute(null);
setSelectedRouteForExecute(null);
} else {
utils.message.error(t('devices.taskCancelFail'));
}
} catch (err) {
console.error('取消任务失败:', err);
utils.message.error(t('devices.fixed2CancelFixed2TaskSuspended2Fixed2AbnormalFixed2'));
}
};
// 暂停任务
const handlePauseTask = async (task) => {
try {
const params = {
taskId: task.id,
orgId: task.orgId,
//deviceId: task.deviceId,
siteId: stationId,
};
const res = await pauseRoute(params);
if (res.code === 200) {
utils.message.success(t('devices.taskPaused'));
fetchTaskPoolList();
fetchTaskHistoryList();
} else {
utils.message.error(res.msg || '暂停失败');
}
} catch (err) {
console.error('暂停任务失败:', err);
utils.message.error(t('devices.fixed2SuspendTaskFixed2AbnormalFixed2'));
}
};
// 恢复任务
const handleRecoverTask = async (task) => {
try {
const params = {
taskId: task.id,
orgId: task.orgId,
siteId: stationId,
};
const res = await recoverRoute(params);
if (res.code === 200) {
utils.message.success(t('devices.taskResumed'));
fetchTaskPoolList();
fetchTaskHistoryList();
} else {
utils.message.error(res.msg || '恢复失败');
}
} catch (err) {
console.error('恢复任务失败:', err);
utils.message.error(t('devices.fixed2ResumeTaskFixed2AbnormalFixed2'));
}
};
useEffect(() => {
if (stationId) {
fetchRouteList();
}
}, [stationId]);
// 监听筛选条件变化
useEffect(() => {
if (stationId) {
fetchTaskList();
}
}, [filterForm, stationId]);
// 监听任务池筛选条件变化
useEffect(() => {
if (stationId) {
fetchTaskPoolList();
}
}, [poolFilterForm, stationId]);
// 监听历史任务筛选条件变化
useEffect(() => {
if (stationId) {
fetchTaskHistoryList();
}
}, [historyFilterForm, stationId]);
const taskColumns = [
{
title: t('devices.taskName'),
dataIndex: 'planName',
width: 160,
ellipsis: true,
fixed: "left"
},
{
title: t('devices.device'),
dataIndex: 'deviceAlias',
width: 320,
render: (_, record) => {
return record.deviceAlias ? record.deviceAlias : record.deviceId;
}
},
{
title: t('devices.creator'),
dataIndex: 'createBy',
width: 100,
render: (text) => text
},
{
title: t('common.createTime'),
dataIndex: 'createTime',
width: 170,
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
// 执行计划 + 周期时间
{
title: t('devices.executionPlan'),
width: 280,
render: (_, record) => {
const rule = record.deviceTaskPlanRules || {};
const { type, startDate, dayTime, days, dayOfMonth } = rule;
// 周映射
const weekMap = {
SUNDAY: t('devices.sun'),
MONDAY: t('devices.mon'),
TUESDAY: t('devices.tue'),
WEDNESDAY: t('devices.wed'),
THURSDAY: t('devices.thu'),
FRIDAY: t('devices.fri'),
SATURDAY: t('devices.sat')
};
switch (type) {
case 'DAY':
return t('devices.planByDay', { startDate: startDate || '', dayTime: dayTime || '' });
case 'WEEK':
const weekText = (days || []).map(d => weekMap[d]).join('、');
return t('devices.planByWeek', { weekText, dayTime: dayTime || '' });
case 'MONTH':
const monthText = (dayOfMonth || []).map(d => t('devices.daySuffix', { day: d })).join('、');
return t('devices.planByMonth', { monthText, dayTime: dayTime || '' });
default:
return t('devices.unknownPlan');
}
}
},
{
title: t('devices.executionCycle'),
width: 320,
render: (_, record) => {
return `${record.startTime || ''} - ${record.finishTime || ''}`;
}
},
{
title: t('roles.status'),
dataIndex: 'taskStaus',
width: 100,
render: (status, record) => {
console.log('status', status, record);
const statusMap = {
'NEW': { color: 'default', text: t('devices.statusNew') },
'EXECUTING': { color: 'processing', text: t('workOrder.inProgress') },
'PAUSE': { color: 'warning', text: t('devices.statusPaused') },
'FINISH': { color: 'success', text: t('devices.statusFinished') },
'FAILED': { color: 'error', text: t('devices.statusFailed') },
"CANCELING": { color: 'warning', text: t('devices.statusCanceling') },
};
const item = statusMap[status] || { color: 'default', text: record.taskStausTranslate || t('constants.locationUnknown') };
return <Tag color={item.color}>{record.taskStausTranslate}</Tag>;
}
},
{
title: t('roles.actions'),
width: 300,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Popconfirm
title={t('devices.confirmDeleteTask')}
onConfirm={() => handleDelete(record.id)}
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
>
{t('common.delete')}
</Button>
</Popconfirm>
</Space>
)
}
];
// 任务池表格列
const taskPoolColumns = [
{
title: t('devices.device'),
dataIndex: 'deviceId',
width: 320,
},
{
title: t('roles.status'),
dataIndex: 'taskStaus',
width: 100,
render: (status, record) => {
const statusMap = {
'NEW': { color: 'default', text: t('devices.statusNew') },
'EXECUTING': { color: 'processing', text: t('workOrder.inProgress') },
'PAUSE': { color: 'warning', text: t('devices.statusPaused') },
'FINISH': { color: 'success', text: t('devices.statusFinished') },
'FAILED': { color: 'error', text: t('devices.statusFailed') },
"CANCELING": { color: 'warning', text: t('devices.statusCanceling') },
};
const item = statusMap[status] || { color: 'default', text: t('constants.locationUnknown') };
return <Tag color={item.color}>{item.text}</Tag>;
}
},
{
title: t('common.createTime'),
dataIndex: 'createTime',
width: 170,
},
{
title: t('roles.actions'),
width: 100, // 加宽一点
fixed: 'right',
render: (_, record) => (
<Space size="small">
<>
{/*{record.taskStaus === 'NEW' && (*/}
<Popconfirm
title={t('devices.confirmCancelTask')}
description={
<Radio.Group
value={cancelMode}
onChange={(e) => setCancelMode(e.target.value)}
style={{ marginTop: 8 }}
>
<Space direction="vertical">
<Radio value="normal">{t('devices.normalCancel')}</Radio>
<Radio value="force">{t('devices.forceCancel')}</Radio>
</Space>
</Radio.Group>
}
onConfirm={() => handleCancelTask(record, cancelMode)}
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<Button
type="text"
size="small"
danger
icon={<CloseOutlined />}
onClick={(e) => e.stopPropagation()}
>
{t('common.cancel')}
</Button>
</Popconfirm>
{/*//)}*/}
{/*{record.taskStaus === 'EXECUTING' && (*/}
<Popconfirm
title={t('devices.confirmPauseTask')}
onConfirm={() => handlePauseTask(record)}
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" style={{ color: '#FF9500' }} onClick={(e) => e.stopPropagation()}>
{t('video.pause')}
</Button>
</Popconfirm>
{/*)}/*/}
{record.taskStaus === 'PAUSE' && (
<Popconfirm
title={t('devices.confirmResumeTask')}
onConfirm={() => handleRecoverTask(record)}
okText={t('common.ok')} cancelText={t('common.cancel')}
>
<Button type="text" size="small" style={{ color: '#00B42A' }} onClick={(e) => e.stopPropagation()}>
{t('devices.recover')}
</Button>
</Popconfirm>
)}
</>
</Space>
)
},
];
// 历史任务表格列
const taskHistoryColumns = [
{
title: t('devices.device'),
dataIndex: 'deviceId',
width: 320,
},
{
title: t('roles.status'),
dataIndex: 'taskStausTranslate',
width: 100,
render: (text, record) => {
const status = record.taskStaus; // 原始状态值
let color = 'default';
if (status === 'NEW') color = 'default';
else if (status === 'EXECUTING') color = 'processing';
else if (status === 'PAUSE') color = 'warning';
else if (status === 'FINISH') color = 'success';
else if (status === 'FAILED') color = 'error';
return <Tag color={color} style={{ margin: 0 }}>{text}</Tag>;
},
},
{
title: t('devices.startTime'),
dataIndex: 'startTime',
width: 170,
},
{
title: t('devices.endTime'),
dataIndex: 'finishTime',
width: 170,
}, {
title: t('common.createTime'),
dataIndex: 'createTime',
width: 170,
}
];
const handleRowClick = (record) => {
if (record.longitude && record.latitude) {
setFocusLocation({
lon: record.longitude,
lat: record.latitude,
type: 'robot',
name: record.deviceName
});
}
};
const groupBoardData = useMemo(() => {
const waitList = taskList.filter(item => item.taskStaus === 'NEW');
const runList = taskList.filter(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus));
const finishList = taskList.filter(item => ['FINISH', 'FAILED'].includes(item.taskStaus));
return { waitList, runList, finishList };
}, [taskList]);
const historyData = useMemo(() => {
const successfulTaskList = taskHistoryList.filter(item => ['FINISH', 'FAILED'].includes(item.taskStaus));
return { successfulTaskList };
}, [taskHistoryList]);
const { waitList, runList, finishList } = groupBoardData;
const { successfulTaskList } = historyData;
const topCards = [
{ title: t('devices.statTodayPlan'), value: taskList.length, unit: t('devices.unitPlan'), desc: t('devices.statInProgress'), icon: <LayoutGrid size={22} />, color: '#165DFF' },
{ title: t('devices.statWorkingRobots'), value: taskPoolList?.filter(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus))?.length, unit: t('devices.unitRobot'), desc: t('devices.statWorking'), icon: <Car size={22} />, color: '#165DFF' },
{ title: t('devices.statTotalDistance'), value: '12.5', unit: 'km', trend: t('devices.vsYesterday', { value: '+1.2' }), trendColor: '#00B42A', icon: <TrendingUp size={22} />, color: '#722ED1' },
{ title: t('devices.statCompletionRate'), value: successfulTaskList.length ? Math.round((successfulTaskList.length / taskHistoryList.length) * 100) : 0, unit: '%', trend: t('devices.vsYesterday', { value: '+5%' }), trendColor: '#00B42A', icon: <CheckCircle2 size={22} />, color: '#00B42A' },
{ title: t('devices.statAlerts'), value: '0', unit: t('devices.unitTimes'), trend: t('devices.vsYesterday', { value: '-1' }), trendColor: '#00B42A', icon: <AlertCircle size={22} />, color: '#F53F3F' },
{
title: t('devices.statOnlineRate'),
value: `${devices?.length ? Math.round((devices.filter(d => d.onlineStatus == 1).length / devices.length) * 100) : 0}`,
unit: '%',
desc: t('devices.good'),
icon: <Satellite size={22} />,
color: '#165DFF'
}];
return (
<div className="normal-spacing" style={{ background: '#f5f7fa', minHeight: '100vh', padding: '0px', paddingTop: 0 }}>
<div style={{ padding: '4px 0px 8px' }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>{t('devices.robotTask')}</Typography.Title>
</div>
<Row gutter={[16, 16]} style={{ marginBottom: 8 }}>
{topCards.map((item, index) => (
<Col xs={12} sm={8} md={4} key={index}>
<Card
variant="borderless"
styles={{
body: {
padding: '12px 14px',
height: '100%',
display: 'flex',
alignItems: 'center'
}
}}
style={{
...cardStyle,
height: '100%',
minHeight: '84px'
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%' }}>
<div
style={{
width: 36,
height: 36,
background: 'rgba(22, 93, 255, 0.1)',
borderRadius: 10,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: item.color,
flexShrink: 0
}}
>
{React.cloneElement(item.icon as React.ReactElement, { size: 18 })}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, color: '#86909C', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{item.title}
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, margin: '2px 0' }}>
<span
style={{
fontSize: 'clamp(16px, 1vw, 20px)',
fontWeight: 700,
color: '#1D2129',
lineHeight: '1.2'
}}
>
{item.value}
</span>
<span style={{ fontSize: 9, color: '#4E5969' }}>{item.unit}</span>
</div>
<div
style={{
fontSize: 9,
color: item.trendColor || '#86909C',
fontWeight: 500,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{item.trend || item.desc}
</div>
</div>
</div>
</Card>
</Col>
))}
</Row>
<Row gutter={10} style={{ marginBottom: 10 }}>
<Col xs={24} xl={14} style={{ display: "flex", flexDirection: "column" }}>
<Card
title={
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
flexWrap: 'wrap',
// 新增核心修复样式
flex: 1,
overflow: 'visible',
whiteSpace: 'normal',
minWidth: 0, // flex 子元素收缩兜底,超长时允许压缩换行
}}
>
<span style={{ fontSize: fontNormal, fontWeight: 700 }}>
{t('devices.robotRouteMonitor')}
</span>
</div>
}
size="small"
extra={
<Space size={4} wrap> {/* Space开启自动换行,小屏按钮自动折行 */}
<Button size="small" icon={<DeleteOutlined />} type="primary" style={{ borderRadius: 4 }} onClick={() => setClearNavLine(v => v + 1)}>{t('devices.clearRealtimeTrack')}</Button>
<Button size="small" icon={<DeleteOutlined />} danger style={{ borderRadius: 4 }} onClick={() => { setPoints([]); setPolyPoints([]); setRouteMode(""); setClearPlanLine(v => v + 1); }}>{t('devices.clearPlanRoute')}</Button>
{/*<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4 }}>{t('common.addNew')}</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4 }}>{t('common.import')}</Button>
<Button size="small" icon={<NodeIndexOutlined />} style={{ borderRadius: 4 }}>{t('devices.optimize')}</Button>
<Button size="small" icon={<SaveOutlined />} style={{ borderRadius: 4 }}>{t('common.save')}</Button>*/}
{/*<Button size="small" icon={<Maximize2 size={12} />} />*/}
</Space>
}
variant="borderless"
styles={{ body: { padding: 0, position: 'relative' } }}
style={{ ...cardStyle, marginBottom: 10 }}
>
<div style={{ width: '100%', height: 420, background: '#e8f3ff center/cover', position: 'relative' }}>
<CesiumMap
devices={devices}
points={points}
focusLocation={focusLocation}
realtimePosition={realtimePosition}
polyPoints={polyPoints}
realtimeType="robot"
from="devicesTask"
drawRealTime={execute}
finishPoint={arrivedPoint}
ifClearNavLine={clearNavLine}
ifClearPlanLine={clearPlanLine}
routeMode={routeMode}
mapLayer={currentStation?.siteMapName}
/>
{/* 机器人状态悬浮面板 - 绝对定位左上角 */}
{execute && executingRobot && (
<div
style={{
position: 'absolute',
top: 16,
left: 10, // 避开左侧图例面板,防止重叠
background: 'rgba(22, 93, 255, 0.88)',
backdropFilter: 'blur(6px)',
color: '#fff',
fontSize: '12px',
padding: '8px 12px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.15)',
maxWidth: 360, // 限制最大宽度,不会无限拉长
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: '6px 12px',
rowGap: 6,
zIndex: 10,
}}
>
<span style={{ fontWeight: 600 }}>
🤖 {executingRobot.deviceAlias || executingRobot.serialNumber}
</span>
<span>
{t('devices.controlMode')}
{robotRealtimePosition?.controlMode == '1' ? (
<span style={{ color: '#ff6f6f' }}>{t('devices.localMode')}</span>
) : (
<span style={{ color: '#9eff9e' }}>{t('devices.remoteMode')}</span>
)}
</span>
<span>
{t('devices.location')}
{robotRealtimePosition?.fix_status ? fixStatusMap[robotRealtimePosition?.fix_status] : t('constants.locationUnknown')}
</span>
{/* 视频开关 */}
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{t('devices.video')}
<Switch
size="small"
checked={showVideo}
onChange={setShowVideo}
/>
</span>
</div>
)}
</div>
</Card>
{/* 机器人任务模块,带Tab切换 */}
<Card
size="small"
variant="borderless"
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: "calc(100%)", overflow: "auto" } }}
style={{ ...cardStyle, maxHeight: "300px", flex: 1, marginBottom: 0 }}
>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
size="small"
style={{ padding: '0 12px' }}
items={[
{
key: 'pool',
label: t('devices.taskPool'),
},
{
key: 'task',
label: t('devices.taskExecutionPlan'),
},
{
key: 'history',
label: t('devices.taskHistory'),
}
]}
tabBarExtraContent={
<Space size={8}>
<Button size="small" icon={<ReloadOutlined />} onClick={() => {
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
}} />
<Tooltip title={t('devices.createTask')}>
<PlusOutlined
style={{ cursor: 'pointer', color: '#165DFF', fontSize: 14 }}
onClick={() => setIsTaskModalOpen(true)}
/>
</Tooltip>
</Space>
}
/>
<div style={{ padding: '0 12px 12px', flex: 1, overflow: 'auto' }}>
{/* 任务列表Tab */}
{activeTab === 'task' && (
<>
<div style={{ marginBottom: 8, display: 'flex', gap: 8, alignItems: 'center' }}>
<RangePicker
size="small"
format="YYYY-MM-DD"
placeholder={[t('devices.startDate'), t('devices.endDate')]}
value={filterForm.dateRange}
onChange={(dates) => {
setFilterForm(prev => ({ ...prev, dateRange: dates }));
}}
/>
<Select
style={{ width: 120 }}
size="small"
placeholder={t('roles.status')}
allowClear
options={[
{ label: t('devices.statusNew'), value: 'NEW' },
{ label: t('workOrder.inProgress'), value: 'EXECUTING' },
{ label: t('devices.statusPaused'), value: 'PAUSE' },
{ label: t('devices.statusFinished'), value: 'FINISH' },
{ label: t('devices.statusFailed'), value: 'FAILED' },
]}
value={filterForm.taskStatus}
onChange={(val) => {
setFilterForm(prev => ({ ...prev, taskStatus: val }));
}}
/>
<Select
style={{ width: 140 }}
size="small"
placeholder={t('devices.allRobots')}
allowClear
options={devices?.map(item => ({
value: item.serialNumber,
label: item.deviceAlias || item.deviceName
}))}
value={filterForm.robot?.serialNumber}
onChange={(val) => {
if (!val) {
setFilterForm(prev => ({ ...prev, robot: null }));
return;
}
const robot = devices.find(i => i.serialNumber === val);
setFilterForm(prev => ({ ...prev, robot: robot }));
}}
/>
</div>
<Table
size="small"
rowKey="id"
columns={taskColumns}
dataSource={taskList}
pagination={false}
scroll={{ x: 1200 }}
onRow={(record) => ({
onClick: () => handleRowClick(record),
style: { cursor: 'pointer' }
})}
/>
</>
)}
{/* 任务池Tab */}
{activeTab === 'pool' && (
<>
<div style={{ marginBottom: 8, display: 'flex', gap: 8, alignItems: 'center' }}>
<Select
style={{ width: 120 }}
size="small"
placeholder={t('roles.status')}
allowClear
options={[
{ label: t('devices.statusNew'), value: 'NEW' },
{ label: t('workOrder.inProgress'), value: 'EXECUTING' },
{ label: t('devices.statusPaused'), value: 'PAUSE' },
]}
value={poolFilterForm.taskStatus}
onChange={(val) => {
setPoolFilterForm(prev => ({ ...prev, taskStatus: val }));
}}
/>
<Select
style={{ width: 140 }}
size="small"
placeholder={t('devices.allRobots')}
allowClear
options={devices?.map(item => ({
value: item.serialNumber,
label: item.deviceAlias || item.deviceName
}))}
value={poolFilterForm.robot?.serialNumber}
onChange={(val) => {
if (!val) {
setPoolFilterForm(prev => ({ ...prev, robot: null }));
return;
}
const robot = devices.find(i => i.serialNumber === val);
setPoolFilterForm(prev => ({ ...prev, robot: robot }));
}}
/>
</div>
<Table
size="small"
rowKey="id"
columns={taskPoolColumns}
dataSource={taskPoolList}
pagination={false}
scroll={{ x: 800, y: 150 }}
onRow={(record) => ({
onClick: () => handlePoolTaskClick(record),
style: {
cursor: 'pointer',
backgroundColor: record.id === runningPoolTask?.id ? '#e6f4ff' : 'transparent'
}
})}
/>
</>
)}
{/* 历史任务Tab */}
{activeTab === 'history' && (
<>
<div style={{ marginBottom: 8, display: 'flex', gap: 8, alignItems: 'center' }}>
<RangePicker
size="small"
format="YYYY-MM-DD HH:mm:ss"
placeholder={[t('devices.startTime'), t('devices.endTime')]}
value={historyFilterForm.dateRange}
onChange={(dates) => {
setHistoryFilterForm(prev => ({ ...prev, dateRange: dates }));
}}
showTime
/>
<Select
style={{ width: 120 }}
size="small"
placeholder={t('roles.status')}
allowClear
options={[
{ label: t('devices.statusFinished'), value: 'FINISH' },
{ label: t('devices.statusFailed'), value: 'FAILED' },
{ label: t('clean.cancelled'), value: 'CANCELED' },
]}
value={historyFilterForm.taskStatus}
onChange={(val) => {
setHistoryFilterForm(prev => ({ ...prev, taskStatus: val }));
}}
/>
<Select
style={{ width: 340 }}
size="small"
placeholder={t('devices.allRobots')}
allowClear
options={devices?.map(item => ({
value: item.serialNumber,
label: item.deviceAlias || item.deviceName
}))}
value={historyFilterForm.robot?.serialNumber}
onChange={(val) => {
if (!val) {
setHistoryFilterForm(prev => ({ ...prev, robot: null }));
return;
}
const robot = devices.find(i => i.serialNumber === val);
setHistoryFilterForm(prev => ({ ...prev, robot: robot }));
}}
/>
</div>
<Table
size="small"
rowKey="id"
columns={taskHistoryColumns}
dataSource={taskHistoryList}
pagination={false}
scroll={{ x: 800, y: 150 }}
/>
</>
)}
</div>
</Card>
</Col>
<Col xs={24} xl={10}>
<Card
title={
<div className="flex justify-between items-center w-full">
<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.presetRoutes')}</span>
<Input
placeholder={t('devices.searchPlaceholder')}
size="small"
style={{ width: 200 }}
value={routeSearch}
onChange={e => setRouteSearch(e.target.value)}
/>
</div>
}
variant="borderless"
styles={{ body: { padding: '12px', height: 732, overflow: 'auto' } }}
style={cardStyle}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{routeList
//.filter(item => {
// if (!routeSearch) return true;
// const name = (item.workName || item.name || '').toLowerCase();
// const id = String(item.id || '').toLowerCase();
// const key = routeSearch.toLowerCase();
// return name.includes(key) || id.includes(key);
//})
.map(route => (
<div
key={route.id}
style={{
padding: 12,
borderRadius: 8,
background: '#F7F8FA',
cursor: 'pointer',
// 核心:判断当前项是否被选中,动态设置边框
border: selectedPathWork?.id === route.id ? '1px solid #165DFF' : '1px solid transparent',
display: 'flex',
gap: 12,
alignItems: 'center'
}}
onClick={(e) => {
e.stopPropagation();
// 先清空再设置,确保每次点击都能触发 useEffect 的重绘
setSelectedPathWork(null);
// 在下一个 tick 中设置,确保 state 重置生效
setTimeout(() => {
setSelectedPathWork(route);
}, 0);
}}
onMouseEnter={e => {
// 只有未选中的项,悬浮才变色
if (selectedPathWork?.id !== route.id) {
e.currentTarget.style.borderColor = '#165DFF';
}
}}
onMouseLeave={e => {
// 只有未选中的项,离开才恢复透明
if (selectedPathWork?.id !== route.id) {
e.currentTarget.style.borderColor = 'transparent';
}
}}
>
<div style={{
width: 40,
height: 40,
borderRadius: 6,
background: '#e8f3ff',
overflow: 'hidden',
flexShrink: 0
}}>
{route.imgUrl ? (
<img
src={route.imgUrl}
alt={route.workName || route.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#165DFF'
}}>
<EyeOutlined />
</div>
)}
</div>
{/* 路线信息 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{route.workName || route.name || t('devices.unnamedRoute')}
</div>
<div style={{ fontSize: 12, color: '#86909C', marginTop: 4 }}>
ID: {route.id}
</div>
</div>
{/* 删除按钮 */}
<Tooltip title={t('devices.deleteRoute')}>
<Button
type="text"
icon={<DeleteOutlined style={{ color: '#F53F3F', fontSize: 20 }} />}
onClick={(e) => {
e.stopPropagation();
setSelectedPathWork(route);
// 替换为你的删除处理函数
handleDeleteRoute(route);
}}
/>
</Tooltip>
<Tooltip title={t('devices.createTask')}>
<Button
type="text"
icon={<PlusOutlined style={{ color: '#165DFF', fontSize: 20 }} />}
onClick={(e) => {
e.stopPropagation();
setSelectedPathWork(route);
setIsTaskModalOpen(true);
}}
/>
</Tooltip>
{/* 执行按钮 */}
<Tooltip title={t('devices.executeRoute')}>
<Button
type="text"
icon={<PlayCircleOutlined style={{ color: '#165DFF', fontSize: 28 }} />}
onClick={(e) => {
e.stopPropagation();
setSelectedPathWork(route);
handleTaskexcuted(route);
}}
/>
</Tooltip>
</div>
))}
</div>
</Card>
</Col>
</Row>
<Row gutter={16} >
<Col xs={24} xl={6} style={{ display: "flex", flexDirection: "column" }} >
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.operationSuggestion')}</span>}
variant="borderless"
styles={{ body: { padding: '12px 16px' } }}
style={{ ...cardStyle, marginBottom: 8 }}
>
<Timeline
mode="left"
items={[
{
color: 'green',
label: '09:30',
children: (
<>
<Text strong>{t('devices.timelineMow', { robot: 'RBT-01' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMowDetail')}</div>
</>
)
},
{
color: 'blue',
label: '11:00',
children: (
<>
<Text strong>{t('devices.timelineInspect', { robot: 'RBT-02' })}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineInspectDetail')}</div>
</>
)
},
{
color: 'gray',
label: '14:00',
children: (
<>
<Text strong>{t('devices.timelineMaintenance')}</Text>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.timelineMaintenanceDetail')}</div>
</>
)
}
]}
/>
<Button type="primary" block style={{ marginTop: 16, borderRadius: 18 }}>{t('devices.autoGenerateTodayPlan')}</Button>
</Card>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.deviceEnvironment')}</span>}
variant="borderless"
styles={{ body: { padding: '16px' } }}
style={{ ...cardStyle, flex: 1 }}
>
<Row gutter={[8, 8]}>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.currentTemp')}</div>
<div style={{ fontSize: 18, fontWeight: 700 }}>24.5 ℃</div>
</div>
</Col>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.humidity')}</div>
<div style={{ fontSize: 18, fontWeight: 700 }}>65 %</div>
</div>
</Col>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.positionAccuracy')}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: '#00B42A' }}>{t('devices.rtkFixed')}</div>
</div>
</Col>
<Col span={12}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 10 }}>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.networkLatency')}</div>
<div style={{ fontSize: 18, fontWeight: 700 }}>45 ms</div>
</div>
</Col>
</Row>
<div style={{ marginTop: 8, padding: 12, background: 'rgba(255, 125, 0, 0.1)', borderRadius: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
<AlertCircle size={16} style={{ color: '#FF7D00' }} />
<span style={{ fontSize: 13, color: '#FF7D00' }}>{t('devices.waterRiskWarning')}</span>
</div>
</Card>
</Col>
<Col xs={24} xl={18} style={{ display: 'flex', flexDirection: 'column', }}>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.taskBoard')}</span>}
variant="borderless"
styles={{ body: { padding: '12px', flex: 1 } }}
style={{ ...cardStyle, height: '100%', display: "flex", flexDirection: "column" }}
>
<Row gutter={16} style={{ height: "100%" }} >
<Col span={8}>
<div style={{ background: '#F7F8FA', padding: 12, borderRadius: 8, height: "100%" }}>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#86909C" text={t('devices.pending')} /></Title>
{waitList.map((item, idx) => (
<Card key={idx} size="small" style={{ marginBottom: 8, borderRadius: 6 }}>
<div style={{ fontWeight: 600 }}>{item.planName || item.workName}</div>
<div style={{ fontSize: 12, color: '#86909C' }}>{t('devices.device')}: {item.deviceId}</div>
</Card>
))}
</div>
</Col>
<Col span={8}>
<div style={{ background: '#E8F3FF', padding: 12, borderRadius: 8, height: "100%" }}>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#165DFF" text={t('devices.statInProgress')} /></Title>
{runList.map((item, idx) => (
<Card key={idx} size="small" style={{ marginBottom: 8, borderRadius: 6 }}>
<div style={{ fontWeight: 600 }}>{item.planName || item.workName}</div>
<div style={{ fontSize: 12, color: '#165DFF' }}>
{item.taskStaus === 'EXECUTING' ? t('devices.executingEllipsis') : t('devices.paused')}
</div>
</Card>
))}
</div>
</Col>
<Col span={8}>
<div style={{ background: '#F6FFED', padding: 12, borderRadius: 8, height: "100%" }}>
<Title level={5} style={{ fontSize: 14, marginBottom: 12 }}><Badge color="#52C41A" text={t('workOrder.completed')} /></Title>
{finishList.slice(0, 3).map((item, idx) => (
<Card key={idx} size="small" style={{ marginBottom: 8, borderRadius: 6 }}>
<div style={{ fontWeight: 600 }}>{item.planName || item.workName}</div>
<div style={{ fontSize: 12, color: item.taskStaus === 'FAILED' ? '#F53F3F' : '#52C41A' }}>
{item.taskStaus === 'FAILED' ? t('devices.statusFailed') : t('devices.statusFinished')}
</div>
</Card>
))}
</div>
</Col>
</Row>
</Card>
</Col>
</Row>
{/* 🔥 传递 routeList 给弹窗 */}
<NewTaskModal
isOpen={isTaskModalOpen}
onClose={() => setIsTaskModalOpen(false)}
robots={devices}
selectedPathWork={selectedPathWork}
handlGetTaskList={fetchTaskList}
routeList={routeList}
stationId={stationId}
onTaskCreated={handleTaskCreated}
userInfo={userInfo}
/>
{/* 执行任务弹窗 */}
<Modal
title={t('devices.executeRouteTask')}
open={executeModalVisible}
onCancel={() => setExecuteModalVisible(false)}
onOk={handleExecuteConfirm}
okText={t('devices.execute')}
cancelText={t('common.cancel')}
>
<div style={{ padding: '10px 0' }}>
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('devices.routeInfo')}</div>
<div style={{ padding: 12, background: '#f5f7fa', borderRadius: 6 }}>
<div><strong>{t('devices.routeName')}</strong>{selectedRouteForExecute?.workName || selectedRouteForExecute?.name}</div>
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('workOrder.selectDevice')}</div>
<Select
style={{ width: '100%' }}
placeholder={t('devices.selectExecuteDevice')}
value={selectedRobotForExecute?.serialNumber}
onChange={(val) => {
const robot = devices.find(d => d.serialNumber === val);
setSelectedRobotForExecute(robot);
}}
>
{devices.map(device => (
<Select.Option key={device.serialNumber} value={device.serialNumber}>
{device.deviceAlias} ({device.serialNumber})
</Select.Option>
))}
</Select>
</div>
</div>
</Modal>
{showVideo && executingRobot && execute && taskPoolList.length > 0 && (
<DraggableBox
defaultPosition={{ x: 50, y: 50 }}
title={t('devices.robotPanoramaVideo', { robot: executingRobot.deviceAlias || executingRobot.serialNumber })}
onClose={() => setShowVideo(false)}
>
<div style={{ height: '100%', width: '100%' }}>
<MowerAllView
serialNumber={executingRobot.serialNumber}
robotVideo={robotVideo}
ready={ready}
error={error}
/>
</div>
</DraggableBox>
)}
</div >
);
}
const NewTaskModal = ({ isOpen, onClose, robots, selectedPathWork, handlGetTaskList, routeList, stationId, onTaskCreated, userInfo }) => {
const { t } = useTranslation();
const [formData, setFormData] = useState({
path: selectedPathWork || null,
device: null,
repeatPlan: 'DAY',
days: [],
dayOfMonth: [],
startDate: '',
dayTime: '',
planName: '',
excuteDayStartTime: '',
excuteDayEndTime: '',
});
const { currentStation } = useSelector((state: RootState) => state.station);
useEffect(() => {
if (selectedPathWork) {
setFormData(prev => ({ ...prev, path: selectedPathWork }));
}
}, [selectedPathWork]);
// ====================== 周/月选择 ======================
const weekDays = [
{ label: t('devices.wdSun'), value: 'SUNDAY' },
{ label: t('devices.wdMon'), value: 'MONDAY' },
{ label: t('devices.wdTue'), value: 'TUESDAY' },
{ label: t('devices.wdWed'), value: 'WEDNESDAY' },
{ label: t('devices.wdThu'), value: 'THURSDAY' },
{ label: t('devices.wdFri'), value: 'FRIDAY' },
{ label: t('devices.wdSat'), value: 'SATURDAY' },
];
const monthDays = Array.from({ length: 31 }, (_, i) => (i + 1).toString());
const handleToggleDay = (val) => {
setFormData(prev => {
const exists = prev.days.includes(val);
const newDays = exists ? prev.days.filter(d => d !== val) : [...prev.days, val];
return { ...prev, days: newDays };
});
};
const handleToggleMonthDay = (val) => {
setFormData(prev => {
const exists = prev.dayOfMonth.includes(val);
const newDays = exists ? prev.dayOfMonth.filter(d => d !== val) : [...prev.dayOfMonth, val];
return { ...prev, dayOfMonth: newDays };
});
};
// ====================== 日期时间 ======================
const handleDateChange = (date) => {
if (!date) return;
setFormData({ ...formData, startDate: date.format('YYYY-MM-DD') });
};
const handleTimeChange = (time, timeString) => {
setFormData({ ...formData, dayTime: timeString });
};
const handleCycleChange = (dates) => {
if (!dates || !dates[0] || !dates[1]) return;
setFormData(prev => ({
...prev,
excuteDayStartTime: dates[0].format('YYYY-MM-DD HH:mm:ss'),
excuteDayEndTime: dates[1].format('YYYY-MM-DD HH:mm:ss'),
}));
};
// ====================== 保存 ======================
const handleSave = () => {
const missingFields = [];
if (!formData.device?.serialNumber) missingFields.push(t('devices.deviceSerial'));
if (!formData.planName) missingFields.push(t('devices.taskName'));
if (!formData.path?.id) missingFields.push(t('devices.route'));
//if (!formData.excuteDayStartTime) missingFields.push(t('workOrder.execStartTime'));
//if (!formData.excuteDayEndTime) missingFields.push('执行结束时间');
if (missingFields.length > 0) {
utils.message.error({ title: t('constants.info'), content: t('devices.fillRequired', { fields: missingFields.join('、') }) });
return;
}
// 统一格式:年月日 时分秒
const startTime = formData.excuteDayStartTime;
const finishTime = formData.excuteDayEndTime;
const finalData = {
deviceId: formData.device.serialNumber,
orgId: currentStation?.orgId,
planName: formData.planName,
routeId: formData.path.id,
siteId: stationId,
deviceTaskPlanRules: { type: formData.repeatPlan },
startTime: startTime,
finishTime: finishTime,
};
if (formData.repeatPlan === 'DAY') {
finalData.deviceTaskPlanRules = {
type: 'DAY',
startDate: formData.startDate,
dayTime: formData.dayTime
};
} else if (formData.repeatPlan === 'WEEK') {
finalData.deviceTaskPlanRules = {
type: 'WEEK',
days: formData.days,
dayTime: formData.dayTime
};
} else if (formData.repeatPlan === 'MONTH') {
finalData.deviceTaskPlanRules = {
type: 'MONTH',
dayOfMonth: formData.dayOfMonth,
dayTime: formData.dayTime
};
}
if (!finalData.deviceTaskPlanRules.dayTime) {
utils.message.error(t('devices.selectExecTime'));
return;
}
saveDeviceTask(finalData).then(res => {
if (res.code === 200) {
utils.message.success(t('devices.taskCreated'));
onClose();
onTaskCreated();
} else {
utils.message.error(res.data.msg || t('devices.createFailed'));
}
}).catch(() => utils.message.error(t('devices.createTaskError')));
};
// ====================== 渲染 ======================
return (
<Modal
title={t('devices.newTask')}
open={isOpen}
onCancel={onClose}
width={500}
maskClosable={false}
footer={null}
>
<div className="space-y-4">
{/* 任务名称 */}
<div>
<label className="block mb-2 font-medium">{t('devices.taskName')}</label>
<Input
value={formData.planName}
onChange={(e) => setFormData({ ...formData, planName: e.target.value })}
placeholder={t('devices.inputTaskName')}
/>
</div>
{/* 执行路线 */}
<div>
<label className="block mb-2 font-medium">{t('devices.executeRouteLabel')}</label>
<Select
className="w-full"
placeholder={t('devices.selectRoute')}
onSelect={(_, option) => {
const route = routeList.find(r => r.id === option.value);
setFormData(prev => ({ ...prev, path: route }));
}}
value={formData.path?.id}
>
{routeList.map(route => (
<Select.Option
key={route.id}
value={route.id}
>
{route.workName || route.name}
</Select.Option>
))}
</Select>
</div>
{/* 选择设备 */}
<div className="relative">
<label className="block mb-2 font-medium">{t('video.selectDevice')}</label>
<Select
className="w-full"
placeholder={t('video.selectDevice')}
onSelect={(_, option) => {
const robot = robots.find(r => r.serialNumber === option.value);
setFormData(prev => ({ ...prev, device: robot }));
}}
value={formData.device?.serialNumber}
>
{robots.map(robot => (
<Select.Option
key={robot.serialNumber}
value={robot.serialNumber}
>
{robot.deviceAlias} ({robot.serialNumber})
</Select.Option>
))}
</Select>
</div>
{/* 重复计划 */}
<div>
<label className="block mb-2 font-medium">{t('devices.repeatPlan')}</label>
<Select
value={formData.repeatPlan}
onChange={(val) => setFormData({ ...formData, repeatPlan: val })}
className="w-full"
>
<Select.Option value="DAY">{t('devices.day')}</Select.Option>
<Select.Option value="WEEK">{t('devices.week')}</Select.Option>
<Select.Option value="MONTH">{t('devices.month')}</Select.Option>
</Select>
</div>
{/* 周选择 */}
{formData.repeatPlan === 'WEEK' && (
<div className="flex gap-1 flex-wrap">
{weekDays.map(day => (
<Button
key={day.value}
type={formData.days.includes(day.value) ? 'primary' : 'default'}
onClick={() => handleToggleDay(day.value)}
size="small"
shape="circle"
>
{day.label}
</Button>
))}
</div>
)}
{/* 月选择 */}
{formData.repeatPlan === 'MONTH' && (
<div className="grid grid-cols-7 gap-1">
{monthDays.map(day => (
<Button
key={day}
type={formData.dayOfMonth.includes(day) ? 'primary' : 'default'}
onClick={() => handleToggleMonthDay(day)}
size="small"
>
{day}
</Button>
))}
</div>
)}
{/* 按天:选择日期 */}
{formData.repeatPlan === 'DAY' && (
<>
<label className="block mb-2 font-medium">{t('devices.execDate')}</label>
<DatePicker
onChange={handleDateChange}
className="w-full"
placeholder={t('devices.selectExecDate')}
/>
</>
)}
{/* 时间 */}
<label className="block mb-2 font-medium">{t('devices.execTime')}</label>
<TimePicker
format="HH:mm:ss"
onChange={handleTimeChange}
className="w-full"
placeholder={t('devices.selectExecTime')}
/>
{/* 执行周期 */}
<label className="block mb-2 font-medium">{t('devices.executionCycle')}</label>
<RangePicker
onChange={handleCycleChange}
className="w-full"
placeholder={[t('devices.startDate'), t('devices.endDate')]}
showTime
format="YYYY-MM-DD HH:mm:ss"
/>
{/* 底部按钮 */}
<div className="flex justify-end gap-2 mt-6">
<Button onClick={onClose}>{t('common.cancel')}</Button>
<Button type="primary" onClick={handleSave}>{t('devices.saveTask')}</Button>
</div>
</div>
</Modal>
);
};