diff --git a/env.js b/env.js index 5f29637..8259afa 100644 --- a/env.js +++ b/env.js @@ -15,8 +15,8 @@ const envConfig = { }, [ENV.PROD]: { // baseURL: 'http://192.168.2.56:8081', - //baseURL: 'http://1.95.137.212:59015',//http://192.168.2.56:8081 - baseURL: 'https://serviceri.satabot.com', + baseURL: 'http://1.95.137.212:59015',//http://192.168.2.56:8081 + //baseURL: 'https://serviceri.satabot.com', WS_URL: 'wss://ri.satabot.com/ws/' }, }; diff --git a/src/components/SystemSetting/UserPage.tsx b/src/components/SystemSetting/UserPage.tsx index 96f97df..f75a9a3 100644 --- a/src/components/SystemSetting/UserPage.tsx +++ b/src/components/SystemSetting/UserPage.tsx @@ -447,14 +447,14 @@ export default function UserRolePage() { : 停用 }, { - title: '是否有设备权限', dataIndex: 'deviceOperatePerm', width: 80, + title: '是否有设备权限', dataIndex: 'deviceOperatePerm', width: 140, render: (_, s) => s.deviceOperatePerm == true ? 有权限 : 无权限 }, { title: '操作', - width: 220, + width: 300, fixed: 'right', render: (_, r) => ( @@ -478,7 +478,6 @@ export default function UserRolePage() { onChange={(p) => setUserPagination({ ...userPagination, pageNum: p.current!, pageSize: p.pageSize! })} bordered size="middle" - // 👇 下面这行是修复不自适应的关键 scroll={{ x: 'max-content' }} /> diff --git a/src/components/devices/DeviceStatusPage.tsx b/src/components/devices/DeviceStatusPage.tsx index e1f45a5..64fc4f2 100644 --- a/src/components/devices/DeviceStatusPage.tsx +++ b/src/components/devices/DeviceStatusPage.tsx @@ -1,344 +1,298 @@ -import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import React, { + useState, + useEffect, + useMemo, + useRef, + useCallback, +} from 'react'; + import { - Row, Col, Card, Statistic, Tag, Typography, - Space, Empty, Descriptions, Badge, Progress, Button, - Divider, Select, message, Modal + Row, + Col, + Card, + Tag, + Typography, + Space, + Empty, + Descriptions, + Badge, + Progress, + Button, + Select, + message, + Modal, } from 'antd'; + import { - ThunderboltOutlined, DashboardOutlined, AimOutlined, - EnvironmentOutlined, RocketOutlined, GlobalOutlined, - VideoCameraOutlined, ControlOutlined, - CameraOutlined + DashboardOutlined, + GlobalOutlined, + VideoCameraOutlined, + ControlOutlined, + CameraOutlined, + ThunderboltOutlined, + EnvironmentOutlined, } from '@ant-design/icons'; + import XboxController from './XboxController'; import DroneLivePlayer from '../VolcRtcPlayer.jsx'; -import { getLiveStream, getControlRight, getJurisdiction } from '../../api/device.ts'; +import { getLiveStream } from '../../api/device.ts'; import CesiumMap from '../Map.jsx'; -import { wsManager } from '../WebSocketManager.ts'; import { useSelector } from 'react-redux'; import { RootState } from '@/src/store/index.ts'; -import envConfig from '../../../env'; import { useWebRTCPlayer } from '../useWebRTCStream.tsx'; import VideoPlayer from '../VideoPlayer.tsx'; +import DeviceController from './DeviceController'; const { Text } = Typography; -const OBSTACLE_TYPE = { - 0: '未知', - 1: '人', - 2: '车辆', - 3: '围栏', - 4: '坑洞', - 5: '草丛', -}; - -export default function DeviceStatusPage({ devicesAll, serialNumber, onDeviceChange }) { - const [planeVideo, setPlaneVideo] = useState([]); - const [isControlled, setIsControlled] = useState(false); - const [wsDeviceInfo, setWsDeviceInfo] = useState({}); - const [wsStatus, setWsStatus] = useState('disconnected'); - const [isRequestControl, setIsRequestControl] = useState(false); - const connectedDeviceSnRef = useRef(null); - const requestDeviceId = useRef(''); - const controlRef = useRef(false); +export default function DeviceStatusPage({ + devicesAll, + serialNumber, + onDeviceChange, +}) { const { userInfo } = useSelector((state: RootState) => state.user); + const [messageApi, contextHolder] = message.useMessage(); + const controllerRef = useRef(null); + + const [planeVideo, setPlaneVideo] = useState([]); + const [isControlled, setIsControlled] = useState(false); + const [hasControlRight, setHasControlRight] = useState(false); + const [connecting, setConnecting] = useState(false); + const [wsStatus, setWsStatus] = useState('disconnected'); + const [wsDeviceInfo, setWsDeviceInfo] = useState({}); + const [isModalOpen, setIsModalOpen] = useState(false); - const [modalContent, setModalContent] = useState({ title: '', content: '' }); - const [modelType, setModelType] = useState(''); - const [hasControlRight, setHasContronRight] = useState(false); + const [modalContent, setModalContent] = useState({ + title: '', + content: '', + }); - // ========== 机器人视角切换(只给机器人用)========== - const [mowerView, setMowerView] = useState<'front' | 'back' | 'left' | 'right' | 'top' | 'all'>('front'); + const [mowerView, setMowerView] = useState('front'); - // 主视频(机器人用) - const { video: robotVideo, error, ready } = useWebRTCPlayer( - serialNumber || '', - `webrtc://ri.satabot.com/live/livestream/${serialNumber}` - ); - - // 获取选中的设备详情 const device = useMemo(() => { - const sn = serialNumber || (devicesAll?.[0]?.device_sn || devicesAll?.[0]?.serialNumber); + const sn = + serialNumber || + devicesAll?.[0]?.device_sn || + devicesAll?.[0]?.serialNumber; + if (!sn) return null; - return devicesAll?.find(d => (d.device_sn || d.serialNumber) == sn); + + return devicesAll?.find( + (d) => (d.device_sn || d.serialNumber) === sn, + ); }, [devicesAll, serialNumber]); - // 判断是否为无人机 const isUAV = useMemo(() => { - return device && (device.device_sn); + return !!device?.device_sn; }, [device]); - // 选中设备(给视频用) - const selectedDevice = useMemo(() => { - return device; - }, [device]); - - // 优先显示 WebSocket 推送的实时数据 const runningStatus = useMemo(() => { - const baseStatus = device?.lastRunningStatus || {}; - if (!isUAV && Object.keys(wsDeviceInfo).length > 0) { - return { - ...baseStatus, - ...wsDeviceInfo + const base = device?.lastRunningStatus || {}; + + return isUAV + ? base + : { + ...base, + ...wsDeviceInfo, }; - } - return baseStatus; }, [device, wsDeviceInfo, isUAV]); - const fix2 = (val) => { - if (val === null || val === undefined || isNaN(val)) return '-'; - return Number(val).toFixed(2); - }; - + const { video: robotVideo, error, ready } = useWebRTCPlayer( + serialNumber || '', + `webrtc://ri.satabot.com/live/livestream/${serialNumber}`, + ); const getVal = (val) => val === "no_rain" ? "无" : (val || '-'); - // 获取权限 - const getControl = useCallback((serialnum) => { - const data = { platform: "web", deviceId: serialnum }; - getJurisdiction(data).then(res => { - if (res?.code == 200 && res?.data?.remoteControl == true) { - controlRef.current = true; - setIsControlled(true); - } else { - controlRef.current = false; - setIsControlled(false); - } - }).catch(err => { - controlRef.current = false; - setIsControlled(false); - }) - }, []); - const showModal = (content, type) => { - setModalContent(content); - setModelType(type); - setIsModalOpen(true); - }; + useEffect(() => { + const controller = new DeviceController(userInfo, messageApi); - const sendPermissionRequest = (data, type) => { - const wsdata = { - type: type, - switchResult: data, - token: userInfo.token, - deviceId: type === "switchPermission" ? connectedDeviceSnRef.current : requestDeviceId.current, - userName: userInfo.username, + controller.onDeviceInfoUpdate = setWsDeviceInfo; + controller.onControlStatusChange = setIsControlled; + controller.onHasControlRightChange = setHasControlRight; + controller.onRequestControlStatusChange = setConnecting; + controller.onWsStatusChange = setWsStatus; + + controller.onPermissionRequest = (content) => { + setModalContent(content); + setIsModalOpen(true); }; - wsManager.send(wsdata); - }; - const handleOk = () => { - sendPermissionRequest(true, modelType); - if (modelType === "switchPermission") { - setIsRequestControl(true); - messageApi.loading({ key: "control", content: '权限请求中...', duration: 0 }); - } else { - if (requestDeviceId.current === connectedDeviceSnRef.current) { - controlRef.current = false; - setIsControlled(false); - } - } - setIsModalOpen(false); - }; + controller.init(); - const handleCancel = () => { - if (modelType === "switchPermission") { - setIsRequestControl(false); - } else { - sendPermissionRequest(false, modelType); - } - setIsModalOpen(false); - }; + controllerRef.current = controller; - // WebSocket 消息处理 - const handleWsMessage = useCallback((messageData) => { - switch (messageData.type) { - case 'deviceInfo': - const deviceData = {}; - messageData.data.forEach(item => { deviceData[item.name] = item.value; }); - setWsDeviceInfo(deviceData); - break; - case 'AUTH': - if (messageData.isAuth === true) { - getControl(connectedDeviceSnRef.current); - } - break; - case 'switchResult': - messageApi.success({ content: '权限获取成功', key: 'control', duration: 2 }); - setIsRequestControl(false); - controlRef.current = true; - setIsControlled(true); - break; - case 'switchPermission': - const rawPlatform = messageData.platform; - if (rawPlatform == "null") return; - const platform = (messageData.platform || '').toLowerCase().trim(); - if (platform === "web" || !platform) return; - requestDeviceId.current = messageData.deviceId; - showModal({ - title: "权限请求", - content: `${messageData.platform || '未知平台'}请求控制${requestDeviceId.current === connectedDeviceSnRef.current ? "" : requestDeviceId.current}权限, 是否同意?` - }, 'switchResult'); - break; - case 'detectionReport': - const obsData = messageData.data; - if (obsData && obsData.deviceId === connectedDeviceSnRef.current) { - const type = obsData?.obstacle?.type; - const isSecure = obsData?.isSecure; - const distance = obsData?.obstacle?.distance?.toFixed(2); - if (isSecure) return; - messageApi.warning({ - key: 'obstacle-warning', - content: `设备检测到${OBSTACLE_TYPE[type] ?? '未知物'},距离 ${distance}m!`, - duration: 3 - }); - } - break; - } - }, [getControl, messageApi]); - - // 连接设备 WebSocket - const connectDevice = useCallback(async (targetDevice) => { - if (!targetDevice || isUAV) return; - wsManager.close(); - connectedDeviceSnRef.current = null; - setWsDeviceInfo({}); - const token = userInfo.token; - const userName = userInfo.username; - const wsUrl = envConfig.WS_URL || "wss://ri.satabot.com/ws/"; - wsManager.connect(targetDevice.serialNumber || targetDevice.device_sn, wsUrl); - - try { - const res = await getControlRight({ userName, sourceType: 1, token }); - if (res?.code == 200) { - connectedDeviceSnRef.current = targetDevice.serialNumber || targetDevice.device_sn; - setHasContronRight(true); - await getControl(connectedDeviceSnRef.current); - } else { - setHasContronRight(false); - messageApi.warning("其他端正在控制"); - } - } catch (err) { - messageApi.error("权限校验失败"); - } - }, [isUAV, getControl, userInfo, messageApi]); + return () => { + controller.destroy(); + controllerRef.current = null; + }; + }, [userInfo, messageApi]); useEffect(() => { - if (device) { - connectDevice(device); - } - return () => { wsManager.close(); }; - }, [device, connectDevice]); + if (!device || !controllerRef.current) return; - useEffect(() => { - const unsubStatus = wsManager.onStatusChange(setWsStatus); - const unsubMsg = wsManager.onMessage(handleWsMessage); - return () => { unsubStatus(); unsubMsg(); }; - }, [handleWsMessage]); + controllerRef.current.connectDevice(device); + }, [device]); useEffect(() => { if (!device?.gateway_sn) { setPlaneVideo([]); return; } - const data = { - getway_sn: device?.gateway_sn, - drone_sn: device?.device_sn, - getway_camera_list: device?.gateway_camera_list ?? [], - drone_camera_list: device?.drone_camera_list ?? [], - video_expire: 7200, - quality_type: "adaptive", - } - getLiveStream(data).then(res => { - if (res.code == 200) { - setPlaneVideo(res.data || []); + + const loadVideo = async () => { + try { + const data = { + getway_sn: device.gateway_sn, + drone_sn: device.device_sn, + getway_camera_list: device.gateway_camera_list ?? [], + drone_camera_list: device.drone_camera_list ?? [], + video_expire: 7200, + quality_type: 'adaptive', + }; + + const res = await getLiveStream(data); + + if (res.code === 200) { + setPlaneVideo(res.data || []); + } + } catch (err) { + console.error(err); } - }) + }; + + loadVideo(); }, [device]); - const handleGamepadUpdate = (state) => { - if (!isControlled || !connectedDeviceSnRef.current) return; - const payload = { - type: 'gamepad', - deviceId: connectedDeviceSnRef.current, - data: { axes: state.axes, buttons: state.buttons } - }; - wsManager.send(payload); - }; - - const handleControlModal = (data) => { - if (isRequestControl) return; - if (!hasControlRight) { - messageApi.warning("当前网页端无控制权限"); - return; - } - if (data.control == true) { - showModal({ title: "权限请求", content: "是否请求控制权限?" }, 'switchPermission'); - } else { - setIsModalOpen(false); - } - } - - // 归一化地图显示的设备列表 const mapDevices = useMemo(() => { if (!device) return []; - const normalizedDevice = { + + return { ...device, lastRunningStatus: { ...device.lastRunningStatus, - longitude: runningStatus.longitude || device.longitude, - latitude: runningStatus.latitude || device.latitude, + longitude: + runningStatus.longitude || device.longitude, + latitude: + runningStatus.latitude || device.latitude, }, - name: device.deviceAlias || device.callsign || device.deviceName || '当前设备', - type: isUAV ? 'drone' : (device.productName?.includes('割草机') ? 'mower' : 'robot') + name: + device.deviceAlias || + device.callsign || + device.deviceName || + '设备', + type: isUAV + ? 'drone' + : device.productName?.includes('割草机') + ? 'mower' + : 'robot', }; - return [normalizedDevice]; }, [device, isUAV, runningStatus]); + const handleGamepadUpdate = useCallback((state) => { + controllerRef.current?.sendGamepad(state); + }, []); + + const handleControlModal = useCallback(() => { + controllerRef.current?.requestControl(); + }, []); + + const handleOk = () => { + controllerRef.current?.sendPermissionRequest( + true, + 'switchResult', + ); + + setIsModalOpen(false); + }; + + const handleCancel = () => { + controllerRef.current?.sendPermissionRequest( + false, + 'switchResult', + ); + + setIsModalOpen(false); + }; + + const fix2 = (val) => { + if (val === null || val === undefined || isNaN(val)) { + return '-'; + } + + return Number(val).toFixed(2); + }; + if (!device) { - return ; + return ( + + ); } return ( -
+
{contextHolder} -
+ +
选择设备: +