设备状态界面
This commit is contained in:
@@ -28,51 +28,54 @@ api.interceptors.request.use(
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器(处理 401 + 网络错误 + 服务挂了)
|
||||
// 统一登出跳转方法
|
||||
const handleLogoutAndRedirect = () => {
|
||||
localStorage.clear();
|
||||
Cookies.remove('userId');
|
||||
Cookies.remove('Admin-Token');
|
||||
|
||||
// 动态导入避免循环依赖
|
||||
import('../store').then(store => {
|
||||
import('../store/userSlice').then(({ logout }) => {
|
||||
store.default.dispatch(logout());
|
||||
});
|
||||
});
|
||||
|
||||
message.error('登录已过期,请重新登录');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 800);
|
||||
};
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data;
|
||||
(response: AxiosResponse) => {
|
||||
const data = response.data;
|
||||
|
||||
// ==============================================
|
||||
// 👇 重点:如果接口返回 code = 401,直接跳转登录
|
||||
// ==============================================
|
||||
if (data?.code == 401) {
|
||||
handleLogoutAndRedirect();
|
||||
return Promise.reject(new Error('登录已过期'));
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
// ==============================================
|
||||
// 👇 这里处理:服务挂了 / 断网 / 请求失败 / 500
|
||||
// ==============================================
|
||||
// 网络异常 / 服务器挂了
|
||||
if (!error.response) {
|
||||
message.error('服务异常或网络断开,即将重新登录');
|
||||
localStorage.clear();
|
||||
Cookies.remove('userId');
|
||||
Cookies.remove('Admin-Token');
|
||||
|
||||
// 动态加载 store 避免循环引用
|
||||
const store = await import('../store');
|
||||
const { logout } = await import('../store/userSlice');
|
||||
store.default.dispatch(logout());
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 800);
|
||||
handleLogoutAndRedirect();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// 👇 这里处理:401 未授权 / token 过期
|
||||
// ==============================================
|
||||
// HTTP 401
|
||||
if (error.response.status === 401) {
|
||||
localStorage.clear();
|
||||
Cookies.remove('userId');
|
||||
Cookies.remove('Admin-Token');
|
||||
|
||||
const store = await import('../store');
|
||||
const { logout } = await import('../store/userSlice');
|
||||
store.default.dispatch(logout());
|
||||
|
||||
message.error('登录已过期,请重新登录');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 800);
|
||||
handleLogoutAndRedirect();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 其他错误(500等)不跳转,只返回错误
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function DroneLivePlayer({ streamData, videoMode = "NORMAL" }) {
|
||||
timerRef.current = null;
|
||||
}
|
||||
leaveRoom(); // 先离开旧房间
|
||||
await new Promise((resolve) => setTimeout(resolve, 100)); // 等待 SDK 内部清理完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// 3. 组件已卸载,直接放弃
|
||||
if (!mountedRef.current) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
|
||||
Typography, Space, Progress, Badge, Timeline, message,
|
||||
Select
|
||||
Select, Modal
|
||||
} from 'antd';
|
||||
import {
|
||||
EnvironmentOutlined, RobotOutlined, BranchesOutlined,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CloudOutlined, ClockCircleOutlined, ReloadOutlined,
|
||||
AimOutlined, ApiOutlined, SettingOutlined,
|
||||
PlusOutlined, ArrowRightOutlined,
|
||||
EditOutlined
|
||||
EditOutlined, FullscreenOutlined, InfoCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -44,9 +44,10 @@ const logs = [
|
||||
];
|
||||
|
||||
// 设备总览组件
|
||||
export default function DeviceOverviewPage({ devices, planedevices, devicesAll }) {
|
||||
export default function DeviceOverviewPage({ devices, planedevices, devicesAll, onViewStatus }) {
|
||||
const [selectedDevice, setSelectedDevice] = useState(null);
|
||||
const [planeVideo, setPlaneVideo] = useState('');
|
||||
const [planeVideo, setPlaneVideo] = useState([]); // 初始为空数组
|
||||
const [zoomedVideo, setZoomedVideo] = useState(null); // 放大查看的视频数据
|
||||
|
||||
// 动态计算顶部卡片数据
|
||||
const dynamicTopCards = React.useMemo(() => {
|
||||
@@ -205,8 +206,12 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll }
|
||||
</Col>
|
||||
|
||||
<Col xs={24} lg={10}>
|
||||
<Card title={<> 设备控制面板</>} bordered={false} style={{ borderRadius: 12, marginBottom: 16 }} bodyStyle={{ padding: 16, paddingBottom: 0 }}>
|
||||
<Select
|
||||
<Card
|
||||
title={<><SettingOutlined style={{ marginRight: 6, }} /> 设备控制面板</>}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12, marginBottom: 16 }}
|
||||
bodyStyle={{ padding: 16, paddingBottom: 0 }}
|
||||
> <Select
|
||||
style={{ width: 220, marginBottom: 10 }}
|
||||
placeholder="请选择设备"
|
||||
value={selectedDevice?.device_sn || selectedDevice?.serialNumber}
|
||||
@@ -233,6 +238,16 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll }
|
||||
<Tag color={selectedDevice.onlineStatus === 1 ? "success" : "default"} size="small">
|
||||
{selectedDevice.onlineStatus == 1 ? "在线" : "离线"}
|
||||
</Tag>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
icon={<InfoCircleOutlined />}
|
||||
style={{ borderRadius: 4, fontSize: 12, marginLeft: 'auto' }}
|
||||
onClick={() => onViewStatus && onViewStatus(selectedDevice.device_sn || selectedDevice.serialNumber)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
@@ -412,6 +427,19 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll }
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
342
src/components/devices/DeviceStatusPage.tsx
Normal file
342
src/components/devices/DeviceStatusPage.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Row, Col, Card, Statistic, Tag, Typography,
|
||||
Space, Empty, Descriptions, Badge, Progress, Button,
|
||||
Divider, Select
|
||||
} from 'antd';
|
||||
import {
|
||||
ThunderboltOutlined, DashboardOutlined, AimOutlined,
|
||||
EnvironmentOutlined, RocketOutlined, GlobalOutlined,
|
||||
VideoCameraOutlined, ControlOutlined, FullscreenOutlined
|
||||
} from '@ant-design/icons';
|
||||
import XboxController from './XboxController';
|
||||
import DroneLivePlayer from '../VolcRtcPlayer.jsx';
|
||||
import { getLiveStream } from '../../api/device.ts';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
// 模拟地图组件
|
||||
const MockMap = ({ longitude, latitude }) => (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: '#e6f7ff url(https://picsum.photos/seed/map/1200/800) center/cover',
|
||||
borderRadius: 8,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
left: 16,
|
||||
background: 'rgba(255,255,255,0.9)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: 4,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
zIndex: 10
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 'bold', marginBottom: 4 }}>实时坐标</div>
|
||||
<div style={{ fontSize: 12 }}>经度: {longitude || '118.78'}</div>
|
||||
<div style={{ fontSize: 12 }}>纬度: {latitude || '32.06'}</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
background: '#1890ff',
|
||||
borderRadius: '50%',
|
||||
border: '3px solid #fff',
|
||||
boxShadow: '0 0 10px rgba(24,144,255,0.8)',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: -30,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: '#1890ff',
|
||||
color: '#fff',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
fontSize: 10,
|
||||
whiteSpace: 'nowrap'
|
||||
}}>当前设备</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function DeviceStatusPage({ devicesAll, serialNumber, onDeviceChange }) {
|
||||
const [planeVideo, setPlaneVideo] = useState([]);
|
||||
const [isControlled, setIsControlled] = useState(false);
|
||||
|
||||
// 获取选中的设备详情
|
||||
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.device_sn || device.productName?.includes('无人机'));
|
||||
}, [device]);
|
||||
|
||||
const runningStatus = device?.lastRunningStatus || {};
|
||||
|
||||
// 辅助函数:数字格式化
|
||||
const fix2 = (val) => {
|
||||
if (val === null || val === undefined || isNaN(val)) return '-';
|
||||
return Number(val).toFixed(2);
|
||||
};
|
||||
|
||||
// 辅助函数:安全取值
|
||||
const getVal = (val) => val === "no_rain" ? "无" : (val || '-');
|
||||
|
||||
useEffect(() => {
|
||||
if (!device?.gateway_sn) {
|
||||
setPlaneVideo([]);
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
getway_sn: device?.gateway_sn,
|
||||
drone_sn: device?.device_sn,
|
||||
getway_camera_list: device?.gateway_camera_list ?? [],
|
||||
drone_camera_list: device?.drone_camera_list ?? [],
|
||||
video_expire: 7200,
|
||||
quality_type: "adaptive",
|
||||
}
|
||||
getLiveStream(data).then(res => {
|
||||
if (res.code == 200) {
|
||||
setPlaneVideo(res.data || []);
|
||||
}
|
||||
})
|
||||
}, [device]);
|
||||
|
||||
const handleGamepadUpdate = (state) => {
|
||||
// console.log('Gamepad Update:', state);
|
||||
};
|
||||
|
||||
const handleControlModal = ({ control }) => {
|
||||
setIsControlled(control);
|
||||
};
|
||||
|
||||
if (!device) {
|
||||
return <Empty description="请选择一个设备以查看状态" style={{ marginTop: 100 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0px' }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Text strong>选择设备:</Text>
|
||||
<Select
|
||||
style={{ width: 280 }}
|
||||
placeholder="请选择要查看的设备"
|
||||
value={device?.device_sn || device?.serialNumber}
|
||||
onChange={(val) => onDeviceChange && onDeviceChange(val)}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={devicesAll?.map(item => ({
|
||||
value: item.device_sn || item.serialNumber,
|
||||
label: item.deviceAlias || item.drone_callsign || item.callsign || item.deviceName || '未知设备'
|
||||
}))}
|
||||
/>
|
||||
{device && (
|
||||
<Tag color="blue" style={{ marginLeft: 8 }}>
|
||||
{isUAV ? '无人机' : '机器人'}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 左侧:视频和地图 */}
|
||||
<Col xs={24} lg={16}>
|
||||
<Card
|
||||
title={<><VideoCameraOutlined /> 实时视频监控 - {device.deviceAlias || device.callsign || '监控画面'}</>}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12, marginBottom: 16 }}
|
||||
bodyStyle={{ padding: 12 }}
|
||||
>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: '8px',
|
||||
gridTemplateColumns: planeVideo.length > 1 ? '1fr 1fr' : '1fr',
|
||||
gridTemplateRows: planeVideo.length > 2 ? '1fr 1fr' : '1fr',
|
||||
height: 450,
|
||||
background: '#000',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{planeVideo.length > 0 ? (
|
||||
planeVideo.map((stream, index) => (
|
||||
<div key={index} style={{
|
||||
position: 'relative',
|
||||
gridColumn: (planeVideo.length === 3 && index === 0) ? 'span 2' : 'span 1'
|
||||
}}>
|
||||
<DroneLivePlayer streamData={stream} />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
color: '#fff',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 4,
|
||||
fontSize: 12
|
||||
}}>
|
||||
{stream.camera_position === 'indoor' ? '库内监控' : `画面 ${index + 1}`}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#666', gridColumn: 'span 2' }}>
|
||||
暂无实时画面
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={<><GlobalOutlined /> 位置态势图</>}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12 }}
|
||||
bodyStyle={{ padding: 12, height: 400 }}
|
||||
>
|
||||
<MockMap longitude={device.longitude} latitude={device.latitude} />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:状态面板和控制 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card
|
||||
title={<><DashboardOutlined /> {isUAV ? '无人机状态' : '机器人状态'}详情</>}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12, marginBottom: 16 }}
|
||||
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={device.capacity_percent || 0}
|
||||
size="small"
|
||||
status={(device.capacity_percent || 0) < 20 ? 'exception' : 'active'}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={<><EnvironmentOutlined /> 飞行高度</>}>
|
||||
<Text strong style={{ fontSize: 16 }}>{fix2(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="降雨状态">
|
||||
<Tag color={device.rainfall === 'no_rain' ? 'green' : 'red'}>{getVal(device.rainfall)}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="返航距离">
|
||||
<Text>{fix2(device.home_distance)} m</Text>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="GPS 卫星数">
|
||||
<Badge count={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' }} />
|
||||
</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>
|
||||
</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 }}>{runningStatus.voltage || '-'} V</Text>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="左电机电流">
|
||||
<Text>{runningStatus?.leftCurrent || '-'} A</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="右电机电流">
|
||||
<Text>{runningStatus?.rightCurrent || '-'} A</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="左轮速度">
|
||||
<Text>{runningStatus?.leftMeasureSpeed || '-'} A</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="右轮速度">
|
||||
<Text>{runningStatus?.rightMeasureSpeed || '-'} A</Text>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="左电机温度">
|
||||
<Text>{runningStatus?.leftMotorTemp || '-'} ℃</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="右电机温度">
|
||||
<Text>{runningStatus?.rightMotorTemp || '-'} ℃</Text>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="主控温度">
|
||||
<Text>{runningStatus?.chipTemp || '-'} ℃</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="卫星数量">
|
||||
<Badge showZero={true}
|
||||
count={runningStatus?.satelliteCnt || 0} style={{ backgroundColor: '#52c41a' }} />
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="偏航角 (Yaw)">
|
||||
<Text>{runningStatus?.yaw || '-'}°</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="俯仰角 (Pitch)">
|
||||
<Text>{runningStatus?.pitch || '-'}°</Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
|
||||
|
||||
</Card>
|
||||
{isUAV && (
|
||||
<Card
|
||||
title={<><ControlOutlined /> 远程控制</>}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12, background: '#0f172a' }}
|
||||
headStyle={{ color: '#fff', borderBottom: '1px solid #1e293b' }}
|
||||
>
|
||||
<XboxController
|
||||
onGamepadUpdate={handleGamepadUpdate}
|
||||
handleControlModal={handleControlModal}
|
||||
isControlled={isControlled}
|
||||
/>
|
||||
<div style={{ marginTop: 12, textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Button type="primary" danger ghost={!isControlled} onClick={() => setIsControlled(!isControlled)}>
|
||||
{isControlled ? '释放控制权限' : '请求控制权限'}
|
||||
</Button>
|
||||
</Space>
|
||||
<div style={{ color: '#64748b', fontSize: 12, marginTop: 8 }}>
|
||||
请连接 Xbox 控制器并按下任意按键以激活
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export default function WayLinePage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ background: '#f5f7fa', minHeight: '100vh', padding: '16px 24px' }}>
|
||||
<div style={{ background: '#f5f7fa', minHeight: '100vh', padding: '0px 24px' }}>
|
||||
|
||||
|
||||
{/* 顶部统计卡片 */}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Layout, Typography, Menu } from 'antd';
|
||||
import { AimOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import DeviceOverviewPage from '../components/devices/DeviceOverviewPage';
|
||||
import DeviceStatusPage from '../components/devices/DeviceStatusPage';
|
||||
import { getDeviceListApi, getPlaneDeviceList } from '../api/device';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store';
|
||||
@@ -10,14 +11,17 @@ import WayLinePage from '../components/devices/WayLinePage';
|
||||
const { Header, Content } = Layout;
|
||||
const { Text } = Typography;
|
||||
|
||||
const DeviceStatusSettingPage = () => (
|
||||
const DeviceStatusSettingPage = ({ serialNumber }) => (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h2>设置状态页面(/devices/status)</h2>
|
||||
<h2>设置状态页面</h2>
|
||||
<p>当前查看设备序列号: <span style={{ fontWeight: 'bold', color: '#1890ff' }}>{serialNumber || '未选择设备'}</span></p>
|
||||
{/* 这里后续可以根据 serialNumber 加载具体的设备状态详情 */}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function DeviceIndexPage() {
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [selectedSn, setSelectedSn] = useState(null);
|
||||
const { userInfo } = useSelector((state: RootState) => state.user);
|
||||
const [devices, setDevices] = useState([]);
|
||||
const [planedevices, setPlaneDevices] = useState([]);
|
||||
@@ -25,7 +29,7 @@ export default function DeviceIndexPage() {
|
||||
|
||||
const tabs = [
|
||||
{ key: 'overview', label: '装备总览', icon: <AppstoreOutlined /> },
|
||||
{ key: 'status', label: '设置状态', icon: <SettingOutlined /> },
|
||||
{ key: 'status', label: '设备状态', icon: <SettingOutlined /> },
|
||||
{ key: 'wayLineTask', label: '航线任务', icon: <AimOutlined /> },
|
||||
];
|
||||
|
||||
@@ -44,11 +48,9 @@ export default function DeviceIndexPage() {
|
||||
const devData = deviceRes?.code === 200 ? (deviceRes.data || []) : [];
|
||||
const planeData = planeRes?.code === 200 ? (planeRes.data?.detail || []) : [];
|
||||
|
||||
// 3. 分别保存
|
||||
setDevices(devData);
|
||||
setPlaneDevices(planeData);
|
||||
|
||||
// 4. 最终合并(只执行一次!)
|
||||
setDevicesAll([...devData, ...planeData]);
|
||||
|
||||
} catch (err) {
|
||||
@@ -60,15 +62,27 @@ export default function DeviceIndexPage() {
|
||||
fetchAllData();
|
||||
}, [userInfo]);
|
||||
|
||||
const handleViewStatus = (sn) => {
|
||||
setSelectedSn(sn);
|
||||
setActiveTab('status');
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (activeTab === 'overview') {
|
||||
return <DeviceOverviewPage
|
||||
devices={devices}
|
||||
planedevices={planedevices}
|
||||
devicesAll={devicesAll}
|
||||
onViewStatus={handleViewStatus}
|
||||
/>;
|
||||
}
|
||||
if (activeTab === 'status') {
|
||||
return <DeviceStatusPage
|
||||
devicesAll={devicesAll}
|
||||
serialNumber={selectedSn}
|
||||
onDeviceChange={(sn) => setSelectedSn(sn)}
|
||||
/>;
|
||||
}
|
||||
if (activeTab === 'status') return <DeviceStatusSettingPage />;
|
||||
if (activeTab === 'wayLineTask') return <WayLinePage />;
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user