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