|
|
@ -25,14 +25,13 @@ |
|
|
<!-- 任务计划 --> |
|
|
<!-- 任务计划 --> |
|
|
<div class="business-panel" :class="{ active: view === 'plans' }"> |
|
|
<div class="business-panel" :class="{ active: view === 'plans' }"> |
|
|
<div class="business-toolbar"> |
|
|
<div class="business-toolbar"> |
|
|
<label class="business-search"><svg><use href="#i-search" /></svg><input v-model="search" type="text" placeholder="搜索任务名称、编号或设备" /></label> |
|
|
|
|
|
<div> |
|
|
|
|
|
<select v-model="planFilter"> |
|
|
|
|
|
<option value="">全部状态</option> |
|
|
|
|
|
<option value="mission">执行中</option> |
|
|
|
|
|
<option value="pending">待执行</option> |
|
|
|
|
|
<option value="offline">已暂停</option> |
|
|
|
|
|
</select> |
|
|
|
|
|
|
|
|
<div class="business-search"> |
|
|
|
|
|
<t-input v-model="search" clearable placeholder="搜索任务名称、编号或设备"> |
|
|
|
|
|
<template #prefix-icon><t-icon name="search" /></template> |
|
|
|
|
|
</t-input> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="toolbar-group"> |
|
|
|
|
|
<t-select v-model="planFilter" :options="planFilterOptions" placeholder="全部状态" style="width: 140px" /> |
|
|
<button class="plain-command" @click="resetPlanFilter">重置</button> |
|
|
<button class="plain-command" @click="resetPlanFilter">重置</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
@ -70,12 +69,16 @@ |
|
|
<!-- 执行记录 --> |
|
|
<!-- 执行记录 --> |
|
|
<div class="business-panel" :class="{ active: view === 'records' }"> |
|
|
<div class="business-panel" :class="{ active: view === 'records' }"> |
|
|
<div class="business-toolbar"> |
|
|
<div class="business-toolbar"> |
|
|
<div> |
|
|
|
|
|
<select v-model="execFilter"><option value="">全部设备</option><option v-for="d in devices" :key="d.id" :value="d.dockId || d.droneSn">{{ d.name }}</option></select> |
|
|
|
|
|
<div class="business-search"><svg><use href="#i-search" /></svg><input v-model="execSearch" type="text" placeholder="搜索执行记录" /></div> |
|
|
|
|
|
|
|
|
<div class="toolbar-group"> |
|
|
|
|
|
<t-select v-model="execFilter" :options="execFilterOptions" placeholder="全部设备" style="width: 160px" /> |
|
|
|
|
|
<div class="business-search"> |
|
|
|
|
|
<t-input v-model="execSearch" clearable placeholder="搜索执行记录"> |
|
|
|
|
|
<template #prefix-icon><t-icon name="search" /></template> |
|
|
|
|
|
</t-input> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="record-header"><div><h2>执行记录</h2><span>最近 30 天执行记录</span></div><select v-model="execResultFilter"><option value="">全部执行结果</option><option value="succeeded">成功</option><option value="failed">失败</option><option value="cancelled">已取消</option></select></div> |
|
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="record-header"><div><h2>执行记录</h2><span>最近 30 天执行记录</span></div><t-select v-model="execResultFilter" :options="execResultFilterOptions" placeholder="全部执行结果" style="width: 160px" /></div> |
|
|
<div class="business-table-wrap"> |
|
|
<div class="business-table-wrap"> |
|
|
<table class="business-table execution-record-table"> |
|
|
<table class="business-table execution-record-table"> |
|
|
<thead> |
|
|
<thead> |
|
|
@ -111,7 +114,11 @@ |
|
|
<div class="business-panel route-library" :class="{ active: view === 'routes' }"> |
|
|
<div class="business-panel route-library" :class="{ active: view === 'routes' }"> |
|
|
<div class="route-library-view" :hidden="routeEditorOpen"> |
|
|
<div class="route-library-view" :hidden="routeEditorOpen"> |
|
|
<div class="business-toolbar"> |
|
|
<div class="business-toolbar"> |
|
|
<label class="business-search"><svg><use href="#i-search" /></svg><input v-model="routeSearch" type="text" placeholder="搜索航线名称或区域" /></label> |
|
|
|
|
|
|
|
|
<div class="business-search"> |
|
|
|
|
|
<t-input v-model="routeSearch" clearable placeholder="搜索航线名称或区域"> |
|
|
|
|
|
<template #prefix-icon><t-icon name="search" /></template> |
|
|
|
|
|
</t-input> |
|
|
|
|
|
</div> |
|
|
<button class="plain-command" @click="openRouteEditor"><svg><use href="#i-plus" /></svg>新建航线</button> |
|
|
<button class="plain-command" @click="openRouteEditor"><svg><use href="#i-plus" /></svg>新建航线</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="route-grid"> |
|
|
<div class="route-grid"> |
|
|
@ -137,11 +144,11 @@ |
|
|
</header> |
|
|
</header> |
|
|
<div class="route-editor-body"> |
|
|
<div class="route-editor-body"> |
|
|
<aside class="route-config-panel"> |
|
|
<aside class="route-config-panel"> |
|
|
<label><span>航线名称 <b>*</b></span><input v-model="routeForm.name" maxlength="128" placeholder="请输入航线名称" /></label> |
|
|
|
|
|
|
|
|
<label><span>航线名称 <b>*</b></span><t-input v-model="routeForm.name" :maxlength="128" placeholder="请输入航线名称" /></label> |
|
|
<div class="route-default-title"><strong>新增航点默认参数</strong></div> |
|
|
<div class="route-default-title"><strong>新增航点默认参数</strong></div> |
|
|
<div class="route-default-row"> |
|
|
<div class="route-default-row"> |
|
|
<label><span>高度</span><div class="number-input"><input v-model.number="routeForm.altitude" type="number" min="20" max="120" /><em>m</em></div></label> |
|
|
|
|
|
<label><span>速度</span><div class="number-input"><input v-model.number="routeForm.speed" type="number" min="1" max="20" step="0.1" /><em>m/s</em></div></label> |
|
|
|
|
|
|
|
|
<label><span>高度</span><div class="number-input"><t-input-number v-model="routeForm.altitude" theme="normal" :min="20" :max="120" style="width: 100%" /><em>m</em></div></label> |
|
|
|
|
|
<label><span>速度</span><div class="number-input"><t-input-number v-model="routeForm.speed" theme="normal" :min="1" :max="20" :step="0.1" style="width: 100%" /><em>m/s</em></div></label> |
|
|
</div> |
|
|
</div> |
|
|
<label><span>转弯方式</span><div class="mode-segment route-turn-segment"><button type="button" :class="{ active: routeForm.turnMode === 'auto' }" @click="routeForm.turnMode = 'auto'">自动转弯</button><button type="button" :class="{ active: routeForm.turnMode === 'stop' }" @click="routeForm.turnMode = 'stop'">定点转弯</button></div></label> |
|
|
<label><span>转弯方式</span><div class="mode-segment route-turn-segment"><button type="button" :class="{ active: routeForm.turnMode === 'auto' }" @click="routeForm.turnMode = 'auto'">自动转弯</button><button type="button" :class="{ active: routeForm.turnMode === 'stop' }" @click="routeForm.turnMode = 'stop'">定点转弯</button></div></label> |
|
|
<button class="apply-all-waypoints" type="button" :disabled="!routePoints.length" @click="applyRouteDefaults"><svg><use href="#i-settings" /></svg>应用到全部航点</button> |
|
|
<button class="apply-all-waypoints" type="button" :disabled="!routePoints.length" @click="applyRouteDefaults"><svg><use href="#i-settings" /></svg>应用到全部航点</button> |
|
|
@ -168,19 +175,13 @@ |
|
|
<button class="icon-button" type="button" @click="createTaskOpen = false"><svg><use href="#i-x" /></svg></button> |
|
|
<button class="icon-button" type="button" @click="createTaskOpen = false"><svg><use href="#i-x" /></svg></button> |
|
|
</header> |
|
|
</header> |
|
|
<form novalidate @submit.prevent="submitCreateTask"> |
|
|
<form novalidate @submit.prevent="submitCreateTask"> |
|
|
<label><span>任务名称<b>*</b></span><input v-model="taskForm.name" type="text" maxlength="40" placeholder="例如:园区西侧道路巡检" /></label> |
|
|
|
|
|
|
|
|
<label><span>任务名称<b>*</b></span><t-input v-model="taskForm.name" :maxlength="40" placeholder="例如:园区西侧道路巡检" /></label> |
|
|
<div class="form-row"> |
|
|
<div class="form-row"> |
|
|
<label><span>执行机巢<b>*</b></span> |
|
|
<label><span>执行机巢<b>*</b></span> |
|
|
<select v-model="taskForm.dockId"> |
|
|
|
|
|
<option value="">请选择机巢</option> |
|
|
|
|
|
<option v-for="dock in deviceStore.docks" :key="dock.id" :value="dock.dockId">{{ dock.name }} · {{ dock.dockId }}</option> |
|
|
|
|
|
</select> |
|
|
|
|
|
|
|
|
<t-select v-model="taskForm.dockId" :options="dockOptions" placeholder="请选择机巢" style="width: 100%" /> |
|
|
</label> |
|
|
</label> |
|
|
<label><span>飞行航线<b>*</b></span> |
|
|
<label><span>飞行航线<b>*</b></span> |
|
|
<select v-model="taskForm.routeId"> |
|
|
|
|
|
<option value="">请选择航线</option> |
|
|
|
|
|
<option v-for="taskRoute in routes" :key="taskRoute.id" :value="taskRoute.id">{{ taskRoute.name }}</option> |
|
|
|
|
|
</select> |
|
|
|
|
|
|
|
|
<t-select v-model="taskForm.routeId" :options="routeOptions" placeholder="请选择航线" style="width: 100%" /> |
|
|
</label> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
<div class="task-schedule-settings"> |
|
|
<div class="task-schedule-settings"> |
|
|
@ -192,14 +193,14 @@ |
|
|
<button type="button" :class="{ active: taskForm.scheduleType === 'cron' && taskForm.schedulePeriod === 'custom' }" @click="setScheduleType('custom')">自定义 Cron</button> |
|
|
<button type="button" :class="{ active: taskForm.scheduleType === 'cron' && taskForm.schedulePeriod === 'custom' }" @click="setScheduleType('custom')">自定义 Cron</button> |
|
|
</div> |
|
|
</div> |
|
|
<div v-if="taskForm.scheduleType === 'cron' && taskForm.schedulePeriod !== 'custom'" class="form-row"> |
|
|
<div v-if="taskForm.scheduleType === 'cron' && taskForm.schedulePeriod !== 'custom'" class="form-row"> |
|
|
<label v-if="taskForm.schedulePeriod === 'week'"><span>星期</span><select v-model.number="taskForm.scheduleWeekday"><option v-for="day in weekdays" :key="day.value" :value="day.value">{{ day.label }}</option></select></label> |
|
|
|
|
|
<label><span>执行时间</span><input v-model="taskForm.scheduleTime" type="time" /></label> |
|
|
|
|
|
|
|
|
<label v-if="taskForm.schedulePeriod === 'week'"><span>星期</span><t-select v-model="taskForm.scheduleWeekday" :options="weekdayOptions" style="width: 100%" /></label> |
|
|
|
|
|
<label><span>执行时间</span><t-time-picker v-model="taskForm.scheduleTime" format="HH:mm" allow-input style="width: 100%" /></label> |
|
|
</div> |
|
|
</div> |
|
|
<label v-if="taskForm.scheduleType === 'cron' && taskForm.schedulePeriod === 'custom'"><span>Cron 表达式<b>*</b></span><input v-model.trim="taskForm.scheduleCron" type="text" maxlength="64" placeholder="例如:0 8 * * 1-5" /><small>标准五段式:分 时 日 月 周,不支持秒级。</small></label> |
|
|
|
|
|
|
|
|
<label v-if="taskForm.scheduleType === 'cron' && taskForm.schedulePeriod === 'custom'"><span>Cron 表达式<b>*</b></span><t-input v-model.trim="taskForm.scheduleCron" :maxlength="64" placeholder="例如:0 8 * * 1-5" /><small>标准五段式:分 时 日 月 周,不支持秒级。</small></label> |
|
|
</div> |
|
|
</div> |
|
|
<div class="task-video-settings"> |
|
|
<div class="task-video-settings"> |
|
|
<div class="task-video-title"><strong>视频策略</strong><span>按任务配置</span></div> |
|
|
<div class="task-video-title"><strong>视频策略</strong><span>按任务配置</span></div> |
|
|
<label class="task-video-option"><span><strong>原始视频录制</strong><small>任务结束后自动上传原始文件</small></span><input v-model="taskForm.rawRecordingEnabled" type="checkbox" /><i></i></label> |
|
|
|
|
|
|
|
|
<label class="task-video-option"><span><strong>原始视频录制</strong><small>任务结束后自动上传原始文件</small></span><t-switch v-model="taskForm.rawRecordingEnabled" /></label> |
|
|
</div> |
|
|
</div> |
|
|
<div class="form-error">{{ taskFormError }}</div> |
|
|
<div class="form-error">{{ taskFormError }}</div> |
|
|
<footer> |
|
|
<footer> |
|
|
@ -215,8 +216,8 @@ |
|
|
<header><div><span>航线编辑</span><h3>编辑航点 {{ editingWaypointIndex + 1 }}</h3></div><button class="icon-button" type="button" @click="closeWaypointEditor"><svg><use href="#i-x" /></svg></button></header> |
|
|
<header><div><span>航线编辑</span><h3>编辑航点 {{ editingWaypointIndex + 1 }}</h3></div><button class="icon-button" type="button" @click="closeWaypointEditor"><svg><use href="#i-x" /></svg></button></header> |
|
|
<form novalidate @submit.prevent="saveWaypoint"> |
|
|
<form novalidate @submit.prevent="saveWaypoint"> |
|
|
<div class="waypoint-id-row"><span>航点 ID</span><strong>{{ editingWaypointIndex + 1 }}</strong></div> |
|
|
<div class="waypoint-id-row"><span>航点 ID</span><strong>{{ editingWaypointIndex + 1 }}</strong></div> |
|
|
<div class="form-row"><label><span>经度 <b>*</b></span><input v-model.number="waypointForm.longitude" type="number" step="any" /></label><label><span>纬度 <b>*</b></span><input v-model.number="waypointForm.latitude" type="number" step="any" /></label></div> |
|
|
|
|
|
<div class="form-row"><label><span>高度 <b>*</b></span><div class="number-input"><input v-model.number="waypointForm.altitude" type="number" min="20" max="120" /><em>m</em></div></label><label><span>速度 <b>*</b></span><div class="number-input"><input v-model.number="waypointForm.speed" type="number" min="1" max="20" step="0.1" /><em>m/s</em></div></label></div> |
|
|
|
|
|
|
|
|
<div class="form-row"><label><span>经度 <b>*</b></span><t-input-number v-model="waypointForm.longitude" theme="normal" :min="-180" :max="180" :step="0.000001" style="width: 100%" /></label><label><span>纬度 <b>*</b></span><t-input-number v-model="waypointForm.latitude" theme="normal" :min="-90" :max="90" :step="0.000001" style="width: 100%" /></label></div> |
|
|
|
|
|
<div class="form-row"><label><span>高度 <b>*</b></span><div class="number-input"><t-input-number v-model="waypointForm.altitude" theme="normal" :min="20" :max="120" style="width: 100%" /><em>m</em></div></label><label><span>速度 <b>*</b></span><div class="number-input"><t-input-number v-model="waypointForm.speed" theme="normal" :min="1" :max="20" :step="0.1" style="width: 100%" /><em>m/s</em></div></label></div> |
|
|
<label><span>转弯方式</span><div class="mode-segment waypoint-turn-segment"><button type="button" :class="{ active: waypointForm.turnMode === 'auto' }" @click="waypointForm.turnMode = 'auto'">自动转弯</button><button type="button" :class="{ active: waypointForm.turnMode === 'stop' }" @click="waypointForm.turnMode = 'stop'">定点转弯</button></div></label> |
|
|
<label><span>转弯方式</span><div class="mode-segment waypoint-turn-segment"><button type="button" :class="{ active: waypointForm.turnMode === 'auto' }" @click="waypointForm.turnMode = 'auto'">自动转弯</button><button type="button" :class="{ active: waypointForm.turnMode === 'stop' }" @click="waypointForm.turnMode = 'stop'">定点转弯</button></div></label> |
|
|
<div class="route-form-error">{{ waypointFormError }}</div> |
|
|
<div class="route-form-error">{{ waypointFormError }}</div> |
|
|
<footer><button class="plain-command" type="button" @click="closeWaypointEditor">取消</button><button class="primary-command" type="submit">保存航点</button></footer> |
|
|
<footer><button class="plain-command" type="button" @click="closeWaypointEditor">取消</button><button class="primary-command" type="submit">保存航点</button></footer> |
|
|
@ -250,9 +251,21 @@ const deviceStore = useDeviceStore() |
|
|
const view = ref(route.query.view || 'plans') |
|
|
const view = ref(route.query.view || 'plans') |
|
|
const search = ref('') |
|
|
const search = ref('') |
|
|
const planFilter = ref('') |
|
|
const planFilter = ref('') |
|
|
|
|
|
const planFilterOptions = [ |
|
|
|
|
|
{ label: '全部状态', value: '' }, |
|
|
|
|
|
{ label: '执行中', value: 'mission' }, |
|
|
|
|
|
{ label: '待执行', value: 'pending' }, |
|
|
|
|
|
{ label: '已暂停', value: 'offline' } |
|
|
|
|
|
] |
|
|
const execSearch = ref('') |
|
|
const execSearch = ref('') |
|
|
const execFilter = ref('') |
|
|
const execFilter = ref('') |
|
|
const execResultFilter = ref('') |
|
|
const execResultFilter = ref('') |
|
|
|
|
|
const execResultFilterOptions = [ |
|
|
|
|
|
{ label: '全部执行结果', value: '' }, |
|
|
|
|
|
{ label: '成功', value: 'succeeded' }, |
|
|
|
|
|
{ label: '失败', value: 'failed' }, |
|
|
|
|
|
{ label: '已取消', value: 'cancelled' } |
|
|
|
|
|
] |
|
|
const routeSearch = ref('') |
|
|
const routeSearch = ref('') |
|
|
const routeEditorOpen = ref(false) |
|
|
const routeEditorOpen = ref(false) |
|
|
const routeSaving = ref(false) |
|
|
const routeSaving = ref(false) |
|
|
@ -276,6 +289,22 @@ const records = ref([]) |
|
|
let refreshTimer = null |
|
|
let refreshTimer = null |
|
|
const routes = ref([]) |
|
|
const routes = ref([]) |
|
|
const devices = computed(() => [...deviceStore.drones, ...deviceStore.docks]) |
|
|
const devices = computed(() => [...deviceStore.drones, ...deviceStore.docks]) |
|
|
|
|
|
const execFilterOptions = computed(() => [ |
|
|
|
|
|
{ label: '全部设备', value: '' }, |
|
|
|
|
|
...devices.value.map((d) => ({ label: d.name, value: d.dockId || d.droneSn })) |
|
|
|
|
|
]) |
|
|
|
|
|
const dockOptions = computed(() => |
|
|
|
|
|
deviceStore.docks.map((dock) => ({ |
|
|
|
|
|
label: `${dock.name} · ${dock.dockId}`, |
|
|
|
|
|
value: dock.dockId |
|
|
|
|
|
})) |
|
|
|
|
|
) |
|
|
|
|
|
const routeOptions = computed(() => |
|
|
|
|
|
routes.value.map((taskRoute) => ({ |
|
|
|
|
|
label: taskRoute.name, |
|
|
|
|
|
value: taskRoute.id |
|
|
|
|
|
})) |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
const createTaskOpen = ref(false) |
|
|
const createTaskOpen = ref(false) |
|
|
const creatingTask = ref(false) |
|
|
const creatingTask = ref(false) |
|
|
@ -292,7 +321,7 @@ const taskForm = reactive({ |
|
|
rawRecordingEnabled: false |
|
|
rawRecordingEnabled: false |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
const weekdays = [ |
|
|
|
|
|
|
|
|
const weekdayOptions = [ |
|
|
{ value: 1, label: '星期一' }, |
|
|
{ value: 1, label: '星期一' }, |
|
|
{ value: 2, label: '星期二' }, |
|
|
{ value: 2, label: '星期二' }, |
|
|
{ value: 3, label: '星期三' }, |
|
|
{ value: 3, label: '星期三' }, |
|
|
@ -930,3 +959,40 @@ async function downloadRawVideo(record) { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
<style scoped> |
|
|
|
|
|
.business-search :deep(.t-input) { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
} |
|
|
|
|
|
.business-search { |
|
|
|
|
|
border: 0; |
|
|
|
|
|
padding: 0; |
|
|
|
|
|
height: auto; |
|
|
|
|
|
background: transparent; |
|
|
|
|
|
} |
|
|
|
|
|
.toolbar-group { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
.toolbar-group :deep(.t-select) { |
|
|
|
|
|
flex-shrink: 0; |
|
|
|
|
|
} |
|
|
|
|
|
.add-dock-dialog :deep(.t-input), |
|
|
|
|
|
.add-dock-dialog :deep(.t-input-number), |
|
|
|
|
|
.add-dock-dialog :deep(.t-select), |
|
|
|
|
|
.add-dock-dialog :deep(.t-time-picker), |
|
|
|
|
|
.route-config-panel :deep(.t-input), |
|
|
|
|
|
.route-config-panel :deep(.t-input-number) { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
} |
|
|
|
|
|
.number-input :deep(.t-input-number) { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
} |
|
|
|
|
|
.number-input :deep(.t-input) { |
|
|
|
|
|
padding-right: 34px; |
|
|
|
|
|
} |
|
|
|
|
|
.task-video-option :deep(.t-switch) { |
|
|
|
|
|
justify-self: end; |
|
|
|
|
|
} |
|
|
|
|
|
</style> |
|
|
|