路径规划界面增加视频 封装拖拽组件 和 全景视频组件 设备状态界面增加完成点 机器人任务界面的topcard值的优化
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
60
src/components/Draggable.tsx
Normal file
60
src/components/Draggable.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user