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 @@ - - - - 机巢名称* - 机巢 SN - 通信卡 ICCID - - 经度* - 纬度* - - 设备位置选填 - {{ error }} - - - 取消 - {{ saving ? '保存中…' : '保存修改' }} - - - - - - - + + + + 机巢名称* + 机巢 SN + + 经度* + 纬度* + + 设备位置选填 + {{ error }} + + + 取消 + {{ saving ? '保存中…' : '保存修改' }} + + + + + + + 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 @@ - - - 账户中心管理云媒体流量、通信卡和账户资料 - - - 账户{{ profileName }}{{ accountStatus }} - 云媒体流量{{ balance }}GB - 通信卡{{ simTotal }}张 - 接入设备{{ deviceCount }}{{ deviceLoadFailed ? '加载失败' : '机巢 + 无人机' }} - - - - - - - - - - - - - - - 账户资料以账户资料为准 - - {{ profileInitial }} - {{ profileName }}账户角色 · {{ profileRole }} - 编辑 - - - 账户名称{{ profileName }} - 联系电话{{ profilePhone }} - 邮箱{{ profileEmail }} - 注册时间{{ createdAt }} - - - - 资源概览实时账户数据 - - 云媒体流量 {{ balance }} GB - 通信卡 {{ activeSimCount }} 张正常 / {{ simTotal }} 张 - 接入设备 {{ resourceSummary ? `${resourceSummary.dockCount} 个机巢 / ${resourceSummary.droneCount} 架无人机` : deviceCount }} - 视频资产 {{ resourceSummary?.videoCount ?? '--' }} 个 / 本月下载 {{ resourceSummary?.monthDownloadCount ?? '--' }} 次 - 本月媒体流量 {{ resourceSummary ? `${Number(resourceSummary.monthUsageGb).toFixed(2)} GB` : '--' }} - - 查看流量订单管理通信卡 - - - - - - 当前云媒体流量余额{{ balance }} GB流量用于直播、回放与原始素材下载 - - - - 选择流量套餐 - - - - {{ pkg.name }} - {{ (Number(pkg.amountBytes) / 1024 / 1024 / 1024).toFixed(0) }} GB - {{ money(pkg.price) }} - 有效期 {{ pkg.validityDays }} 天 - 长期有效 - - - - 暂无可用套餐 - - - - 订单概要 - 云媒体流量订单 - {{ money(selectedTrafficPackage.price) }} - 创建订单后发起微信支付;金额以服务端订单为准。 - 购买并支付 - - - 订单记录共 {{ orderTotal }} 条 - - {{ orderContentLabel(row) }} - {{ money(row.unitPrice) }} - {{ money(row.totalPrice) }} - {{ payStatusName(row.payStatus) }} - {{ fmtTime(row.createdAt) }} - - 去支付 - 取消 - -- - - - - 消费明细共 {{ usageTotal }} 条 - - {{ sourceName(row.sourceType) }} - {{ row.sourceId || '--' }} - {{ fmtGb(row.bytesUsed) }} - {{ fmtGb(row.balanceBefore) }} - {{ fmtGb(row.balanceAfter) }} - {{ fmtTime(row.createdAt) }} - - - 下载记录共 {{ downloadTotal }} 条 - - {{ row.fileName || '--' }} - {{ row.droneSn || '--' }} - {{ row.executionId || '--' }} - {{ fmtGb(row.bytes) }} - {{ fmtTime(row.createdAt) }} - - - - - {{ simTotal }}通信卡总数{{ activeSimCount }}正常{{ warnSimCount }}异常或未同步 - - {{ row.carrier || '--' }} - {{ row.iccid || '--' }}{{ row.phone || '--' }} - {{ row.dockId || '--' }} - {{ row.lastSyncAt ? `${Number(row.usedGb || 0).toFixed(1)} / ${row.planGb || 0} GB` : '暂无运营商同步数据' }} - {{ simStatus(row).name }} - {{ fmtTime(row.lastSyncAt) }} - - 充值 - - - - - - 待支付通信卡充值 - - {{ cardLabel(order.simCardId) }} - · {{ order.amountGb != null ? `${order.amountGb} GB` : '--' }} - · {{ moneyFromFen(order.totalFeeFen) }} - · {{ payStatusName(order.paymentStatus) }} - - - - 去支付 - 取消 - - - 通信卡充值记录共 {{ simRechargeTotal }} 条 - - {{ cardLabel(row.simCardId) }} - {{ row.amountGb }} GB - {{ money(row.totalPrice) }} - {{ payStatusName(row.payStatus || row.paymentStatus) }} - {{ rechargeStatusName(row.rechargeStatus) }} - {{ fmtTime(row.createdAt) }} - - - - - - 账号安全修改密码后将退出所有设备 - - 登录手机号{{ maskPhone(profilePhone) }} - 绑定邮箱{{ profileEmail }} - 登录会话共 {{ sessions.length }} 个有效会话退出其他设备 - - - {{ row.userAgent || '未知设备' }}{{ row.current ? '(当前)' : '' }} - {{ row.ip || '--' }} - {{ fmtTime(row.lastActiveAt) }} - {{ fmtTime(row.expiresAt) }} - - 退出 - 当前会话 - - - 修改密码 - - - - 发票资料与申请仅对已人工确认的流量订单开放申请 - - - 发票抬头 - 纳税人识别号 - 接收邮箱 - 保存默认发票资料 - - - 可申请订单 - - 提交开具发票 - - - 发票记录仅展示已提交的开具申请 - - {{ row.orderId }} - {{ money(row.amount) }} - {{ row.title }} - {{ invoiceStatusName(row.status) }} - {{ fmtTime(row.requestedAt) }} - - - - - - 账户名称 - 邮箱 - - 取消 - {{ saving ? '保存中…' : '保存' }} - - - - 当前密码 - 新密码 - - 取消 - 确认修改 - - - - ICCID:{{ simRechargeCard?.iccid || '--' }} - - 选择套餐 - - - - {{ pkg.name }} - {{ pkg.amountGb }} GB - {{ moneyFromFen(pkg.saleFeeFen) }} - 有效期 {{ pkg.validityMonths }} 个月 - 按运营商规则 - - - - 暂无可用通信卡套餐 - - - 取消 - 创建并支付 - - - - - - 应付金额 - {{ payDialog.amountText }} - - - 业务{{ payDialog.businessType === 'sim_recharge_order' ? '通信卡充值' : '云媒体流量' }} - 订单号{{ payDialog.businessOrderId || '--' }} - 支付单{{ payDialog.transactionId || '--' }} - 状态{{ payDialog.statusText || '--' }} - - - - 请使用微信扫码支付 - - {{ payDialog.error || '暂无二维码' }} - - - 关闭 - 我已支付,刷新状态 - - - - - - - - - - + + \ 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 @@ - - - - - 返回设备列表 - - - - {{ isDrone ? '无人机设备' : '机巢设备' }} - {{ record.name }} - {{ record.code }} - - {{ record.status }} - - - 编辑信息 - 远程控制 - 实时监控 - - - - - - - - - - - - - - - 控制模式{{ record.mode }} - 无人机在位{{ booleanText(rt('dronePresent')) }} - 舱门状态{{ doorSummary }} - 充电状态{{ chargeText(rt('chargingState')) }} - 当前告警{{ record.alarms?.length ?? 0 }} - 最近上报{{ record.updated }} - - - - - 机巢运行状态全部状态 - - - - 充电系统 - - 充电状态{{ chargeText(rt('chargingState')) }} - 充电电压{{ withUnit(rt('chargingVoltage'), ' V') }} - 充电电流{{ withUnit(rt('chargingCurrent'), ' A') }} - 无人机电量{{ boundDrone?.battery || '--' }} - 无人机电源{{ booleanText(rt('dronePowerOnComplete')) }} - 充电启动反馈{{ completeText(rt('chargeStartComplete')) }} - - - - 环境监测 - - - - - - 绑定无人机 - - - - {{ boundDrone?.name || record.binding }} - {{ boundDrone ? `${boundDrone.status} · 电量 ${boundDrone.battery}` : (record.bindingState === 'bound' ? '绑定关系正常' : '当前未绑定设备') }} - - {{ boundDrone.status }} - - - 飞控 SN{{ boundDrone?.sn || '--' }} - 定位状态{{ boundDrone?.gps || '--' }} - 当前任务{{ boundDrone?.mission || '--' }} - - - - 最近流程 - - - {{ latestLog.commandName }}{{ latestLog.createdAt }} - {{ latestLog.statusName }} - - - - 设备信息编辑 - - 机巢 SN{{ record.sn }} - 通信卡 ICCID{{ record.iccid }} - 设备位置{{ record.location }} - 经纬度{{ coordinateText }} - 软件版本{{ record.version }} - - - - - - - - 飞行状态{{ record.status }} - 剩余电量{{ percentValue(record.battery) }} - 相对高度{{ withUnit(asset?.altitude, ' m') }} - 地速{{ withUnit(asset?.speed, ' m/s') }} - 卫星数量{{ displayValue(asset?.satellites) }} - 链路质量{{ withUnit(rt('linkQuality'), '%') }} - - - - - 飞行数据 - - 飞行模式{{ record.mode }} - 解锁状态{{ armedText }} - GPS 状态{{ record.gps }} - 电池电压{{ withUnit(rt('batteryVoltage'), ' V') }} - 航向角{{ withUnit(rt('yaw'), '°') }} - 返航点{{ homeSetText }} - - - - 当前任务 - - {{ record.mission && record.mission !== '--' ? record.mission : '暂无任务' }}{{ record.binding }} - - - - - - 所属机巢 - - - {{ boundDock?.name || record.binding }}{{ record.bindingState === 'bound' ? '绑定关系正常' : '当前未绑定设备' }} - {{ boundDock.status }} - - - - 实时位置 - - 经度{{ displayValue(rt('longitude')) }} - 纬度{{ displayValue(rt('latitude')) }} - Roll{{ withUnit(rt('roll'), '°') }} - Pitch{{ withUnit(rt('pitch'), '°') }} - Yaw{{ withUnit(rt('yaw'), '°') }} - - - - - - - - - - - - 舱门与归中机构 - - 左舱门{{ doorText('left') }}关门反馈:{{ completeText(rt('doorCloseComplete')) }} - 右舱门{{ doorText('right') }}开门反馈:{{ completeText(rt('doorOpenComplete')) }} - 左右归中{{ centeringText('leftRight') }}夹紧反馈:{{ completeText(rt('centeringTightComplete')) }} - 前后归中{{ centeringText('frontBack') }}松开反馈:{{ completeText(rt('centeringLooseComplete')) }} - - - - 流程反馈 - - 起飞准备{{ completeText(rt('takeoffPreparationComplete')) }}takeoffPreparationComplete - 起飞收尾{{ completeText(rt('hangarActionComplete')) }}hangarActionComplete - 降落准备{{ completeText(rt('landingPreparationComplete')) }}landingPreparationComplete - 降落收尾{{ completeText(rt('landingExecutionComplete')) }}landingExecutionComplete - 整机复位{{ completeText(rt('resetComplete')) }}resetComplete - 急停状态{{ isTrue(rt('emergencyStop')) ? '已触发' : isFalse(rt('emergencyStop')) ? '未触发' : '--' }}emergencyStop - - - - - - - - 飞控状态 - - 在线状态{{ record.online ? '在线' : '离线' }}{{ record.updated }} - 解锁状态{{ armedText }}armed - 飞行模式{{ record.mode }}mode code:{{ displayValue(rt('flightModeCode')) }} - 返航点{{ homeSetText }}homeSet - - - - 任务状态 - - 任务状态{{ record.statusClass === 'mission' ? '执行中' : displayValue(record.mission) }}{{ record.status }} - 飞控 SN{{ record.sn }}已同步 - 卫星数量{{ displayValue(asset?.satellites) }}GPS {{ record.gps }} - 链路质量{{ withUnit(rt('linkQuality'), '%') }}MAVLink - - - - - - - - - 告警记录当前设备 - - - - - {{ row.levelName }} - - - {{ row.statusName }} - - - - - - - 操作记录远程控制与配置变更 - 刷新 - - - - {{ row.result }} - - - - - - - - - 边缘服务 - - MQTT 连接{{ record.online ? '正常' : '离线' }}{{ record.updated }} - PLC / Modbus{{ booleanText(rt('plcConnected')) === '是' ? '正常' : booleanText(rt('plcConnected')) }}plcConnected - MAVLink--未单独上报 - 视频服务--见直播状态 - - - - 软件与身份 - - 边缘版本{{ record.version }} - 协议版本{{ record.protocol }} - 设备连接{{ record.status }} - 最近更新{{ record.updated }} - 告警数量{{ record.alarms?.length ?? 0 }} - PLC 连接{{ booleanText(rt('plcConnected')) }} - - - - - - - - 飞控通信 - - MAVLink 心跳{{ record.online ? '正常' : '离线' }}{{ record.updated }} - 遥测数据{{ record.online ? '正常' : '--' }}最近 {{ record.updated }} - 飞控序列号{{ record.sn && record.sn !== '--' ? '已识别' : '--' }}{{ record.sn }} - - - - 飞控信息 - - 当前 SysID{{ record.sysid }} - 飞控版本{{ record.firmwareVer }} - 机型{{ record.model }} - 链路质量{{ withUnit(rt('linkQuality'), '%') }} - - - - - - - - - - - 未找到该设备 - 返回设备列表 - - - - - - - + + + + + 返回设备列表 + + + + {{ isDrone ? '无人机设备' : '机巢设备' }} + {{ record.name }} + {{ record.code }} + + {{ record.status }} + + + 编辑信息 + 远程控制 + 实时监控 + + + + + + + + + + + + + + + 控制模式{{ record.mode }} + 无人机在位{{ booleanText(rt('dronePresent')) }} + 舱门状态{{ doorSummary }} + 充电状态{{ chargeText(rt('chargingState')) }} + 当前告警{{ record.alarms?.length ?? 0 }} + 最近上报{{ record.updated }} + + + + + 机巢运行状态全部状态 + + + + 充电系统 + + 充电状态{{ chargeText(rt('chargingState')) }} + 充电电压{{ withUnit(rt('chargingVoltage'), ' V') }} + 充电电流{{ withUnit(rt('chargingCurrent'), ' A') }} + 无人机电量{{ boundDrone?.battery || '--' }} + 无人机电源{{ booleanText(rt('dronePowerOnComplete')) }} + 充电启动反馈{{ completeText(rt('chargeStartComplete')) }} + + + + 环境监测 + + + + + + 绑定无人机 + + + + {{ boundDrone?.name || record.binding }} + {{ boundDrone ? `${boundDrone.status} · 电量 ${boundDrone.battery}` : (record.bindingState === 'bound' ? '绑定关系正常' : '当前未绑定设备') }} + + {{ boundDrone.status }} + + + 飞控 SN{{ boundDrone?.sn || '--' }} + 定位状态{{ boundDrone?.gps || '--' }} + 当前任务{{ boundDrone?.mission || '--' }} + + + + 最近流程 + + + {{ latestLog.commandName }}{{ latestLog.createdAt }} + {{ latestLog.statusName }} + + + + 设备信息编辑 + + 机巢 SN{{ record.sn }} + 通信卡 ICCID{{ record.iccid }} + 设备位置{{ record.location }} + 经纬度{{ coordinateText }} + 软件版本{{ record.version }} + + + + + + + + 飞行状态{{ record.status }} + 剩余电量{{ percentValue(record.battery) }} + 相对高度{{ withUnit(asset?.altitude, ' m') }} + 地速{{ withUnit(asset?.speed, ' m/s') }} + 卫星数量{{ displayValue(asset?.satellites) }} + 链路质量{{ withUnit(rt('linkQuality'), '%') }} + + + + + 飞行数据 + + 飞行模式{{ record.mode }} + 解锁状态{{ armedText }} + GPS 状态{{ record.gps }} + 电池电压{{ withUnit(rt('batteryVoltage'), ' V') }} + 航向角{{ withUnit(rt('yaw'), '°') }} + 返航点{{ homeSetText }} + + + + 当前任务 + + {{ record.mission && record.mission !== '--' ? record.mission : '暂无任务' }}{{ record.binding }} + + + + + + 所属机巢 + + + {{ boundDock?.name || record.binding }}{{ record.bindingState === 'bound' ? '绑定关系正常' : '当前未绑定设备' }} + {{ boundDock.status }} + + + + 实时位置 + + 经度{{ displayValue(rt('longitude')) }} + 纬度{{ displayValue(rt('latitude')) }} + Roll{{ withUnit(rt('roll'), '°') }} + Pitch{{ withUnit(rt('pitch'), '°') }} + Yaw{{ withUnit(rt('yaw'), '°') }} + + + + + + + + + + + + 舱门与归中机构 + + 左舱门{{ doorText('left') }}关门反馈:{{ completeText(rt('doorCloseComplete')) }} + 右舱门{{ doorText('right') }}开门反馈:{{ completeText(rt('doorOpenComplete')) }} + 左右归中{{ centeringText('leftRight') }}夹紧反馈:{{ completeText(rt('centeringTightComplete')) }} + 前后归中{{ centeringText('frontBack') }}松开反馈:{{ completeText(rt('centeringLooseComplete')) }} + + + + 流程反馈 + + 起飞准备{{ completeText(rt('takeoffPreparationComplete')) }}takeoffPreparationComplete + 起飞收尾{{ completeText(rt('hangarActionComplete')) }}hangarActionComplete + 降落准备{{ completeText(rt('landingPreparationComplete')) }}landingPreparationComplete + 降落收尾{{ completeText(rt('landingExecutionComplete')) }}landingExecutionComplete + 整机复位{{ completeText(rt('resetComplete')) }}resetComplete + 急停状态{{ isTrue(rt('emergencyStop')) ? '已触发' : isFalse(rt('emergencyStop')) ? '未触发' : '--' }}emergencyStop + + + + + + + + 飞控状态 + + 在线状态{{ record.online ? '在线' : '离线' }}{{ record.updated }} + 解锁状态{{ armedText }}armed + 飞行模式{{ record.mode }}mode code:{{ displayValue(rt('flightModeCode')) }} + 返航点{{ homeSetText }}homeSet + + + + 任务状态 + + 任务状态{{ record.statusClass === 'mission' ? '执行中' : displayValue(record.mission) }}{{ record.status }} + 飞控 SN{{ record.sn }}已同步 + 卫星数量{{ displayValue(asset?.satellites) }}GPS {{ record.gps }} + 链路质量{{ withUnit(rt('linkQuality'), '%') }}MAVLink + + + + + + + + + 告警记录当前设备 + + + + + {{ row.levelName }} + + + {{ row.statusName }} + + + + + + + 操作记录远程控制与配置变更 + 刷新 + + + + {{ row.result }} + + + + + + + + + 边缘服务 + + MQTT 连接{{ record.online ? '正常' : '离线' }}{{ record.updated }} + PLC / Modbus{{ booleanText(rt('plcConnected')) === '是' ? '正常' : booleanText(rt('plcConnected')) }}plcConnected + MAVLink--未单独上报 + 视频服务--见直播状态 + + + + 软件与身份 + + 边缘版本{{ record.version }} + 协议版本{{ record.protocol }} + 设备连接{{ record.status }} + 最近更新{{ record.updated }} + 告警数量{{ record.alarms?.length ?? 0 }} + PLC 连接{{ booleanText(rt('plcConnected')) }} + + + + + + + + 飞控通信 + + MAVLink 心跳{{ record.online ? '正常' : '离线' }}{{ record.updated }} + 遥测数据{{ record.online ? '正常' : '--' }}最近 {{ record.updated }} + 飞控序列号{{ record.sn && record.sn !== '--' ? '已识别' : '--' }}{{ record.sn }} + + + + 飞控信息 + + 当前 SysID{{ record.sysid }} + 飞控版本{{ record.firmwareVer }} + 机型{{ record.model }} + 链路质量{{ withUnit(rt('linkQuality'), '%') }} + + + + + + + + + + + 未找到该设备 + 返回设备列表 + + + + + + + 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 @@ - - - - - 设备管理 - {{ updatedLabel }} - - - - 添加机巢 - - - - - 机巢总数{{ overview.docks }}台 - 在线设备{{ overview.online }}台 - 执行任务{{ overview.mission }}台 - 当前告警{{ overview.alarm }}台 - 绑定无人机{{ overview.boundDrones }}架 - - - - - - - - - - - - - - - - - - 重置 - - - - - - - - - - - {{ row.name }}{{ row.code }} - - - - {{ row.status }} - - - - {{ row.binding }} - - - - - - 固件升级 - 远程控制 - 详情 - 删除 - - - 航线上传 - 远程控制 - 详情 - - - - - - - - - - - - - - - - 机巢名称* - 机巢 SN* - 通信卡 ICCID* - - 经度* - 纬度* - - 设备位置选填 - {{ addDockError }} - - - 取消 - 确认添加 - - - - - - - - - - - {{ firmwareRecord.name }} - 当前版本 {{ firmwareRecord.version }} - - {{ firmwareRecord.status }} - - - 目标固件版本 - - - - 升级前请确认设备状态 - - 升级期间机巢将暂时停止远程控制和视频服务,请确认当前没有飞行任务。升级完成后设备会自动重启并恢复连接。 - {{ firmwareError }} - - - 取消 - {{ upgrading ? '下发中…' : '开始升级' }} - - - - - - - - - - + + + + + 设备管理 + {{ updatedLabel }} + + + + 添加机巢 + + + + + 机巢总数{{ overview.docks }}台 + 在线设备{{ overview.online }}台 + 执行任务{{ overview.mission }}台 + 当前告警{{ overview.alarm }}台 + 绑定无人机{{ overview.boundDrones }}架 + + + + + + + + + + + + + + + + + + 重置 + + + + + + + + + + + {{ row.name }}{{ row.code }} + + + + {{ row.status }} + + + + {{ row.binding }} + + + + + + 固件升级 + 远程控制 + 详情 + 删除 + + + 航线上传 + 远程控制 + 详情 + + + + + + + + + + + + + + + + 机巢名称* + 机巢 SN* + 通信卡 ICCID* + + 经度* + 纬度* + + 设备位置选填 + {{ addDockError }} + + + 取消 + 确认添加 + + + + + + + + + + + {{ firmwareRecord.name }} + 当前版本 {{ firmwareRecord.version }} + + {{ firmwareRecord.status }} + + + 目标固件版本 + + + + 升级前请确认设备状态 + + 升级期间机巢将暂时停止远程控制和视频服务,请确认当前没有飞行任务。升级完成后设备会自动重启并恢复连接。 + {{ firmwareError }} + + + 取消 + {{ upgrading ? '下发中…' : '开始升级' }} + + + + + + + + + + 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 @@ - - - - - 流量运营 - 管理云媒体流量套餐、平台采购入池与账务流水 - - - - 测试发放 - - - - - - - - - - - - - - - - - - - - - - 新建套餐 - - - - - - - - {{ row.statusName }} - - - - 编辑 - 下架 - - - - - - - - - - - 尚未建立资源池,首次录入采购后自动创建 - - - - 资源池总量 - {{ bytesToGbText(pool.totalBytes) }} - {{ pool.totalBytes ?? 0 }} bytes - - - 可用流量 - {{ bytesToGbText(pool.availableBytes) }} - {{ pool.availableBytes ?? 0 }} bytes - - - 池状态 - {{ pool.status === 'active' ? '有效' : (pool.status === 'inactive' ? '失效' : (pool.status || '—')) }} - {{ pool.status === 'active' ? '有效' : (pool.status === 'inactive' ? '失效' : (pool.status || '—')) }} - - - - - - - - - - - - - - - - - - 录入采购 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 编码* - - - - 名称* - - - - - 额度(GB)* - - - - 价格(元) - - - - - - 有效期(天) - - - - 排序 - - - - - 状态 - - - 有效期填 0 表示长期有效 - {{ pkgFormError }} - - - 取消 - - {{ pkgEditingId ? '保存' : '确认新建' }} - - - - - - - - 批次号* - - - - 供应商* - - - - - 总量(GB)* - - - - 单价 - - - - - - 总成本 - - - - 到期时间 - - - - - 备注 - - - {{ purchaseFormError }} - - - 取消 - 确认录入 - - - - - - - 搜索用户 - - - - 用户 ID* - - - - 额度(GB)* - - - - 原因* - - - {{ grantFormError }} - - 最新余额 - {{ grantResultBalanceText }} - - - - 取消 - 确认发放 - - - - - - - - - - + + + + + 流量运营 + 管理云媒体流量套餐、平台采购入池与账务流水 + + + + 测试发放 + + + + + + + + + + + + + + + + + + + + + + + 新建套餐 + + + + + + + + {{ row.statusName }} + + + + 编辑 + 下架 + + + + + + + + + + 刷新从 SIMBOSS 查询套餐新建通信卡套餐 + {{ row.status === 'active' ? '上架' : '下架' }}编辑下架 + + + + + + + 尚未建立资源池,首次录入采购后自动创建 + + + + 资源池总量 + {{ bytesToGbText(pool.totalBytes) }} + {{ pool.totalBytes ?? 0 }} bytes + + + 可用流量 + {{ bytesToGbText(pool.availableBytes) }} + {{ pool.availableBytes ?? 0 }} bytes + + + 池状态 + {{ pool.status === 'active' ? '有效' : (pool.status === 'inactive' ? '失效' : (pool.status || '—')) }} + {{ pool.status === 'active' ? '有效' : (pool.status === 'inactive' ? '失效' : (pool.status || '—')) }} + + + + + + + + + + + + + + + + + + 录入采购 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 套餐编码*套餐名称*运营商供应商套餐ID流量(GB)*有效期(月)*销售价(元)*成本价(元)状态{{ simPkgError }}取消保存 + + + + + 编码* + + + + 名称* + + + + + 额度(GB)* + + + + 价格(元) + + + + + + 有效期(天) + + + + 排序 + + + + + 状态 + + + 有效期填 0 表示长期有效 + {{ pkgFormError }} + + + 取消 + + {{ pkgEditingId ? '保存' : '确认新建' }} + + + + + 查询使用此套餐 + + + + + 批次号* + + + + 供应商* + + + + + 总量(GB)* + + + + 单价 + + + + + + 总成本 + + + + 到期时间 + + + + + 备注 + + + {{ purchaseFormError }} + + + 取消 + 确认录入 + + + + + + + 搜索用户 + + + + 用户 ID* + + + + 额度(GB)* + + + + 原因* + + + {{ grantFormError }} + + 最新余额 + {{ grantResultBalanceText }} + + + + 取消 + 确认发放 + + + + + + + + + +
流量用于直播、回放与原始素材下载
ICCID:{{ simRechargeCard?.iccid || '--' }}
{{ payDialog.error || '暂无二维码' }}
{{ record.code }}
升级期间机巢将暂时停止远程控制和视频服务,请确认当前没有飞行任务。升级完成后设备会自动重启并恢复连接。
{{ grantFormError }}