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.
297 lines
12 KiB
297 lines
12 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 p-4 mb-4">
|
|
<div class="flex flex-wrap items-end gap-3">
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-xs text-slate-500">状态</span></label>
|
|
<select v-model.number="filterEnabled" class="select select-bordered select-sm w-32">
|
|
<option :value="-1">全部</option>
|
|
<option :value="1">启用中</option>
|
|
<option :value="0">已停用</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control flex-1 min-w-[220px]">
|
|
<label class="label py-1"><span class="label-text text-xs text-slate-500">关键字</span></label>
|
|
<input v-model="keyword" type="text" class="input input-bordered input-sm w-full" placeholder="厂商名称 / CID" />
|
|
</div>
|
|
<button class="btn btn-ghost btn-sm" @click="resetFilter">重置</button>
|
|
<span class="text-xs text-slate-400 ml-auto">共 {{ viewRows.length }} / {{ rows.length }} 条</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th class="cursor-pointer select-none" @click="toggleSort('id')">CID{{ sortMark('id') }}</th>
|
|
<th class="cursor-pointer select-none" @click="toggleSort('name')">厂商名称{{ sortMark('name') }}</th>
|
|
<th class="cursor-pointer select-none" @click="toggleSort('sort')">排序{{ sortMark('sort') }}</th>
|
|
<th>方案商数</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="loading">
|
|
<td colspan="6" class="text-center py-8"><span class="loading loading-spinner loading-sm"></span></td>
|
|
</tr>
|
|
<tr v-else-if="!viewRows.length">
|
|
<td colspan="6" class="text-center text-slate-400 py-8">暂无数据</td>
|
|
</tr>
|
|
<tr v-for="v in viewRows" :key="v.id">
|
|
<td class="font-mono text-sm">
|
|
<span class="badge badge-primary badge-outline badge-sm">0x{{ hexCid(v.id) }}</span>
|
|
</td>
|
|
<td class="font-medium">{{ v.name }}</td>
|
|
<td class="text-sm text-slate-500">{{ v.sort || 0 }}</td>
|
|
<td class="text-sm">
|
|
<span class="badge badge-sm" :class="providerCount(v.id) ? 'badge-primary badge-outline' : 'badge-ghost'">
|
|
{{ providerCount(v.id) }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-sm" :class="v.enabled === false ? 'badge-ghost' : 'badge-success badge-outline'">
|
|
{{ v.enabled === false ? '已停用' : '启用中' }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-1">
|
|
<button class="btn btn-ghost btn-xs" @click="showForm(v)">编辑</button>
|
|
<button class="btn btn-ghost btn-xs text-red-500" @click="delVendor(v)">删除</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-xs text-slate-400 mt-3">
|
|
芯片厂商用于给「方案商」归组:同一家芯片厂商下往往有多套方案,每套方案在
|
|
<NuxtLink to="/provider" class="link link-primary">方案商管理</NuxtLink> 里单独建一条记录。
|
|
名下还有方案商的厂商不能删除,需要下线时改成「停用」——停用后不再出现在新建方案商的下拉里,
|
|
但存量方案商仍能正常显示它的名字。
|
|
</p>
|
|
</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-2xl">
|
|
<form @submit.prevent="saveVendor">
|
|
<!-- CID 就是这条记录的主键,手工指定的十六进制编码,可改(改号会级联更新方案商引用) -->
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">CID(十六进制)*</span></label>
|
|
<div class="flex items-stretch">
|
|
<span class="bg-slate-100 text-slate-400 font-mono px-3 flex items-center rounded-l-lg border border-r-0 border-slate-200">0x</span>
|
|
<input
|
|
v-model="form.cidHex"
|
|
type="text"
|
|
class="input input-bordered w-full font-mono uppercase rounded-l-none"
|
|
placeholder="ABF4"
|
|
maxlength="8"
|
|
required
|
|
/>
|
|
</div>
|
|
<span class="label-text-alt text-slate-400 mt-1">
|
|
CID 就是芯片厂商的唯一标识,全后台不可重复。<b>可以随时改</b>——改号时挂在它下面的方案商会一起跟过去。
|
|
</span>
|
|
</div>
|
|
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">厂商名称 *</span></label>
|
|
<input v-model="form.name" type="text" class="input input-bordered w-full" placeholder="如:杰里 / 恒玄 / 博通" 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>
|
|
<input v-model.number="form.sort" type="number" class="input input-bordered w-full" />
|
|
<span class="label-text-alt text-slate-400 mt-1">小的排前面,相同则按 ID</span>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">状态</span></label>
|
|
<label class="label cursor-pointer justify-start gap-3">
|
|
<input v-model="form.enabled" type="checkbox" class="toggle toggle-primary" />
|
|
<span class="label-text">{{ form.enabled ? '启用中' : '已停用' }}</span>
|
|
</label>
|
|
<span class="label-text-alt text-slate-400 mt-1">停用后不再出现在新建方案商的下拉里</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-2">
|
|
<button type="submit" class="btn btn-primary text-white" :disabled="submitting">
|
|
<span v-if="submitting" class="loading loading-spinner loading-xs"></span>
|
|
{{ isEditing ? '保存' : '创建' }}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost" @click="showList()">取消</button>
|
|
</div>
|
|
</form>
|
|
</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'
|
|
import { useTableSort } from '~/composables/useTableSort'
|
|
import { invalidateChipVendors, type ChipVendor } from '~/utils/chipvendors'
|
|
|
|
definePageMeta({ title: '芯片管理' })
|
|
|
|
interface Provider {
|
|
id: number
|
|
vendor: number
|
|
}
|
|
|
|
const { webApi } = useApi()
|
|
const { success, error, warn } = useToast()
|
|
|
|
const view = ref<'list' | 'form'>('list')
|
|
const rows = ref<ChipVendor[]>([])
|
|
// 方案商列表只用来在「方案商数」列上做引用计数,让运营删之前一眼看出有没有人挂着
|
|
const providers = ref<Provider[]>([])
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const isEditing = ref(false)
|
|
const filterEnabled = ref(-1) // -1 全部;1 启用中;0 已停用
|
|
const keyword = ref('')
|
|
|
|
const form = reactive({ id: 0, name: '', cidHex: '', sort: 0, enabled: true })
|
|
|
|
// CID 统一按十六进制大写展示;0 视为未设置,由模板另行处理,这里不会被调到
|
|
function hexCid(v?: number) {
|
|
return Number(v || 0).toString(16).toUpperCase()
|
|
}
|
|
function providerCount(vendorId: number) {
|
|
return providers.value.filter((p) => Number(p.vendor || 0) === vendorId).length
|
|
}
|
|
|
|
const { toggleSort, sortMark, applySort } = useTableSort<ChipVendor>({
|
|
id: (a, b) => Number(a.id) - Number(b.id),
|
|
name: (a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'zh') || Number(a.id) - Number(b.id),
|
|
sort: (a, b) => Number(a.sort || 0) - Number(b.sort || 0) || Number(a.id) - Number(b.id),
|
|
}, 'sort')
|
|
|
|
const viewRows = computed(() => {
|
|
const kw = keyword.value.trim().toLowerCase()
|
|
const list = rows.value.filter((v) => {
|
|
if (filterEnabled.value >= 0 && (v.enabled === false ? 0 : 1) !== filterEnabled.value) return false
|
|
if (!kw) return true
|
|
return (v.name || '').toLowerCase().includes(kw) || hexCid(v.id).toLowerCase().includes(kw)
|
|
})
|
|
return applySort(list)
|
|
})
|
|
function resetFilter() {
|
|
filterEnabled.value = -1
|
|
keyword.value = ''
|
|
}
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const d = await webApi<{ vendors: ChipVendor[] }>('getchipvendors', {})
|
|
rows.value = d?.vendors ?? []
|
|
} catch (e: any) {
|
|
error(e?.message ?? '获取芯片厂商列表失败')
|
|
rows.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
// 引用计数用,拉不到不影响主流程(列显示 0)
|
|
try {
|
|
const d = await webApi<{ providers: Provider[] }>('getsolutionproviders', {})
|
|
providers.value = d?.providers ?? []
|
|
} catch {
|
|
providers.value = []
|
|
}
|
|
}
|
|
|
|
function showList() {
|
|
view.value = 'list'
|
|
}
|
|
|
|
function showForm(v?: ChipVendor) {
|
|
if (v) {
|
|
isEditing.value = true
|
|
form.id = Number(v.id)
|
|
form.name = v.name || ''
|
|
form.cidHex = hexCid(v.id)
|
|
form.sort = Number(v.sort || 0)
|
|
form.enabled = v.enabled !== false
|
|
} else {
|
|
isEditing.value = false
|
|
form.id = 0
|
|
form.name = ''
|
|
form.cidHex = ''
|
|
form.sort = 0
|
|
form.enabled = true
|
|
}
|
|
view.value = 'form'
|
|
}
|
|
|
|
async function saveVendor() {
|
|
if (!form.name.trim()) { warn('请填写厂商名称'); return }
|
|
// CID 即主键,必填且必须是合法十六进制
|
|
const hex = form.cidHex.trim().replace(/^0x/i, '')
|
|
if (!/^[0-9a-fA-F]{1,8}$/.test(hex)) { warn('CID 需填十六进制,如 ABF4'); return }
|
|
const cid = Number.parseInt(hex, 16)
|
|
if (!cid) { warn('CID 不能为 0'); return }
|
|
// 占用校验排除自己,否则编辑时不改号也会被自己挡住
|
|
if (rows.value.some((v) => Number(v.id) === cid && Number(v.id) !== form.id)) { warn(`CID ${hex.toUpperCase()} 已被占用`); return }
|
|
submitting.value = true
|
|
try {
|
|
const payload: { vendor: Record<string, unknown>; oldid?: number } = {
|
|
vendor: {
|
|
id: cid,
|
|
name: form.name.trim(),
|
|
sort: Number(form.sort || 0),
|
|
enabled: form.enabled,
|
|
},
|
|
}
|
|
// 编辑时带原 CID:服务端拿它定位记录,再决定要不要改主键
|
|
if (isEditing.value) payload.oldid = form.id
|
|
await webApi(isEditing.value ? 'updatechipvendor' : 'addchipvendor', payload)
|
|
success(isEditing.value ? '已保存' : '已创建')
|
|
invalidateChipVendors() // 让方案商页下次进入时拿到最新的厂商列表
|
|
showList()
|
|
await loadData()
|
|
} catch (e: any) {
|
|
error(e?.message ?? '保存失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function delVendor(v: ChipVendor) {
|
|
const n = providerCount(v.id)
|
|
if (n > 0) {
|
|
// 前端先挡一道,省一次必然失败的请求;后端 delChipVendor 也有同样的拦截,两边都不能少
|
|
warn(`「${v.name}」下还有 ${n} 个方案商,请先把它们改到别的厂商下,或把本厂商改成「停用」`)
|
|
return
|
|
}
|
|
if (!window.confirm(`确认删除芯片厂商「${v.name}」?`)) return
|
|
webApi('delchipvendor', { id: v.id })
|
|
.then(async () => {
|
|
success('已删除')
|
|
invalidateChipVendors()
|
|
await loadData()
|
|
})
|
|
.catch((e: any) => error(e?.message ?? '删除失败'))
|
|
}
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|