Files
web-Iot/src/components/devices/DeviceStatusPage.tsx
2026-06-24 15:47:52 +08:00

1236 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {
useState,
useEffect,
useMemo,
useRef,
useCallback,
} from 'react';
import {
Row,
Col,
Card,
Tag,
Typography,
Space,
Empty,
Descriptions,
Badge,
Progress,
Button,
Select,
message,
Modal,
Switch,
} from 'antd';
import {
DashboardOutlined,
GlobalOutlined,
VideoCameraOutlined,
ControlOutlined,
CameraOutlined,
ThunderboltOutlined,
EnvironmentOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import {
getsinglePlan,
getsinglePlanTrack,
getFlightTaskDetail,
getUAVState,
getSiteDevice,
getSiteUAVList,
addRoute,
} from '../../api/device.ts';
import XboxController from './XboxController';
import DroneVideoPlayer from './DroneVideoPlayer';
import CesiumMap from '../Map.jsx';
import { useSelector } from 'react-redux';
import { RootState } from '@/src/store/index.ts';
import { useWebRTCPlayer } from '../useWebRTCStream.tsx';
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';
import { dataUrlToFile } from '@/lib/utils.ts';
const { RangePicker } = DatePicker;
const { Text } = Typography;
export default function DeviceStatusPage({
serialNumber,
onDeviceChange,
}) {
const { userInfo } = useSelector((state: RootState) => state.user);
const { stationId } = useSelector((state: RootState) => state.station);
const [persistentDroneData, setPersistentDroneData] = useState(null); // 持久化无人机数据
const [persistentHostData, setPersistentHostData] = useState(null); // 持久化机场数据
const [devices, setDevices] = useState([]);
const [planedevices, setPlaneDevices] = useState([]);
const [devicesAll, setDevicesAll] = useState([]);
const [arrivedPoint, setArrivedPoint] = useState(null);//完成点
const [isLoopMode, setIsLoopMode] = useState(true);
// 开关切换回调
const handleChange = (checked: boolean) => {
setIsLoopMode(checked);
if (checked) {
console.log('当前作业方式:回字');
} else {
console.log('当前作业方式:单点');
}
};
// 获取数据
const fetchAllData = async () => {
if (!stationId) return;
try {
// 1. 同时发请求
const [deviceRes, planeRes] = await Promise.all([
getSiteDevice({ siteId: stationId, pageSize: 9999, pageNum: 1, orgId: userInfo.orgId }),
getSiteUAVList(stationId),
]);
// 2. 处理普通设备
const devData = deviceRes?.code === 200 ? (deviceRes.rows || []) : [];
// 3. 处理无人机 + 自动补全地图需要的字段(关键改动)
let planeData = planeRes?.code === 200 ? (planeRes?.rows || []) : [];
planeData = planeData.map(item => ({
...item,
type: 'plane',
lastRunningStatus: {
longitude: item.longitude || 0,
latitude: item.latitude || 0,
},
deviceAlias: item.drone_callsign || item.device_sn
|| '无人机',
}));
setDevices(devData);
setPlaneDevices(planeData);
// 合并所有设备
const all = [...devData, ...planeData];
setDevicesAll(all);
} catch (err) {
console.error('数据请求失败', err);
}
};
const handleFinishEdPoint = (point) => {
setArrivedPoint(point);
}
useEffect(() => {
fetchAllData();
}, [userInfo, stationId]);
// 获取当前选中的无人机设备
const device = useMemo(() => {
const sn =
serialNumber ||
devicesAll?.[0]?.device_sn ||
devicesAll?.[0]?.serialNumber;
if (!sn) return null;
return devicesAll?.find(
(d) => (d.device_sn || d.serialNumber) === sn,
);
}, [devicesAll, serialNumber]);
const isUAV = useMemo(() => {
return !!device?.device_sn;
}, [device]);
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
const handleMqttMessage = (topic, message) => {
try {
const parsedMessage = JSON.parse(message);
if (parsedMessage.data && parsedMessage.data.host) {
const { host } = parsedMessage.data;
// 判断是无人机数据还是机场数据:无人机数据有 battery 字段
const isDroneData = host.battery !== undefined;
if (isDroneData) {
// 处理无人机数据
setPersistentDroneData(prevData => {
const newData = { ...prevData };
// 无人机特有字段
if (host.battery?.capacity_percent !== undefined) {
newData.capacity_percent = host.battery.capacity_percent;
}
if (host.attitude_head !== undefined) {
newData.heading = host.attitude_head;
}
if (host.horizontal_speed !== undefined) {
newData.horizontal_speed = host.horizontal_speed;
}
if (host.vertical_speed !== undefined) {
newData.vertical_speed = host.vertical_speed;
}
if (host.total_flight_time !== undefined) {
newData.total_flight_time = host.total_flight_time;
}
if (host.total_flight_sorties !== undefined) {
newData.total_flight_sorties = host.total_flight_sorties;
}
if (host.mode_code !== undefined) {
newData.mode_code = host.mode_code;
}
// 通用字段
if (host.height !== undefined) {
newData.height = host.height;
}
if (host.wind_speed !== undefined) {
newData.wind_speed = host.wind_speed;
}
if (host.position_state !== undefined) {
newData.position_state = host.position_state;
}
if (host.longitude !== undefined) {
newData.longitude = host.longitude;
}
if (host.latitude !== undefined) {
newData.latitude = host.latitude;
}
return newData;
});
} else {
// 处理机场数据
setPersistentHostData(prevData => {
const newData = { ...prevData };
// 机场特有字段
if (host.drone_charge_state?.capacity_percent !== undefined) {
newData.capacity_percent = host.drone_charge_state.capacity_percent;
}
if (host.air_conditioner?.air_conditioner_state !== undefined) {
newData.air_conditioner_state = host.air_conditioner.air_conditioner_state;
}
if (host.air_conditioner_state !== undefined) {
newData.air_conditioner_state = host.air_conditioner_state;
}
// 通用字段
if (host.environment_temperature !== undefined) {
newData.environment_temperature = host.environment_temperature;
}
if (host.wind_speed !== undefined) {
newData.wind_speed = host.wind_speed;
}
if (host.rainfall !== undefined) {
newData.rainfall = host.rainfall === 0 ? 'no_rain' : 'rain';
}
if (host.humidity !== undefined) {
newData.humidity = host.humidity;
}
if (host.drone_in_dock !== undefined) {
newData.drone_in_dock = host.drone_in_dock;
}
if (host.cover_state !== undefined) {
newData.cover_state = host.cover_state;
}
if (host.temperature !== undefined) {
newData.temperature = host.temperature;
}
if (host.home_distance !== undefined) {
newData.home_distance = host.home_distance;
}
return newData;
});
}
}
} catch (err) {
console.error('DeviceStatusPage MQTT 消息解析失败:', err);
}
};
// 根据选中设备动态构建MQTT主题
const mqttTopics = useMemo(() => {
if (!isUAV || !device) {
return [];
}
const topics = [];
if (device.device_sn) {
topics.push(`thing/product/${device.device_sn}/osd`);
}
if (device.gateway_sn && device.gateway_sn !== device.device_sn) {
topics.push(`thing/product/${device.gateway_sn}/osd`);
}
return topics;
}, [isUAV, device]);
// 订阅MQTT消息
useMqtt({
url: envConfig.mqtt_url,
topic: mqttTopics,
onMessage: handleMqttMessage,
});
// 合并无人机和机场数据
const currentDroneData = React.useMemo(() => {
return { ...persistentHostData, ...persistentDroneData };
}, [persistentDroneData, persistentHostData]);
// 实时位置数据(用于地图)
const [messageApi, contextHolder] = message.useMessage();
const controllerRef = useRef<DeviceController | null>(null);
const [isControlled, setIsControlled] = useState(false);
const [hasControlRight, setHasControlRight] = useState(false);
const [connecting, setConnecting] = useState(false);
const [wsStatus, setWsStatus] = useState('disconnected');
const [wsDeviceInfo, setWsDeviceInfo] = useState({});
const [selectedDrone, setSelectedDrone] = useState(null);
const [taskList, setTaskList] = useState([]);
// 👇 加上这两个
const [queryTimeRange, setQueryTimeRange] = useState([]); // 时间范围
const [startTimestamp, setStartTimestamp] = useState(''); // 开始时间戳
const [endTimestamp, setEndTimestamp] = useState(''); // 结束时间戳
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState({
title: '',
content: '',
});
const [mowerView, setMowerView] = useState('front');
const [uavState, setUavState] = useState<any>(null);
const [points, setPoints] = useState([]);
const [clearNavLine, setClearNavLine] = useState(0);
const [clearPlanLine, setClearPlanLine] = useState(0);
const timerRef = useRef<any>(null);
// 打点相关状态
const [markedPoints, setMarkedPoints] = useState([]); // 标记的点
const [completeFlag, setCompleteFlag] = useState(false); // 是否完成路径规划
const [savePath, setSavePath] = useState(null); // 保存的路径数据
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); // 保存模态框可见性
const [plotName, setPlotName] = useState(''); // 地块名称
const [isSaving, setIsSaving] = useState(false); // 是否正在保存
const mapRef = useRef(null); // 地图 ref
const runningStatus = useMemo(() => {
// 判断对象是否为空
if ((!wsDeviceInfo || Object.keys(wsDeviceInfo).length === 0) && !isUAV) {
return {};
}
if (isUAV) {
return {
...wsDeviceInfo,
};
}
const base = device?.lastRunningStatus || {};
return isUAV
? base
: {
...base,
...wsDeviceInfo,
};
}, [device, wsDeviceInfo, isUAV]);
const { video: robotVideo, error, ready } = useWebRTCPlayer(
!isUAV ? serialNumber : '',
`webrtc://1.95.137.212/live/livestream/${serialNumber}`,
);
const getVal = (val) => val === "no_rain" ? "无" : (val || '-');
const realtimePosition = React.useMemo(() => {
// 1. 无人机:优先用 MQTT 实时数据
if (isUAV) {
if (currentDroneData?.longitude && currentDroneData?.latitude) {
return {
lon: currentDroneData.longitude,
lat: currentDroneData.latitude,
heading: currentDroneData.heading || 0,
};
}
return null;
}
// 2. 机器人:从 wsDeviceInfo / runningStatus 取实时经纬度
const robotLon = runningStatus.longitude || wsDeviceInfo.longitude;
const robotLat = runningStatus.latitude || wsDeviceInfo.latitude;
if (robotLon && robotLat) {
return {
lon: robotLon,
lat: robotLat,
heading: runningStatus.pitch || wsDeviceInfo.pitch || 0,
};
}
return null;
}, [isUAV, currentDroneData, runningStatus, wsDeviceInfo]);
useEffect(() => {
const controller = new DeviceController(userInfo, messageApi);
controller.onDeviceInfoUpdate = setWsDeviceInfo;
controller.onControlStatusChange = setIsControlled;
controller.onHasControlRightChange = setHasControlRight;
controller.onRequestControlStatusChange = setConnecting;
controller.onWsStatusChange = setWsStatus;
controller.handleArrivedPoint = handleFinishEdPoint;
controller.onPermissionRequest = (content) => {
setModalContent(content);
setIsModalOpen(true);
};
controller.init();
controllerRef.current = controller;
return () => {
controller?.destroy();
controllerRef.current?.destroy();
controllerRef.current = null;
};
}, []);
useEffect(() => {
if (!device || !controllerRef.current || !device?.serialNumber || isUAV) return;
setWsDeviceInfo({});
controllerRef.current.connectDevice(device);
}, [device?.serialNumber]);
// 定时获取无人机状态
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]);
// 获取执行中任务的轨迹
useEffect(() => {
const fetchExecutingTaskTrack = async () => {
if (isUAV && device?.gateway_sn) {
try {
const todayStart = dayjs().startOf('day').unix();
const todayEnd = dayjs().endOf('day').unix();
const sns = [device.gateway_sn];
const res = await getsinglePlan({ sns, beginAt: todayStart, endAt: todayEnd });
if (res?.code === 200 && res.data) {
let combinedList = [];
Object.values(res.data).forEach((item: any) => {
if (item.list) combinedList = combinedList.concat(item.list);
});
// 筛选出执行中的任务
const executingTask = combinedList.find(t =>
['executing', 'running', 'restored'].includes(t.status)
);
if (executingTask) {
const trackRes = await getsinglePlanTrack(executingTask);
if (trackRes.code === 0 && trackRes.data.track?.points?.length) {
const trackPoints = trackRes.data.track.points.map(item => ({
lon: item.longitude || item.lon,
lat: item.latitude || item.lat
}));
setPoints(trackPoints);
} else {
setPoints([]);
}
} else {
setPoints([]);
}
} else {
setPoints([]);
}
} catch (err) {
console.error('获取执行中任务轨迹失败', err);
setPoints([]);
}
} else {
setPoints([]);
}
};
fetchExecutingTaskTrack();
}, [isUAV, device]);
const mapDevices = useMemo(() => {
if (!device) return null;
// 优先使用持久化MQTT数据,其次使用uavState
const lng = currentDroneData?.longitude ?? uavState?.longitude ?? runningStatus.longitude ?? device.longitude;
const lat = currentDroneData?.latitude ?? uavState?.latitude ?? runningStatus.latitude ?? device.latitude;
return {
...device,
lastRunningStatus: {
...device.lastRunningStatus,
longitude: lng,
latitude: lat,
},
// 让地图取最新经纬度
longitude: lng,
latitude: lat,
name: device.deviceAlias || device.callsign || device.deviceName || '设备',
type: isUAV
? 'drone'
: device.productName?.includes('割草机')
? 'mower'
: 'robot',
};
}, [device, isUAV, runningStatus, uavState, currentDroneData]);
const realtimeDevice = useMemo(() => {
if (!device) return null;
return {
...device,
...uavState,
...currentDroneData, // 持久化数据最后覆盖,确保优先显示
};
}, [device, uavState, currentDroneData]);
const handleGamepadUpdate = useCallback((state) => {
controllerRef.current?.sendGamepad(state);
}, []);
const handleControlModal = useCallback(() => {
controllerRef.current?.requestControl();
}, []);
const handleOk = () => {
controllerRef.current?.sendPermissionRequest(
true,
'switchResult',
);
setIsControlled(false);
setIsModalOpen(false);
};
const handleCancel = () => {
controllerRef.current?.sendPermissionRequest(
false,
'switchResult',
);
setIsControlled(true);
setIsModalOpen(false);
};
const fix2 = (val) => {
if (val === null || val === undefined || isNaN(val)) {
return '-';
}
return Number(val).toFixed(2);
};
// 打点功能
const handleMarkPoint = () => {
if (!realtimePosition) {
message.warning('无法获取实时位置,请检查设备连接');
return;
}
const newPoint = {
lon: Number(realtimePosition.lon),
lat: Number(realtimePosition.lat),
};
setMarkedPoints([...markedPoints, newPoint]);
messageApi.success(`已标记第 ${markedPoints.length + 1} 个点`);
};
// 清空标记点
const handleClearPoints = () => {
setMarkedPoints([]);
setCompleteFlag(false);
setSavePath(null);
};
// 完成功能 - 调用路径规划接口
const handleComplete = async () => {
//如果时弓子模式
if (isLoopMode) {
if (markedPoints.length < 3) {
messageApi.warning('请至少标记 3 个点');
return;
}
try {
// 准备路径规划请求参数
const positionPoints = [...markedPoints];
const requestParams = {
reference: {
lat: positionPoints[0].lat,
lon: positionPoints[0].lon,
},
heading: 0,
outer: {
position: positionPoints.map(point => ({
lat: point.lat,
lon: point.lon,
})),
sideWidth: 0.0,
},
holes: [],
workType: 0,
};
const response = await fetch('https://servicepathplan.satabot.com/api/path', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestParams),
});
const result = await response.json();
if (result) {
const path = result?.data?.path?.map(p => {
return {
lng: p.lon,
lat: p.lat
}
})
setPoints(path);
setSavePath({
path: path, // 存储返回的路径结果
outer: requestParams?.outer?.position?.map(p => {
return {
lng: p.lon,
lat: p.lat
}
}
),
planModel: 0,
});
setCompleteFlag(true);
messageApi.success('路径规划完成');
}
} catch (error) {
console.error('路径规划失败:', error);
messageApi.error('路径规划失败,请重试');
}
} else {
const positionPoints = [...markedPoints].map(item => {
return {
lng: item.lon,
lat: item.lat
}
});
setPoints(positionPoints);
setSavePath({
path: positionPoints, // 存储返回的路径结果
outer: [],
planModel: 2,
});
setCompleteFlag(true);
}
};
// 打开保存模态框
const handleOpenSaveModal = () => {
if (!completeFlag) {
messageApi.warning('请先点击完成生成路径');
return;
}
setPlotName('');
setIsSaveModalVisible(true);
};
// 保存功能
const handleSave = async () => {
if (!plotName.trim()) {
messageApi.warning('请输入地块名称');
return;
}
setIsSaving(true);
try {
// 1. 获取地图截图
let screenshotDataUrl = null;
if (mapRef.current) {
screenshotDataUrl = await mapRef.current.captureScreenshot();
}
// 2. 创建 FormData
const formData = new FormData();
// 3. 添加文件
if (screenshotDataUrl) {
// 将 dataUrl 转换为 Blob
const file = dataUrlToFile(screenshotDataUrl, "screenshot.jpg");
console.log("blob", file)
if (file instanceof File) {
formData.append('file', file);
}
}
// 4. 添加 workRecord
const workRecord = {
workName: plotName,
siteId: stationId,
jsonData: savePath,
};
formData.append(
"workRecord",
new Blob([JSON.stringify(workRecord)], {
type: "application/json"
})
);
//formData.append('workRecord', JSON.stringify(workRecord));
// 5. 调用保存接口
const res = await addRoute(formData);
if (res.code === 200) {
messageApi.success('保存成功');
setIsSaveModalVisible(false);
// 重置状态
setMarkedPoints([]);
setCompleteFlag(false);
setSavePath(null);
} else {
messageApi.error(res.msg || '保存失败');
}
} catch (error) {
console.error('保存失败:', error);
messageApi.error('保存失败');
} finally {
setIsSaving(false);
}
};
const realtimeType = useMemo(() => {
return isUAV ? 'plane' : 'car';
}, [isUAV]);
if (!device) {
return (
<Empty
description="请选择设备"
style={{ marginTop: 100 }}
/>
);
}
return (
<div style={{ padding: 0 }}>
{contextHolder}
{/* 页面顶部按钮区域 */}
{!isUAV && (
<div
style={{
marginBottom: 8,
padding: '4px 0px',
background: '#f5f7fa',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Text strong>路径规划:</Text>
<Text type="secondary">已标记 {markedPoints.length} 个点</Text>
</div>
<Space size="middle">
<Space>
<span>作业方式</span>
<Switch
size="small"
checked={isLoopMode}
onChange={handleChange}
/>
<span>{isLoopMode ? '弓字' : 'AB点'}</span>
</Space>
<Button
type="primary"
onClick={handleMarkPoint}
disabled={!realtimePosition}
>
打点
</Button>
<Button
type="default"
onClick={handleClearPoints}
>
清空标记
</Button>
<Button
type="primary"
ghost
onClick={handleComplete}
disabled={markedPoints.length < 2}
>
完成
</Button>
<Button
type="primary"
onClick={handleOpenSaveModal}
disabled={!completeFlag}
>
保存
</Button>
</Space>
</div>
)}
<div
style={{
marginBottom: 8,
display: 'flex',
alignItems: 'center',
gap: 12,
}}
>
<Text strong>选择设备:</Text>
<Select
style={{ width: 380 }}
value={device?.device_sn || device?.serialNumber}
onChange={(val) => onDeviceChange?.(val)}
showSearch
optionFilterProp="label"
options={[
{
label: <span>机器人</span>,
title: 'manager',
options: devicesAll.filter(item => item.serialNumber).map(item => ({
value: item.device_sn || item.serialNumber,
label: item.deviceAlias || item.deviceName || '未知设备'
})),
},
{
label: <span>无人机</span>,
title: 'engineer',
options: devicesAll.filter(item => item.gateway_sn).map(item => ({
value: item.device_sn,
label: item.drone_callsign || item.callsign || '未知设备'
})),
},
]}
/>
<Tag color="blue">
{isUAV ? '无人机' : '机器人'}
</Tag>
</div>
<Row gutter={[16, 16]}>
<Col xs={24} lg={16}>
<Card
title={
<>
<VideoCameraOutlined />
实时视频监控
</>
}
//extra={
// <Button
// size="small"
// onClick={() => {
// // 取当前选中设备编号,执行设备切换/刷新逻辑
// const currentSn = device?.device_sn || device?.serialNumber;
// console.log('currentSn', currentSn);
// onDeviceChange?.(currentSn);
// }}
// >
// 刷新
// </Button>
//}
bordered={false}
style={{ borderRadius: 12, marginBottom: 8 }}
bodyStyle={{ padding: 12 }}
>
{!isUAV && (
<Space style={{ marginBottom: 8 }}>
<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>
{/*<Button type={mowerView === 'top' ? 'primary' : 'default'} onClick={() => setMowerView('top')}>顶视</Button>*/}
<Button type={mowerView === 'all' ? 'primary' : 'default'} onClick={() => setMowerView('all')}>全景</Button>
</Space>
)}
<div
style={{
borderRadius: 8,
overflow: 'hidden',
display: 'flex',
// flexWrap: 'wrap', // 左右排布不换行
}}
>
{isUAV ? (
<DroneVideoPlayer
device={realtimeDevice}
height={450}
dualVideo={true}
showSourceSelect={false}
defaultSource="drone"
/>
) : robotVideo ? (
// 全景模式 = T 型排布
mowerView === 'all' ? (
<MowerAllView
serialNumber={serialNumber}
robotVideo={robotVideo}
error={error}
ready={ready}
/>
) : (
// 单画面模式
<VideoPlayer
deviceId={serialNumber}
idx={
mowerView === 'front' ? 1
: mowerView === 'back' ? 2
: mowerView === 'left' ? 3
: 4
}
className="w-full h-full object-contain"
video={robotVideo}
error={error}
ready={ready}
/>
)
) : (
<div style={{ color: '#ccc', margin: 'auto' }}>
视频加载中...
</div>
)}
</div>
</Card>
<Card
title={
<>
<GlobalOutlined />
位置态势图
</>
}
extra={
<Space size={4}>
<Button
size="small"
type="primary"
icon={<DeleteOutlined />}
onClick={() => {
setClearNavLine((v) => v + 1);
}}
>
清空实时轨迹
</Button>
<Button
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => {
setPoints([]);
setClearPlanLine((v) => v + 1);
}}
>
清空规划航线
</Button>
</Space>
}
bordered={false}
style={{ borderRadius: 12 }}
bodyStyle={{
padding: 12,
height: 400,
}}
>
<CesiumMap
ref={mapRef}
devices={[mapDevices]}
points={points}
markedPoints={markedPoints}
realtimePosition={realtimePosition}
realtimeType={realtimeType}
drawRealTime={true}
finishPoint={arrivedPoint}
ifClearNavLine={clearNavLine}
ifClearPlanLine={clearPlanLine}
from="deviceStatus"
/>
</Card>
</Col>
<Col xs={24} lg={8}>
<Card
title={<><DashboardOutlined /> {isUAV ? '无人机状态' : '机器人状态'}详情</>}
bordered={false}
style={{ borderRadius: 12, marginBottom: 8 }}
extra={<Tag color={device.onlineStatus === 1 ? 'success' : 'default'}>{device.onlineStatus === 1 ? '在线' : '离线'}</Tag>}
>
{isUAV ? (
<Descriptions column={2} size="small" layout="vertical">
<Descriptions.Item label={<><ThunderboltOutlined /> 电池电量</>}>
<Progress percent={(currentDroneData || uavState || device).capacity_percent || 0} size="small" status={((currentDroneData || uavState || device).capacity_percent || 0) < 20 ? 'exception' : 'active'} />
</Descriptions.Item>
<Descriptions.Item label={<><EnvironmentOutlined /> 飞行高度</>}>
<Text strong style={{ fontSize: 16 }}>{fix2((currentDroneData || uavState || device).height)} m</Text>
</Descriptions.Item>
<Descriptions.Item label="水平速度">
<Text>{fix2(currentDroneData?.horizontal_speed) || '-'} m/s</Text>
</Descriptions.Item>
<Descriptions.Item label="垂直速度">
<Text>{fix2(currentDroneData?.vertical_speed) || '-'} m/s</Text>
</Descriptions.Item>
<Descriptions.Item label="环境风速">
<Text>{fix2((currentDroneData || uavState || device).wind_speed)} m/s</Text>
</Descriptions.Item>
<Descriptions.Item label="环境温度">
<Text>{fix2((currentDroneData || uavState || device).environment_temperature)} ℃</Text>
</Descriptions.Item>
<Descriptions.Item label="环境湿度">
<Text>{(currentDroneData?.humidity) || '-'}{currentDroneData?.humidity ? '%' : ''}</Text>
</Descriptions.Item>
<Descriptions.Item label="机场温度">
<Text>{fix2(currentDroneData?.temperature)} ℃</Text>
</Descriptions.Item>
<Descriptions.Item label="降雨状态">
<Tag color={(currentDroneData || uavState || device).rainfall === 'no_rain' ? 'green' : 'red'}>{getVal((currentDroneData || uavState || device).rainfall)}</Tag>
</Descriptions.Item>
<Descriptions.Item label="航向角">
<Text>{fix2(currentDroneData?.heading)}°</Text>
</Descriptions.Item>
<Descriptions.Item label="飞行模式">
<Tag color="blue">
{currentDroneData?.mode_code !== undefined ? `模式 ${currentDroneData.mode_code}` : '-'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="无人机位置">
<Tag color="blue">
{currentDroneData?.drone_in_dock === 1 ? '在库' : '外出'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="机库状态">
<Tag color={currentDroneData?.cover_state === 1 ? 'processing' : 'default'}>
{currentDroneData?.cover_state === 1 ? '舱门打开' : '舱门关闭'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="机场空调">
<Tag color={currentDroneData?.air_conditioner_state === 1 ? 'success' : 'default'}>
{currentDroneData?.air_conditioner_state === 1 ? '开启' : '关闭'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="GPS 卫星数">
<Badge showZero count={(currentDroneData || uavState || device).position_state?.gps_number || 0} style={{ backgroundColor: '#1890ff' }} />
</Descriptions.Item>
<Descriptions.Item label="RTK 卫星数">
<Badge showZero count={(currentDroneData || uavState || device).position_state?.rtk_number || 0} style={{ backgroundColor: '#52c41a' }} />
</Descriptions.Item>
<Descriptions.Item label="RTK 状态">
<Tag color={(currentDroneData || uavState || device).position_state?.is_fixed === 2 || (currentDroneData || uavState || device).position_state?.is_fixed === 'fixing_successful' ? 'success' : 'warning'}>
{(currentDroneData || uavState || device).position_state?.is_fixed === 2 || (currentDroneData || uavState || device).position_state?.is_fixed === 'fixing_successful' ? 'RTK 已固定' : '未固定'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="实时经纬度" span={2}>
<Text style={{ fontSize: '12px' }}>
{currentDroneData?.latitude && currentDroneData?.longitude ?
`${currentDroneData.latitude.toFixed(6)}, ${currentDroneData.longitude.toFixed(6)}` : '-'}
</Text>
</Descriptions.Item>
</Descriptions>
) : (
<Descriptions column={2} size="small" layout="vertical">
<Descriptions.Item label={<><ThunderboltOutlined /> 电池电量</>}>
<Progress percent={runningStatus.battery || 0} size="small" status={(runningStatus.battery || 0) < 20 ? 'exception' : 'active'} />
</Descriptions.Item>
<Descriptions.Item label={<><DashboardOutlined /> 系统电压</>}>
<Text strong style={{ fontSize: 16 }}>{fix2(runningStatus.voltage)} V</Text>
</Descriptions.Item>
<Descriptions.Item label="左电机电流"><Text>{fix2(runningStatus?.leftCurrent)} A</Text></Descriptions.Item>
<Descriptions.Item label="右电机电流"><Text>{fix2(runningStatus?.rightCurrent)} A</Text></Descriptions.Item>
<Descriptions.Item label="左轮速度"><Text>{fix2(runningStatus?.leftMeasureSpeed)} rpm</Text></Descriptions.Item>
<Descriptions.Item label="右轮速度"><Text>{fix2(runningStatus?.rightMeasureSpeed)} rpm</Text></Descriptions.Item>
<Descriptions.Item label="左电机温度"><Text>{fix2(runningStatus?.leftMotorTemp)} ℃</Text></Descriptions.Item>
<Descriptions.Item label="右电机温度"><Text>{fix2(runningStatus?.rightMotorTemp)} ℃</Text></Descriptions.Item>
<Descriptions.Item label="主控温度"><Text>{fix2(runningStatus?.chipTemp)} ℃</Text></Descriptions.Item>
<Descriptions.Item label="航向角状态"><Text>{(runningStatus?.headingStatus == 1 ? "已初始化" : "未初始化")}</Text></Descriptions.Item>
<Descriptions.Item label="控制模式"><Text>{(runningStatus?.controlMode == 1 ? "本地模式" : runningStatus?.controlMode == 3 ? "远程模式" : "控制模式")}</Text></Descriptions.Item>
<Descriptions.Item label="定位质量">
<Text>
{runningStatus?.qual == 0
? '无效'
: runningStatus?.qual == 1
? 'GPS 单点定位'
: runningStatus?.qual == 2
? 'DGPS 伪距差分或 SBAS'
: runningStatus?.qual == 4
? 'RTK 固定解'
: runningStatus?.qual == 5
? 'RTK 浮点解'
: '未知'}
</Text>
</Descriptions.Item>
<Descriptions.Item label="卫星数量">
<Badge showZero count={runningStatus?.satelliteCnt || 0} style={{ backgroundColor: '#52c41a' }} />
</Descriptions.Item>
<Descriptions.Item label="偏航角 (Yaw)"><Text>{fix2(runningStatus?.yaw)}°</Text></Descriptions.Item>
<Descriptions.Item label="俯仰角 (Pitch)"><Text>{fix2(runningStatus?.pitch)}°</Text></Descriptions.Item>
</Descriptions>
)}
</Card>
{!isUAV && (
<Card
title={
<>
<ControlOutlined />
远程控制
</>
}
bordered={false}
style={{
borderRadius: 12,
background: '#0f172a',
}}
headStyle={{ color: '#fff' }}
bodyStyle={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}
>
<XboxController
onGamepadUpdate={handleGamepadUpdate}
handleControlModal={handleControlModal}
isControlled={isControlled}
hasControlRight={hasControlRight}
isConnecting={connecting}
/>
</Card>
)}
</Col>
</Row>
<Modal
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
title={modalContent.title}
okText="确定"
cancelText="取消"
>
<p>{modalContent.content}</p>
</Modal>
{/* 保存路径模态框 */}
<Modal
open={isSaveModalVisible}
title="保存路径"
onOk={handleSave}
onCancel={() => setIsSaveModalVisible(false)}
okText="保存"
cancelText="取消"
confirmLoading={isSaving}
>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 8 }}>地块名称</label>
<input
type="text"
value={plotName}
onChange={(e) => setPlotName(e.target.value)}
placeholder="请输入地块名称"
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #d9d9d9',
borderRadius: 4,
fontSize: 14,
}}
/>
</div>
{isSaving && (
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<div style={{
display: 'inline-block',
width: 30,
height: 30,
border: '3px solid #1890ff',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}} />
<div style={{ marginTop: 8, color: '#666' }}>保存中...</div>
</div>
)}
<style>
{`
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`}
</style>
</Modal>
</div >
);
}