|
|
@ -16,11 +16,11 @@ |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="business-workspace"> |
|
|
<div class="business-workspace"> |
|
|
<div class="business-tabs"> |
|
|
|
|
|
<button :class="{ active: view === 'plans' }" @click="setView('plans')">任务计划<b>{{ plans.length }}</b></button> |
|
|
|
|
|
<button :class="{ active: view === 'records' }" @click="setView('records')">执行记录<b>{{ records.length }}</b></button> |
|
|
|
|
|
<button :class="{ active: view === 'routes' }" @click="setView('routes')">航线库<b>{{ routes.length }}</b></button> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<t-tabs v-model="view" class="business-tdesign-tabs" @change="setView"> |
|
|
|
|
|
<t-tab-panel value="plans" :label="plansTabLabel" :destroy-on-hide="false" /> |
|
|
|
|
|
<t-tab-panel value="records" :label="recordsTabLabel" :destroy-on-hide="false" /> |
|
|
|
|
|
<t-tab-panel value="routes" :label="routesTabLabel" :destroy-on-hide="false" /> |
|
|
|
|
|
</t-tabs> |
|
|
|
|
|
|
|
|
<!-- 任务计划 --> |
|
|
<!-- 任务计划 --> |
|
|
<div class="business-panel" :class="{ active: view === 'plans' }"> |
|
|
<div class="business-panel" :class="{ active: view === 'plans' }"> |
|
|
@ -36,33 +36,34 @@ |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="business-table-wrap"> |
|
|
<div class="business-table-wrap"> |
|
|
<table class="business-table task-plan-table"> |
|
|
|
|
|
<thead> |
|
|
|
|
|
<tr> |
|
|
|
|
|
<th>任务名称</th> |
|
|
|
|
|
<th>执行设备</th> |
|
|
|
|
|
<th>执行方式</th> |
|
|
|
|
|
<th>下次执行</th> |
|
|
|
|
|
<th>视频策略</th> |
|
|
|
|
|
<th>任务状态</th> |
|
|
|
|
|
<th>最近结果</th> |
|
|
|
|
|
<th>操作</th> |
|
|
|
|
|
</tr> |
|
|
|
|
|
</thead> |
|
|
|
|
|
<tbody> |
|
|
|
|
|
<tr v-for="t in filteredPlans" :key="t.id"> |
|
|
|
|
|
<td><div class="primary-table-cell"><span class="task-table-icon" :class="{ muted: t.statusClass === 'offline' }"><svg><use href="#i-route" /></svg></span><span><strong>{{ t.name }}</strong><small>{{ t.code }} · {{ t.route }}</small></span></div></td> |
|
|
|
|
|
<td>{{ t.device }}</td> |
|
|
|
|
|
<td>{{ t.cycle }}</td> |
|
|
|
|
|
<td :class="{ 'blue-text': t.statusClass === 'mission' }">{{ t.next }}</td> |
|
|
|
|
|
<td><span class="media-policy" :class="t.videoPolicyClass">{{ t.videoPolicy }}</span></td> |
|
|
|
|
|
<td><span class="table-status" :class="t.statusClass"><i></i>{{ t.status }}</span></td> |
|
|
|
|
|
<td>{{ t.lastResult }}</td> |
|
|
|
|
|
<td><div class="row-actions"><t-button v-if="canExecuteTask(t)" variant="text" theme="primary" @click="executeTask(t)">立即执行</t-button><t-button variant="text" theme="danger" @click="deleteTask(t)">删除</t-button></div></td> |
|
|
|
|
|
</tr> |
|
|
|
|
|
</tbody> |
|
|
|
|
|
</table> |
|
|
|
|
|
<div v-if="!filteredPlans.length" class="table-empty">暂无任务计划</div> |
|
|
|
|
|
|
|
|
<t-table |
|
|
|
|
|
row-key="id" |
|
|
|
|
|
:data="filteredPlans" |
|
|
|
|
|
:columns="planColumns" |
|
|
|
|
|
empty="暂无任务计划" |
|
|
|
|
|
> |
|
|
|
|
|
<template #name="{ row }"> |
|
|
|
|
|
<div class="primary-table-cell"> |
|
|
|
|
|
<span class="task-table-icon" :class="{ muted: row.statusClass === 'offline' }"><svg><use href="#i-route" /></svg></span> |
|
|
|
|
|
<span><strong>{{ row.name }}</strong><small>{{ row.code }} · {{ row.route }}</small></span> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
<template #next="{ row }"> |
|
|
|
|
|
<span :class="{ 'blue-text': row.statusClass === 'mission' }">{{ row.next }}</span> |
|
|
|
|
|
</template> |
|
|
|
|
|
<template #videoPolicy="{ row }"> |
|
|
|
|
|
<span class="media-policy" :class="row.videoPolicyClass">{{ row.videoPolicy }}</span> |
|
|
|
|
|
</template> |
|
|
|
|
|
<template #status="{ row }"> |
|
|
|
|
|
<span class="table-status" :class="row.statusClass"><i></i>{{ row.status }}</span> |
|
|
|
|
|
</template> |
|
|
|
|
|
<template #actions="{ row }"> |
|
|
|
|
|
<div class="row-actions"> |
|
|
|
|
|
<t-button v-if="canExecuteTask(row)" variant="text" theme="primary" @click="executeTask(row)">立即执行</t-button> |
|
|
|
|
|
<t-button variant="text" theme="danger" @click="deleteTask(row)">删除</t-button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
</t-table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
@ -80,36 +81,32 @@ |
|
|
</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="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"> |
|
|
|
|
|
<thead> |
|
|
|
|
|
<tr> |
|
|
|
|
|
<th>执行编号</th> |
|
|
|
|
|
<th>任务名称</th> |
|
|
|
|
|
<th>执行设备</th> |
|
|
|
|
|
<th>开始时间</th> |
|
|
|
|
|
<th>飞行时长</th> |
|
|
|
|
|
<th>采集成果</th> |
|
|
|
|
|
<th>执行结果</th> |
|
|
|
|
|
<th>操作</th> |
|
|
|
|
|
</tr> |
|
|
|
|
|
</thead> |
|
|
|
|
|
<tbody> |
|
|
|
|
|
<tr v-for="r in filteredRecords" :key="r.id"> |
|
|
|
|
|
<td>{{ r.id }}</td> |
|
|
|
|
|
<td><div class="primary-table-cell"><span class="task-table-icon"><svg><use href="#i-route" /></svg></span><span><strong>{{ r.name }}</strong><small>{{ r.type }}</small></span></div></td> |
|
|
|
|
|
<td>{{ r.device }}</td> |
|
|
|
|
|
<td>{{ r.start }}</td> |
|
|
|
|
|
<td>{{ r.duration }}</td> |
|
|
|
|
|
<td>{{ r.collect }}</td> |
|
|
|
|
|
<td><span class="table-status" :class="r.statusClass"><i></i>{{ r.result }}</span></td> |
|
|
|
|
|
<td><div class="row-actions"><t-button variant="text" theme="primary" :disabled="r.statusClass === 'mission'" @click="goReplay(r)">{{ r.statusClass === 'mission' ? '执行中' : '轨迹回放' }}</t-button><t-button v-if="r.rawVideoId" variant="text" theme="primary" :disabled="r.rawVideoStatus !== 'ready'" @click="downloadRawVideo(r)">{{ r.rawVideoStatus === 'ready' ? '下载原视频' : '视频处理中' }}</t-button></div></td> |
|
|
|
|
|
</tr> |
|
|
|
|
|
</tbody> |
|
|
|
|
|
</table> |
|
|
|
|
|
<div v-if="!filteredRecords.length" class="table-empty">暂无执行记录</div> |
|
|
|
|
|
|
|
|
<t-table |
|
|
|
|
|
row-key="id" |
|
|
|
|
|
:data="filteredRecords" |
|
|
|
|
|
:columns="recordColumns" |
|
|
|
|
|
empty="暂无执行记录" |
|
|
|
|
|
> |
|
|
|
|
|
<template #name="{ row }"> |
|
|
|
|
|
<div class="primary-table-cell"> |
|
|
|
|
|
<span class="task-table-icon"><svg><use href="#i-route" /></svg></span> |
|
|
|
|
|
<span><strong>{{ row.name }}</strong><small>{{ row.type }}</small></span> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
<template #result="{ row }"> |
|
|
|
|
|
<span class="table-status" :class="row.statusClass"><i></i>{{ row.result }}</span> |
|
|
|
|
|
</template> |
|
|
|
|
|
<template #actions="{ row }"> |
|
|
|
|
|
<div class="row-actions"> |
|
|
|
|
|
<t-button variant="text" theme="primary" :disabled="row.statusClass === 'mission'" @click="goReplay(row)">{{ row.statusClass === 'mission' ? '执行中' : '轨迹回放' }}</t-button> |
|
|
|
|
|
<t-button v-if="row.rawVideoId" variant="text" theme="primary" :disabled="row.rawVideoStatus !== 'ready'" @click="downloadRawVideo(row)">{{ row.rawVideoStatus === 'ready' ? '下载原视频' : '视频处理中' }}</t-button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
</t-table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 航线库 --> |
|
|
<!-- 航线库 --> |
|
|
<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"> |
|
|
@ -150,7 +147,7 @@ |
|
|
<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.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> |
|
|
<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><t-radio-group v-model="routeForm.turnMode" class="task-mode-radio" theme="button" variant="default-filled" :options="turnModeOptions" /></label> |
|
|
<t-button class="apply-all-waypoints" variant="outline" type="button" :disabled="!routePoints.length" @click="applyRouteDefaults"><svg><use href="#i-settings" /></svg>应用到全部航点</t-button> |
|
|
<t-button class="apply-all-waypoints" variant="outline" type="button" :disabled="!routePoints.length" @click="applyRouteDefaults"><svg><use href="#i-settings" /></svg>应用到全部航点</t-button> |
|
|
<div class="waypoint-heading"><strong>航点列表</strong><span>{{ routePoints.length }}</span></div> |
|
|
<div class="waypoint-heading"><strong>航点列表</strong><span>{{ routePoints.length }}</span></div> |
|
|
<div class="waypoint-list"> |
|
|
<div class="waypoint-list"> |
|
|
@ -187,12 +184,14 @@ |
|
|
</div> |
|
|
</div> |
|
|
<div class="task-schedule-settings"> |
|
|
<div class="task-schedule-settings"> |
|
|
<div class="task-video-title"><strong>执行方式</strong><span>按计划自动执行</span></div> |
|
|
<div class="task-video-title"><strong>执行方式</strong><span>按计划自动执行</span></div> |
|
|
<div class="mode-segment"> |
|
|
|
|
|
<button type="button" :class="{ active: taskForm.scheduleType === 'once' }" @click="setScheduleType('once')">手动执行</button> |
|
|
|
|
|
<button type="button" :class="{ active: taskForm.scheduleType === 'cron' && taskForm.schedulePeriod === 'day' }" @click="setScheduleType('day')">每天</button> |
|
|
|
|
|
<button type="button" :class="{ active: taskForm.scheduleType === 'cron' && taskForm.schedulePeriod === 'week' }" @click="setScheduleType('week')">每周</button> |
|
|
|
|
|
<button type="button" :class="{ active: taskForm.scheduleType === 'cron' && taskForm.schedulePeriod === 'custom' }" @click="setScheduleType('custom')">自定义 Cron</button> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<t-radio-group |
|
|
|
|
|
class="task-mode-radio task-schedule-radio" |
|
|
|
|
|
:value="scheduleMode" |
|
|
|
|
|
theme="button" |
|
|
|
|
|
variant="default-filled" |
|
|
|
|
|
:options="scheduleModeOptions" |
|
|
|
|
|
@change="setScheduleType" |
|
|
|
|
|
/> |
|
|
<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><t-select v-model="taskForm.scheduleWeekday" :options="weekdayOptions" style="width: 100%" /></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> |
|
|
<label><span>执行时间</span><t-time-picker v-model="taskForm.scheduleTime" format="HH:mm" allow-input style="width: 100%" /></label> |
|
|
@ -222,7 +221,7 @@ |
|
|
<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><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><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> |
|
|
<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><t-radio-group v-model="waypointForm.turnMode" class="task-mode-radio" theme="button" variant="default-filled" :options="turnModeOptions" /></label> |
|
|
<div class="route-form-error">{{ waypointFormError }}</div> |
|
|
<div class="route-form-error">{{ waypointFormError }}</div> |
|
|
</form> |
|
|
</form> |
|
|
<template #footer> |
|
|
<template #footer> |
|
|
@ -337,6 +336,50 @@ const weekdayOptions = [ |
|
|
{ value: 0, label: '星期日' } |
|
|
{ value: 0, label: '星期日' } |
|
|
] |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
const turnModeOptions = [ |
|
|
|
|
|
{ label: '自动转弯', value: 'auto' }, |
|
|
|
|
|
{ label: '定点转弯', value: 'stop' } |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
const scheduleModeOptions = [ |
|
|
|
|
|
{ label: '手动执行', value: 'once' }, |
|
|
|
|
|
{ label: '每天', value: 'day' }, |
|
|
|
|
|
{ label: '每周', value: 'week' }, |
|
|
|
|
|
{ label: '自定义 Cron', value: 'custom' } |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
const scheduleMode = computed(() => { |
|
|
|
|
|
if (taskForm.scheduleType === 'once') return 'once' |
|
|
|
|
|
return taskForm.schedulePeriod || 'day' |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const plansTabLabel = computed(() => `任务计划 ${plans.value.length}`) |
|
|
|
|
|
const recordsTabLabel = computed(() => `执行记录 ${records.value.length}`) |
|
|
|
|
|
const routesTabLabel = computed(() => `航线库 ${routes.value.length}`) |
|
|
|
|
|
|
|
|
|
|
|
const planColumns = [ |
|
|
|
|
|
{ colKey: 'name', title: '任务名称' }, |
|
|
|
|
|
{ colKey: 'device', title: '执行设备' }, |
|
|
|
|
|
{ colKey: 'cycle', title: '执行方式' }, |
|
|
|
|
|
{ colKey: 'next', title: '下次执行' }, |
|
|
|
|
|
{ colKey: 'videoPolicy', title: '视频策略' }, |
|
|
|
|
|
{ colKey: 'status', title: '任务状态' }, |
|
|
|
|
|
{ colKey: 'lastResult', title: '最近结果' }, |
|
|
|
|
|
{ colKey: 'actions', title: '操作', width: '16%' } |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
const recordColumns = [ |
|
|
|
|
|
{ colKey: 'id', title: '执行编号' }, |
|
|
|
|
|
{ colKey: 'name', title: '任务名称' }, |
|
|
|
|
|
{ colKey: 'device', title: '执行设备' }, |
|
|
|
|
|
{ colKey: 'start', title: '开始时间' }, |
|
|
|
|
|
{ colKey: 'duration', title: '飞行时长' }, |
|
|
|
|
|
{ colKey: 'collect', title: '采集成果' }, |
|
|
|
|
|
{ colKey: 'result', title: '执行结果' }, |
|
|
|
|
|
{ colKey: 'actions', title: '操作', width: '18%' } |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch( |
|
|
watch( |
|
|
() => route.query.view, |
|
|
() => route.query.view, |
|
|
(v) => { if (v) view.value = v } |
|
|
(v) => { if (v) view.value = v } |
|
|
@ -985,6 +1028,28 @@ async function downloadRawVideo(record) { |
|
|
.toolbar-group :deep(.t-select) { |
|
|
.toolbar-group :deep(.t-select) { |
|
|
flex-shrink: 0; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
} |
|
|
|
|
|
.business-tdesign-tabs :deep(.t-tabs__nav-container) { |
|
|
|
|
|
padding: 0 18px; |
|
|
|
|
|
} |
|
|
|
|
|
.business-tdesign-tabs :deep(.t-tabs__nav-wrap) { |
|
|
|
|
|
margin-left: 0; |
|
|
|
|
|
} |
|
|
|
|
|
.business-table-wrap :deep(.t-table) { |
|
|
|
|
|
background: transparent; |
|
|
|
|
|
} |
|
|
|
|
|
.task-mode-radio { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
} |
|
|
|
|
|
.task-mode-radio :deep(.t-radio-button) { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
justify-content: center; |
|
|
|
|
|
} |
|
|
|
|
|
.task-schedule-radio :deep(.t-radio-button) { |
|
|
|
|
|
min-width: 0; |
|
|
|
|
|
padding: 0 4px; |
|
|
|
|
|
white-space: nowrap; |
|
|
|
|
|
} |
|
|
.route-config-panel :deep(.t-input), |
|
|
.route-config-panel :deep(.t-input), |
|
|
.route-config-panel :deep(.t-input-number) { |
|
|
.route-config-panel :deep(.t-input-number) { |
|
|
width: 100%; |
|
|
width: 100%; |
|
|
|