801 lines
34 KiB
TypeScript
801 lines
34 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
|
||
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker
|
||
} from 'antd';
|
||
import {
|
||
PlusOutlined, ImportOutlined, NodeIndexOutlined,
|
||
SaveOutlined, FullscreenOutlined, EnvironmentOutlined,
|
||
BranchesOutlined, CloudOutlined, ThunderboltOutlined, CheckCircleOutlined,
|
||
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, Maximize2, Play, Square, X, MapPin, RotateCw
|
||
} from 'lucide-react';
|
||
import { getsinglePlan, getsinglePlanTrack, updateFlightTaskStatus, postFlightCommmand } from '@/api/device';
|
||
import dayjs from 'dayjs';
|
||
import utils from '@/lib/utils';
|
||
import CesiumMap from '../Map';
|
||
|
||
const { Text, Title } = Typography;
|
||
const { RangePicker } = DatePicker;
|
||
|
||
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 [listDrone, setListDrone] = useState(null);
|
||
const [listDateRange, setListDateRange] = useState(null);
|
||
const [taskList, setTaskList] = useState([]);
|
||
|
||
// 工作看板 独立筛选状态
|
||
const [boardDrone, setBoardDrone] = useState(null);
|
||
const [boardDateRange, setBoardDateRange] = useState(null);
|
||
const [boardTaskList, setBoardTaskList] = useState([]);
|
||
|
||
const [points, setPoints] = useState([]);
|
||
|
||
// 任务控制相关状态
|
||
const [selectedTask, setSelectedTask] = useState(null);
|
||
const [taskstatus, setTaskStatus] = useState('suspended');
|
||
const [isReturningHome, setIsReturningHome] = useState(false);
|
||
const [isTaskPaused, setIsTaskPaused] = useState(false);
|
||
const [flightCommandLoading, setFlightCommandLoading] = useState(false);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
// 顶部统计
|
||
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 getListTask = async () => {
|
||
if (!planedevices?.length) return;
|
||
try {
|
||
const defaultStart = dayjs().startOf('day').unix(); // 秒级
|
||
const defaultEnd = dayjs().endOf('day').unix(); // 秒级
|
||
|
||
const [beginAt, endAt] = listDateRange?.length === 2
|
||
? [dayjs(listDateRange[0]).unix(), dayjs(listDateRange[1]).unix()]
|
||
: [defaultStart, defaultEnd];
|
||
|
||
const sns = listDrone ? [listDrone.gateway_sn] : planedevices.map(item => item.gateway_sn);
|
||
const res = await getsinglePlan({ sns, beginAt, endAt });
|
||
if (res?.code === 200) {
|
||
// 处理嵌套数据结构:data: { [sn]: { list: [] } }
|
||
let combinedList = [];
|
||
if (res.data) {
|
||
Object.values(res.data).forEach((item: any) => {
|
||
if (item.list) combinedList = combinedList.concat(item.list);
|
||
});
|
||
}
|
||
setTaskList(combinedList);
|
||
}
|
||
} catch (err) {
|
||
message.error('任务列表数据获取失败');
|
||
}
|
||
};
|
||
|
||
// 获取看板任务数据 【时间戳已改为秒级】
|
||
const getBoardTask = async () => {
|
||
if (!planedevices?.length) return;
|
||
try {
|
||
const defaultStart = dayjs().startOf('day').unix(); // 秒级
|
||
const defaultEnd = dayjs().endOf('day').unix(); // 秒级
|
||
|
||
const [beginAt, endAt] = boardDateRange?.length === 2
|
||
? [dayjs(boardDateRange[0]).unix(), dayjs(boardDateRange[1]).unix()]
|
||
: [defaultStart, defaultEnd];
|
||
|
||
const sns = boardDrone ? [boardDrone.gateway_sn] : planedevices.map(item => item.gateway_sn);
|
||
const res = await getsinglePlan({ sns, beginAt, endAt });
|
||
if (res?.code === 200) {
|
||
// 处理嵌套数据结构
|
||
let combinedList = [];
|
||
if (res.data) {
|
||
Object.values(res.data).forEach((item: any) => {
|
||
if (item.list) combinedList = combinedList.concat(item.list);
|
||
});
|
||
}
|
||
setBoardTaskList(combinedList);
|
||
}
|
||
} catch (err) {
|
||
message.error('任务看板数据获取失败');
|
||
}
|
||
};
|
||
|
||
// 初始化默认全部设备
|
||
useEffect(() => {
|
||
if (planedevices?.length) {
|
||
setListDrone(planedevices[0]); // 默认显示第一个无人机
|
||
setBoardDrone(null); // 默认显示所有
|
||
}
|
||
}, [planedevices]);
|
||
|
||
// 各自筛选变更独立请求
|
||
useEffect(() => {
|
||
getListTask();
|
||
}, [listDrone, listDateRange]);
|
||
|
||
useEffect(() => {
|
||
getBoardTask();
|
||
}, [boardDrone, boardDateRange]);
|
||
|
||
// 任务状态标签
|
||
const getStatusTag = (status) => {
|
||
const statusMap = {
|
||
success: { color: 'success', text: '执行成功' },
|
||
preparing: { color: 'blue', text: '准备中' },
|
||
running: { color: 'processing', text: '执行中' },
|
||
failed: { color: 'error', text: '执行失败' },
|
||
waiting: { color: 'default', text: '待开始' },
|
||
starting_failure: { color: 'error', text: '启动失败' },
|
||
executing: { color: 'processing', text: '执行中' },
|
||
suspended: { color: 'warning', text: '已挂起' },
|
||
restored: { color: 'processing', text: '恢复执行' },
|
||
terminated: { color: 'default', text: '已终止' },
|
||
timeout: { color: 'error', text: '执行超时' },
|
||
};
|
||
const item = statusMap[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: '开始时间',
|
||
dataIndex: 'begin_at',
|
||
width: 170,
|
||
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||
},
|
||
{
|
||
title: '结束时间',
|
||
dataIndex: 'end_at',
|
||
width: 170,
|
||
render: t => t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||
},
|
||
];
|
||
|
||
// 表格行点击查看轨迹
|
||
const handleRowClick = async (record) => {
|
||
setSelectedTask(record); // 记录当前选中的任务
|
||
try {
|
||
const res = await getsinglePlanTrack(record);
|
||
if (res.code === 0 && res.data.track?.points?.length) {
|
||
const points = res.data.track.points.map(item => ({
|
||
lon: item.longitude || item.lon,
|
||
lat: item.latitude || item.lat
|
||
}));
|
||
setPoints(points);
|
||
} else {
|
||
utils.message.warning("暂无航线数据");
|
||
setPoints([]);
|
||
}
|
||
} catch (err) {
|
||
message.error('请求失败');
|
||
console.error(err);
|
||
}
|
||
};
|
||
|
||
// 看板数据分组处理
|
||
const groupBoardData = () => {
|
||
// 待执行 = 准备中 + 待开始 + 已挂起 + 已终止
|
||
const waitList = boardTaskList.filter(item =>
|
||
['preparing', 'waiting', 'suspended', 'terminated'].includes(item.status)
|
||
);
|
||
// 执行中 = 运行中 + 执行中 + 恢复执行
|
||
const runList = boardTaskList.filter(item =>
|
||
['running', 'executing', 'restored'].includes(item.status)
|
||
);
|
||
// 已完成 = 执行成功
|
||
const finishList = boardTaskList.filter(item =>
|
||
['success'].includes(item.status)
|
||
);
|
||
return { waitList, runList, finishList };
|
||
};
|
||
const { waitList, runList, finishList } = groupBoardData();
|
||
|
||
// 指令下发逻辑
|
||
const sendFlightCommand = async (command: 'return_home' | 'return_home_cancel' | 'flighttask_pause' | 'flighttask_recovery') => {
|
||
if (!listDrone?.device_sn) {
|
||
utils.message.warning('未选中无人机或无人机SN不存在');
|
||
return;
|
||
}
|
||
|
||
setFlightCommandLoading(true);
|
||
const data = {
|
||
command,
|
||
deviceSn: listDrone.device_sn,
|
||
}
|
||
try {
|
||
const res: any = await postFlightCommmand(data);
|
||
if (res.code === 200 || res.code === 0) {
|
||
utils.message.success('指令下发成功');
|
||
// 切换状态
|
||
if (command === 'return_home') setIsReturningHome(true);
|
||
if (command === 'return_home_cancel') setIsReturningHome(false);
|
||
if (command === 'flighttask_pause') setIsTaskPaused(true);
|
||
if (command === 'flighttask_recovery') setIsTaskPaused(false);
|
||
} else {
|
||
utils.message.error(res.msg || '指令下发失败');
|
||
}
|
||
} catch (err) {
|
||
utils.message.error('接口请求异常');
|
||
} finally {
|
||
setFlightCommandLoading(false);
|
||
}
|
||
};
|
||
|
||
// 任务状态更新逻辑
|
||
const updateTaskStatus = async (taskId: string, status: string) => {
|
||
if (!taskId) {
|
||
utils.message.warning('请先选择航线任务');
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
try {
|
||
const res: any = await updateFlightTaskStatus(taskId, status);
|
||
if (res.code === 200 || res.code === 0) {
|
||
const statusText = {
|
||
executing: '任务执行成功',
|
||
suspended: '任务已挂起',
|
||
restored: '任务已继续',
|
||
return_home: '任务已返航'
|
||
}[status] || '状态更新成功';
|
||
|
||
if (status === "suspended") {
|
||
setTaskStatus('suspended');
|
||
} else {
|
||
setTaskStatus('restored');
|
||
}
|
||
utils.message.success(statusText);
|
||
// 刷新任务列表
|
||
getListTask();
|
||
} else {
|
||
utils.message.error(res.msg || '操作失败');
|
||
}
|
||
} catch (err) {
|
||
utils.message.error('操作接口异常');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="normal-spacing" style={{ background: '#f5f7fa', minHeight: '100vh', padding: '16px', paddingTop: 0 }}>
|
||
<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, { size: 18 })}
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<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 }}>
|
||
{item.trend || item.desc}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
))}
|
||
</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 }}>航线编辑与路径规划</span>}
|
||
size="small"
|
||
extra={
|
||
<Space size={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' }}
|
||
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' }}>
|
||
<CesiumMap points={points} from="device-control" />
|
||
<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' }}></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 }}>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>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 独立筛选 - 设备任务列表 */}
|
||
<Card
|
||
title={
|
||
<div className="flex justify-between w-full items-center">
|
||
<span style={{ fontSize: 14, fontWeight: 700 }}>设备任务列表</span>
|
||
<Space size={12}>
|
||
<RangePicker
|
||
size="small"
|
||
showTime
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
value={listDateRange}
|
||
onChange={(val) => setListDateRange(val)}
|
||
placeholder={['开始时间', '结束时间']}
|
||
/>
|
||
<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={listDrone?.device_sn}
|
||
onChange={(val) => {
|
||
const find = planedevices.find(i => i.device_sn === val);
|
||
setListDrone(find || null);
|
||
}}
|
||
/>
|
||
</Space>
|
||
</div>
|
||
}
|
||
size="small"
|
||
extra={<PlusOutlined style={{ cursor: 'pointer', color: '#165DFF', fontSize: 12, marginLeft: 10 }} />}
|
||
bordered={false}
|
||
bodyStyle={{ padding: 0, display: 'flex', flexDirection: 'column', flex: 1 }}
|
||
style={{ ...cardStyle, flex: 1 }}
|
||
>
|
||
<Table
|
||
size="small"
|
||
rowKey="uuid"
|
||
columns={taskColumns}
|
||
dataSource={taskList}
|
||
pagination={false}
|
||
scroll={{ y: 'auto', maxHeight: 260 }}
|
||
style={{ height: '100%' }}
|
||
onRow={(record) => ({
|
||
onClick: () => handleRowClick(record),
|
||
style: { cursor: 'pointer' }
|
||
})}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col xs={24} xl={10}>
|
||
<Card
|
||
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>任务编排与 AI 建议</span>}
|
||
bordered={false}
|
||
bodyStyle={{ padding: '12px 16px' }}
|
||
style={{ ...cardStyle, marginBottom: 16 }}
|
||
>
|
||
<Table
|
||
size="small"
|
||
pagination={false}
|
||
dataSource={[
|
||
{ key: '1', id: 'FL-03', device: 'UAV-A01', type: '无人机热斑巡检', level: '高', time: '10:00 - 11:20', duration: '1h20m', status: '建议执行' },
|
||
{ key: '2', id: 'RB-02', device: 'RBT-01', type: '围栏巡检', level: '高', time: '11:30 - 12:30', duration: '1h00m', status: '建议执行' },
|
||
{ key: '3', id: 'CL-05', device: 'UAV-C01', type: '组件清洗', level: '中', time: '14:00 - 16:00', duration: '2h00m', status: '等待执行' },
|
||
{ key: '4', id: 'GR-04', device: 'GR-01', type: '阵列除草', level: '中', time: '09:30 - 11:30', duration: '2h00m', status: '建议执行' },
|
||
{ key: '5', id: 'FL-07', device: 'UAV-A01', type: 'AI 复检航线', level: '低', time: '16:30 - 17:10', duration: '0h40m', status: '待审批' },
|
||
]}
|
||
columns={[
|
||
{ title: '编号', dataIndex: 'id', width: 70, render: (t) => <Text style={{ color: '#165DFF', fontWeight: 600 }}>{t}</Text> },
|
||
{ title: '装备', dataIndex: 'device', width: 80 },
|
||
{ title: '类型', dataIndex: 'type', ellipsis: true },
|
||
{ title: '优先级', dataIndex: 'level', width: 80, render: (l) => <span style={{ color: l === '高' ? '#F53F3F' : l === '中' ? '#FF7D00' : '#86909C', fontSize: 11, fontWeight: 600 }}>▲ {l}</span> },
|
||
{ title: '建议时间', dataIndex: 'time', width: 110 },
|
||
{ title: '状态', dataIndex: 'status', width: 90, render: (s) => <Text style={{ color: s.includes('建议') ? '#165DFF' : '#86909C', fontSize: 11, fontWeight: 500 }}>{s}</Text> },
|
||
]}
|
||
/>
|
||
<div style={{ background: '#F8F9FB', padding: '10px 14px', borderRadius: 8, marginTop: 14, border: '1px solid #F1F2F5' }}>
|
||
<div style={{ display: 'flex', gap: 12, fontSize: fontSmall, alignItems: 'center' }}>
|
||
<span style={{ fontWeight: 700, color: '#1D2129' }}>AI 建议:</span>
|
||
<Space split={<Divider type="vertical" style={{ margin: 0 }} />}>
|
||
<Space size={4}><CheckCircle2 size={13} color="#165DFF" /> <span>推荐优先执行 FL-03 航线</span></Space>
|
||
<Space size={4}><Clock size={13} color="#165DFF" /> <span>14:00-16:00 风速最佳</span></Space>
|
||
<Space size={4}><AlertCircle size={13} color="#FF7D00" /> <span>区块 B-12 存在高温风险</span></Space>
|
||
</Space>
|
||
</div>
|
||
</div>
|
||
<Row gutter={12} style={{ marginTop: 16 }}>
|
||
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 36, borderRadius: 18, fontWeight: 600 }}>一键排程</Button></Col>
|
||
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 36, borderRadius: 18, fontWeight: 600 }}>智能分配</Button></Col>
|
||
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 36, borderRadius: 18, fontWeight: 600 }}>生成工单</Button></Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
<Card
|
||
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>航线参数 / 作业参数</span>}
|
||
extra={<Text type="secondary" style={{ fontSize: 12 }}>当前航线:FL-03</Text>}
|
||
bordered={false}
|
||
bodyStyle={{ padding: '16px' }}
|
||
style={{ ...cardStyle, marginBottom: 16 }}
|
||
>
|
||
<Row gutter={[24, 16]}>
|
||
{[
|
||
{ label: '飞行高度', value: '35', unit: 'm' },
|
||
{ label: '速度', value: '6', unit: 'm/s' },
|
||
{ label: '航向重叠', value: '75', unit: '%' },
|
||
{ label: '旁向重叠', value: '65', unit: '%' },
|
||
{ label: '安全返航电量', value: '25', unit: '%' },
|
||
{ label: '清洗喷洒量', value: '120', unit: 'ml/㎡' },
|
||
{ label: '除草宽幅', value: '1.2', unit: 'm' },
|
||
{ label: '巡检采样频率', value: '2', unit: 'Hz' },
|
||
].map((item, idx) => (
|
||
<Col span={6} key={idx}>
|
||
<div style={{ fontSize: 11, color: '#86909C', marginBottom: 6 }}>{item.label}</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<Input size="small" defaultValue={item.value} style={{ width: '100%', height: 26, borderRadius: 4, textAlign: 'center', fontWeight: 600 }} />
|
||
<span style={{ fontSize: 11, color: '#4E5969' }}>{item.unit}</span>
|
||
</div>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
<Divider style={{ margin: '16px 0' }} />
|
||
<div style={{ display: 'flex', gap: 32 }}>
|
||
<Space size={8}>
|
||
<span style={{ fontSize: fontNormal, color: '#4E5969' }}>避障开关</span>
|
||
<Switch size="small" defaultChecked />
|
||
</Space>
|
||
<Space size={8}>
|
||
<span style={{ fontSize: fontNormal, color: '#4E5969' }}>夜航限制</span>
|
||
<Switch size="small" />
|
||
</Space>
|
||
</div>
|
||
<Divider style={{ margin: '16px 0' }} />
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
|
||
{/* 执行/挂起/继续 逻辑 */}
|
||
{selectedTask?.status === 'executing' || selectedTask?.status === 'running' || selectedTask?.status === 'restored' ? (
|
||
<Button
|
||
type="primary"
|
||
danger
|
||
icon={<Square size={16} />}
|
||
onClick={() => updateTaskStatus(selectedTask?.uuid, 'suspended')}
|
||
loading={loading}
|
||
// 统一样式:高度36 + 圆角18 + 字重600 + 图标文字居中
|
||
style={{ height: 36, borderRadius: 18, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||
>
|
||
挂起任务
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
type="primary"
|
||
icon={<Play size={16} />}
|
||
onClick={() => {
|
||
if (selectedTask?.status === 'suspended') {
|
||
updateTaskStatus(selectedTask?.uuid, 'restored');
|
||
} else {
|
||
updateTaskStatus(selectedTask?.uuid, 'executing');
|
||
}
|
||
}}
|
||
loading={loading}
|
||
// 统一样式:主色#165DFF + 高度36 + 圆角18 + 字重600
|
||
style={{ background: '#165DFF', height: 36, borderRadius: 18, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||
>
|
||
{selectedTask?.status === 'suspended' ? '继续任务' : '执行任务'}
|
||
</Button>
|
||
)}
|
||
{/* 返航逻辑 */}
|
||
<Button
|
||
type="primary"
|
||
danger={isReturningHome}
|
||
icon={isReturningHome ? <X size={16} /> : <MapPin size={16} />}
|
||
onClick={() => sendFlightCommand(isReturningHome ? 'return_home_cancel' : 'return_home')}
|
||
loading={flightCommandLoading}
|
||
style={{ background: '#165DFF', height: 36, borderRadius: 18, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||
>
|
||
{isReturningHome ? '取消返航' : '返航'}
|
||
</Button>
|
||
|
||
{/* 暂停/恢复逻辑 */}
|
||
<Button
|
||
type="default"
|
||
icon={isTaskPaused ? <RotateCw size={16} /> : <Square size={16} />}
|
||
onClick={() => sendFlightCommand(isTaskPaused ? 'flighttask_recovery' : 'flighttask_pause')}
|
||
loading={flightCommandLoading}
|
||
style={{ height: 36, borderRadius: 18, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||
>
|
||
{isTaskPaused ? '恢复任务' : '暂停任务'}
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
|
||
<Card
|
||
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>天气窗口与执行约束</span>}
|
||
bordered={false}
|
||
bodyStyle={{ padding: '16px' }}
|
||
style={cardStyle}
|
||
>
|
||
<Row gutter={16} align="middle">
|
||
<Col span={6}>
|
||
<div style={{ textAlign: 'center', background: '#F6FFED', padding: '12px 8px', borderRadius: 10, border: '1px solid #B7EB8F' }}>
|
||
<div style={{ fontSize: 11, color: '#52C41A', fontWeight: 700, marginBottom: 8 }}>适宜执行</div>
|
||
<CheckCircle2 size={24} color="#52C41A" style={{ margin: '0 auto 8px' }} />
|
||
<div style={{ fontSize: 10, color: '#86909C' }}>适宜航行</div>
|
||
</div>
|
||
</Col>
|
||
<Col span={18}>
|
||
<Row gutter={[16, 16]}>
|
||
{[
|
||
{
|
||
label: '风速',
|
||
value: `${listDrone?.wind_speed ?? '-'} m/s`,
|
||
icon: <Wind size={16} />,
|
||
color: '#165DFF',
|
||
judge: (val) => {
|
||
const speed = parseFloat(val);
|
||
if (isNaN(speed)) return { desc: '未知', color: '#86909C' };
|
||
if (speed <= 5) return { desc: '适宜', color: '#52C41A' };
|
||
if (speed <= 8) return { desc: '谨慎', color: '#FF7D00' };
|
||
return { desc: '不适宜', color: '#F53F3F' };
|
||
}
|
||
},
|
||
{
|
||
label: '温度',
|
||
value: `${listDrone?.environment_temperature ?? '-'}℃`,
|
||
icon: <Thermometer size={16} />,
|
||
color: '#FF7D00',
|
||
judge: (val) => {
|
||
const temp = parseFloat(val);
|
||
if (isNaN(temp)) return { desc: '未知', color: '#86909C' };
|
||
if (temp >= 0 && temp <= 40) return { desc: '适宜', color: '#52C41A' };
|
||
return { desc: '不适宜', color: '#F53F3F' };
|
||
}
|
||
},
|
||
{
|
||
label: '降雨',
|
||
value: `${listDrone?.rainfall ?? '-'} `,
|
||
icon: <ThunderboltOutlined size={16} />,
|
||
color: '#FF7D00',
|
||
judge: (val) => {
|
||
if (val?.trim() === "no_rain") {
|
||
return { desc: '适宜', color: '#52C41A' };
|
||
} else {
|
||
return { desc: '不适宜', color: '#F53F3F' };
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: '信号质量',
|
||
value: (() => {
|
||
const q = listDrone?.network_state?.quality;
|
||
if (q === 'good') return '优';
|
||
if (q === 'better') return '良';
|
||
if (q === 'moderate') return '中';
|
||
if (q === 'poor') return '差';
|
||
if (q === 'too_pool') return '极差';
|
||
return '正常';
|
||
})(),
|
||
icon: <Satellite size={16} />,
|
||
color: '#00B42A',
|
||
judge: (val) => {
|
||
if (val === '优' || val === '良' || val === '正常') return { desc: '适宜', color: '#52C41A' };
|
||
if (val === '中') return { desc: '谨慎', color: '#FF7D00' };
|
||
return { desc: '不适宜', color: '#F53F3F' };
|
||
}
|
||
},
|
||
].map((item, idx) => {
|
||
const status = item.judge(item.value);
|
||
return (
|
||
<Col span={12} key={idx}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<div
|
||
style={{
|
||
width: 32, height: 32, borderRadius: 8,
|
||
background: '#F7F8FA', display: 'flex',
|
||
justifyContent: 'center', alignItems: 'center',
|
||
color: item.color
|
||
}}
|
||
>
|
||
{item.icon}
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 10, color: '#86909C' }}>{item.label}</div>
|
||
<div style={{ fontSize: fontNormal, fontWeight: 700, color: '#1D2129' }}>
|
||
{item.value}
|
||
</div>
|
||
<div style={{ fontSize: 10, color: status.color, fontWeight: 500 }}>
|
||
{status.desc}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Col>
|
||
);
|
||
})}
|
||
</Row>
|
||
</Col>
|
||
</Row>
|
||
<div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 6, fontSize: 11, color: '#86909C', borderTop: '1px solid #F1F2F5', paddingTop: 12 }}>
|
||
<Space size={6}><AlertCircle size={14} color="#FF7D00" /> <span>16:30 后风速升高,建议提前完成任务</span></Space>
|
||
<Space size={6}><Clock size={14} color="#165DFF" /> <span>预计 14:00-16:00 为最佳执行窗口</span></Space>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
{/* 底部内容区 */}
|
||
<Row gutter={16}>
|
||
<Col xs={24} xl={14}>
|
||
<Card
|
||
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>任务甘特视图 / 排班视图 <Text type="secondary" style={{ fontSize: 11, fontWeight: 400, marginLeft: 12 }}>今日 (2025-05-24)</Text></span>}
|
||
extra={<MoreOutlined style={{ cursor: 'pointer', fontSize: 18 }} />}
|
||
bordered={false}
|
||
bodyStyle={{ padding: 16 }}
|
||
style={{ ...cardStyle, height: '100%' }}
|
||
>
|
||
<div style={{ position: 'relative', border: '1px solid #F2F3F5', borderRadius: 8, padding: '16px 12px' }}>
|
||
<div style={{ display: 'flex', fontSize: 10, color: '#86909C', borderBottom: '1px solid #F2F3F5', paddingBottom: 10, textAlign: 'center' }}>
|
||
<div style={{ width: 140 }}></div>
|
||
<div style={{ flex: 1, display: 'flex', justifyContent: 'space-around' }}>
|
||
<span>08:00</span><span>10:00</span><span>12:00</span><span>14:00</span><span>16:00</span><span>18:00</span>
|
||
</div>
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 16 }}>
|
||
{[
|
||
{ label: '无人机机场 A01', sub: '(UAV-A01)', tasks: [{ start: '25%', width: '18%', title: 'FL-03 热斑巡检', time: '10:00-11:20', color: '#165DFF' }, { start: '75%', width: '12%', title: 'FL-07 复检', time: '16:30-17:10', color: '#165DFF' }] },
|
||
{ label: '巡检机器人 RBT-01', sub: '', tasks: [{ start: '45%', width: '15%', title: 'RB-02 围栏巡检', time: '11:30-12:30', color: '#FF7D00' }] },
|
||
{ label: '除草机器人 GR-01', sub: '', tasks: [{ start: '10%', width: '22%', title: 'GR-04 阵列除草', time: '09:30-11:30', color: '#00B42A' }] },
|
||
{ label: '清洗无人机 UAV-C01', sub: '', tasks: [{ start: '65%', width: '20%', title: 'CL-05 组件清洗', time: '14:00-16:00', color: '#13C2C2' }] },
|
||
].map((row, idx) => (
|
||
<div key={idx} style={{ display: 'flex', alignItems: 'center' }}>
|
||
<div style={{ width: 140, paddingRight: 12 }}>
|
||
<div style={{ fontSize: 11, fontWeight: 600, color: '#1D2129' }}>{row.label}</div>
|
||
<div style={{ fontSize: 10, color: '#86909C' }}>{row.sub}</div>
|
||
</div>
|
||
<div style={{ flex: 1, height: 40, background: '#F7F8FA', borderRadius: 6, position: 'relative' }}>
|
||
{row.tasks.map((task, tidx) => (
|
||
<div key={tidx} style={{
|
||
position: 'absolute', left: task.start, width: task.width, height: '100%',
|
||
background: task.color, borderRadius: 6, color: '#fff',
|
||
padding: '4px 8px', display: 'flex', flexDirection: 'column', justifyContent: 'center',
|
||
boxShadow: '0 2px 6px rgba(0,0,0,0.1)', zIndex: 2
|
||
}}>
|
||
<div style={{ fontSize: 10, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{task.title}</div>
|
||
<div style={{ fontSize: 9, opacity: 0.9 }}>{task.time}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div style={{ position: 'absolute', left: '38%', top: 0, bottom: 0, width: 2, background: '#F53F3F', zIndex: 1, opacity: 0.6 }}>
|
||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#F53F3F', position: 'absolute', top: -3, left: -2 }} />
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col xs={24} xl={10}>
|
||
<Card
|
||
title={
|
||
<div className="flex justify-between items-center w-full">
|
||
<span style={{ fontSize: fontTitle, fontWeight: 700 }}>工作任务看板</span>
|
||
<Space size={8}>
|
||
<RangePicker
|
||
size="small"
|
||
showTime
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
value={boardDateRange}
|
||
onChange={(val) => setBoardDateRange(val)}
|
||
placeholder={['开始', '结束']}
|
||
/>
|
||
<Select
|
||
style={{ width: 140 }}
|
||
size="small"
|
||
placeholder="全部无人机"
|
||
showSearch
|
||
allowClear
|
||
options={planedevices?.map(item => ({
|
||
value: item.device_sn,
|
||
label: item.drone_callsign || item.device_sn
|
||
}))}
|
||
value={boardDrone?.device_sn}
|
||
onChange={(val) => {
|
||
const find = planedevices.find(i => i.device_sn === val);
|
||
setBoardDrone(find || null);
|
||
}}
|
||
/>
|
||
</Space>
|
||
</div>
|
||
}
|
||
bordered={false}
|
||
bodyStyle={{ padding: 16 }}
|
||
style={{ ...cardStyle, height: '100%', maxHeight: 480, overflow: "auto" }}
|
||
>
|
||
<Row gutter={10}>
|
||
{[
|
||
{
|
||
title: '待执行', count: waitList.length, color: '#4E5969', bg: '#F7F8FA',
|
||
list: waitList.map(item => ({
|
||
id: item.sn?.slice(-5) || '',
|
||
name: item.name,
|
||
device: item.sn,
|
||
begin_at: ''
|
||
}))
|
||
},
|
||
{
|
||
title: '执行中', count: runList.length, color: '#165DFF', bg: '#E8F3FF',
|
||
list: runList.map(item => ({
|
||
id: item.sn?.slice(-5) || '',
|
||
name: item.name,
|
||
device: item.sn,
|
||
begin_at: ''
|
||
}))
|
||
},
|
||
{
|
||
title: '已完成', count: finishList.length, color: '#00B42A', bg: '#E8FFEA',
|
||
list: finishList.map(item => ({
|
||
id: item.sn?.slice(-5) || '',
|
||
name: item.name,
|
||
device: item.sn,
|
||
begin_at: item.begin_at,
|
||
completed_at: item.completed_at
|
||
}))
|
||
},
|
||
].map((col, idx) => (
|
||
<Col span={8} key={idx}>
|
||
<div style={{ background: col.bg, borderRadius: 10, padding: 10, height: '100%', minHeight: 280 }}>
|
||
<div style={{ fontSize: 12, fontWeight: 700, marginBottom: 12, display: 'flex', justifyContent: 'space-between', color: col.color }}>
|
||
<span>{col.title} ({col.count})</span>
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||
{col.list.map((task, tidx) => (
|
||
<div key={tidx} style={{ background: '#fff', borderRadius: 8, padding: 10, boxShadow: '0 2px 4px rgba(0,0,0,0.04)', border: '1px solid #F1F2F5' }}>
|
||
<div style={{ fontSize: 11, fontWeight: 700, color: '#165DFF', marginBottom: 6, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||
{task.name}
|
||
</div> <div style={{ fontSize: 10, color: '#86909C' }}>
|
||
<div style={{ marginBottom: 2 }}>装备: {task.device || 'UAV-A01'}</div>
|
||
{task.begin_at && <div style={{ marginTop: 2 }}>开始时间: {task.begin_at}</div>}
|
||
{task.completed_at && <div style={{ marginTop: 2 }}>完成时间: {task.completed_at}</div>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
{/*<div style={{ textAlign: 'center', marginTop: 12 }}>
|
||
<Button type="link" size="small" style={{ fontSize: 12, color: '#86909C' }}>查看全部 <ArrowRightOutlined style={{ fontSize: 10 }} /></Button>
|
||
</div>*/}
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
);
|
||
}
|