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.
267 lines
9.7 KiB
267 lines
9.7 KiB
<template>
|
|
<section class="operation-page">
|
|
<header class="operation-page-header">
|
|
<div>
|
|
<h1>固件管理</h1>
|
|
<span>维护机巢固件版本,发布后供用户对设备升级</span>
|
|
</div>
|
|
<div class="page-actions">
|
|
<button class="icon-command" title="刷新" @click="load"><svg><use href="#i-refresh" /></svg></button>
|
|
<button class="primary-command" @click="openAdd"><svg><use href="#i-plus" /></svg>新增固件</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="business-workspace">
|
|
<div class="asset-toolbar">
|
|
<div class="asset-search"><svg><use href="#i-search" /></svg><input v-model="keyword" type="text" placeholder="搜索版本号 / 描述" @keyup.enter="reload" /></div>
|
|
<div class="toolbar-group">
|
|
<select v-model="statusFilter" @change="reload">
|
|
<option value="">全部状态</option>
|
|
<option value="draft">草稿</option>
|
|
<option value="released">已发布</option>
|
|
<option value="deprecated">已弃用</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="asset-table-wrap">
|
|
<table class="asset-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:12%">版本</th>
|
|
<th style="width:14%">组件</th>
|
|
<th style="width:22%">描述</th>
|
|
<th style="width:9%">大小</th>
|
|
<th style="width:8%">升级方式</th>
|
|
<th style="width:9%">状态</th>
|
|
<th style="width:16%">创建时间</th>
|
|
<th style="width:10%">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="f in list" :key="f.id">
|
|
<td><strong>{{ f.version }}</strong></td>
|
|
<td>{{ f.component }}</td>
|
|
<td><span :title="f.description">{{ f.description || '—' }}</span></td>
|
|
<td>{{ f.sizeText }}</td>
|
|
<td>{{ f.mandatory ? '强制' : '普通' }}</td>
|
|
<td><span class="table-status" :class="f.statusClass"><i></i>{{ f.statusName }}</span></td>
|
|
<td>{{ f.createdAt }}</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button @click="openEdit(f)">编辑</button>
|
|
<button class="more-action" @click="remove(f)"><svg><use href="#i-trash" /></svg></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-if="!list.length" class="table-empty">暂无固件版本</div>
|
|
</div>
|
|
|
|
<div class="table-footer">
|
|
<span>共 {{ total }} 个固件版本 · 第 {{ pageNum }} / {{ pages }} 页</span>
|
|
<div>
|
|
<button @click="goPage(pageNum - 1)"><svg><use href="#i-chevron" /></svg></button>
|
|
<b>{{ pageNum }}</b>
|
|
<button @click="goPage(pageNum + 1)"><svg><use href="#i-chevron" /></svg></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add / edit dialog -->
|
|
<div class="modal-backdrop" :class="{ open: modalVisible }">
|
|
<div class="modal add-dock-dialog" role="dialog" aria-modal="true">
|
|
<header>
|
|
<div><span>固件版本</span><h3>{{ editingId ? '编辑固件' : '新增固件' }}</h3></div>
|
|
<button class="icon-button" @click="modalVisible = false"><svg><use href="#i-x" /></svg></button>
|
|
</header>
|
|
<form novalidate @submit.prevent="submit">
|
|
<div class="form-row">
|
|
<label><span>版本号<b>*</b></span><input v-model="form.version" type="text" placeholder="例如 1.3.0" :disabled="!!editingId" /></label>
|
|
<label><span>组件</span><input v-model="form.component" type="text" placeholder="dock-edge-agent" /></label>
|
|
</div>
|
|
<label><span>版本描述</span><input v-model="form.description" type="text" placeholder="本次更新内容摘要" /></label>
|
|
<label><span>文件地址<b>*</b></span><input v-model="form.fileUrl" type="text" placeholder="https://.../firmware.bin" /></label>
|
|
<div class="form-row">
|
|
<label><span>SHA256<b>*</b></span><input v-model="form.sha256" type="text" placeholder="文件校验值" /></label>
|
|
<label><span>文件大小(MB)</span><input v-model="form.fileSizeMb" type="number" min="0" placeholder="0" /></label>
|
|
</div>
|
|
<label><span>签名</span><input v-model="form.signature" type="text" placeholder="固件签名" /></label>
|
|
<div class="form-row">
|
|
<label><span>升级方式</span>
|
|
<select v-model="form.mandatory">
|
|
<option :value="0">普通升级</option>
|
|
<option :value="1">强制升级</option>
|
|
</select>
|
|
</label>
|
|
<label><span>状态</span>
|
|
<select v-model="form.status">
|
|
<option value="draft">草稿</option>
|
|
<option value="released">已发布</option>
|
|
<option value="deprecated">已弃用</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<div class="form-error">{{ formError }}</div>
|
|
<footer>
|
|
<button class="plain-command" type="button" @click="modalVisible = false">取消</button>
|
|
<button class="primary-command" type="submit">{{ editingId ? '保存' : '确认新增' }}</button>
|
|
</footer>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
import { useUiStore } from '@/stores/modules/uiStore'
|
|
import request from '@/utils/http'
|
|
import * as urls from '@/config/urls'
|
|
|
|
const ui = useUiStore()
|
|
const list = ref([])
|
|
const total = ref(0)
|
|
const pageNum = ref(1)
|
|
const pageSize = ref(10)
|
|
const pages = ref(1)
|
|
const keyword = ref('')
|
|
const statusFilter = ref('')
|
|
|
|
const modalVisible = ref(false)
|
|
const editingId = ref(null)
|
|
const formError = ref('')
|
|
const form = reactive({ version: '', component: '', description: '', fileUrl: '', sha256: '', signature: '', fileSizeMb: '', mandatory: 0, status: 'draft' })
|
|
|
|
const STATUS = {
|
|
draft: { name: '草稿', cls: 'pending' },
|
|
released: { name: '已发布', cls: 'online' },
|
|
deprecated: { name: '已弃用', cls: 'offline' }
|
|
}
|
|
|
|
function fmtSize(b) {
|
|
if (!b) return '--'
|
|
const n = Number(b)
|
|
if (n >= 1024 * 1024 * 1024) return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
|
if (n >= 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB'
|
|
if (n >= 1024) return (n / 1024).toFixed(0) + ' KB'
|
|
return n + ' B'
|
|
}
|
|
|
|
function fmtTime(t) {
|
|
if (!t) return '--'
|
|
const d = new Date(t)
|
|
if (Number.isNaN(d.getTime())) return '--'
|
|
return d.toLocaleString('zh-CN', { hour12: false })
|
|
}
|
|
|
|
async function load() {
|
|
try {
|
|
const params = { pageNum: pageNum.value, pageSize: pageSize.value }
|
|
if (keyword.value) params.keyword = keyword.value
|
|
if (statusFilter.value) params.status = statusFilter.value
|
|
const page = await request.get(urls.FIRMWARES, { params })
|
|
total.value = page?.total || 0
|
|
pages.value = page?.pages || 1
|
|
list.value = (page?.records || []).map((f) => ({
|
|
id: String(f.id),
|
|
version: f.version,
|
|
component: f.component,
|
|
description: f.description || '',
|
|
sizeText: fmtSize(f.fileSize),
|
|
mandatory: Number(f.mandatory) === 1,
|
|
status: f.status,
|
|
statusName: STATUS[f.status]?.name || f.status,
|
|
statusClass: STATUS[f.status]?.cls || 'offline',
|
|
createdAt: fmtTime(f.createdAt),
|
|
raw: f
|
|
}))
|
|
} catch (e) {
|
|
ui.toast(e.message || '加载固件列表失败')
|
|
}
|
|
}
|
|
|
|
function reload() {
|
|
pageNum.value = 1
|
|
load()
|
|
}
|
|
|
|
function goPage(p) {
|
|
if (p < 1 || p > pages.value) return
|
|
pageNum.value = p
|
|
load()
|
|
}
|
|
|
|
function openAdd() {
|
|
Object.assign(form, { version: '', component: '', description: '', fileUrl: '', sha256: '', signature: '', fileSizeMb: '', mandatory: 0, status: 'draft' })
|
|
editingId.value = null
|
|
formError.value = ''
|
|
modalVisible.value = true
|
|
}
|
|
|
|
function openEdit(f) {
|
|
const r = f.raw
|
|
Object.assign(form, {
|
|
version: r.version,
|
|
component: r.component || '',
|
|
description: r.description || '',
|
|
fileUrl: r.fileUrl || '',
|
|
sha256: r.sha256 || '',
|
|
signature: r.signature || '',
|
|
fileSizeMb: r.fileSize ? (r.fileSize / 1024 / 1024).toFixed(1) : '',
|
|
mandatory: Number(r.mandatory) === 1 ? 1 : 0,
|
|
status: r.status || 'draft'
|
|
})
|
|
editingId.value = f.id
|
|
formError.value = ''
|
|
modalVisible.value = true
|
|
}
|
|
|
|
async function submit() {
|
|
formError.value = ''
|
|
if (!form.version || !form.fileUrl || !form.sha256) {
|
|
formError.value = '请填写版本号、文件地址和 SHA256。'
|
|
return
|
|
}
|
|
const payload = {
|
|
version: form.version,
|
|
component: form.component || 'dock-edge-agent',
|
|
description: form.description,
|
|
fileUrl: form.fileUrl,
|
|
sha256: form.sha256,
|
|
signature: form.signature || '',
|
|
fileSize: Math.round(Number(form.fileSizeMb || 0) * 1024 * 1024),
|
|
mandatory: Number(form.mandatory) === 1 ? 1 : 0,
|
|
status: form.status || 'draft'
|
|
}
|
|
try {
|
|
if (editingId.value) {
|
|
payload.status = form.status
|
|
await request.put(urls.FIRMWARE(editingId.value), payload)
|
|
ui.toast('固件已更新')
|
|
} else {
|
|
await request.post(urls.FIRMWARES, payload)
|
|
ui.toast(form.status === 'released' ? '固件已新增并发布' : '固件已新增(草稿状态)')
|
|
}
|
|
modalVisible.value = false
|
|
await load()
|
|
} catch (e) {
|
|
formError.value = e.message || '保存失败'
|
|
}
|
|
}
|
|
|
|
async function remove(f) {
|
|
const ok = await ui.confirm(`确认删除固件版本 ${f.version}?`, '删除后不可恢复,已绑定该版本的设备不受影响。')
|
|
if (!ok) return
|
|
try {
|
|
await request.delete(urls.FIRMWARE(f.id))
|
|
ui.toast('固件已删除')
|
|
await load()
|
|
} catch (e) {
|
|
ui.toast(e.message || '删除失败')
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|