无人机视频组件修改

This commit is contained in:
mmc
2026-05-27 14:30:46 +08:00
parent 6e4baa9bb7
commit 50f7a96751
4 changed files with 131 additions and 62 deletions

View File

@@ -215,4 +215,13 @@ export function getAllDevice(data) {
})
}
//根据单个机场获取信息/iot/device/getUAVState
export function getSingleUAV(data) {
return request({
url: `/iot/device/getUAVState`,
method: 'get',
params: data
})
}

View File

@@ -288,18 +288,19 @@ export default function DeviceStatusPage({
<div
style={{
//height: 450,
height: 450,
borderRadius: 8,
overflow: 'hidden',
display: 'flex',
flexWrap: 'wrap', // 允许换行
// flexWrap: 'wrap', // 左右排布不换行
}}
>
{isUAV ? (
<DroneVideoPlayer
device={device}
height={450}
showSourceSelect={true}
dualVideo={true}
showSourceSelect={false}
defaultSource="drone"
/>
) : robotVideo ? (

View File

@@ -11,6 +11,7 @@ interface DroneVideoPlayerProps {
device: any;
height?: number | string;
showSourceSelect?: boolean;
dualVideo?: boolean;
defaultSource?: 'station' | 'drone';
videoSource?: 'station' | 'drone';
onSourceChange?: (source: 'station' | 'drone') => void;
@@ -20,6 +21,7 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
device,
height = 160,
showSourceSelect = true,
dualVideo = false,
defaultSource = 'drone',
videoSource: propsVideoSource,
onSourceChange
@@ -35,8 +37,10 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
const [selectedLens, setSelectedLens] = useState<'wide' | 'zoom' | 'ir'>('wide');
// 当前选中的摄像头 index
const [selectedDroneCameraIdx, setSelectedDroneCameraIdx] = useState<number>(0);
const [streamData, setStreamData] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [droneStreamData, setDroneStreamData] = useState<any>(null);
const [stationStreamData, setStationStreamData] = useState<any>(null);
const [loadingDrone, setLoadingDrone] = useState(false);
const [loadingStation, setLoadingStation] = useState(false);
const switchingLock = useRef(false);
@@ -45,11 +49,11 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
setSelectedCamera(cameraPosition);
if (!device?.gateway_sn || switchingLock.current) return;
switchingLock.current = true;
setLoading(true);
setLoadingStation(true);
try {
const cameraIndex = device?.gateway_camera_list[0]?.camera_index || ""; // 目前先默认用第一个摄像头
if (!cameraIndex) {
setStreamData(null);
setStationStreamData(null);
utils.message.warning(`未找到${cameraPosition === 'indoor' ? '室内' : '室外'}摄像头配置`);
return;
}
@@ -62,12 +66,12 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
videoExpire: 7200,
});
if (res.code == 200) setStreamData(res.data);
if (res.code == 200) setStationStreamData(res.data);
else utils.message.error(res.msg || '切换机场摄像头失败');
} catch (err) {
console.error('切换机场摄像头失败', err);
} finally {
setLoading(false);
setLoadingStation(false);
switchingLock.current = false;
}
};
@@ -78,7 +82,7 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
if (!device?.device_sn || switchingLock.current) return;
switchingLock.current = true;
setLoading(true);
setLoadingDrone(true);
try {
const cameraList = device.drone_camera_list || [];
@@ -98,12 +102,12 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
videoExpire: 7200,
});
if (res.code === 200) setStreamData(res.data);
if (res.code === 200) setDroneStreamData(res.data);
else utils.message.error(res.msg || '切换无人机镜头失败');
} catch (err) {
console.error('切换无人机镜头失败', err);
} finally {
setLoading(false);
setLoadingDrone(false);
switchingLock.current = false;
}
};
@@ -116,14 +120,22 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
// 自动加载
useEffect(() => {
if (device) {
setStreamData(null);
if (videoSource === 'station') {
if (dualVideo) {
setDroneStreamData(null);
setStationStreamData(null);
loadStationCamera(selectedCamera);
} else {
loadDroneLens(selectedLens);
} else {
if (videoSource === 'station') {
setStationStreamData(null);
loadStationCamera(selectedCamera);
} else {
setDroneStreamData(null);
loadDroneLens(selectedLens);
}
}
}
}, [device, videoSource, selectedDroneCameraIdx]);
}, [device, videoSource, selectedDroneCameraIdx, dualVideo]);
// 生成摄像头下拉选项
const droneCameraOptions = device?.drone_camera_list?.map((item: any, idx: number) => ({
@@ -131,30 +143,18 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
value: idx,
})) || [];
return (
<div style={{ width: '100%' }}>
{showSourceSelect && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<Select
size="small"
style={{ width: 120 }}
value={videoSource}
onChange={(val) => setVideoSource(val)}
options={[
{ label: '机场视频', value: 'station' },
{ label: '无人机视频', value: 'drone' },
]}
/>
</div>
)}
const renderVideo = (type: 'drone' | 'station', streamData: any, loading: boolean) => {
const isDrone = type === 'drone';
return (
<div style={{
width: '100%',
height,
flex: 1,
height: height,
borderRadius: 8,
position: 'relative',
overflow: 'hidden',
background: '#000'
background: '#000',
border: dualVideo ? '1px solid #243157' : 'none',
minWidth: dualVideo ? '300px' : 'auto' // 确保在双视频模式下有最小宽度
}}>
{/* 视频控制按钮 */}
<div style={{
@@ -166,26 +166,25 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
zIndex: 10,
flexWrap: 'wrap',
}}>
{videoSource === 'station' ? (
{!isDrone ? (
<>
<Button disabled={loading} size="small" type="text" style={{
background: selectedCamera === 'indoor' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, borderRadius: 4
}} onClick={() => loadStationCamera('indoor')}>室内</Button>
<Button disabled={loading} size="small" type="text" style={{
background: selectedCamera === 'outdoor' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, borderRadius: 4
}} onClick={() => loadStationCamera('outdoor')}>室外</Button>
</>
) : (
<>
{/* 动态摄像头下拉框 */}
{droneCameraOptions.length > 0 && (
{droneCameraOptions.length > 1 && (
<Select
disabled={loading}
size="small"
style={{ width: 80, fontSize: 10 }}
value={selectedDroneCameraIdx}
onChange={switchDroneCamera}
options={droneCameraOptions}
@@ -195,17 +194,17 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
<Button disabled={loading} size="small" type="text" style={{
background: selectedLens === 'wide' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, borderRadius: 4
}} onClick={() => loadDroneLens('wide')}>广角</Button>
<Button disabled={loading} size="small" type="text" style={{
background: selectedLens === 'zoom' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, borderRadius: 4
}} onClick={() => loadDroneLens('zoom')}>变焦</Button>
<Button disabled={loading} size="small" type="text" style={{
background: selectedLens === 'ir' ? '#1890ff' : 'rgba(0,0,0,0.4)',
color: '#fff', fontSize: 12, padding: '0 6px', height: 22, borderRadius: 4
color: '#fff', fontSize: 10, padding: '0 4px', height: 20, borderRadius: 4
}} onClick={() => loadDroneLens('ir')}>红外</Button>
</>
)}
@@ -231,8 +230,22 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
</div>
)}
<div style={{
position: 'absolute',
top: 6,
left: 6,
background: 'rgba(0,0,0,0.5)',
color: '#fff',
padding: '2px 6px',
borderRadius: 4,
fontSize: 10,
zIndex: 5
}}>
{isDrone ? '无人机视频' : '机场视频'}
</div>
{/* 底部信息叠加 */}
{videoSource === 'drone' && device && (
{isDrone && device && (
<div style={{
position: 'absolute',
bottom: 0,
@@ -242,14 +255,52 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
color: '#fff',
display: 'flex',
justifyContent: 'space-between',
padding: '8px 12px',
fontSize: 12
padding: '4px 8px',
fontSize: 10
}}>
<span>高度 {device.height?.toFixed(2) || '-'}m</span>
<span>风速 {device.wind_speed?.toFixed(2) || '-'}m/s</span>
</div>
)}
</div>
);
};
return (
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
{!dualVideo && showSourceSelect && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<Select
size="small"
style={{ width: 120 }}
value={videoSource}
onChange={(val) => setVideoSource(val)}
options={[
{ label: '机场视频', value: 'station' },
{ label: '无人机视频', value: 'drone' },
]}
/>
</div>
)}
<div style={{
width: '100%',
flex: 1,
display: 'flex',
flexDirection: 'row',
gap: dualVideo ? 8 : 0
}}>
{dualVideo ? (
<>
{renderVideo('drone', droneStreamData, loadingDrone)}
{renderVideo('station', stationStreamData, loadingStation)}
</>
) : (
videoSource === 'station'
? renderVideo('station', stationStreamData, loadingStation)
: renderVideo('drone', droneStreamData, loadingDrone)
)}
</div>
</div>
);
};

View File

@@ -180,6 +180,7 @@ export default function WayLinePage({ planedevices }) {
utils.message.success('创建任务成功');
resetModals();
getListTask();
} else {
utils.message.error(res.msg || '创建失败');
}
@@ -359,23 +360,27 @@ export default function WayLinePage({ planedevices }) {
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
];
const getTrackFn = async (record) => {
const res = await getsinglePlanTrack(record);
if (res.code === 0 && res.data.track?.points?.length) {
const points = res.data.track.points.map(item => ({
lon: item.longitude || item.lon,
lat: item.latitude || item.lat
}));
setPoints(points);
} else {
utils.message.warning("暂无航线数据");
setPoints([]);
}
}
// 表格行点击查看轨迹
const handleRowClick = async (record) => {
setSelectedTask(record);
try {
getTaskDetail(record.uuid);
const res = await getsinglePlanTrack(record);
if (res.code === 0 && res.data.track?.points?.length) {
const points = res.data.track.points.map(item => ({
lon: item.longitude || item.lon,
lat: item.latitude || item.lat
}));
setPoints(points);
} else {
utils.message.warning("暂无航线数据");
setPoints([]);
}
await getTrackFn(record);
} catch (err) {
message.error('请求失败');
}
@@ -437,6 +442,11 @@ export default function WayLinePage({ planedevices }) {
utils.message.success('航线执行任务创建成功');
setRthModalVisible(false);
getListTask();
//await getTrackFn({
// uuid: res.data.task_uuid
//});
} else {
utils.message.error(res.msg || '执行失败');
}
@@ -1456,9 +1466,7 @@ export default function WayLinePage({ planedevices }) {
onChange={val => setTempRthAltitude(val || 100)}
style={{ width: '100%' }}
/>
<div style={{ marginTop: 4, fontSize: 12, color: '#86909C' }}>
建议高度:80m - 150m,请确保高于周围障碍物。
</div>
</div>
</div>
</Modal>