路径规划界面增加视频 封装拖拽组件 和 全景视频组件 设备状态界面增加完成点 机器人任务界面的topcard值的优化

This commit is contained in:
mmc
2026-06-15 11:30:32 +08:00
parent 3a542f1aad
commit 3fd82e2fd9
7 changed files with 136 additions and 112 deletions

8
package-lock.json generated
View File

@@ -33,7 +33,7 @@
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-draggable": "^4.5.0",
"react-draggable": "^4.6.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.14.1",
"recharts": "^3.8.1",
@@ -6545,9 +6545,9 @@
}
},
"node_modules/react-draggable": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
"integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.6.0.tgz",
"integrity": "sha512-g4vqY53xhmPrBnZvGP+1YQV0eYnB3o0VLzoi6q2IpwnQrxIZ34tYRKpVtsWIXPg4D/pvLn+oYCW5gOK2cWIrgA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1",

View File

@@ -36,7 +36,7 @@
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-draggable": "^4.5.0",
"react-draggable": "^4.6.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.14.1",
"recharts": "^3.8.1",

View File

@@ -0,0 +1,60 @@
import { PlayCircleOutlined, CloseOutlined } from '@ant-design/icons';
import React, { useRef } from 'react';
import Draggable from 'react-draggable';
import clsx from 'clsx';
interface Props {
children: React.ReactNode;
defaultPosition?: { x: number; y: number };
// 新增
title: string;
onClose: () => void;
}
const DraggableBox: React.FC<Props> = ({
children,
defaultPosition = { x: 0, y: 0 },
title,
onClose,
}) => {
const nodeRef = useRef<HTMLDivElement | null>(null);
return (
<Draggable
nodeRef={nodeRef}
defaultPosition={defaultPosition}
handle=".drag-header" // 仅标题栏可拖拽,内容区不触发拖拽
>
<div
ref={nodeRef}
className={clsx(
'fixed select-none z-[9999] bg-white shadow-xl rounded-lg overflow-hidden',
'border-[8px] border-slate-200 top-[8px] left-[8px] w-[600px] h-[300px]' // 整体8px外边框
)}
>
{/* 拖拽标题栏 + 关闭按钮 */}
<div className="drag-header flex items-center justify-between px-4 py-3 bg-slate-100 cursor-move border-b border-slate-200">
<div className="flex items-center gap-2">
<PlayCircleOutlined className="text-slate-600" />
<span className="font-semibold text-slate-800">{title}</span>
</div>
{/* 关闭按钮,阻止拖拽冒泡 */}
<div
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="w-6 h-6 flex items-center justify-center rounded hover:bg-red-100 cursor-pointer transition-colors"
>
<CloseOutlined className="text-slate-500 hover:text-red-500" />
</div>
</div>
{/* 传入的视频内容 */}
<div>{children}</div>
</div>
</Draggable>
);
};
export default DraggableBox;

View File

