diff --git a/src/components/devices/DPadControl.tsx b/src/components/devices/DPadControl.tsx new file mode 100644 index 0000000..27aeac7 --- /dev/null +++ b/src/components/devices/DPadControl.tsx @@ -0,0 +1,157 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Circle } from 'lucide-react'; + +export interface GamepadState { + buttons: boolean[]; + axes: number[]; + connected: boolean; + ts?: number; +} + +interface DPadControlProps { + onGamepadUpdate?: (state: GamepadState) => void; + onSpeedChange?: (speed: number) => void; + disabled?: boolean; +} + +const DPadControl: React.FC = ({ onGamepadUpdate, onSpeedChange, disabled = false }) => { + const { t } = useTranslation(); + const [pressed, setPressed] = useState>({}); + const [speed, setSpeed] = useState(0.5); + const pressTimer = useRef(null); + // 用 ref 存储最新状态,避免 setInterval 闭包捕获旧值 + const pressedRef = useRef>({}); + const speedRef = useRef(0.5); + + // 构建状态:只传输 xy 坐标(axes),buttons 全部为 false + const buildState = useCallback( + (overrides: Record = {}): GamepadState => { + const btnMap = { ...pressedRef.current, ...overrides }; + + // 方向键映射到左摇杆 axes[0] X, axes[1] Y + let axisX = 0; + let axisY = 0; + const spd = speedRef.current; + if (btnMap.left) axisX -= spd; + if (btnMap.right) axisX += spd; + if (btnMap.up) axisY -= spd; + if (btnMap.down) axisY += spd; + + return { + buttons: new Array(17).fill(false), + axes: [axisX, axisY, 0, 0], + connected: true, + ts: Date.now(), + }; + }, + [], + ); + + const emit = useCallback( + (overrides?: Record) => { + if (disabled) return; + const state = overrides ? buildState(overrides) : buildState(); + onGamepadUpdate?.(state); + }, + [buildState, disabled, onGamepadUpdate], + ); + + const startPress = useCallback( + (key: string) => { + if (disabled) return; + pressedRef.current = { ...pressedRef.current, [key]: true }; + setPressed(pressedRef.current); + emit({ [key]: true }); + // 持续发送(模拟手柄按住) + if (pressTimer.current) window.clearInterval(pressTimer.current); + pressTimer.current = window.setInterval(() => { + emit(); + }, 100); + }, + [disabled, emit], + ); + + const endPress = useCallback( + (key: string) => { + if (disabled) return; + if (pressTimer.current) { + window.clearInterval(pressTimer.current); + pressTimer.current = null; + } + pressedRef.current = { ...pressedRef.current, [key]: false }; + setPressed(pressedRef.current); + emit({ [key]: false }); + }, + [disabled, emit], + ); + + const btnBase = + 'flex items-center justify-center w-8 h-8 rounded-xl border transition-all duration-150 select-none active:scale-95'; + const btnActive = 'border-cyan-400 bg-cyan-500/30 text-cyan-300 shadow-[0_0_16px_rgba(34,211,238,0.4)]'; + const btnIdle = 'border-slate-600 bg-slate-800/60 text-slate-300 hover:border-cyan-500/60 hover:text-cyan-300'; + + const renderBtn = (key: string, icon: React.ReactNode, ariaLabel: string) => ( + + ); + + return ( +
+ {/* 十字方向键 */} +
+
+ {renderBtn('up', , t('devices.dpad.up'))} +
+ {renderBtn('left', , t('devices.dpad.left'))} + {renderBtn('center', , t('devices.dpad.center'))} + {renderBtn('right', , t('devices.dpad.right'))} +
+ {renderBtn('down', , t('devices.dpad.down'))} +
+
+ + {/* 速度滑块 */} +
+
+ {t('devices.dpad.speed')} + {Math.round(speed * 100)}% +
+ { + const v = parseFloat(e.target.value); + setSpeed(v); + speedRef.current = v; + onSpeedChange?.(v); + }} + className="w-full h-2.5 rounded-full appearance-none cursor-pointer bg-slate-700 accent-cyan-400" + style={{ + background: `linear-gradient(to right, #22d3ee 0%, #22d3ee ${speed * 100}%, #334155 ${speed * 100}%, #334155 100%)`, + }} + /> +
+ 0% + 50% + 100% +
+
+
+ ); +}; + +export default DPadControl; diff --git a/src/components/devices/DeviceControl.tsx b/src/components/devices/DeviceControl.tsx deleted file mode 100644 index 067f63a..0000000 --- a/src/components/devices/DeviceControl.tsx +++ /dev/null @@ -1,2417 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { - Power, RefreshCw, Play, Pause, Settings, - MapPin, Navigation, Video, Wifi, AlertTriangle, - RotateCcw, RotateCw, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, - ChevronLeft, Plus, Trash2, Save, Send, Target, Map, Layers, Box, - Battery, Signal, Square, - Activity, BarChart3, Cpu, - Eye, - X -} from 'lucide-react'; -import XboxController from './XboxController'; -import RobotFeatureButtons from './RobotFeatureButtons'; -import DPadControl from './DPadControl'; -import { getControlRight, getFlightTaskDetail, getJurisdiction, getLiveStream, getPlaneDeviceList, getsinglePlan, getsinglePlanTrack, getUAVState, getWayLine, getWorkData, getWorkList, postFlightCommmand, saveDeviceTask, updateFlightTaskStatus } from '@/api/api'; -import { message as antdMessage, Button, DatePicker, Input, InputNumber, Modal, Select, Tooltip } from 'antd'; - -import CesiumMap from './Map'; -import { MOCK_DEVICES, OBSTACLE_TYPE } from '../const'; -import { useWebRTCPlayer } from "./useWebRTCStream"; -import VideoPlayer from "./VideoPlayer.tsx"; -import { wsManager, WSStatus } from "@/api/websocket/WebSocketManager.ts"; -import Cookies from "js-cookie"; -import DroneLivePlayer from './VolcRtcPlayer.jsx'; -import { useTranslation } from 'react-i18next'; -antdMessage.config({ - top: 100, - -}); - -interface Robot { - id: string; - name: string; - status: 'online' | 'offline' | 'maintenance'; - mode: 'standby' | 'working' | 'maintaining'; - area: string; - battery: number; - signal: 'connected' | 'disconnected' | 'pending'; - mqttTopic: string; - position: { lon: string; lat: string }; - speed: number; - angularSpeed: number; - gpsPlan: string; - taskStatus: string; - remoteStatus: 'SAFE' | 'CONTROL'; - mqttLink: 'WAIT' | 'CONNECTED' | 'DISCONNECTED'; - type: 'robot' | 'plane' | 'mower'; - gateway_sn?: string; - deviceAlias?: string; - capacity_percent?: number; - onlineStatus?: string | number; - lastRunningStatus?: { - latitude: string | number; - longitude: string | number; - battery: string | number; - }; - locationMessage?: { - latitude: string | number; - longitude: string | number; - battery: string | number; - - }; - - drone_callsign?: string; - serialNumber?: string; -} - -interface Waypoint { - id: number; - lon: string; - lat: string; - alt?: number; -} - -const INITIAL_WAYPOINTS: Waypoint[] = []; - -const StatusTagMap = { - working: { text: t('devices.s59'), color: 'bg-[#00e5ff]/10 text-[#00e5ff] border-[#00e5ff]/30' }, - online: { text: t('devices.online'), color: 'bg-[#00e5ff]/10 text-[#00e5ff] border-[#00e5ff]/30' }, - offline: { text: t('devices.offline'), color: 'bg-[#ff4d4f]/10 text-[#ff4d4f] border-[#ff4d4f]/30' }, - maintenance: { text: t('workOrder.maintainTask'), color: 'bg-[#ffad0d]/10 text-[#ffad0d] border-[#ffad0d]/30' }, - configured: { text: t('devices.fixed2Video'), color: 'bg-[#00e5ff]/10 text-[#00e5ff] border-[#00e5ff]/30' }, - no_video: { text: t('devices.noVideo'), color: 'bg-[#6b7280]/10 text-[#9ca3af] border-[#6b7280]/30' }, - failed: { text: t('devices.connectionFailed'), color: 'bg-[#ff4d4f]/10 text-[#ff4d4f] border-[#ff4d4f]/30' } -}; - -const RobotControlCenter = ({ setView }) => { - const { t } = useTranslation(); - const FILTERED_MOCKS = MOCK_DEVICES.filter( - item => item.type === 'robot' || item.type === 'mower' || item.type === 'cut' - ); - - const [selectedDevice, setSelectedDevice] = useState(FILTERED_MOCKS[0]); - const [isRemoteEnabled, setIsRemoteEnabled] = useState(false); - - const [isControlled, setIsControlled] = useState(false); - const [selectedRoute, setSelectedRoute] = useState(null); // ✅ 新增 - const [activeTab, setActiveTab] = useState<'remote' | 'gps' | 'vizanti'>('remote'); - const [waypoints, setWaypoints] = useState(INITIAL_WAYPOINTS); - const [isConnecting, setIsConnecting] = useState(false); // 连接中状态 - const [newWaypoint, setNewWaypoint] = useState({ id: 0, lon: '', lat: '' }); - const [taskIssued, setTaskIssued] = useState(false); - const [taskRunning, setTaskRunning] = useState(false); - const [points, setPoints] = useState([]); - const [deviceRoute, setDeviceRoute] = useState([]) - const mapClearRef = useRef(null); - - const controlRef = useRef(null); - const [robotRoutes, setRobotRoutes] = useState([]); - const [planeDevice, setPlaneDevice] = useState([]); - const [allDevices, setAllDevices] = useState([...FILTERED_MOCKS]); - const [planList, setPlanList] = useState([]); - const [selectedTask, setSelectedTask] = useState(null); - const [taskDetail, setTaskDetail] = useState(null); - const connectedDeviceSnRef = useRef(null); - const requestDeviceId = useRef(''); - - // 无人机任务控制按钮状态 - const [taskstatus, setTaskStatus] = useState<'suspended' | 'executing' | 'restored'>('suspended'); - const [isReturningHome, setIsReturningHome] = useState(false); - const [isTaskPaused, setIsTaskPaused] = useState(false); - const [flightCommandLoading, setFlightCommandLoading] = useState(false); - - - - //飞机创建航线 - const [taskDetailModal, setTaskDetailModal] = useState(false); - const [uploadModal, setUploadModal] = useState(false); - const [createTaskModal, setCreateTaskModal] = useState(false); - const [loading, setLoading] = useState(false); - const [beginTime, setBeginTime] = useState(null); - const [endTime, setEndTime] = useState(null); - const [waylineUuid, setWaylineUuid] = useState(''); - const [waylineList, setWaylineList] = useState([]); - const [selectedDrone, setSelectedDrone] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false);//权限请求的弹窗 - const [hasControlRight, setHasContronRight] = useState(false);//是否有控制权限 - - //远程控制相关 - const [modalcontent, setmodalcontent] = useState({ title: t('devices.s2'), content: t('devices.requestControlPermission2') }); - const [modelType, setmodelType] = useState(''); - const [taskForm, setTaskForm] = useState({ - name: '', - wayline_uuid: '', - sn: '', - landing_dock_sn: '', - rth_altitude: 110, - rth_mode: 'optimal', - wayline_precision_type: 'rtk', - out_of_control_action_in_flight: 'return_home', - resumable_status: 'manual', - task_type: 'immediate', - time_zone: '', - repeat_type: 'nonrepeating', - repeat_option: { interval: 1 }, - begin_at: null, - end_at: null, - recurring_task_start_time_list: [], - continuous_task_periods: [], - min_battery_capacity: 50 - }); - //====新增:websocket状态======= - const [wsStatus, setWsStatus] = useState('disconnected'); - const [wsDeviceInfo, setWsDeviceInfo] = useState([]); - - // 悬浮视频相关状态 - 【修改1】:删除悬浮相关状态,改为固定显示 - const [mowerView, setMowerView] = useState<'front' | 'back' | 'left' | 'right' | 'top' | 'all'>('front'); - // 主视频 - const { video: video, error: error, ready: ready } = useWebRTCPlayer( - selectedDevice?.serialNumber || '', - selectedDevice?.type === 'robot' - ? '' - : `webrtc://ri.satabot.com/live/livestream/${selectedDevice?.serialNumber}` - ); - - // detect1 - const { video: video1, error: error1, ready: ready1 } = useWebRTCPlayer( - selectedDevice?.serialNumber || '', - selectedDevice?.type === 'robot' ? - `webrtc://ri.satabot.com/live/livestream/${selectedDevice?.serialNumber}/detect1` : '' - ); - - // detect2 - const { video: video2, error: error2, ready: ready2 } = useWebRTCPlayer( - selectedDevice?.serialNumber || '', - selectedDevice?.type === 'robot' ? - `webrtc://ri.satabot.com/live/livestream/${selectedDevice?.serialNumber}/detect2` : '' - ); - - // detect3 - const { video: video3, error: error3, ready: ready3 } = useWebRTCPlayer( - selectedDevice?.serialNumber || '', - selectedDevice?.type === 'robot' ? - `webrtc://ri.satabot.com/live/livestream/${selectedDevice?.serialNumber}/detect3` : '' - ); - const { video: video4, error: error4, ready: ready4 } = useWebRTCPlayer( - selectedDevice?.serialNumber || '', - selectedDevice?.type === 'cut' ? - `webrtc://ri.satabot.com/live/livestream/MC700PLUS-CN-JS-1768444624736-0000000A-09/detect1` : '' - ); - - // 拖拽状态 - const [dragPos, setDragPos] = useState({ x: 0, y: 0 }); - const dragRef = useRef(null); - const isDragging = useRef(false); - const startPos = useRef({ x: 0, y: 0 }); - - // 拖拽逻辑 - const handleMouseDown = useCallback((e: React.MouseEvent) => { - isDragging.current = true; - startPos.current = { - x: e.clientX - dragPos.x, - y: e.clientY - dragPos.y - }; - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }, [dragPos]); - - const handleMouseMove = useCallback((e: MouseEvent) => { - if (!isDragging.current) return; - const newX = e.clientX - startPos.current.x; - const newY = e.clientY - startPos.current.y; - setDragPos({ x: newX, y: newY }); - }, []); - - const handleMouseUp = useCallback(() => { - isDragging.current = false; - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }, []); - - const [urlInfo, setUrlInfo] = useState(null); - // 下发无人机飞行指令:返航 / 取消返航 / 暂停 / 恢复 - const sendFlightCommand = async (command: 'return_home' | 'return_home_cancel' | 'flighttask_pause' | 'flighttask_recovery') => { - if (!selectedDevice?.gateway_sn) { - antdMessage.warning(t('devices.uavUavSn')); - return; - } - - setFlightCommandLoading(true); - const data = { - command, - deviceSn: selectedDevice.gateway_sn, - } - try { - // 这里换成你的真实接口 - const res = await postFlightCommmand(data); - - if (res.data.code == 0) { - antdMessage.success(t('devices.fixed2SuccessFixed2')); - - // 切换状态 - if (command === 'return_home') setIsReturningHome(true); - if (command === 'return_home_cancel') setIsReturningHome(false); - if (command === 'flighttask_pause') setIsTaskPaused(true); - if (command === 'flighttask_recovery') setIsTaskPaused(false); - } else { - antdMessage.error(res.data.msg || '指令下发失败'); - } - } catch (err) { - antdMessage.error(t('devices.fixed2AbnormalFixed2')); - } finally { - setFlightCommandLoading(false); - } - }; - - // 更新任务状态:执行 / 挂起 / 继续 - const updateTaskStatus = async (taskId: string, status: string) => { - if (!taskId) { - antdMessage.warning(t('devices.waylineTasksFixed2')); - return; - } - setLoading(true); - try { - const res = await updateFlightTaskStatus(taskId, status); - if (res.data.code === 0) { - const statusText = { - executing: '任务执行成功', - suspended: '任务已挂起', - restored: '任务已继续', - return_home: '任务已返航' - }[status] || '状态更新成功'; - - setTaskStatus(status as any); - antdMessage.success(statusText); - getAllPlan(selectedDevice); - } else { - antdMessage.error(res.data.msg || '操作失败'); - } - } catch (err) { - antdMessage.error(t('devices.fixed2ActionsFixed2Fixed2AbnormalFixed2')); - } finally { - setLoading(false); - } - }; - //获取飞机直播 - const getLive = () => { - console.log("获取直播的设备信息", selectedDevice); - if (!selectedDevice.gateway_sn) return; - const data = { - getway_sn: selectedDevice.gateway_sn, - drone_sn: selectedDevice.device_sn, - getway_camera_list: selectedDevice.gateway_camera_list ?? [], - drone_camera_list: selectedDevice.drone_camera_list ?? [], - video_expire: 7200, - quality_type: "adaptive", - } - getLiveStream(data).then(res => { - if (res.data.code == 200) { - setUrlInfo(res.data.data); - } - }) - } - - useEffect(() => { - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - }, [handleMouseMove, handleMouseUp]); - - - - - - - - - - - - - const handleWsMessage = useCallback((message: any) => { - //接受的推送信息 - console.log("接受的推送信息", message); - switch (message.type) { - case 'deviceInfo': - const deviceData = {}; - message.data.forEach(item => { - deviceData[item.name] = item.value; - }); - // @ts-ignore - setWsDeviceInfo(deviceData); - - break; - case 'AUTH'://连接失败 重新连接 - if (message.isAuth == true) { - getControl(connectedDeviceSnRef.current); - - } - - break; - case 'switchResult'://连接失败 重新连接 - antdMessage.success({ - content: t('devices.successFixed2'), - key: 'control', - duration: 2, - }); - controlRef.current = true; - setIsControlled(true); - - console.log("权限切换结果"); - break; - case 'switchPermission'://连接失败 重新连接 - console.log("平台请求权限", message.platform) - const platform = (message.platform || '').toLowerCase().trim(); - if (platform == "web" || !platform) { - console.log("检测到web平台,执行return终止逻辑"); - return; // 这里的return会终止整个handleMessage函数的执行 - } - requestDeviceId.current = message.deviceId; - console.log(requestDeviceId.current, connectedDeviceSnRef.current) - - - // if(isControlled||isRequestControl) return; - showModal({ title: t('devices.s2'), content: `${message.platform || '未知平台'}请求控制${requestDeviceId.current == connectedDeviceSnRef.current ? "" : requestDeviceId.current}权限,是否同意?` }, 'switchResult'); - // setmodalcontent({title: t('devices.s10'),content:`${message.platform|| '未知平台'}请求控制权限`}); - break; - case 'detectionReport'://连接失败 重新连接 - const obsData = message.data; - if (obsData && obsData.deviceId == connectedDeviceSnRef.current) { - const type = obsData?.obstacle?.type; - const isSecure = obsData?.isSecure; - const distance = obsData?.obstacle?.distance?.toFixed(2); - console.log("收到障碍物检测报告1122", type, isSecure); - if (isSecure) return; - - antdMessage.warning({ - key: 'obstacle-warning', - content: `设备检测到${OBSTACLE_TYPE[type] ?? '未知物'},距离 ${distance}!`, - duration: 3, - }); - console.log("收到障碍物检测报告1122333"); - } - - break; - } - }, []) - - - - - - // 修复版:WebSocket 状态监听 + 自动重连 - useEffect(() => { - if (!selectedDevice) return; - - // 获取最新状态 - const currentStatus = wsManager.getStatus(); - setWsStatus(currentStatus); - - // 监听状态 - const unsubStatus = wsManager.onStatusChange((newStatus) => { - setWsStatus(newStatus); - - // 断开时清空状态 - if (newStatus === "disconnected") { - connectedDeviceSnRef.current = null; - } - }); - - // 监听消息 - const unsubMsg = wsManager.onMessage(handleWsMessage); - - return () => { - unsubStatus(); - unsubMsg(); - }; - }, [selectedDevice, handleWsMessage]); - - const getTaskDetail = async (taskId) => { - if (!taskId) return; - setTaskDetailModal(true); - setTaskDetail(null); - setLoading(true); - try { - const res = await getFlightTaskDetail(taskId); - if (res.data.code === 0) { - setTaskDetail(res.data.data); - - } else { - setTaskDetail(null); - antdMessage.error(t('devices.noDetailFixed2')); - } - } catch (err) { - setTaskDetail(null); - - antdMessage.error(t('devices.detailFixed2Fixed2AbnormalFixed2')); - } finally { - setLoading(false); - } - }; - const getRouteList = () => { - const userId = JSON.parse(localStorage.getItem("userInfo"))?.userId; - getWorkList({ userId }).then(res => { - console.log(res, "获取任务数据") - if (res.data.code == 200) { - setDeviceRoute(res.data.data); - } else { - setDeviceRoute([]) - } - }).catch(err => { - console.error(err, "获取任务出错"); - setDeviceRoute([]); - }); - - - } - const getControl = (serialnum) => { - setIsConnecting(true); - let userParse = null; - try { - const dataStr = localStorage.getItem("userInfo"); - userParse = dataStr ? JSON.parse(dataStr) : null; - } catch (e) { - console.error("用户信息解析失败", e); - } - - const token = userParse.token || Cookies.get("Admin-Token"); - - const data = { platform: "web", deviceId: serialnum, token: token }; - getJurisdiction(data).then(res => { - if (res?.data?.code === 200 && res?.data?.data?.remoteControl == true) { - console.log("获取远程遥控权限成功"); - controlRef.current = true; - setIsControlled(true); - } else { - controlRef.current = false; - setIsControlled(false); - - } - }).catch(err => { - controlRef.current = false; - setIsControlled(false); - }).finally(() => { - setIsConnecting(false); - }); - - }; - const handleSelectRobot = async (robot) => { - // 1. 防御判断(必须放最前面) - if (!robot || !robot.serialNumber) { - antdMessage.warning(t('devices.fixed2DeviceFixed2InfoFixed2AbnormalFixed2')); - return; - } - - //// 2. 判断是否是同一台设备 - //const isSameDevice = connectedDeviceSnRef.current === robot.serialNumber; - - //// 3. 无论是否同一设备,都要更新选中状态(UI必须同步) - setSelectedDevice(robot); - - //// 4. 如果是同一台设备 → 不重连 WS,直接结束 - //if (isSameDevice) { - // console.log("✅ 同一设备,不重新连接 WS"); - // return; - //} - - // 5. 不同设备 → 关闭旧连接 - wsManager.close(); - connectedDeviceSnRef.current = null; - setWsDeviceInfo([]);//清空设备信息 - - // 6. 安全解析用户信息 - let userParse = null; - try { - const dataStr = localStorage.getItem("userInfo"); - userParse = dataStr ? JSON.parse(dataStr) : null; - } catch (e) { - console.error("用户信息解析失败", e); - } - - if (!userParse) { - setIsControlled(false); - antdMessage.warning(t('devices.loginFixed2')); - return; - } - - const token = userParse.token || Cookies.get("Admin-Token"); - const userName = userParse.username; - - if (!token || !userName) { - setIsControlled(false); - antdMessage.warning(t('devices.fixed2LoginFixed2InfoLoginFixed2')); - return; - } - if (robot.type == "plane") return; - wsManager.connect(robot.serialNumber, "wss://ri.satabot.com/ws/"); - try { - // 7. 校验权限 - const res = await getControlRight({ - userName, - sourceType: 1, - token, - }); - - if (res?.data?.code === 200) { - setHasContronRight(true); - connectedDeviceSnRef.current = robot.serialNumber; - await getControl(robot.serialNumber); - - } else { - setHasContronRight(false); - antdMessage.warning(t('devices.fixed2OthersWebControlFixed2')); - connectedDeviceSnRef.current = null; - } - } catch (err) { - console.error("权限校验失败", err); - setHasContronRight(false); - antdMessage.error(t('devices.abnormalRequestFailed')); - connectedDeviceSnRef.current = null; - } - }; - - - const sendGamepad = useCallback((data: any) => { - }, []); - - - - useEffect(() => { - if (!selectedDevice) return; - - if (selectedDevice?.type === 'plane') { - getLive(); - return; - } - try { - const saved = localStorage.getItem(`robot_route_${selectedDevice.id}`); - if (saved) { - const data = JSON.parse(saved); - setWaypoints(data?.waypoints || []); - antdMessage.success(`已加载 ${selectedDevice.name} 的本地航线`); - } else { - setWaypoints([]); - } - } catch (e) { - setWaypoints([]); - } - }, [selectedDevice]); - - - - useEffect(() => { - const routes = []; - for (let key in localStorage) { - if (key.startsWith('robot_route_')) { - const item = localStorage.getItem(key); - if (item) { - const data = JSON.parse(item); - routes.push({ - robotId: data?.robotId, - robotName: data?.robotName, - waypoints: data?.waypoints, - createTime: data?.createTime - }); - } - } - } - setRobotRoutes(routes); - }, [waypoints]); - - // ============================================== - // 【终极修复】页面切后台不会断开 + 切回来自动重连 - // ============================================== - useEffect(() => { - const handleVisibilityChange = () => { - if (document.visibilityState === 'visible') { - console.log('✅ 页面切回前台,强制重连 WebSocket'); - - // 切回来时:如果当前有选中设备,直接自动重连 - if (selectedDevice?.serialNumber && selectedDevice.type !== 'plane') { - setTimeout(() => { - handleSelectRobot(selectedDevice); - }, 500); - } - } - }; - - // 监听页面切换 - document.addEventListener('visibilitychange', handleVisibilityChange); - - // 页面隐藏时也保持心跳(浏览器不会杀死) - const keepAlive = setInterval(() => { - if ( - connectedDeviceSnRef.current && - wsStatus !== 'connected' && - selectedDevice?.type !== 'plane' - ) { - console.log('🔄 后台保活:尝试重连 WebSocket'); - handleSelectRobot(selectedDevice); - } - }, 8000); // 8秒后台保活 - - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - clearInterval(keepAlive); - }; - }, [selectedDevice, wsStatus, handleSelectRobot]); - - useEffect(() => { - getRouteList(); - getPlaneDeviceList().then(res => { - if (res.data.code == 200) { - const drones = res.data.data.detail.map((item, i) => ({ - ...item, - id: `drone_${item.device_sn || i}`, - name: item.drone_callsign || `无人机 ${i + 1}`, - deviceAlias: item.drone_callsign, - status: item.onlineStatus == 1 ? 'online' : 'offline', - mode: 'standby', - area: '无人机空域', - battery: item.capacity_percent || 0, - capacity_percent: item.capacity_percent, - signal: 'connected', - position: { - lon: item.longitude?.toString() || '0', - lat: item.latitude?.toString() || '0' - }, - lastRunningStatus: { - latitude: item.latitude, - longitude: item.longitude, - battery: item.capacity_percent - }, - onlineStatus: item.onlineStatus, - gateway_sn: item.gateway_sn, - serialNumber: item.device_sn, - drone_callsign: item.drone_callsign, - speed: 0.00, - angularSpeed: 0.00, - gpsPlan: '无人机航线', - taskStatus: '待命', - remoteStatus: 'SAFE', - mqttLink: 'CONNECTED', - type: 'plane' - })); - setPlaneDevice(drones); - setAllDevices([...FILTERED_MOCKS, ...drones]); - } - }); - }, []); - const sendPermissionRequest = (data, type) => { - // type: "switchPermission",switchResult 请求控制权 和请求切换请求 - const wsdata = { - type: type, - switchResult: data, - token: localStorage.getItem("token"), - deviceId: type == "switchPermission" ? connectedDeviceSnRef.current : requestDeviceId.current, - userName: localStorage.getItem("userInfo") ? JSON.parse(localStorage.getItem("userInfo")!).username : "", - }; - console.log("发送的权限请求数据", wsdata); - wsManager.send(wsdata); - - } - - const handleOk = () => { - sendPermissionRequest(true, modelType); - if (modelType == "switchPermission") { - antdMessage.loading({ - key: "control", - type: 'loading', - content: t('devices.fixed2PermissionRequesting'), - duration: 0, - }); - } else { - console.log(requestDeviceId.current, connectedDeviceSnRef.current) - if (requestDeviceId.current == connectedDeviceSnRef.current) { - console.log("同意权限给其他平台") - controlRef.current = false; - setIsControlled(false); - } - - } - console.log("同意权限请求"); - setIsModalOpen(false); - }; - - const handleCancel = () => { - if (modelType == "switchPermission") { - } - console.log("同意权限请求关闭"); - setIsModalOpen(false); - }; - - const getAllPlan = (device) => { - if (!device.gateway_sn) return; - getsinglePlan({ gateway_sn: device.gateway_sn }).then(res => { - if (res.data.code == 0) { - setPlanList(res.data.data.list || []); - } else { - setPlanList([]); - } - }).catch(() => setPlanList([])); - }; - - const handleGetTask = (data) => { - setSelectedTask(data); - getsinglePlanTrack({ uuid: data.uuid }).then(res => { - if (res.data.code == 0 && res.data.data.track?.points?.length) { - const points = res.data.data.track.points.map(item => ({ - lon: item.longitude || item.lon, - lat: item.latitude || item.lat - })); - setPoints(points); - - } else { - antdMessage.warning(t('devices.noWaylineDataFixed2')); - setPoints([]); - } - }).catch(() => { - antdMessage.warning(t('devices.fetchWaylineFailed')); - setPoints([]); - }); - }; - - useEffect(() => { - setTaskIssued(false); - setTaskRunning(false); - setSelectedTask(null); - setPoints([]); // 🔥 关键:清空上一个设备的轨迹 - - - if (selectedDevice?.type === 'plane') { - setPlanList([]); - setPoints([]); - if (selectedDevice.gateway_sn) { - getAllPlan(selectedDevice); - } - return; - } - - setPlanList([]); - try { - const pts = selectedDevice.points; - if (!pts || !Array.isArray(pts) || pts.length < 2 || pts.length % 2 !== 0) { - setPoints([]); - return; - } - const formatPoints = []; - for (let i = 0; i + 1 < pts.length; i += 2) { - const lon = Number(pts[i]); - const lat = Number(pts[i + 1]); - if (!isNaN(lon) && !isNaN(lat) && lon !== 0 && lat !== 0) { - formatPoints.push({ lon, lat }); - } - } - setPoints(formatPoints); - } catch (e) { - console.error('路线解析失败', e); - setPoints([]); - } - }, [selectedDevice]); - - const getPathPoints = (coords) => { - if (!coords || coords.length === 0) return; - const newWps = coords.map((p, i) => ({ - id: i + 1, - lon: p.lon.toFixed(6), - lat: p.lat.toFixed(6), - })); - setWaypoints(newWps); - antdMessage.success(`已添加 ${coords.length} 个地图点击航点`); - }; - - const deleteWaypoint = (id) => { - const newWps = waypoints.filter(w => w.id !== id); - setWaypoints(newWps); - const mapPoints = newWps.map(w => ({ - lon: parseFloat(w.lon), - lat: parseFloat(w.lat) - })); - setPoints(mapPoints); - if (mapClearRef.current) mapClearRef.current(); - antdMessage.success(t('devices.deleteFixed2Waypoint')); - }; - - const clearWaypoints = () => { - setWaypoints([]); - setTaskIssued(false); - setTaskRunning(false); - setPoints([]); - if (mapClearRef.current) mapClearRef.current(); - antdMessage.success(t('devices.waypoint2')); - }; - const createFlightTask = async () => { - if (!taskForm.name) { - antdMessage.warning(t('devices.inputTaskName')); - return; - } - if (!waylineUuid) { - antdMessage.warning(t('devices.pleaseSelectWayline')); - return; - } - - const params = { - ...taskForm, - sn: selectedDevice.gateway_sn, - wayline_uuid: waylineUuid, - rth_altitude: Number(taskForm.rth_altitude), - min_battery_capacity: Number(taskForm.min_battery_capacity), - begin_at: beginTime ? Math.floor(beginTime.unix()) : null, - end_at: endTime ? Math.floor(endTime.unix()) : null, - }; - - if (taskForm.task_type !== 'immediate' && !params.begin_at) { - antdMessage.warning(t('devices.selectTaskStartTimeFixed2')); - return; - } - - setLoading(true); - try { - // 请确保你已在顶部引入:import { createWaylineTask } from '@/api/api' - const res = await saveDeviceTask(params); - if (res.data.code === 0) { - antdMessage.success(t('devices.createSuccess')); - setCreateTaskModal(false); - getAllPlan(selectedDevice); - resetModals(); - } else { - antdMessage.error(res.data.msg || '创建失败'); - } - } catch (err) { - antdMessage.error(t('devices.fixed2Fixed2Abnormal')); - } finally { - setLoading(false); - } - }; - - const saveWaypoints = () => { - if (waypoints.length === 0) { - antdMessage.warning(t('devices.fixed2NoWaypointsSaveFixed2')); - return; - } - if (selectedDevice?.type === 'plane') { - antdMessage.warning(t('devices.fixed2UavSaveFixed2Local')); - return; - } - - const key = `robot_route_${selectedDevice.id}_${Date.now()}`; - const data = { - robotId: selectedDevice.id, - robotName: selectedDevice.name, - waypoints, - createTime: new Date().toLocaleString() - }; - - localStorage.setItem(key, JSON.stringify(data)); - antdMessage.success(t('devices.addNewFixed2SaveLocalFixed2')); - refreshRobotRoutes(); - }; - const resetModals = () => { - setUploadModal(false); - setCreateTaskModal(false); - setTaskDetailModal(false); - setBeginTime(null); - setEndTime(null); - setTaskForm({ - name: '', - wayline_uuid: '', - sn: selectedDevice?.gateway_sn, - landing_dock_sn: '', - rth_altitude: 110, - rth_mode: 'optimal', - wayline_precision_type: 'rtk', - out_of_control_action_in_flight: 'return_home', - resumable_status: 'manual', - task_type: 'immediate', - time_zone: '', - repeat_type: 'nonrepeating', - repeat_option: { interval: 1 }, - begin_at: null, - end_at: null, - recurring_task_start_time_list: [], - continuous_task_periods: [], - min_battery_capacity: 50 - }); - }; - - const refreshRobotRoutes = () => { - const routes = []; - for (let key in localStorage) { - if (key.startsWith('robot_route_')) { - const item = localStorage.getItem(key); - if (item) { - try { - routes.push(JSON.parse(item)); - } catch (e) { } - } - } - } - setRobotRoutes(routes); - }; - const fetchWaylineList = async () => { - try { - const res = await getWayLine(); - if (res.data.code === 0) { - const _list = res.data.data.list.map(item => { - return { - ...item, - label: item.name || `航线_${item.id || t('constants.locationUnknown')}`, - value: item.id - } - }) - setWaylineList(_list || []); // 取list数组 - } - } catch (err) { - antdMessage.error(t('devices.fetchWaylineListFailed')); - } - }; - useEffect(() => { - if (createTaskModal) { - fetchWaylineList(); - // 重置表单时间 - setBeginTime(null); - setEndTime(null); - } - }, [createTaskModal]); - - - useEffect(() => { - if (!selectedDevice || selectedDevice?.type === 'plane') { - setWaypoints([]); - refreshRobotRoutes(); - if (selectedDevice?.type === 'plane') { - setActiveTab('gps'); - } else { - setActiveTab('remote'); - } - return; - } - - try { - let latestWaypoints = []; - let maxTime = 0; - - for (let key in localStorage) { - if (key.startsWith(`robot_route_${selectedDevice.id}`)) { - const item = localStorage.getItem(key); - if (item) { - const data = JSON.parse(item); - const time = new Date(data?.createTime).getTime(); - if (time > maxTime) { - maxTime = time; - latestWaypoints = data?.waypoints; - } - } - } - } - - setWaypoints(latestWaypoints); - refreshRobotRoutes(); - } catch (e) { - setWaypoints([]); - refreshRobotRoutes(); - } - }, [selectedDevice]); - - const loadRoute = (route) => { - setSelectedRoute(route); // ✅ 加上这句 - setWaypoints(route.waypoints); - const mapPoints = route.waypoints.map(w => ({ - lon: parseFloat(w.lon), - lat: parseFloat(w.lat) - })); - setPoints(mapPoints); - antdMessage.success(`已加载航线:${route.robotName}`); - }; - - const issueGPSTask = () => { - //if (waypoints.length === 0) return; - setTaskIssued(true); - setTaskRunning(false); - antdMessage.success(t('devices.gpsTaskIssued')); - }; - - const toggleTaskRunning = () => { - if (!taskIssued) { - antdMessage.warning(t('devices.taskSuspended2Fixed23')); - return; - } - setTaskRunning(!taskRunning); - antdMessage.success(taskRunning ? "已暂停" : "已开始执行"); - }; - - const stopGPSTask = () => { - setTaskRunning(false); - setTaskIssued(false); - antdMessage.success(t('devices.taskStopped')); - }; - - const mapFocus = selectedDevice ? { - lat: selectedDevice?.locationMessage?.latitude || selectedDevice?.lastRunningStatus?.latitude, - lon: selectedDevice?.locationMessage?.longitude || selectedDevice?.lastRunningStatus?.longitude, - height: 500 - } : { lat: '31.030129089704428', lon: '120.76671769055027', height: 800 }; - //手柄数据 - const handleGamepad = (data) => { - /* if (!wsRef.current || // 1. WebSocket 是否存在 - wsRef.current.readyState !== WebSocket.OPEN || // 2. 连接是否打开 - !controlRef.current) { // 3. 是否有控制权限 - return; - }*/ - const dataStr = localStorage.getItem('userInfo'); - let parse = JSON.parse(dataStr); - var token = parse.token; - // @ts-ignore - var token = parse?.token; - console.log(wsStatus); - // 📦 数据包装 - const wsdata = { - type: "remoteControl", // 消息类型 - remoteData: data, // 原始手柄数据 - token: token, // 认证令牌 - userName: parse.username, - }; - wsManager.send(wsdata); - //console.log(data) - }; - const showModal = (content, type) => { - setmodalcontent(content); - setmodelType(type); - console.log("显示弹窗", content, type); - setIsModalOpen(true); - }; - const handleControlModal = (data) => { - if (!hasControlRight) return; - if (data.control == true) { - showModal({ title: t('devices.s2'), content: t('devices.requestControlPermission2') }, 'switchPermission'); - } else { - setIsModalOpen(false); - } - } - const [uavState, setUavState] = useState(null); - const uavTimerRef = useRef(null); - - // 定时获取无人机状态 - - - const calcPercent = (value, max) => { - const num = Number(value); - if (!Number.isFinite(num) || max <= 0) return 0; - return Math.min(100, Math.round((num / max) * 100)); - }; - const handleGetRoute = (work) => { - setSelectedRoute(work); // ✅ 加上这句 - - const data = { - workName: work.workName - } - getWorkData(data).then(res => { - if (res.data?.code === 200) { - console.log(res, "获取单独列表数据"); - - // 安全解析 jsonData - let jsonData = {}; - try { - const rawJson = res.data?.data?.jsonData; - if (rawJson) jsonData = JSON.parse(rawJson); - } catch (e) { - console.warn("路径数据解析失败", e); - } - - // 按优先级取路径:path → outer → 空数组 - const path = - jsonData?.path?.length > 0 - ? jsonData.path - : jsonData.outer?.length > 0 - ? jsonData.outer - : []; - - // 格式化坐标 - const formatPoints = path.map(p => ({ - lon: p.lon ?? p.longitude ?? p[0], - lat: p.lat ?? p.latitude ?? p[1] - })); - - setPoints(formatPoints); - } else { - setPoints([]); - } - }).catch(err => { - console.error("获取数据失败", err); - setPoints([]); - }); - } - - const Gauge = ({ label, value, unit, color, percent }) => { - const colorMap = { cyan: 'stroke-cyan-400', rose: 'stroke-rose-400', yellow: 'stroke-yellow-400', purple: 'stroke-purple-400' }; - return ( -
-
- - - - - -
- {value} - {unit} -
-
- {label} -
- ); - }; - - const onlineCount = allDevices.filter(r => r.status === 'online' || r.onlineStatus == 1 || r.status == "working").length; - - // 【新增】视频渲染组件 - 提取成独立函数,方便复用 - const renderVideoContent = () => { - return ( -
- - {/* 割草机视角切换 */} - {selectedDevice?.type === 'cut' && ( -
- {['front', 'back', 'left', 'right', 'all'].map(v => ( - - ))} -
- )} - {selectedDevice?.type === 'mower' && ( -
- {['front', 'back', 'left', 'right', 'all'].map(v => ( - - ))} -
- )} - - {/* 巡检机器人:3路视频 → 紧凑横向,不滚动,全部可见 */} - {selectedDevice?.type === 'robot' && ( -
-
- -
- {/* 主画面:小一点 */} -
- -
-
- -
-
- )} - - {/* 割草机单路 */} - {selectedDevice?.type === 'cut' && mowerView !== 'all' && ( -
- {mowerView != "top" && } - {mowerView == "top" && } - -
- )} - {selectedDevice?.type === 'mower' && mowerView !== 'all' && ( -
- {mowerView != "top" && } - - -
- )} - - {/* ✅ 修复:割草机4路视频 —— 绝不压扁,保持16:9 */} - {/* {selectedDevice?.type === 'mower' && mowerView === 'all' && ( -
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- )}*/} - {selectedDevice?.type === 'mower' && mowerView === 'all' && ( -
- {/* 上 */} - {/*
- -
*/} - - {/* 左 + 中 + 右 */} -
-
- -
-
- -
-
- -
-
- - {/* 下 */} -
- -
-
- )} - {selectedDevice?.type === 'cut' && mowerView === 'all' && ( -
- {/* 上 */} - {/*
- -
*/} - - {/* 左 + 中 + 右 */} -
-
- -
-
- -
-
- -
-
- - {/* 下 */} -
- -
-
- )} - - {/*其他设备 - {selectedDevice?.type !== 'robot' && selectedDevice?.type !== 'mower' && ( -
- -
- {selectedDevice?.name}|实时视频 -
-
- )}*/} -
- ); - }; - - return ( -
- {/* 左侧设备列表 */} -
-
-
- -

- {t('devices.deviceList')}

-
-
-
- {t('devices.onlineDevices')} - {onlineCount}/{allDevices.length} -
- {/*
-

{t('devices.fixed2TaskSuspended2')}

-

{selectedDevice.taskStatus}

-
*/} -
-
-
- {allDevices.map(device => ( -
{ - handleSelectRobot(device); - }} - className={`bg-white/5 backdrop-blur-sm rounded-xl p-4 border transition-all duration-200 hover:scale-[1.01] cursor-pointer ${selectedDevice.id === device.id ? 'border-[#00e5ff]/50 bg-[#00e5ff]/5 shadow-lg shadow-[#00e5ff]/5' : 'border-[#243157]/40'}`} - > -
-

- {device?.type === 'plane' && } - {device.name} - [{device?.type == 'plane' ? t('constants.uav') : device?.type === 'mower' ? t('constants.mower') : '巡检机'}] - [{device?.type == 'plane' ? '支持GPS规划' : "支持摇控、GPS规划"}] -

- - {StatusTagMap[device?.status]?.text} - -
- {/*

- 区域: {device?.area} -

*/} -
- - - 电量: {device?.battery}% - -
- {device?.type === 'plane' &&
- SN: {device?.gateway_sn?.slice(-6) || '--'} -
} - {device?.type !== 'plane' &&
- {device?.serialNumber || '--'} -
} - -
- ))} -
-
- - {/* 中间操作区 */} -
-
-
-
-

