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.
 
 
 
 

600 lines
30 KiB

<template>
<section class="device-detail-page" :class="{ 'drone-detail': isDrone }">
<template v-if="record">
<header class="full-detail-header">
<button class="back-command" @click="router.push({ name: 'devices' })"><svg><use href="#i-chevron" /></svg>返回设备列表</button>
<div class="full-detail-identity">
<span :class="{ drone: isDrone }"><svg><use :href="isDrone ? '#i-plane' : '#i-home'" /></svg></span>
<div>
<small>{{ isDrone ? '无人机设备' : '机巢设备' }}</small>
<h1>{{ record.name }}</h1>
<p>{{ record.code }}</p>
</div>
<em class="status" :class="record.statusClass">{{ record.status }}</em>
</div>
<div class="full-detail-actions" v-if="!isAdmin">
<button v-if="!isDrone" class="plain-command" @click="editDockVisible = true">编辑信息</button>
<button class="plain-command" @click="goControl"><svg><use href="#i-zap" /></svg>远程控制</button>
<button class="primary-command" @click="goMonitor"><svg><use href="#i-radio" /></svg>实时监控</button>
</div>
</header>
<div class="full-detail-tabs">
<button :class="{ active: activeTab === 'overview' }" @click="activeTab = 'overview'">概览</button>
<button :class="{ active: activeTab === 'state' }" @click="activeTab = 'state'">设备状态</button>
<button :class="{ active: activeTab === 'alarms' }" @click="activeTab = 'alarms'">告警记录<b v-if="alarmRecords.length">{{ alarmRecords.length }}</b></button>
<button :class="{ active: activeTab === 'logs' }" @click="activeTab = 'logs'">操作记录</button>
<button :class="{ active: activeTab === 'diagnostics' }" @click="activeTab = 'diagnostics'">运维诊断</button>
</div>
<div v-if="activeTab === 'overview'" class="full-detail-content">
<template v-if="!isDrone">
<div class="detail-metric-band">
<div><span>控制模式</span><strong>{{ record.mode }}</strong></div>
<div><span>无人机在位</span><strong :class="{ 'green-text': isTrue(rt('dronePresent')) }">{{ booleanText(rt('dronePresent')) }}</strong></div>
<div><span>舱门状态</span><strong>{{ doorSummary }}</strong></div>
<div><span>充电状态</span><strong :class="{ 'green-text': chargeTone }">{{ chargeText(rt('chargingState')) }}</strong></div>
<div><span>当前告警</span><strong>{{ record.alarms?.length ?? 0 }}</strong></div>
<div><span>最近上报</span><strong>{{ record.updated }}</strong></div>
</div>
<div class="detail-columns">
<div class="detail-main-column">
<section class="data-section">
<div class="data-section-title"><h2>机巢运行状态</h2><button @click="activeTab = 'state'">全部状态</button></div>
<div class="status-matrix">
<div><svg><use href="#i-door" /></svg><span>左侧舱门</span><strong>{{ doorText('left') }}</strong><i v-if="isClosed(doorValue('left'))" class="ok"></i></div>
<div><svg><use href="#i-door" /></svg><span>右侧舱门</span><strong>{{ doorText('right') }}</strong><i v-if="isClosed(doorValue('right'))" class="ok"></i></div>
<div><svg><use href="#i-grid" /></svg><span>左右归中</span><strong>{{ centeringText('leftRight') }}</strong><i v-if="isTight(centeringValue('leftRight'))" class="ok"></i></div>
<div><svg><use href="#i-grid" /></svg><span>前后归中</span><strong>{{ centeringText('frontBack') }}</strong><i v-if="isTight(centeringValue('frontBack'))" class="ok"></i></div>
<div><svg><use href="#i-alert" /></svg><span>急停状态</span><strong>{{ isTrue(rt('emergencyStop')) ? '已触发' : booleanText(rt('emergencyStop')) === '--' ? '--' : '未触发' }}</strong><i v-if="isFalse(rt('emergencyStop'))" class="ok"></i></div>
<div><svg><use href="#i-refresh" /></svg><span>整机状态</span><strong>{{ record.status }}</strong><i v-if="record.online" class="ok"></i></div>
</div>
</section>
<section class="data-section">
<h2>充电系统</h2>
<div class="value-table">
<div><span>充电状态</span><strong :class="{ 'green-text': chargeTone }">{{ chargeText(rt('chargingState')) }}</strong></div>
<div><span>充电电压</span><strong>{{ withUnit(rt('chargingVoltage'), ' V') }}</strong></div>
<div><span>充电电流</span><strong>{{ withUnit(rt('chargingCurrent'), ' A') }}</strong></div>
<div><span>无人机电量</span><strong>{{ boundDrone?.battery || '--' }}</strong></div>
<div><span>无人机电源</span><strong>{{ booleanText(rt('dronePowerOnComplete')) }}</strong></div>
<div><span>充电启动反馈</span><strong>{{ completeText(rt('chargeStartComplete')) }}</strong></div>
</div>
</section>
<section class="data-section">
<h2>环境监测</h2>
<div class="environment-values">
<div><svg><use href="#i-sun" /></svg><span>舱外温度</span><strong>{{ withUnit(record.environment?.outsideTemperature, '℃') }}</strong></div>
<div><svg><use href="#i-sun" /></svg><span>舱内温度</span><strong>{{ withUnit(record.environment?.insideTemperature, '℃') }}</strong></div>
<div><svg><use href="#i-cloud-rain" /></svg><span>舱外湿度</span><strong>{{ withUnit(record.environment?.outsideHumidity, '%RH') }}</strong></div>
<div><svg><use href="#i-cloud-rain" /></svg><span>舱内湿度</span><strong>{{ withUnit(record.environment?.insideHumidity, '%RH') }}</strong></div>
<div><svg><use href="#i-wind" /></svg><span>实时风速</span><strong>{{ withUnit(record.environment?.windSpeed, ' m/s') }}</strong></div>
<div><svg><use href="#i-cloud-rain" /></svg><span>降雨</span><strong>{{ booleanText(record.environment?.rain) }}</strong></div>
</div>
</section>
</div>
<div class="detail-side-column">
<section class="data-section">
<div class="data-section-title"><h2>绑定无人机</h2></div>
<div class="bound-asset-large">
<span><svg><use href="#i-plane" /></svg></span>
<div>
<strong>{{ boundDrone?.name || record.binding }}</strong>
<small>{{ boundDrone ? `${boundDrone.status} · 电量 ${boundDrone.battery}` : (record.bindingState === 'bound' ? '绑定关系正常' : '当前未绑定设备') }}</small>
</div>
<i v-if="boundDrone" class="status" :class="boundDrone.statusClass">{{ boundDrone.status }}</i>
</div>
<dl>
<div><dt>飞控 SN</dt><dd>{{ boundDrone?.sn || '--' }}</dd></div>
<div><dt>定位状态</dt><dd>{{ boundDrone?.gps || '--' }}</dd></div>
<div><dt>当前任务</dt><dd>{{ boundDrone?.mission || '--' }}</dd></div>
</dl>
</section>
<section class="data-section" v-if="latestLog">
<h2>最近流程</h2>
<div class="workflow-summary">
<span class="workflow-icon"><svg><use href="#i-home" /></svg></span>
<div><strong>{{ latestLog.commandName }}</strong><small>{{ latestLog.createdAt }}</small></div>
<i>{{ latestLog.statusName }}</i>
</div>
</section>
<section class="data-section">
<div class="data-section-title"><h2>设备信息</h2><button v-if="!isAdmin" @click="editDockVisible = true">编辑</button></div>
<dl class="compact-properties">
<div><dt>机巢 SN</dt><dd>{{ record.sn }}</dd></div>
<div><dt>通信卡 ICCID</dt><dd>{{ record.iccid }}</dd></div>
<div><dt>设备位置</dt><dd>{{ record.location }}</dd></div>
<div><dt>经纬度</dt><dd>{{ coordinateText }}</dd></div>
<div><dt>软件版本</dt><dd>{{ record.version }}</dd></div>
</dl>
</section>
</div>
</div>
</template>
<template v-else>
<div class="detail-metric-band">
<div><span>飞行状态</span><strong :class="{ 'blue-text': record.statusClass === 'mission' }">{{ record.status }}</strong></div>
<div><span>剩余电量</span><strong>{{ percentValue(record.battery) }}</strong></div>
<div><span>相对高度</span><strong>{{ withUnit(asset?.altitude, ' m') }}</strong></div>
<div><span>地速</span><strong>{{ withUnit(asset?.speed, ' m/s') }}</strong></div>
<div><span>卫星数量</span><strong>{{ displayValue(asset?.satellites) }}</strong></div>
<div><span>链路质量</span><strong>{{ withUnit(rt('linkQuality'), '%') }}</strong></div>
</div>
<div class="detail-columns">
<div class="detail-main-column">
<section class="data-section">
<h2>飞行数据</h2>
<div class="value-table">
<div><span>飞行模式</span><strong>{{ record.mode }}</strong></div>
<div><span>解锁状态</span><strong>{{ armedText }}</strong></div>
<div><span>GPS 状态</span><strong class="green-text">{{ record.gps }}</strong></div>
<div><span>电池电压</span><strong>{{ withUnit(rt('batteryVoltage'), ' V') }}</strong></div>
<div><span>航向角</span><strong>{{ withUnit(rt('yaw'), '°') }}</strong></div>
<div><span>返航点</span><strong>{{ homeSetText }}</strong></div>
</div>
</section>
<section class="data-section">
<h2>当前任务</h2>
<div class="mission-detail-row">
<div><strong>{{ record.mission && record.mission !== '--' ? record.mission : '暂无任务' }}</strong><small>{{ record.binding }}</small></div>
</div>
</section>
</div>
<div class="detail-side-column">
<section class="data-section">
<h2>所属机巢</h2>
<div class="bound-asset-large dock">
<span><svg><use href="#i-home" /></svg></span>
<div><strong>{{ boundDock?.name || record.binding }}</strong><small>{{ record.bindingState === 'bound' ? '绑定关系正常' : '当前未绑定设备' }}</small></div>
<i v-if="boundDock" class="status" :class="boundDock.statusClass">{{ boundDock.status }}</i>
</div>
</section>
<section class="data-section">
<h2>实时位置</h2>
<dl class="compact-properties">
<div><dt>经度</dt><dd>{{ displayValue(rt('longitude')) }}</dd></div>
<div><dt>纬度</dt><dd>{{ displayValue(rt('latitude')) }}</dd></div>
<div><dt>Roll</dt><dd>{{ withUnit(rt('roll'), '°') }}</dd></div>
<div><dt>Pitch</dt><dd>{{ withUnit(rt('pitch'), '°') }}</dd></div>
<div><dt>Yaw</dt><dd>{{ withUnit(rt('yaw'), '°') }}</dd></div>
</dl>
</section>
</div>
</div>
</template>
</div>
<div v-else-if="activeTab === 'state'" class="full-detail-content">
<template v-if="!isDrone">
<div class="state-groups">
<section class="data-section">
<h2>舱门与归中机构</h2>
<div class="state-detail-grid">
<div><span>左舱门</span><strong>{{ doorText('left') }}</strong><small>关门反馈:{{ completeText(rt('doorCloseComplete')) }}</small></div>
<div><span>右舱门</span><strong>{{ doorText('right') }}</strong><small>开门反馈:{{ completeText(rt('doorOpenComplete')) }}</small></div>
<div><span>左右归中</span><strong>{{ centeringText('leftRight') }}</strong><small>夹紧反馈:{{ completeText(rt('centeringTightComplete')) }}</small></div>
<div><span>前后归中</span><strong>{{ centeringText('frontBack') }}</strong><small>松开反馈:{{ completeText(rt('centeringLooseComplete')) }}</small></div>
</div>
</section>
<section class="data-section">
<h2>流程反馈</h2>
<div class="state-detail-grid">
<div><span>起飞准备</span><strong>{{ completeText(rt('takeoffPreparationComplete')) }}</strong><small>takeoffPreparationComplete</small></div>
<div><span>起飞收尾</span><strong>{{ completeText(rt('hangarActionComplete')) }}</strong><small>hangarActionComplete</small></div>
<div><span>降落准备</span><strong>{{ completeText(rt('landingPreparationComplete')) }}</strong><small>landingPreparationComplete</small></div>
<div><span>降落收尾</span><strong>{{ completeText(rt('landingExecutionComplete')) }}</strong><small>landingExecutionComplete</small></div>
<div><span>整机复位</span><strong>{{ completeText(rt('resetComplete')) }}</strong><small>resetComplete</small></div>
<div><span>急停状态</span><strong>{{ isTrue(rt('emergencyStop')) ? '已触发' : isFalse(rt('emergencyStop')) ? '未触发' : '--' }}</strong><small>emergencyStop</small></div>
</div>
</section>
</div>
</template>
<template v-else>
<div class="state-groups">
<section class="data-section">
<h2>飞控状态</h2>
<div class="state-detail-grid">
<div><span>在线状态</span><strong>{{ record.online ? '在线' : '离线' }}</strong><small>{{ record.updated }}</small></div>
<div><span>解锁状态</span><strong>{{ armedText }}</strong><small>armed</small></div>
<div><span>飞行模式</span><strong>{{ record.mode }}</strong><small>mode code:{{ displayValue(rt('flightModeCode')) }}</small></div>
<div><span>返航点</span><strong>{{ homeSetText }}</strong><small>homeSet</small></div>
</div>
</section>
<section class="data-section">
<h2>任务状态</h2>
<div class="state-detail-grid">
<div><span>任务状态</span><strong>{{ record.statusClass === 'mission' ? '执行中' : displayValue(record.mission) }}</strong><small>{{ record.status }}</small></div>
<div><span>飞控 SN</span><strong>{{ record.sn }}</strong><small>已同步</small></div>
<div><span>卫星数量</span><strong>{{ displayValue(asset?.satellites) }}</strong><small>GPS {{ record.gps }}</small></div>
<div><span>链路质量</span><strong>{{ withUnit(rt('linkQuality'), '%') }}</strong><small>MAVLink</small></div>
</div>
</section>
</div>
</template>
</div>
<div v-else-if="activeTab === 'alarms'" class="full-detail-content">
<div class="record-header">
<div><h2>告警记录</h2><span>当前设备</span></div>
<t-select v-model="alarmStatusFilter" :options="alarmStatusOptions" placeholder="全部告警状态" style="width: 160px" />
</div>
<table class="record-table">
<thead><tr><th>告警时间</th><th>告警编码</th><th>告警内容</th><th>级别</th><th>状态</th><th>恢复时间</th></tr></thead>
<tbody>
<tr v-for="item in filteredAlarms" :key="item.id">
<td>{{ item.triggeredAt }}</td>
<td>{{ item.code }}</td>
<td>{{ item.message }}</td>
<td><i class="severity" :class="item.levelClass">{{ item.levelName }}</i></td>
<td><span class="table-status" :class="item.statusClass"><i></i>{{ item.statusName }}</span></td>
<td>{{ item.resolvedAt }}</td>
</tr>
</tbody>
</table>
<div v-if="!filteredAlarms.length" class="table-empty">暂无告警记录</div>
</div>
<div v-else-if="activeTab === 'logs'" class="full-detail-content">
<div class="record-header">
<div><h2>操作记录</h2><span>远程控制与配置变更</span></div>
<button @click="loadLogs">刷新</button>
</div>
<table class="record-table">
<thead><tr><th>操作时间</th><th>操作类型</th><th>操作内容</th><th>执行结果</th><th>目标设备</th></tr></thead>
<tbody>
<tr v-for="log in logs" :key="log.id">
<td>{{ log.createdAt }}</td>
<td>{{ log.category }}</td>
<td>{{ log.commandName }}</td>
<td><span :class="log.resultClass">{{ log.result }}</span></td>
<td>{{ log.droneSn }}</td>
</tr>
</tbody>
</table>
<div v-if="!logs.length" class="table-empty">暂无设备操作记录</div>
</div>
<div v-else class="full-detail-content">
<template v-if="!isDrone">
<div class="diagnostics-layout">
<section class="data-section">
<h2>边缘服务</h2>
<div class="diagnostic-list">
<div><span><i :class="record.online ? 'ok' : 'standby'"></i>MQTT 连接</span><strong>{{ record.online ? '正常' : '离线' }}</strong><small>{{ record.updated }}</small></div>
<div><span><i :class="isTrue(rt('plcConnected')) ? 'ok' : 'standby'"></i>PLC / Modbus</span><strong>{{ booleanText(rt('plcConnected')) === '是' ? '正常' : booleanText(rt('plcConnected')) }}</strong><small>plcConnected</small></div>
<div><span><i class="standby"></i>MAVLink</span><strong>--</strong><small>未单独上报</small></div>
<div><span><i class="standby"></i>视频服务</span><strong>--</strong><small>见直播状态</small></div>
</div>
</section>
<section class="data-section">
<h2>软件与身份</h2>
<div class="value-table">
<div><span>边缘版本</span><strong>{{ record.version }}</strong></div>
<div><span>协议版本</span><strong>{{ record.protocol }}</strong></div>
<div><span>设备连接</span><strong>{{ record.status }}</strong></div>
<div><span>最近更新</span><strong>{{ record.updated }}</strong></div>
<div><span>告警数量</span><strong>{{ record.alarms?.length ?? 0 }}</strong></div>
<div><span>PLC 连接</span><strong>{{ booleanText(rt('plcConnected')) }}</strong></div>
</div>
</section>
</div>
</template>
<template v-else>
<div class="diagnostics-layout">
<section class="data-section">
<h2>飞控通信</h2>
<div class="diagnostic-list">
<div><span><i :class="record.online ? 'ok' : 'standby'"></i>MAVLink 心跳</span><strong>{{ record.online ? '正常' : '离线' }}</strong><small>{{ record.updated }}</small></div>
<div><span><i :class="record.online ? 'ok' : 'standby'"></i>遥测数据</span><strong>{{ record.online ? '正常' : '--' }}</strong><small>最近 {{ record.updated }}</small></div>
<div><span><i :class="record.sn && record.sn !== '--' ? 'ok' : 'standby'"></i>飞控序列号</span><strong>{{ record.sn && record.sn !== '--' ? '已识别' : '--' }}</strong><small>{{ record.sn }}</small></div>
</div>
</section>
<section class="data-section">
<h2>飞控信息</h2>
<div class="value-table">
<div><span>当前 SysID</span><strong>{{ record.sysid }}</strong></div>
<div><span>飞控版本</span><strong>{{ record.firmwareVer }}</strong></div>
<div><span>机型</span><strong>{{ record.model }}</strong></div>
<div><span>链路质量</span><strong>{{ withUnit(rt('linkQuality'), '%') }}</strong></div>
</div>
</section>
</div>
</template>
</div>
</template>
<DockEditModal :visible="editDockVisible" :dock="record" @close="editDockVisible = false" @saved="handleDockSaved" />
<div v-if="!record" class="table-empty" style="flex:1;display:grid;place-items:center;color:var(--muted);font-size:12px">
未找到该设备
<button class="primary-command" style="margin-top:10px" @click="router.push({ name: 'devices' })">返回设备列表</button>
</div>
</section>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useDeviceStore } from '@/stores/modules/devicesStore'
import { useUiStore } from '@/stores/modules/uiStore'
import { useUserStore } from '@/stores/modules/userStore'
import request from '@/utils/http'
import DockEditModal from '@/components/DockEditModal.vue'
import * as urls from '@/config/urls'
const route = useRoute()
const router = useRouter()
const devices = useDeviceStore()
const ui = useUiStore()
const userStore = useUserStore()
const isAdmin = computed(() => userStore.isAdmin())
const activeTab = ref('overview')
const logs = ref([])
const alarmRecords = ref([])
const alarmStatusFilter = ref('')
const alarmStatusOptions = [
{ label: '全部告警状态', value: '' },
{ label: '未恢复', value: 'active' },
{ label: '已恢复', value: 'resolved' }
]
const editDockVisible = ref(false)
const record = computed(() => devices.record(route.params.id))
const asset = computed(() => devices.asset(route.params.id))
const isDrone = computed(() => record.value?.kind === 'drone')
const boundDrone = computed(() => (record.value?.kind === 'dock' ? devices.drones.find((d) => d.dockId === record.value.dockId) : null))
const boundDock = computed(() => (record.value?.kind === 'drone' ? devices.docks.find((d) => d.dockId === record.value.dockId) : null))
const latestLog = computed(() => logs.value[0] || null)
const coordinateText = computed(() => {
const rec = record.value
if (!rec || rec.longitude == null || rec.latitude == null) return '--'
return `${rec.longitude}, ${rec.latitude}`
})
const chargeTone = computed(() => String(rt('chargingState') || '').toLowerCase() === 'charging')
const doorSummary = computed(() => {
const left = doorValue('left')
const right = doorValue('right')
if (isClosed(left) && isClosed(right)) return '已关闭'
if (left === 'open' || right === 'open') return '已打开'
return displayValue(left || right)
})
const armedText = computed(() => {
if (isTrue(rt('armed'))) return '已解锁'
if (isFalse(rt('armed'))) return '未解锁'
return '--'
})
const homeSetText = computed(() => {
if (isTrue(rt('homeSet'))) return '已设置'
if (isFalse(rt('homeSet'))) return '未设置'
return '--'
})
const filteredAlarms = computed(() => {
if (!alarmStatusFilter.value) return alarmRecords.value
if (alarmStatusFilter.value === 'active') return alarmRecords.value.filter((item) => item.status !== 'resolved')
return alarmRecords.value.filter((item) => item.status === alarmStatusFilter.value)
})
function percentValue(value) {
return value == null || value === '' || value === '--' ? '--' : `${value}`
}
function displayValue(value) {
return value == null || value === '' ? '--' : value
}
function withUnit(value, unit) {
return value == null || value === '' ? '--' : `${value}${unit}`
}
function isTrue(value) {
return value === true || value === 'true' || value === '1' || value === 1
}
function isFalse(value) {
return value === false || value === 'false' || value === '0' || value === 0
}
function booleanText(value) {
if (isTrue(value)) return '是'
if (isFalse(value)) return '否'
return '--'
}
function completeText(value) {
if (isTrue(value)) return '已完成'
if (isFalse(value)) return '未执行'
return '--'
}
function rt(key, ...aliases) {
const realtime = record.value?.realtime || {}
for (const name of [key, ...aliases]) {
if (realtime[name] != null && realtime[name] !== '') return realtime[name]
}
return null
}
function rtObj(key) {
const raw = record.value?.realtime?.[key]
if (!raw) return {}
if (typeof raw === 'object') return raw
try { return JSON.parse(raw) || {} } catch { return {} }
}
function doorValue(side) {
return rtObj('door')[side]
}
function centeringValue(key) {
return rtObj('centering')[key]
}
function isClosed(value) {
return value === 'closed' || value === 'close'
}
function isTight(value) {
return value === 'tight'
}
function doorText(side) {
const value = doorValue(side)
if (isClosed(value)) return '已关闭'
if (value === 'open') return '已打开'
return displayValue(value)
}
function centeringText(key) {
const value = centeringValue(key)
if (value === 'tight') return '已夹紧'
if (value === 'loose') return '已松开'
return displayValue(value)
}
function chargeText(value) {
const key = String(value || '').toLowerCase()
if (key === 'charging') return '充电中'
if (key === 'idle') return '空闲'
if (key === 'full' || key === 'charged') return '已充满'
return displayValue(value)
}
onMounted(async () => {
try {
await devices.load()
if (!isAdmin.value) {
await Promise.all([loadLogs(), loadAlarms()])
}
} catch (e) {
ui.toast(e.message || '加载设备失败')
}
})
const COMMAND_NAMES = {
'dock.open': '打开舱门',
'dock.close': '关闭舱门',
'dock.reset': '整机复位',
'dock.prepare_takeoff': '起飞准备',
'dock.complete_takeoff': '起飞准备完成',
'dock.prepare_landing': '降落准备',
'dock.complete_landing': '降落准备完成',
'dock.charge_start': '开始充电',
'dock.drone_power_on': '无人机开机',
'dock.drone_power_off': '无人机关机',
'dock.reboot': '远程重启',
'dock.clear_alarm': '清除设备告警',
'drone.takeoff': '无人机起飞',
'drone.land': '无人机降落',
'drone.return': '一键返航',
'drone.mission_upload': '上传任务',
'drone.mission_start': '开始任务',
'drone.mission_pause': '暂停任务',
'drone.mission_resume': '继续任务',
'drone.mission_cancel': '取消任务',
'workflow.start_task': '执行任务',
'workflow.one_key_takeoff': '一键起飞',
'workflow.one_key_land': '一键降落',
'video.start_stream': '开始直播',
'video.stop_stream': '停止直播'
}
const STATUS_NAMES = {
sent: { name: '已下发', cls: 'processing', result: '等待应答' },
acked: { name: '已确认', cls: 'result-success', result: '执行成功' },
timeout: { name: '已超时', cls: 'offline', result: '已超时' },
terminal: { name: '执行失败', cls: 'offline', result: '执行失败' }
}
const ALARM_LEVELS = {
critical: { name: '紧急', cls: 'high' },
high: { name: '紧急', cls: 'high' },
warning: { name: '一般', cls: 'medium' },
medium: { name: '一般', cls: 'medium' },
info: { name: '提示', cls: 'low' },
low: { name: '提示', cls: 'low' }
}
function fmtTime(t) {
if (!t) return '--'
const d = new Date(t)
return Number.isNaN(d.getTime()) ? '--' : d.toLocaleString('zh-CN', { hour12: false })
}
async function loadLogs() {
const current = record.value
if (!current || isAdmin.value) return
try {
const page = await request.get(urls.COMMANDS, { params: { pageNum: 1, pageSize: 100, dockId: current.dockId } })
logs.value = (page?.records || []).filter((log) => current.kind !== 'drone' || log.droneSn === current.droneSn).map((log) => {
const status = STATUS_NAMES[log.status] || { name: log.status || '--', cls: 'offline', result: log.status || '--' }
const type = log.commandType || ''
return {
id: String(log.id),
category: type.startsWith('workflow.') ? '工作流' : '控制指令',
commandName: COMMAND_NAMES[type] || type || '--',
droneSn: log.droneSn || '机巢本体',
statusName: status.name,
result: log.ackResultCode || status.result,
resultClass: log.status === 'acked' ? 'result-success' : '',
createdAt: fmtTime(log.createdAt)
}
})
} catch (e) {
if (e?.response?.status === 403) return
ui.toast(e.message || '加载设备操作记录失败')
}
}
async function loadAlarms() {
const current = record.value
if (!current || isAdmin.value) return
try {
const page = await request.get(urls.ALARMS, {
params: {
pageNum: 1,
pageSize: 100,
dockId: current.dockId,
deviceType: current.kind === 'drone' ? 'drone' : 'dock'
}
})
alarmRecords.value = (page?.records || []).map((item) => {
const level = ALARM_LEVELS[item.level] || { name: item.level || '--', cls: 'medium' }
const resolved = item.status === 'resolved'
return {
id: String(item.id),
code: item.code || '--',
message: item.messageCn || item.code || '--',
levelName: level.name,
levelClass: level.cls,
status: item.status,
statusName: resolved ? '已恢复' : item.status === 'acknowledged' ? '已确认' : '未恢复',
statusClass: resolved ? 'online' : 'offline',
triggeredAt: fmtTime(item.triggeredAt || item.createdAt),
resolvedAt: fmtTime(item.resolvedAt)
}
})
} catch (e) {
if (e?.response?.status === 403) return
ui.toast(e.message || '加载告警记录失败')
}
}
async function handleDockSaved() {
editDockVisible.value = false
try {
await devices.load()
ui.toast('机巢信息已更新')
} catch (e) {
ui.toast(e.message || '机巢已保存,但刷新设备数据失败')
}
}
function goControl() {
router.push({ name: 'monitor', query: { deviceId: route.params.id, control: '1' } })
}
function goMonitor() {
router.push({ name: 'monitor', query: { deviceId: route.params.id } })
}
</script>