setActiveType('routeGen')}
diff --git a/src/components/devices/RobotTaskPage.tsx b/src/components/devices/RobotTaskPage.tsx
index 3915eda..b84fba2 100644
--- a/src/components/devices/RobotTaskPage.tsx
+++ b/src/components/devices/RobotTaskPage.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef, useMemo } from 'react';
+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,
@@ -16,13 +16,15 @@ import {
CloseOutlined,
ReloadOutlined,
ExclamationCircleOutlined,
- EnvironmentOutlined
+ 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,
@@ -39,6 +41,7 @@ import {
getRouteByTask,
deleteRoute,
getTaskPath,
+ createRegionDefaultTask,
} from '../../api/device';
import dayjs from 'dayjs';
import DraggableBox from '../Draggable';
@@ -53,6 +56,7 @@ 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;
@@ -472,8 +476,26 @@ export default function RobotTaskPage() {
}, [userInfo, stationId]);
// 新建任务弹窗状态
- const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
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 () => {
@@ -1204,6 +1226,214 @@ export default function RobotTaskPage() {
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 sn of dispatchRobotSns) {
+ for (const route of routes) {
+ const finalData = {
+ deviceId: sn,
+ 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 (
@@ -1320,7 +1550,7 @@ export default function RobotTaskPage() {
styles={{ body: { padding: 0, position: 'relative' } }}
style={{ ...cardStyle, marginBottom: 10 }}
>
-
+
{/* 机器人状态悬浮面板 - 绝对定位左上角 */}
@@ -1672,135 +1903,449 @@ export default function RobotTaskPage() {
-
+
- {t('devices.presetRoutes')}
- setRouteSearch(e.target.value)}
- />
-
- }
+ title={
{t('devices.taskDispatch')}}
variant="borderless"
- styles={{ body: { padding: '12px', height: 732, overflow: 'auto' } }}
- style={cardStyle}
+ styles={{ body: { padding: '12px', overflow: 'auto', flex: 1 } }}
+ style={{ ...cardStyle, flex: 1, display: 'flex', flexDirection: 'column' }}
>
-
- {
- // 判断有无数据
- (!routeList || routeList.length === 0) ? (
-
-
+
+
+
+
{t('devices.dispatchSource')}
+
{ setDispatchSource(e.target.value); setDispatchRouteIds([]); setDispatchRegionIds([]); }} buttonStyle="solid">
+ {t('devices.dispatchByRoute')}
+ {t('devices.dispatchByRegion')}
+
+
+
+ {/* 按路线下发:路径多选 */}
+ {dispatchSource === 'route' && (
+
+
{t('devices.taskSelectRoute')}
+ {(!routeList || routeList.length === 0) ? (
+
+ ) : (
+
+ {routeList.map(route => {
+ const checked = dispatchRouteIds.includes(route.id);
+
+ return (
+ {
+ // 按路线下发:单选
+ setDispatchRouteIds(nextChecked ? [route.id] : []);
+
+ if (nextChecked) {
+ flyToRoute(route.id);
+ }
+ }}
+ >
+
+ {route.workName || route.name}
+
+
+ );
+ })}
+
+ )}
- ) : (
- routeList.map(route => (
-
{
- 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';
- }
- }}
- >
-
- {route.imgUrl ? (
-

- ) : (
-
+
+
{t('devices.taskSelectRegion')}
+ {regionList.length === 0 ? (
+
+ ) : (
+
-
+ boxSizing: 'border-box',
+ padding: 8,
+ border: '1px solid #e6edf5',
+ borderRadius: 6,
+ background: '#fff',
+ }}
+ >
+ {/* 顶部提示文字 */}
+
+ 选择需要执行任务的区域(可多选)
+
+
+ {/* 区域选择卡片 */}
+
+ {regionList.map((region, index) => {
+ const checked = dispatchRegionIds.includes(region.id);
+
+ const regionColors = [
+ '#1677ff',
+ '#13b879',
+ '#fa8c16',
+ '#a33cff',
+ ];
+
+ const color = regionColors[index % regionColors.length];
+
+ return (
+ {
+ setDispatchRegionIds(prev => {
+ const next = nextChecked
+ ? [...prev, region.id]
+ : prev.filter(id => id !== region.id);
+
+ // 默认仅选中已选区域的 defaultRoute
+ const defaultIds = new Set();
+
+ 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',
+ }}
+ >
+ {/* 复选框 */}
+
+ {checked ? '✓' : ''}
+
+
+ {/* 区域定位图标 */}
+
+
+ {/* 区域名称 */}
+
+ {region.regionName}
+
+
+ );
+ })}
+
+
+ )}
+
+ {dispatchRegionIds.length > 0 && (
+
+
{t('devices.taskSelectRoute')}
+ {regionAvailableRoutes.length === 0 ? (
+
+ ) : (
+
+ {/* 顶部提示文字 */}
+
+ 选择需要执行任务的路线(可多选)
+
+
+ {/* 路线选择卡片 */}
+
+ {regionAvailableRoutes.map((route, index) => {
+ const checked = dispatchRouteIds.includes(route.id);
+
+ const regionColors = [
+ '#1677ff',
+ '#13b879',
+ '#fa8c16',
+ '#a33cff',
+ ];
+
+ const color = regionColors[index % regionColors.length];
+
+ return (
+ {
+ setDispatchRouteIds(prev =>
+ nextChecked
+ ? [...prev, route.id]
+ : prev.filter(id => id !== route.id)
+ );
+
+ if (nextChecked) {
+ flyToRoute(route.id);
+ }
+ }}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: 5,
+ width: '100%',
+ minWidth: 0,
+ height: 40,
+ boxSizing: 'border-box',
+ margin: 0,
+ padding: '0 8px',
+ color: '#344054',
+ background: '#fff',
+ border: '1px solid #dce5f2',
+ borderRadius: 4,
+ fontSize: 12,
+ lineHeight: 'normal',
+ cursor: 'pointer',
+ overflow: 'hidden',
+ }}
+ >
+ {/* 自定义复选框 */}
+
+ {checked ? '✓' : ''}
+
+
+ {/* 路线定位图标 */}
+
+
+ {/* 路线名称 */}
+
+ {route.workName || route.name}
+
+
+ );
+ })}
+
)}
- {/* 路线信息 */}
-
-
- {route.workName || route.name || t('devices.unnamedRoute')}
-
-
- {t('devices.rtMode')}: {route.mode == "coverage" ? t('devices.coverageMode') : t('devices.singleMode')}
-
+ )}
+ >
+ )}
+ {/* 任务方式:立即执行 / 定时任务 */}
+ {/*
+
{t('devices.taskDispatchMode')}
+
setDispatchMode(e.target.value)} buttonStyle="solid">
+ {t('devices.taskModeImmediate')}
+ {t('devices.taskModeScheduled')}
+
+
*/}
-
-
- }
- onClick={(e) => {
- e.stopPropagation();
- setSelectedPathWork(route);
- setIsTaskModalOpen(true);
- }}
- />
-
- {/* 执行按钮 */}
-
- }
- onClick={(e) => {
- e.stopPropagation();
- setSelectedPathWork(route);
- handleTaskexcuted(route);
- }}
- />
-
+
+
+ {/* 定时任务表单 */}
+ {dispatchMode === 'scheduled' && (
+
+
+
+ setScheduleForm(prev => ({ ...prev, planName: e.target.value }))} placeholder={t('devices.inputTaskName')} />
- ))
- )
- }
-
+
+
+
+
+ {scheduleForm.repeatPlan === 'WEEK' && (
+
+ {weekDays.map(day => (
+
+ ))}
+
+ )}
+ {scheduleForm.repeatPlan === 'MONTH' && (
+
+ {monthDays.map(day => (
+
+ ))}
+
+ )}
+ {scheduleForm.repeatPlan === 'DAY' && (
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ )}
+ {/* 选择执行机器人 */}
+
+
+
+
+ {/* 下发方式:按路线 / 按区域 */}
+
+
+ {/* 下发按钮 + 取消按钮 */}
+
+
+
+
+
+
+
+
+
@@ -1942,56 +2487,6 @@ export default function RobotTaskPage() {
- {/* 🔥 传递 routeList 给弹窗 */}
-
setIsTaskModalOpen(false)}
- robots={devices}
- selectedPathWork={selectedPathWork}
- handlGetTaskList={fetchTaskList}
- routeList={routeList}
- stationId={stationId}
- onTaskCreated={handleTaskCreated}
- userInfo={userInfo}
- />
-
- {/* 执行任务弹窗 */}
- setExecuteModalVisible(false)}
- onOk={handleExecuteConfirm}
- okText={t('devices.execute')}
- cancelText={t('common.cancel')}
- >
-
-
-
{t('devices.routeInfo')}
-
-
{t('devices.routeName')}{selectedRouteForExecute?.workName || selectedRouteForExecute?.name}
-
-
-
-
-
{t('workOrder.selectDevice')}
-
-
-
-
{/*{showVideo && executingRobot && execute && taskPoolList.length > 0 && (
);
}
-
-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]);
-
- // ====================== Tab / 返航任务 ======================
- const [activeTab, setActiveTab] = useState('scheduled');
- const [returnRoute, setReturnRoute] = useState(null);
- const [returnDevice, setReturnDevice] = useState(null);
-
- // 预设路径:仅保留单次任务路径
- const singleRouteList = useMemo(
- () => (routeList || []).filter(route => route.mode == 'single_point'),
- [routeList]
- );
-
- const handleSaveReturnTask = () => {
- // TODO: 返航任务保存接口待接入,暂时打印
- console.log('新建返航任务', { route: returnRoute, device: returnDevice });
- };
-
- // ====================== 周/月选择 ======================
- 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 (
-
-
-
- {/*
*/}
-
- {activeTab === 'scheduled' ? (
- <>
-
- {/* 任务名称 */}
-
-
- setFormData({ ...formData, planName: e.target.value })}
- placeholder={t('devices.inputTaskName')}
- />
-
-
- {/* 执行路线 */}
-
-
-
-
-
- {/* 选择设备 */}
-
-
-
-
-
- {/* 重复计划 */}
-
-
-
-
-
- {/* 周选择 */}
- {formData.repeatPlan === 'WEEK' && (
-
- {weekDays.map(day => (
-
- ))}
-
- )}
-
- {/* 月选择 */}
- {formData.repeatPlan === 'MONTH' && (
-
- {monthDays.map(day => (
-
- ))}
-
- )}
-
- {/* 按天:选择日期 */}
- {formData.repeatPlan === 'DAY' && (
- <>
-
-
- >
- )}
-
- {/* 时间 */}
-
-
-
- {/* 执行周期 */}
-
-
-
- >
- ) : (
-
- {/* 预设路径列表 */}
-
-
-
- {singleRouteList.length === 0 ? (
-
- ) : (
- singleRouteList.map(route => {
- const selected = returnRoute?.id === route.id;
- return (
-
setReturnRoute(route)}
- className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${selected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-blue-300'}`}
- >
-
- {route.imgUrl ? (
-

- ) : (
-
- )}
-
-
{route.workName || route.name}
- {selected &&
}
-
- );
- })
- )}
-
-
-
- {/* 选择机器 */}
-
-
-
-
- )}
-
- {/* 底部按钮 */}
-
-
-
-
-
-
- );
-};
diff --git a/src/components/taskRadio/TaskTypeRadio.css b/src/components/taskRadio/TaskTypeRadio.css
new file mode 100644
index 0000000..64ae59b
--- /dev/null
+++ b/src/components/taskRadio/TaskTypeRadio.css
@@ -0,0 +1,161 @@
+.task-type-radio {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.task-type-radio__title {
+ margin-bottom: 12px;
+ color: #000;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 22px;
+}
+
+.task-type-radio__group {
+ display: flex;
+ width: 100%;
+ gap: 12px;
+}
+
+.task-type-radio__option.ant-radio-wrapper {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ margin-inline-end: 0;
+ align-items: stretch;
+ color: inherit;
+ white-space: normal;
+}
+
+.task-type-radio__option > .ant-radio {
+ display: none;
+}
+
+.task-type-radio__option > span:last-child {
+ display: block;
+ flex: 1;
+ min-width: 0;
+}
+
+.task-type-radio__card {
+ position: relative;
+ display: flex;
+ width: 100%;
+ min-height: 62px;
+ padding: 10px;
+ box-sizing: border-box;
+ align-items: center;
+ gap: 10px;
+
+ background: #fff;
+ border: 1px solid #dce5f2;
+ border-radius: 6px;
+
+ cursor: pointer;
+ transition:
+ border-color 0.2s ease,
+ background-color 0.2s ease,
+ box-shadow 0.2s ease;
+}
+
+.task-type-radio__card:hover {
+ border-color: #91caff;
+}
+
+.task-type-radio__card--checked {
+ border: 1px solid #4096ff;
+ background: #f0f7ff;
+ box-shadow: 0 0 0 1px rgb(22 119 255 / 5%);
+}
+
+.task-type-radio__icon {
+ display: flex;
+ flex-shrink: 0;
+ width: 32px;
+ height: 32px;
+ align-items: center;
+ justify-content: center;
+
+ color: #1677ff;
+ background: #e6f4ff;
+ border-radius: 5px;
+ font-size: 20px;
+}
+
+.task-type-radio__content {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.task-type-radio__name {
+ margin-bottom: 4px;
+ overflow: hidden;
+
+ color: #262626;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 18px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.task-type-radio__description {
+ overflow: hidden;
+
+ color: #8c9bb0;
+ font-size: 11px;
+ font-weight: 400;
+ line-height: 15px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.task-type-radio__status {
+ display: flex;
+ flex-shrink: 0;
+ width: 14px;
+ height: 14px;
+ align-items: center;
+ justify-content: center;
+
+ color: #1677ff;
+ font-size: 12px;
+}
+
+.task-type-radio__empty {
+ display: block;
+ width: 12px;
+ height: 12px;
+ box-sizing: border-box;
+ border: 1px solid #cbd5e1;
+ border-radius: 50%;
+ background: #fff;
+}
+
+.task-type-radio__option:focus-visible
+ .task-type-radio__card {
+ outline: 2px solid #91caff;
+ outline-offset: 2px;
+}
+
+@media (max-width: 480px) {
+ .task-type-radio__group {
+ gap: 8px;
+ }
+
+ .task-type-radio__card {
+ padding: 8px;
+ gap: 7px;
+ }
+
+ .task-type-radio__icon {
+ width: 28px;
+ height: 28px;
+ font-size: 17px;
+ }
+
+ .task-type-radio__description {
+ white-space: normal;
+ }
+}
diff --git a/src/components/taskRadio/taskRadio.tsx b/src/components/taskRadio/taskRadio.tsx
new file mode 100644
index 0000000..518841c
--- /dev/null
+++ b/src/components/taskRadio/taskRadio.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { Radio } from 'antd';
+import {
+ LoginOutlined,
+ ClockCircleOutlined,
+ CheckCircleFilled,
+} from '@ant-design/icons';
+import './TaskTypeRadio.css'
+type DispatchMode = 'immediate' | 'scheduled';
+
+interface TaskTypeRadioProps {
+ value: DispatchMode;
+ onChange: (value: DispatchMode) => void;
+}
+
+const TaskTypeRadio: React.FC = ({
+ value,
+ onChange,
+}) => {
+ const options: {
+ value: DispatchMode;
+ title: string;
+ description: string;
+ icon: React.ReactNode;
+ }[] = [
+ {
+ value: 'immediate',
+ title: '立即执行任务',
+ description: '下发后机器人立即开始执行',
+ icon: ,
+ },
+ {
+ value: 'scheduled',
+ title: '定时任务',
+ description: '设置开始时间和重复周期',
+ icon: ,
+ },
+ ];
+
+ return (
+
+
+ 选择任务类型
+
+
+
onChange(e.target.value)}
+ className="task-type-radio__group"
+ >
+ {options.map((option) => {
+ const checked = value === option.value;
+
+ return (
+
+
+
+ {option.icon}
+
+
+
+
+ {option.title}
+
+
+
+ {option.description}
+
+
+
+
+ {checked ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default TaskTypeRadio;
diff --git a/src/locales/en/index.ts b/src/locales/en/index.ts
index 3e83100..a33c73d 100644
--- a/src/locales/en/index.ts
+++ b/src/locales/en/index.ts
@@ -960,6 +960,28 @@
repeatNone: 'Repeat None',
repeatTypeLabel: 'Repeat Type Label',
repeatWeekly: 'Repeat Weekly',
+ taskDispatch: 'Task Dispatch',
+ dispatchSource: 'Dispatch Source',
+ dispatchByRoute: 'By Route',
+ dispatchByRegion: 'By Region',
+ noRouteInRegion: 'No routes bound to this region',
+ taskSelectRegion: 'Select Task Region',
+ taskDispatchMode: 'Dispatch Mode',
+ taskModeImmediate: 'Immediate',
+ taskModeScheduled: 'Scheduled',
+ taskModeImmediateTip: 'Execute immediately after dispatch',
+ taskModeScheduledTip: 'Execute automatically by the configured schedule',
+ taskConfig: 'Task Configuration',
+ taskSelectRoute: 'Select Routes',
+ taskSelectRegionTip: 'Please select task regions first — bound routes will be linked automatically',
+ taskSelectRobot: 'Execute Robot',
+ taskScheduleSetting: 'Scheduled Task Settings',
+ taskExecuteNow: 'Dispatch',
+ taskSaveSchedule: 'Save Schedule',
+ taskSelectRegionFirst: 'Please select task region first',
+ taskSelectRouteFirst: 'The selected region has no bound routes',
+ taskSelectRobotFirst: 'Please select an execute robot',
+ taskDispatchResult: 'Dispatched. Success {success}, Failed {fail}',
requestControlPermission: 'Request Control Permission',
controlRequesting: 'Requesting...',
controlGranted: 'Control Granted',
@@ -2582,6 +2604,42 @@
workOrderCompletionRate: 'Work Order Completion Rate',
},
systemSetting: {
+ ruleConfig: 'Rule Config',
+ ruleName: 'Rule Name',
+ ruleType: 'Rule Type',
+ ruleTypeLowBatteryReturn: 'Low Battery Return',
+ deviceScope: 'Device Scope',
+ deviceScopeAll: 'All Devices',
+ deviceScopeGroup: 'Device Group',
+ deviceScopeSome: 'Specific Devices',
+ triggerKey: 'Trigger Field',
+ triggerKeyBattery: 'Battery',
+ triggerKeyTemperature: 'Temperature',
+ triggerKeyOfflineDuration: 'Offline Duration',
+ triggerKeyVoltage: 'Voltage',
+ triggerValue: 'Threshold',
+ actionType: 'Action Type',
+ actionTypeAutoTask: 'Auto Task',
+ actionTypeSendCommand: 'Send Command',
+ actionTypeAlarm: 'Alarm',
+ actionTypeWorkOrder: 'Work Order',
+ actionArgs: 'Action Args',
+ enabledLabel: 'Enabled',
+ forceExecute: 'Force Execute',
+ pleaseInputRuleName: 'Please input rule name',
+ pleaseSelectRuleType: 'Please select rule type',
+ pleaseSelectDeviceScope: 'Please select device scope',
+ pleaseSelectTriggerKey: 'Please select trigger field',
+ pleaseSelectOperator: 'Please select operator',
+ pleaseInputTriggerValue: 'Please input threshold',
+ pleaseSelectActionType: 'Please select action type',
+ ruleList: 'Rule List',
+ addRule: 'Add Rule',
+ editRule: 'Edit Rule',
+ viewRule: 'View Rule',
+ operator: 'Operator',
+ invalidJson: 'Invalid action args JSON',
+ confirmDeleteRule: 'Are you sure to delete rule "{{name}}"?',
flvPlaybackError: 'FLV playback error',
flvNetworkError: 'Network error while loading the stream',
mediaDecodeError: 'Media decode error',
diff --git a/src/locales/zh/index.ts b/src/locales/zh/index.ts
index 5ce0062..11d2eeb 100644
--- a/src/locales/zh/index.ts
+++ b/src/locales/zh/index.ts
@@ -902,6 +902,28 @@ const zh = {
repeatNone: '重复无',
repeatTypeLabel: '重复类型标签',
repeatWeekly: '重复每周',
+ taskDispatch: '任务下发',
+ dispatchSource: '下发方式',
+ dispatchByRoute: '按路线下发',
+ dispatchByRegion: '按区域下发',
+ noRouteInRegion: '该区域暂无绑定路径',
+ taskSelectRegion: '选择任务区域',
+ taskDispatchMode: '选择任务方式',
+ taskModeImmediate: '立即执行',
+ taskModeScheduled: '定时任务',
+ taskModeImmediateTip: '下发后立即开始执行',
+ taskModeScheduledTip: '按照设定的时间周期自动执行',
+ taskConfig: '任务配置',
+ taskSelectRoute: '选择路径',
+ taskSelectRegionTip: '请先选择任务区域,系统会自动关联已绑定的路径',
+ taskSelectRobot: '执行机器人',
+ taskScheduleSetting: '定时任务设置',
+ taskExecuteNow: '下发任务',
+ taskSaveSchedule: '保存定时任务',
+ taskSelectRegionFirst: '请先选择任务区域',
+ taskSelectRouteFirst: '所选区域未关联任何路径',
+ taskSelectRobotFirst: '请先选择执行机器人',
+ taskDispatchResult: '下发完成。成功 {success} 条,失败 {fail} 条',
requestControlPermission: '请求控制权限',
controlRequesting: '申请中...',
controlGranted: '已获得控制',
@@ -2644,6 +2666,42 @@ const zh = {
workOrderCompletionRate: '工作工单完成率',
},
systemSetting: {
+ ruleConfig: '规则配置',
+ ruleName: '规则名称',
+ ruleType: '规则类型',
+ ruleTypeLowBatteryReturn: '低电量返航',
+ deviceScope: '适用范围',
+ deviceScopeAll: '所有设备',
+ deviceScopeGroup: '设备组',
+ deviceScopeSome: '特定设备',
+ triggerKey: '触发字段',
+ triggerKeyBattery: '电量',
+ triggerKeyTemperature: '温度',
+ triggerKeyOfflineDuration: '离线时长',
+ triggerKeyVoltage: '电压',
+ triggerValue: '阈值',
+ actionType: '执行动作',
+ actionTypeAutoTask: '路径规划任务',
+ actionTypeSendCommand: '下发指令',
+ actionTypeAlarm: '告警',
+ actionTypeWorkOrder: '工单下发',
+ actionArgs: '动作参数',
+ enabledLabel: '是否启用',
+ forceExecute: '强制执行',
+ pleaseInputRuleName: '请输入规则名称',
+ pleaseSelectRuleType: '请选择规则类型',
+ pleaseSelectDeviceScope: '请选择适用范围',
+ pleaseSelectTriggerKey: '请选择触发字段',
+ pleaseSelectOperator: '请选择比较运算符',
+ pleaseInputTriggerValue: '请输入阈值',
+ pleaseSelectActionType: '请选择执行动作',
+ ruleList: '规则列表',
+ addRule: '新增规则',
+ editRule: '编辑规则',
+ viewRule: '查看规则',
+ operator: '比较运算符',
+ invalidJson: '动作参数 JSON 格式不正确',
+ confirmDeleteRule: '确定删除规则「{{name}}」吗?',
flvPlaybackError: 'FLV 播放出错',
flvNetworkError: '网络错误,加载失败',
mediaDecodeError: '媒体解码失败',