Compare commits
2 Commits
32859c3598
...
6a72df102d
| Author | SHA1 | Date |
|---|---|---|
|
|
6a72df102d | 2 weeks ago |
|
|
5ad820af0a | 2 weeks ago |
7 changed files with 4329 additions and 4236 deletions
@ -1,114 +1,113 @@ |
|||
<template> |
|||
<t-dialog |
|||
:visible="visible" |
|||
header="编辑机巢" |
|||
width="520px" |
|||
destroy-on-close |
|||
dialog-class-name="add-dock-dialog" |
|||
placement="center" |
|||
:close-on-overlay-click="!saving" |
|||
:close-on-esc-keydown="!saving" |
|||
:close-btn="!saving" |
|||
@update:visible="onVisibleUpdate" |
|||
@close="close" |
|||
> |
|||
<form novalidate @submit.prevent="submit"> |
|||
<label><span>机巢名称<b>*</b></span><t-input v-model="form.name" :maxlength="64" placeholder="例如:青山湖 03 号机巢" /></label> |
|||
<label><span>机巢 SN</span><t-input :model-value="dock?.sn || ''" readonly /></label> |
|||
<label><span>通信卡 ICCID</span><t-input :model-value="dock?.iccid || ''" readonly /></label> |
|||
<div class="form-row"> |
|||
<label><span>经度<b>*</b></span><t-input-number v-model="form.longitude" theme="normal" :min="-180" :max="180" :step="0.000001" placeholder="例如:120.219375" style="width: 100%" /></label> |
|||
<label><span>纬度<b>*</b></span><t-input-number v-model="form.latitude" theme="normal" :min="-90" :max="90" :step="0.000001" placeholder="例如:30.259244" style="width: 100%" /></label> |
|||
</div> |
|||
<label><span>设备位置<em>选填</em></span><t-input v-model="form.location" :maxlength="128" placeholder="例如:青山湖科技城 C 区" /></label> |
|||
<div class="form-error">{{ error }}</div> |
|||
</form> |
|||
<template #footer> |
|||
<t-button variant="outline" type="button" :disabled="saving" @click="close">取消</t-button> |
|||
<t-button theme="primary" type="button" :disabled="saving" :loading="saving" @click="submit">{{ saving ? '保存中…' : '保存修改' }}</t-button> |
|||
</template> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { reactive, ref, watch } from 'vue' |
|||
import request from '@/utils/http' |
|||
import * as urls from '@/config/urls' |
|||
|
|||
const props = defineProps({ |
|||
visible: Boolean, |
|||
dock: { type: Object, default: null } |
|||
}) |
|||
const emit = defineEmits(['close', 'saved']) |
|||
|
|||
const form = reactive({ name: '', longitude: undefined, latitude: undefined, location: '' }) |
|||
const error = ref('') |
|||
const saving = ref(false) |
|||
|
|||
watch(() => props.visible, (visible) => { |
|||
if (!visible || !props.dock) return |
|||
Object.assign(form, { |
|||
name: props.dock.name || '', |
|||
longitude: props.dock.longitude == null || props.dock.longitude === '' ? undefined : Number(props.dock.longitude), |
|||
latitude: props.dock.latitude == null || props.dock.latitude === '' ? undefined : Number(props.dock.latitude), |
|||
location: props.dock.location === '--' ? '' : props.dock.location || '' |
|||
}) |
|||
error.value = '' |
|||
}) |
|||
|
|||
function close() { |
|||
if (!saving.value) emit('close') |
|||
} |
|||
|
|||
function onVisibleUpdate(next) { |
|||
if (!next) close() |
|||
} |
|||
|
|||
async function submit() { |
|||
const name = form.name.trim() |
|||
const longitude = Number(form.longitude) |
|||
const latitude = Number(form.latitude) |
|||
|
|||
error.value = '' |
|||
if (!name || form.longitude === '' || form.longitude == null || form.latitude === '' || form.latitude == null) { |
|||
error.value = '请填写机巢名称、经度和纬度。' |
|||
return |
|||
} |
|||
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) { |
|||
error.value = '经度应在 -180~180 之间。' |
|||
return |
|||
} |
|||
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) { |
|||
error.value = '纬度应在 -90~90 之间。' |
|||
return |
|||
} |
|||
if (!props.dock?.id) return |
|||
|
|||
saving.value = true |
|||
try { |
|||
await request.put(urls.DOCK(props.dock.id), { |
|||
name, |
|||
longitude, |
|||
latitude, |
|||
location: form.location.trim() |
|||
}) |
|||
emit('saved') |
|||
} catch (e) { |
|||
error.value = e.message || '保存失败' |
|||
} finally { |
|||
saving.value = false |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.add-dock-dialog .t-input, |
|||
.add-dock-dialog .t-input-number { |
|||
width: 100%; |
|||
} |
|||
.add-dock-dialog .t-dialog__footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 8px; |
|||
} |
|||
</style> |
|||
<template> |
|||
<t-dialog |
|||
:visible="visible" |
|||
header="编辑机巢" |
|||
width="520px" |
|||
destroy-on-close |
|||
dialog-class-name="add-dock-dialog" |
|||
placement="center" |
|||
:close-on-overlay-click="!saving" |
|||
:close-on-esc-keydown="!saving" |
|||
:close-btn="!saving" |
|||
@update:visible="onVisibleUpdate" |
|||
@close="close" |
|||
> |
|||
<form novalidate @submit.prevent="submit"> |
|||
<label><span>机巢名称<b>*</b></span><t-input v-model="form.name" :maxlength="64" placeholder="例如:青山湖 03 号机巢" /></label> |
|||
<label><span>机巢 SN</span><t-input :model-value="dock?.sn || ''" readonly /></label> |
|||
<div class="form-row"> |
|||
<label><span>经度<b>*</b></span><t-input-number v-model="form.longitude" theme="normal" :min="-180" :max="180" :step="0.000001" placeholder="例如:120.219375" style="width: 100%" /></label> |
|||
<label><span>纬度<b>*</b></span><t-input-number v-model="form.latitude" theme="normal" :min="-90" :max="90" :step="0.000001" placeholder="例如:30.259244" style="width: 100%" /></label> |
|||
</div> |
|||
<label><span>设备位置<em>选填</em></span><t-input v-model="form.location" :maxlength="128" placeholder="例如:青山湖科技城 C 区" /></label> |
|||
<div class="form-error">{{ error }}</div> |
|||
</form> |
|||
<template #footer> |
|||
<t-button variant="outline" type="button" :disabled="saving" @click="close">取消</t-button> |
|||
<t-button theme="primary" type="button" :disabled="saving" :loading="saving" @click="submit">{{ saving ? '保存中…' : '保存修改' }}</t-button> |
|||
</template> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { reactive, ref, watch } from 'vue' |
|||
import request from '@/utils/http' |
|||
import * as urls from '@/config/urls' |
|||
|
|||
const props = defineProps({ |
|||
visible: Boolean, |
|||
dock: { type: Object, default: null } |
|||
}) |
|||
const emit = defineEmits(['close', 'saved']) |
|||
|
|||
const form = reactive({ name: '', longitude: undefined, latitude: undefined, location: '' }) |
|||
const error = ref('') |
|||
const saving = ref(false) |
|||
|
|||
watch(() => props.visible, (visible) => { |
|||
if (!visible || !props.dock) return |
|||
Object.assign(form, { |
|||
name: props.dock.name || '', |
|||
longitude: props.dock.longitude == null || props.dock.longitude === '' ? undefined : Number(props.dock.longitude), |
|||
latitude: props.dock.latitude == null || props.dock.latitude === '' ? undefined : Number(props.dock.latitude), |
|||
location: props.dock.location === '--' ? '' : props.dock.location || '' |
|||
}) |
|||
error.value = '' |
|||
}) |
|||
|
|||
function close() { |
|||
if (!saving.value) emit('close') |
|||
} |
|||
|
|||
function onVisibleUpdate(next) { |
|||
if (!next) close() |
|||
} |
|||
|
|||
async function submit() { |
|||
const name = form.name.trim() |
|||
const longitude = Number(form.longitude) |
|||
const latitude = Number(form.latitude) |
|||
|
|||
error.value = '' |
|||
if (!name || form.longitude === '' || form.longitude == null || form.latitude === '' || form.latitude == null) { |
|||
error.value = '请填写机巢名称、经度和纬度。' |
|||
return |
|||
} |
|||
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) { |
|||
error.value = '经度应在 -180~180 之间。' |
|||
return |
|||
} |
|||
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) { |
|||
error.value = '纬度应在 -90~90 之间。' |
|||
return |
|||
} |
|||
if (!props.dock?.id) return |
|||
|
|||
saving.value = true |
|||
try { |
|||
await request.put(urls.DOCK(props.dock.id), { |
|||
name, |
|||
longitude, |
|||
latitude, |
|||
location: form.location.trim() |
|||
}) |
|||
emit('saved') |
|||
} catch (e) { |
|||
error.value = e.message || '保存失败' |
|||
} finally { |
|||
saving.value = false |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.add-dock-dialog .t-input, |
|||
.add-dock-dialog .t-input-number { |
|||
width: 100%; |
|||
} |
|||
.add-dock-dialog .t-dialog__footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 8px; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,96 +1,101 @@ |
|||
/** API 路径集中配置。axios baseURL 已是 /api,此处用相对路径。 */ |
|||
export const BASE_URL = '/api' |
|||
|
|||
// Auth
|
|||
export const AUTH_CAPTCHA = '/v1/auth/captcha' |
|||
export const AUTH_LOGIN = '/v1/auth/login' |
|||
export const AUTH_REGISTER = '/v1/auth/register' |
|||
export const AUTH_SMS_CODE = '/v1/auth/sms-code' |
|||
export const AUTH_RESET_PASSWORD = '/v1/auth/reset-password' |
|||
export const AUTH_REFRESH = '/v1/auth/refresh' |
|||
export const AUTH_LOGOUT = '/v1/auth/logout' |
|||
|
|||
// Docks
|
|||
export const DOCKS = '/v1/docks' |
|||
export const DOCK = (id) => `/v1/docks/${id}` |
|||
export const DOCK_COMMAND = (id) => `/v1/docks/${id}/command` |
|||
export const DOCK_FIRMWARE_UPGRADE = (id) => `/v1/docks/${id}/firmware/upgrade` |
|||
|
|||
// Drones
|
|||
export const DRONES = '/v1/drones' |
|||
export const DRONE = (id) => `/v1/drones/${id}` |
|||
|
|||
// Tasks
|
|||
export const TASKS = '/v1/tasks' |
|||
export const TASK = (id) => `/v1/tasks/${id}` |
|||
export const TASK_EXECUTE = (id) => `/v1/tasks/${id}/execute` |
|||
// Executions
|
|||
export const EXECUTIONS = '/v1/executions' |
|||
export const EXECUTION = (key) => `/v1/executions/${key}` |
|||
export const EXECUTION_TRAJECTORY = (key) => `/v1/executions/${key}/trajectory` |
|||
|
|||
// Routes
|
|||
export const ROUTES = '/v1/routes' |
|||
export const ROUTE = (id) => `/v1/routes/${id}` |
|||
|
|||
// Live / Videos
|
|||
export const LIVE = '/v1/live' |
|||
export const LIVE_SESSIONS = (dockId) => `/v1/live/${dockId}/sessions` |
|||
export const LIVE_PLAY_URL = (dockId) => `/v1/live/${dockId}/play-url` |
|||
export const LIVE_HEARTBEAT = (dockId, sessionId) => `/v1/live/${dockId}/sessions/${sessionId}/heartbeat` |
|||
export const LIVE_LEAVE = (dockId, sessionId) => `/v1/live/${dockId}/sessions/${sessionId}/viewers/me` |
|||
export const LIVE_SESSION = (dockId, sessionId) => `/v1/live/${dockId}/sessions/${sessionId}` |
|||
export const LIVE_STOP = (dockId) => `/v1/live/${dockId}/stop` |
|||
export const VIDEOS = '/v1/videos' |
|||
export const VIDEO_DOWNLOAD = (id) => `/v1/videos/${id}/download` |
|||
|
|||
// Alarms
|
|||
export const ALARMS = '/v1/alarms' |
|||
export const ALARM_RESOLVE = (id) => `/v1/alarms/${id}/resolve` |
|||
|
|||
// Commands
|
|||
export const COMMANDS = '/v1/commands' |
|||
export const COMMAND = (id) => `/v1/commands/${id}` |
|||
|
|||
// Account
|
|||
export const ACCOUNT_PROFILE = '/v1/account/profile' |
|||
export const TRAFFIC_BALANCE = '/v1/account/traffic/balance' |
|||
export const TRAFFIC_PACKAGES = '/v1/account/traffic-packages' |
|||
export const TRAFFIC_ORDERS = '/v1/account/traffic/orders' |
|||
export const TRAFFIC_ORDER = (id) => `/v1/account/traffic/orders/${id}` |
|||
export const TRAFFIC_USAGE = '/v1/account/traffic/usage' |
|||
export const ACCOUNT_RESOURCE_SUMMARY = '/v1/account/resource-summary' |
|||
export const ACCOUNT_DOWNLOADS = '/v1/account/downloads' |
|||
export const ACCOUNT_CHANGE_PASSWORD = '/v1/account/security/change-password' |
|||
export const ACCOUNT_SESSIONS = '/v1/account/security/sessions' |
|||
export const INVOICE_PROFILES = '/v1/account/invoice-profiles' |
|||
export const INVOICE_PROFILE = (id) => `/v1/account/invoice-profiles/${id}` |
|||
export const INVOICE_ORDERS = '/v1/account/invoice-orders' |
|||
export const INVOICE_REQUESTS = '/v1/account/invoice-requests' |
|||
export const PAYMENTS = '/v1/account/payments' |
|||
export const PAYMENT = (id) => `/v1/account/payments/${id}` |
|||
export const SIM_CARDS = '/v1/account/sim-cards' |
|||
export const SIM_CARD = (id) => `/v1/account/sim-cards/${id}` |
|||
export const SIM_PACKAGES = '/v1/account/sim-packages' |
|||
export const SIM_CARD_RECHARGE_ORDERS = (id) => `/v1/account/sim-cards/${id}/recharge-orders` |
|||
export const SIM_RECHARGE_ORDER = (id) => `/v1/account/sim-recharge-orders/${id}` |
|||
export const SIM_RECHARGE_LOGS = '/v1/account/sim-recharge-logs' |
|||
|
|||
// Admin traffic ops
|
|||
export const ADMIN_TRAFFIC_POOL = '/v1/admin/traffic/pool' |
|||
export const ADMIN_TRAFFIC_PURCHASES = '/v1/admin/traffic/purchases' |
|||
export const ADMIN_TRAFFIC_PACKAGES = '/v1/admin/traffic/packages' |
|||
export const ADMIN_TRAFFIC_PACKAGE = (id) => `/v1/admin/traffic/packages/${id}` |
|||
export const ADMIN_TRAFFIC_LEDGER = '/v1/admin/traffic/ledger' |
|||
export const ADMIN_TRAFFIC_TEST_GRANTS = '/v1/admin/traffic/test-grants' |
|||
|
|||
// Users
|
|||
export const USERS = '/v1/users' |
|||
|
|||
// Firmwares
|
|||
export const FIRMWARES = '/v1/firmwares' |
|||
export const FIRMWARES_RELEASED = '/v1/firmwares/released' |
|||
export const FIRMWARE = (id) => `/v1/firmwares/${id}` |
|||
|
|||
// Health
|
|||
export const HEALTH = '/health' |
|||
/** API 路径集中配置。axios baseURL 已是 /api,此处用相对路径。 */ |
|||
export const BASE_URL = '/api' |
|||
|
|||
// Auth
|
|||
export const AUTH_CAPTCHA = '/v1/auth/captcha' |
|||
export const AUTH_LOGIN = '/v1/auth/login' |
|||
export const AUTH_REGISTER = '/v1/auth/register' |
|||
export const AUTH_SMS_CODE = '/v1/auth/sms-code' |
|||
export const AUTH_RESET_PASSWORD = '/v1/auth/reset-password' |
|||
export const AUTH_REFRESH = '/v1/auth/refresh' |
|||
export const AUTH_LOGOUT = '/v1/auth/logout' |
|||
|
|||
// Docks
|
|||
export const DOCKS = '/v1/docks' |
|||
export const DOCK = (id) => `/v1/docks/${id}` |
|||
export const DOCK_COMMAND = (id) => `/v1/docks/${id}/command` |
|||
export const DOCK_FIRMWARE_UPGRADE = (id) => `/v1/docks/${id}/firmware/upgrade` |
|||
|
|||
// Drones
|
|||
export const DRONES = '/v1/drones' |
|||
export const DRONE = (id) => `/v1/drones/${id}` |
|||
|
|||
// Tasks
|
|||
export const TASKS = '/v1/tasks' |
|||
export const TASK = (id) => `/v1/tasks/${id}` |
|||
export const TASK_EXECUTE = (id) => `/v1/tasks/${id}/execute` |
|||
// Executions
|
|||
export const EXECUTIONS = '/v1/executions' |
|||
export const EXECUTION = (key) => `/v1/executions/${key}` |
|||
export const EXECUTION_TRAJECTORY = (key) => `/v1/executions/${key}/trajectory` |
|||
|
|||
// Routes
|
|||
export const ROUTES = '/v1/routes' |
|||
export const ROUTE = (id) => `/v1/routes/${id}` |
|||
|
|||
// Live / Videos
|
|||
export const LIVE = '/v1/live' |
|||
export const LIVE_SESSIONS = (dockId) => `/v1/live/${dockId}/sessions` |
|||
export const LIVE_PLAY_URL = (dockId) => `/v1/live/${dockId}/play-url` |
|||
export const LIVE_HEARTBEAT = (dockId, sessionId) => `/v1/live/${dockId}/sessions/${sessionId}/heartbeat` |
|||
export const LIVE_LEAVE = (dockId, sessionId) => `/v1/live/${dockId}/sessions/${sessionId}/viewers/me` |
|||
export const LIVE_SESSION = (dockId, sessionId) => `/v1/live/${dockId}/sessions/${sessionId}` |
|||
export const LIVE_STOP = (dockId) => `/v1/live/${dockId}/stop` |
|||
export const VIDEOS = '/v1/videos' |
|||
export const VIDEO_DOWNLOAD = (id) => `/v1/videos/${id}/download` |
|||
|
|||
// Alarms
|
|||
export const ALARMS = '/v1/alarms' |
|||
export const ALARM_RESOLVE = (id) => `/v1/alarms/${id}/resolve` |
|||
|
|||
// Commands
|
|||
export const COMMANDS = '/v1/commands' |
|||
export const COMMAND = (id) => `/v1/commands/${id}` |
|||
|
|||
// Account
|
|||
export const ACCOUNT_PROFILE = '/v1/account/profile' |
|||
export const TRAFFIC_BALANCE = '/v1/account/traffic/balance' |
|||
export const TRAFFIC_PACKAGES = '/v1/account/traffic-packages' |
|||
export const TRAFFIC_ORDERS = '/v1/account/traffic/orders' |
|||
export const TRAFFIC_ORDER = (id) => `/v1/account/traffic/orders/${id}` |
|||
export const TRAFFIC_USAGE = '/v1/account/traffic/usage' |
|||
export const ACCOUNT_RESOURCE_SUMMARY = '/v1/account/resource-summary' |
|||
export const ACCOUNT_DOWNLOADS = '/v1/account/downloads' |
|||
export const ACCOUNT_CHANGE_PASSWORD = '/v1/account/security/change-password' |
|||
export const ACCOUNT_SESSIONS = '/v1/account/security/sessions' |
|||
export const INVOICE_PROFILES = '/v1/account/invoice-profiles' |
|||
export const INVOICE_PROFILE = (id) => `/v1/account/invoice-profiles/${id}` |
|||
export const INVOICE_ORDERS = '/v1/account/invoice-orders' |
|||
export const INVOICE_REQUESTS = '/v1/account/invoice-requests' |
|||
export const PAYMENTS = '/v1/account/payments' |
|||
export const PAYMENT = (id) => `/v1/account/payments/${id}` |
|||
export const SIM_CARDS = '/v1/account/sim-cards' |
|||
export const SIM_CARD_CREATE = '/v1/account/sim-cards' |
|||
export const SIM_CARD = (id) => `/v1/account/sim-cards/${id}` |
|||
export const SIM_PACKAGES = '/v1/account/sim-packages' |
|||
export const SIM_CARD_RECHARGE_ORDERS = (id) => `/v1/account/sim-cards/${id}/recharge-orders` |
|||
export const SIM_RECHARGE_ORDER = (id) => `/v1/account/sim-recharge-orders/${id}` |
|||
export const SIM_RECHARGE_LOGS = '/v1/account/sim-recharge-logs' |
|||
|
|||
// Admin traffic ops
|
|||
export const ADMIN_TRAFFIC_POOL = '/v1/admin/traffic/pool' |
|||
export const ADMIN_TRAFFIC_PURCHASES = '/v1/admin/traffic/purchases' |
|||
export const ADMIN_TRAFFIC_PACKAGES = '/v1/admin/traffic/packages' |
|||
export const ADMIN_TRAFFIC_PACKAGE = (id) => `/v1/admin/traffic/packages/${id}` |
|||
export const ADMIN_TRAFFIC_LEDGER = '/v1/admin/traffic/ledger' |
|||
export const ADMIN_TRAFFIC_TEST_GRANTS = '/v1/admin/traffic/test-grants' |
|||
|
|||
// Sim 通信卡套餐管理
|
|||
export const ADMIN_SIM_PACKAGES = '/v1/admin/sim-packages' |
|||
export const ADMIN_SIM_PROVIDER_PLANS = '/v1/admin/sim-packages/provider-plans' |
|||
export const ADMIN_SIM_PACKAGE = (id) => `/v1/admin/sim-packages/${id}` |
|||
|
|||
export const USERS = '/v1/users' |
|||
|
|||
// Firmwares
|
|||
export const FIRMWARES = '/v1/firmwares' |
|||
export const FIRMWARES_RELEASED = '/v1/firmwares/released' |
|||
export const FIRMWARE = (id) => `/v1/firmwares/${id}` |
|||
|
|||
// Health
|
|||
export const HEALTH = '/health' |
|||
|
|||
@ -1,420 +1,420 @@ |
|||
import { defineStore } from 'pinia' |
|||
import { reactive, ref } from 'vue' |
|||
import request from '@/utils/http' |
|||
import * as urls from '@/config/urls' |
|||
|
|||
// 后端 status 值 → 展示文案 + 状态样式
|
|||
const DOCK_STATUS = { |
|||
online: { status: '在线', statusClass: 'online' }, |
|||
offline: { status: '离线', statusClass: 'offline' } |
|||
} |
|||
|
|||
function fmtTime(t) { |
|||
if (!t) return '--' |
|||
const d = new Date(t) |
|||
if (Number.isNaN(d.getTime())) return '--' |
|||
return d.toLocaleString('zh-CN', { hour12: false }) |
|||
} |
|||
|
|||
function gpsText(q) { |
|||
const key = String(q || '').toUpperCase().replace(/-/g, '_') |
|||
switch (key) { |
|||
case 'RTK_FIX': |
|||
case 'RTK_FIXED': return 'RTK 固定解' |
|||
case 'RTK_FLOAT': return 'RTK 浮点解' |
|||
case '3D_FIX': return '3D 定位' |
|||
case '2D_FIX': return '2D 定位' |
|||
default: return q ? String(q) : '无定位' |
|||
} |
|||
} |
|||
|
|||
function modeText(m) { |
|||
const key = String(m || '').toUpperCase() |
|||
switch (key) { |
|||
case 'STANDBY': return '待命' |
|||
case 'AUTO': return 'AUTO' |
|||
case 'GUIDED': return 'GUIDED' |
|||
case 'LOITER': return '悬停' |
|||
case 'RTL': return '返航' |
|||
default: return m || '--' |
|||
} |
|||
} |
|||
|
|||
function controlModeText(m) { |
|||
const key = String(m || '').toLowerCase() |
|||
if (key === 'auto' || key === 'automatic') return '自动' |
|||
if (key === 'manual') return '手动' |
|||
return m || '--' |
|||
} |
|||
|
|||
function parseJson(value, fallback = null) { |
|||
if (!value) return fallback |
|||
if (typeof value === 'object') return value |
|||
try { return JSON.parse(value) } catch { return fallback } |
|||
} |
|||
|
|||
function environmentValues(realtime) { |
|||
const nested = parseJson(realtime?.environment, {}) || {} |
|||
const value = (key, ...aliases) => { |
|||
for (const name of [key, ...aliases]) { |
|||
if (nested[name] != null && nested[name] !== '') return nested[name] |
|||
if (realtime?.[name] != null && realtime[name] !== '') return realtime[name] |
|||
} |
|||
return null |
|||
} |
|||
return { |
|||
rain: value('rain'), |
|||
rainfall: value('rainfall', 'rainAmount', 'rainFall', 'precipitation'), |
|||
windSpeed: value('windSpeed', 'wind_speed'), |
|||
outsideTemperature: value('outsideTemperature', 'outside_temperature'), |
|||
outsideHumidity: value('outsideHumidity', 'outside_humidity'), |
|||
insideTemperature: value('insideTemperature', 'inside_temperature', 'temperature'), |
|||
insideHumidity: value('insideHumidity', 'inside_humidity', 'humidity') |
|||
} |
|||
} |
|||
|
|||
function alarmCodes(realtime) { |
|||
const codes = parseJson(realtime?.alarmCodes, []) |
|||
return Array.isArray(codes) ? codes : [] |
|||
} |
|||
|
|||
function displayValue(value) { |
|||
return value == null || value === '' ? '--' : String(value) |
|||
} |
|||
|
|||
function coordinate(value, min, max) { |
|||
const number = Number(value) |
|||
return Number.isFinite(number) && number >= min && number <= max ? number : null |
|||
} |
|||
|
|||
// 指令标题 → 后端指令 type
|
|||
const COMMANDS = { |
|||
'一键起飞': 'workflow.one_key_takeoff', |
|||
'一键降落': 'workflow.one_key_land', |
|||
'起飞准备': 'dock.prepare_takeoff', |
|||
'起飞准备完成': 'dock.complete_takeoff', |
|||
'降落准备': 'dock.prepare_landing', |
|||
'降落准备完成': 'dock.complete_landing', |
|||
'打开舱门': 'dock.open', |
|||
'关闭舱门': 'dock.close', |
|||
'开始充电': 'dock.charge_start', |
|||
'整机复位': 'dock.reset', |
|||
'机巢复位': 'dock.reset', |
|||
'无人机开机': 'dock.drone_power_on', |
|||
'无人机关机': 'dock.drone_power_off', |
|||
'远程重启': 'dock.reboot', |
|||
'清除设备告警': 'dock.clear_alarm', |
|||
'设备自检': 'dock.self_check', |
|||
'起飞': 'drone.takeoff', |
|||
'降落': 'drone.land', |
|||
'一键返航': 'drone.return', |
|||
'悬停': 'drone.hover', |
|||
'紧急停止': 'drone.emergency_stop', |
|||
'开始任务': 'drone.mission_start', |
|||
'暂停任务': 'drone.mission_pause', |
|||
'继续任务': 'drone.mission_resume', |
|||
'取消任务': 'drone.mission_cancel' |
|||
} |
|||
|
|||
// 经纬度 → 地图百分比坐标(按设备经纬范围动态投影,北向上)
|
|||
function project(lon, lat, bounds) { |
|||
const lonSpan = bounds.maxLon - bounds.minLon || 1 |
|||
const latSpan = bounds.maxLat - bounds.minLat || 1 |
|||
const x = ((lon - bounds.minLon) / lonSpan) * 80 + 10 |
|||
const y = ((bounds.maxLat - lat) / latSpan) * 70 + 15 |
|||
return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 } |
|||
} |
|||
|
|||
function dockStatusFromDetail(registerStatus, online, rt) { |
|||
const pending = registerStatus === 'pending' && !online |
|||
if (pending) return { status: '待连接', statusClass: 'pending' } |
|||
if (!online) return DOCK_STATUS.offline |
|||
if (alarmCodes(rt).length) return { status: '告警', statusClass: 'alarm' } |
|||
return DOCK_STATUS.online |
|||
} |
|||
|
|||
function droneStatusFromDetail(online, rt) { |
|||
const armed = ['1', 'true', 1, true].includes(rt.armed) |
|||
const altitude = coordinate(rt.altitude, -1000, 100000) |
|||
const flying = armed || (altitude != null && altitude > 0) |
|||
if (!online) return { status: '离线', statusClass: 'offline', altitude, flying } |
|||
if (flying) return { status: '飞行中', statusClass: 'mission', altitude, flying } |
|||
return { status: '在巢', statusClass: 'online', altitude, flying } |
|||
} |
|||
|
|||
|
|||
export const useDeviceStore = defineStore('devices', () => { |
|||
const assets = reactive({}) |
|||
const docks = ref([]) |
|||
const drones = ref([]) |
|||
const loaded = ref(false) |
|||
|
|||
function asset(id) { |
|||
return assets[id] || null |
|||
} |
|||
|
|||
function record(id) { |
|||
const key = String(id) |
|||
return docks.value.find((d) => d.id === key) || drones.value.find((d) => d.id === key) || null |
|||
} |
|||
|
|||
function commandType(title) { |
|||
return COMMANDS[title] || '' |
|||
} |
|||
|
|||
async function sendCommand(rec, title, params = {}) { |
|||
const type = COMMANDS[title] |
|||
if (!type) throw new Error('未知指令') |
|||
if (rec.kind === 'dock') { |
|||
return request.post(urls.DOCK_COMMAND(rec.id), { type, params }) |
|||
} |
|||
const dock = docks.value.find((d) => d.dockId === rec.dockId) |
|||
if (!dock) throw new Error('未找到绑定机巢') |
|||
return request.post(urls.DOCK_COMMAND(dock.id), { type, params, droneSn: rec.droneSn }) |
|||
} |
|||
|
|||
async function load(options = {}) { |
|||
const silent = Boolean(options.silent) |
|||
const reqOpts = silent ? { skipErrorToast: true } : {} |
|||
const [dockPage, dronePage] = await Promise.all([ |
|||
request.get(urls.DOCKS, { params: { pageNum: 1, pageSize: 100 }, ...reqOpts }), |
|||
request.get(urls.DRONES, { params: { pageNum: 1, pageSize: 100 }, ...reqOpts }) |
|||
]) |
|||
|
|||
const dockList = dockPage?.records || [] |
|||
const droneList = dronePage?.records || [] |
|||
const [dockDetails, droneDetails] = await Promise.all([ |
|||
Promise.all(dockList.map((d) => request.get(urls.DOCK(d.id), reqOpts).then((r) => r || {}).catch(() => ({})))), |
|||
Promise.all(droneList.map((d) => request.get(urls.DRONE(d.id), reqOpts).then((r) => r || {}).catch(() => ({})))) |
|||
]) |
|||
const coords = dockList |
|||
.map((d) => ({ longitude: coordinate(d.longitude, -180, 180), latitude: coordinate(d.latitude, -90, 90) })) |
|||
.filter((d) => d.longitude != null && d.latitude != null) |
|||
const defaultLon = coords[0]?.longitude ?? 0 |
|||
const defaultLat = coords[0]?.latitude ?? 0 |
|||
const bounds = { |
|||
minLon: Math.min(...coords.map((d) => Number(d.longitude)), defaultLon), |
|||
maxLon: Math.max(...coords.map((d) => Number(d.longitude)), defaultLon), |
|||
minLat: Math.min(...coords.map((d) => Number(d.latitude)), defaultLat), |
|||
maxLat: Math.max(...coords.map((d) => Number(d.latitude)), defaultLat) |
|||
} |
|||
if (bounds.minLon === bounds.maxLon) { bounds.minLon -= 0.01; bounds.maxLon += 0.01 } |
|||
if (bounds.minLat === bounds.maxLat) { bounds.minLat -= 0.01; bounds.maxLat += 0.01 } |
|||
|
|||
const newDocks = dockList.map((d, i) => { |
|||
const id = String(d.id) |
|||
const detail = dockDetails[i] || {} |
|||
const rt = detail.realtime || {} |
|||
const online = Boolean(detail.online) |
|||
const registerStatus = d.registerStatus || 'registered' |
|||
const st = dockStatusFromDetail(registerStatus, online, rt) |
|||
const name = d.name || d.dockId |
|||
const environment = environmentValues(rt) |
|||
return { |
|||
id, |
|||
kind: 'dock', |
|||
dockId: d.dockId, |
|||
name, |
|||
code: d.code || d.sn || d.dockId, |
|||
sn: d.sn || d.dockId || '--', |
|||
iccid: d.iccid || '--', |
|||
longitude: coordinate(d.longitude, -180, 180), |
|||
latitude: coordinate(d.latitude, -90, 90), |
|||
location: d.location || '--', |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
binding: '--', |
|||
bindingState: 'unbound', |
|||
mode: controlModeText(rt.controlMode), |
|||
version: d.softwareVer ? `v${d.softwareVer}` : '--', |
|||
updated: fmtTime(d.updatedAt), |
|||
protocol: d.protocolVer ? `MQTT v${d.protocolVer}` : '--', |
|||
registerStatus, |
|||
realtime: rt, |
|||
environment, |
|||
alarms: alarmCodes(rt), |
|||
online |
|||
} |
|||
}) |
|||
|
|||
const newDrones = droneList.map((d, i) => { |
|||
const id = String(d.id) |
|||
const detail = droneDetails[i] || {} |
|||
const rt = detail.realtime || {} |
|||
const online = Boolean(detail.online) |
|||
const st = droneStatusFromDetail(online, rt) |
|||
const altitude = st.altitude |
|||
const dock = newDocks.find((dk) => dk.dockId === d.dockId) |
|||
const battery = rt.batteryPercent ?? rt.batteryPct ?? d.battery |
|||
return { |
|||
id, |
|||
kind: 'drone', |
|||
droneSn: d.droneSn, |
|||
dockId: d.dockId, |
|||
name: d.name || d.droneSn, |
|||
code: d.code || d.droneSn, |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
binding: dock?.name || '--', |
|||
bindingState: dock ? 'bound' : 'unbound', |
|||
battery: battery != null && battery !== '' ? `${battery}%` : '--', |
|||
mode: modeText(rt.flightMode), |
|||
mission: displayValue(rt.missionName || rt.mission), |
|||
updated: fmtTime(d.updatedAt), |
|||
sysid: displayValue(rt.currentSysId), |
|||
gps: gpsText(rt.gpsQuality), |
|||
sn: d.droneSn || d.code || '--', |
|||
model: d.model || '--', |
|||
firmwareVer: d.firmwareVer || '--', |
|||
localRoutes: [], |
|||
realtime: rt, |
|||
alarms: alarmCodes(rt), |
|||
online, |
|||
_lat: coordinate(rt.latitude, -90, 90), |
|||
_lon: coordinate(rt.longitude, -180, 180), |
|||
_alt: altitude, |
|||
_speed: coordinate(rt.groundSpeed, 0, 1000), |
|||
_sats: coordinate(rt.satellites, 0, 1000) |
|||
} |
|||
}) |
|||
|
|||
for (const dk of newDocks) { |
|||
const dr = newDrones.find((d) => d.dockId === dk.dockId) |
|||
if (dr) { |
|||
dk.binding = dr.name |
|||
dk.bindingState = 'bound' |
|||
} |
|||
} |
|||
|
|||
docks.value = newDocks |
|||
drones.value = newDrones |
|||
|
|||
// 重建地图资产
|
|||
Object.keys(assets).forEach((k) => delete assets[k]) |
|||
for (const dk of newDocks) { |
|||
const hasCoordinates = dk.longitude != null && dk.latitude != null |
|||
const p = hasCoordinates ? project(dk.longitude, dk.latitude, bounds) : { x: null, y: null } |
|||
assets[dk.id] = { |
|||
id: dk.id, type: 'dock', name: dk.name, location: dk.location, |
|||
status: dk.status, statusClass: dk.statusClass, x: p.x, y: p.y, hasCoordinates, |
|||
longitude: hasCoordinates ? dk.longitude : null, |
|||
latitude: hasCoordinates ? dk.latitude : null, |
|||
realtime: dk.realtime, environment: dk.environment, alarms: dk.alarms, online: dk.online |
|||
} |
|||
} |
|||
for (const dr of newDrones) { |
|||
const dock = newDocks.find((d) => d.dockId === dr.dockId) |
|||
const lon = dr._lon != null ? dr._lon : dock?.longitude |
|||
const lat = dr._lat != null ? dr._lat : dock?.latitude |
|||
const hasCoordinates = lon != null && lat != null |
|||
const p = hasCoordinates ? project(lon, lat, bounds) : { x: null, y: null } |
|||
assets[dr.id] = { |
|||
id: dr.id, type: 'drone', name: dr.name, |
|||
location: dr.statusClass === 'mission' ? `${dock?.location || '--'} · 飞行中` : `停放于${dock?.name || '--'}`, |
|||
status: dr.status, statusClass: dr.statusClass, x: p.x, y: p.y, hasCoordinates, |
|||
longitude: hasCoordinates ? lon : null, |
|||
latitude: hasCoordinates ? lat : null, |
|||
parent: dock?.id || '', parentName: dock?.name || '--', |
|||
inDock: dr.statusClass !== 'mission', altitude: dr._alt, speed: dr._speed, satellites: dr._sats, |
|||
realtime: dr.realtime, alarms: dr.alarms, online: dr.online |
|||
} |
|||
} |
|||
|
|||
loaded.value = true |
|||
return { docks: newDocks, drones: newDrones } |
|||
} |
|||
|
|||
function applyDockDetail(id, detail) { |
|||
const key = String(id) |
|||
const dock = docks.value.find((d) => d.id === key) |
|||
if (!dock) { |
|||
console.warn('[devices] applyDockDetail missing dock', key) |
|||
return false |
|||
} |
|||
const rt = detail?.realtime || {} |
|||
const online = Boolean(detail?.online) |
|||
const st = dockStatusFromDetail(dock.registerStatus || 'registered', online, rt) |
|||
const environment = environmentValues(rt) |
|||
const alarms = alarmCodes(rt) |
|||
Object.assign(dock, { |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
mode: controlModeText(rt.controlMode), |
|||
realtime: rt, |
|||
environment, |
|||
alarms, |
|||
online, |
|||
}) |
|||
const assetRow = assets[key] |
|||
if (assetRow) { |
|||
Object.assign(assetRow, { |
|||
status: dock.status, |
|||
statusClass: dock.statusClass, |
|||
realtime: rt, |
|||
environment, |
|||
alarms, |
|||
online, |
|||
}) |
|||
} |
|||
return true |
|||
} |
|||
|
|||
function applyDroneDetail(id, detail) { |
|||
const key = String(id) |
|||
const drone = drones.value.find((d) => d.id === key) |
|||
if (!drone) { |
|||
console.warn('[devices] applyDroneDetail missing drone', key) |
|||
return false |
|||
} |
|||
const rt = detail?.realtime || {} |
|||
const online = Boolean(detail?.online) |
|||
const st = droneStatusFromDetail(online, rt) |
|||
const altitude = st.altitude |
|||
const battery = rt.batteryPercent ?? rt.batteryPct |
|||
const alarms = alarmCodes(rt) |
|||
const dock = docks.value.find((d) => d.dockId === drone.dockId) |
|||
Object.assign(drone, { |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
battery: battery != null && battery !== '' ? `${battery}%` : drone.battery, |
|||
mode: modeText(rt.flightMode), |
|||
mission: displayValue(rt.missionName || rt.mission), |
|||
sysid: displayValue(rt.currentSysId), |
|||
gps: gpsText(rt.gpsQuality), |
|||
realtime: rt, |
|||
alarms, |
|||
online, |
|||
_lat: coordinate(rt.latitude, -90, 90), |
|||
_lon: coordinate(rt.longitude, -180, 180), |
|||
_alt: altitude, |
|||
_speed: coordinate(rt.groundSpeed, 0, 1000), |
|||
_sats: coordinate(rt.satellites, 0, 1000), |
|||
}) |
|||
const assetRow = assets[key] |
|||
if (assetRow) { |
|||
const lon = drone._lon != null ? drone._lon : dock?.longitude |
|||
const lat = drone._lat != null ? drone._lat : dock?.latitude |
|||
const hasCoordinates = lon != null && lat != null |
|||
Object.assign(assetRow, { |
|||
status: drone.status, |
|||
statusClass: drone.statusClass, |
|||
location: drone.statusClass === 'mission' |
|||
? `${dock?.location || '--'} · 飞行中` |
|||
: `停放于${dock?.name || '--'}`, |
|||
hasCoordinates, |
|||
longitude: hasCoordinates ? lon : null, |
|||
latitude: hasCoordinates ? lat : null, |
|||
inDock: drone.statusClass !== 'mission', |
|||
altitude: drone._alt, |
|||
speed: drone._speed, |
|||
satellites: drone._sats, |
|||
realtime: rt, |
|||
alarms, |
|||
online, |
|||
}) |
|||
} |
|||
return true |
|||
} |
|||
|
|||
return { assets, docks, drones, loaded, asset, record, commandType, sendCommand, load, applyDockDetail, applyDroneDetail } |
|||
}) |
|||
import { defineStore } from 'pinia' |
|||
import { reactive, ref } from 'vue' |
|||
import request from '@/utils/http' |
|||
import * as urls from '@/config/urls' |
|||
|
|||
// 后端 status 值 → 展示文案 + 状态样式
|
|||
const DOCK_STATUS = { |
|||
online: { status: '在线', statusClass: 'online' }, |
|||
offline: { status: '离线', statusClass: 'offline' } |
|||
} |
|||
|
|||
function fmtTime(t) { |
|||
if (!t) return '--' |
|||
const d = new Date(t) |
|||
if (Number.isNaN(d.getTime())) return '--' |
|||
return d.toLocaleString('zh-CN', { hour12: false }) |
|||
} |
|||
|
|||
function gpsText(q) { |
|||
const key = String(q || '').toUpperCase().replace(/-/g, '_') |
|||
switch (key) { |
|||
case 'RTK_FIX': |
|||
case 'RTK_FIXED': return 'RTK 固定解' |
|||
case 'RTK_FLOAT': return 'RTK 浮点解' |
|||
case '3D_FIX': return '3D 定位' |
|||
case '2D_FIX': return '2D 定位' |
|||
default: return q ? String(q) : '无定位' |
|||
} |
|||
} |
|||
|
|||
function modeText(m) { |
|||
const key = String(m || '').toUpperCase() |
|||
switch (key) { |
|||
case 'STANDBY': return '待命' |
|||
case 'AUTO': return 'AUTO' |
|||
case 'GUIDED': return 'GUIDED' |
|||
case 'LOITER': return '悬停' |
|||
case 'RTL': return '返航' |
|||
default: return m || '--' |
|||
} |
|||
} |
|||
|
|||
function controlModeText(m) { |
|||
const key = String(m || '').toLowerCase() |
|||
if (key === 'auto' || key === 'automatic') return '自动' |
|||
if (key === 'manual') return '手动' |
|||
return m || '--' |
|||
} |
|||
|
|||
function parseJson(value, fallback = null) { |
|||
if (!value) return fallback |
|||
if (typeof value === 'object') return value |
|||
try { return JSON.parse(value) } catch { return fallback } |
|||
} |
|||
|
|||
function environmentValues(realtime) { |
|||
const nested = parseJson(realtime?.environment, {}) || {} |
|||
const value = (key, ...aliases) => { |
|||
for (const name of [key, ...aliases]) { |
|||
if (nested[name] != null && nested[name] !== '') return nested[name] |
|||
if (realtime?.[name] != null && realtime[name] !== '') return realtime[name] |
|||
} |
|||
return null |
|||
} |
|||
return { |
|||
rain: value('rain'), |
|||
rainfall: value('rainfall', 'rainAmount', 'rainFall', 'precipitation'), |
|||
windSpeed: value('windSpeed', 'wind_speed'), |
|||
outsideTemperature: value('outsideTemperature', 'outside_temperature'), |
|||
outsideHumidity: value('outsideHumidity', 'outside_humidity'), |
|||
insideTemperature: value('insideTemperature', 'inside_temperature', 'temperature'), |
|||
insideHumidity: value('insideHumidity', 'inside_humidity', 'humidity') |
|||
} |
|||
} |
|||
|
|||
function alarmCodes(realtime) { |
|||
const codes = parseJson(realtime?.alarmCodes, []) |
|||
return Array.isArray(codes) ? codes : [] |
|||
} |
|||
|
|||
function displayValue(value) { |
|||
return value == null || value === '' ? '--' : String(value) |
|||
} |
|||
|
|||
function coordinate(value, min, max) { |
|||
const number = Number(value) |
|||
return Number.isFinite(number) && number >= min && number <= max ? number : null |
|||
} |
|||
|
|||
// 指令标题 → 后端指令 type
|
|||
const COMMANDS = { |
|||
'一键起飞': 'workflow.one_key_takeoff', |
|||
'一键降落': 'workflow.one_key_land', |
|||
'起飞准备': 'dock.prepare_takeoff', |
|||
'起飞准备完成': 'dock.complete_takeoff', |
|||
'降落准备': 'dock.prepare_landing', |
|||
'降落准备完成': 'dock.complete_landing', |
|||
'打开舱门': 'dock.open', |
|||
'关闭舱门': 'dock.close', |
|||
'开始充电': 'dock.charge_start', |
|||
'整机复位': 'dock.reset', |
|||
'机巢复位': 'dock.reset', |
|||
'无人机开机': 'dock.drone_power_on', |
|||
'无人机关机': 'dock.drone_power_off', |
|||
'远程重启': 'dock.reboot', |
|||
'清除设备告警': 'dock.clear_alarm', |
|||
'设备自检': 'dock.self_check', |
|||
'起飞': 'drone.takeoff', |
|||
'降落': 'drone.land', |
|||
'一键返航': 'drone.return', |
|||
'悬停': 'drone.hover', |
|||
'紧急停止': 'drone.emergency_stop', |
|||
'开始任务': 'drone.mission_start', |
|||
'暂停任务': 'drone.mission_pause', |
|||
'继续任务': 'drone.mission_resume', |
|||
'取消任务': 'drone.mission_cancel' |
|||
} |
|||
|
|||
// 经纬度 → 地图百分比坐标(按设备经纬范围动态投影,北向上)
|
|||
function project(lon, lat, bounds) { |
|||
const lonSpan = bounds.maxLon - bounds.minLon || 1 |
|||
const latSpan = bounds.maxLat - bounds.minLat || 1 |
|||
const x = ((lon - bounds.minLon) / lonSpan) * 80 + 10 |
|||
const y = ((bounds.maxLat - lat) / latSpan) * 70 + 15 |
|||
return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 } |
|||
} |
|||
|
|||
function dockStatusFromDetail(registerStatus, online, rt) { |
|||
const pending = registerStatus === 'pending' && !online |
|||
if (pending) return { status: '待连接', statusClass: 'pending' } |
|||
if (!online) return DOCK_STATUS.offline |
|||
if (alarmCodes(rt).length) return { status: '告警', statusClass: 'alarm' } |
|||
return DOCK_STATUS.online |
|||
} |
|||
|
|||
function droneStatusFromDetail(online, rt) { |
|||
const armed = ['1', 'true', 1, true].includes(rt.armed) |
|||
const altitude = coordinate(rt.altitude, -1000, 100000) |
|||
const flying = armed || (altitude != null && altitude > 0) |
|||
if (!online) return { status: '离线', statusClass: 'offline', altitude, flying } |
|||
if (flying) return { status: '飞行中', statusClass: 'mission', altitude, flying } |
|||
return { status: '在巢', statusClass: 'online', altitude, flying } |
|||
} |
|||
|
|||
|
|||
export const useDeviceStore = defineStore('devices', () => { |
|||
const assets = reactive({}) |
|||
const docks = ref([]) |
|||
const drones = ref([]) |
|||
const loaded = ref(false) |
|||
|
|||
function asset(id) { |
|||
return assets[id] || null |
|||
} |
|||
|
|||
function record(id) { |
|||
const key = String(id) |
|||
return docks.value.find((d) => d.id === key) || drones.value.find((d) => d.id === key) || null |
|||
} |
|||
|
|||
function commandType(title) { |
|||
return COMMANDS[title] || '' |
|||
} |
|||
|
|||
async function sendCommand(rec, title, params = {}) { |
|||
const type = COMMANDS[title] |
|||
if (!type) throw new Error('未知指令') |
|||
if (rec.kind === 'dock') { |
|||
return request.post(urls.DOCK_COMMAND(rec.id), { type, params }) |
|||
} |
|||
const dock = docks.value.find((d) => d.dockId === rec.dockId) |
|||
if (!dock) throw new Error('未找到绑定机巢') |
|||
return request.post(urls.DOCK_COMMAND(dock.id), { type, params, droneSn: rec.droneSn }) |
|||
} |
|||
|
|||
async function load(options = {}) { |
|||
const silent = Boolean(options.silent) |
|||
const reqOpts = silent ? { skipErrorToast: true } : {} |
|||
const [dockPage, dronePage] = await Promise.all([ |
|||
request.get(urls.DOCKS, { params: { pageNum: 1, pageSize: 100 }, ...reqOpts }), |
|||
request.get(urls.DRONES, { params: { pageNum: 1, pageSize: 100 }, ...reqOpts }) |
|||
]) |
|||
|
|||
const dockList = dockPage?.records || [] |
|||
const droneList = dronePage?.records || [] |
|||
const [dockDetails, droneDetails] = await Promise.all([ |
|||
Promise.all(dockList.map((d) => request.get(urls.DOCK(d.id), reqOpts).then((r) => r || {}).catch(() => ({})))), |
|||
Promise.all(droneList.map((d) => request.get(urls.DRONE(d.id), reqOpts).then((r) => r || {}).catch(() => ({})))) |
|||
]) |
|||
const coords = dockList |
|||
.map((d) => ({ longitude: coordinate(d.longitude, -180, 180), latitude: coordinate(d.latitude, -90, 90) })) |
|||
.filter((d) => d.longitude != null && d.latitude != null) |
|||
const defaultLon = coords[0]?.longitude ?? 0 |
|||
const defaultLat = coords[0]?.latitude ?? 0 |
|||
const bounds = { |
|||
minLon: Math.min(...coords.map((d) => Number(d.longitude)), defaultLon), |
|||
maxLon: Math.max(...coords.map((d) => Number(d.longitude)), defaultLon), |
|||
minLat: Math.min(...coords.map((d) => Number(d.latitude)), defaultLat), |
|||
maxLat: Math.max(...coords.map((d) => Number(d.latitude)), defaultLat) |
|||
} |
|||
if (bounds.minLon === bounds.maxLon) { bounds.minLon -= 0.01; bounds.maxLon += 0.01 } |
|||
if (bounds.minLat === bounds.maxLat) { bounds.minLat -= 0.01; bounds.maxLat += 0.01 } |
|||
|
|||
const newDocks = dockList.map((d, i) => { |
|||
const id = String(d.id) |
|||
const detail = dockDetails[i] || {} |
|||
const rt = detail.realtime || {} |
|||
const online = Boolean(detail.online) |
|||
const registerStatus = d.registerStatus || 'registered' |
|||
const st = dockStatusFromDetail(registerStatus, online, rt) |
|||
const name = d.name || d.dockId |
|||
const environment = environmentValues(rt) |
|||
return { |
|||
id, |
|||
kind: 'dock', |
|||
dockId: d.dockId, |
|||
name, |
|||
code: d.code || d.sn || d.dockId, |
|||
sn: d.sn || d.dockId || '--', |
|||
iccid: '--', |
|||
longitude: coordinate(d.longitude, -180, 180), |
|||
latitude: coordinate(d.latitude, -90, 90), |
|||
location: d.location || '--', |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
binding: '--', |
|||
bindingState: 'unbound', |
|||
mode: controlModeText(rt.controlMode), |
|||
version: d.softwareVer ? `v${d.softwareVer}` : '--', |
|||
updated: fmtTime(d.updatedAt), |
|||
protocol: d.protocolVer ? `MQTT v${d.protocolVer}` : '--', |
|||
registerStatus, |
|||
realtime: rt, |
|||
environment, |
|||
alarms: alarmCodes(rt), |
|||
online |
|||
} |
|||
}) |
|||
|
|||
const newDrones = droneList.map((d, i) => { |
|||
const id = String(d.id) |
|||
const detail = droneDetails[i] || {} |
|||
const rt = detail.realtime || {} |
|||
const online = Boolean(detail.online) |
|||
const st = droneStatusFromDetail(online, rt) |
|||
const altitude = st.altitude |
|||
const dock = newDocks.find((dk) => dk.dockId === d.dockId) |
|||
const battery = rt.batteryPercent ?? rt.batteryPct ?? d.battery |
|||
return { |
|||
id, |
|||
kind: 'drone', |
|||
droneSn: d.droneSn, |
|||
dockId: d.dockId, |
|||
name: d.name || d.droneSn, |
|||
code: d.code || d.droneSn, |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
binding: dock?.name || '--', |
|||
bindingState: dock ? 'bound' : 'unbound', |
|||
battery: battery != null && battery !== '' ? `${battery}%` : '--', |
|||
mode: modeText(rt.flightMode), |
|||
mission: displayValue(rt.missionName || rt.mission), |
|||
updated: fmtTime(d.updatedAt), |
|||
sysid: displayValue(rt.currentSysId), |
|||
gps: gpsText(rt.gpsQuality), |
|||
sn: d.droneSn || d.code || '--', |
|||
model: d.model || '--', |
|||
firmwareVer: d.firmwareVer || '--', |
|||
localRoutes: [], |
|||
realtime: rt, |
|||
alarms: alarmCodes(rt), |
|||
online, |
|||
_lat: coordinate(rt.latitude, -90, 90), |
|||
_lon: coordinate(rt.longitude, -180, 180), |
|||
_alt: altitude, |
|||
_speed: coordinate(rt.groundSpeed, 0, 1000), |
|||
_sats: coordinate(rt.satellites, 0, 1000) |
|||
} |
|||
}) |
|||
|
|||
for (const dk of newDocks) { |
|||
const dr = newDrones.find((d) => d.dockId === dk.dockId) |
|||
if (dr) { |
|||
dk.binding = dr.name |
|||
dk.bindingState = 'bound' |
|||
} |
|||
} |
|||
|
|||
docks.value = newDocks |
|||
drones.value = newDrones |
|||
|
|||
// 重建地图资产
|
|||
Object.keys(assets).forEach((k) => delete assets[k]) |
|||
for (const dk of newDocks) { |
|||
const hasCoordinates = dk.longitude != null && dk.latitude != null |
|||
const p = hasCoordinates ? project(dk.longitude, dk.latitude, bounds) : { x: null, y: null } |
|||
assets[dk.id] = { |
|||
id: dk.id, type: 'dock', name: dk.name, location: dk.location, |
|||
status: dk.status, statusClass: dk.statusClass, x: p.x, y: p.y, hasCoordinates, |
|||
longitude: hasCoordinates ? dk.longitude : null, |
|||
latitude: hasCoordinates ? dk.latitude : null, |
|||
realtime: dk.realtime, environment: dk.environment, alarms: dk.alarms, online: dk.online |
|||
} |
|||
} |
|||
for (const dr of newDrones) { |
|||
const dock = newDocks.find((d) => d.dockId === dr.dockId) |
|||
const lon = dr._lon != null ? dr._lon : dock?.longitude |
|||
const lat = dr._lat != null ? dr._lat : dock?.latitude |
|||
const hasCoordinates = lon != null && lat != null |
|||
const p = hasCoordinates ? project(lon, lat, bounds) : { x: null, y: null } |
|||
assets[dr.id] = { |
|||
id: dr.id, type: 'drone', name: dr.name, |
|||
location: dr.statusClass === 'mission' ? `${dock?.location || '--'} · 飞行中` : `停放于${dock?.name || '--'}`, |
|||
status: dr.status, statusClass: dr.statusClass, x: p.x, y: p.y, hasCoordinates, |
|||
longitude: hasCoordinates ? lon : null, |
|||
latitude: hasCoordinates ? lat : null, |
|||
parent: dock?.id || '', parentName: dock?.name || '--', |
|||
inDock: dr.statusClass !== 'mission', altitude: dr._alt, speed: dr._speed, satellites: dr._sats, |
|||
realtime: dr.realtime, alarms: dr.alarms, online: dr.online |
|||
} |
|||
} |
|||
|
|||
loaded.value = true |
|||
return { docks: newDocks, drones: newDrones } |
|||
} |
|||
|
|||
function applyDockDetail(id, detail) { |
|||
const key = String(id) |
|||
const dock = docks.value.find((d) => d.id === key) |
|||
if (!dock) { |
|||
console.warn('[devices] applyDockDetail missing dock', key) |
|||
return false |
|||
} |
|||
const rt = detail?.realtime || {} |
|||
const online = Boolean(detail?.online) |
|||
const st = dockStatusFromDetail(dock.registerStatus || 'registered', online, rt) |
|||
const environment = environmentValues(rt) |
|||
const alarms = alarmCodes(rt) |
|||
Object.assign(dock, { |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
mode: controlModeText(rt.controlMode), |
|||
realtime: rt, |
|||
environment, |
|||
alarms, |
|||
online, |
|||
}) |
|||
const assetRow = assets[key] |
|||
if (assetRow) { |
|||
Object.assign(assetRow, { |
|||
status: dock.status, |
|||
statusClass: dock.statusClass, |
|||
realtime: rt, |
|||
environment, |
|||
alarms, |
|||
online, |
|||
}) |
|||
} |
|||
return true |
|||
} |
|||
|
|||
function applyDroneDetail(id, detail) { |
|||
const key = String(id) |
|||
const drone = drones.value.find((d) => d.id === key) |
|||
if (!drone) { |
|||
console.warn('[devices] applyDroneDetail missing drone', key) |
|||
return false |
|||
} |
|||
const rt = detail?.realtime || {} |
|||
const online = Boolean(detail?.online) |
|||
const st = droneStatusFromDetail(online, rt) |
|||
const altitude = st.altitude |
|||
const battery = rt.batteryPercent ?? rt.batteryPct |
|||
const alarms = alarmCodes(rt) |
|||
const dock = docks.value.find((d) => d.dockId === drone.dockId) |
|||
Object.assign(drone, { |
|||
status: st.status, |
|||
statusClass: st.statusClass, |
|||
battery: battery != null && battery !== '' ? `${battery}%` : drone.battery, |
|||
mode: modeText(rt.flightMode), |
|||
mission: displayValue(rt.missionName || rt.mission), |
|||
sysid: displayValue(rt.currentSysId), |
|||
gps: gpsText(rt.gpsQuality), |
|||
realtime: rt, |
|||
alarms, |
|||
online, |
|||
_lat: coordinate(rt.latitude, -90, 90), |
|||
_lon: coordinate(rt.longitude, -180, 180), |
|||
_alt: altitude, |
|||
_speed: coordinate(rt.groundSpeed, 0, 1000), |
|||
_sats: coordinate(rt.satellites, 0, 1000), |
|||
}) |
|||
const assetRow = assets[key] |
|||
if (assetRow) { |
|||
const lon = drone._lon != null ? drone._lon : dock?.longitude |
|||
const lat = drone._lat != null ? drone._lat : dock?.latitude |
|||
const hasCoordinates = lon != null && lat != null |
|||
Object.assign(assetRow, { |
|||
status: drone.status, |
|||
statusClass: drone.statusClass, |
|||
location: drone.statusClass === 'mission' |
|||
? `${dock?.location || '--'} · 飞行中` |
|||
: `停放于${dock?.name || '--'}`, |
|||
hasCoordinates, |
|||
longitude: hasCoordinates ? lon : null, |
|||
latitude: hasCoordinates ? lat : null, |
|||
inDock: drone.statusClass !== 'mission', |
|||
altitude: drone._alt, |
|||
speed: drone._speed, |
|||
satellites: drone._sats, |
|||
realtime: rt, |
|||
alarms, |
|||
online, |
|||
}) |
|||
} |
|||
return true |
|||
} |
|||
|
|||
return { assets, docks, drones, loaded, asset, record, commandType, sendCommand, load, applyDockDetail, applyDroneDetail } |
|||
}) |
|||
|
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue