This commit is contained in:
2025-09-27 22:57:59 +08:00
parent 8a458ff0a4
commit ed26244cdb
12585 changed files with 1914308 additions and 3474 deletions

View File

@@ -3,6 +3,7 @@ import axios from 'axios'
const USE_MOCK = String(import.meta.env.VITE_USE_MOCK || 'false').toLowerCase() === 'true'
const storageBase = ((): string => { try { return localStorage.getItem('API_BASE_URL') || '' } catch { return '' } })()
const API_BASE_URL = (storageBase || import.meta.env.VITE_APP_API_BASE_URL || 'http://127.0.0.1:8080').replace(/\/$/, '')
export const API_BASE = API_BASE_URL
const SHOP_ID = ((): number => {
try { const v = localStorage.getItem('SHOP_ID'); if (v) return Number(v) } catch {}
return Number(import.meta.env.VITE_APP_SHOP_ID || '1')
@@ -17,11 +18,20 @@ export const http = axios.create({ baseURL: API_BASE_URL, timeout: 15000 })
http.interceptors.request.use(cfg => {
cfg.headers = cfg.headers || {}
if (!cfg.headers['X-Shop-Id']) cfg.headers['X-Shop-Id'] = String(SHOP_ID)
cfg.headers['Accept'] = cfg.headers['Accept'] || 'application/json,application/octet-stream'
const uid = ((): string => {
try { const v = localStorage.getItem('USER_ID') || '' ; if (v) return v } catch {}
return String((import.meta as any).env?.VITE_ADMIN_USER_ID || '')
})()
if (uid) cfg.headers['X-User-Id'] = uid
// 管理端接口需要 X-Admin-Id 头
const aid = ((): string => {
try { const v = localStorage.getItem('ADMIN_ID') || '' ; if (v) return v } catch {}
return String((import.meta as any).env?.VITE_ADMIN_ID || '')
})()
if (aid) cfg.headers['X-Admin-Id'] = aid
else if (uid) cfg.headers['X-Admin-Id'] = uid
else cfg.headers['X-Admin-Id'] = String(SHOP_ID)
return cfg
})
@@ -36,4 +46,12 @@ export function post<T>(url: string, body?: any) { return http.post<T>(url, body
export function put<T>(url: string, body?: any) { return http.put<T>(url, body).then(r => r.data) }
export function del<T>(url: string, body?: any) { return http.delete<T>(url, { data: body }).then(r => r.data) }
export function withBaseUrl(u: string): string {
if (!u) return u
const s = String(u)
if (/^(?:https?:)?\/\//i.test(s) || s.startsWith('data:')) return s
if (s.startsWith('/')) return API_BASE_URL + s
return API_BASE_URL + '/' + s
}

View File

@@ -5,6 +5,17 @@ import './styles/theme.scss'
import App from './App.vue'
import router from './router'
// 自动注入管理员身份(仅当本地未设置时)
try {
const k = 'USER_ID'
const existed = localStorage.getItem(k)
if (!existed || !existed.trim()) {
// 可被环境变量覆盖VITE_ADMIN_USER_ID
const envUid = String((import.meta as any).env?.VITE_ADMIN_USER_ID || '')
localStorage.setItem(k, envUid || '1')
}
} catch {}
const app = createApp(App)
app.use(ElementPlus)
app.use(router)

View File

@@ -6,12 +6,15 @@ const routes: RouteRecordRaw[] = [
path: '/',
component: Shell,
children: [
{ path: '', redirect: '/vip-review' },
{ path: 'vip-review', component: () => import('../views/vip/VipReview.vue') },
{ path: 'vip', component: () => import('../views/vip/VipList.vue') },
{ path: '', redirect: '/vip/system' },
{ path: 'vip/system', component: () => import('../views/vip/VipSystem.vue') },
{ path: 'vip/list', component: () => import('../views/vip/VipList.vue') },
{ path: 'users', component: () => import('../views/users/UserList.vue') },
{ path: 'parts', component: () => import('../views/parts/UserParts.vue') },
{ path: 'parts/submissions', component: () => import('../views/parts/Submissions.vue') },
{ path: 'parts/templates', component: () => import('../views/parts/Templates.vue') },
{ path: 'consult', component: () => import('../views/consult/ConsultList.vue') }
,{ path: 'notice/list', component: () => import('../views/notice/NoticeList.vue') }
,{ path: 'dict/units', component: () => import('../views/dict/Units.vue') }
,{ path: 'dict/categories', component: () => import('../views/dict/Categories.vue') }
]

View File

@@ -6,11 +6,14 @@
<span class="name">配件管家·管理端</span>
</div>
<el-menu :default-active="active" router background-color="transparent" text-color="var(--muted-color)" active-text-color="var(--primary-royal)">
<el-menu-item index="/vip-review"><i class="el-icon-star-off"></i><span>VIP审核</span></el-menu-item>
<el-menu-item index="/vip"><i class="el-icon-star-on"></i><span>VIP管理</span></el-menu-item>
<el-menu-item index="/vip/system"><i class="el-icon-setting"></i><span>VIP系统</span></el-menu-item>
<el-menu-item index="/vip/list"><i class="el-icon-star-on"></i><span>VIP列表</span></el-menu-item>
<el-menu-item index="/users"><i class="el-icon-user"></i><span>用户管理</span></el-menu-item>
<el-menu-item index="/parts"><i class="el-icon-box"></i><span>用户配件管理</span></el-menu-item>
<el-menu-item index="/parts/submissions"><i class="el-icon-finished"></i><span>配件审核</span></el-menu-item>
<el-menu-item index="/parts/templates"><i class="el-icon-collection"></i><span>模板管理</span></el-menu-item>
<el-menu-item index="/consult"><i class="el-icon-message"></i><span>咨询回复</span></el-menu-item>
<el-menu-item index="/notice/list"><i class="el-icon-notebook-1"></i><span>公告管理</span></el-menu-item>
<el-menu-item index="/dict/units"><i class="el-icon-collection"></i><span>主单位</span></el-menu-item>
<el-menu-item index="/dict/categories"><i class="el-icon-collection-tag"></i><span>主类别</span></el-menu-item>
</el-menu>

View File

@@ -20,19 +20,35 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
import { get } from '../../../api/http'
interface Props {
isVip: boolean
vipStart?: string | Date | null
vipEnd?: string | Date | null
placeholder?: string
const isVip = ref(false)
const vipStart = ref<string | Date | null>('')
const vipEnd = ref<string | Date | null>('')
const placeholder = ref('-')
const props = withDefaults(defineProps<{ userId?: number }>(), { userId: undefined })
const route = useRoute()
async function loadVipByUser(userId?: number){
try {
if (!userId) return
const data = await get(`/api/admin/vips/user/${userId}`)
if (data && Object.keys(data).length > 0) {
const active = Number(data.isVip || 0) === 1 && Number(data.status || 0) === 1 && (!!data.expireAt ? new Date(data.expireAt).getTime() >= Date.now() : true)
isVip.value = active
vipStart.value = data.createdAt || ''
vipEnd.value = data.expireAt || ''
} else {
isVip.value = false
vipStart.value = ''
vipEnd.value = ''
}
} catch(e) { isVip.value = false }
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '-'
})
function toDisplay(value?: string | Date | null, placeholder = '-') {
if (value === null || value === undefined || value === '') return placeholder
if (value instanceof Date) {
@@ -46,8 +62,23 @@ function toDisplay(value?: string | Date | null, placeholder = '-') {
return String(value)
}
const displayStart = computed(() => toDisplay(props.vipStart, props.placeholder))
const displayEnd = computed(() => toDisplay(props.vipEnd, props.placeholder))
const displayStart = computed(() => toDisplay(vipStart.value, placeholder.value))
const displayEnd = computed(() => toDisplay(vipEnd.value, placeholder.value))
function resolveUserId(): number | undefined {
try {
if (props.userId) return Number(props.userId)
const qId = (route.query?.userId as any) || (route.params?.userId as any)
if (qId) return Number(qId)
const ls = ((): number | undefined => { try { const v = localStorage.getItem('USER_ID') || ''; return v?Number(v):undefined } catch { return undefined } })()
return ls
} catch { return undefined }
}
onMounted(() => { const id = resolveUserId(); if (id) loadVipByUser(id) })
watch(() => [props.userId, route.fullPath], () => { const id = resolveUserId(); if (id) loadVipByUser(id) })
defineExpose({ loadVipByUser })
</script>
<style scoped>

View File

@@ -7,7 +7,7 @@
<el-select v-model="q.status" style="width:140px"><el-option label="未解决" value="open" /><el-option label="已解决" value="resolved" /></el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input v-model="q.kw" placeholder="主题/内容/用户ID" clearable />
<el-input v-model="q.kw" placeholder="内容/用户ID" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetch">查询</el-button>
@@ -19,15 +19,16 @@
<el-table :data="rows" size="large" @row-dblclick="openReply">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="topic" label="主题" width="220" />
<el-table-column prop="message" label="内容" />
<el-table-column prop="replyContent" label="回复内容" />
<el-table-column label="状态" width="120">
<template #default="{row}"><el-tag :type="row.status==='resolved'?'success':'warning'">{{ row.status==='resolved'?'已解决':'未解决' }}</el-tag></template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="220">
<template #default="{row}">
<el-button size="small" type="primary" @click="openReply(row)">回复</el-button>
<el-button size="small" type="primary" @click="openReply(row)" :disabled="row.status==='resolved'">回复</el-button>
<el-button size="small" @click="resolve(row)" v-if="row.status!=='resolved'">标记解决</el-button>
<el-button size="small" v-if="row.replyContent" @click="viewReply(row)">查看回复</el-button>
</template>
</el-table-column>
</el-table>
@@ -47,6 +48,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessageBox } from 'element-plus'
import { get, post, put } from '../../api/http'
const q = reactive({ status: 'open', kw: '' })
@@ -64,6 +66,7 @@ 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 resolve(row: any){ await put(`/api/admin/consults/${row.id}/resolve`, {}); row.status = 'resolved' }
function viewReply(row: any){ ElMessageBox.alert(row.replyContent || '无', '已回复内容', { confirmButtonText: '知道了' }) }
onMounted(fetch)
</script>

View File

@@ -27,6 +27,7 @@
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { get, post, put, del } from '../../api/http'
type Category = { id: number; name: string }
@@ -38,28 +39,43 @@ async function reload() {
try {
const res = await get<any>('/api/product-categories')
state.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (e: any) {
ElMessage.error(e?.message || '加载失败')
} finally { state.loading = false }
}
async function create() {
if (!state.newName.trim()) return
if (!state.newName || !state.newName.trim()) { ElMessage.warning('请输入名称'); return }
state.loading = true
try {
await post('/api/admin/dicts/categories', { name: state.newName.trim() })
state.newName = ''
await reload()
ElMessage.success('已新增')
} catch (e: any) {
ElMessage.error(e?.message || '新增失败')
} finally { state.loading = false }
}
async function update(row: Category) {
if (!row?.id || !row.name?.trim()) return
await put(`/api/admin/dicts/categories/${row.id}`, { name: row.name.trim() })
try {
await put(`/api/admin/dicts/categories/${row.id}`, { name: row.name.trim() })
ElMessage.success('已保存')
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
}
}
async function remove(row: Category) {
if (!row?.id) return
await del(`/api/admin/dicts/categories/${row.id}`)
await reload()
try {
await del(`/api/admin/dicts/categories/${row.id}`)
await reload()
ElMessage.success('已删除')
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
onMounted(reload)

View File

@@ -27,6 +27,7 @@
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { get, post, put, del } from '../../api/http'
type Unit = { id: number; name: string }
@@ -38,28 +39,43 @@ async function reload() {
try {
const res = await get<any>('/api/product-units')
state.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (e: any) {
ElMessage.error(e?.message || '加载失败')
} finally { state.loading = false }
}
async function create() {
if (!state.newName.trim()) return
if (!state.newName || !state.newName.trim()) { ElMessage.warning('请输入名称'); return }
state.loading = true
try {
await post('/api/admin/dicts/units', { name: state.newName.trim() })
state.newName = ''
await reload()
ElMessage.success('已新增')
} catch (e: any) {
ElMessage.error(e?.message || '新增失败')
} finally { state.loading = false }
}
async function update(row: Unit) {
if (!row?.id || !row.name?.trim()) return
await put(`/api/admin/dicts/units/${row.id}`, { name: row.name.trim() })
try {
await put(`/api/admin/dicts/units/${row.id}`, { name: row.name.trim() })
ElMessage.success('已保存')
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
}
}
async function remove(row: Unit) {
if (!row?.id) return
await del(`/api/admin/dicts/units/${row.id}`)
await reload()
try {
await del(`/api/admin/dicts/units/${row.id}`)
await reload()
ElMessage.success('已删除')
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
onMounted(reload)

View File

@@ -0,0 +1,201 @@
<template>
<div class="page">
<div class="page-title"><span class="royal">📢</span> 公告管理</div>
<div class="panel" style="padding: 12px; margin-bottom: 12px;">
<el-form :inline="true" :model="q">
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部" clearable style="width: 160px;">
<el-option label="全部" :value="''" />
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已下线" value="offline" />
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="q.kw" placeholder="标题/内容" clearable style="width: 240px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
<el-form-item style="float:right; margin-left:auto;">
<el-button type="success" @click="openEdit()">新建公告</el-button>
</el-form-item>
</el-form>
</div>
<div class="panel" style="padding:0;">
<el-table :data="rows" style="width:100%" size="large" :border="false">
<el-table-column type="index" width="60" label="#" />
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="tag" label="标签" width="120" />
<el-table-column prop="pinned" label="置顶" width="80">
<template #default="{ row }">
<el-tag v-if="row.pinned" type="warning">YES</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="startsAt" label="开始" width="180" />
<el-table-column prop="endsAt" label="结束" width="180" />
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="primary" @click="publish(row)" :disabled="row.status==='published'">发布</el-button>
<el-button size="small" type="warning" @click="offline(row)" :disabled="row.status==='offline'">下线</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="dlg.visible" :title="dlgTitle" width="680px">
<el-form :model="form" label-width="100px">
<el-form-item label="标题" required>
<el-input v-model="form.title" maxlength="120" show-word-limit />
</el-form-item>
<el-form-item label="内容" required>
<el-input v-model="form.content" type="textarea" :rows="4" maxlength="500" show-word-limit />
</el-form-item>
<el-form-item label="标签">
<el-input v-model="form.tag" maxlength="32" />
</el-form-item>
<el-form-item label="置顶">
<el-switch v-model="form.pinned" />
</el-form-item>
<el-form-item label="开始时间">
<el-date-picker v-model="form.startsAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssXXX" placeholder="不填即刻生效" />
</el-form-item>
<el-form-item label="结束时间">
<el-date-picker v-model="form.endsAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssXXX" placeholder="不填长期有效" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status" style="width: 160px;">
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已下线" value="offline" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dlg.visible=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { get, post, put } from '../../api/http'
type Notice = {
id?: number
title: string
content: string
tag?: string
pinned?: boolean
startsAt?: string | null
endsAt?: string | null
status?: 'draft'|'published'|'offline'
createdAt?: string
updatedAt?: string
}
const q = reactive({ status: '' as '' | 'draft' | 'published' | 'offline', kw: '' })
const rows = ref<Notice[]>([])
const dlg = reactive({ visible: false, editingId: 0 as number })
const form = reactive<Notice>({ title: '', content: '', tag: '', pinned: false, status: 'draft', startsAt: '', endsAt: '' })
const dlgTitle = computed(() => dlg.editingId ? '编辑公告' : '新建公告')
function statusLabel(s?: string) {
if (s === 'published') return '已发布'; if (s === 'offline') return '已下线'; return '草稿'
}
function statusTagType(s?: string) {
if (s === 'published') return 'success'; if (s === 'offline') return 'info'; return 'warning'
}
function resetQuery(){ q.status=''; q.kw=''; fetchList() }
async function fetchList(){
const data = await get<any>('/api/admin/notices', { status: q.status || undefined, kw: q.kw || undefined })
const list = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
rows.value = list.map((n: any) => ({
id: n.id,
title: n.title,
content: n.content,
tag: n.tag,
pinned: !!n.pinned,
startsAt: n.startsAt ? toDateTimeString(n.startsAt) : '',
endsAt: n.endsAt ? toDateTimeString(n.endsAt) : '',
status: n.status,
createdAt: toDateTimeString(n.createdAt),
updatedAt: toDateTimeString(n.updatedAt)
}))
}
function openEdit(row?: Notice){
dlg.editingId = row?.id || 0
form.title = row?.title || ''
form.content = row?.content || ''
form.tag = row?.tag || ''
form.pinned = !!row?.pinned
form.startsAt = row?.startsAt || ''
form.endsAt = row?.endsAt || ''
form.status = (row?.status as any) || 'draft'
dlg.visible = true
}
async function save(){
const payload: any = {
title: (form.title||'').trim(),
content: (form.content||'').trim(),
tag: form.tag||'',
pinned: !!form.pinned,
startsAt: form.startsAt || null,
endsAt: form.endsAt || null,
status: form.status
}
if (!payload.title) { return window.alert('请输入标题') }
if (!payload.content) { return window.alert('请输入内容') }
if (dlg.editingId) await put(`/api/admin/notices/${dlg.editingId}`, payload)
else await post('/api/admin/notices', payload)
dlg.visible = false
await fetchList()
}
async function publish(row: Notice){
if (!row.id) return
await post(`/api/admin/notices/${row.id}/publish`)
await fetchList()
}
async function offline(row: Notice){
if (!row.id) return
await post(`/api/admin/notices/${row.id}/offline`)
await fetchList()
}
function toDateTimeString(v: any): string {
try {
const d = new Date(v)
if (Number.isNaN(d.getTime())) return ''
const pad = (n:number)=> String(n).padStart(2,'0')
const yyyy=d.getFullYear(), MM=pad(d.getMonth()+1), dd=pad(d.getDate()), hh=pad(d.getHours()), mm=pad(d.getMinutes()), ss=pad(d.getSeconds())
return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`
} catch { return '' }
}
onMounted(fetchList)
</script>
<style scoped>
.page { position: relative; z-index: 1; }
</style>

View File

@@ -0,0 +1,322 @@
<template>
<div class="page">
<div class="page-title">配件审核</div>
<div class="panel" style="padding:12px; margin-bottom:12px;">
<el-form :inline="true" :model="query">
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" style="width:140px;">
<el-option label="全部" value="" />
<el-option label="待审核" value="pending" />
<el-option label="已通过" value="approved" />
<el-option label="已驳回" value="rejected" />
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="query.kw" placeholder="型号/名称/品牌" clearable />
</el-form-item>
<el-form-item label="提交时间">
<el-date-picker v-model="query.range" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DDTHH:mm:ss" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchList">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
<el-form-item style="margin-left:auto;">
<el-button type="success" @click="exportExcel" :loading="exporting">导出Excel</el-button>
</el-form-item>
</el-form>
</div>
<div class="panel" style="padding:0;">
<el-table :data="rows" style="width:100%" :loading="loading">
<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" />
<el-table-column prop="brand" label="品牌" width="120" />
<el-table-column prop="status" label="状态" width="120">
<template #default="{row}">
<el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="提交时间" width="180">
<template #default="{row}">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="reviewedAt" label="审核时间" width="180">
<template #default="{row}">{{ formatDate(row.reviewedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{row}">
<el-button size="small" @click="openDetail(row.id)">查看</el-button>
<el-button size="small" type="primary" @click="approve(row.id)" :disabled="row.status!=='pending'">通过</el-button>
<el-button size="small" type="danger" @click="openReject(row.id)" :disabled="row.status!=='pending'">驳回</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
background
layout="prev, pager, next, jumper, ->, total"
:total="total"
:page-size="query.size"
:current-page="query.page"
@current-change="onPage"
/>
</div>
</div>
<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-form-item>
<el-form-item label="图片">
<div class="thumb-list">
<div v-for="(img,idx) in detail.images" :key="idx" class="thumb">
<el-image :src="withBase(img)" :preview-src-list="detail.images.map(withBase)" fit="cover" />
<el-button type="text" @click="removeImage(idx)">删除</el-button>
</div>
</div>
<el-upload
action="#"
:http-request="uploadImage"
:show-file-list="false"
accept="image/*"
>
<el-button>上传图片</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="detail.visible=false">关闭</el-button>
<el-button type="primary" @click="saveDetail" :loading="saving">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="reject.visible" title="驳回原因" width="420px">
<el-input type="textarea" v-model="reject.reason" placeholder="请输入驳回原因" :rows="4" />
<template #footer>
<el-button @click="reject.visible=false">取消</el-button>
<el-button type="danger" @click="doReject" :loading="reject.loading">确认驳回</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { get, post, put, withBaseUrl, http } from '../../api/http'
const query = reactive({ status: '', kw: '', range: [], page: 1, size: 20 })
const rows = ref<any[]>([])
const total = ref(0)
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 paramRows = ref<any[]>([])
const jsonPlaceholder = '{"key":"value"}'
const saving = ref(false)
const reject = reactive({ visible: false, id: 0, reason: '', loading: false })
function statusLabel(status?: string) {
if (status === 'approved') return '已通过'
if (status === 'rejected') return '已驳回'
return '待审核'
}
function statusType(status?: string) {
if (status === 'approved') return 'success'
if (status === 'rejected') return 'danger'
return 'warning'
}
function formatDate(v?: string) {
if (!v) return '-'
try {
const d = new Date(v)
if (Number.isNaN(d.getTime())) return '-'
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')}`
} catch { return '-' }
}
function withBase(u: string){ return withBaseUrl(u) }
async function fetchList(){
loading.value = true
try {
const params: any = { status: query.status || undefined, kw: query.kw || undefined, page: query.page, size: query.size }
if (Array.isArray(query.range) && query.range.length === 2) {
params.startAt = query.range[0]
params.endAt = query.range[1]
}
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)
} catch (e:any) {
ElMessage.error(e.message || '加载失败')
} finally {
loading.value = false
}
}
function reset(){
query.status=''
query.kw=''
query.range=[]
query.page=1
fetchList()
}
function onPage(p:number){ query.page = p; fetchList() }
async function openDetail(id:number){
try {
const data = await get(`/api/admin/parts/submissions/${id}`)
detail.visible = true
detail.id = id
detail.data = data
detail.images = Array.isArray(data?.images) ? [...data.images] : []
detail.parameterText = data?.parameters ? JSON.stringify(data.parameters, null, 2) : ''
paramRows.value = []
detail.template = null
if (data?.templateId) {
try {
const t = await get(`/api/admin/part-templates/${data.templateId}`)
detail.template = t
const mp = data?.parameters || {}
paramRows.value = (t?.params||[]).map((p:any)=>({ fieldLabel: p.fieldLabel, unit: p.unit, required: !!p.required, value: mp[p.fieldKey] }))
} catch {}
}
} catch (e:any) {
ElMessage.error(e.message || '加载失败')
}
}
function removeImage(idx:number){ detail.images.splice(idx,1) }
async function uploadImage(options:any){
try {
const file = options.file
const formData = new FormData()
formData.append('file', file)
const res = await fetch(withBaseUrl('/api/attachments'), {
method: 'POST',
body: formData
})
if (!res.ok) throw new Error('上传失败')
const data = await res.json()
if (data && data.url) {
detail.images.push(data.url)
options.onSuccess(data, file)
} else {
throw new Error('上传失败')
}
} catch (e:any) {
options.onError(e)
ElMessage.error(e.message || '上传失败')
}
}
async function saveDetail(){
if (!detail.data) return
saving.value = true
try {
let paramsObj: any = null
if (detail.parameterText) {
try { paramsObj = JSON.parse(detail.parameterText) } catch { return ElMessage.warning('参数JSON格式错误') }
}
await put(`/api/admin/parts/submissions/${detail.id}`, {
name: detail.data.name,
brand: detail.data.brand,
spec: detail.data.spec,
unitId: detail.data.unitId,
categoryId: detail.data.categoryId,
parameters: paramsObj,
images: detail.images,
remark: detail.data.remark,
barcode: detail.data.barcode
})
ElMessage.success('保存成功')
detail.visible=false
fetchList()
} catch (e:any) {
ElMessage.error(e.message || '保存失败')
} finally { saving.value=false }
}
async function approve(id:number){
try {
await post(`/api/admin/parts/submissions/${id}/approve`, {})
ElMessage.success('已通过')
fetchList()
} catch (e:any) { ElMessage.error(e.message || '操作失败') }
}
function openReject(id:number){ reject.visible=true; reject.id=id; reject.reason='' }
async function doReject(){
if (!reject.reason.trim()) return ElMessage.warning('请输入驳回原因')
reject.loading=true
try {
await post(`/api/admin/parts/submissions/${reject.id}/reject`, { remark: reject.reason })
reject.visible=false
detail.visible=false
ElMessage.success('已驳回')
fetchList()
} catch (e:any) {
ElMessage.error(e.message || '操作失败')
} finally { reject.loading=false }
}
async function exportExcel(){
exporting.value = true
try {
const params: any = { status: query.status || undefined, kw: query.kw || undefined }
if (Array.isArray(query.range) && query.range.length === 2) {
params.startAt = query.range[0]
params.endAt = query.range[1]
}
const res = await http.get('/api/admin/parts/submissions/export', { params, responseType: 'blob' })
const blob = res.data
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `配件审核_${Date.now()}.xlsx`
link.click()
URL.revokeObjectURL(link.href)
} catch (e:any) { ElMessage.error(e.message || '导出失败') }
finally { exporting.value=false }
}
onMounted(fetchList)
</script>
<style scoped>
.page { position: relative; }
.pager { padding: 12px; display:flex; justify-content:flex-end; }
.detail-form { max-height: 60vh; overflow-y:auto; }
.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; }
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div class="page">
<div class="header">
<h2>模板管理</h2>
<div>
<el-button type="primary" @click="openCreate()">新建模板</el-button>
</div>
</div>
<el-form inline :model="query">
<el-form-item label="分类">
<el-select v-model="query.categoryId" clearable placeholder="全部分类" @change="load()" style="width:200px">
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
</el-form>
<el-table :data="list" style="width:100%" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="模板名" />
<el-table-column prop="categoryId" label="分类ID" width="100" />
<el-table-column prop="modelRule" label="型号规则" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}">
<el-tag :type="row.status===1?'success':'info'">{{ row.status===1?'启用':'停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{row}">
<el-button type="primary" text @click="openEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dlg.visible" :title="dlg.id? '编辑模板':'新建模板'" width="720">
<el-form :model="form" label-width="100px">
<el-form-item label="分类">
<el-select v-model="form.categoryId" placeholder="选择分类">
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
<el-form-item label="模板名">
<el-input v-model="form.name" maxlength="120" />
</el-form-item>
<el-form-item label="型号规则">
<el-input v-model="form.modelRule" placeholder="可填备注或正则" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-divider>参数字段</el-divider>
<div>
<el-button size="small" @click="addParam()">新增字段</el-button>
</div>
<el-table :data="form.params" size="small" style="width:100%;margin-top:10px">
<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="英文字母/下划线" /></template>
</el-table-column>
<el-table-column label="名称">
<template #default="{row}"><el-input v-model="row.fieldLabel" /></template>
</el-table-column>
<el-table-column label="类型" width="120">
<template #default="{row}">
<el-select v-model="row.type" style="width:110px">
<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-select>
</template>
</el-table-column>
<el-table-column label="必填" width="80">
<template #default="{row}"><el-switch v-model="row.required" /></template>
</el-table-column>
<el-table-column label="单位" width="120">
<template #default="{row}"><el-input v-model="row.unit" /></template>
</el-table-column>
<el-table-column label="枚举项">
<template #default="{row}"><el-input v-model="row.enumOptionsText" placeholder="逗号分隔type=enum" /></template>
</el-table-column>
<el-table-column label="检索" width="80">
<template #default="{row}"><el-switch v-model="row.searchable" /></template>
</el-table-column>
<el-table-column label="去重" width="80">
<template #default="{row}"><el-switch v-model="row.dedupeParticipate" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }"><el-button text type="danger" @click="removeParam($index)">删除</el-button></template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button @click="dlg.visible=false">取消</el-button>
<el-button type="primary" @click="save()">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { http } from '../../api/http'
const list = ref<any[]>([])
const categories = ref<any[]>([])
const query = reactive({ categoryId: undefined as any })
const dlg = reactive({ visible: false, id: 0 })
const form = reactive<any>({ id: 0, categoryId: undefined, name: '', modelRule: '', status: 1, params: [] as any[] })
function load() {
http.get('/api/product-templates', { params: { categoryId: query.categoryId } }).then(res => {
list.value = res.data.list || res.data || []
})
}
function loadCategories() {
http.get('/api/product-categories').then(res => {
categories.value = res.data.list || res.data || []
})
}
function openCreate() {
dlg.visible = true; dlg.id = 0
Object.assign(form, { id:0, categoryId: undefined, name:'', modelRule:'', status:1, params:[] })
}
function openEdit(row:any) {
dlg.visible = true; dlg.id = row.id
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(',') })) })
})
}
function addParam() {
form.params.push({ fieldKey:'', fieldLabel:'', type:'string', required:false, unit:'', enumOptionsText:'', searchable:false, dedupeParticipate:false, sortOrder:0 })
}
function removeParam(i:number) {
form.params.splice(i,1)
}
function save() {
// 前端校验fieldKey/fieldLabel/类型/重复
const seen = new Set<string>()
for (const p of form.params) {
const key = String(p.fieldKey||'').trim()
const label = String(p.fieldLabel||'').trim()
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 (seen.has(key)) { ElMessage.warning(`参数键重复: ${key}`); return }
seen.add(key)
}
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 })) }
const req = dlg.id ? http.put(`/api/admin/part-templates/${dlg.id}`, { ...payload, deleteAllRelatedProductsAndSubmissions: true })
: http.post('/api/admin/part-templates', payload)
req.then(()=>{ ElMessage.success('保存成功'); dlg.visible=false; load() })
}
onMounted(()=>{ loadCategories(); load() })
</script>
<style scoped>
.page { padding: 16px; }
.header { display:flex; align-items:center; justify-content: space-between; margin-bottom: 12px; }
</style>

View File

@@ -3,13 +3,6 @@
<div class="page-title"><span class="royal"></span> 用户配件管理</div>
<div class="panel" style="padding:12px; margin-bottom: 12px;">
<el-form :inline="true" :model="q">
<el-form-item label="状态">
<el-select v-model="q.status" style="width:140px">
<el-option label="待审" value="pending" />
<el-option label="通过" value="approved" />
<el-option label="驳回" value="rejected" />
</el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input v-model="q.kw" placeholder="品牌/型号/规格/用户ID" clearable />
</el-form-item>
@@ -29,19 +22,12 @@
<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="img" fit="cover" style="width:48px;height:48px;border-radius:8px;" :preview-src-list="row.images" />
<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)" />
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="140">
<el-table-column fixed="right" 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="260">
<template #default="{row}">
<el-button size="small" type="warning" @click="blacklist(row)" v-if="row.status===1">拉黑</el-button>
<el-button size="small" type="success" @click="restore(row)" v-else>恢复</el-button>
<el-button size="small" @click="edit(row)">编辑</el-button>
</template>
</el-table-column>
@@ -65,9 +51,9 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { get, put } from '../../api/http'
import { get, put, withBaseUrl } from '../../api/http'
const q = reactive({ status: 'pending', kw: '' })
const q = reactive({ kw: '' })
const rows = ref<any[]>([])
const visible = ref(false)
const form = reactive<any>({})
@@ -77,15 +63,12 @@ const imagesConcat = computed({
})
async function fetch(){
// 需要后端提供GET /api/admin/parts?status=&kw=
const data = await get<any>('/api/admin/parts', { status: q.status, kw: q.kw })
const data = await get<any>('/api/admin/parts', { kw: q.kw })
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
}
function reset(){ q.status='pending'; q.kw=''; fetch() }
function reset(){ q.kw=''; fetch() }
function edit(row: any){ visible.value=true; Object.assign(form, JSON.parse(JSON.stringify(row))) }
async function save(){ await put(`/api/admin/parts/${form.id}`, form); visible.value=false; await fetch() }
async function blacklist(row: any){ await put(`/api/admin/parts/${row.id}/blacklist`, {}); await fetch() }
async function restore(row: any){ await put(`/api/admin/parts/${row.id}/restore`, {}); await fetch() }
onMounted(fetch)
</script>

