Files
web-Iot/src/components/devices/RobotTaskPage.tsx

2507 lines
80 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,
Empty
} from 'antd';
import {
PlusOutlined, ImportOutlined, NodeIndexOutlined,
SaveOutlined, PlayCircleOutlined, EyeOutlined,
DeleteOutlined,
CloseOutlined,
ReloadOutlined,
ExclamationCircleOutlined,
EnvironmentOutlined,
EnvironmentFilled
} 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 TaskTypeRadio from '../taskRadio/taskRadio';
import {
getRobotRouteList,
getSingleRouteList,
saveDeviceTask, getRobotTaskList,
deleteDeviceTask,
getSiteDevice,
excuetRoute,
cancelRoute,
pauseRoute,
recoverRoute,
getDeviceTastPool,
getSiteDeviceTaskHistory,
getRouteByTask,
deleteRoute,
getTaskPath,
createRegionDefaultTask,
} 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';
import { getStationRegions } from '@/api/stationManage/index.js';
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"
// 全量设备实时状态(mapPush),实时数据统一从这里取
const [realtimeMap, setRealtimeMap] = useState<any>({});
const [execute, setExecute] = useState(false); // 是否执行任务
const [taskList, setTaskList] = useState([]);
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 [proPoints, setProPoints] = useState({});//规划点
// 当前执行任务的机器人
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 mapRef = useRef(null);
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) => {
console.log('任务完成回调', data);
setExecute(false);
setShowVideo(false); // 任务完成关闭视频
utils.message.success(t('devices.fixed2TaskSuspended2Fixed2CompletedFixed2'));
setClearNavLine(clearNavLine + 1);
setSelectedRobotForExecute(null);
setSelectedRouteForExecute(null);
fetchTaskList();
fetchTaskHistoryList();
fetchTaskPoolList();
}
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);
}
};
// 根据任务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);
fetchRouteDetail(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);
// 实时数据统一从全量设备状态(mapPush)获取,不再订阅单机 WS 的实时数据
controller.onIsFinished = handleFinish;
controller.onIsExecuting = handleRreshTask;
// 处理路线点回调:taskId必须和当前选中任务一致
controller.onRoutePost = (msg) => {
console.log('收到 route_post 消息:', msg);
setProPoints(msg);
};
controller.deviceStatusChangedMsg = () => {
fetchAllData();
}
controller.init();
controllerRef.current = controller;
return () => {
if (controllerRef.current) {
controllerRef.current.destroy?.();
}
setExecute(false);
controllerRef.current = null;
};
}, []);
// 全量设备实时状态 WebSocket(mapPush):实时数据统一从这里获取,退出页面断开连接
useEffect(() => {
if (!userInfo || !stationId || !controllerRef.current) return;
const controller = controllerRef.current;
controller.siteId = stationId;
controller.initMapPush((rtMap: any) => setRealtimeMap(rtMap || {}));
return () => {
controller.closeMapPush?.();
};
}, [userInfo, stationId]);
// 监听任务列表和设备列表变化,自动连接正在执行中的机器人(仅在没有选择任务时)
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', "NEW"].includes(item.taskStaus));
if (!runningTask) return;
//fetchRouteDetail(runningTask?.id);
// 找到对应的设备
const targetDevice = devices.find(d => d.serialNumber === runningTask.deviceId);
setRunningPoolTask(runningTask); // 设置当前任务
setExecute(true);
setExecutingRobot(targetDevice);
setClearNavLine(v => v + 1); // 清空地图轨迹
if (!targetDevice) return;
// 判断是否是新设备(避免重复连接)
const isSameDevice = executingRobot?.serialNumber === targetDevice.serialNumber;
if (isSameDevice) return;
// 切换设备
console.log('🔄 检测到正在执行任务的新设备,开始连接', targetDevice);
//if (isFirstConnect.current) { isFirstConnect.current = false; controllerRef.current?.connectDevice(targetDevice); } else { controllerRef.current?.switchDevice(targetDevice); }
//setShowVideo(true);
}, [devices, taskPoolList, executingRobot, runningPoolTask]);
useEffect(() => {
if (!runningPoolTask?.id || !proPoints?.taskId) return;
if (runningPoolTask?.id == proPoints?.taskId) {
setPoints(proPoints?.data || []);
}
}, [runningPoolTask, proPoints]);
// 从全量设备状态(mapPush)里匹配当前设备
const pickRealtimeRecord = (rtMap: any, d: any) => {
for (const key of [d?.serialNumber, d?.device_sn, d?.sn, d?.id, d?.deviceId]) {
if (key == null || String(key) === '') continue;
const hit = rtMap?.[String(key)];
if (hit) return hit;
}
return null;
};
// 全量数据字段名归一(lng/lat/fixStatus → 页面原有字段名)
const robotRealtimePosition = useMemo(() => {
const rt = pickRealtimeRecord(realtimeMap, executingRobot);
if (!rt) return null;
return {
...rt,
longitude: rt.lng,
latitude: rt.lat,
fix_status: rt.fixStatus,
controlMode: rt.controlMode ?? executingRobot?.lastRunningStatus?.controlMode,
};
}, [realtimeMap, executingRobot]);
const realtimePosition = useMemo(() => {
let lon = robotRealtimePosition?.longitude;
let lat = robotRealtimePosition?.latitude;
// 没有实时经纬度时,从 lastRunningStatus 兜底
if ((!lon || !lat) && executingRobot?.lastRunningStatus) {
lon = lon || executingRobot.lastRunningStatus.longitude;
lat = lat || executingRobot.lastRunningStatus.latitude;
}
// 最终兜底:设备静态经纬度
if ((!lon || !lat) && executingRobot) {
lon = lon || executingRobot.longitude;
lat = lat || executingRobot.latitude;
}
if (!lon || !lat || isNaN(Number(lon)) || isNaN(Number(lat))) return null;
return {
lon: Number(lon),
lat: Number(lat),
heading: robotRealtimePosition?.yaw || executingRobot?.lastRunningStatus?.heading || 0,
};
}, [robotRealtimePosition, executingRobot])
// 寻车:地图定位到当前机器人位置
const handleFindCar = () => {
// 1. 优先使用实时经纬度(WebSocket 推送)
let lon = robotRealtimePosition?.longitude;
let lat = robotRealtimePosition?.latitude;
console.log('实时经纬度:', lon, lat, executingRobot);
// 2. 没有实时经纬度时,从 lastRunningStatus 兜底
if ((!lon || !lat) && (executingRobot?.lastRunningStatus || executingRobot?.locationMessage)) {
lon = lon || executingRobot?.lastRunningStatus?.longitude || executingRobot?.locationMessage?.longitude;
lat = lat || executingRobot?.lastRunningStatus?.latitude || executingRobot?.locationMessage?.latitude;
}
// 3. 最终兜底:设备静态经纬度
if ((!lon || !lat) && executingRobot) {
lon = lon || executingRobot.longitude;
lat = lat || executingRobot.latitude;
}
if (lon && lat && !isNaN(Number(lon)) && !isNaN(Number(lat))) {
mapRef.current?.flyToPosition?.({ lon: Number(lon), lat: Number(lat) }, { height: 300 });
} else {
messageApi.warning(t('devices.locationUnknown'));
}
};
//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])
// 获取数据
useEffect(() => {
fetchAllData();
}, [userInfo, stationId]);
// 新建任务弹窗状态
const [selectedPathWork, setSelectedPathWork] = useState(null);
// ===== 任务下发表单 =====
const [dispatchSource, setDispatchSource] = useState('route'); // 'route' | 'region'
const [dispatchMode, setDispatchMode] = useState('immediate'); // 'immediate' | 'scheduled'
const [dispatchRouteIds, setDispatchRouteIds] = useState([]);
const [dispatchRegionIds, setDispatchRegionIds] = useState([]);
const [dispatchRobotSns, setDispatchRobotSns] = useState([]);
const [regionList, setRegionList] = useState([]);
const [scheduleForm, setScheduleForm] = useState({
planName: '',
repeatPlan: 'DAY',
days: [],
dayOfMonth: [],
startDate: '',
dayTime: '',
excuteDayStartTime: '',
excuteDayEndTime: '',
});
const [dispatchLoading, setDispatchLoading] = useState(false);
const [focusOverlay, setFocusOverlay] = useState(null); // 地图定位:region / route
// 获取任务列表数据
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 {
if (!selectedPathWork?.id) return;
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) => {
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' || runningPoolTask.taskStaus == 'NEW') && !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 handleRreshTask = (data) => {
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
}
// 获取任务池数据
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: 180,
},
{
title: t('devices.taskId'),
dataIndex: 'id',
},
{
title: t('devices.routeId'),
dataIndex: 'routeId',
},
{
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'
}];
// ===== 分区列表 =====
useEffect(() => {
if (!stationId) return;
getStationRegions({ siteId: stationId }).then((res) => {
const list = Array.isArray(res?.data) ? res.data : (Array.isArray(res?.rows) ? res.rows : []);
setRegionList(list);
}).catch(() => setRegionList([]));
}, [stationId]);
// 按区域下发时,选中区域的可用路径(bindRoutes + defaultRoute)
const regionAvailableRoutes = useMemo(() => {
const ids = new Set();
regionList.forEach(r => {
if (dispatchRegionIds.includes(r.id)) {
(r.bindRoutes || []).forEach(id => ids.add(Number(id)));
if (r.defaultRoute != null) ids.add(Number(r.defaultRoute));
}
});
return (routeList || []).filter(rt => ids.has(rt.id));
}, [regionList, dispatchRegionIds, routeList]);
// 定时任务:周/月选项
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 toggleScheduleDay = (val) => {
setScheduleForm(prev => {
const exists = prev.days.includes(val);
return { ...prev, days: exists ? prev.days.filter(d => d !== val) : [...prev.days, val] };
});
};
const toggleScheduleMonthDay = (val) => {
setScheduleForm(prev => {
const exists = prev.dayOfMonth.includes(val);
return { ...prev, dayOfMonth: exists ? prev.dayOfMonth.filter(d => d !== val) : [...prev.dayOfMonth, val] };
});
};
const handleScheduleDateChange = (date) => {
if (!date) return;
setScheduleForm(prev => ({ ...prev, startDate: date.format('YYYY-MM-DD') }));
};
const handleScheduleTimeChange = (time, timeString) => {
setScheduleForm(prev => ({ ...prev, dayTime: timeString }));
};
const handleScheduleCycleChange = (dates) => {
if (!dates || !dates[0] || !dates[1]) return;
setScheduleForm(prev => ({
...prev,
excuteDayStartTime: dates[0].format('YYYY-MM-DD HH:mm:ss'),
excuteDayEndTime: dates[1].format('YYYY-MM-DD HH:mm:ss'),
}));
};
// 选中路线后飞到该路线并绘制
const flyToRoute = async (routeId) => {
try {
const res = await getSingleRouteList({ id: routeId });
const pathPoints = res?.data?.jsonData?.path || [];
if (pathPoints.length > 0) {
setPolyPoints(pathPoints);
setRouteMode(res?.data?.mode || '');
setFocusOverlay({ type: 'route', points: pathPoints, nonce: Date.now() });
}
} catch (e) {
console.error('flyToRoute error', e);
}
};
// 取消:清空所有选择
const handleDispatchCancel = () => {
setDispatchRouteIds([]);
setDispatchRegionIds([]);
setDispatchRobotSns([]);
setDispatchMode('immediate');
setScheduleForm({
planName: '',
repeatPlan: 'DAY',
days: [],
dayOfMonth: [],
startDate: '',
dayTime: '',
excuteDayStartTime: '',
excuteDayEndTime: '',
});
setPolyPoints([]);
setRouteMode('');
setFocusOverlay(null);
};
// 统一下发任务(立即执行 / 定时任务)
const handleDispatchTask = async () => {
if (dispatchRouteIds.length === 0) {
utils.message.warning(t('devices.taskSelectRouteFirst'));
return;
}
if (dispatchRobotSns.length === 0) {
utils.message.warning(t('devices.taskSelectRobotFirst'));
return;
}
setDispatchLoading(true);
try {
const routes = (routeList || []).filter(r => dispatchRouteIds.includes(r.id));
console.log('routes', routes);
if (dispatchMode === 'immediate' && dispatchSource == "route") {
let success = true;
for (const route of routes) {
const params = {
deviceId: dispatchRobotSns,
routeId: route.id,
siteId: stationId,
...(currentStation?.orgId ? { orgId: currentStation.orgId } : {}),
};
console.log('params', params);
const res = await excuetRoute(params);
if (res.code !== 200) {
success = false;
utils.message.error(`${route.workName || route.name} - ${res.msg || t('devices.createFailed')}`);
}
}
if (success) {
utils.message.success(t('devices.taskExecSuccess'));
const firstRobot = devices?.find(d => d.serialNumber === dispatchRobotSns);
if (firstRobot) {
setExecute(true);
setExecutingRobot(firstRobot);
setClearNavLine(v => v + 1);
}
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
}
} else if (dispatchMode === 'immediate' && dispatchSource == "region") {
const data = {
deviceId: dispatchRobotSns,
siteId: stationId,
orgId: currentStation?.orgId,
regionId: dispatchRegionIds,
routeId: dispatchRouteIds,
}
createRegionDefaultTask(data)
} else {
if (!scheduleForm.planName) {
utils.message.error(t('devices.fillRequired', { fields: t('devices.taskName') }));
setDispatchLoading(false);
return;
}
if (!scheduleForm.dayTime) {
utils.message.error(t('devices.selectExecTime'));
setDispatchLoading(false);
return;
}
let rules = { type: scheduleForm.repeatPlan };
if (scheduleForm.repeatPlan === 'DAY') {
rules = { type: 'DAY', startDate: scheduleForm.startDate, dayTime: scheduleForm.dayTime };
} else if (scheduleForm.repeatPlan === 'WEEK') {
rules = { type: 'WEEK', days: scheduleForm.days, dayTime: scheduleForm.dayTime };
} else if (scheduleForm.repeatPlan === 'MONTH') {
rules = { type: 'MONTH', dayOfMonth: scheduleForm.dayOfMonth, dayTime: scheduleForm.dayTime };
}
let success = true;
for (const route of routes) {
const finalData = {
deviceId: dispatchRobotSns,
orgId: currentStation?.orgId,
planName: scheduleForm.planName,
routeId: route.id,
siteId: stationId,
deviceTaskPlanRules: rules,
startTime: scheduleForm.excuteDayStartTime,
finishTime: scheduleForm.excuteDayEndTime,
};
const res = await saveDeviceTask(finalData);
if (res.code !== 200) {
success = false;
utils.message.error(res.msg || t('devices.createFailed'));
}
}
if (success) {
utils.message.success(t('devices.taskCreated'));
fetchTaskList();
fetchTaskPoolList();
fetchTaskHistoryList();
}
}
} catch (err) {
console.error('下发任务失败:', err);
utils.message.error(t('devices.createTaskError'));
} finally {
setDispatchLoading(false);
}
};
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: 320, background: '#e8f3ff center/cover', position: 'relative' }}>
<CesiumMap
ref={mapRef}
//devices={devices}
devices={devices.filter(d => d.onlineStatus == 1)}
points={points}
focusLocation={focusLocation}
realtimePosition={realtimePosition}
polyPoints={polyPoints}
realtimeType="robot"
from="devicesTask"
drawRealTime={execute}
ifClearNavLine={clearNavLine}
ifClearPlanLine={clearPlanLine}
routeMode={routeMode}
mapLayer={currentStation?.siteMapName}
focusOverlay={focusOverlay}
/>
{/* 机器人状态悬浮面板 - 绝对定位左上角 */}
{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>
{/* 寻车按钮 */}
<span
onClick={handleFindCar}
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
padding: '3px 10px',
background: 'rgba(255,255,255,0.18)',
borderRadius: 12,
cursor: 'pointer',
whiteSpace: 'nowrap',
transition: 'background 0.2s',
}}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.3)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.18)'}
>
<EnvironmentOutlined />
{t('devices.features.findCar')}
</span>
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
padding: '3px 10px',
background: 'rgba(255,255,255,0.18)',
borderRadius: 12,
cursor: 'pointer',
whiteSpace: 'nowrap',
transition: 'background 0.2s',
}}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.3)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.18)'}
>
<EnvironmentOutlined />
{t('devices.features.autoback')}
</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} style={{ display: "flex", flexDirection: "column" }}>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>{t('devices.taskDispatch')}</span>}
variant="borderless"
styles={{ body: { padding: '12px', overflow: 'auto', flex: 1 } }}
style={{ ...cardStyle, flex: 1, display: 'flex', flexDirection: 'column' }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, flex: 1, height: "100%", justifyContent: "space-between" }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxHeight: 560, overflow: "auto" }}>
<div>
<div style={{ fontWeight: 600, marginBottom: 8 }}>{t('devices.dispatchSource')}</div>
<Radio.Group value={dispatchSource} onChange={e => { setDispatchSource(e.target.value); setDispatchRouteIds([]); setDispatchRegionIds([]); }} buttonStyle="solid">
<Radio.Button value="route">{t('devices.dispatchByRoute')}</Radio.Button>
<Radio.Button value="region">{t('devices.dispatchByRegion')}</Radio.Button>
</Radio.Group>
</div>
{/* 按路线下发:路径多选 */}
{dispatchSource === 'route' && (
<div>
<div style={{ fontWeight: 600, marginBottom: 8 }}>{t('devices.taskSelectRoute')}</div>
{(!routeList || routeList.length === 0) ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{routeList.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] : []);
if (nextChecked) {
flyToRoute(route.id);
}
}}
>
<span
style={{
color: '#344054',
display: 'inline-block',
}}
>
{route.workName || route.name}
</span>
</Tag.CheckableTag>
);
})}
</div>
)}
</div>
)}
{/* 按区域下发:区域多选 + 区域内路径多选 */}
{dispatchSource === 'region' && (
<>
<div>
<div style={{ fontWeight: 600, marginBottom: 8 }}>{t('devices.taskSelectRegion')}</div>
{regionList.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
<div
style={{
width: '100%',
boxSizing: 'border-box',
padding: 8,
border: '1px solid #e6edf5',
borderRadius: 6,
background: '#fff',
}}
>
{/* 顶部提示文字 */}
<div
style={{
marginBottom: 8,
color: '#8c9bb0',
fontSize: 11,
lineHeight: '16px',
}}
>
选择需要执行任务的区域(可多选)
</div>
{/* 区域选择卡片 */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gap: 8,
width: '100%',
}}
>
{regionList.map((region, index) => {
const checked = dispatchRegionIds.includes(region.id);
const regionColors = [
'#1677ff',
'#13b879',
'#fa8c16',
'#a33cff',
];
const color = regionColors[index % regionColors.length];
return (
<Tag.CheckableTag
key={region.id}
checked={checked}
onChange={nextChecked => {
setDispatchRegionIds(prev => {
const next = nextChecked
? [...prev, region.id]
: prev.filter(id => id !== region.id);
// 默认仅选中已选区域的 defaultRoute
const defaultIds = new Set<number>();
regionList.forEach(r => {
if (next.includes(r.id) && r.defaultRoute != null) {
defaultIds.add(Number(r.defaultRoute));
}
});
setDispatchRouteIds(Array.from(defaultIds));
return next;
});
if (nextChecked) {
setFocusOverlay({
type: 'region',
id: region.id,
nonce: Date.now(),
});
}
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 2,
width: '100%',
minWidth: 0,
height: 40,
boxSizing: 'border-box',
margin: 0,
padding: '0 8px',
border: '1px solid #dce5f2',
borderRadius: 4,
background: '#fff',
color: '#344054',
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={{
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{region.regionName}
</span>
</Tag.CheckableTag>
);
})}
</div>
</div>
)}
</div>
{dispatchRegionIds.length > 0 && (
<div>
<div style={{ fontWeight: 600, marginBottom: 8 }}>{t('devices.taskSelectRoute')}</div>
{regionAvailableRoutes.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('devices.noRouteInRegion')} />
) : (
<div
style={{
width: '100%',
boxSizing: 'border-box',
padding: 8,
border: '1px solid #e6edf5',
borderRadius: 6,
background: '#fff',
}}
>
{/* 顶部提示文字 */}
<div
style={{
marginBottom: 8,
color: '#8c9bb0',
fontSize: 11,
lineHeight: '16px',
}}
>
选择需要执行任务的路线(可多选)
</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) {
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>
<div style={{ fontWeight: 600, marginBottom: 8 }}>{t('devices.taskDispatchMode')}</div>
<Radio.Group value={dispatchMode} onChange={e => setDispatchMode(e.target.value)} buttonStyle="solid">
<Radio.Button value="immediate">{t('devices.taskModeImmediate')}</Radio.Button>
<Radio.Button value="scheduled">{t('devices.taskModeScheduled')}</Radio.Button>
</Radio.Group>
</div>*/}
<TaskTypeRadio value={dispatchMode} onChange={setDispatchMode} />
{/* 定时任务表单 */}
{dispatchMode === 'scheduled' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 12, background: '#F7F8FA', borderRadius: 8 }}>
<div>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{t('devices.taskName')}</label>
<Input value={scheduleForm.planName} onChange={e => setScheduleForm(prev => ({ ...prev, planName: e.target.value }))} placeholder={t('devices.inputTaskName')} />
</div>
<div>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{t('devices.repeatPlan')}</label>
<Select value={scheduleForm.repeatPlan} onChange={val => setScheduleForm(prev => ({ ...prev, repeatPlan: val }))} style={{ width: '100%' }}>
<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>
{scheduleForm.repeatPlan === 'WEEK' && (
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{weekDays.map(day => (
<Button key={day.value} type={scheduleForm.days.includes(day.value) ? 'primary' : 'default'} onClick={() => toggleScheduleDay(day.value)} size="small" shape="circle">{day.label}</Button>
))}
</div>
)}
{scheduleForm.repeatPlan === 'MONTH' && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4 }}>
{monthDays.map(day => (
<Button key={day} type={scheduleForm.dayOfMonth.includes(day) ? 'primary' : 'default'} onClick={() => toggleScheduleMonthDay(day)} size="small">{day}</Button>
))}
</div>
)}
{scheduleForm.repeatPlan === 'DAY' && (
<div>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{t('devices.execDate')}</label>
<DatePicker onChange={handleScheduleDateChange} style={{ width: '100%' }} placeholder={t('devices.selectExecDate')} />
</div>
)}
<div>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{t('devices.execTime')}</label>
<TimePicker format="HH:mm:ss" onChange={handleScheduleTimeChange} style={{ width: '100%' }} placeholder={t('devices.selectExecTime')} />
</div>
<div>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{t('devices.executionCycle')}</label>
<RangePicker onChange={handleScheduleCycleChange} style={{ width: '100%' }} showTime format="YYYY-MM-DD HH:mm:ss" placeholder={[t('devices.startDate'), t('devices.endDate')]} />
</div>
</div>
)}
{/* 选择执行机器人 */}
<div>
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{t('devices.taskSelectRobot')}</label>
<Select
placeholder={t('devices.taskSelectRobot')}
value={dispatchRobotSns}
onChange={setDispatchRobotSns}
style={{ width: '100%' }}
options={(devices || []).map(item => ({ value: item.serialNumber, label: `${item.deviceAlias || item.deviceName} (${item.serialNumber})` }))}
/>
</div>
</div>
{/* 下发方式:按路线 / 按区域 */}
{/* 下发按钮 + 取消按钮 */}
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1 }}>
</div>
<Button onClick={handleDispatchCancel}>{t('common.cancel')}</Button>
<Button type="primary" style={{ flex: 1 }} loading={dispatchLoading} onClick={handleDispatchTask}>
{dispatchMode === 'immediate' ? t('devices.taskExecuteNow') : t('devices.taskSaveSchedule')}
</Button>
</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>
{/*{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 >
);
}