UAV/src/pages/task.vue
18796357645 5a255f7068 ADD
2025-07-17 10:13:33 +08:00

722 lines
18 KiB
Vue

<template>
<div class="mission-management">
<div class="header">
<h2>任务管理</h2>
<div class="operation-buttons">
<el-button type="primary" @click="handleCreate">
<el-icon>
<Plus />
</el-icon>
创建任务
</el-button>
<el-button
type="danger"
:disabled="!selectedIds.length"
@click="handleBatchCancel"
>
<el-icon>
<Close />
</el-icon>
批量取消
</el-button>
</div>
</div>
<div class="filter-container">
<el-input
v-model="listQuery.keyword"
placeholder="搜索任务名称/描述"
style="width: 300px"
clearable
@keyup.enter="handleFilter"
>
<template #append>
<el-button :icon="Search" @click="handleFilter" />
</template>
</el-input>
<el-select
v-model="listQuery.status"
placeholder="状态筛选"
clearable
style="width: 120px; margin-left: 10px"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-date-picker
v-model="listQuery.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="margin-left: 10px; width: 280px"
/>
</div>
<el-table
v-loading="listLoading"
:data="missionList"
border
fit
highlight-current-row
style="width: 100%; margin-top: 20px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="任务名称" min-width="150" />
<el-table-column label="关联无人机" width="180">
<template #default="{ row }">
<el-tag v-for="drone in row.drones" :key="drone.id" style="margin-right: 5px">
{{ drone.name }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="type" label="任务类型" width="120">
<template #default="{ row }">
<el-tag :type="typeTagMap[row.type]">
{{ typeMap[row.type] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-tag :type="statusTagMap[row.status]">
{{ statusMap[row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="180" />
<el-table-column prop="endTime" label="结束时间" width="180" />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleDetail(row)">
<el-icon>
<View />
</el-icon>
详情
</el-button>
<el-button
size="small"
type="primary"
:disabled="row.status !== 'pending'"
@click="handleExecute(row)"
>
<el-icon>
<VideoPlay />
</el-icon>
执行
</el-button>
<el-button
size="small"
type="danger"
:disabled="!['pending', 'progress'].includes(row.status)"
@click="handleCancel(row)"
>
<el-icon>
<Close />
</el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="listQuery.page"
v-model:page-size="listQuery.limit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 任务创建/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'edit' ? '编辑任务' : '创建任务'"
width="700px"
>
<el-form
ref="missionForm"
:model="missionForm"
:rules="missionRules"
label-width="100px"
>
<el-form-item label="任务名称" prop="name">
<el-input v-model="missionForm.name" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="任务类型" prop="type">
<el-select v-model="missionForm.type" placeholder="请选择任务类型">
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关联无人机" prop="drones">
<el-select
v-model="missionForm.drones"
multiple
filterable
placeholder="请选择无人机"
style="width: 100%"
>
<el-option
v-for="drone in droneOptions"
:key="drone.id"
:label="drone.name"
:value="drone.id"
/>
</el-select>
</el-form-item>
<el-form-item label="任务时间" prop="timeRange">
<el-date-picker
v-model="missionForm.timeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input
v-model="missionForm.description"
type="textarea"
:rows="3"
placeholder="请输入任务描述"
/>
</el-form-item>
<el-form-item label="航线规划" prop="route">
<el-button type="primary" @click="showRouteMap = true">
<el-icon>
<MapLocation />
</el-icon>
规划航线
</el-button>
<div v-if="missionForm.route" class="route-preview">
<span>已设置 {{ missionForm.route.points.length }} 个航点</span>
<el-button type="text" @click="showRouteMap = true">查看</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmMission">确认</el-button>
</template>
</el-dialog>
<!-- 航线规划地图 -->
<el-dialog
v-model="showRouteMap"
title="航线规划"
width="80%"
top="5vh"
fullscreen
>
<div class="map-container">
<!-- 这里放置地图组件 -->
<div style="height: 80vh; background: #f5f5f5; display: flex; align-items: center; justify-content: center">
<h3>地图组件区域 (可集成高德/Google/百度地图)</h3>
</div>
</div>
<template #footer>
<el-button @click="showRouteMap = false">取消</el-button>
<el-button type="primary" @click="saveRoute">保存航线</el-button>
</template>
</el-dialog>
<!-- 任务详情抽屉 -->
<el-drawer
v-model="detailVisible"
title="任务详情"
direction="rtl"
size="50%"
>
<div v-if="currentMission" class="mission-detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="任务ID">{{ currentMission.id }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ currentMission.name }}</el-descriptions-item>
<el-descriptions-item label="任务类型">
<el-tag :type="typeTagMap[currentMission.type]">
{{ typeMap[currentMission.type] }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="任务状态">
<el-tag :type="statusTagMap[currentMission.status]">
{{ statusMap[currentMission.status] }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="关联无人机">
<div style="display: flex; flex-wrap: wrap; gap: 5px">
<el-tag
v-for="drone in currentMission.drones"
:key="drone.id"
type="info"
>
{{ drone.name }} ({{ drone.model }})
</el-tag>
</div>
</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ currentMission.startTime }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ currentMission.endTime }}</el-descriptions-item>
<el-descriptions-item label="任务描述">
<div style="white-space: pre-line">{{ currentMission.description }}</div>
</el-descriptions-item>
<el-descriptions-item label="航线规划">
<div
style="height: 300px; background: #f5f5f5; display: flex; align-items: center; justify-content: center">
<h3>航线地图预览</h3>
</div>
</el-descriptions-item>
</el-descriptions>
<div v-if="currentMission.status === 'progress'" class="real-time-data">
<h3 style="margin: 20px 0 10px">实时数据</h3>
<el-row :gutter="20">
<el-col :span="8">
<el-card shadow="hover">
<div slot="header" class="card-header">
<span>无人机状态</span>
</div>
<div class="card-content">
<div v-for="drone in currentMission.drones" :key="drone.id" class="drone-status">
<div class="status-item">
<span class="label">{{ drone.name }}:</span>
<el-tag :type="drone.status === 'online' ? 'success' : 'danger'" size="small">
{{ drone.status === 'online' ? '在线' : '离线' }}
</el-tag>
</div>
<div class="status-item">
<span class="label">电量:</span>
<el-progress
:percentage="drone.battery"
:color="batteryColor(drone.battery)"
:show-text="false"
:stroke-width="12"
style="width: 80px"
/>
<span class="value">{{ drone.battery }}%</span>
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div slot="header" class="card-header">
<span>任务进度</span>
</div>
<div class="card-content">
<el-progress
:percentage="missionProgress"
:stroke-width="16"
:text-inside="true"
/>
<div class="progress-detail">
<div>已完成航点: 12/24</div>
<div>预计剩余时间: 32分钟</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div slot="header" class="card-header">
<span>实时画面</span>
</div>
<div class="card-content" style="text-align: center">
<div
style="height: 160px; background: #000; display: flex; align-items: center; justify-content: center">
<span style="color: #fff">视频直播画面</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
Close,
Search,
View,
VideoPlay,
MapLocation
} from '@element-plus/icons-vue'
// 状态映射
const statusMap = {
pending: '待执行',
progress: '进行中',
completed: '已完成',
cancelled: '已取消',
failed: '失败'
}
const statusTagMap = {
pending: 'warning',
progress: 'primary',
completed: 'success',
cancelled: 'info',
failed: 'danger'
}
const typeMap = {
patrol: '巡检',
mapping: '测绘',
emergency: '应急',
transport: '运输'
}
const typeTagMap = {
patrol: '',
mapping: 'success',
emergency: 'danger',
transport: 'info'
}
const statusOptions = [
{ value: 'pending', label: '待执行' },
{ value: 'progress', label: '进行中' },
{ value: 'completed', label: '已完成' },
{ value: 'cancelled', label: '已取消' },
{ value: 'failed', label: '失败' }
]
const typeOptions = [
{ value: 'patrol', label: '巡检' },
{ value: 'mapping', label: '测绘' },
{ value: 'emergency', label: '应急' },
{ value: 'transport', label: '运输' }
]
// 电池颜色
const batteryColor = (percentage) => {
if (percentage > 70) return '#67C23A'
if (percentage > 30) return '#E6A23C'
return '#F56C6C'
}
// 列表相关
const listLoading = ref(false)
const missionList = ref([])
const total = ref(0)
const selectedIds = ref([])
const listQuery = reactive({
page: 1,
limit: 10,
keyword: '',
status: '',
dateRange: []
})
// 无人机选项(模拟数据)
const droneOptions = ref([
{ id: 1, name: '巡检无人机1号', model: 'DJI Mavic 3' },
{ id: 2, name: '测绘无人机A', model: 'DJI Phantom 4 RTK' },
{ id: 3, name: '应急无人机', model: 'DJI Matrice 300' }
])
// 表单相关
const dialogVisible = ref(false)
const detailVisible = ref(false)
const showRouteMap = ref(false)
const dialogType = ref('add')
const currentMission = ref(null)
const missionProgress = ref(45) // 模拟进度
const missionForm = reactive({
id: '',
name: '',
type: 'patrol',
drones: [],
timeRange: [],
description: '',
route: null
})
const missionRules = reactive({
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
drones: [{ required: true, message: '请选择至少一架无人机', trigger: 'change' }],
timeRange: [{ required: true, message: '请选择任务时间', trigger: 'change' }]
})
// 获取任务列表
const fetchMissions = async () => {
listLoading.value = true
try {
// 这里替换为实际API调用
// const res = await getMissionList(listQuery)
// 模拟数据
setTimeout(() => {
missionList.value = [
{
id: 1,
name: '园区安全巡检',
type: 'patrol',
status: 'pending',
drones: [
{ id: 1, name: '巡检无人机1号', model: 'DJI Mavic 3', status: 'online', battery: 85 },
{ id: 3, name: '应急无人机', model: 'DJI Matrice 300', status: 'online', battery: 90 }
],
startTime: '2023-05-20 09:00:00',
endTime: '2023-05-20 11:00:00',
description: '每日例行园区安全巡检,重点检查围墙和屋顶区域'
},
{
id: 2,
name: '建筑工地测绘',
type: 'mapping',
status: 'progress',
drones: [
{ id: 2, name: '测绘无人机A', model: 'DJI Phantom 4 RTK', status: 'online', battery: 65 }
],
startTime: '2023-05-19 14:00:00',
endTime: '2023-05-19 16:30:00',
description: '新建筑工地三维建模测绘'
},
{
id: 3,
name: '紧急物资运输',
type: 'emergency',
status: 'completed',
drones: [
{ id: 3, name: '应急无人机', model: 'DJI Matrice 300', status: 'offline', battery: 25 }
],
startTime: '2023-05-18 10:15:00',
endTime: '2023-05-18 11:30:00',
description: '向山区运送紧急医疗物资'
}
].slice(
(listQuery.page - 1) * listQuery.limit,
listQuery.page * listQuery.limit
)
total.value = 3
listLoading.value = false
}, 500)
} catch (error) {
console.error(error)
listLoading.value = false
}
}
// 表格选择
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map((item) => item.id)
}
// 筛选
const handleFilter = () => {
listQuery.page = 1
fetchMissions()
}
// 分页
const handleSizeChange = (val) => {
listQuery.limit = val
fetchMissions()
}
const handleCurrentChange = (val) => {
listQuery.page = val
fetchMissions()
}
// 创建任务
const handleCreate = () => {
dialogType.value = 'add'
Object.assign(missionForm, {
id: '',
name: '',
type: 'patrol',
drones: [],
timeRange: [],
description: '',
route: null
})
dialogVisible.value = true
}
// 查看详情
const handleDetail = (row) => {
currentMission.value = row
detailVisible.value = true
}
// 执行任务
const handleExecute = (row) => {
ElMessageBox.confirm(`确认开始执行任务 "${row.name}"?`, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消'
}).then(() => {
// 这里替换为实际API调用
// await executeMission(row.id)
ElMessage.success('任务已开始执行')
fetchMissions()
}).catch(() => {
})
}
// 取消任务
const handleCancel = (row) => {
ElMessageBox.confirm(
`确认${row.status === 'progress' ? '终止' : '取消'}任务 "${row.name}"?`,
'警告',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 这里替换为实际API调用
// await cancelMission(row.id)
ElMessage.success(`任务已${row.status === 'progress' ? '终止' : '取消'}`)
fetchMissions()
}).catch(() => {
})
}
// 批量取消
const handleBatchCancel = () => {
ElMessageBox.confirm(
`确认取消选中的 ${selectedIds.value.length} 个任务?`,
'警告',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 这里替换为实际API调用
// await batchCancelMissions(selectedIds.value)
ElMessage.success('取消成功')
selectedIds.value = []
fetchMissions()
}).catch(() => {
})
}
// 保存航线
const saveRoute = () => {
// 这里应该是从地图组件获取航线数据
missionForm.route = {
points: [
{ lat: 39.9042, lng: 116.4074 },
{ lat: 39.9082, lng: 116.4074 },
{ lat: 39.9082, lng: 116.4174 },
{ lat: 39.9042, lng: 116.4174 }
],
distance: '2.5km',
estimatedTime: '15分钟'
}
showRouteMap.value = false
ElMessage.success('航线已保存')
}
// 确认表单
const confirmMission = () => {
// 这里替换为实际表单验证和API调用
ElMessage.success(
dialogType.value === 'add' ? '创建成功' : '更新成功'
)
dialogVisible.value = false
fetchMissions()
}
onMounted(() => {
fetchMissions()
})
</script>
<style scoped lang="scss">
.mission-management {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.filter-container {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.route-preview {
margin-top: 10px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
}
.mission-detail {
padding: 20px;
.drone-status {
margin-bottom: 10px;
.status-item {
display: flex;
align-items: center;
margin-bottom: 5px;
.label {
width: 80px;
color: #909399;
}
.value {
margin-left: 10px;
}
}
}
.progress-detail {
margin-top: 10px;
color: #606266;
font-size: 13px;
div {
margin-bottom: 5px;
}
}
}
.card-header {
font-weight: bold;
}
.card-content {
padding: 10px;
}
</style>