View File

@@ -0,0 +1,91 @@
<template>
<div class="page">
<div class="page-title"><span class="royal"></span> 供应商管理</div>
<div class="panel" style="padding:12px; margin-bottom: 12px;">
<el-form :inline="true" :model="q">
<el-form-item label="关键词">
<el-input v-model="q.kw" placeholder="名称/联系人/手机/电话" clearable />
</el-form-item>
<el-form-item>
<el-checkbox v-model="q.debtOnly">只看欠款</el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetch">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增供应商</el-button>
</el-form-item>
</el-form>
</div>
<div class="panel" style="padding:0;">
<el-table :data="rows" size="large">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="供应商名称" min-width="200" />
<el-table-column prop="contactName" label="联系人" width="140" />
<el-table-column prop="mobile" label="手机" width="160" />
<el-table-column prop="phone" label="电话" width="160" />
<el-table-column prop="address" label="经营地址" min-width="240" show-overflow-tooltip />
<el-table-column label="应付款" width="140">
<template #default="{row}">¥ {{ Number(row.apPayable||0).toFixed(2) }}</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="200">
<template #default="{row}">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="visible" :title="form.id ? '编辑供应商' : '新增供应商'" width="640">
<el-form :model="form" label-width="120px">
<el-form-item label="供应商名称"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="联系人"><el-input v-model="form.contactName" /></el-form-item>
<el-form-item label="手机"><el-input v-model="form.mobile" /></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.address" /></el-form-item>
<el-form-item label="初始应付款"><el-input v-model.number="form.apOpening" type="number" /></el-form-item>
<el-form-item label="当前应付款"><el-input v-model.number="form.apPayable" type="number" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="200" /></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>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { get, post, put } from '../../api/http'
const q = reactive({ kw: '', debtOnly: false })
const rows = ref<any[]>([])
const visible = ref(false)
const form = reactive<any>({ id: null, name:'', contactName:'', mobile:'', phone:'', address:'', apOpening:0, apPayable:0, remark:'' })
async function fetch(){
const data = await get<any>('/api/suppliers', { kw: q.kw, debtOnly: q.debtOnly, page: 1, size: 100 })
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data) ? data : [])
}
function reset(){ q.kw=''; q.debtOnly=false; fetch() }
function openCreate(){ Object.assign(form, { id:null, name:'', contactName:'', mobile:'', phone:'', address:'', apOpening:0, apPayable:0, remark:'' }); visible.value=true }
function openEdit(row: any){ Object.assign(form, row); visible.value=true }
async function save(){
if (!form.name) return (window as any).ElMessage?.warning?.('请填写供应商名称') || void 0
if (form.id) await put(`/api/suppliers/${form.id}`, form)
else await post('/api/suppliers', form)
visible.value=false
await fetch()
}
onMounted(fetch)
</script>
<style scoped>
.page { position: relative; z-index: 1; }
</style>

