控制
This commit is contained in:
4
env.js
4
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/'
|
||||
},
|
||||
};
|
||||
|
||||
@@ -447,14 +447,14 @@ export default function UserRolePage() {
|
||||
: <Tag color="red">停用</Tag>
|
||||
},
|
||||
{
|
||||
title: '是否有设备权限', dataIndex: 'deviceOperatePerm', width: 80,
|
||||
title: '是否有设备权限', dataIndex: 'deviceOperatePerm', width: 140,
|
||||
render: (_, s) => s.deviceOperatePerm == true
|
||||
? <Tag color="green">有权限</Tag>
|
||||
: <Tag color="red">无权限</Tag>
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
width: 300,
|
||||
fixed: 'right',
|
||||
render: (_, r) => (
|
||||
<Space wrap>
|
||||
@@ -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' }}
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
@@ -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<DeviceController | null>(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 <Empty description="请选择一个设备以查看状态" style={{ marginTop: 100 }} />;
|
||||
return (
|
||||
<Empty
|
||||
description="请选择设备"
|
||||
style={{ marginTop: 100 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0px' }}>
|
||||
<div style={{ padding: 0 }}>
|
||||
{contextHolder}
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Text strong>选择设备:</Text>
|
||||
|
||||
<Select
|
||||
style={{ width: 280 }}
|
||||
placeholder="请选择要查看的设备"
|
||||
value={device?.device_sn || device?.serialNumber}
|
||||
onChange={(val) => onDeviceChange && onDeviceChange(val)}
|
||||
onChange={(val) => onDeviceChange?.(val)}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={devicesAll?.map(item => ({
|
||||
options={devicesAll?.map((item) => ({
|
||||
value: item.device_sn || item.serialNumber,
|
||||
label: item.deviceAlias || item.drone_callsign || item.callsign || item.deviceName || '未知设备'
|
||||
label:
|
||||
item.deviceAlias ||
|
||||
item.drone_callsign ||
|
||||
item.callsign ||
|
||||
item.deviceName ||
|
||||
'未知',
|
||||
}))}
|
||||
/>
|
||||
{device && (
|
||||
<Tag color="blue" style={{ marginLeft: 8 }}>
|
||||
{isUAV ? '无人机' : '机器人'}
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
<Tag color="blue">
|
||||
{isUAV ? '无人机' : '机器人'}
|
||||
</Tag>
|
||||
|
||||
{wsStatus === 'connected' && (
|
||||
<Badge status="processing" color="#52c41a" text="实时连接中" style={{ color: '#52c41a' }} />
|
||||
<Badge
|
||||
status="processing"
|
||||
color="#52c41a"
|
||||
text="实时连接中"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 左侧:视频和地图 */}
|
||||
<Col xs={24} lg={16}>
|
||||
<Card
|
||||
title={<><VideoCameraOutlined /> 实时视频监控 - {device.deviceAlias || device.callsign || '监控画面'}</>}
|
||||
title={
|
||||
<>
|
||||
<VideoCameraOutlined />
|
||||
实时视频监控
|
||||
</>
|
||||
}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12, marginBottom: 16 }}
|
||||
style={{ borderRadius: 12 }}
|
||||
bodyStyle={{ padding: 12 }}
|
||||
>
|
||||
{/* ====================== 机器人:视角切换按钮(只在机器人显示)====================== */}
|
||||
{!isUAV && (
|
||||
<Space style={{ marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<Button type={mowerView === 'front' ? 'primary' : 'default'} icon={<CameraOutlined />} onClick={() => setMowerView('front')}>前视</Button>
|
||||
<Button type={mowerView === 'back' ? 'primary' : 'default'} onClick={() => setMowerView('back')}>后视</Button>
|
||||
<Button type={mowerView === 'left' ? 'primary' : 'default'} onClick={() => setMowerView('left')}>左视</Button>
|
||||
@@ -348,75 +302,78 @@ export default function DeviceStatusPage({ devicesAll, serialNumber, onDeviceCha
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{/* ====================== 视频区域 ====================== */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: '8px',
|
||||
gridTemplateColumns: isUAV
|
||||
? (planeVideo.length > 1 ? '1fr 1fr' : '1fr')
|
||||
: (mowerView === 'all' ? '1fr 1fr' : '1fr'),
|
||||
gridTemplateRows: isUAV
|
||||
? (planeVideo.length > 2 ? '1fr 1fr' : '1fr')
|
||||
: (mowerView === 'all' ? '1fr 1fr' : '1fr'),
|
||||
height: 450,
|
||||
background: '#000',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* ---------------------- 无人机视频(保持原样)---------------------- */}
|
||||
<div
|
||||
style={{
|
||||
height: 450,
|
||||
background: '#000',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{isUAV ? (
|
||||
planeVideo.length > 0 ? (
|
||||
planeVideo.map((stream, index) => (
|
||||
<div key={index} style={{
|
||||
position: 'relative',
|
||||
gridColumn: (planeVideo.length === 3 && index === 0) ? 'span 2' : 'span 1'
|
||||
}}>
|
||||
<DroneLivePlayer streamData={stream} />
|
||||
<div style={{
|
||||
position: 'absolute', top: 8, left: 8,
|
||||
background: 'rgba(0,0,0,0.5)', color: '#fff',
|
||||
padding: '2px 8px', borderRadius: 4, fontSize: 12
|
||||
}}>
|
||||
{stream?.camera_position === 'indoor' ? '库内监控' : `画面 ${index + 1}`}
|
||||
</div>
|
||||
planeVideo.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<DroneLivePlayer streamData={s} />
|
||||
|
||||
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#666', gridColumn: 'span 2' }}>
|
||||
暂无实时画面
|
||||
<div style={{ color: '#ccc', margin: 'auto' }}>
|
||||
暂无视频
|
||||
</div>
|
||||
)
|
||||
) : robotVideo ? (
|
||||
<VideoPlayer
|
||||
deviceId={serialNumber}
|
||||
idx={
|
||||
mowerView === 'front'
|
||||
? 1
|
||||
: mowerView === 'back'
|
||||
? 2
|
||||
: mowerView === 'left'
|
||||
? 3
|
||||
: 4
|
||||
}
|
||||
className="w-full h-full object-contain"
|
||||
video={robotVideo}
|
||||
error={error}
|
||||
ready={ready}
|
||||
/>
|
||||
) : (
|
||||
/* ---------------------- 机器人视频(带视角切换)---------------------- */
|
||||
robotVideo ? (
|
||||
<VideoPlayer
|
||||
deviceId={serialNumber}
|
||||
idx={mowerView === "front" ? 1 : mowerView === "back" ? 2 : mowerView === "left" ? 3 : 4}
|
||||
className="w-full h-full object-contain"
|
||||
video={robotVideo}
|
||||
error={error}
|
||||
ready={ready}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ccc' }}>
|
||||
机器人视频加载中...
|
||||
</div>
|
||||
)
|
||||
<div style={{ color: '#ccc', margin: 'auto' }}>
|
||||
视频加载中...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={<><GlobalOutlined /> 位置态势图</>}
|
||||
title={
|
||||
<>
|
||||
<GlobalOutlined />
|
||||
位置态势图
|
||||
</>
|
||||
}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12 }}
|
||||
bodyStyle={{ padding: 12, height: 400 }}
|
||||
bodyStyle={{
|
||||
padding: 12,
|
||||
height: 400,
|
||||
}}
|
||||
>
|
||||
<CesiumMap devices={mapDevices} />
|
||||
<CesiumMap devices={[mapDevices]} />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:状态面板和控制 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card
|
||||
title={<><DashboardOutlined /> {isUAV ? '无人机状态' : '机器人状态'}详情</>}
|
||||
@@ -473,18 +430,27 @@ export default function DeviceStatusPage({ devicesAll, serialNumber, onDeviceCha
|
||||
</Descriptions>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{!isUAV && (
|
||||
<Card
|
||||
title={<><ControlOutlined /> 远程控制</>}
|
||||
title={
|
||||
<>
|
||||
<ControlOutlined />
|
||||
远程控制
|
||||
</>
|
||||
}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12, background: '#0f172a' }}
|
||||
headStyle={{ color: '#fff', borderBottom: '1px solid #1e293b' }}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
background: '#0f172a',
|
||||
}}
|
||||
headStyle={{ color: '#fff' }}
|
||||
>
|
||||
<XboxController
|
||||
onGamepadUpdate={handleGamepadUpdate}
|
||||
handleControlModal={handleControlModal}
|
||||
isControlled={isControlled}
|
||||
hasControlRight={hasControlRight}
|
||||
isConnecting={connecting}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
@@ -492,10 +458,10 @@ export default function DeviceStatusPage({ devicesAll, serialNumber, onDeviceCha
|
||||
</Row>
|
||||
|
||||
<Modal
|
||||
title={modalContent.title}
|
||||
open={isModalOpen}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
title={modalContent.title}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
|
||||
const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isConnecting }) => {
|
||||
const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isConnecting, hasControlRight }) => {
|
||||
const [gamepadState, setGamepadState] = useState<{
|
||||
buttons: boolean[];
|
||||
axes: number[];
|
||||
@@ -89,7 +89,7 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
|
||||
|
||||
const handleDisconnect = (e: GamepadEvent) => {
|
||||
console.log('❌ Gamepad disconnected');
|
||||
handleControlModal({ control: false });
|
||||
//handleControlModal({ control: false });
|
||||
setGamepadState(prev => ({ ...prev, connected: false }));
|
||||
};
|
||||
|
||||
@@ -103,6 +103,8 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
|
||||
}, []);
|
||||
|
||||
// UI mapping for Xbox Gamepad
|
||||
|
||||
//点火熄火 上面俩 AB升降底盘 XY割刀加减速 button急停
|
||||
// Buttons: 0:A, 1:B, 2:X, 3:Y, 4:LB, 5:RB, 6:LT, 7:RT, 8:Back, 9:Start, 10:LS, 11:RS, 12:Up, 13:Down, 14:Left, 15:Right
|
||||
const { buttons, axes, connected } = gamepadState;
|
||||
|
||||
@@ -113,8 +115,7 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
|
||||
return (
|
||||
<div className="relative w-full aspect-[4/3] flex items-center justify-center "
|
||||
onClick={() => {
|
||||
if (connected && !isControlled) {
|
||||
// 这里写点击后要执行的逻辑,比如打开权限请求弹窗
|
||||
if (connected && !isControlled && hasControlRight) {
|
||||
console.log("点击了无控制权限,发起权限请求");
|
||||
// 示例:调用你代码中已有的权限请求弹窗逻辑
|
||||
handleControlModal({ control: true, type: "switchPermission" });
|
||||
@@ -123,7 +124,7 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
|
||||
{/* Gamepad Status Badge */}
|
||||
<div
|
||||
className={`absolute top-0 right-0 px-2 py-0.5 rounded text-[8px] font-bold tracking-widest border
|
||||
${connected
|
||||
${connected || !hasControlRight
|
||||
? isConnecting
|
||||
? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
|
||||
: isControlled
|
||||
@@ -133,14 +134,17 @@ const XboxController = ({ onGamepadUpdate, handleControlModal, isControlled, isC
|
||||
}
|
||||
`}
|
||||
>
|
||||
{connected
|
||||
? isConnecting
|
||||
? '连接中...'
|
||||
: isControlled
|
||||
? '控制中'
|
||||
: '无控制权限'
|
||||
: '未连接设备'
|
||||
}
|
||||
{!connected ? (
|
||||
'未连接设备'
|
||||
) : !hasControlRight ? (
|
||||
'其他 Web 端已连接'
|
||||
) : isConnecting ? (
|
||||
'请求控制中...'
|
||||
) : isControlled ? (
|
||||
'控制中'
|
||||
) : (
|
||||
'点击获取控制权限'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<svg viewBox="0 0 400 300" className="w-full h-full drop-shadow-[0_0_20px_rgba(34,211,238,0.15)]">
|
||||
|
||||
@@ -1,73 +1,99 @@
|
||||
// src/components/device/deviceController.js
|
||||
import { message } from 'antd';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { wsManager } from '../WebSocketManager.ts';
|
||||
import { getControlRight, getJurisdiction } from '../../api/device.ts';
|
||||
import {
|
||||
getControlRight,
|
||||
getJurisdiction,
|
||||
} from '../../api/device.ts';
|
||||
|
||||
import envConfig from '../../../env';
|
||||
|
||||
export default class DeviceController {
|
||||
constructor(userInfo, messageApi) {
|
||||
this.userInfo = userInfo;
|
||||
this.messageApi = messageApi;
|
||||
this.wsUrl = envConfig.WS_URL || 'wss://ri.satabot.com/ws/';
|
||||
|
||||
// 状态
|
||||
this.wsUrl =
|
||||
envConfig.WS_URL || 'wss://ri.satabot.com/ws/';
|
||||
|
||||
this.connectedDeviceSn = null;
|
||||
|
||||
this.requestDeviceId = '';
|
||||
|
||||
this.isControlled = false;
|
||||
this.isRequestControl = false;
|
||||
this.hasControlRight = false;
|
||||
this.wsDeviceInfo = {};
|
||||
this.wsStatus = 'disconnected';
|
||||
|
||||
// 回调
|
||||
this.wsStatus = 'disconnected';
|
||||
this.wsDeviceInfo = {};
|
||||
|
||||
|
||||
this.controlTimeout = null;
|
||||
|
||||
this.onDeviceInfoUpdate = null;
|
||||
this.onControlStatusChange = null;
|
||||
this.onWsStatusChange = null;
|
||||
this.onHasControlRightChange = null;
|
||||
this.onRequestControlStatusChange = null;
|
||||
this.onPermissionRequest = null;
|
||||
|
||||
// 绑定 this
|
||||
this.handleWsMessage = this.handleWsMessage.bind(this);
|
||||
this.getControl = this.getControl.bind(this);
|
||||
this.connectDevice = this.connectDevice.bind(this);
|
||||
this.sendPermissionRequest = this.sendPermissionRequest.bind(this);
|
||||
this.handleWsStatus = this.handleWsStatus.bind(this);
|
||||
|
||||
this.sendGamepad = throttle(
|
||||
this.sendGamepad.bind(this),
|
||||
50,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化监听
|
||||
init() {
|
||||
wsManager.onMessage(this.handleWsMessage);
|
||||
wsManager.onStatusChange((status) => {
|
||||
this.wsStatus = status;
|
||||
if (this.onWsStatusChange) this.onWsStatusChange(status);
|
||||
});
|
||||
this.unSubMessage = wsManager.onMessage(
|
||||
this.handleWsMessage,
|
||||
);
|
||||
|
||||
this.unSubStatus = wsManager.onStatusChange(
|
||||
this.handleWsStatus,
|
||||
);
|
||||
}
|
||||
|
||||
// 销毁
|
||||
destroy() {
|
||||
this.clearControlTimeout();
|
||||
|
||||
this.unSubMessage?.();
|
||||
this.unSubStatus?.();
|
||||
|
||||
wsManager.close();
|
||||
}
|
||||
|
||||
// 获取设备控制权限
|
||||
async getControl(serialnum) {
|
||||
try {
|
||||
const data = { platform: 'web', deviceId: serialnum };
|
||||
const res = await getJurisdiction(data);
|
||||
const ok = res?.code === 200 && res?.data?.remoteControl === true;
|
||||
this.isControlled = ok;
|
||||
if (this.onControlStatusChange) this.onControlStatusChange(ok);
|
||||
} catch (err) {
|
||||
handleWsStatus(status) {
|
||||
this.wsStatus = status;
|
||||
|
||||
this.onWsStatusChange?.(status);
|
||||
|
||||
if (status === 'disconnected') {
|
||||
this.isControlled = false;
|
||||
if (this.onControlStatusChange) this.onControlStatusChange(false);
|
||||
|
||||
this.onControlStatusChange?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 连接设备 WebSocket
|
||||
async connectDevice(device) {
|
||||
if (!device) return;
|
||||
const sn = device.serialNumber || device.device_sn;
|
||||
const sn =
|
||||
device.serialNumber || device.device_sn;
|
||||
|
||||
this.connectedDeviceSn = null;
|
||||
|
||||
this.wsDeviceInfo = {};
|
||||
|
||||
this.isControlled = false;
|
||||
|
||||
this.onControlStatusChange?.(false);
|
||||
|
||||
wsManager.close();
|
||||
this.connectedDeviceSn = null;
|
||||
this.wsDeviceInfo = {};
|
||||
|
||||
try {
|
||||
const res = await getControlRight({
|
||||
@@ -76,23 +102,103 @@ export default class DeviceController {
|
||||
token: this.userInfo.token,
|
||||
});
|
||||
|
||||
if (res?.code === 200) {
|
||||
this.hasControlRight = true;
|
||||
wsManager.connect(sn, this.wsUrl);
|
||||
this.connectedDeviceSn = sn;
|
||||
await this.getControl(sn);
|
||||
} else {
|
||||
this.hasControlRight = false;
|
||||
|
||||
this.hasControlRight = res?.code === 200;
|
||||
|
||||
this.onHasControlRightChange?.(
|
||||
this.hasControlRight,
|
||||
);
|
||||
|
||||
if (!this.hasControlRight) {
|
||||
this.messageApi.warning('其他端正在控制');
|
||||
return;
|
||||
}
|
||||
|
||||
wsManager.connect(sn, this.wsUrl);
|
||||
|
||||
this.connectedDeviceSn = sn;
|
||||
|
||||
await this.getControl(sn);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
this.messageApi.error('权限校验失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 发送权限请求
|
||||
async getControl(serialnum) {
|
||||
try {
|
||||
const res = await getJurisdiction({
|
||||
platform: 'web',
|
||||
deviceId: serialnum,
|
||||
});
|
||||
|
||||
|
||||
this.isControlled =
|
||||
res?.code === 200 &&
|
||||
res?.data?.remoteControl === true;
|
||||
|
||||
this.onControlStatusChange?.(
|
||||
this.isControlled,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
this.isControlled = false;
|
||||
|
||||
this.onControlStatusChange?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
requestControl() {
|
||||
if (this.isRequestControl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasControlRight) {
|
||||
this.messageApi.warning('无控制权限');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRequestControl = true;
|
||||
|
||||
this.onRequestControlStatusChange?.(true);
|
||||
|
||||
this.messageApi.loading({
|
||||
key: 'control',
|
||||
content: '请求控制中...',
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
this.sendPermissionRequest(
|
||||
true,
|
||||
'switchPermission',
|
||||
);
|
||||
|
||||
this.clearControlTimeout();
|
||||
|
||||
this.controlTimeout = setTimeout(() => {
|
||||
if (!this.isRequestControl) return;
|
||||
|
||||
this.isRequestControl = false;
|
||||
|
||||
this.onRequestControlStatusChange?.(false);
|
||||
|
||||
this.messageApi.destroy('control');
|
||||
|
||||
this.messageApi.error('控制请求超时');
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
clearControlTimeout() {
|
||||
if (this.controlTimeout) {
|
||||
clearTimeout(this.controlTimeout);
|
||||
this.controlTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
sendPermissionRequest(data, type) {
|
||||
const wsData = {
|
||||
wsManager.send({
|
||||
type,
|
||||
switchResult: data,
|
||||
token: this.userInfo.token,
|
||||
@@ -101,61 +207,86 @@ export default class DeviceController {
|
||||
? this.connectedDeviceSn
|
||||
: this.requestDeviceId,
|
||||
userName: this.userInfo.username,
|
||||
};
|
||||
wsManager.send(wsData);
|
||||
}
|
||||
|
||||
// 处理外部请求控制
|
||||
requestControl() {
|
||||
if (this.isRequestControl || !this.hasControlRight) {
|
||||
if (!this.hasControlRight) this.messageApi.warning('当前网页端无控制权限');
|
||||
return;
|
||||
}
|
||||
this.isRequestControl = true;
|
||||
this.messageApi.loading({
|
||||
key: 'control',
|
||||
content: '权限请求中...',
|
||||
duration: 0,
|
||||
});
|
||||
this.sendPermissionRequest(true, 'switchPermission');
|
||||
}
|
||||
|
||||
// 处理 WebSocket 消息
|
||||
handleWsMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'deviceInfo':
|
||||
case 'deviceInfo': {
|
||||
const data = {};
|
||||
msg.data.forEach((item) => (data[item.name] = item.value));
|
||||
this.wsDeviceInfo = data;
|
||||
if (this.onDeviceInfoUpdate) this.onDeviceInfoUpdate(data);
|
||||
break;
|
||||
|
||||
case 'AUTH':
|
||||
if (msg.isAuth) this.getControl(this.connectedDeviceSn);
|
||||
break;
|
||||
|
||||
case 'switchResult':
|
||||
this.messageApi.success({
|
||||
content: '权限获取成功',
|
||||
key: 'control',
|
||||
duration: 2,
|
||||
msg.data.forEach((item) => {
|
||||
data[item.name] = item.value;
|
||||
});
|
||||
this.isRequestControl = false;
|
||||
this.isControlled = true;
|
||||
if (this.onControlStatusChange) this.onControlStatusChange(true);
|
||||
break;
|
||||
|
||||
case 'switchPermission':
|
||||
const platform = (msg.platform || '').toLowerCase().trim();
|
||||
if (!msg.platform || platform === 'web') return;
|
||||
this.requestDeviceId = msg.deviceId;
|
||||
if (this.onPermissionRequest) {
|
||||
this.onPermissionRequest({
|
||||
title: '权限请求',
|
||||
content: `${msg.platform} 请求控制 ${msg.deviceId}`,
|
||||
});
|
||||
}
|
||||
this.wsDeviceInfo = data;
|
||||
|
||||
this.onDeviceInfoUpdate?.(data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'AUTH': {
|
||||
if (msg.isAuth) {
|
||||
this.getControl(this.connectedDeviceSn);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'switchResult': {
|
||||
this.clearControlTimeout();
|
||||
|
||||
this.isRequestControl = false;
|
||||
|
||||
this.onRequestControlStatusChange?.(false);
|
||||
|
||||
if (
|
||||
msg.success === false ||
|
||||
msg.switchResult === false
|
||||
) {
|
||||
this.isControlled = false;
|
||||
|
||||
this.onControlStatusChange?.(false);
|
||||
|
||||
this.messageApi.error({
|
||||
key: 'control',
|
||||
content: msg.message || '控制失败',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isControlled = true;
|
||||
|
||||
this.onControlStatusChange?.(true);
|
||||
|
||||
this.messageApi.success({
|
||||
key: 'control',
|
||||
content: '控制成功',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'switchPermission': {
|
||||
const platform = (
|
||||
msg.platform || ''
|
||||
).toLowerCase();
|
||||
|
||||
if (!platform || platform === 'web' || platform == "null") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestDeviceId = msg.deviceId;
|
||||
|
||||
this.onPermissionRequest?.({
|
||||
title: '权限请求',
|
||||
content: `${msg.platform} 请求控制设备`,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'detectionReport':
|
||||
if (msg.data?.deviceId !== this.connectedDeviceSn) return;
|
||||
@@ -170,16 +301,28 @@ export default class DeviceController {
|
||||
duration: 3,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送游戏手柄指令
|
||||
sendGamepad(state) {
|
||||
if (!this.isControlled || !this.connectedDeviceSn) return;
|
||||
if (
|
||||
!this.isControlled ||
|
||||
!this.connectedDeviceSn
|
||||
) {
|
||||
return;
|
||||
}
|
||||
console.log(state, "state")
|
||||
|
||||
wsManager.send({
|
||||
type: 'gamepad',
|
||||
type: 'remoteControl',
|
||||
deviceId: this.connectedDeviceSn,
|
||||
data: { axes: state.axes, buttons: state.buttons },
|
||||
data: {
|
||||
axes: state.axes,
|
||||
buttons: state.buttons,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user