@@ -641,6 +641,7 @@ export default function DeviceOverviewPage({ onViewStatus, selectedSn, onDeviceS
title: '操作',
key: 'action',
width: 80,
fixed: 'right',
render: (_, record) => (
<Space size={4}>
{/* 编辑按钮绑定弹窗 */}
@@ -1007,10 +1008,7 @@ export default function DeviceOverviewPage({ onViewStatus, selectedSn, onDeviceS
)}
</Modal>
{/*原有视频放大弹窗*/}
{/*<Modal title="视频预览" open={!!zoomedVideo} onCancel={() => setZoomedVideo(null)} footer={null} width={1000} centered bodyStyle={{ padding: 0, height: 600, background: '#000' }} destroyOnClose>
{zoomedVideo && <DroneLivePlayer streamData={zoomedVideo} videoMode="LARGE" />}
</Modal>*/}
</Content>

View File

@@ -53,6 +53,7 @@ import VideoPlayer from '../VideoPlayer.tsx';
import DeviceController from './DeviceController';
import { DatePicker } from 'antd';
import dayjs from 'dayjs';
import { MowerAllView } from './MowerAllView';
import { useMqtt } from '@/hooks/useMqtt';
import envConfig from '../../../env';
const { RangePicker } = DatePicker;
@@ -70,6 +71,8 @@ export default function DeviceStatusPage({
const [devices, setDevices] = useState([]);
const [planedevices, setPlaneDevices] = useState([]);
const [devicesAll, setDevicesAll] = useState([]);
const [arrivedPoint, setArrivedPoint] = useState(null);//完成点
// 获取数据
const fetchAllData = async () => {
@@ -112,6 +115,11 @@ export default function DeviceStatusPage({
}
};
const handleFinishEdPoint = (point) => {
setArrivedPoint(point);
}
useEffect(() => {
fetchAllData();
}, [userInfo, stationId]);
@@ -367,6 +375,8 @@ export default function DeviceStatusPage({
controller.onHasControlRightChange = setHasControlRight;
controller.onRequestControlStatusChange = setConnecting;
controller.onWsStatusChange = setWsStatus;
controller.handleArrivedPoint = handleFinishEdPoint;
controller.onPermissionRequest = (content) => {
setModalContent(content);
@@ -628,7 +638,7 @@ export default function DeviceStatusPage({
>
{!isUAV && (
<Space style={{ marginBottom: 8 }}>
<Button type={mowerView === 'front' ? 'primary' : 'default'} icon={<CameraOutlined />} onClick={() => setMowerView('front')}>前视</Button>
<Button type={mowerView === 'front' ? 'primary' : 'default'} onClick={() => setMowerView('front')}>前视</Button>
<Button type={mowerView === 'back' ? 'primary' : 'default'} onClick={() => setMowerView('back')}>后视</Button>
<Button type={mowerView === 'left' ? 'primary' : 'default'} onClick={() => setMowerView('left')}>左视</Button>
<Button type={mowerView === 'right' ? 'primary' : 'default'} onClick={() => setMowerView('right')}>右视</Button>
@@ -658,101 +668,12 @@ export default function DeviceStatusPage({
) : robotVideo ? (
// 全景模式 = T 型排布
mowerView === 'all' ? (
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
gap: 8, // 窗口之间间距
padding: 8,
boxSizing: 'border-box',
overflow: 'auto' // 固定宽高后超出滚动
}}>
{/* 第一行:左视 + 前视 + 右视 三等分 */}
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
{/* 左视 */}
<div style={{
width: 360, // 固定宽度
height: 240, // 固定高度
position: 'relative',
border: '1px solid #eae4e4',
flexShrink: 0,// 禁止压缩
borderRadius: 8,
}}>
<VideoPlayer
deviceId={serialNumber}
idx={3}
className="w-full h-full object-contain"
video={robotVideo}
error={error}
ready={ready}
/>
<div style={{ position: 'absolute', top: 4, left: 8, color: '#fff', fontSize: 12, background: '#0006', padding: '2px 6px', borderRadius: 4 }}>左视</div>
</div>
{/* 前视 */}
<div style={{
width: 360,
height: 240,
position: 'relative',
border: '1px solid #eae4e4',
flexShrink: 0,// 禁止压缩
borderRadius: 8,
}}>
<VideoPlayer
deviceId={serialNumber}
idx={1}
className="w-full h-full object-contain"
video={robotVideo}
error={error}
ready={ready}
/>
<div style={{ position: 'absolute', top: 4, left: 8, color: '#fff', fontSize: 12, background: '#0006', padding: '2px 6px', borderRadius: 4 }}>前视</div>
</div>
{/* 右视 */}
<div style={{
width: 360,
height: 240,
position: 'relative',
border: '1px solid #eae4e4',
flexShrink: 0,// 禁止压缩
borderRadius: 8,
}}>
<VideoPlayer
deviceId={serialNumber}
idx={4}
className="w-full h-full object-contain"
video={robotVideo}
error={error}
ready={ready}
/>
<div style={{ position: 'absolute', top: 4, left: 8, color: '#fff', fontSize: 12, background: '#0006', padding: '2px 6px', borderRadius: 4 }}>右视</div>
</div>
</div>
{/* 第二行:后视 单独一行居中 */}
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{
width: 360,
height: 240,
position: 'relative',
border: '1px solid #eae4e4',
flexShrink: 0,// 禁止压缩
borderRadius: 8,
}}>
<VideoPlayer
deviceId={serialNumber}
idx={2}
className="w-full h-full object-contain"
video={robotVideo}
error={error}
ready={ready}
/>
<div style={{ position: 'absolute', top: 4, left: 8, color: '#fff', fontSize: 12, background: '#0006', padding: '2px 6px', borderRadius: 4 }}>后视</div>
</div>
</div>
</div>
<MowerAllView
serialNumber={serialNumber}
robotVideo={robotVideo}
error={error}
ready={ready}
/>
) : (
// 单画面模式
<VideoPlayer
@@ -822,6 +743,7 @@ export default function DeviceStatusPage({
realtimePosition={realtimePosition}
realtimeType={realtimeType}
drawRealTime={true}
finishPoint={arrivedPoint}
ifClearNavLine={clearNavLine}
ifClearPlanLine={clearPlanLine}
/>
@@ -987,6 +909,6 @@ export default function DeviceStatusPage({
>
<p>{modalContent.content}</p>
</Modal>
</div>
</div >
);
}

View File

@@ -33,6 +33,7 @@ import {
getSiteDeviceTaskHistory,
} from '../../api/device';
import dayjs from 'dayjs';
import DraggableBox from '../Draggable';
import utils, { getLocationQuality } from '../../lib/utils';
import CesiumMap from '../Map';
import { fix2, getVal } from '../../lib/utils';
@@ -40,6 +41,9 @@ import { useSelector } from 'react-redux';
import { RootState } from '@/src/store';
import envConfig from '../../../env';
import DeviceController from './DeviceController';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import { MowerAllView } from './mowerAllView.jsx';
import { useWebRTCPlayer } from '../useWebRTCStream.js';
const { Text, Title } = Typography;
const { RangePicker } = DatePicker;
@@ -54,6 +58,7 @@ export default function RobotTaskPage() {
const [taskList, setTaskList] = useState([]);
const [arrivedPoint, setArrivedPoint] = useState(null);//完成点
// 设备数据
const [devices, setDevices] = useState([]);
@@ -74,6 +79,7 @@ export default function RobotTaskPage() {
// 当前执行任务的机器人
const [executingRobot, setExecutingRobot] = useState(null);
const [showVideo, setShowVideo] = useState(false); // 控制视频组件显示
// 任务池和历史任务数据
const [taskPoolList, setTaskPoolList] = useState([]);
@@ -88,7 +94,10 @@ export default function RobotTaskPage() {
robot: null,
planId: null,
});
const { video: robotVideo, error, ready } = useWebRTCPlayer(
executingRobot?.serialNumber ? executingRobot?.serialNumber : '',
`webrtc://1.95.137.212/live/livestream/${executingRobot?.serialNumber}`,
);
const [historyFilterForm, setHistoryFilterForm] = useState({
taskStatus: null,
robot: null,
@@ -96,6 +105,9 @@ export default function RobotTaskPage() {
});
const fontSmall = 'clamp(10px, 0.7vw, 11px)';
const fontNormal = 'clamp(12px, 0.75vw, 13px)';
const fontTitle = 'clamp(14px, 0.85vw, 16px)';
@@ -114,6 +126,7 @@ export default function RobotTaskPage() {
const controllerRef = useRef<DeviceController | null>(null);
const handleFinish = (data) => {
setExecute(false);
setShowVideo(false); // 任务完成关闭视频
utils.message.success('任务已完成');
//if (controllerRef.current) {
// controllerRef.current.destroy?.();
@@ -128,7 +141,6 @@ export default function RobotTaskPage() {
}
const handleFinishEdPoint = (point) => {
console.log('到达完成点11111111111', point);
setArrivedPoint(point);
}
@@ -139,6 +151,7 @@ export default function RobotTaskPage() {
controller.onDeviceInfoUpdate = setRobotRealtimePosition;
controller.onIsFinished = handleFinish;
controller.handleArrivedPoint = handleFinishEdPoint;
controller.init();
@@ -377,6 +390,8 @@ export default function RobotTaskPage() {
fetchTaskPoolList();
fetchTaskHistoryList();
controllerRef.current?.connectDevice(selectedRobotForExecute);
setShowVideo(true); // 连接成功后自动打开视频
} else {
setExecute(false);
@@ -804,13 +819,13 @@ export default function RobotTaskPage() {
}
};
const groupBoardData = () => {
const groupBoardData = useMemo(() => {
const waitList = taskList.filter(item => item.taskStaus === 'NEW');
const runList = taskList.filter(item => ['EXECUTING', 'PAUSE'].includes(item.taskStaus));
const finishList = taskList.filter(item => ['FINISH', 'FAILED'].includes(item.taskStaus));
return { waitList, runList, finishList };
};
const { waitList, runList, finishList } = groupBoardData();
}, [taskList]);
const { waitList, runList, finishList } = groupBoardData;
const topCards = [
{ title: '今日机器人计划', value: taskList.length, unit: '条', desc: '进行中', icon: <LayoutGrid size={22} />, color: '#165DFF' },
@@ -1005,6 +1020,17 @@ export default function RobotTaskPage() {
return <span style={{ color: info.color }}>{info.text}</span>;
})()}
</span>
{/* 视频开关 */}
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
视频:
<Switch
size="small"
checked={showVideo}
onChange={setShowVideo}
/>
</span>
</div>
)}
@@ -1532,6 +1558,23 @@ export default function RobotTaskPage() {
</div>
</div>
</Modal>
{showVideo && executingRobot && (
<DraggableBox
defaultPosition={{ x: 50, y: 50 }}
title={`机器人全景视频 - ${executingRobot.deviceAlias || executingRobot.serialNumber}`}
onClose={() => setShowVideo(false)}
>
<div style={{ height: 'clamp(300px, 30vw, 600px)', overflow: 'hidden' }}>
<MowerAllView
serialNumber={executingRobot.serialNumber}
robotVideo={robotVideo}
ready={ready}
error={error}
/>
</div>
</DraggableBox>
)}
</div >
);
}

View File

@@ -47,7 +47,8 @@ export function useWebRTCPlayer(deviceId: string, url: string) {
v.style.height = '100%';
v.style.objectFit = 'contain';
videoRef.current = v;
const token = "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6Ijg4N2JiMWU4LTdmNjMtNDlhNy05NjVmLTRjMWFlOGMyZTIzYiJ9.PVeQayWLEPtHHWBQFtHb1Qkc5T-KVINPdXxChJ5vRasBpXOvyiGRfPPjWvB63aKdTZWLE2xV6r_8BCV0aphYmg"
const token = "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjFkZTA1ZDU2LTM2NTYtNDdmNS05NjI4LWM1NGZhNjRhOWU4OCJ9.Wpa9LO3fQ9DKV8Ct-VildxD4amrtCzTJWjPCGLgHu60P44PxRsArBSlAKhO9vrErmVRaHLe6_5Z_v5kegg07Aw"
//const token = localStorage.getItem('videotoken') || JSON.parse(localStorage.getItem('userInfo') || '{}')?.token;
const videoUrl = `${url}?token=${token}`;