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', '起飞准备': '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' } // 同名指令在机巢/无人机上语义不同:机巢「一键返航」走 workflow 编排(返航+降落), // 无人机「一键返航」是 drone.return 直达指令 const DOCK_COMMANDS = { '一键返航': 'workflow.one_key_landing' } // 经纬度 → 地图百分比坐标(按设备经纬范围动态投影,北向上) 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, kind = '') { if (kind === 'dock' && DOCK_COMMANDS[title]) return DOCK_COMMANDS[title] return COMMANDS[title] || '' } async function sendCommand(rec, title, params = {}) { const type = commandType(title, rec.kind) 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 } })