722 lines
18 KiB
Vue
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>
|