View File

@@ -9,7 +9,7 @@
<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-button type="success" @click="openEdit()">新增VIP</el-button>
</el-form-item>
</el-form>
</div>
@@ -49,6 +49,8 @@
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -86,6 +88,8 @@ async function toggle(row: any){
await fetch()
}
onMounted(fetch)
</script>

View File

@@ -1,80 +0,0 @@
<template>
<div class="page">
<div class="page-title"><span class="royal"></span> VIP审核</div>
<div class="panel" style="padding:12px; margin-bottom: 12px;">
<el-form :inline="true" :model="q">
<el-form-item label="关键词">
<el-input v-model="q.kw" placeholder="手机号/姓名/用户ID" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetch">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="panel" style="padding:16px;">
<el-empty v-if="rows.length===0" description="暂无待审核" />
<div v-else class="cards">
<el-card v-for="it in rows" :key="it.id" class="vip-card" shadow="hover">
<div class="vip-card__header">
<div class="avatar">{{ (it.name||'用').slice(0,1) }}</div>
<div class="meta">
<div class="name">{{ it.name || ('用户#'+it.userId) }}</div>
<div class="sub">ID {{ it.userId }} · {{ it.phone || '无手机号' }}</div>
</div>
</div>
<div class="vip-card__body">
<div class="remark" :title="it.remark || ''">{{ it.remark || '无备注' }}</div>
</div>
<div class="vip-card__actions">
<el-button size="small" type="success" @click="approve(it)">通过</el-button>
<el-button size="small" type="warning" @click="reject(it)">驳回</el-button>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { get, post } from '../../api/http'
const q = reactive({ kw: '' })
const rows = ref<any[]>([])
function levelLabel(v: string){
const m: Record<string,string> = { gold: '黄金VIP', platinum: '铂金VIP', diamond: '钻石VIP' }
return m[v] || v || 'VIP'
}
async function fetch(){
// 使用 /api/admin/vips?status=0 列出待审核未启用记录phone 作为筛选
const params: any = { status: 0 }
if (q.kw) params.phone = q.kw
const data = await get<any>('/api/admin/vips', params)
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
}
function reset(){ q.kw=''; fetch() }
async function approve(row: any){ await post(`/api/admin/vips/${row.id}/approve`, {}); await fetch() }
async function reject(row: any){ await post(`/api/admin/vips/${row.id}/reject`, {}); await fetch() }
onMounted(fetch)
</script>
<style scoped>
.page { position: relative; z-index: 1; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.vip-card { border-radius: 14px; overflow: hidden; }
.vip-card__header { display:flex; align-items:center; gap:12px; padding: 6px 4px 6px; border-bottom: 1px solid var(--panel-border); }
.avatar { width: 36px; height: 36px; border-radius: 18px; background: rgba(180,136,15,.18); display:flex; align-items:center; justify-content:center; font-weight: 800; color: var(--primary-royal); }
.meta { display:flex; flex-direction:column; gap:2px; }
.name { font-weight: 700; }
.sub { font-size: 12px; color: var(--muted-color); }
.level { margin-left: auto; }
.vip-card__body { padding: 8px 4px; color: var(--muted-color); min-height: 44px; }
.remark { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vip-card__actions { display:flex; justify-content:flex-end; gap: 8px; padding-top: 6px; }
</style>

View File

@@ -0,0 +1,67 @@
<template>
<div class="page">
<div class="page-title"><span class="royal"></span> VIP系统</div>
<div class="panel" style="padding:12px; margin-bottom: 12px;">
<el-form :inline="true" :model="price">
<el-form-item label="VIP价格(元/月)">
<el-input v-model.number="price.value" type="number" placeholder="输入价格" style="width:160px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="savePrice">保存</el-button>
<el-button @click="loadPrice">刷新</el-button>
</el-form-item>
</el-form>
</div>
<div class="panel" style="padding:0;">
<div class="panel-title" style="padding: 12px; border-bottom: 1px solid var(--panel-border); display:flex; align-items:center; justify-content:space-between;">
<div>充值记录</div>
<div>
<el-input v-model="q.kw" placeholder="手机号/姓名" size="small" style="width:220px; margin-right: 8px;" clearable />
<el-button size="small" type="primary" @click="fetchRecharges">查询</el-button>
</div>
</div>
<el-table :data="rows" style="width:100%" size="large" :border="false">
<el-table-column type="index" width="60" label="#" />
<el-table-column prop="shopName" label="店铺" width="160" />
<el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="name" label="姓名" width="140" />
<el-table-column prop="phone" label="手机号" width="140" />
<el-table-column prop="price" label="价格" width="100" />
<el-table-column prop="durationDays" label="时长(天)" width="100" />
<el-table-column prop="expireTo" label="到期时间" width="180" />
<el-table-column prop="channel" label="渠道" width="120" />
<el-table-column prop="createdAt" label="创建时间" width="180" />
</el-table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { get, put } from '../../api/http'
const price = reactive({ value: 0 })
const rows = ref<any[]>([])
const q = reactive({ kw: '' })
async function loadPrice(){
const data = await get<any>('/api/admin/vip/system/price')
price.value = Number(data?.price || 0)
}
async function savePrice(){
await put('/api/admin/vip/system/price', { price: price.value })
}
async function fetchRecharges(){
const data = await get<any>('/api/admin/vip/system/recharges', { kw: q.kw })
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
}
onMounted(async () => { await loadPrice(); await fetchRecharges() })
</script>
<style scoped>
.page { position: relative; z-index: 1; }
</style>