diff --git a/src/api/request.ts b/src/api/request.ts index c0b8f93..466a698 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -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); } ); diff --git a/src/components/VolcRtcPlayer.jsx b/src/components/VolcRtcPlayer.jsx index a8645ea..f9fcc67 100644 --- a/src/components/VolcRtcPlayer.jsx +++ b/src/components/VolcRtcPlayer.jsx @@ -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) { diff --git a/src/components/devices/DeviceOverviewPage.tsx b/src/components/devices/DeviceOverviewPage.tsx index f9ff706..4ca243d 100644 --- a/src/components/devices/DeviceOverviewPage.tsx +++ b/src/components/devices/DeviceOverviewPage.tsx @@ -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 } - 设备控制面板} bordered={false} style={{ borderRadius: 12, marginBottom: 16 }} bodyStyle={{ padding: 16, paddingBottom: 0 }}> - {selectedDevice.onlineStatus == 1 ? "在线" : "离线"} +
@@ -412,6 +427,19 @@ export default function DeviceOverviewPage({ devices, planedevices, devicesAll } + + setZoomedVideo(null)} + footer={null} + width={1000} + centered + bodyStyle={{ padding: 0, height: 600, background: '#000' }} + destroyOnClose + > + {zoomedVideo && } + ); } diff --git a/src/components/devices/DeviceStatusPage.tsx b/src/components/devices/DeviceStatusPage.tsx new file mode 100644 index 0000000..a7d135a --- /dev/null +++ b/src/components/devices/DeviceStatusPage.tsx @@ -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 }) => ( +
+
+
实时坐标
+
经度: {longitude || '118.78'}
+
纬度: {latitude || '32.06'}
+
+
+
当前设备
+
+
+); + +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 ; + } + + return ( +
+
+ 选择设备: +