Files
web-Iot/src/components/devices/WayLinePage.tsx

1547 lines
58 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, useRef } from 'react';
import {
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker,
Modal, InputNumber
} from 'antd';
import {
PlusOutlined, ImportOutlined, NodeIndexOutlined,
SaveOutlined, FullscreenOutlined, EnvironmentOutlined,
BranchesOutlined, CloudOutlined, ThunderboltOutlined, CheckCircleOutlined,
InfoCircleOutlined, ClockCircleOutlined,
SettingOutlined, WarningOutlined, ArrowRightOutlined,
SearchOutlined, MoreOutlined, AimOutlined, PlayCircleOutlined
} 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, getWayLine, createWaylineTask, getFlightTaskDetail
} from '@/api/device';
import dayjs from 'dayjs';
import utils from '@/lib/utils';
import CesiumMap from '../Map';
import { getUAVState } from '@/api/device';
import { fix2, getVal } from '@/lib/utils';
const { Text, Title } = Typography;
const { RangePicker } = DatePicker;
export default function WayLinePage({ planedevices, onRefresh }) {
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 [listDroneRealtime, setListDroneRealtime] = useState(null);
const [listDateRange, setListDateRange] = useState(null);
const [taskList, setTaskList] = useState([]);
const listDroneTimerRef = useRef<any>(null);
// 定时获取任务列表所选无人机的实时状态
useEffect(() => {
const fetchRealtime = async () => {
if (listDrone?.gateway_sn && listDrone?.device_sn) {
try {
const res = await getUAVState({
sn: listDrone.gateway_sn,
droneSn: listDrone.device_sn
});
if (res.code === 200) {
setListDroneRealtime(res.data);
}
} catch (err) {
console.error('获取列表无人机实时状态失败', err);
}
} else {
setListDroneRealtime(null);
}
};
fetchRealtime();
listDroneTimerRef.current = setInterval(fetchRealtime, 3000);
return () => {
if (listDroneTimerRef.current) clearInterval(listDroneTimerRef.current);
};
}, [listDrone]);
// 工作看板 独立筛选状态
const [boardDrone, setBoardDrone] = useState(null);
const [boardDateRange, setBoardDateRange] = useState(null);
const [boardTaskList, setBoardTaskList] = useState([]);
// ✅ 航线列表 独立无人机筛选(新增)
const [waylineDrone, setWaylineDrone] = useState(null);
const [focusLocation, setFocusLocation] = useState(null);
const [points, setPoints] = useState([]);
const fetchAndFocusDrone = async (drone) => {
if (!drone?.gateway_sn || !drone?.device_sn) return;
try {
const res = await getUAVState({
sn: drone.gateway_sn,
droneSn: drone.device_sn
});
if (res.code === 200 && res.data) {
setFocusLocation({
lon: res.data.longitude,
lat: res.data.latitude,
type: drone.type || 'plane',
name: drone.drone_callsign || drone.name || drone.device_sn
});
}
} catch (err) {
console.error('获取无人机位置失败', err);
}
};
// 航线列表相关状态
const [selectedWayline, setSelectedWayline] = useState(null);
const [rthModalVisible, setRthModalVisible] = useState(false);
const [tempRthAltitude, setTempRthAltitude] = useState(20);
const [waylineSearch, setWaylineSearch] = 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 [createTaskModal, setCreateTaskModal] = useState(false);
const [taskDetail, setTaskDetail] = useState(null);
const [waylineUuid, setWaylineUuid] = useState('');
const [waylineList, setWaylineList] = useState([]); // 航线列表
const [taskForm, setTaskForm] = useState({
name: '',
sn: '',
landing_dock_sn: '',
rth_altitude: 10,
rth_mode: 'optimal',
wayline_precision_type: 'gps',
resumable_status: 'auto',
min_battery_capacity: 50,
task_type: 'immediate',
repeat_type: 'nonrepeating',
repeat_option: {
interval: 1,
days_of_week: [],
days_of_month: [],
week_of_month: 1,
},
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
recurring_task_start_time_list: [],
});
// 根据 repeat_type 自动构建接口需要的 repeat_option
const buildRepeatOption = () => {
const { repeat_type, repeat_option } = taskForm;
const { interval, days_of_week, days_of_month, week_of_month } = repeat_option;
if (repeat_type === 'nonrepeating') return undefined;
switch (repeat_type) {
case 'daily':
return { interval: Math.max(1, interval) };
case 'weekly':
return { interval, days_of_week };
case 'absolute_monthly':
return { interval, days_of_month };
case 'relative_monthly':
return { interval, week_of_month, days_of_week };
default:
return undefined;
}
};
const fetchWaylineList = async () => {
try {
const res = await getWayLine();
if (res?.code == 200 || res?.code == 0) {
const _list = res?.data?.list.map(item => {
return {
...item,
label: item.name || `航线_${item.id || '未知'}`,
value: item.id
}
})
setWaylineList(_list || []);
}
} catch (err) {
message.error('获取航线列表失败');
}
};
// 关闭弹窗重置
const resetModals = () => {
setCreateTaskModal(false);
setWaylineUuid('');
setTaskForm({
name: '',
sn: waylineDrone?.gateway_sn || '',
landing_dock_sn: '',
rth_altitude: 10,
rth_mode: 'optimal',
wayline_precision_type: 'gps',
resumable_status: 'auto',
min_battery_capacity: 50,
task_type: 'immediate',
repeat_type: 'nonrepeating',
repeat_option: { interval: 1 },
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
begin_at: null,
end_at: null,
recurring_task_start_time_list: [],
});
};
// 创建任务提交
const createFlightTask = async () => {
if (!taskForm.name || !waylineUuid || !taskForm.sn) {
utils.message.warning('请完善必填项');
return;
}
setLoading(true);
try {
const payload = {
...taskForm,
wayline_uuid: waylineUuid,
repeat_type: taskForm.repeat_type,
repeat_option: buildRepeatOption(),
recurring_task_start_time_list: taskForm.recurring_task_start_time_list.map(t => dayjs(t).unix()),
};
const res: any = await createWaylineTask(payload);
if (res.code === 200 || res.code === 0) {
utils.message.success('创建任务成功');
resetModals();
getListTask();
if (onRefresh) onRefresh();
} else {
utils.message.error(res.msg || '创建失败');
}
} catch (err) {
utils.message.error('创建异常');
} finally {
setLoading(false);
}
};
// 航线列表无人机变更时同步到表单
useEffect(() => {
if (waylineDrone) {
setTaskForm(prev => ({
...prev,
sn: waylineDrone.gateway_sn || ''
}));
}
}, [waylineDrone]);
// 顶部统计
const [todayCount, setTodayCount] = useState(0);
const [yesterdayCount, setYesterdayCount] = useState(0);
const [trend, setTrend] = useState('');
const [trendColor, setTrendColor] = useState('');
// 计算今日/昨日任务数量
const calcTodayTaskCount = () => {
if (!taskList?.length) return;
const todayStart = dayjs().startOf('day').valueOf();
const todayEnd = dayjs().endOf('day').valueOf();
const yesterdayStart = dayjs().subtract(1, 'day').startOf('day').valueOf();
const yesterdayEnd = dayjs().subtract(1, 'day').endOf('day').valueOf();
const todayTotal = taskList.filter(t => {
const time = dayjs(t.begin_at).valueOf();
return time >= todayStart && time <= todayEnd;
}).length;
const yesterdayTotal = taskList.filter(t => {
const time = dayjs(t.begin_at).valueOf();
return time >= yesterdayStart && time <= yesterdayEnd;
}).length;
const diff = todayTotal - yesterdayTotal;
let trendText = '';
let color = '#00B42A';
if (diff > 0) {
trendText = `较昨日 +${diff}`;
color = '#00B42A';
} else if (diff < 0) {
trendText = `较昨日 ${diff}`;
color = '#F53F3F';
} else {
trendText = '较昨日 持平';
color = '#86909C';
}
setTodayCount(todayTotal);
setYesterdayCount(yesterdayTotal);
setTrend(trendText);
setTrendColor(color);
};
useEffect(() => {
calcTodayTaskCount();
}, [taskList]);
// 获取任务列表数据
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) {
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(() => {
fetchWaylineList();
}, [])
// 初始化默认设备
useEffect(() => {
if (planedevices?.length) {
setListDrone(planedevices[0]);
setWaylineDrone(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: '设备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 getTrackFn = async (record) => {
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([]);
}
}
// 表格行点击查看轨迹
const handleRowClick = async (record) => {
setSelectedTask(record);
try {
getTaskDetail(record.uuid);
await getTrackFn(record);
} catch (err) {
message.error('请求失败');
}
};
// 航线列表点击处理
const handleWaylineClick = (wayline) => {
setSelectedWayline(wayline);
if (wayline.points && wayline.points.length > 0) {
const mapPoints = wayline.points.map(p => ({
lon: p.longitude || p.lon,
lat: p.latitude || p.lat
}));
setPoints(mapPoints);
} else if (wayline.track?.points?.length > 0) {
const mapPoints = wayline.track.points.map(p => ({
lon: p.longitude || p.lon,
lat: p.latitude || p.lat
}));
setPoints(mapPoints);
}
};
// 执行航线按钮点击(改为图标)
const handleExecuteClick = (e, wayline) => {
e.stopPropagation();
// ✅ 使用航线列表自己的无人机
if (!waylineDrone?.gateway_sn) {
utils.message.warning('请先在航线列表上方选择执行无人机');
return;
}
setSelectedWayline(wayline);
setTempRthAltitude(20);
setRthModalVisible(true);
};
useEffect(() => {
fetchAndFocusDrone(waylineDrone);
}, [waylineDrone])
// 确认执行航线任务
const confirmExecuteWayline = async () => {
if (!selectedWayline || !waylineDrone?.gateway_sn) return;
setLoading(true);
try {
const payload = {
name: `${selectedWayline.name}_${dayjs().format('MMDDHHmm')}`,
sn: waylineDrone.gateway_sn, // ✅ 使用航线列表无人机
wayline_uuid: selectedWayline.id || selectedWayline.uuid,
rth_altitude: tempRthAltitude,
task_type: 'immediate',
rth_mode: 'optimal',
wayline_precision_type: 'gps',
resumable_status: 'auto',
min_battery_capacity: 50,
repeat_type: 'nonrepeating',
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
const res: any = await createWaylineTask(payload);
if (res.code === 200 || res.code === 0) {
utils.message.success('航线执行任务创建成功');
setRthModalVisible(false);
getListTask();
//if (onRefresh) {
// setTimeout(() => {
// onRefresh();
// }, 1500)
//}
//await getTrackFn({
// uuid: res.data.task_uuid
//});
} else {
utils.message.error(res.msg || '执行失败');
}
} catch (err) {
utils.message.error('创建任务异常');
} finally {
setLoading(false);
}
};
// 看板数据分组处理
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 topCards = [
{
title: '今日航线计划', value: todayCount, unit: '条', desc: '进行中', trend: trend, trendColor: trendColor,
icon: <LayoutGrid size={22} />, color: '#165DFF'
},
{ title: '执行中任务', value: runList.length, unit: '个', desc: '进行中', icon: <Plane size={22} />, color: '#165DFF' },
{ title: '待审批任务', value: '4', unit: '个', trend: '较昨日 -1', trendColor: '#F53F3F', icon: <Calendar size={22} />, color: '#FF7D00' },
{ title: '任务完成率', value: finishList.length ? Math.round((finishList.length / taskList.length) * 100) : 0, 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 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 getTaskDetail = async (taskId) => {
if (!taskId) return;
setTaskDetail(null);
setLoading(true);
try {
const res = await getFlightTaskDetail(taskId);
if (res.code === 0) {
setTaskDetail(res.data);
} else {
setTaskDetail(null);
utils.message.error('暂无详情');
}
} catch (err) {
setTaskDetail(null);
utils.message.error('获取详情接口异常');
} finally {
setLoading(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: '0px', paddingTop: 0 }}>
<Row gutter={[16, 16]} style={{ marginBottom: 8 }}>
{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: 13, 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 devices={planedevices} points={points} focusLocation={focusLocation} 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 || item.callsign
}))}
value={listDrone?.device_sn}
onChange={(val) => {
const find = planedevices.find(i => i.device_sn === val);
setListDrone(find || null);
}}
/>
</Space>
</div>
}
size="small"
extra={
<Tooltip title="创建任务">
<PlusOutlined
style={{ cursor: 'pointer', color: '#165DFF', fontSize: 12, marginLeft: 10 }}
onClick={() => {
resetModals();
setCreateTaskModal(true);
}}
/>
</Tooltip>
}
bordered={false}
bodyStyle={{ padding: 0, display: 'flex', flexDirection: 'column', height: "calc(100% - 40px)", overflow: "auto" }}
style={{ ...cardStyle, maxHeight: "250px", flex: 1, marginBottom: 0 }}
>
<Table
size="small"
rowKey="uuid"
columns={taskColumns}
dataSource={taskList}
pagination={false}
style={{ height: "calc(100% - 40px)" }}
rowClassName={(record) =>
selectedTask?.uuid === record.uuid ? 'table-row-highlight' : ''
}
onRow={(record) => ({
onClick: () => handleRowClick(record),
style: { cursor: 'pointer' }
})}
/>
</Card>
</Col>
<Col xs={24} xl={10} style={{ display: "flex", flexDirection: "column" }} >
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>任务编排与 AI 建议</span>}
bordered={false}
bodyStyle={{ padding: '12px 16px' }}
style={{ ...cardStyle, marginBottom: 8 }}
>
<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: '2px 14px', borderRadius: 8, marginTop: 8, 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: 8 }}>
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600 }}>一键排程</Button></Col>
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600 }}>智能分配</Button></Col>
<Col span={8}><Button type="primary" block style={{ background: '#165DFF', height: 28, borderRadius: 18, fontWeight: 600 }}>生成工单</Button></Col>
</Row>
</Card>
<Card
title={<span style={{ fontSize: fontTitle, fontWeight: 700 }}>航线参数 / 作业参数</span>}
extra={<Text type="secondary" style={{ fontSize: 12 }}>当前航线:{selectedTask?.name || '-'}</Text>}
bordered={false}
bodyStyle={{ padding: '16px' }}
style={{ ...cardStyle, marginBottom: 8 }}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px', marginBottom: 8 }}>
{[
{ label: '飞行高度', value: taskDetail?.rth_altitude ?? '-', unit: 'm' },
{ label: '返航模式', value: taskDetail?.rth_mode === 'preset' ? '预设' : taskDetail?.rth_mode === 'optimal' ? '智能' : '-', unit: '' },
{ label: '航线精度', value: taskDetail?.wayline_precision_type === 'gps' ? 'GPS' : taskDetail?.wayline_precision_type === 'rtk' ? 'RTK' : '-', unit: '' },
{ label: '断点续飞', value: taskDetail?.resumable_status === 'auto' ? '自动' : taskDetail?.resumable_status === 'manual' ? '手动' : '-', unit: '' },
{ label: '安全返航电量', value: taskDetail?.min_battery_capacity ?? '-', unit: '%' },
{
label: '任务类型', value:
taskDetail?.task_type === 'immediate' ? '立即任务' :
taskDetail?.task_type === 'timed' ? '定时任务' :
taskDetail?.task_type === 'recurring' ? '重复任务' :
taskDetail?.task_type === 'continuous' ? '连续任务' : '-',
unit: ''
},
{
label: '重复类型', value:
taskDetail?.repeat_type === 'nonrepeating' ? '不重复' :
taskDetail?.repeat_type === 'daily' ? '每天' :
taskDetail?.repeat_type === 'weekly' ? '每周' :
taskDetail?.repeat_type === 'absolute_monthly' ? '按月(日期)' :
taskDetail?.repeat_type === 'relative_monthly' ? '按月(星期)' : '-',
unit: ''
},
{ label: '失控动作', value: taskDetail?.out_of_control_action_in_flight === 'return_home' ? '返航' : '-', unit: '' },
].map((item, idx) => (
<div
key={idx}
style={{
background: '#F7F8FA',
borderRadius: 8,
padding: '4px 10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<span style={{ fontSize: 12, color: '#4E5969' }}>{item.label}</span>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#1D2129' }}>{item.value}</span>
<span style={{ fontSize: 11, color: '#86909C' }}>{item.unit}</span>
</div>
</div>
))}
</div>
<div style={{ display: 'flex', gap: 32, marginBottom: 8 }}>
<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>
<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}
style={{ height: 28, 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}
style={{ background: '#165DFF', height: 28, 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: 28, 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: 28, 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, flex: 1, }}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '8px', marginBottom: 16 }}>
<div style={{ background: '#F7F8FA', borderRadius: 10, padding: '12px', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'rgba(22, 93, 255, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CloudOutlined size={20} style={{ color: '#165DFF' }} />
</div>
<div>
<div style={{ fontSize: 12, color: '#86909C' }}>适宜性</div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#00B42A' }}>适宜执行</div>
</div>
</div>
<div style={{ background: '#F7F8FA', borderRadius: 10, padding: '12px', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'rgba(22, 93, 255, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Wind size={20} style={{ color: '#165DFF' }} />
</div>
<div>
<div style={{ fontSize: 12, color: '#86909C' }}>风速</div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129' }}>{fix2((listDroneRealtime || listDrone)?.wind_speed)} m/s</div>
<div style={{ fontSize: 12, color: '#00B42A' }}>适宜</div>
</div>
</div>
<div style={{ background: '#F7F8FA', borderRadius: 10, padding: '12px', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'rgba(255, 125, 0, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Thermometer size={20} style={{ color: '#FF7D00' }} />
</div>
<div>
<div style={{ fontSize: 12, color: '#86909C' }}>温度</div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129' }}>{fix2((listDroneRealtime || listDrone)?.environment_temperature)}℃</div>
<div style={{ fontSize: 12, color: '#00B42A' }}>适宜</div>
</div>
</div>
<div style={{ background: '#F7F8FA', borderRadius: 10, padding: '12px', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'rgba(255, 125, 0, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Droplets size={20} style={{ color: '#FF7D00' }} />
</div>
<div>
<div style={{ fontSize: 12, color: '#86909C' }}>降水状态</div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129' }}>{getVal((listDroneRealtime || listDrone)?.rainfall)}</div>
<div style={{ fontSize: 12, color: (listDroneRealtime || listDrone)?.rainfall === 'no_rain' ? '#00B42A' : '#F53F3F' }}>
{(listDroneRealtime || listDrone)?.rainfall === 'no_rain' ? '低风险' : '有雨'}
</div>
</div>
</div>
<div style={{ background: '#F7F8FA', borderRadius: 10, padding: '12px', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'rgba(0, 180, 42, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Satellite size={20} style={{ color: '#00B42A' }} />
</div>
<div>
<div style={{ fontSize: 12, color: '#86909C' }}>GNSS质量</div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#1D2129' }}>
{(listDroneRealtime || listDrone)?.position_state?.rtk_number ?? 0} RTK
</div>
<div style={{ fontSize: 12, color: (listDroneRealtime || listDrone)?.position_state?.is_fixed === 'fixing_successful' ? '#00B42A' : '#FF7D00' }}>
{(listDroneRealtime || listDrone)?.position_state?.is_fixed === 'fixing_successful' ? '已固定' : '未固定'}
</div>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={{ flex: 1, background: 'rgba(255, 125, 0, 0.1)', borderRadius: 8, padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8 }}>
<AlertCircle size={16} style={{ color: '#FF7D00' }} />
<span style={{ fontSize: 13, color: '#FF7D00', fontWeight: 500 }}>16:30 后风速升高,建议提前完成任务</span>
</div>
<div style={{ flex: 1, background: 'rgba(22, 93, 255, 0.1)', borderRadius: 8, padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8 }}>
<InfoCircleOutlined size={16} style={{ color: '#165DFF' }} />
<span style={{ fontSize: 13, color: '#165DFF', fontWeight: 500 }}>预计 14:00-16:00 为最佳执行窗口</span>
</div>
</div>
</Card>
</Col>
</Row>
{/* 底部内容区 */}
<Row gutter={16}>
<Col xs={24} xl={4}>
<Card
title={
<div className="flex justify-between items-center w-full">
<span style={{ fontSize: fontTitle, fontWeight: 700 }}>航线列表</span>
{/* ✅ 航线列表独立无人机筛选 */}
<Select
size="small"
style={{ width: 120 }}
placeholder="选择无人机"
showSearch
allowClear
options={planedevices?.map(item => ({
value: item.device_sn,
label: item.drone_callsign || item.device_sn || item.callsign
}))}
value={waylineDrone?.device_sn}
onChange={(val) => {
const find = planedevices.find(i => i.device_sn === val);
setWaylineDrone(find || null);
}}
/>
</div>
}
bordered={false}
bodyStyle={{ padding: '12px', display: 'flex', flexDirection: 'column', height: 480 }}
style={{ ...cardStyle, height: '100%' }}
>
<Input
placeholder="搜索航线..."
prefix={<SearchOutlined style={{ color: '#86909C' }} />}
style={{ marginBottom: 12, borderRadius: 6 }}
size="small"
value={waylineSearch}
onChange={e => setWaylineSearch(e.target.value)}
/>
<div style={{ flex: 1, overflowY: 'auto', paddingRight: 4 }} className="custom-scrollbar">
{waylineList
.filter(item => item.name?.toLowerCase().includes(waylineSearch.toLowerCase()))
.map((item) => (
<div
key={item.id || item.uuid}
onClick={() => handleWaylineClick(item)}
style={{
padding: '10px 12px',
borderRadius: 8,
marginBottom: 8,
cursor: 'pointer',
background: selectedWayline?.id === item.id ? '#E8F3FF' : '#F7F8FA',
border: selectedWayline?.id === item.id ? '1px solid #165DFF' : '1px solid transparent',
transition: 'all 0.2s',
position: 'relative'
}}
>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1D2129', marginBottom: 4, paddingRight: 40 }}>
{item.name}
</div>
<div style={{ fontSize: 11, color: '#86909C', display: 'flex', alignItems: 'center', gap: 4 }}>
<Clock size={12} /> {item.update_time ? dayjs(item.update_time).format('MM-DD HH:mm') : '未知时间'}
</div>
{/* ✅ 执行按钮改为图标 */}
<Tooltip title="执行航线">
<Button
type="text"
icon={<PlayCircleOutlined style={{ color: '#165DFF', fontSize: 18 }} />}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
}}
onClick={(e) => handleExecuteClick(e, item)}
/>
</Tooltip>
</div>
))}
{waylineList.length === 0 && (
<div style={{ textAlign: 'center', padding: '20px 0', color: '#86909C', fontSize: 12 }}>
暂无航线数据
</div>
)}
</div>
</Card>
</Col>
<Col xs={24} xl={12}>
<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={8}>
<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>
</Card>
</Col>
</Row>
{/* 创建任务弹窗 */}
<Modal
open={createTaskModal}
title="创建航线任务"
onCancel={resetModals}
footer={null}
maskClosable={false}
width={650}
>
<div className="py-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">航线名称 <span className="text-red-500">*</span></label>
<Input
value={taskForm.name}
onChange={(e) => setTaskForm({ ...taskForm, name: e.target.value })}
placeholder="请输入"
/>
</div>
<div>
<label className="text-sm block mb-1">选择航线 <span className="text-red-500">*</span></label>
<Select
showSearch
value={waylineUuid}
onChange={setWaylineUuid}
options={waylineList}
style={{ width: '100%' }}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">机场SN <span className="text-red-500">*</span></label>
<Input value={taskForm.sn} disabled />
</div>
<div>
<label className="text-sm block mb-1">降落地场SN</label>
<Input
value={taskForm.landing_dock_sn}
onChange={(e) => setTaskForm({ ...taskForm, landing_dock_sn: e.target.value })}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">返航高度(m)</label>
<InputNumber
min={0}
value={taskForm.rth_altitude}
onChange={(v) => setTaskForm({ ...taskForm, rth_altitude: v })}
style={{ width: '100%' }}
/>
</div>
<div>
<label className="text-sm block mb-1">返航模式</label>
<Select
value={taskForm.rth_mode}
onChange={(v) => setTaskForm({ ...taskForm, rth_mode: v })}
options={[{ label: '智能', value: 'optimal' }, { label: '预设', value: 'preset' }]}
style={{ width: '100%' }}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">航线精度</label>
<Select
value={taskForm.wayline_precision_type}
onChange={(v) => setTaskForm({ ...taskForm, wayline_precision_type: v })}
options={[{ label: 'GPS', value: 'gps' }, { label: 'RTK', value: 'rtk' }]}
style={{ width: '100%' }}
/>
</div>
<div>
<label className="text-sm block mb-1">断点续飞</label>
<Select
value={taskForm.resumable_status}
onChange={(v) => setTaskForm({ ...taskForm, resumable_status: v })}
options={[{ label: '自动', value: 'auto' }, { label: '手动', value: 'manual' }]}
style={{ width: '100%' }}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">最低电量(%)</label>
<InputNumber
min={50}
max={100}
value={taskForm.min_battery_capacity}
onChange={(v) => setTaskForm({ ...taskForm, min_battery_capacity: v })}
style={{ width: '100%' }}
/>
</div>
<div>
<label className="text-sm block mb-1">任务类型</label>
<Select
value={taskForm.task_type}
onChange={(v) => setTaskForm({ ...taskForm, task_type: v })}
options={[
{ label: '立即', value: 'immediate' },
]}
style={{ width: '100%' }}
/>
</div>
</div>
{/* 时间选择 */}
{(taskForm.task_type === 'timed' || taskForm.task_type === 'recurring' || taskForm.task_type === 'continuous') && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">开始时间</label>
<DatePicker
showTime
value={taskForm.begin_at}
onChange={(v) => setTaskForm({ ...taskForm, begin_at: v })}
style={{ width: '100%' }}
/>
</div>
{(taskForm.task_type === 'recurring' || taskForm.task_type === 'continuous') && (
<div>
<label className="text-sm block mb-1">结束时间</label>
<DatePicker
showTime
value={taskForm.end_at}
onChange={(v) => setTaskForm({ ...taskForm, end_at: v })}
style={{ width: '100%' }}
/>
</div>
)}
</div>
)}
{/* 重复任务配置 repeat_option 对象绑定 */}
{taskForm.task_type === 'recurring' && (
<>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">重复模式</label>
<Select
value={taskForm.repeat_type}
onChange={(v) => setTaskForm({ ...taskForm, repeat_type: v })}
options={[
{ label: '不重复', value: 'nonrepeating' },
{ label: '每天', value: 'daily' },
{ label: '每周', value: 'weekly' },
{ label: '每月(日期)', value: 'absolute_monthly' },
{ label: '每月(星期)', value: 'relative_monthly' },
]}
style={{ width: '100%' }}
/>
</div>
<div>
<label className="text-sm block mb-1">重复间隔</label>
<InputNumber
min={1}
value={taskForm.repeat_option.interval}
onChange={(v) => setTaskForm({
...taskForm,
repeat_option: { ...taskForm.repeat_option, interval: v }
})}
style={{ width: '100%' }}
/>
</div>
</div>
{/* 每周 */}
{taskForm.repeat_type === 'weekly' && (
<div>
<label className="text-sm block mb-1">周几执行</label>
<Select
mode="multiple"
value={taskForm.repeat_option.days_of_week}
onChange={(v) => setTaskForm({
...taskForm,
repeat_option: { ...taskForm.repeat_option, days_of_week: v }
})}
options={[
{ label: '周日', value: 0 },
{ label: '周一', value: 1 },
{ label: '周二', value: 2 },
{ label: '周三', value: 3 },
{ label: '周四', value: 4 },
{ label: '周五', value: 5 },
{ label: '周六', value: 6 }
]}
style={{ width: '100%' }}
/>
</div>
)}
{/* 按月日期 */}
{taskForm.repeat_type === 'absolute_monthly' && (
<div>
<label className="text-sm block mb-1">每月几日</label>
<Select
mode="multiple"
value={taskForm.repeat_option.days_of_month}
onChange={(v) => setTaskForm({
...taskForm,
repeat_option: { ...taskForm.repeat_option, days_of_month: v }
})}
options={Array.from({ length: 31 }, (_, i) => ({ label: i + 1, value: i + 1 }))}
style={{ width: '100%' }}
/>
</div>
)}
{/* 按月星期 */}
{taskForm.repeat_type === 'relative_monthly' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm block mb-1">第几周</label>
<Select
value={taskForm.repeat_option.week_of_month}
onChange={(v) => setTaskForm({
...taskForm,
repeat_option: { ...taskForm.repeat_option, week_of_month: v }
})}
options={[1, 2, 3, 4].map(w => ({ label: `第${w}周`, value: w }))}
style={{ width: '100%' }}
/>
</div>
<div>
<label className="text-sm block mb-1">周几</label>
<Select
mode="multiple"
value={taskForm.repeat_option.days_of_week}
onChange={(v) => setTaskForm({
...taskForm,
repeat_option: { ...taskForm.repeat_option, days_of_week: v }
})}
options={[
{ label: '周日', value: 0 },
{ label: '周一', value: 1 },
{ label: '周二', value: 2 },
{ label: '周三', value: 3 },
{ label: '周四', value: 4 },
{ label: '周五', value: 5 },
{ label: '周六', value: 6 }
]}
style={{ width: '100%' }}
/>
</div>
</div>
)}
<div>
<label className="text-sm block mb-1">执行时间点</label>
<Select
mode="tags"
value={taskForm.recurring_task_start_time_list}
onChange={(v) => setTaskForm({
...taskForm,
recurring_task_start_time_list: v
})}
placeholder="输入时间戳"
style={{ width: '100%' }}
/>
</div>
</>
)}
{/* 按钮 */}
<div className="flex justify-end gap-2 mt-4">
<Button onClick={resetModals}>取消</Button>
<Button type="primary" loading={loading} onClick={createFlightTask}>确认创建</Button>
</div>
</div>
</Modal>
{/* 执行航线设置返航高度 Modal */}
<Modal
title="执行航线任务"
open={rthModalVisible}
onOk={confirmExecuteWayline}
onCancel={() => setRthModalVisible(false)}
confirmLoading={loading}
okText="立即执行"
cancelText="取消"
centered
width={400}
>
<div style={{ padding: '10px 0' }}>
<div style={{ marginBottom: 16 }}>
<Text strong>所选航线:</Text>
<Text>{selectedWayline?.name}</Text>
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>执行设备:</Text>
<Text>{waylineDrone?.drone_callsign || waylineDrone?.device_sn || '未选择设备'}</Text>
</div>
<div>
<div style={{ marginBottom: 8 }}><Text strong>返航高度 (m):</Text></div>
<InputNumber
min={10}
max={500}
value={tempRthAltitude}
onChange={val => setTempRthAltitude(val || 20)}
style={{ width: '100%' }}
/>
</div>
</div>
</Modal>
</div>
);
}