This commit is contained in:
2025-09-30 00:03:43 +08:00
parent 19117de6c8
commit 8885fb766a
39 changed files with 517 additions and 159 deletions

View File

@@ -15,13 +15,13 @@ export {};
type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
type __VLS_SpreadMerge<A, B> = Omit<A, keyof B> & B;
type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string, N3 extends string> =
N1 extends keyof LocalComponents ? N1 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N1] } :
N2 extends keyof LocalComponents ? N2 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N2] } :
N3 extends keyof LocalComponents ? N3 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N3] } :
N1 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N1] } :
N2 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N2] } :
N3 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N3] } :
Self extends object ? { [K in N0]: Self } :
N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } :
N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } :
N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } :
N1 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N1] } :
N2 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N2] } :
N3 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N3] } :
{};
type __VLS_FunctionalComponentCtx<T, K> = __VLS_PickNotAny<'__ctx' extends keyof __VLS_PickNotAny<K, {}>
? K extends { __ctx?: infer Ctx } ? NonNullable<Ctx> : never : any
@@ -83,6 +83,10 @@ export {};
}
>
>;
type __VLS_EmitsToProps<T> = __VLS_PrettifyGlobal<{
[K in string & keyof T as `on${Capitalize<K>}`]?:
(...args: T[K] extends (...args: infer P) => any ? P : T[K] extends null ? any[] : never) => any;
}>;
type __VLS_ResolveEmits<
Comp,
Emits,
@@ -90,10 +94,16 @@ export {};
NormalizedEmits = __VLS_NormalizeEmits<Emits> extends infer E ? string extends keyof E ? {} : E : never,
> = __VLS_SpreadMerge<NormalizedEmits, TypeEmits>;
type __VLS_ResolveDirectives<T> = {
[K in Exclude<keyof T, keyof __VLS_GlobalDirectives> & string as `v${Capitalize<K>}`]: T[K];
[K in keyof T & string as `v${Capitalize<K>}`]: T[K];
};
type __VLS_PrettifyGlobal<T> = { [K in keyof T as K]: T[K]; } & {};
type __VLS_WithDefaultsGlobal<P, D> = {
[K in keyof P as K extends keyof D ? K : never]-?: P[K];
} & {
[K in keyof P as K extends keyof D ? never : K]: P[K];
};
type __VLS_UseTemplateRef<T> = Readonly<import('vue').ShallowRef<T | null>>;
type __VLS_ProxyRefs<T> = import('vue').ShallowUnwrapRef<T>;
function __VLS_getVForSourceType<T extends number | string | any[] | Iterable<any>>(source: T): [
item: T extends number ? number
@@ -115,7 +125,6 @@ export {};
: T extends (...args: any) => any
? T
: (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
function __VLS_makeOptional<T>(t: T): { [K in keyof T]?: T[K] };
function __VLS_asFunctionalComponent<T, K = T extends new (...args: any) => any ? InstanceType<T> : unknown>(t: T, instance?: K):
T extends new (...args: any) => any ? __VLS_FunctionalComponent<K>
: T extends () => any ? (props: {}, ctx?: any) => ReturnType<T>

View File

@@ -64,7 +64,13 @@ async function fetch(){
}
function reset(){ q.status='open'; q.kw=''; fetch() }
function openReply(row: any){ visible.value = true; current.value = row; reply.value='' }
async function sendReply(){ await post(`/api/admin/consults/${current.value.id}/reply`, { content: reply.value }); visible.value=false; await fetch() }
async function sendReply(){
const data = await post(`/api/admin/consults/${current.value.id}/reply`, { content: reply.value })
visible.value=false
// 若后端返回状态,立即更新当前行展示
if (current.value && data && data.status) current.value.status = data.status
await fetch()
}
async function resolve(row: any){ await put(`/api/admin/consults/${row.id}/resolve`, {}); row.status = 'resolved' }
function viewReply(row: any){ ElMessageBox.alert(row.replyContent || '无', '已回复内容', { confirmButtonText: '知道了' }) }

View File

@@ -28,7 +28,19 @@
</div>
<div class="panel" style="padding:0;">
<el-table :data="rows" style="width:100%" :loading="loading">
<el-table :data="rows" style="width:100%" :loading="loading" row-key="id" :expand-row-keys="expanded" @expand-change="onExpandChange">
<el-table-column type="expand" width="28">
<template #default="{row}">
<div class="expand-wrap">
<div class="expand-line">模板<b>{{ (expand[row.id]?.templateName)|| '-' }}</b></div>
<div v-if="expand[row.id]?.loading" class="expand-loading">加载中...</div>
<el-table v-else :data="expand[row.id]?.rows||[]" size="small" style="width:100%;">
<el-table-column prop="label" label="参数" min-width="160" />
<el-table-column prop="value" label="值" />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column type="index" width="60" label="#" />
<el-table-column prop="model" label="型号" width="160" />
<el-table-column prop="name" label="名称" min-width="180" />
@@ -66,36 +78,8 @@
<el-dialog v-model="detail.visible" title="配件详情" width="720px">
<el-form v-if="detail.data" :model="detail.data" label-width="108px" class="detail-form">
<el-form-item label="型号">
<el-input v-model="detail.data.model" disabled />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="detail.data.name" />
</el-form-item>
<el-form-item label="品牌">
<el-input v-model="detail.data.brand" />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="detail.data.spec" />
</el-form-item>
<el-form-item label="模板">
<el-input v-model="detail.data.templateId" disabled />
</el-form-item>
<el-form-item label="参数">
<el-table :data="paramRows" size="small" style="width:100%">
<el-table-column prop="fieldLabel" label="名称" />
<el-table-column prop="unit" label="单位" width="100" />
<el-table-column prop="required" label="必填" width="80">
<template #default="{row}"><el-tag :type="row.required?'danger':'info'">{{ row.required?'是':'否' }}</el-tag></template>
</el-table-column>
<el-table-column prop="value" label="提交值" />
</el-table>
</el-form-item>
<el-form-item label="参数JSON">
<el-input type="textarea" v-model="detail.parameterText" :rows="4" :placeholder="jsonPlaceholder" />
</el-form-item>
<el-form-item label="备注">
<el-input type="textarea" v-model="detail.data.remark" :rows="3" />
<el-input type="textarea" v-model="detail.data.remark" :rows="4" />
</el-form-item>
<el-form-item label="图片">
<div class="thumb-list">
@@ -142,6 +126,8 @@ const loading = ref(false)
const exporting = ref(false)
const detail = reactive({ visible: false, id: 0, data: null as any, images: [] as string[], parameterText: '', template: null as any })
const expand = reactive<Record<number, { loading: boolean, templateName: string, rows: Array<{label:string,value:any}> }>>({})
const expanded = ref<number[]>([])
const paramRows = ref<any[]>([])
const jsonPlaceholder = '{"key":"value"}'
const saving = ref(false)
@@ -179,6 +165,9 @@ async function fetchList(){
const res = await get('/api/admin/parts/submissions', params)
rows.value = Array.isArray(res?.list) ? res.list : []
total.value = Number(res?.total || rows.value.length)
// 默认展开全部并预加载参数
expanded.value = rows.value.map((r:any)=>Number(r.id)).filter((x:number)=>!!x)
for (const r of rows.value) { await ensureExpand(r) }
} catch (e:any) {
ElMessage.error(e.message || '加载失败')
} finally {
@@ -194,6 +183,33 @@ function reset(){
}
function onPage(p:number){ query.page = p; fetchList() }
async function ensureExpand(row:any){
const id = Number(row?.id)
if (!id) return
if (expand[id] && !expand[id].loading && (expand[id].rows||[]).length) return
expand[id] = { loading: true, templateName: '', rows: [] }
try {
const data = await get(`/api/admin/parts/submissions/${id}`)
const params = data?.parameters || {}
if (data?.templateId) {
try {
const t = await get(`/api/admin/part-templates/${data.templateId}`)
expand[id].templateName = t?.name || ''
const defs = (t?.params||[])
expand[id].rows = defs.map((p:any)=>({ label: p.fieldLabel + (p.unit?` (${p.unit})`:''), value: params[p.fieldKey] ?? '' }))
} catch {
expand[id].rows = Object.keys(params).map(k=>({ label: k, value: params[k] }))
}
} else {
expand[id].rows = Object.keys(params).map(k=>({ label: k, value: params[k] }))
}
} finally {
if (expand[id]) expand[id].loading = false
}
}
function onExpandChange(row:any){ ensureExpand(row) }
async function openDetail(id:number){
try {
const data = await get(`/api/admin/parts/submissions/${id}`)
@@ -319,4 +335,7 @@ onMounted(fetchList)
.thumb-list { display:flex; flex-wrap:wrap; gap:12px; }
.thumb { width: 120px; display:flex; flex-direction: column; gap: 8px; }
.thumb .el-image { width: 120px; height: 120px; border-radius: 8px; }
.expand-wrap { padding: 8px 12px; background: rgba(255,255,255,0.02); }
.expand-line { margin-bottom: 6px; font-size: 13px; color: #ddd; }
.expand-loading { padding: 8px 0; color:#999; }
</style>

View File

@@ -36,7 +36,7 @@
</el-table-column>
</el-table>
<el-dialog v-model="dlg.visible" :title="dlg.id? '查看模板' : '新建模板'" width="720">
<el-dialog v-model="dlg.visible" :title="dlg.id? '查看模板' : '新建模板'" width="1100">
<el-form :model="form" label-width="100px">
<el-form-item label="分类">
<el-select v-model="form.categoryId" placeholder="选择分类" :disabled="!!dlg.id">
@@ -60,20 +60,14 @@
<el-table-column label="#" width="48">
<template #default="{ $index }">{{ $index+1 }}</template>
</el-table-column>
<el-table-column label="键">
<template #default="{row}"><el-input v-model="row.fieldKey" placeholder="英文字母/下划线" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="名称">
<template #default="{row}"><el-input v-model="row.fieldLabel" :disabled="!!dlg.id" /></template>
<template #default="{row}"><el-input v-model="row.fieldLabel" :disabled="!!dlg.id" @input="onLabelInput(row)" /></template>
</el-table-column>
<el-table-column label="类型" width="120">
<template #default="{row}">
<el-select v-model="row.type" style="width:110px" :disabled="!!dlg.id">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="boolean" value="boolean" />
<el-option label="enum" value="enum" />
<el-option label="date" value="date" />
<el-option label="数字" value="number" />
<el-option label="非数字" value="string" />
</el-select>
</template>
</el-table-column>
@@ -83,15 +77,25 @@
<el-table-column label="单位" width="120">
<template #default="{row}"><el-input v-model="row.unit" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="枚举项">
<template #default="{row}"><el-input v-model="row.enumOptionsText" placeholder="逗号分隔type=enum" :disabled="!!dlg.id" /></template>
<!-- 检索默认参与前端不再显示开关 -->
<el-table-column label="可模糊" width="90">
<template #default="{row}">
<el-switch v-model="row.fuzzySearchable" :disabled="!!dlg.id || row.type!=='number'" />
</template>
</el-table-column>
<el-table-column label="检索" width="80">
<template #default="{row}"><el-switch v-model="row.searchable" :disabled="!!dlg.id" /></template>
<el-table-column label="卡片展示" width="100">
<template #default="{row}">
<el-switch v-model="row.cardDisplay" :disabled="!!dlg.id" />
</template>
</el-table-column>
<el-table-column label="去重" width="80">
<template #default="{row}"><el-switch v-model="row.dedupeParticipate" :disabled="!!dlg.id" /></template>
<el-table-column label="容差" width="160">
<template #default="{row}">
<el-input v-model="row.fuzzyTolerance" :disabled="!!dlg.id || row.type!=='number' || !row.fuzzySearchable" placeholder="默认1.0" />
<div v-if="row.unit" style="font-size:12px;color:#999">单位{{ row.unit }}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button v-if="!dlg.id" text type="danger" @click="removeParam($index)">删除</el-button>
@@ -136,7 +140,8 @@ function openView(row:any) {
http.get(`/api/admin/part-templates/${row.id}`).then(res => {
const d = res.data
Object.assign(form, { id: d.id, categoryId: d.categoryId, name: d.name, modelRule: d.modelRule, status: d.status,
params: (d.params||[]).map((p:any)=>({ ...p, enumOptionsText: (p.enumOptions||[]).join(',') })) })
params: (d.params||[]).map((p:any)=>({ ...p, enumOptionsText: (p.enumOptions||[]).join(','),
fuzzySearchable: !!p.fuzzySearchable, fuzzyTolerance: p.fuzzyTolerance })) })
})
}
@@ -146,12 +151,15 @@ function openCreate() {
}
function addParam() {
form.params.push({ fieldKey:'', fieldLabel:'', type:'string', required:false, unit:'', enumOptionsText:'', searchable:false, dedupeParticipate:false, sortOrder:0 })
form.params.push({ fieldKey:'', fieldLabel:'', type:'number', required:false, unit:'mm', enumOptionsText:'', searchable:false,
fuzzySearchable:false, fuzzyTolerance:null, sortOrder:0 })
}
function removeParam(i:number) { form.params.splice(i,1) }
function save() {
// 先为所有行生成 fieldKey基于名称拼音首字母并消重
genAllKeys()
// 校验
const seen = new Set<string>()
for (const p of form.params) {
@@ -160,13 +168,27 @@ function save() {
if (!key) { ElMessage.warning('参数键不能为空'); return }
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { ElMessage.warning(`参数键不合法: ${key}`); return }
if (!label) { ElMessage.warning('参数名称不能为空'); return }
if (!['string','number','boolean','enum','date'].includes(p.type)) { ElMessage.warning(`不支持的类型: ${p.type}`); return }
if (!['string','number'].includes(p.type)) { ElMessage.warning(`不支持的类型: ${p.type}`); return }
if (p.fuzzySearchable && p.type !== 'number') { ElMessage.warning(`仅 number 类型可开启可模糊: ${key}`); return }
if (p.fuzzySearchable && p.fuzzyTolerance !== undefined && p.fuzzyTolerance !== null && String(p.fuzzyTolerance).trim() !== '') {
const num = Number(p.fuzzyTolerance)
if (!(num > 0)) { ElMessage.warning(`容差需为正数: ${key}`); return }
p.fuzzyTolerance = num
}
if (seen.has(key)) { ElMessage.warning(`参数键重复: ${key}`); return }
seen.add(key)
}
// 限制卡片展示最多4个
if (form.params.filter((p:any)=>!!p.cardDisplay).length > 4) {
ElMessage.warning('卡片展示最多选择4个参数');
return
}
const payload:any = { categoryId: form.categoryId, name: form.name, modelRule: form.modelRule, status: form.status,
params: form.params.map((p:any)=>({ fieldKey:p.fieldKey, fieldLabel:p.fieldLabel, type:p.type, required:p.required, unit:p.unit,
enumOptions: (p.enumOptionsText||'').split(',').map((s:string)=>s.trim()).filter((s:string)=>s), searchable:p.searchable, dedupeParticipate:p.dedupeParticipate, sortOrder:p.sortOrder })) }
enumOptions: undefined, searchable:true,
fuzzySearchable: !!p.fuzzySearchable, fuzzyTolerance: (p.fuzzyTolerance===null||p.fuzzyTolerance===''? null : Number(p.fuzzyTolerance)),
cardDisplay: !!p.cardDisplay,
sortOrder:p.sortOrder })) }
http.post('/api/admin/part-templates', payload).then(()=>{
ElMessage.success('创建成功'); dlg.visible=false; load()
})
@@ -183,6 +205,51 @@ function doDelete(row:any) {
}
onMounted(()=>{ loadCategories(); load() })
// 生成拼音首字母作为键,并做合法化与消重
// 轻量拼音首字母生成:优先使用浏览器 Intl.Segmenter 分词 + 拉丁字母提取;不依赖外部包
function toInitialKey(label: string): string {
const s = String(label||'').trim()
if (!s) return ''
// 规则:取每个“词”的首字符(非中文/字母/数字作为分隔),保留字母数字;中文用简单映射(常用声母)
const simpleMap: Record<string,string> = {
'长':'c','重':'z','沈':'s','厦':'x','重庆':'c','长沙':'c'
}
let buf = ''
for (let i=0;i<s.length;i++) {
const ch = s[i]
if (/^[A-Za-z0-9]$/.test(ch)) { buf += ch[0] ; while (i+1<s.length && /[A-Za-z0-9]/.test(s[i+1])) i++; continue }
// 中文取一个字符映射为首字母非常简化
if (/^[\u4e00-\u9fa5]$/.test(ch)) { buf += (simpleMap[ch] || 'z') ; continue }
// 其它符号作为分隔不取
}
let key = buf.toLowerCase().replace(/[^a-z0-9_]/g,'')
if (!key) key = s.replace(/\s+/g,'_').replace(/[^A-Za-z0-9_]/g,'').toLowerCase()
if (!/^[A-Za-z_]/.test(key)) key = '_' + key
return key
}
function onLabelInput(row:any){
const base = toInitialKey(row.fieldLabel)
row.fieldKey = uniqueKey(base, row)
}
function genAllKeys(){
for (const r of form.params) {
if (!r.fieldKey) r.fieldKey = toInitialKey(r.fieldLabel)
}
// 再次去重
const counts: Record<string, number> = {}
form.params.forEach(r=>{ r.fieldKey = uniqueKey(r.fieldKey, r) })
}
function uniqueKey(base: string, currentRow:any){
let k = (base||'').toLowerCase()
if (!k) k = '_k'
// 确保在当前列表中唯一
const others = form.params.filter((x:any)=>x!==currentRow).map((x:any)=>String(x.fieldKey||'').toLowerCase())
if (!others.includes(k)) return k
let i = 2
while (others.includes(`${k}${i}`)) i++
return `${k}${i}`
}
</script>
<style scoped>

View File

@@ -18,11 +18,23 @@
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="brand" label="品牌" width="120" />
<el-table-column prop="model" label="型号" width="160" />
<el-table-column prop="spec" label="规格" />
<el-table-column prop="templateName" label="模板" width="220" />
<el-table-column label="模板内容">
<template #default="{row}">
<div v-if="row.attributesJson" style="display:flex; flex-wrap:wrap; gap:8px;">
<el-tag v-for="(v,k) in parseAttrs(row.attributesJson)" :key="k" type="info">{{ k }}: {{ v }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="图片" width="160">
<template #default="{row}">
<div style="display:flex; gap:6px; flex-wrap:wrap;">
<el-image v-for="(img,i) in (row.images||[])" :key="i" :src="withBaseUrl(img)" fit="cover" style="width:48px;height:48px;border-radius:8px;" :preview-src-list="(row.images||[]).map(withBaseUrl)" />
<el-image v-for="(img,i) in (row.images||[])" :key="i" :src="withBaseUrl(img)" fit="cover"
style="width:48px;height:48px;border-radius:8px;"
:z-index="3000"
:preview-teleported="true"
:preview-src-list="(row.images||[]).map(withBaseUrl)" />
</div>
</template>
</el-table-column>
@@ -71,6 +83,16 @@ function edit(row: any){ visible.value=true; Object.assign(form, JSON.parse(JSON
async function save(){ await put(`/api/admin/parts/${form.id}`, form); visible.value=false; await fetch() }
onMounted(fetch)
function parseAttrs(json: any): Record<string, any> {
try {
if (!json) return {}
if (typeof json === 'object') return json as Record<string, any>
return JSON.parse(String(json)) || {}
} catch(e){
return {}
}
}
</script>
<style scoped>

View File

@@ -24,28 +24,15 @@
<el-table-column label="状态" width="120">
<template #default="{row}"><el-tag :type="row.status===1?'success':'danger'">{{ row.status===1?'正常':'黑名单' }}</el-tag></template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="240">
<el-table-column fixed="right" label="操作" width="160">
<template #default="{row}">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button size="small" @click="toggle(row)">{{ row.status===1?'拉黑':'恢复' }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="visible" title="编辑用户" width="520">
<el-form :model="form" label-width="96px">
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="角色"><el-input v-model="form.role" placeholder="owner/staff/finance" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="是否店主"><el-switch v-model="form.isOwner" :active-value="true" :inactive-value="false" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -55,8 +42,6 @@ import { get, put } from '../../api/http'
const q = reactive({ kw: '' })
const rows = ref<any[]>([])
const visible = ref(false)
const form = reactive<any>({})
async function fetch(){
// 需要后端提供GET /api/admin/users?kw=...
@@ -64,8 +49,6 @@ async function fetch(){
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
}
function reset(){ q.kw=''; fetch() }
function openEdit(row: any){ visible.value=true; Object.assign(form, row) }
async function save(){ await put(`/api/admin/users/${form.id}`, form); visible.value=false; await fetch() }
async function toggle(row: any){
const newStatus = row.status===1?0:1
await put(`/api/admin/users/${row.id}`, { status: newStatus })

View File

@@ -9,7 +9,6 @@
<el-form-item>
<el-button type="primary" @click="fetch">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openEdit()">新增VIP</el-button>
</el-form-item>
</el-form>
</div>
@@ -19,9 +18,6 @@
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="name" label="姓名" width="140" />
<el-table-column prop="phone" label="手机号" width="160" />
<el-table-column label="VIP" width="100">
<template #default="{row}"><el-tag :type="row.isVip===1?'warning':'info'">{{ row.isVip===1?'是':'否' }}</el-tag></template>
</el-table-column>
<el-table-column prop="expireAt" label="到期时间" width="180" />
<el-table-column label="状态" width="120">
<template #default="{row}">
@@ -30,25 +26,24 @@
</el-table-column>
<el-table-column fixed="right" label="操作" width="220">
<template #default="{row}">
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="primary" @click="openExpire(row)">到期时间</el-button>
<el-button size="small" @click="toggle(row)">{{ row.status===1?'停用':'启用' }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="visible" :title="editing?'编辑VIP':'新增VIP'" width="520">
<el-form :model="form" label-width="96px">
<el-form-item label="用户ID"><el-input v-model.number="form.userId" placeholder="输入用户ID" /></el-form-item>
<el-form-item label="是否VIP"><el-switch v-model="form.isVip" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="到期时间"><el-date-picker v-model="form.expireAt" type="datetime" placeholder="选择到期时间" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="expireVisible" title="设置到期时间" width="420">
<el-form label-width="96px">
<el-form-item label="到期时间">
<el-date-picker v-model="expireAtVal" type="datetime" placeholder="选择到期时间" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="expireVisible=false">取消</el-button>
<el-button type="primary" @click="saveExpire">保存</el-button>
</template>
</el-dialog>
</div>
@@ -56,13 +51,14 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { get, post, put } from '../../api/http'
import { get, put } from '../../api/http'
const q = reactive({ phone: '' })
const rows = ref<any[]>([])
const visible = ref(false)
const editing = ref(false)
const form = reactive({ id: 0, userId: 0, isVip: 1, expireAt: '', status: 1 })
const expireVisible = ref(false)
const expireAtVal = ref<any>('')
const expireRow = ref<any>(null)
async function fetch(){
// 需要后端提供GET /api/admin/vips?phone=... (或 /api/users + /api/vips 关联)
@@ -70,24 +66,23 @@ async function fetch(){
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
}
function reset(){ q.phone=''; fetch() }
function openEdit(row?: any){
visible.value = true
editing.value = !!row
if (row) Object.assign(form, { id: row.id, userId: row.userId, isVip: row.isVip ?? 1, expireAt: row.expireAt, status: row.status })
else Object.assign(form, { id: 0, userId: 0, isVip: 1, expireAt: '', status: 1 })
}
async function save(){
const body = { userId: form.userId, isVip: form.isVip, expireAt: form.expireAt || null, status: form.status }
if (editing.value) await put(`/api/admin/vips/${form.id}`, body)
else await post('/api/admin/vips', body)
visible.value = false
await fetch()
}
async function toggle(row: any){
await put(`/api/admin/vips/${row.id}`, { ...row, status: row.status===1?0:1 })
await fetch()
}
function openExpire(row: any){
expireRow.value = row
expireAtVal.value = row?.expireAt || ''
expireVisible.value = true
}
async function saveExpire(){
if (!expireRow.value) { expireVisible.value = false; return }
await put(`/api/admin/vips/${expireRow.value.id}`, { expireAt: expireAtVal.value || null })
expireVisible.value = false
await fetch()
}
onMounted(fetch)
@@ -98,3 +93,4 @@ onMounted(fetch)
</style>