2204 lines
67 KiB
TypeScript
2204 lines
67 KiB
TypeScript
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>
|
||
);
|
||
};
|