航线面板

This commit is contained in:
mmc
2026-05-21 15:39:46 +08:00
parent 3734279cb0
commit 0e6de5566e
4 changed files with 134 additions and 284 deletions

View File

@@ -101,90 +101,7 @@ export function loginApi(data) {
// 获取所有航线
export function getsinglePlan(data) {
return api({
url: `/iot/UAV/getFlightTask?sn=${data.gateway_sn}`,
method: 'get',
})
}
// 获取航线轨迹 iot/UAV/getFlightTaskTrack
export function getsinglePlanTrack(data) {
return api({
url: `/iot/UAV/getFlightTaskTrack?taskId=${data.uuid}`,
method: 'get',
})
}
//上传航线
export function uploadWayLine(data) {
return api({
url: '/iot/UAV/uploadWayline',
method: 'post',
data: data, // 上传文件必须用 data,不能用 params
headers: {
'Content-Type': 'multipart/form-data' // 上传文件必加!
}
})
}
//创建航线任务
export function createWaylineTask(data) {
return api({
url: '/iot/UAV/createFlightTask',
method: 'post',
data: data,
})
}
export function updateFlightTaskStatus(taskId, status) {
return api({
url: '/iot/UAV/updateFlightTaskStatus',
method: 'post',
params: { taskId, status }
})
}
export function getFlightTaskDetail(taskId) {
return api({
url: '/iot/UAV/getFlightTaskDetail',
method: 'get',
params: { taskId }
})
}
/// 航线列表
export function getWayLine() {
return api({
url: '/iot/UAV/getWayline',
method: 'get',
})
}
//直播流
///iot/UAV/flightTaskCommand
export function postFlightCommmand(data) {
return api({
url: '/iot/UAV/flightTaskCommand',
method: 'post',
data
})
}
//切换相机
export function changeCamera(data) {
return api({
url: '/iot/UAV/changeCamera',
method: 'post',
data
})
}
//直播心跳

View File

@@ -1,141 +1,120 @@
import React, { useState, useEffect } from 'react';
import {
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
Tabs, Input, Switch, Divider, Tooltip, Timeline
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message
} from 'antd';
import {
PlusOutlined, ImportOutlined, NodeIndexOutlined, SafetyCertificateOutlined,
SaveOutlined, FullscreenOutlined, EnvironmentOutlined, RobotOutlined,
PlusOutlined, ImportOutlined, NodeIndexOutlined,
SaveOutlined, FullscreenOutlined, EnvironmentOutlined,
BranchesOutlined, CloudOutlined, ThunderboltOutlined, CheckCircleOutlined,
InfoCircleOutlined, ClockCircleOutlined, RiseOutlined, FallOutlined,
SettingOutlined, WarningOutlined, ArrowRightOutlined, CloudSyncOutlined,
InfoCircleOutlined, ClockCircleOutlined,
SettingOutlined, WarningOutlined, ArrowRightOutlined,
SearchOutlined, MoreOutlined, AimOutlined
} from '@ant-design/icons';
import {
Plane,
Navigation,
Wind,
Thermometer,
Droplets,
Satellite,
LayoutGrid,
Clock,
CheckCircle2,
AlertCircle,
Zap,
Waves,
Calendar,
User,
TrendingUp,
GripVertical,
Maximize2,
Target
Plane, Navigation, Wind, Thermometer, Droplets, Satellite,
LayoutGrid, Clock, CheckCircle2, AlertCircle, Zap, Waves,
Calendar, User, TrendingUp, Maximize2
} from 'lucide-react';
import { getsinglePlan } from '@/src/api/api';
import { getsinglePlan } from '@/api/device';
import dayjs from 'dayjs';
const { Text, Title } = Typography;
export default function WayLinePage() {
// 响应式字体大小
export default function WayLinePage({ planedevices }) {
const fontSmall = 'clamp(10px, 0.7vw, 11px)';
const fontNormal = 'clamp(12px, 0.75vw, 13px)';
const fontTitle = 'clamp(14px, 0.85vw, 16px)';
const fontLarge = 'clamp(18px, 1.1vw, 24px)';
const cardStyle = { borderRadius: 12, border: 'none', boxShadow: '0 2px 12px rgba(0,0,0,0.04)' };
const [selectedDrone, setSelectedDrone] = useState(null);
const [taskList, setTaskList] = useState([]); // 真实任务列表
// 顶部统计卡片数据
// 顶部统计
const topCards = [
{ title: '今日航线计划', value: '18', unit: '条', trend: '较昨日 +2', trendColor: '#00B42A', icon: <LayoutGrid size={22} />, color: '#165DFF' },
{ title: '执行中任务', value: '6', unit: '个', desc: '进行中', icon: <Plane size={22} />, color: '#165DFF' },
{ title: '待审批任务', value: '4', unit: '个', trend: '较昨日 -1', trendColor: '#F53F3F', icon: <Calendar size={22} />, color: '#FF7D00' },
{ title: '任务完成率', value: '92', unit: '%', trend: '较昨日 +6%', trendColor: '#00B42A', icon: <CheckCircle2 size={22} />, color: '#00B42A' },
{ title: 'AI 优化收益', value: '+7.8', unit: '%', trend: '较昨日 +1.2%', trendColor: '#00B42A', icon: <TrendingUp size={22} />, color: '#722ED1' },
{ title: '覆盖装备', value: '4 类 / 9 台', unit: '', desc: '全部在线', icon: <Satellite size={22} />, color: '#165DFF' },
];
// 获取真实任务
const getTaskList = async (drone) => {
if (!drone || !drone.device_sn) return;
try {
const res = await getsinglePlan(drone);
if (res?.code === 0) {
setTaskList(res.data?.list || []);
}
} catch (err) {
message.error('获取任务失败');
}
};
// 默认选中第一个无人机
useEffect(() => {
if (planedevices?.length) {
setSelectedDrone(planedevices[0]);
}
}, [planedevices]);
// 切换无人机时刷新任务
useEffect(() => {
getTaskList(selectedDrone);
}, [selectedDrone]);
// 任务状态颜色
const getStatusTag = (status) => {
const map = {
success: { color: 'green', text: '成功' },
running: { color: 'blue', text: '执行中' },
failed: { color: 'red', text: '失败' },
};
const item = map[status] || { color: 'default', text: status };
return <Tag color={item.color}>{item.text}</Tag>;
};
// 任务表格列
const taskColumns = [
{ title: '任务名称', dataIndex: 'name', width: 260, ellipsis: true, fixed: "left" },
{ title: '任务UUID', dataIndex: 'uuid', width: 260, ellipsis: true },
{ title: '设备SN', dataIndex: 'sn', width: 140 },
{ title: '状态', dataIndex: 'status', width: 80, render: getStatusTag },
{
title: '今日航线计划',
value: '18',
unit: '条',
trend: '较昨日 +2',
trendColor: '#00B42A',
icon: <LayoutGrid size={22} />,
color: '#165DFF',
bg: 'linear-gradient(135deg, #E8F3FF 0%, #BAE0FF 100%)'
title: '开始时间',
dataIndex: 'begin_at',
width: 170,
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: '执行中任务',
value: '6',
unit: '个',
desc: '进行中',
icon: <Plane size={22} />,
color: '#165DFF',
bg: 'linear-gradient(135deg, #E8F3FF 0%, #BAE0FF 100%)'
},
{
title: '待审批任务',
value: '4',
unit: '个',
trend: '较昨日 -1',
trendColor: '#F53F3F',
icon: <Calendar size={22} />,
color: '#FF7D00',
bg: 'linear-gradient(135deg, #FFF7E6 0%, #FFE4BA 100%)'
},
{
title: '任务完成率',
value: '92',
unit: '%',
trend: '较昨日 +6%',
trendColor: '#00B42A',
icon: <CheckCircle2 size={22} />,
color: '#00B42A',
bg: 'linear-gradient(135deg, #E8FFEA 0%, #AFF0B5 100%)'
},
{
title: 'AI 优化收益',
value: '+7.8',
unit: '%',
trend: '较昨日 +1.2%',
trendColor: '#00B42A',
icon: <TrendingUp size={22} />,
color: '#722ED1',
bg: 'linear-gradient(135deg, #F9F0FF 0%, #EFDBFF 100%)'
},
{
title: '覆盖装备',
value: '4 类 / 9 台',
unit: '',
desc: '全部在线',
icon: <Satellite size={22} />,
color: '#165DFF',
bg: 'linear-gradient(135deg, #E8F3FF 0%, #BAE0FF 100%)'
title: '结束时间',
dataIndex: 'completed_at',
width: 170,
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
},
];
//useEffect(()=>{
// getsinglePlan().then(res=>{
// })
//})
return (
<div className="normal-spacing" style={{ background: '#f5f7fa', minHeight: '100vh', padding: '16px' }}>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
{topCards.map((item, index) => (
<Col xs={12} sm={8} md={4} key={index}>
<Card bordered={false} bodyStyle={{ padding: '12px 14px' }} style={cardStyle}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 36, height: 36,
background: item.bg,
borderRadius: 10,
display: 'flex', justifyContent: 'center', alignItems: 'center',
color: item.color
}}>
{React.cloneElement(item.icon as React.ReactElement, { size: 18 })}
<div style={{ width: 36, height: 36, background: item.bg, borderRadius: 10, display: 'flex', justifyContent: 'center', alignItems: 'center', color: item.color }}>
{React.cloneElement(item.icon, { size: 18 })}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 10, color: '#86909C', marginBottom: 0 }}>{item.title}</div>
<div style={{ fontSize: 10, color: '#86909C' }}>{item.title}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
<span style={{ fontSize: 'clamp(16px, 1vw, 20px)', fontWeight: 700, color: '#1D2129' }}>{item.value}</span>
<span style={{ fontSize: 9, color: '#4E5969' }}>{item.unit}</span>
</div>
<div style={{ fontSize: 9, color: item.trendColor || '#86909C', fontWeight: 500, lineHeight: 1 }}>
<div style={{ fontSize: 9, color: item.trendColor || '#86909C', fontWeight: 500 }}>
{item.trend || item.desc}
</div>
</div>
@@ -145,143 +124,92 @@ export default function WayLinePage() {
))}
</Row>
{/* 中间主要内容区 */}
<Row gutter={10} style={{ marginBottom: 10 }}>
{/* 左侧:航线编辑与路径规划 */}
<Col xs={24} xl={14} style={{ display: "flex", flexDirection: "column" }}>
{/* 地图卡片 */}
<Card
title={<span style={{ fontSize: fontNormal, fontWeight: 700, color: '#1D2129' }}>航线编辑与路径规划</span>}
title={<span style={{ fontSize: fontNormal, fontWeight: 700 }}>航线编辑与路径规划</span>}
size="small"
extra={
<Space size={4}>
<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4, fontSize: 11 }}>新增</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4, fontSize: 11 }}>导入</Button>
<Button size="small" icon={<NodeIndexOutlined />} style={{ borderRadius: 4, fontSize: 11 }}>优化</Button>
<Button size="small" icon={<SaveOutlined />} style={{ borderRadius: 4, fontSize: 11 }}>保存</Button>
<Button size="small" icon={<Maximize2 size={12} />} style={{ borderRadius: 4 }} />
<Button size="small" icon={<PlusOutlined />} style={{ borderRadius: 4 }}>新增</Button>
<Button size="small" icon={<ImportOutlined />} style={{ borderRadius: 4 }}>导入</Button>
<Button size="small" icon={<NodeIndexOutlined />} style={{ borderRadius: 4 }}>优化</Button>
<Button size="small" icon={<SaveOutlined />} style={{ borderRadius: 4 }}>保存</Button>
<Button size="small" icon={<Maximize2 size={12} />} />
</Space>
}
bordered={false}
bodyStyle={{ padding: 0, position: 'relative', overflow: 'hidden' }}
bodyStyle={{ padding: 0, position: 'relative' }}
style={{ ...cardStyle, marginBottom: 10 }}
>
{/* 地图区域 */}
<div style={{ width: '100%', height: 420, background: '#e8f3ff url(https://picsum.photos/seed/solar_map_v2/1600/1000) center/cover', position: 'relative' }}>
{/* SVG 航线与区域覆盖 */}
<svg style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
{/* 航线 1 (蓝色) */}
<path d="M100,120 L200,80 L350,150 L500,100 L650,180" fill="none" stroke="#165DFF" strokeWidth="3" strokeDasharray="8,4" />
{/* 航线 2 (橙色) */}
<path d="M220,300 Q350,220 480,350 T700,280" fill="none" stroke="#FF7D00" strokeWidth="3" />
{/* 航线 3 (绿色) */}
<path d="M400,80 L550,140 L700,100" fill="none" stroke="#00B42A" strokeWidth="3" strokeDasharray="4,2" />
{/* 航点标记 */}
<circle cx="100" cy="120" r="6" fill="#165DFF" stroke="#fff" strokeWidth="2" />
<circle cx="200" cy="80" r="6" fill="#165DFF" stroke="#fff" strokeWidth="2" />
<circle cx="350" cy="150" r="6" fill="#165DFF" stroke="#fff" strokeWidth="2" />
{/* 区域覆盖 */}
<rect x="550" y="50" width="120" height="80" fill="rgba(245,63,63,0.2)" stroke="#F53F3F" strokeWidth="1" strokeDasharray="4,2" />
<rect x="300" y="280" width="150" height="100" fill="rgba(22,93,255,0.1)" stroke="#165DFF" strokeWidth="1" strokeDasharray="4,4" />
<path d="M100,350 L250,350 L250,450 L100,450 Z" fill="rgba(255,125,0,0.15)" stroke="#FF7D00" strokeWidth="1" />
</svg>
{/* 左侧图例 - 拟真样式 */}
<div style={{ position: 'absolute', top: 16, left: 16, background: 'rgba(255,255,255,0.92)', backdropFilter: 'blur(8px)', padding: '14px 16px', borderRadius: 10, width: 150, boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}>
<div style={{ fontSize: fontSmall, display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#165DFF', borderRadius: 1 }}></span> 无人机航线</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#FF7D00', borderRadius: 1 }}></span> 巡检机器人路线</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#00B42A', borderRadius: 1 }}></span> 除草路线</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#13C2C2', borderRadius: 1 }}></span> 清洗航线</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#165DFF' }}></span> 无人机航线</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#FF7D00' }}></span> 巡检路线</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><span style={{ width: 14, height: 2, background: '#00B42A' }}></span> 除草路线</div>
<Divider style={{ margin: '6px 0' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 18, height: 18, background: '#165DFF', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', color: '#fff', fontSize: 10, fontWeight: 700 }}>H</div>
<span>无人机机场</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><Zap size={14} color="#00B42A" fill="#00B42A" /> <span>充电点</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><Waves size={14} color="#165DFF" /> <span>补水点</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><div style={{ width: 18, height: 18, background: '#165DFF', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', color: '#fff', fontSize: 10 }}>H</div> <span>无人机机场</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><Zap size={14} color="#00B42A" /> <span>充电点</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><AlertCircle size={14} color="#F53F3F" /> <span>禁飞区</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><div style={{ width: 14, height: 14, border: '1px dashed #F53F3F', background: 'rgba(245,63,63,0.1)' }} /> <span>高优先巡检区</span></div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}><div style={{ width: 14, height: 14, background: 'rgba(255,125,0,0.2)', border: '1px solid #FF7D00' }} /> <span>障碍物区域</span></div>
</div>
</div>
{/* 地图交互层 */}
<div style={{ position: 'absolute', top: '22%', left: '16%', color: '#fff', textAlign: 'center' }}>
<div style={{ background: '#165DFF', borderRadius: '50%', width: 28, height: 28, display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: 12, fontWeight: 700, boxShadow: '0 0 10px rgba(22,93,255,0.6)', border: '2px solid #fff' }}>H</div>
<div style={{ fontSize: 10, background: 'rgba(29,33,41,0.8)', padding: '2px 8px', borderRadius: 4, marginTop: 4 }}>无人机机场 A01</div>
</div>
<div style={{ position: 'absolute', top: '42%', left: '46%', textAlign: 'center' }}>
<div style={{ border: '2px dashed #F53F3F', background: 'rgba(245,63,63,0.2)', width: 140, height: 90, borderRadius: 8, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<span style={{ fontSize: 11, fontWeight: 700, color: '#fff', textShadow: '0 1px 2px rgba(0,0,0,0.8)' }}>高优先级巡检区 B-12</span>
</div>
</div>
{/* 右下角地图缩放控制 */}
<div style={{ position: 'absolute', bottom: 20, right: 20, display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ background: '#fff', borderRadius: 8, padding: 8, display: 'flex', flexDirection: 'column', gap: 12, boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<AimOutlined style={{ fontSize: 18, color: '#4E5969', cursor: 'pointer' }} />
<Divider style={{ margin: 0 }} />
<PlusOutlined style={{ fontSize: 18, color: '#4E5969', cursor: 'pointer' }} />
<Divider style={{ margin: 0 }} />
<div style={{ width: 18, height: 2, background: '#C9CDD4' }}></div>
</div>
</div>
</div>
</Card>
{/* 航点列表 */}
{/* 真实任务列表表格 */}
<Card
title={<span style={{ fontSize: fontNormal, fontWeight: 700 }}>航线列表 <Text type="secondary" style={{ fontSize: 10, fontWeight: 400, marginLeft: 8 }}>航线:FL-03</Text></span>}
title={
<div className="flex justify-between w-full items-center">
<span style={{ fontSize: 14, fontWeight: 700 }}>
设备任务列表
</span>
<Select
style={{ width: 180 }}
size="small"
placeholder="选择无人机"
showSearch
allowClear
options={planedevices?.map(item => ({
value: item.device_sn,
label: item.drone_callsign || item.device_sn
}))}
value={selectedDrone?.device_sn}
onChange={(val) => {
const find = planedevices.find(i => i.device_sn === val);
setSelectedDrone(find || null);
}}
/>
</div>
}
size="small"
extra={<PlusOutlined style={{ cursor: 'pointer', color: '#165DFF', fontSize: 12 }} />}
extra={<PlusOutlined style={{ cursor: 'pointer', color: '#165DFF', fontSize: 12, marginLeft: 10 }} />}
bordered={false}
bodyStyle={{ padding: 0, height: "100%" }}
style={{
...cardStyle,
flex: 1,
display: "flex",
flexDirection: "column",
height: "100%",
}}
bodyStyle={{ padding: 0 }}
style={{ ...cardStyle, flex: 1 }}
>
<div style={{
flex: 1,
height: "100%",
position: "relative",
}}>
<Table
size="small"
pagination={false}
scroll={{
y: "auto",
}}
dataSource={[
{ key: '1', point: 'WP-01', coord: '30.32921, 120.12345', action: '起飞', height: 35, speed: 6.0, stay: 0, remark: '从机场起飞' },
{ key: '2', point: 'WP-02', coord: '区块 B-01', action: '拍摄', height: 35, speed: 6.0, stay: 2, remark: '组件可见光' },
{ key: '3', point: 'WP-03', coord: '区块 B-05', action: '热成像', height: 35, speed: 6.0, stay: 3, remark: '热斑检测' },
{ key: '4', point: 'WP-04', coord: '区块 B-09', action: '悬停', height: 35, speed: 0.0, stay: 10, remark: '重点检查' },
{ key: '5', point: 'WP-05', coord: '区块 B-12', action: '热成像', height: 35, speed: 6.0, stay: 3, remark: '高优先区' },
{ key: '6', point: 'WP-06', coord: '区块 B-16', action: '拍摄', height: 35, speed: 6.0, stay: 2, remark: '组件可见光' },
]}
columns={[
{ title: '航点', dataIndex: 'point', width: 80, render: (t) => <Text strong>{t}</Text> },
{ title: '坐标/区域', dataIndex: 'coord', width: 120, ellipsis: true },
{ title: '动作', dataIndex: 'action', width: 80, render: (a) => <Tag color={a === '起飞' ? 'blue' : a === '悬停' ? 'orange' : 'cyan'} bordered={false} style={{ margin: 0 }}>{a}</Tag> },
{ title: '高度(m)', dataIndex: 'height', width: 70 },
{ title: '速度(m/s)', dataIndex: 'speed', width: 90 },
{ title: '停留(s)', dataIndex: 'stay', width: 70 },
{ title: '备注', dataIndex: 'remark', ellipsis: true },
]}
style={{ height: "100%" }}
/>
</div>
<Table
size="small"
rowKey="uuid"
columns={taskColumns}
dataSource={taskList}
pagination={false}
scroll={{ y: 260 }}
style={{ height: '100%' }}
/>
</Card>
</Col>
{/* 右侧:任务编排、参数与天气 */}
<Col xs={24} xl={10}>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>任务编排与 AI 建议</span>}

View File

@@ -91,7 +91,7 @@ export default function DeviceIndexPage() {
onDeviceChange={(sn) => setSelectedSn(sn)}
/>;
}
if (activeTab === 'wayLineTask') return <WayLinePage />;
if (activeTab === 'wayLineTask') return <WayLinePage planedevices={planedevices} />;
return null;
};

View File

@@ -21,6 +21,11 @@
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
"noEmit": true,
},
"exclude": [
"src/**/*.jsx",
"node_modules"
]
}