获取单个无人机详情

This commit is contained in:
mmc
2026-05-27 15:08:26 +08:00
parent 50f7a96751
commit 2ab9e3715a
6 changed files with 194 additions and 46 deletions

View File

@@ -97,6 +97,15 @@ export function loginApi(data) {
// 可视化面板获取无人机信息
// 获取无人机实时状态详情
export function getUAVState(params) {
return api({
url: '/iot/device/getUAVState',
method: 'get',
params
})
}
// 获取所有航线

View File

@@ -40,6 +40,15 @@ export function getControlRight(params) {
})
}
// 获取无人机实时状态详情
export function getUAVState(params) {
return request({
url: '/iot/UAV/getUAVState',
method: 'get',
params
})
}
// 远程遥控 判断是否有权限/forward/device/remoteControl platform:web
export function getJurisdiction(data) {
return request({
@@ -215,13 +224,3 @@ export function getAllDevice(data) {
})
}
//根据单个机场获取信息/iot/device/getUAVState
export function getSingleUAV(data) {
return request({
url: `/iot/device/getUAVState`,
method: 'get',
params: data
})
}

View File

@@ -10,7 +10,7 @@ import {
X
} from 'lucide-react';
import XboxController from './XboxController';
import { getControlRight, getFlightTaskDetail, getJurisdiction, getLiveStream, getPlaneDeviceList, getsinglePlan, getsinglePlanTrack, getWayLine, getWorkData, getWorkList, postFlightCommmand, saveDeviceTask, updateFlightTaskStatus } from '@/api/api';
import { getControlRight, getFlightTaskDetail, getJurisdiction, getLiveStream, getPlaneDeviceList, getsinglePlan, getsinglePlanTrack, getUAVState, getWayLine, getWorkData, getWorkList, postFlightCommmand, saveDeviceTask, updateFlightTaskStatus } from '@/api/api';
import { message as antdMessage, Button, DatePicker, Input, InputNumber, Modal, Select, Tooltip } from 'antd';
import CesiumMap from './Map';
@@ -1071,6 +1071,12 @@ const RobotControlCenter = ({ setView }) => {
setIsModalOpen(false);
}
}
const [uavState, setUavState] = useState<any>(null);
const uavTimerRef = useRef<any>(null);
// 定时获取无人机状态
const calcPercent = (value, max) => {
const num = Number(value);
if (!Number.isFinite(num) || max <= 0) return 0;
@@ -1413,13 +1419,69 @@ const RobotControlCenter = ({ setView }) => {
<div>
<h1 className="text-xl font-bold text-white mb-1">
{selectedDevice?.name}
</h1>
</div>
<div className="flex gap-2">
<span className="bg-[#00e5ff]/10 text-[#00e5ff] text-xs px-3 py-1.5 rounded-lg border border-[#00e5ff]/30">视频链路已配置</span>
</div>
</div>
{/* 无人机状态概览 */}
{selectedDevice?.type === 'plane' && (uavState || selectedDevice) && (
<div className="grid grid-cols-5 gap-3 mb-6">
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5 flex items-center gap-1.5">
<Battery className="w-3.5 h-3.5" /> 电池电量
</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${((uavState || selectedDevice).capacity_percent || 0) < 20 ? 'bg-red-500' : 'bg-green-500'}`}
style={{ width: `${(uavState || selectedDevice).capacity_percent || 0}%` }}
></div>
</div>
<span className="text-sm font-bold text-white">{(uavState || selectedDevice).capacity_percent || 0}%</span>
</div>
</div>
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5 flex items-center gap-1.5">
<ArrowUp className="w-3.5 h-3.5" /> 飞行高度
</p>
<p className="text-sm font-bold text-white">
{Number((uavState || selectedDevice).height || 0).toFixed(2)} <span className="text-[10px] font-normal text-slate-400">m</span>
</p>
</div>
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5 flex items-center gap-1.5">
<Wifi className="w-3.5 h-3.5" /> 信号/卫星
</p>
<p className="text-sm font-bold text-white">
{(uavState || selectedDevice).position_state?.gps_number || 0} <span className="text-[10px] font-normal text-slate-400">GPS</span>
<span className="mx-1 text-slate-600">/</span>
{(uavState || selectedDevice).position_state?.rtk_number || 0} <span className="text-[10px] font-normal text-slate-400">RTK</span>
</p>
</div>
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5 flex items-center gap-1.5">
<Activity className="w-3.5 h-3.5" /> 环境风速
</p>
<p className="text-sm font-bold text-white">
{Number((uavState || selectedDevice).wind_speed || 0).toFixed(2)} <span className="text-[10px] font-normal text-slate-400">m/s</span>
</p>
</div>
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5 flex items-center gap-1.5">
<Settings className="w-3.5 h-3.5" /> RTK 状态
</p>
<div className="flex items-center gap-1.5">
<div className={`w-2 h-2 rounded-full ${(uavState || selectedDevice).position_state?.is_fixed === 'fixing_successful' ? 'bg-green-500' : 'bg-yellow-500'}`}></div>
<span className="text-xs font-bold text-white">
{(uavState || selectedDevice).position_state?.is_fixed === 'fixing_successful' ? '已固定' : '未固定'}
</span>
</div>
</div>
</div>
)}
{/*<div className="grid grid-cols-5 gap-3">
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-[#243157]/40 shadow-sm">
<p className="text-xs text-[#9ca3af] mb-1.5">当前位置</p>

View File

@@ -22,6 +22,7 @@ import AgoraTextTest from '../AgoraRtc.tsx';
import { useWebRTCPlayer } from '../useWebRTCStream.tsx';
import VideoPlayer from '../VideoPlayer.tsx.tsx';
import CesiumMap from '../Map.jsx';
import { getUAVState } from '@/api/device.ts';
const { Content } = Layout;
const { Text, Title } = Typography;
@@ -48,6 +49,7 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
const [zoomedVideo, setZoomedVideo] = useState(null);
const [mowerView, setMowerView] = useState('front');
const [videoSource, setVideoSource] = useState('drone'); // 恢复 videoSource 状态管理
const [uavState, setUavState] = useState(null);
const selectedDevice = React.useMemo(() => {
const sn = selectedSn || (devicesAll?.[0]?.device_sn || devicesAll?.[0]?.serialNumber);
@@ -109,6 +111,31 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
if (onDeviceSelect) onDeviceSelect(id);
};
useEffect(() => {
const fetchUavState = async () => {
if (isUAV && selectedDevice?.gateway_sn && selectedDevice?.device_sn) {
try {
const res = await getUAVState({
sn: selectedDevice.gateway_sn,
droneSn: selectedDevice.device_sn
});
if (res.code === 200) {
setUavState(res.data);
}
} catch (err) {
console.error('获取无人机状态失败', err);
}
} else {
setUavState(null);
}
};
fetchUavState();
}, [isUAV, selectedDevice]);
return (
<Content style={{ margin: '0 8', }}>
<Row gutter={[8, 8]} style={{ marginBottom: 12, flexWrap: 'wrap' }}>
@@ -226,12 +253,12 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ color: '#52c41a' }}><ThunderboltOutlined /></span>
<span style={{ fontSize: 12 }}>电量 {fix2(selectedDevice.capacity_percent || 100)}%</span>
<span style={{ fontSize: 12 }}>电量 {fix2((uavState || selectedDevice).capacity_percent || 100)}%</span>
</div>
{selectedDevice.environment_temperature && (
{(uavState || selectedDevice).environment_temperature && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 12 }}>温度</span>
<span style={{ color: '#faad14' }}>{fix2(selectedDevice.environment_temperature || 25)}℃</span>
<span style={{ color: '#faad14' }}>{fix2((uavState || selectedDevice).environment_temperature || 25)}℃</span>
</div>
)}
</div>
@@ -265,11 +292,22 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#666', fontSize: 12 }}>飞行高度</span>
<span style={{ fontSize: 12 }}>{fix2(selectedDevice.height)} m</span>
<span style={{ fontSize: 12 }}>{fix2((uavState || selectedDevice).height)} m</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#666', fontSize: 12 }}>飞行风速</span>
<span style={{ fontSize: 12 }}>{fix2(selectedDevice.wind_speed)} m/s</span>
<span style={{ fontSize: 12 }}>{fix2((uavState || selectedDevice).wind_speed)} m/s</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#666', fontSize: 12 }}>rtk数量</span>
<span style={{ fontSize: 12 }}>
{/*<Badge count={(uavState || selectedDevice).position_state?.gps_number || 0} style={{ backgroundColor: '#1890ff', transform: 'scale(0.8)' }} />
<span style={{ margin: '0 2px' }}>/</span>*/}
<Badge
showZero={true}
count={(uavState || selectedDevice)?.position_state?.rtk_number || 0}
style={{ backgroundColor: '#52c41a', transform: 'scale(0.8)' }}
/> </span>
</div>
</>
)}
@@ -308,6 +346,8 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
<DroneVideoPlayer
device={selectedDevice}
height={160}
wind_speed={uavState?.wind_speed}
h={uavState?.height}
showSourceSelect={false}
videoSource={videoSource}
onSourceChange={setVideoSource}
@@ -374,10 +414,10 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll,
<Card title="环境与飞行条件" bordered={false} style={{ borderRadius: 12, marginBottom: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, alignItems: "center" }}>
<div style={{ display: "flex", border: "1px solid #e8e8e8", borderRadius: 12, overflow: "hidden", flex: 1 }}>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>风速</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2(selectedDevice?.wind_speed)} m/s</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>温度</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2(selectedDevice?.environment_temperature)}℃</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>降雨</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{getVal(selectedDevice?.rainfall)}</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>RTK</div><div style={{ fontSize: 14, fontWeight: 700, color: '#52c41a', marginTop: 2 }}>{selectedDevice?.position_state?.is_fixed === 'fixing_successful' ? '固定' : '未固定'}</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>风速</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2((uavState || selectedDevice)?.wind_speed)} m/s</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>温度</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{fix2((uavState || selectedDevice)?.environment_temperature)}℃</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px", borderRight: "1px solid #e8e8e8" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>降雨</div><div style={{ fontSize: 14, fontWeight: 700, color: '#222', marginTop: 2 }}>{getVal((uavState || selectedDevice)?.rainfall)}</div></div>
<div style={{ flex: 1, textAlign: 'center', whiteSpace: "nowrap", padding: "12px 4px" }}><div style={{ fontSize: 11, color: '#666', fontWeight: 600 }}>RTK</div><div style={{ fontSize: 14, fontWeight: 700, color: '#52c41a', marginTop: 2 }}>{(uavState || selectedDevice)?.position_state?.is_fixed === 'fixing_successful' ? '固定' : '未固定'}</div></div>
</div>
<div style={{ width: 110, padding: '12px 4px', borderRadius: 8, background: '#f6ffed', border: '1px solid #b7eb8f', textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4 }}>
<div className="flex items-center gap-1">

View File

@@ -33,6 +33,12 @@ import {
EnvironmentOutlined,
} from '@ant-design/icons';
import {
getsinglePlanTrack,
getFlightTaskDetail,
getUAVState,
} from '../../api/device.ts';
import XboxController from './XboxController';
import DroneVideoPlayer from './DroneVideoPlayer';
import CesiumMap from '../Map.jsx';
@@ -78,6 +84,8 @@ export default function DeviceStatusPage({
});
const [mowerView, setMowerView] = useState('front');
const [uavState, setUavState] = useState<any>(null);
const timerRef = useRef<any>(null);
const device = useMemo(() => {
const sn =
@@ -144,6 +152,31 @@ export default function DeviceStatusPage({
controllerRef.current.connectDevice(device);
}, [device]);
// 定时获取无人机状态
useEffect(() => {
const fetchUavState = async () => {
if (isUAV && device?.gateway_sn && device?.device_sn) {
try {
const res = await getUAVState({
sn: device.gateway_sn,
droneSn: device.device_sn
});
if (res.code === 200) {
setUavState(res.data);
}
} catch (err) {
console.error('获取无人机状态失败', err);
}
} else {
setUavState(null);
}
};
fetchUavState();
}, [isUAV, device]);
const mapDevices = useMemo(() => {
if (!device) return [];
@@ -413,26 +446,26 @@ export default function DeviceStatusPage({
{isUAV ? (
<Descriptions column={2} size="small" layout="vertical">
<Descriptions.Item label={<><ThunderboltOutlined /> 电池电量</>}>
<Progress percent={device.capacity_percent || 0} size="small" status={(device.capacity_percent || 0) < 20 ? 'exception' : 'active'} />
<Progress percent={(uavState || device).capacity_percent || 0} size="small" status={((uavState || device).capacity_percent || 0) < 20 ? 'exception' : 'active'} />
</Descriptions.Item>
<Descriptions.Item label={<><EnvironmentOutlined /> 飞行高度</>}>
<Text strong style={{ fontSize: 16 }}>{fix2(device.height)} m</Text>
<Text strong style={{ fontSize: 16 }}>{fix2((uavState || device).height)} m</Text>
</Descriptions.Item>
<Descriptions.Item label="环境风速"><Text>{fix2(device.wind_speed)} m/s</Text></Descriptions.Item>
<Descriptions.Item label="环境温度"><Text>{fix2(device.environment_temperature)} ℃</Text></Descriptions.Item>
<Descriptions.Item label="环境风速"><Text>{fix2((uavState || device).wind_speed)} m/s</Text></Descriptions.Item>
<Descriptions.Item label="环境温度"><Text>{fix2((uavState || device).environment_temperature)} ℃</Text></Descriptions.Item>
<Descriptions.Item label="降雨状态">
<Tag color={device.rainfall === 'no_rain' ? 'green' : 'red'}>{getVal(device.rainfall)}</Tag>
<Tag color={(uavState || device).rainfall === 'no_rain' ? 'green' : 'red'}>{getVal((uavState || device).rainfall)}</Tag>
</Descriptions.Item>
<Descriptions.Item label="返航距离"><Text>{fix2(device.home_distance)} m</Text></Descriptions.Item>
<Descriptions.Item label="返航距离"><Text>{fix2((uavState || device).home_distance)} m</Text></Descriptions.Item>
<Descriptions.Item label="GPS 卫星数">
<Badge count={device.position_state?.gps_number || 0} style={{ backgroundColor: '#1890ff' }} />
<Badge count={(uavState || device).position_state?.gps_number || 0} style={{ backgroundColor: '#1890ff' }} />
</Descriptions.Item>
<Descriptions.Item label="RTK 卫星数">
<Badge count={device.position_state?.rtk_number || 0} style={{ backgroundColor: '#52c41a' }} />
<Badge count={(uavState || device).position_state?.rtk_number || 0} style={{ backgroundColor: '#52c41a' }} />
</Descriptions.Item>
<Descriptions.Item label="RTK 状态" span={2}>
<Tag color={device.position_state?.is_fixed === 'fixing_successful' ? 'success' : 'warning'}>
{device.position_state?.is_fixed === 'fixing_successful' ? 'RTK 已固定' : '未固定'}
<Tag color={(uavState || device).position_state?.is_fixed === 'fixing_successful' ? 'success' : 'warning'}>
{(uavState || device).position_state?.is_fixed === 'fixing_successful' ? 'RTK 已固定' : '未固定'}
</Tag>
</Descriptions.Item>
</Descriptions>

View File

@@ -19,6 +19,8 @@ interface DroneVideoPlayerProps {
const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
device,
wind_speed,
h,
height = 160,
showSourceSelect = true,
dualVideo = false,
@@ -230,19 +232,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>
{showSourceSelect && (
<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>
)}
{/* 底部信息叠加 */}
{isDrone && device && (
@@ -258,8 +263,8 @@ const DroneVideoPlayer: React.FC<DroneVideoPlayerProps> = ({
padding: '4px 8px',
fontSize: 10
}}>
<span>高度 {device.height?.toFixed(2) || '-'}m</span>
<span>风速 {device.wind_speed?.toFixed(2) || '-'}m/s</span>
<span>高度 {h.toFixed(2) || device.height?.toFixed(2) || '-'}m</span>
<span>风速 {wind_speed.toFixed(2) || device.wind_speed?.toFixed(2) || '0'}m/s</span>
</div>
)}
</div>