You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

426 lines
16 KiB

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 }
})