|
|
|
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
|
|
|
|
|
Typography, Space, Progress, Badge, Timeline, message,
|
|
|
|
|
Select, Modal
|
|
|
|
|
Select, Modal, Space as AntSpace
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
EnvironmentOutlined, RobotOutlined, BranchesOutlined,
|
|
|
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
|
|
|
|
|
import { useSelector } from 'react-redux';
|
|
|
|
|
import { changeCameragateway, getDeviceListApi, getLiveStream, getPlaneDeviceList } from '../../api/device.ts';
|
|
|
|
|
import { changeCameragateway, changeCameraPlane, getDeviceListApi, getLiveStream, getPlaneDeviceList } from '../../api/device.ts';
|
|
|
|
|
import { RootState } from '@/src/store';
|
|
|
|
|
import DroneLivePlayer from '../VolcRtcPlayer.jsx';
|
|
|
|
|
import AgoraTextTest from '../AgoraRtc.tsx';
|
|
|
|
|
@@ -46,10 +46,13 @@ const logs = [
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export default function DeviceOverviewPage({ devices, planedevices, devicesAll, onViewStatus, selectedSn, onDeviceSelect }) {
|
|
|
|
|
const [planeVideo, setPlaneVideo] = useState([]);
|
|
|
|
|
const [planeVideo, setPlaneVideo] = useState(null);
|
|
|
|
|
const [zoomedVideo, setZoomedVideo] = useState(null);
|
|
|
|
|
const [mowerView, setMowerView] = useState('front');
|
|
|
|
|
const videoTypeRef = useRef('drone'); // drone → 无人机镜头 station → 机场镜头
|
|
|
|
|
const [videoSource, setVideoSource] = useState('station'); // station / drone
|
|
|
|
|
const videoTypeRef = useRef('station');
|
|
|
|
|
const [selectedCamera, setSelectedCamera] = useState('outdoor'); // 机场默认室外
|
|
|
|
|
const [selectedLens, setSelectedLens] = useState('wide'); // 飞机默认广角
|
|
|
|
|
|
|
|
|
|
const selectedDevice = React.useMemo(() => {
|
|
|
|
|
const sn = selectedSn || (devicesAll?.[0]?.device_sn || devicesAll?.[0]?.serialNumber);
|
|
|
|
|
@@ -100,35 +103,71 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
|
|
|
|
const handleRecharge = () => message.info('回充指令已下发');
|
|
|
|
|
const handleEmergencyStop = () => message.error('已触发急停!');
|
|
|
|
|
|
|
|
|
|
// ====================== ✅ 核心:无人机视频获取逻辑(优先无人机 → 无则机场) ======================
|
|
|
|
|
const getDroneLiveStream = async () => {
|
|
|
|
|
// 加载机场摄像头
|
|
|
|
|
const loadStationCamera = async (cameraPosition) => {
|
|
|
|
|
console.log(cameraPosition, "cameraPosition")
|
|
|
|
|
if (!selectedDevice?.gateway_sn) return;
|
|
|
|
|
console.log(selectedDevice, "selectedDevice")
|
|
|
|
|
console.log(selectedDevice, "selectedDevice", cameraPosition)
|
|
|
|
|
try {
|
|
|
|
|
if (selectedDevice.gateway_camera_list && selectedDevice.gateway_camera_list.length > 0) {
|
|
|
|
|
const droneData = {
|
|
|
|
|
sn: selectedDevice.gateway_sn,
|
|
|
|
|
cameraIndex: selectedDevice.gateway_camera_list.filter(item => item.camera_position
|
|
|
|
|
== "indoor")[0]?.camera_index || "",
|
|
|
|
|
|
|
|
|
|
cameraPosition: "indoor",
|
|
|
|
|
qualityType: "adaptive",
|
|
|
|
|
videoExpire: 7200,
|
|
|
|
|
};
|
|
|
|
|
changeCameragateway(droneData).then(res => {
|
|
|
|
|
if (res.code == 200) {
|
|
|
|
|
setPlaneVideo(res.data)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
const cameraIndex = selectedDevice.gateway_camera_list?.find(
|
|
|
|
|
item => item.camera_position == cameraPosition
|
|
|
|
|
)?.camera_index || "";
|
|
|
|
|
console.log(cameraIndex, "cameraIndex")
|
|
|
|
|
if (!cameraIndex) {
|
|
|
|
|
setPlaneVideo({});
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const res = await changeCameragateway({
|
|
|
|
|
sn: selectedDevice.gateway_sn,
|
|
|
|
|
cameraIndex,
|
|
|
|
|
cameraPosition,
|
|
|
|
|
qualityType: "adaptive",
|
|
|
|
|
videoExpire: 7200,
|
|
|
|
|
});
|
|
|
|
|
if (res.code == 200) {
|
|
|
|
|
setPlaneVideo(res.data);
|
|
|
|
|
videoTypeRef.current = 'station';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setPlaneVideo({});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('视频拉流失败', err);
|
|
|
|
|
setPlaneVideo({});
|
|
|
|
|
console.error('切换机场摄像头失败', err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 加载无人机镜头
|
|
|
|
|
const loadDroneLens = async (lensType) => {
|
|
|
|
|
console.log(lensType, "lensType")
|
|
|
|
|
if (!selectedDevice?.gateway_sn || !selectedDevice?.device_sn) return;
|
|
|
|
|
const cameraIndex = selectedDevice.drone_camera_list
|
|
|
|
|
?.find(item => item.drone_camera_list == cameraPosition
|
|
|
|
|
)?.camera_index || "";
|
|
|
|
|
console.log(cameraIndex, "cameraIndex")
|
|
|
|
|
if (!cameraIndex) return;
|
|
|
|
|
try {
|
|
|
|
|
const res = await changeCameraPlane({
|
|
|
|
|
drone_sn: selectedDevice.device_sn,
|
|
|
|
|
lensType,
|
|
|
|
|
});
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
setPlaneVideo(res.data);
|
|
|
|
|
videoTypeRef.current = 'drone';
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('切换无人机镜头失败', err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 自动加载默认画面
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isUAV) {
|
|
|
|
|
setPlaneVideo(null);
|
|
|
|
|
if (videoSource === 'station') {
|
|
|
|
|
loadStationCamera('outdoor');
|
|
|
|
|
} else {
|
|
|
|
|
loadDroneLens('wide');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [selectedDevice, videoSource]);
|
|
|
|
|
|
|
|
|
|
const fix2 = (val) => {
|
|
|
|
|
if (val === null || val === undefined || isNaN(val)) return '-';
|
|
|
|
|
return Number(val).toFixed(2);
|
|
|
|
|
@@ -136,13 +175,6 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
|
|
|
|
|
|
|
|
|
const getVal = (val) => val == "no_rain" ? "无" : (val || '-');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isUAV) {
|
|
|
|
|
setPlaneVideo({});
|
|
|
|
|
getDroneLiveStream();
|
|
|
|
|
}
|
|
|
|
|
}, [selectedDevice]);
|
|
|
|
|
|
|
|
|
|
const handleDeviceChange = (id) => {
|
|
|
|
|
if (onDeviceSelect) onDeviceSelect(id);
|
|
|
|
|
};
|
|
|
|
|
@@ -312,32 +344,84 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: "space-between", flexWrap: 'wrap', gap: 6 }}>
|
|
|
|
|
<Button type="primary" size="small" style={{ borderRadius: 8, flex: 1 }} onClick={handleStart}>启动</Button>
|
|
|
|
|
<Button style={{ borderRadius: 8, flex: 1 }} size="small" onClick={handlePause}>暂停</Button>
|
|
|
|
|
<Button type="primary" style={{ borderRadius: 8, flex: 1 }} size="small" onClick={handleReturn}>返航</Button>
|
|
|
|
|
<Button style={{ flex: 1, background: '#52c41a', borderColor: '#52c41a', color: "#fff", borderRadius: 8, }} size="small" onClick={handleRecharge}>回充</Button>
|
|
|
|
|
<Button type="primary" style={{ borderRadius: 8, flex: 1 }} onClick={handleStart}>启动</Button>
|
|
|
|
|
<Button style={{ borderRadius: 8, flex: 1 }} onClick={handlePause}>暂停</Button>
|
|
|
|
|
<Button type="primary" style={{ borderRadius: 8, flex: 1 }} onClick={handleReturn}>返航</Button>
|
|
|
|
|
<Button style={{ flex: 1, background: '#52c41a', borderColor: '#52c41a', color: "#fff", borderRadius: 8, }} onClick={handleRecharge}>回充</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<div style={{ marginBottom: 8 }}>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
|
|
|
|
<span style={{ fontSize: 14, fontWeight: 500, color: "#666" }}>实时画面</span>
|
|
|
|
|
{!isUAV && (
|
|
|
|
|
<Select size="small" style={{ width: 110 }} value={mowerView} onChange={setMowerView}
|
|
|
|
|
options={[{ label: '前视', value: 'front' }, { label: '后视', value: 'back' }, { label: '左视', value: 'left' }, { label: '右视', value: 'right' }]}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{/* 显示当前镜头类型 */}
|
|
|
|
|
{isUAV && (
|
|
|
|
|
<Tag size="small" color={videoTypeRef.current === 'drone' ? 'blue' : 'orange'}>
|
|
|
|
|
{videoTypeRef.current === 'drone' ? '无人机镜头' : '机场镜头'}
|
|
|
|
|
</Tag>
|
|
|
|
|
<Select
|
|
|
|
|
size="small"
|
|
|
|
|
style={{ width: 120 }}
|
|
|
|
|
value={videoSource}
|
|
|
|
|
onChange={setVideoSource}
|
|
|
|
|
options={[
|
|
|
|
|
{ label: '机场视频', value: 'station' },
|
|
|
|
|
{ label: '无人机视频', value: 'drone' },
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 视频容器 */}
|
|
|
|
|
<div style={{ width: '100%', height: 160, borderRadius: 8, position: 'relative', overflow: 'hidden', background: '#000' }}>
|
|
|
|
|
|
|
|
|
|
{/* 右上角摄像头切换按钮 */}
|
|
|
|
|
{isUAV && (
|
|
|
|
|
<div style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: '6px',
|
|
|
|
|
right: '6px',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
gap: '4px',
|
|
|
|
|
zIndex: 10,
|
|
|
|
|
}}>
|
|
|
|
|
{videoSource === 'station' ? (
|
|
|
|
|
<>
|
|
|
|
|
<Button size="small" type="text" style={{
|
|
|
|
|
background: 'rgba(0,0,0,0.4)', color: '#fff', fontSize: '12px', padding: '0 2px', height: '22px',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
...(selectedCamera === 'indoor' && { background: '#1890ff', color: '#fff' })
|
|
|
|
|
}} onClick={() => { setSelectedCamera('indoor'); loadStationCamera('indoor'); }}>室内</Button>
|
|
|
|
|
|
|
|
|
|
<Button size="small" type="text" style={{
|
|
|
|
|
background: 'rgba(0,0,0,0.4)', color: '#fff', fontSize: '12px', padding: '0 6px', height: '22px',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
...(selectedCamera === 'outdoor' && { background: '#1890ff', color: '#fff' })
|
|
|
|
|
}} onClick={() => { setSelectedCamera('outdoor'); loadStationCamera('outdoor'); }}>室外</Button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Button size="small" type="text" style={{
|
|
|
|
|
background: 'rgba(0,0,0,0.4)', color: '#fff', fontSize: '12px', padding: '0 6px', height: '22px',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
...(selectedLens === 'wide' && { background: '#1890ff', color: '#fff' })
|
|
|
|
|
}} onClick={() => { setSelectedLens('wide'); loadDroneLens('wide'); }}>广角</Button>
|
|
|
|
|
|
|
|
|
|
<Button size="small" type="text" style={{
|
|
|
|
|
background: 'rgba(0,0,0,0.4)', color: '#fff', fontSize: '12px', padding: '0 6px', height: '22px',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
...(selectedLens === 'zoom' && { background: '#1890ff', color: '#fff' })
|
|
|
|
|
}} onClick={() => { setSelectedLens('zoom'); loadDroneLens('zoom'); }}>变焦</Button>
|
|
|
|
|
|
|
|
|
|
<Button size="small" type="text" style={{
|
|
|
|
|
background: 'rgba(0,0,0,0.4)', color: '#fff', fontSize: '12px', padding: '0 6px', height: '22px',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
...(selectedLens === 'ir' && { background: '#1890ff', color: '#fff' })
|
|
|
|
|
}} onClick={() => { setSelectedLens('ir'); loadDroneLens('ir'); }}>红外</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 视频内容 */}
|
|
|
|
|
{isUAV ? (
|
|
|
|
|
planeVideo ? (
|
|
|
|
|
<DroneLivePlayer streamData={planeVideo} />
|
|
|
|
|
@@ -381,7 +465,6 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* 下面的卡片逻辑保持不变 */}
|
|
|
|
|
<Card title={<><ApiOutlined /> AI分析与操作建议</>} bordered={false} style={{ borderRadius: 12, marginBottom: 16 }}>
|
|
|
|
|
<Row gutter={16} style={{ marginBottom: 20 }}>
|
|
|
|
|
<Col span={6}><div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 12px 4px', borderRadius: 12, border: '1px solid #e8e8e8' }}><div style={{ width: 30, height: 30, borderRadius: '50%', background: 'rgba(255,77,79,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ff4d4f', fontSize: 20 }}><span role="img" aria-label="hotspot">🔥</span></div><div><div style={{ fontSize: 11, color: '#666' }}>热斑疑似</div><div style={{ fontSize: 22, fontWeight: 700, color: '#222' }}>2 <span style={{ fontSize: 12 }}>处</span></div></div></div></Col>
|
|
|
|
|
|