- {selectedDevice?.name} -

-
-
- {t('devices.fixed2Video')} -
-
- - {/* 无人机状态概览 */} - {selectedDevice?.type === 'plane' && (uavState || selectedDevice) && ( -
-
-

- {t('devices.batteryFixed2')}

-
-
-
-
- {(uavState || selectedDevice).capacity_percent || 0}% -
-
-
-

- {t('devices.flightAltitude')}

-

- {Number((uavState || selectedDevice).height || 0).toFixed(2)} m -

-
-
-

- {t('devices.s62')}

-

- {(uavState || selectedDevice).position_state?.gps_number || 0} GPS - / - {(uavState || selectedDevice).position_state?.rtk_number || 0} RTK -

-
-
-

- {t('devices.s72')}

-

- {Number((uavState || selectedDevice).wind_speed || 0).toFixed(2)} m/s -

-
-
-

- {t('devices.fixed2RtkFixed2StatusFixed2')}

-
-
- - {(uavState || selectedDevice).position_state?.is_fixed === 'fixing_successful' ? '已固定' : '未固定'} - -
-
-
- )} - {/*
-
-

{t('devices.fixed2CurrentFixed2LocationFixed2')}

-

- {selectedDevice?.position?.lon - ? Number(selectedDevice.position.lon).toFixed(2) - : '--'}, - {selectedDevice?.position?.lat - ? Number(selectedDevice.position.lat).toFixed(2) - : '--'} -

-
-

{t('devices.fixed2CurrentFixed2AbModeFixed2')}

-

{selectedDevice?.type === 'plane' ? '无人机待命' : selectedDevice?.type === 'mower' ? '割草待命' : '待命'}

-
-
-

{t('devices.fixed2Battery')}

-

{selectedDevice?.battery}% / 92°

-
-
-

{t('devices.speed')}

-

{selectedDevice?.speed} m/s

-
-
- {t('devices.fixed2Enable')} - -
-
*/} -
- -
-
- {selectedDevice.type !== 'plane' && - } - -
-
- - {/* 遥控与态势 - 【修改2】:把视频嵌入到这里 */} - {activeTab === 'remote' && ( -
- {/* 视频区域 - 独立大窗口 */} - {selectedDevice.type !== 'plane' && ( -
- {renderVideoContent()} -
- )} - - {/* 状态卡片行 */} - {/*
-
-

{t('devices.tcp')}

-

{selectedDevice?.mqttLink}

-
-
-

{t('devices.s6')}

-

{selectedDevice.remoteStatus}

-
-
-

{t('devices.s7')}

-

{selectedDevice.gpsPlan}

-
-
-

{t('devices.taskStatus')}

-

{selectedDevice.taskStatus}

-
-
*/} - - {/* Feature buttons */} -
-

{t('devices.features.title')}

- { - console.log('[Feature]', feature, enabled); - }} - /> -
- - {/* 控制区:左轮 + Xbox + 右轮 —— 紧凑布局 */} -
- {/* 左轮速度面板 - 缩小 */} -
-
- {/* 顶部标题 */} -
-

{t('devices.leftWheelDrive')}

- {t('layout.normalRunning')} -
- - {/* 仪表盘(居中放大) */} -
-
- -
-
- - {/* 速度 + 电流 一体化卡片(核心视觉) */} -
- {/* 速度 */} -
-

{t('devices.realtimeSpeed')}

-

- {Number(wsDeviceInfo?.leftMeasureSpeed || 0).toFixed(2)} - rpm -

-
- - {/* 电流 */} -
-

{t('devices.s21')}

-

- {Number(wsDeviceInfo?.leftCurrent || 0).toFixed(2)} - A -

-
-
-
-
- - {/* Xbox 手柄 - 缩小、居中 */} -
-
- -
-
-

{t('devices.dpad.title')}

- -
-
- - {/* 右轮速度面板 - 缩小 */} -
-
- {/* 顶部标题 + 状态 */} -
-

{t('devices.rightWheelDrive')}

- {t('layout.normalRunning')} -
- - {/* 仪表盘(居中优化) */} -
-
- -
-
- - {/* 核心数据卡片:速度 + 目标值(干净对称) */} -
- {/* 实时速度 */} -
-

{t('devices.realtimeSpeed')}

-

- {Number(wsDeviceInfo?.rightMeasureSpeed || 0).toFixed(2)} - rpm -

-
- - {/* 速度输出 */} -
-

{t('devices.s21')}

-

- {(Number(wsDeviceInfo?.rightMeasureSpeed || 0)).toFixed(2)} - A -

-
-
-
-
-
-
- )} - - {/* GPS规划 */} - {activeTab === 'gps' && ( -
-
-

{t('devices.gpsWaypoint')}

-
- {selectedDevice?.type !== 'plane' && ( - - )} - -
-
- - {selectedDevice?.type === 'plane' && ( -
-
-
{t('devices.droneWaylineTask')}
- - -
-
{planList.length > 0 ? planList.map((task, index) => { - // 状态映射(和你上面的风格完全统一) - const statusMap = { - "waiting": { text: t('devices.s68'), color: "bg-yellow-500" }, - "starting_failure": { text: t('devices.failedFixed2'), color: "bg-red-500" }, - "executing": { text: t('workOrder.inProgress'), color: "bg-blue-500" }, - "suspended": { text: t('workOrder.suspended'), color: "bg-orange-500" }, - "restored": { text: t('workOrder.inProgress'), color: "bg-blue-500" }, - "terminated": { text: t('devices.s70'), color: "bg-black" }, - "success": { text: t('common.success'), color: "bg-green-500" }, - "timeout": { text: t('devices.s71'), color: "bg-red-600" } - }; - const status = statusMap[task.status] || { text: t('constants.locationUnknown'), color: "bg-gray-400" }; - const isSelected = selectedTask?.uuid === task.uuid; - - return ( -
{ - e.stopPropagation(); - handleGetTask(task); - }} - > -
-
{task.name}
- - { - e.stopPropagation(); - getTaskDetail(task.uuid); - }} /> - -
- {/* 状态标签(和你原版完全一样) */} - - {status.text} - -
- ); - }) : ( -
{t('devices.noWaylineTaskFixed2')}
- )} -
-
- )} - {(selectedDevice?.type === 'robot' || selectedDevice?.type === 'mower') && ( -
-
{t('devices.robotTaskData')}
-
- {deviceRoute.length > 0 ? deviceRoute.map((task) => ( -
handleGetRoute(task)} - className={`p-3 rounded-lg border cursor-pointer transition-all - ${selectedRoute?.id === task.id - ? 'bg-[#00e5ff]/15 border-[#00e5ff] shadow-lg shadow-[#00e5ff]/10' - : 'bg-slate-80/50 border-blue-900/50 hover:border-blue-500' - }`} - > -
{task.workName}
-
- )) : ( -
{t('common.noData')}
- )} -
-
- )} - -
-
- { mapClearRef.current = fn; }} - realtimePosition={{ lat: wsDeviceInfo?.latitude, lon: wsDeviceInfo?.longitude }} - /> -
-
- - {selectedDevice?.type !== 'plane' && ( -
-
-

- 航点列表 ({waypoints.length}) -

- {waypoints.length === 0 ? ( -
- {t('devices.noWaypointsFixed2')}
- ) : ( -
- {waypoints.map((wp) => ( -
-
-

航点 {wp.id}

-

{wp.lon}, {wp.lat}

-
-
- ))} -
- )} -
- -
-

- {t('devices.fixed2RobotFixed2Local')}

- {robotRoutes.length === 0 ? ( -
{t('devices.save')}
- ) : ( -
- {robotRoutes.map((rt, i) => ( -
loadRoute(rt)} - className={`p-3 rounded-lg border cursor-pointer transition-all - ${selectedTask?.robotId === rt.robotId - ? 'bg-[#00e5ff]/15 border-[#00e5ff] shadow-lg shadow-[#00e5ff]/10' - : 'bg-slate-800/50 border-slate-700/50 hover:border-cyan-500' - }`} - > -

{rt.robotName}

-

航点:{rt.waypoints.length} 个

-
- ))} -
- )} -
-
- )} - - {/*{selectedDevice.type !== 'plane' && (*/} - {/*
- - - -
*/} - {/* 无人机专用任务控制按钮 */} - {selectedDevice?.type === 'plane' ? ( -
- {/* 动态按钮:执行中 → 挂起 / 已挂起 → 继续 */} - {taskstatus === 'suspended' ? ( - - ) : ( - - )} - - - - {/* 暂停 / 恢复 */} - {isTaskPaused ? ( - - ) : ( - - )} - {/* 动态:返航 / 取消返航 */} - {isReturningHome ? ( - - ) : ( - - )} -
- ) : ( - // 机器人/割草机保留原来的按钮 -
- - - -
- )} - {/*//)}*/} -
- )} -
- - {/* 右侧视频面板(遥控页隐藏)- 保持不变 */} - {activeTab !== 'remote' && ( -
-
-

-

-
-
-
-
-
-
- {/* 设备类型:机器人 → 显示 4 路视频 */} - {selectedDevice?.type == 'robot' && ( - <> -
-
- -
-
-
-
- -
-
-
-
- -
-
- - - - - )} - - {/* 设备类型:割草机 → 显示 1 路视频 */} - {(selectedDevice?.type == 'mower' || selectedDevice?.type == 'cut') && ( - <> -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - - )} - - {/* 设备类型:无人机 → 遍历显示视频流 */} - {selectedDevice?.type !== 'robot' && selectedDevice?.type !== 'mower' && ( - <> - {urlInfo?.length > 0 ? ( - urlInfo.map((stream, index) => ( -
-
- -
-
- )) - ) : ( -
{t('devices.noVideoStreamFixed2')}
- )} - - )} -
-
- )} - - {/* 【修改3】:删除原来的悬浮视频窗口代码 */} - - -
- {taskDetail ? ( -
- {Object.entries(taskDetail).map(([k, v]) => { - // 过滤不需要的字段 - if (k === 'folder_info' || k === 'folder_id' || k == 'week_of_month' || k == "days_of_month" || k == "days_of_week" || k == "interval") return null; - - // 空值、null、undefined 直接不显示 - if (v === null || v === undefined || v === '') return null; - - // 字段中文名映射 - const keyMap = { - name: t('devices.taskName'), - uuid: '任务ID', - wayline_uuid: '航线UUID', - sn: '机场SN', - landing_dock_sn: '降落地场SN', - rth_altitude: '返航高度(m)', - rth_mode: '返航模式', - wayline_precision_type: '航线精度', - out_of_control_action_in_flight: '失控动作', - resumable_status: '断点续飞', - task_type: '任务类型', - repeat_type: '重复模式', - begin_at: t('devices.startTime'), - end_at: t('devices.endTime'), - min_battery_capacity: '最低电量(%)', - status: t('devices.taskStatus'), - time_zone: t('systemSetting.timezone'), - current_waypoint_index: '当前航点', - }; - - // 枚举汉化 - const enumMap = { - rth_mode: { optimal: '智能高度', preset: '设定高度' }, - wayline_precision_type: { gps: 'GPS模式', rtk: 'RTK高精度' }, - resumable_status: { auto: '自动', manual: '手动' }, - task_type: { - immediate: '立即任务', - timed: '单次定时', - recurring: '重复任务', - continuous: '连续任务', - }, - repeat_type: { - nonrepeating: '不重复', - daily: '每日', - weekly: '每周', - absolute_monthly: '每月(日期)', - relative_monthly: '每月(星期)', - }, - status: { - waiting: '待开始', - starting_failure: '启动失败', - executing: t('workOrder.inProgress'), - suspended: t('workOrder.suspended'), - restored: '继续执行', - terminated: '已终止', - success: t('workOrder.completed'), - timeout: '执行超时', - }, - }; - - // 渲染值 - const renderValue = () => { - if (enumMap[k]?.[v]) return enumMap[k][v]; - if ((k === 'begin_at' || k === 'end_at') && !isNaN(Number(v))) { - return new Date(Number(v) * 1000).toLocaleString('zh-CN'); - } - return v; - }; - - const label = keyMap[k] || k; - - return ( -
- {label}: - {renderValue()} -
- ); - })} -
- ) : ( -
- {/* 旋转加载圈 */} -
-

{t('common.loading')}

-
- )} -
-
- { - resetModals(); - }} - footer={null} - maskClosable={false} - width={600} - > -
-
-
- - setTaskForm({ ...taskForm, name: e.target.value })} - placeholder={t('devices.inputWaylineNameFixed2')} - /> -
-
- - -
-
- - setTaskForm({ ...taskForm, landing_dock_sn: e.target.value })} - placeholder={t('devices.s57')} - /> -
-
- - {/* 飞行参数 */} -
-
- - setTaskForm({ ...taskForm, rth_altitude: val || 0 })} - style={{ width: '100%' }} - /> -
-
- - setTaskForm({ ...taskForm, wayline_precision_type: val })} - options={[ - { label: t('devices.fixed2GpsModeRtkFixed2'), value: 'gps' }, - { label: t('devices.fixed2RtkHighPrecision'), value: 'rtk' } - ]} - style={{ width: '100%' }} - /> -
-
- - setTaskForm({ ...taskForm, task_type: val })} - options={[ - { label: t('devices.taskSuspended2Fixed22'), value: 'immediate' }, - //{ label: t('devices.taskSuspended2Fixed22'), value: 'timed' }, - //{ label: t('devices.taskSuspended2Fixed22'), value: 'recurring' }, - //{ label: t('devices.taskSuspended2Fixed2'), value: 'continuous' } - ]} - style={{ width: '100%' }} - /> -
-
- - {/* 时间选择 */} - {taskForm.task_type !== 'immediate' && ( -
-
- - setBeginTime(date)} - style={{ width: '100%' }} - placeholder={t('devices.startTimeFixed2')} - /> -
- {['recurring', 'continuous'].includes(taskForm.task_type) && ( -
- - setEndTime(date)} - style={{ width: '100%' }} - /> -
- )} -
- )} - - {/* 重复任务 */} - {taskForm.task_type === 'recurring' && ( -
-
- -