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

<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()">&larr; 返回列表</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>