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.
 
 
 
 
 
 

329 lines
14 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>
<!-- 筛选:芯片分组 + 关键字(公司名 / 方案说明 / ID) -->
<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="filterVendor" class="select select-bordered select-sm w-40">
<option :value="-1">全部芯片</option>
<option v-for="v in CHIP_VENDORS" :key="v.value" :value="v.value">{{ v.label }}</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="方案商公司名 / 方案说明 / ID(十六进制)" />
</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')">ID{{ sortMark('id') }}</th>
<th class="cursor-pointer select-none" @click="toggleSort('vendor')">芯片分组{{ sortMark('vendor') }}</th>
<th class="cursor-pointer select-none" @click="toggleSort('name')">方案商公司{{ sortMark('name') }}</th>
<th>方案说明</th>
<th>联系邮箱</th>
<th class="cursor-pointer select-none" @click="toggleSort('createtime')">创建时间{{ sortMark('createtime') }}</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="7" class="text-center py-8"><span class="loading loading-spinner loading-sm"></span></td>
</tr>
<tr v-else-if="!viewRows.length">
<td colspan="7" class="text-center text-slate-400 py-8">暂无数据</td>
</tr>
<tr v-for="p in viewRows" :key="p.id">
<td class="font-mono text-sm">{{ hexId(p.id) }}</td>
<td>
<span class="badge badge-sm" :class="p.vendor ? 'badge-primary badge-outline' : 'badge-ghost'">{{ chipVendorName(p.vendor) }}</span>
</td>
<td class="font-medium">{{ p.name }}</td>
<td class="text-sm text-slate-500 max-w-xs truncate" :title="p.description">{{ p.description || '' }}</td>
<td class="text-sm">
<template v-if="mailList(p.contactmails).length">
<span v-for="(e, i) in mailList(p.contactmails)" :key="i" class="badge badge-ghost badge-sm mr-1">{{ e }}</span>
</template>
<span v-else class="text-slate-300">-</span>
</td>
<td class="text-sm text-slate-500">{{ formatTs(p.createtime) }}</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-red-500" @click="delProvider(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()">&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="saveProvider">
<!-- 方案商 ID:手工指定的有含义编码,落库后不可改(已烧录进设备) -->
<div class="form-control mb-4">
<label class="label"><span class="label-text font-medium">方案商 ID(十六进制)*</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.idHex"
type="text"
class="input input-bordered w-full font-mono uppercase rounded-l-none"
:disabled="isEditing"
placeholder="C101"
maxlength="8"
required
/>
</div>
<span class="label-text-alt text-slate-400 mt-1">
方案商 ID 不自增,由人工指定成有含义的编码(如 C101 = 杰里 01 方案)。新增一律用 C 开头段(C1xx…);
历史 ID(ABEF / ABF0~ABF4 / AB21)已烧录进设备,<b>创建后不可修改</b>。
</span>
<span v-if="!isEditing && usedIdHint" class="label-text-alt text-slate-400 mt-1">{{ usedIdHint }}</span>
</div>
<!-- 芯片分组:一个芯片厂商可以有多套方案,每套方案单独一条记录 -->
<div class="form-control mb-4">
<label class="label"><span class="label-text font-medium">芯片分组 *</span></label>
<select v-model.number="form.vendor" class="select select-bordered w-full" required>
<option :value="0" disabled>请选择芯片厂商</option>
<option v-for="v in CHIP_VENDORS" :key="v.value" :value="v.value">{{ v.label }}</option>
</select>
<span class="label-text-alt text-slate-400 mt-1">同一个芯片厂商可以有多套方案,每套方案单独建一条方案商记录,靠这里归组。</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="如:深圳市XX科技有限公司" required />
</div>
<div class="form-control mb-4">
<label class="label"><span class="label-text font-medium">方案说明</span></label>
<textarea v-model="form.description" class="textarea textarea-bordered w-full" rows="3" placeholder="这套方案是做什么的:芯片型号、适用品类、能力范围等"></textarea>
</div>
<!-- 多邮箱管理 -->
<div class="form-control mb-6">
<label class="label"><span class="label-text font-medium">联系邮箱</span></label>
<div class="space-y-2">
<p v-if="!emails.length" class="text-xs text-slate-400">暂无邮箱</p>
<div v-for="(e, i) in emails" :key="i" class="flex items-center gap-2">
<span class="badge badge-lg gap-1 bg-slate-100 border-slate-200">
<svg class="h-3 w-3 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
{{ e }}
</span>
<button type="button" class="btn btn-ghost btn-xs text-red-400 hover:text-red-600" @click="removeEmail(i)">×</button>
</div>
</div>
<div class="flex gap-2 mt-2">
<input v-model="newEmail" type="email" class="input input-bordered input-sm flex-1" placeholder="输入邮箱地址" @keydown.enter.prevent="addEmail" />
<button type="button" class="btn btn-outline btn-sm btn-primary" @click="addEmail">添加</button>
</div>
</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">保存</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 { CHIP_VENDORS, chipVendorName } from '~/utils/chipvendors'
import { useTableSort } from '~/composables/useTableSort'
definePageMeta({ title: '方案商管理' })
interface Provider {
id: number
name: string
description: string
contactmails: string
createtime: number
vendor: number
}
const { webApi } = useApi()
const { success, error, warn } = useToast()
const view = ref<'list' | 'form'>('list')
const rows = ref<Provider[]>([])
const loading = ref(false)
const submitting = ref(false)
const isEditing = ref(false)
const emails = ref<string[]>([])
const newEmail = ref('')
// 列表筛选 / 排序
const filterVendor = ref(-1) // -1 全部;0 未分组;其余为芯片厂商值
const keyword = ref('')
const form = reactive({ id: 0, idHex: '', name: '', description: '', vendor: 0 })
function hexId(id: number) {
return id ? Number(id).toString(16).toUpperCase() : ''
}
function mailList(s?: string) {
return (s || '').split(',').map((e) => e.trim()).filter(Boolean)
}
function formatTs(ts?: number) {
if (!ts) return '-'
const d = new Date(ts * 1000)
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') +
' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
}
// 同芯片厂商下已占用的 ID,填新 ID 时提示一眼,避免撞号
const usedIdHint = computed(() => {
if (!form.vendor) return ''
const used = rows.value.filter((p) => Number(p.vendor) === form.vendor).map((p) => hexId(p.id))
return used.length ? `该芯片厂商已用 ID:${used.join('、')}` : '该芯片厂商下还没有方案商'
})
// 列表排序:默认按方案商 ID 升序,表头可切
const { toggleSort, sortMark, applySort } = useTableSort<Provider>({
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),
// 同芯片厂商再按 ID,同厂商的多套方案挨着排
vendor: (a, b) => Number(a.vendor || 0) - Number(b.vendor || 0) || Number(a.id) - Number(b.id),
createtime: (a, b) => Number(a.createtime || 0) - Number(b.createtime || 0) || Number(a.id) - Number(b.id),
}, 'id')
const viewRows = computed(() => {
const kw = keyword.value.trim().toLowerCase()
const list = rows.value.filter((p) => {
if (filterVendor.value >= 0 && Number(p.vendor || 0) !== filterVendor.value) return false
if (!kw) return true
return (p.name || '').toLowerCase().includes(kw) ||
(p.description || '').toLowerCase().includes(kw) ||
hexId(p.id).toLowerCase().includes(kw)
})
return applySort(list)
})
function resetFilter() {
filterVendor.value = -1
keyword.value = ''
}
async function loadData() {
loading.value = true
try {
const data = await webApi<{ providers: Provider[] }>('getsolutionproviders', {})
rows.value = data?.providers ?? []
} catch (e: any) {
error(e?.message ?? '获取方案商列表失败')
rows.value = []
} finally {
loading.value = false
}
}
function showList() {
view.value = 'list'
loadData()
}
function showForm(p?: Provider) {
if (p) {
isEditing.value = true
form.id = p.id
form.idHex = hexId(p.id)
form.name = p.name || ''
form.description = p.description || ''
form.vendor = Number(p.vendor || 0)
emails.value = mailList(p.contactmails)
} else {
isEditing.value = false
form.id = 0
form.idHex = ''
form.name = ''
form.description = ''
form.vendor = filterVendor.value > 0 ? filterVendor.value : 0
emails.value = []
}
newEmail.value = ''
view.value = 'form'
}
function addEmail() {
const val = newEmail.value.trim()
if (!val) return
if (emails.value.includes(val)) { warn('邮箱已存在'); return }
emails.value.push(val)
newEmail.value = ''
}
function removeEmail(idx: number) {
emails.value.splice(idx, 1)
}
async function saveProvider() {
if (!form.vendor) { warn('请选择芯片分组'); return }
let id = form.id
if (!isEditing.value) {
const hex = form.idHex.trim().replace(/^0x/i, '')
if (!/^[0-9a-fA-F]{1,8}$/.test(hex)) { warn('方案商 ID 需填十六进制,如 C101'); return }
id = Number.parseInt(hex, 16)
if (!id) { warn('方案商 ID 不能为 0'); return }
if (rows.value.some((p) => Number(p.id) === id)) { warn(`ID ${hex.toUpperCase()} 已被占用`); return }
}
submitting.value = true
try {
const payload: { provider: Record<string, unknown> } = {
provider: {
id,
name: form.name,
description: form.description,
contactmails: emails.value.join(','),
vendor: form.vendor,
},
}
await webApi(isEditing.value ? 'updatesolutionprovider' : 'addsolutionprovider', payload)
success('保存成功')
showList()
} catch (e: any) {
error(e?.message ?? '保存失败')
} finally {
submitting.value = false
}
}
function delProvider(p: Provider) {
if (!window.confirm(`确定删除方案商 "${p.name}" 吗?`)) return
webApi('delsolutionprovider', { id: p.id })
.then(() => { success('删除成功'); loadData() })
.catch((e: any) => error(e?.message ?? '删除失败'))
}
onMounted(loadData)
</script>