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.
572 lines
22 KiB
572 lines
22 KiB
<template>
|
|
<section class="device-page">
|
|
<header class="device-page-header">
|
|
<div>
|
|
<h1>设备管理</h1>
|
|
<span>{{ updatedLabel }}</span>
|
|
</div>
|
|
<div class="page-actions">
|
|
<button class="icon-command" title="刷新" @click="reload"><svg><use href="#i-refresh" /></svg></button>
|
|
<button v-if="!isAdmin" class="primary-command" @click="openAddDock"><svg><use href="#i-plus" /></svg>添加机巢</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="asset-overview">
|
|
<div><span>机巢总数</span><strong>{{ overview.docks }}</strong><small>台</small></div>
|
|
<div><span>在线设备</span><strong class="green-text">{{ overview.online }}</strong><small>台</small></div>
|
|
<div><span>执行任务</span><strong class="blue-text">{{ overview.mission }}</strong><small>台</small></div>
|
|
<div><span>当前告警</span><strong class="red-text">{{ overview.alarm }}</strong><small>台</small></div>
|
|
<div><span>绑定无人机</span><strong>{{ overview.boundDrones }}</strong><small>架</small></div>
|
|
</div>
|
|
|
|
<div class="asset-workspace">
|
|
<div class="asset-tabs">
|
|
<button class="asset-tab" :class="{ active: tab === 'docks' }" @click="setTab('docks')">机巢设备<b>{{ devices.docks.length }}</b></button>
|
|
<button class="asset-tab" :class="{ active: tab === 'drones' }" @click="setTab('drones')">无人机<b>{{ devices.drones.length }}</b></button>
|
|
</div>
|
|
|
|
<div class="asset-toolbar">
|
|
<div class="asset-search">
|
|
<t-input v-model="search" clearable placeholder="搜索设备名称、编号或 SN">
|
|
<template #prefix-icon><t-icon name="search" /></template>
|
|
</t-input>
|
|
</div>
|
|
<div class="toolbar-group">
|
|
<t-select v-model="statusFilter" :options="statusOptions" placeholder="全部状态" style="width: 140px" />
|
|
<t-select v-model="bindingFilter" :options="bindingOptions" placeholder="全部绑定状态" style="width: 160px" />
|
|
<button class="plain-command" type="button" @click="resetFilters">重置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="asset-table-wrap">
|
|
<table class="asset-table">
|
|
<thead>
|
|
<tr v-if="tab === 'docks'">
|
|
<th style="width:18%">机巢设备</th>
|
|
<th>状态</th>
|
|
<th>绑定无人机</th>
|
|
<th>控制模式</th>
|
|
<th>设备位置</th>
|
|
<th>软件版本</th>
|
|
<th>最近上报</th>
|
|
<th style="width:20%">操作</th>
|
|
</tr>
|
|
<tr v-else>
|
|
<th style="width:18%">无人机</th>
|
|
<th>状态</th>
|
|
<th>所属机巢</th>
|
|
<th>电量</th>
|
|
<th>飞行模式</th>
|
|
<th>当前任务</th>
|
|
<th>最近上报</th>
|
|
<th style="width:22%">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody v-if="tab === 'docks'">
|
|
<tr v-for="d in filteredDocks" :key="d.id" @click="openDrawer(d.id)">
|
|
<td>
|
|
<div class="asset-name-cell">
|
|
<span class="table-asset-icon"><svg><use href="#i-home" /></svg></span>
|
|
<span><strong>{{ d.name }}</strong><small>{{ d.code }}</small></span>
|
|
</div>
|
|
</td>
|
|
<td><span class="table-status" :class="d.statusClass"><i></i>{{ d.status }}</span></td>
|
|
<td><span class="binding-cell" :class="{ unbound: d.bindingState === 'unbound' }"><svg><use href="#i-link" /></svg>{{ d.binding }}</span></td>
|
|
<td>{{ d.mode }}</td>
|
|
<td>{{ d.location }}</td>
|
|
<td>{{ d.version }}</td>
|
|
<td>{{ d.updated }}</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button v-if="!isAdmin" @click.stop="openFirmwareUpgrade(d.id)">固件升级</button>
|
|
<button v-if="!isAdmin" @click.stop="goControl(d.id)">远程控制</button>
|
|
<button @click.stop="goDetail(d.id)">详情</button>
|
|
<button v-if="!isAdmin" @click.stop="deleteDock(d)">删除</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!filteredDocks.length"><td class="table-empty" colspan="8">暂无符合条件的设备</td></tr>
|
|
</tbody>
|
|
<tbody v-else>
|
|
<tr v-for="d in filteredDrones" :key="d.id" @click="openDrawer(d.id)">
|
|
<td>
|
|
<div class="asset-name-cell">
|
|
<span class="table-asset-icon drone"><svg><use href="#i-plane" /></svg></span>
|
|
<span><strong>{{ d.name }}</strong><small>{{ d.code }}</small></span>
|
|
</div>
|
|
</td>
|
|
<td><span class="table-status" :class="d.statusClass"><i></i>{{ d.status }}</span></td>
|
|
<td><span class="binding-cell"><svg><use href="#i-link" /></svg>{{ d.binding }}</span></td>
|
|
<td>{{ d.battery }}</td>
|
|
<td>{{ d.mode }}</td>
|
|
<td>{{ d.mission }}</td>
|
|
<td>{{ d.updated }}</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button v-if="!isAdmin" @click.stop="ui.toast('请选择航线后上传')">航线上传</button>
|
|
<button v-if="!isAdmin" @click.stop="goControl(d.id)">远程控制</button>
|
|
<button @click.stop="goDetail(d.id)">详情</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!filteredDrones.length"><td class="table-empty" colspan="8">暂无符合条件的设备</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="table-footer">
|
|
<span>共 {{ tab === 'docks' ? filteredDocks.length : filteredDrones.length }} 条</span>
|
|
<div>
|
|
<button disabled><svg><use href="#i-chevron" /></svg></button>
|
|
<b>1</b>
|
|
<button disabled><svg><use href="#i-chevron" /></svg></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Drawer -->
|
|
<div class="drawer-scrim" :class="{ open: drawerOpen }" @click="drawerOpen = false"></div>
|
|
<aside class="device-drawer" :class="{ open: drawerOpen }">
|
|
<template v-if="drawerRecord">
|
|
<header>
|
|
<div class="drawer-identity">
|
|
<span :class="{ drone: drawerRecord.kind === 'drone' }"><svg><use :href="drawerRecord.kind === 'drone' ? '#i-plane' : '#i-home'" /></svg></span>
|
|
<div>
|
|
<small>{{ drawerRecord.kind === 'drone' ? '无人机设备' : '机巢设备' }}</small>
|
|
<h2>{{ drawerRecord.name }}</h2>
|
|
<p>{{ drawerRecord.code }}</p>
|
|
</div>
|
|
</div>
|
|
<button class="icon-button" @click="drawerOpen = false"><svg><use href="#i-x" /></svg></button>
|
|
</header>
|
|
|
|
<div class="drawer-status">
|
|
<em class="status" :class="drawerRecord.statusClass">{{ drawerRecord.status }}</em>
|
|
<span>{{ drawerStatusText }}</span>
|
|
</div>
|
|
|
|
<section class="drawer-section">
|
|
<h3>基本信息</h3>
|
|
<dl class="property-list">
|
|
<template v-if="drawerRecord.kind === 'dock'">
|
|
<div><dt>机巢 SN</dt><dd>{{ drawerRecord.sn }}</dd></div>
|
|
<div><dt>通信卡 ICCID</dt><dd>{{ drawerRecord.iccid }}</dd></div>
|
|
<div><dt>设备位置</dt><dd>{{ drawerRecord.location }}</dd></div>
|
|
<div><dt>经度</dt><dd>{{ displayCoord(drawerRecord.longitude) }}</dd></div>
|
|
<div><dt>纬度</dt><dd>{{ displayCoord(drawerRecord.latitude) }}</dd></div>
|
|
<div><dt>控制模式</dt><dd>{{ drawerRecord.mode }}</dd></div>
|
|
<div><dt>软件版本</dt><dd>{{ drawerRecord.version }}</dd></div>
|
|
</template>
|
|
<template v-else>
|
|
<div><dt>飞控 SN</dt><dd>{{ drawerRecord.sn }}</dd></div>
|
|
<div><dt>当前 SysID</dt><dd>{{ drawerRecord.sysid }}</dd></div>
|
|
<div><dt>飞行模式</dt><dd>{{ drawerRecord.mode }}</dd></div>
|
|
<div><dt>剩余电量</dt><dd>{{ drawerRecord.battery }}</dd></div>
|
|
<div><dt>定位状态</dt><dd>{{ drawerRecord.gps }}</dd></div>
|
|
<div><dt>最近上报</dt><dd>{{ drawerRecord.updated }}</dd></div>
|
|
</template>
|
|
</dl>
|
|
</section>
|
|
|
|
<section class="drawer-section">
|
|
<div class="drawer-section-title"><h3>绑定关系</h3><button v-if="!isAdmin" @click="ui.toast('管理绑定')">管理绑定</button></div>
|
|
<button class="binding-device" @click="goBinding">
|
|
<span><svg><use :href="drawerRecord.kind === 'drone' ? '#i-home' : '#i-plane'" /></svg></span>
|
|
<div><strong>{{ drawerRecord.binding }}</strong><small>{{ drawerBindingText }}</small></div>
|
|
<svg><use href="#i-chevron" /></svg>
|
|
</button>
|
|
</section>
|
|
|
|
<footer>
|
|
<button v-if="!isAdmin && drawerRecord.kind === 'dock'" class="plain-command danger-command" @click="deleteDock(drawerRecord)">删除机巢</button>
|
|
<button v-if="!isAdmin && drawerRecord.kind === 'dock'" class="plain-command" @click="openEditDock(drawerRecord)">编辑信息</button>
|
|
<button class="primary-command" @click="goDetail(drawerRecord.id)">查看完整详情</button>
|
|
</footer>
|
|
</template>
|
|
</aside>
|
|
|
|
<t-dialog
|
|
v-model:visible="addDockVisible"
|
|
header="添加机巢"
|
|
width="520px"
|
|
dialog-class-name="add-dock-dialog"
|
|
placement="center"
|
|
>
|
|
<form novalidate @submit.prevent="submitAddDock">
|
|
<label><span>机巢名称<b>*</b></span><t-input v-model="dockForm.name" :maxlength="30" placeholder="例如:青山湖 03 号机巢" /></label>
|
|
<label><span>机巢 SN<b>*</b></span><t-input v-model="dockForm.sn" :maxlength="40" placeholder="请输入机身铭牌 SN" /></label>
|
|
<label><span>通信卡 ICCID<b>*</b></span><t-input v-model="dockForm.iccid" :maxlength="22" placeholder="请输入通信卡 ICCID" /></label>
|
|
<div class="form-row">
|
|
<label><span>经度<b>*</b></span><t-input-number v-model="dockForm.longitude" theme="normal" :min="-180" :max="180" :step="0.000001" placeholder="例如:120.219375" style="width: 100%" /></label>
|
|
<label><span>纬度<b>*</b></span><t-input-number v-model="dockForm.latitude" theme="normal" :min="-90" :max="90" :step="0.000001" placeholder="例如:30.259244" style="width: 100%" /></label>
|
|
</div>
|
|
<label><span>设备位置<em>选填</em></span><t-input v-model="dockForm.location" :maxlength="50" placeholder="例如:青山湖科技城 C 区" /></label>
|
|
<div class="form-error">{{ addDockError }}</div>
|
|
</form>
|
|
<template #footer>
|
|
<button class="plain-command" type="button" @click="addDockVisible = false">取消</button>
|
|
<button class="primary-command" type="button" @click="submitAddDock">确认添加</button>
|
|
</template>
|
|
</t-dialog>
|
|
|
|
<DockEditModal :visible="editDockVisible" :dock="editDockRecord" @close="editDockVisible = false" @saved="handleDockSaved" />
|
|
|
|
<t-dialog
|
|
v-model:visible="firmwareVisible"
|
|
header="固件升级"
|
|
width="500px"
|
|
dialog-class-name="firmware-dialog"
|
|
placement="center"
|
|
>
|
|
<div class="firmware-content" v-if="firmwareRecord">
|
|
<div class="firmware-device">
|
|
<span class="table-asset-icon"><svg><use href="#i-home" /></svg></span>
|
|
<div>
|
|
<strong>{{ firmwareRecord.name }}</strong>
|
|
<small>当前版本 {{ firmwareRecord.version }}</small>
|
|
</div>
|
|
<i class="status" :class="firmwareRecord.statusClass">{{ firmwareRecord.status }}</i>
|
|
</div>
|
|
<label>
|
|
<span>目标固件版本</span>
|
|
<t-select
|
|
v-model="upgradeFirmwareId"
|
|
:options="upgradeFirmwareOptions"
|
|
:placeholder="firmwareList.length ? '选择目标版本' : '暂无已发布固件'"
|
|
:disabled="!firmwareList.length"
|
|
clearable
|
|
style="width: 100%"
|
|
/>
|
|
</label>
|
|
<div class="firmware-checks">
|
|
<span>升级前请确认设备状态</span>
|
|
</div>
|
|
<p class="firmware-note">升级期间机巢将暂时停止远程控制和视频服务,请确认当前没有飞行任务。升级完成后设备会自动重启并恢复连接。</p>
|
|
<div class="form-error">{{ firmwareError }}</div>
|
|
</div>
|
|
<template #footer>
|
|
<button class="plain-command" type="button" @click="firmwareVisible = false">取消</button>
|
|
<button class="primary-command" type="button" :disabled="!upgradeFirmwareId || upgrading" @click="doUpgrade">{{ upgrading ? '下发中…' : '开始升级' }}</button>
|
|
</template>
|
|
</t-dialog>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { useDeviceStore } from '@/stores/modules/devicesStore'
|
|
import { useUiStore } from '@/stores/modules/uiStore'
|
|
import { useRouter } from 'vue-router'
|
|
import { useUserStore } from '@/stores/modules/userStore'
|
|
import request from '@/utils/http'
|
|
import DockEditModal from '@/components/DockEditModal.vue'
|
|
import * as urls from '@/config/urls'
|
|
|
|
const devices = useDeviceStore()
|
|
const ui = useUiStore()
|
|
const router = useRouter()
|
|
const userStore = useUserStore()
|
|
const isAdmin = computed(() => userStore.isAdmin())
|
|
|
|
const tab = ref('docks')
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
await devices.load()
|
|
loadedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
|
} catch (e) {
|
|
ui.toast(e.message || '加载设备失败')
|
|
}
|
|
if (!isAdmin.value) loadFirmwares()
|
|
})
|
|
const search = ref('')
|
|
const statusFilter = ref('')
|
|
const bindingFilter = ref('')
|
|
const statusOptions = [
|
|
{ label: '全部状态', value: '' },
|
|
{ label: '在线', value: 'online' },
|
|
{ label: '任务中', value: 'mission' },
|
|
{ label: '告警', value: 'alarm' },
|
|
{ label: '离线', value: 'offline' },
|
|
{ label: '待连接', value: 'pending' }
|
|
]
|
|
const bindingOptions = [
|
|
{ label: '全部绑定状态', value: '' },
|
|
{ label: '已绑定', value: 'bound' },
|
|
{ label: '未绑定', value: 'unbound' }
|
|
]
|
|
const loadedAt = ref('')
|
|
const drawerOpen = ref(false)
|
|
const drawerId = ref(null)
|
|
const addDockVisible = ref(false)
|
|
const editDockVisible = ref(false)
|
|
const editDockRecord = ref(null)
|
|
const addDockError = ref('')
|
|
const dockForm = reactive({ name: '', sn: '', iccid: '', longitude: undefined, latitude: undefined, location: '' })
|
|
const firmwareList = ref([])
|
|
const upgradeFirmwareId = ref(null)
|
|
const upgrading = ref(false)
|
|
const firmwareVisible = ref(false)
|
|
const firmwareError = ref('')
|
|
const firmwareRecord = computed(() => (drawerId.value ? devices.record(drawerId.value) : null))
|
|
const upgradeFirmwareOptions = computed(() =>
|
|
firmwareList.value.map((f) => ({ label: firmwareOptionLabel(f), value: f.id }))
|
|
)
|
|
|
|
const drawerRecord = computed(() => (drawerId.value ? devices.record(drawerId.value) : null))
|
|
|
|
const overview = computed(() => {
|
|
const all = [...devices.docks, ...devices.drones]
|
|
return {
|
|
docks: devices.docks.length,
|
|
online: all.filter((d) => d.statusClass === 'online' || d.statusClass === 'mission').length,
|
|
mission: all.filter((d) => d.statusClass === 'mission').length,
|
|
alarm: all.filter((d) => d.statusClass === 'alarm').length,
|
|
boundDrones: devices.drones.filter((d) => d.bindingState === 'bound').length
|
|
}
|
|
})
|
|
|
|
const updatedLabel = computed(() => (loadedAt.value ? `数据更新于 ${loadedAt.value}` : '管理机巢与无人机设备的接入、绑定与运行状态'))
|
|
|
|
const drawerStatusText = computed(() => {
|
|
const rec = drawerRecord.value
|
|
if (!rec) return ''
|
|
if (rec.statusClass === 'pending') return '机巢已添加,等待设备连接'
|
|
if (rec.statusClass === 'offline') return `设备最后上报于 ${rec.updated}`
|
|
return `状态数据更新于 ${rec.updated}`
|
|
})
|
|
|
|
const drawerBindingText = computed(() => {
|
|
const rec = drawerRecord.value
|
|
if (!rec) return ''
|
|
if (rec.kind === 'dock' && rec.bindingState === 'bound') {
|
|
const drone = devices.drones.find((d) => d.dockId === rec.dockId)
|
|
return drone?.battery && drone.battery !== '--' ? `已入巢 · 电量 ${drone.battery}` : '绑定关系正常'
|
|
}
|
|
return rec.bindingState === 'bound' ? '绑定关系正常' : '当前未绑定设备'
|
|
})
|
|
|
|
function displayCoord(value) {
|
|
return value == null || value === '' ? '--' : value
|
|
}
|
|
|
|
function matchesFilters(d) {
|
|
const q = search.value.trim().toLowerCase()
|
|
if (statusFilter.value && d.statusClass !== statusFilter.value) return false
|
|
if (bindingFilter.value && d.bindingState !== bindingFilter.value) return false
|
|
if (!q) return true
|
|
const haystack = [d.name, d.code, d.sn, d.location, d.binding].filter(Boolean).join(' ').toLowerCase()
|
|
return haystack.includes(q)
|
|
}
|
|
|
|
const filteredDocks = computed(() => devices.docks.filter(matchesFilters))
|
|
|
|
const filteredDrones = computed(() => devices.drones.filter(matchesFilters))
|
|
|
|
function setTab(next) {
|
|
tab.value = next
|
|
resetFilters()
|
|
drawerOpen.value = false
|
|
}
|
|
|
|
function resetFilters() {
|
|
search.value = ''
|
|
statusFilter.value = ''
|
|
bindingFilter.value = ''
|
|
}
|
|
|
|
function openDrawer(id) {
|
|
drawerId.value = id
|
|
drawerOpen.value = true
|
|
}
|
|
|
|
function goControl(id) {
|
|
router.push({ name: 'monitor', query: { deviceId: id, control: '1' } })
|
|
}
|
|
|
|
async function reload() {
|
|
try {
|
|
await devices.load()
|
|
loadedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
|
ui.toast('设备列表已刷新')
|
|
} catch (e) {
|
|
ui.toast(e.message || '刷新设备列表失败')
|
|
}
|
|
}
|
|
|
|
function goDetail(id) {
|
|
drawerOpen.value = false
|
|
router.push({ name: 'device-detail', params: { id } })
|
|
}
|
|
|
|
async function openFirmwareUpgrade(id) {
|
|
drawerId.value = id
|
|
firmwareError.value = ''
|
|
upgradeFirmwareId.value = null
|
|
firmwareVisible.value = true
|
|
await loadFirmwares()
|
|
if (!firmwareList.value.length && !firmwareError.value) {
|
|
firmwareError.value = '暂无已发布固件,请先在固件管理中发布版本。'
|
|
}
|
|
}
|
|
|
|
function goBinding() {
|
|
ui.toast('请选择要绑定的设备')
|
|
}
|
|
|
|
function openEditDock(dock) {
|
|
editDockRecord.value = dock
|
|
editDockVisible.value = true
|
|
}
|
|
async function deleteDock(dock) {
|
|
const ok = await ui.confirm(`确认硬删除机巢「${dock.name}」?`, '删除后机巢及其关联设备数据将永久移除,且无法恢复。')
|
|
if (!ok) return
|
|
try {
|
|
await request.delete(urls.DOCK(dock.id))
|
|
drawerOpen.value = false
|
|
ui.toast('机巢已永久删除')
|
|
await devices.load()
|
|
} catch (e) {
|
|
ui.toast(e.message || '删除机巢失败')
|
|
}
|
|
}
|
|
|
|
|
|
async function handleDockSaved() {
|
|
editDockVisible.value = false
|
|
try {
|
|
await devices.load()
|
|
ui.toast('机巢信息已更新')
|
|
} catch (e) {
|
|
ui.toast(e.message || '机巢已保存,但刷新设备数据失败')
|
|
}
|
|
}
|
|
|
|
function openAddDock() {
|
|
Object.assign(dockForm, { name: '', sn: '', iccid: '', longitude: undefined, latitude: undefined, location: '' })
|
|
addDockError.value = ''
|
|
addDockVisible.value = true
|
|
}
|
|
|
|
async function submitAddDock() {
|
|
addDockError.value = ''
|
|
const name = dockForm.name.trim()
|
|
const sn = dockForm.sn.trim().toUpperCase()
|
|
const iccid = dockForm.iccid.trim().replace(/\s+/g, '')
|
|
const longitude = Number(dockForm.longitude)
|
|
const latitude = Number(dockForm.latitude)
|
|
|
|
if (!name || !sn || !iccid || dockForm.longitude === '' || dockForm.longitude == null || dockForm.latitude === '' || dockForm.latitude == null) {
|
|
addDockError.value = '请填写机巢名称、机巢 SN、通信卡 ICCID、经度和纬度。'
|
|
return
|
|
}
|
|
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
|
|
addDockError.value = '经度应在 -180~180 之间。'
|
|
return
|
|
}
|
|
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
|
|
addDockError.value = '纬度应在 -90~90 之间。'
|
|
return
|
|
}
|
|
if (devices.docks.some((dock) => dock.sn.toUpperCase() === sn || dock.iccid.replace(/--/g, '') === iccid)) {
|
|
addDockError.value = '机巢 SN 或通信卡 ICCID 已存在。'
|
|
return
|
|
}
|
|
|
|
try {
|
|
await request.post(urls.DOCKS, {
|
|
dockId: sn,
|
|
name,
|
|
code: sn,
|
|
sn,
|
|
iccid,
|
|
location: dockForm.location.trim(),
|
|
longitude,
|
|
latitude
|
|
})
|
|
addDockVisible.value = false
|
|
ui.toast('机巢已成功接入')
|
|
await devices.load()
|
|
} catch (e) {
|
|
addDockError.value = e.message || '接入失败'
|
|
}
|
|
}
|
|
|
|
async function loadFirmwares() {
|
|
try {
|
|
const list = await request.get(urls.FIRMWARES_RELEASED)
|
|
const records = Array.isArray(list) ? list : list?.records || []
|
|
firmwareList.value = records.map((f) => ({
|
|
id: String(f.id),
|
|
version: f.version,
|
|
description: f.description || '',
|
|
mandatory: Number(f.mandatory) === 1
|
|
}))
|
|
} catch (e) {
|
|
firmwareList.value = []
|
|
firmwareError.value = e.message || '加载固件版本失败'
|
|
}
|
|
}
|
|
|
|
function firmwareOptionLabel(f) {
|
|
const tag = f.mandatory ? '强制升级' : '推荐稳定版'
|
|
return f.description ? `${f.version} · ${tag} · ${f.description}` : `${f.version} · ${tag}`
|
|
}
|
|
|
|
async function doUpgrade() {
|
|
const rec = firmwareRecord.value
|
|
firmwareError.value = ''
|
|
if (!rec || !upgradeFirmwareId.value) {
|
|
firmwareError.value = '请选择目标固件版本。'
|
|
return
|
|
}
|
|
if (rec.statusClass === 'offline') {
|
|
firmwareError.value = '设备离线,无法下发升级。'
|
|
return
|
|
}
|
|
const ok = await ui.confirm('确认升级固件?', `将把机巢「${rec.name}」升级到所选版本,升级期间设备可能短暂离线。`)
|
|
if (!ok) return
|
|
upgrading.value = true
|
|
try {
|
|
await request.post(urls.DOCK_FIRMWARE_UPGRADE(rec.id), { firmwareId: Number(upgradeFirmwareId.value) })
|
|
firmwareVisible.value = false
|
|
upgradeFirmwareId.value = null
|
|
ui.toast('升级指令已下发')
|
|
} catch (e) {
|
|
firmwareError.value = e.message || '升级下发失败'
|
|
} finally {
|
|
upgrading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.asset-search :deep(.t-input) {
|
|
width: 100%;
|
|
}
|
|
.asset-search {
|
|
border: 0;
|
|
padding: 0;
|
|
height: auto;
|
|
}
|
|
.toolbar-group :deep(.t-select) {
|
|
flex-shrink: 0;
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
.add-dock-dialog .t-input,
|
|
.add-dock-dialog .t-input-number,
|
|
.firmware-dialog .t-select {
|
|
width: 100%;
|
|
}
|
|
.add-dock-dialog .t-dialog__footer,
|
|
.firmware-dialog .t-dialog__footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
.firmware-dialog .firmware-content {
|
|
padding: 0;
|
|
}
|
|
</style>
|
|
|