|
|
|
@ -127,6 +127,7 @@ |
|
|
|
:distance="r.distance" |
|
|
|
:collected-at="r.collectedAt" |
|
|
|
:map-class="r.mapClass" |
|
|
|
@edit="openRouteEditorForEdit(r)" |
|
|
|
@delete="deleteRoute(r)" |
|
|
|
/> |
|
|
|
<div v-if="!filteredRoutes.length" class="table-empty" style="grid-column:1/-1">暂无航线</div> |
|
|
|
@ -136,8 +137,8 @@ |
|
|
|
<div class="route-editor" :hidden="!routeEditorOpen"> |
|
|
|
<header class="route-editor-header"> |
|
|
|
<t-button class="back-command" variant="text" type="button" @click="closeRouteEditor"><svg><use href="#i-chevron" /></svg>返回航线库</t-button> |
|
|
|
<div><strong>新建航线</strong><span>{{ routeSummary }}</span></div> |
|
|
|
<div><t-button variant="outline" type="button" :disabled="!routePoints.length" @click="undoRoutePoint"><svg><use href="#i-refresh" /></svg>撤销</t-button><t-button variant="outline" type="button" :disabled="!routePoints.length" @click="clearRoutePoints"><svg><use href="#i-x" /></svg>清空</t-button><t-button theme="primary" type="button" :disabled="routeSaving" @click="submitRoute">{{ routeSaving ? '保存中…' : '保存航线' }}</t-button></div> |
|
|
|
<div><strong>{{ editingRouteId ? '编辑航线' : '新建航线' }}</strong><span>{{ routeSummary }}</span></div> |
|
|
|
<div><t-button variant="outline" type="button" :disabled="!routePoints.length" @click="undoRoutePoint"><svg><use href="#i-refresh" /></svg>撤销</t-button><t-button variant="outline" type="button" :disabled="!routePoints.length" @click="clearRoutePoints"><svg><use href="#i-x" /></svg>清空</t-button><t-button theme="primary" type="button" :disabled="routeSaving" @click="submitRoute">{{ routeSaving ? '保存中…' : (editingRouteId ? '保存修改' : '保存航线') }}</t-button></div> |
|
|
|
</header> |
|
|
|
<div class="route-editor-body"> |
|
|
|
<aside class="route-config-panel"> |
|
|
|
@ -276,6 +277,7 @@ const execResultFilterOptions = [ |
|
|
|
] |
|
|
|
const routeSearch = ref('') |
|
|
|
const routeEditorOpen = ref(false) |
|
|
|
const editingRouteId = ref(null) |
|
|
|
const routeSaving = ref(false) |
|
|
|
const routeFormError = ref('') |
|
|
|
const routeCursor = ref('--') |
|
|
|
@ -542,11 +544,33 @@ function formatCoord(lon, lat) { |
|
|
|
return `${lon.toFixed(6)}, ${lat.toFixed(6)}` |
|
|
|
} |
|
|
|
|
|
|
|
function mapApiWaypointToPoint(wp) { |
|
|
|
return { |
|
|
|
longitude: Number(wp.longitude), |
|
|
|
latitude: Number(wp.latitude), |
|
|
|
altitude: Number(wp.altitude), |
|
|
|
speed: Number(wp.speed), |
|
|
|
turnMode: Number(wp.holdSec) > 0 ? 'stop' : 'auto', |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function mapPointToApiWaypoint(point) { |
|
|
|
return { |
|
|
|
longitude: point.longitude, |
|
|
|
latitude: point.latitude, |
|
|
|
altitude: point.altitude, |
|
|
|
speed: point.speed, |
|
|
|
yaw: 0, |
|
|
|
holdSec: point.turnMode === 'stop' ? 1 : 0, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function resetRouteEditor() { |
|
|
|
Object.assign(routeForm, { name: '', altitude: 50, speed: 10, turnMode: 'auto' }) |
|
|
|
routePoints.value = [] |
|
|
|
routeFormError.value = '' |
|
|
|
routeCursor.value = '--' |
|
|
|
editingRouteId.value = null |
|
|
|
} |
|
|
|
|
|
|
|
function clearTaskLayers(map) { |
|
|
|
@ -745,6 +769,31 @@ function closeRouteEditor() { |
|
|
|
teardownRouteMap() |
|
|
|
routeEditorOpen.value = false |
|
|
|
waypointEditorOpen.value = false |
|
|
|
editingRouteId.value = null |
|
|
|
} |
|
|
|
|
|
|
|
async function openRouteEditorForEdit(route) { |
|
|
|
routeFormError.value = '' |
|
|
|
try { |
|
|
|
const detail = await request.get(urls.ROUTE(route.id)) |
|
|
|
resetRouteEditor() |
|
|
|
editingRouteId.value = String(detail?.id ?? route.id) |
|
|
|
const list = Array.isArray(detail?.waypoints) ? detail.waypoints : [] |
|
|
|
routePoints.value = list.map(mapApiWaypointToPoint) |
|
|
|
const first = routePoints.value[0] |
|
|
|
Object.assign(routeForm, { |
|
|
|
name: detail?.name || route.name || '', |
|
|
|
altitude: first ? first.altitude : 50, |
|
|
|
speed: first ? first.speed : 10, |
|
|
|
turnMode: first ? first.turnMode : 'auto', |
|
|
|
}) |
|
|
|
routeEditorOpen.value = true |
|
|
|
await attachRouteMap() |
|
|
|
// boot already syncs; optional extra if ready: |
|
|
|
if (routeMapReady.value) syncRouteLayer() |
|
|
|
} catch (e) { |
|
|
|
ui.toast(e.message || '加载航线详情失败') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function routeDistance(points) { |
|
|
|
@ -858,17 +907,23 @@ async function submitRoute() { |
|
|
|
routeFormError.value = '至少需要设置 2 个航点。' |
|
|
|
return |
|
|
|
} |
|
|
|
const payload = { |
|
|
|
name, |
|
|
|
waypoints: routePoints.value.map(mapPointToApiWaypoint), |
|
|
|
} |
|
|
|
routeSaving.value = true |
|
|
|
try { |
|
|
|
await request.post(urls.ROUTES, { |
|
|
|
name, |
|
|
|
waypoints: routePoints.value.map((point) => ({ longitude: point.longitude, latitude: point.latitude, altitude: point.altitude, speed: point.speed, yaw: 0, holdSec: point.turnMode === 'stop' ? 1 : 0 })) |
|
|
|
}) |
|
|
|
if (editingRouteId.value) { |
|
|
|
await request.put(urls.ROUTE(editingRouteId.value), payload) |
|
|
|
ui.toast('航线已更新') |
|
|
|
} else { |
|
|
|
await request.post(urls.ROUTES, payload) |
|
|
|
ui.toast('航线已保存') |
|
|
|
} |
|
|
|
closeRouteEditor() |
|
|
|
ui.toast('航线已保存') |
|
|
|
await load() |
|
|
|
} catch (e) { |
|
|
|
routeFormError.value = e.message || '保存航线失败' |
|
|
|
routeFormError.value = e.message || (editingRouteId.value ? '更新航线失败' : '保存航线失败') |
|
|
|
} finally { |
|
|
|
routeSaving.value = false |
|
|
|
} |
|
|
|
@ -1058,27 +1113,73 @@ async function downloadRawVideo(record) { |
|
|
|
} |
|
|
|
.task-mode-radio { |
|
|
|
width: 100%; |
|
|
|
display: flex; |
|
|
|
max-width: 100%; |
|
|
|
flex-wrap: nowrap; |
|
|
|
--td-bg-color-component: #edf1f4; |
|
|
|
--td-bg-color-container-select: #fff; |
|
|
|
--td-text-color-secondary: #64727e; |
|
|
|
--td-text-color-primary: #245f9e; |
|
|
|
--td-component-border: transparent; |
|
|
|
--td-comp-size-m: 36px; |
|
|
|
--td-comp-paddingTB-xxs: 3px; |
|
|
|
--td-font-body-medium: 12px / 18px var(--td-font-family); |
|
|
|
--td-radius-default: 4px; |
|
|
|
--td-radius-small: 3px; |
|
|
|
} |
|
|
|
.task-mode-radio :deep(.t-radio-button) { |
|
|
|
flex: 1; |
|
|
|
flex: 1 1 0; |
|
|
|
min-width: 0; |
|
|
|
margin: 0; |
|
|
|
gap: 0; |
|
|
|
display: inline-flex; |
|
|
|
flex-direction: row; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
} |
|
|
|
.task-mode-radio :deep(.t-radio-group__bg-block) { |
|
|
|
box-shadow: 0 1px 4px rgba(23, 39, 50, 0.12); |
|
|
|
} |
|
|
|
.task-schedule-radio :deep(.t-radio-button) { |
|
|
|
min-width: 0; |
|
|
|
padding: 0 4px; |
|
|
|
white-space: nowrap; |
|
|
|
padding-left: 4px; |
|
|
|
padding-right: 4px; |
|
|
|
} |
|
|
|
.route-config-panel :deep(.t-input), |
|
|
|
.route-config-panel :deep(.t-input-number) { |
|
|
|
.route-config-panel :deep(.t-input-number), |
|
|
|
.route-config-panel :deep(.t-input-number .t-input) { |
|
|
|
width: 100%; |
|
|
|
} |
|
|
|
.route-config-panel :deep(.t-input) { |
|
|
|
height: 35px; |
|
|
|
min-height: 35px; |
|
|
|
padding: 0 9px; |
|
|
|
border: 1px solid var(--line); |
|
|
|
border-radius: 4px; |
|
|
|
color: #354550; |
|
|
|
background: #fff; |
|
|
|
font-size: 11px; |
|
|
|
box-shadow: none; |
|
|
|
} |
|
|
|
.route-config-panel :deep(.t-input__inner) { |
|
|
|
height: auto; |
|
|
|
padding: 0; |
|
|
|
border: 0; |
|
|
|
background: transparent; |
|
|
|
font-size: 11px; |
|
|
|
box-shadow: none; |
|
|
|
} |
|
|
|
.number-input :deep(.t-input-number) { |
|
|
|
width: 100%; |
|
|
|
} |
|
|
|
.number-input :deep(.t-input) { |
|
|
|
padding-right: 34px; |
|
|
|
} |
|
|
|
.route-config-panel :deep(.apply-all-waypoints.t-button) { |
|
|
|
width: 100%; |
|
|
|
height: 33px; |
|
|
|
min-height: 33px; |
|
|
|
flex-shrink: 0; |
|
|
|
overflow: visible; |
|
|
|
} |
|
|
|
.task-video-option :deep(.t-switch) { |
|
|
|
justify-self: end; |
|
|
|
} |
|
|
|
|