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.
508 lines
24 KiB
508 lines
24 KiB
<template>
|
|
<div>
|
|
<!-- ── 产品列表 ── -->
|
|
<div v-show="view === 'list'">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<h2 class="text-lg font-semibold text-slate-800">产品列表</h2>
|
|
<button class="btn btn-primary btn-sm text-white" @click="showForm()">+ 新增产品</button>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra text-sm">
|
|
<thead>
|
|
<tr><th>ID</th><th>图片</th><th>名称</th><th>工厂</th><th>类型</th><th>版本</th><th>功能</th><th>操作</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="loading"><td colspan="8" class="text-center py-8"><span class="loading loading-spinner loading-sm"></span></td></tr>
|
|
<tr v-else-if="!rows.length"><td colspan="8" class="text-center text-slate-400 py-8">暂无数据</td></tr>
|
|
<tr v-for="p in rows" :key="p.id">
|
|
<td class="font-mono text-sm">{{ hexId(p.id) }}</td>
|
|
<td>
|
|
<img v-if="p.productimage" :src="p.productimage" class="w-10 h-10 rounded object-cover" />
|
|
<div v-else class="w-10 h-10 rounded bg-slate-100 flex items-center justify-center text-slate-300">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
</div>
|
|
</td>
|
|
<td class="font-medium">{{ p.devicename }}</td>
|
|
<td>{{ factories[p.factoryid] || '' }}</td>
|
|
<td><span class="badge badge-outline badge-sm">{{ deviceTypes[p.devicetype] || p.devicetype }}</span></td>
|
|
<td class="text-sm"><span v-if="p.version">{{ p.version }}</span><span v-else class="text-slate-300">-</span></td>
|
|
<td><span class="text-xs text-slate-500">{{ featureCount(p) }}/14</span></td>
|
|
<td>
|
|
<div class="flex gap-1">
|
|
<button class="btn btn-ghost btn-xs" @click="showForm(p)">编辑</button>
|
|
<button class="btn btn-ghost btn-xs text-indigo-600" @click="showVersions(p)">版本</button>
|
|
<button class="btn btn-ghost btn-xs text-red-500" @click="delProduct(p)">删除</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── 产品表单 ── -->
|
|
<div v-show="view === 'form'">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<button class="btn btn-ghost btn-sm" @click="showList()">← 返回列表</button>
|
|
<h2 class="text-lg font-semibold text-slate-800">{{ isEditing ? '编辑产品' : '新增产品' }}</h2>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6 max-w-3xl">
|
|
<form @submit.prevent="saveProduct">
|
|
<h3 class="text-sm font-semibold text-slate-500 mb-3">基本信息</h3>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">产品名称 *</span></label>
|
|
<input v-model="form.devicename" type="text" class="input input-bordered w-full" required />
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">所属工厂</span></label>
|
|
<select v-model.number="form.factoryid" class="select select-bordered w-full">
|
|
<option :value="0">请选择</option>
|
|
<option v-for="f in factoryOptions" :key="f.id" :value="f.id">{{ f.factory }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">设备类型</span></label>
|
|
<select v-model.number="form.devicetype" class="select select-bordered w-full">
|
|
<option v-for="(label, val) in deviceTypeOptions" :key="val" :value="Number(val)">{{ label }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">扫描UUID</span></label>
|
|
<input v-model="form.scanuuid" type="text" class="input input-bordered w-full" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">设备码数量(每副)</span></label>
|
|
<select v-model.number="form.devicecodecount" class="select select-bordered w-full">
|
|
<option :value="1">单份 (1个/副)</option>
|
|
<option :value="2">双份 (2个/副)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 渠道 -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">渠道方</span></label>
|
|
<select v-model="form.channel" class="select select-bordered w-full">
|
|
<option value="official">官方</option>
|
|
<option value="mobile">移动</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div v-show="form.channel === 'mobile'" class="mb-4">
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="form-control"><label class="label"><span class="label-text font-medium">移动平台ID</span></label>
|
|
<input v-model="mobileExtra.platform_id" type="text" class="input input-bordered w-full" placeholder="移动平台分配的 ID" /></div>
|
|
<div class="form-control"><label class="label"><span class="label-text font-medium">移动平台KEY</span></label>
|
|
<input v-model="mobileExtra.platform_key" type="text" class="input input-bordered w-full" placeholder="移动平台分配的 KEY" /></div>
|
|
<div class="form-control"><label class="label"><span class="label-text font-medium">TUI(8位)</span></label>
|
|
<input v-model="mobileExtra.tui" type="text" maxlength="8" class="input input-bordered w-full" placeholder="CMEI 前缀,如 11113396" /></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 产品图片 -->
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">产品图片</span></label>
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex-1">
|
|
<input v-model="form.productimage" type="text" class="input input-bordered w-full input-sm mb-2" placeholder="图片URL(可手动输入或上传)" />
|
|
<div class="flex items-center gap-2">
|
|
<input type="file" accept="image/*" class="file-input file-input-bordered file-input-sm flex-1" @change="onImagePick" />
|
|
<button type="button" class="btn btn-outline btn-sm btn-primary" :disabled="imgUploading" @click="uploadImage">
|
|
<span v-if="imgUploading" class="loading loading-spinner loading-xs"></span>上传
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="form.productimage">
|
|
<img :src="form.productimage" class="w-20 h-20 object-cover rounded-lg border" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 绑定奖励 -->
|
|
<div class="divider text-sm text-slate-400">绑定奖励设置</div>
|
|
<div class="grid grid-cols-3 gap-4 mb-4">
|
|
<div class="form-control"><label class="label"><span class="label-text font-medium">VIP时长(天)</span></label>
|
|
<input v-model.number="form.bindrewardvpitime" type="number" min="0" class="input input-bordered w-full" /></div>
|
|
<div class="form-control"><label class="label"><span class="label-text font-medium">翻译时长(分钟)</span></label>
|
|
<input v-model.number="form.bindrewardtranslate" type="number" min="0" class="input input-bordered w-full" /></div>
|
|
<div class="form-control"><label class="label"><span class="label-text font-medium">会议时长(分钟)</span></label>
|
|
<input v-model.number="form.bindrewardmeeting" type="number" min="0" class="input input-bordered w-full" /></div>
|
|
</div>
|
|
|
|
<!-- 功能开关 -->
|
|
<div class="divider text-sm text-slate-400">功能开关</div>
|
|
<div class="grid grid-cols-3 gap-3 mb-6">
|
|
<label v-for="t in toggleFields" :key="t.key" class="flex items-center gap-2 cursor-pointer">
|
|
<input v-model="form.toggles[t.key]" type="checkbox" class="toggle toggle-primary toggle-sm" />
|
|
<span class="text-sm">{{ t.label }}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex gap-3">
|
|
<button type="button" class="btn btn-ghost" @click="showList()">取消</button>
|
|
<button type="submit" class="btn btn-primary text-white" :disabled="submitting">
|
|
<span v-if="submitting" class="loading loading-spinner loading-xs"></span>保存
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── 版本管理 ── -->
|
|
<div v-show="view === 'version'">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<button class="btn btn-ghost btn-sm" @click="showList()">← 返回产品列表</button>
|
|
<h2 class="text-lg font-semibold text-slate-800">版本管理 - {{ currentProduct?.devicename || '' }}</h2>
|
|
</div>
|
|
<!-- 当前版本信息 -->
|
|
<div v-if="currentProduct" class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 mb-5">
|
|
<div class="flex items-center gap-6 text-sm flex-wrap">
|
|
<span class="text-slate-400">产品ID:</span><span class="font-mono">{{ hexId(currentProduct.id) }}</span>
|
|
<span class="text-slate-400">当前版本:</span><span class="badge badge-primary badge-sm">{{ currentProduct.version || '无' }}</span>
|
|
<span class="text-slate-400">版本ID:</span><span class="text-xs">{{ currentProduct.versionid || '无' }}</span>
|
|
<span class="text-slate-400">更新包:</span><span class="text-xs max-w-md truncate">{{ currentProduct.updatepackageaddress || '无' }}</span>
|
|
</div>
|
|
</div>
|
|
<!-- 添加新版本 -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-5 mb-5">
|
|
<h3 class="text-sm font-semibold text-slate-700 mb-3">添加新版本</h3>
|
|
<form @submit.prevent="addVersion">
|
|
<div class="grid grid-cols-2 gap-4 mb-3">
|
|
<div class="form-control"><label class="label"><span class="label-text text-xs">版本号 *</span></label>
|
|
<input v-model="ver.version" type="text" class="input input-bordered input-sm w-full" placeholder="如 1.2.0" /></div>
|
|
<div class="form-control"><label class="label"><span class="label-text text-xs">描述</span></label>
|
|
<input v-model="ver.desc" type="text" class="input input-bordered input-sm w-full" placeholder="版本描述" /></div>
|
|
</div>
|
|
<div class="form-control mb-3">
|
|
<label class="label"><span class="label-text text-xs">OTA 更新包 *</span></label>
|
|
<input type="file" class="file-input file-input-bordered file-input-sm w-full" accept=".bin,.hex,.zip,.gz,.tar,.ota,.ufw,.fot" @change="onOtaPick" />
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<label class="flex items-center gap-2 cursor-pointer"><input v-model="ver.isuse" type="checkbox" class="checkbox checkbox-primary checkbox-sm" /><span class="text-sm">设为当前使用版本</span></label>
|
|
<button type="submit" class="btn btn-primary btn-sm text-white ml-auto" :disabled="!canAddVersion || versionSubmitting">
|
|
<span v-if="versionSubmitting" class="loading loading-spinner loading-xs"></span>添加版本
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- 版本列表 -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra text-sm">
|
|
<thead><tr><th>ID</th><th>版本号</th><th>描述</th><th>更新包地址</th><th>操作</th></tr></thead>
|
|
<tbody>
|
|
<tr v-if="!versions.length"><td colspan="5" class="text-center text-slate-400 py-8">暂无版本</td></tr>
|
|
<tr v-for="v in versions" :key="v.id">
|
|
<td>{{ v.id }}</td>
|
|
<td>
|
|
<span class="badge badge-sm" :class="isCurrentVer(v) ? 'badge-primary' : 'badge-outline'">{{ v.version }}</span>
|
|
<span v-if="isCurrentVer(v)" class="badge badge-success badge-xs ml-1">当前</span>
|
|
</td>
|
|
<td class="text-sm">{{ v.description || '' }}</td>
|
|
<td class="text-xs text-slate-400 max-w-xs truncate">{{ v.updatepackageaddress || '' }}</td>
|
|
<td>
|
|
<div class="flex gap-1">
|
|
<span v-if="isCurrentVer(v)" class="text-xs text-slate-400">使用中</span>
|
|
<template v-else>
|
|
<button class="btn btn-ghost btn-xs text-indigo-600" @click="useVersion(v)">设为当前</button>
|
|
<button class="btn btn-ghost btn-xs text-red-500" @click="delVersion(v)">删除</button>
|
|
</template>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { useToast } from '~/composables/useToast'
|
|
|
|
definePageMeta({ title: '产品管理' })
|
|
|
|
interface Product {
|
|
id: number; devicename: string; factoryid: number; devicetype: number
|
|
scanuuid: string; devicecodecount: number; productimage: string
|
|
channel: string; channelextra: string
|
|
bindrewardvpitime: number; bindrewardtranslate: number; bindrewardmeeting: number
|
|
version?: string; versionid?: number; updatepackageaddress?: string
|
|
[key: string]: unknown
|
|
}
|
|
interface Version { id: number; version: string; description: string; updatepackageaddress: string; productid: number }
|
|
|
|
const { webApi, uploadFile } = useApi()
|
|
const { success, error, warn } = useToast()
|
|
|
|
const deviceTypes: Record<number, string> = { 0: '未知', 1: '经典蓝牙', 2: 'BLE双端', 3: 'BLE单端', 4: '录音笔', 5: '智能眼镜', 6: '激活码卡片' }
|
|
const deviceTypeOptions: Record<number, string> = { 0: '未知', 1: '经典蓝牙耳机', 2: 'BLE双端', 3: 'BLE单端', 4: '智能录音笔', 5: '智能眼镜', 6: '激活码卡片' }
|
|
|
|
const toggleFields = [
|
|
{ key: 'iscanbindagain', label: '可重复绑定奖励' },
|
|
{ key: 'iscontinuouschat', label: '连续对话' },
|
|
{ key: 'eqenable', label: 'EQ 均衡器' },
|
|
{ key: 'keyenable', label: '按键控制' },
|
|
{ key: 'workmodeenable', label: '工作模式' },
|
|
{ key: 'callmodeenable', label: '通话模式' },
|
|
{ key: 'ledenable', label: 'LED 灯' },
|
|
{ key: 'finddeviceenable', label: '查找设备' },
|
|
{ key: 'noisereductionenable', label: '降噪' },
|
|
{ key: 'shutdownenable', label: '定时关机' },
|
|
{ key: 'showcasebattery', label: '充电盒电量' },
|
|
{ key: 'wakeupenable', label: '唤醒控制' },
|
|
{ key: 'broadcastpeertranslate', label: '播报对方翻译' },
|
|
{ key: 'hideaiassistant', label: '隐藏AI助手' },
|
|
]
|
|
|
|
const view = ref<'list' | 'form' | 'version'>('list')
|
|
const rows = ref<Product[]>([])
|
|
const factories = ref<Record<number, string>>({})
|
|
const factoryOptions = ref<{ id: number; factory: string }[]>([])
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const isEditing = ref(false)
|
|
|
|
function emptyToggles(): Record<string, boolean> {
|
|
const o: Record<string, boolean> = {}
|
|
toggleFields.forEach((t) => { o[t.key] = false })
|
|
return o
|
|
}
|
|
|
|
const form = reactive({
|
|
id: 0, devicename: '', factoryid: 0, devicetype: 0, scanuuid: '', devicecodecount: 1,
|
|
productimage: '', channel: 'official',
|
|
bindrewardvpitime: 0, bindrewardtranslate: 0, bindrewardmeeting: 0,
|
|
toggles: emptyToggles() as Record<string, boolean>,
|
|
})
|
|
const mobileExtra = reactive({ platform_id: '', platform_key: '', tui: '' })
|
|
|
|
function hexId(id: number) { return id ? Number(id).toString(16).toUpperCase() : '' }
|
|
function featureCount(p: Product) { return toggleFields.filter((t) => !!p[t.key]).length }
|
|
|
|
// ── 列表 ──
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const fd = await webApi<{ factorys: { id: number; factory: string }[] }>('getfactorys', {})
|
|
const fmap: Record<number, string> = {}
|
|
;(fd?.factorys ?? []).forEach((f) => { fmap[f.id] = f.factory })
|
|
factories.value = fmap
|
|
const pd = await webApi<{ products: Product[] }>('getproducts', {})
|
|
rows.value = pd?.products ?? []
|
|
} catch (e: any) {
|
|
error(e?.message ?? '获取产品列表失败')
|
|
rows.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadFactories() {
|
|
try {
|
|
const fd = await webApi<{ factorys: { id: number; factory: string }[] }>('getfactorys', {})
|
|
factoryOptions.value = fd?.factorys ?? []
|
|
} catch { factoryOptions.value = [] }
|
|
}
|
|
|
|
function showList() { view.value = 'list'; loadData() }
|
|
|
|
function showForm(p?: Product) {
|
|
loadFactories()
|
|
if (p) {
|
|
isEditing.value = true
|
|
form.id = p.id
|
|
form.devicename = p.devicename || ''
|
|
form.factoryid = p.factoryid || 0
|
|
form.devicetype = p.devicetype || 0
|
|
form.scanuuid = p.scanuuid || ''
|
|
form.devicecodecount = p.devicecodecount === 2 ? 2 : 1
|
|
form.productimage = p.productimage || ''
|
|
form.channel = p.channel || 'official'
|
|
form.bindrewardvpitime = p.bindrewardvpitime || 0
|
|
form.bindrewardtranslate = p.bindrewardtranslate || 0
|
|
form.bindrewardmeeting = p.bindrewardmeeting || 0
|
|
let extra: any = {}
|
|
try { extra = p.channelextra ? JSON.parse(p.channelextra) : {} } catch { extra = {} }
|
|
mobileExtra.platform_id = extra.platform_id || ''
|
|
mobileExtra.platform_key = extra.platform_key || ''
|
|
mobileExtra.tui = extra.tui || ''
|
|
toggleFields.forEach((t) => { form.toggles[t.key] = !!p[t.key] })
|
|
} else {
|
|
isEditing.value = false
|
|
form.id = 0
|
|
form.devicename = ''
|
|
form.factoryid = 0
|
|
form.devicetype = 0
|
|
form.scanuuid = ''
|
|
form.devicecodecount = 1
|
|
form.productimage = ''
|
|
form.channel = 'official'
|
|
form.bindrewardvpitime = 0
|
|
form.bindrewardtranslate = 0
|
|
form.bindrewardmeeting = 0
|
|
mobileExtra.platform_id = ''
|
|
mobileExtra.platform_key = ''
|
|
mobileExtra.tui = ''
|
|
toggleFields.forEach((t) => { form.toggles[t.key] = false })
|
|
}
|
|
view.value = 'form'
|
|
}
|
|
|
|
async function saveProduct() {
|
|
submitting.value = true
|
|
try {
|
|
let channelextra = ''
|
|
if (form.channel === 'mobile') {
|
|
channelextra = JSON.stringify({ platform_id: mobileExtra.platform_id || '', platform_key: mobileExtra.platform_key || '', tui: mobileExtra.tui || '' })
|
|
}
|
|
const product: Record<string, unknown> = {
|
|
devicename: form.devicename,
|
|
factoryid: Number(form.factoryid) || 0,
|
|
devicetype: Number(form.devicetype) || 0,
|
|
scanuuid: form.scanuuid,
|
|
devicecodecount: Number(form.devicecodecount) || 1,
|
|
productimage: form.productimage,
|
|
channel: form.channel,
|
|
channelextra,
|
|
bindrewardvpitime: Number(form.bindrewardvpitime) || 0,
|
|
bindrewardtranslate: Number(form.bindrewardtranslate) || 0,
|
|
bindrewardmeeting: Number(form.bindrewardmeeting) || 0,
|
|
}
|
|
toggleFields.forEach((t) => { product[t.key] = form.toggles[t.key] })
|
|
if (isEditing.value) product.id = Number(form.id)
|
|
await webApi(isEditing.value ? 'updateproduct' : 'addproduct', { product })
|
|
success('保存成功')
|
|
showList()
|
|
} catch (e: any) {
|
|
error(e?.message ?? '保存失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function delProduct(p: Product) {
|
|
if (!window.confirm(`确定删除产品 "${p.devicename}" 吗?`)) return
|
|
webApi('delproduct', { id: p.id })
|
|
.then(() => { success('已删除'); loadData() })
|
|
.catch((e: any) => error(e?.message ?? '删除失败'))
|
|
}
|
|
|
|
// ── 图片上传 ──
|
|
const imgFile = ref<File | null>(null)
|
|
const imgUploading = ref(false)
|
|
function onImagePick(e: Event) {
|
|
imgFile.value = (e.target as HTMLInputElement).files?.[0] ?? null
|
|
}
|
|
async function uploadImage() {
|
|
if (!imgFile.value) { warn('请先选择图片'); return }
|
|
imgUploading.value = true
|
|
try {
|
|
const path = form.id ? `products/${form.id}/images/` : 'products/images/'
|
|
const url = await uploadFile(imgFile.value, path)
|
|
form.productimage = url
|
|
success('上传成功')
|
|
} catch (e: any) {
|
|
error(e?.message ?? '上传失败')
|
|
} finally {
|
|
imgUploading.value = false
|
|
}
|
|
}
|
|
|
|
// ── 版本管理 ──
|
|
const currentProduct = ref<Product | null>(null)
|
|
const versions = ref<Version[]>([])
|
|
const versionSubmitting = ref(false)
|
|
const ver = reactive({ version: '', desc: '', isuse: false })
|
|
const otaFile = ref<File | null>(null)
|
|
|
|
const canAddVersion = computed(() => !!ver.version.trim() && !!otaFile.value)
|
|
|
|
function isCurrentVer(v: Version) { return !!currentProduct.value?.versionid && v.id === currentProduct.value.versionid }
|
|
|
|
async function showVersions(p: Product) {
|
|
view.value = 'version'
|
|
currentProduct.value = null
|
|
versions.value = []
|
|
ver.version = ''; ver.desc = ''; ver.isuse = false; otaFile.value = null
|
|
try {
|
|
const d = await webApi<{ product: Product }>('getproduct', { id: p.id })
|
|
currentProduct.value = d?.product ?? p
|
|
} catch { currentProduct.value = p }
|
|
await loadVersions(p.id)
|
|
}
|
|
|
|
async function loadVersions(pid: number) {
|
|
try {
|
|
const d = await webApi<{ versions: Version[] }>('getproductversions', { pid })
|
|
versions.value = d?.versions ?? []
|
|
} catch { versions.value = [] }
|
|
}
|
|
|
|
function onOtaPick(e: Event) {
|
|
otaFile.value = (e.target as HTMLInputElement).files?.[0] ?? null
|
|
}
|
|
|
|
async function switchCurrentVersion(v: Version): Promise<boolean> {
|
|
if (!currentProduct.value) { error('产品信息未加载'); return false }
|
|
const product = { ...currentProduct.value, versionid: v.id }
|
|
try {
|
|
await webApi('updateproduct', { product })
|
|
const d = await webApi<{ product: Product }>('getproduct', { id: product.id })
|
|
currentProduct.value = d?.product ?? product
|
|
return true
|
|
} catch (e: any) {
|
|
error(e?.message ?? '切换失败')
|
|
return false
|
|
}
|
|
}
|
|
|
|
function useVersion(v: Version) {
|
|
if (!window.confirm(`确定将版本 "${v.version}" 设为当前使用版本?`)) return
|
|
switchCurrentVersion(v).then((ok) => {
|
|
if (ok) { success('切换成功'); if (currentProduct.value) loadVersions(currentProduct.value.id) }
|
|
})
|
|
}
|
|
|
|
async function addVersion() {
|
|
const pid = currentProduct.value?.id
|
|
if (!pid) { error('产品ID不存在'); return }
|
|
if (!ver.version.trim()) { warn('请输入版本号'); return }
|
|
if (!otaFile.value) { warn('请选择固件文件'); return }
|
|
versionSubmitting.value = true
|
|
try {
|
|
const url = await uploadFile(otaFile.value, `products/${pid}/`)
|
|
const d = await webApi<{ version: Version }>('addproductversion', {
|
|
version: { productid: pid, version: ver.version.trim(), description: ver.desc, updatepackageaddress: url },
|
|
isuse: false,
|
|
})
|
|
success('添加成功')
|
|
if (ver.isuse && d?.version) await switchCurrentVersion(d.version)
|
|
ver.version = ''; ver.desc = ''; ver.isuse = false; otaFile.value = null
|
|
await loadVersions(pid)
|
|
} catch (e: any) {
|
|
error(e?.message ?? '添加失败')
|
|
} finally {
|
|
versionSubmitting.value = false
|
|
}
|
|
}
|
|
|
|
function delVersion(v: Version) {
|
|
if (currentProduct.value?.versionid === v.id) { error('不能删除当前使用中的版本'); return }
|
|
if (!window.confirm('删除此版本?')) return
|
|
webApi('delproductversion', { id: v.id })
|
|
.then(() => { success('已删除'); if (currentProduct.value) loadVersions(currentProduct.value.id) })
|
|
.catch((e: any) => error(e?.message ?? '删除失败'))
|
|
}
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|