From 5ad820af0a842ad3bf88b2b866baac0e1dbc531e Mon Sep 17 00:00:00 2001 From: root Date: Sat, 5 Sep 2026 17:40:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E9=80=9A=E4=BF=A1?= =?UTF-8?q?=E5=8D=A1=E4=B8=8E=E8=BF=90=E8=90=A5=E5=A5=97=E9=A4=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DockEditModal.vue | 227 +- src/config/urls.js | 197 +- src/stores/modules/devicesStore.js | 840 +++---- src/views/AccountView/AccountView.vue | 2142 +++++++++-------- .../DeviceDetailView/DeviceDetailView.vue | 1442 +++++------ src/views/DevicesView/DevicesView.vue | 1658 ++++++------- src/views/TrafficOpsView/TrafficOpsView.vue | 2048 ++++++++-------- 7 files changed, 4318 insertions(+), 4236 deletions(-) diff --git a/src/components/DockEditModal.vue b/src/components/DockEditModal.vue index f6e420a..c7f8efe 100644 --- a/src/components/DockEditModal.vue +++ b/src/components/DockEditModal.vue @@ -1,114 +1,113 @@ - - - - - + + + + + diff --git a/src/config/urls.js b/src/config/urls.js index 789ea5c..54999a2 100644 --- a/src/config/urls.js +++ b/src/config/urls.js @@ -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' diff --git a/src/stores/modules/devicesStore.js b/src/stores/modules/devicesStore.js index 29d4ee4..ac4c210 100644 --- a/src/stores/modules/devicesStore.js +++ b/src/stores/modules/devicesStore.js @@ -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 } +}) diff --git a/src/views/AccountView/AccountView.vue b/src/views/AccountView/AccountView.vue index 1f37d27..1a07928 100644 --- a/src/views/AccountView/AccountView.vue +++ b/src/views/AccountView/AccountView.vue @@ -1,1056 +1,1088 @@ - - - - - - - + + \ No newline at end of file diff --git a/src/views/DeviceDetailView/DeviceDetailView.vue b/src/views/DeviceDetailView/DeviceDetailView.vue index 3f64aba..4c31009 100644 --- a/src/views/DeviceDetailView/DeviceDetailView.vue +++ b/src/views/DeviceDetailView/DeviceDetailView.vue @@ -1,721 +1,721 @@ - - - - - + + + + + diff --git a/src/views/DevicesView/DevicesView.vue b/src/views/DevicesView/DevicesView.vue index b66eb5c..4833643 100644 --- a/src/views/DevicesView/DevicesView.vue +++ b/src/views/DevicesView/DevicesView.vue @@ -1,829 +1,829 @@ - - - - - - - + + + + + + + diff --git a/src/views/TrafficOpsView/TrafficOpsView.vue b/src/views/TrafficOpsView/TrafficOpsView.vue index d9d7cb1..462bcec 100644 --- a/src/views/TrafficOpsView/TrafficOpsView.vue +++ b/src/views/TrafficOpsView/TrafficOpsView.vue @@ -1,1001 +1,1047 @@ - - - - - - - + + + + + + +