<template>
<div class="ml-page">
<eb-page-header title="会员管理" subtitle="统一维护会员档案、等级与状态">
<template #actions>
<eb-button size="small" @click="handleRefresh">
<eb-icon name="refresh" :size="14" />
刷新
</eb-button>
</template>
</eb-page-header>
<eb-section-card class="ml-card">
<eb-search-filter
v-model="query"
:fields="filterFields"
:loading="loading"
@search="onSearch"
/>
<eb-data-table
ref="tableRef"
title="会员列表"
selectable
show-index
:operations-width="120"
:loading="loading"
:columns="visibleColumns"
:data="rows"
v-model:page="page"
v-model:page-size="pageSize"
:total="total"
@page-change="load"
@selection-change="onSelectionChange"
>
<template #toolbar>
<eb-button type="primary" size="small" @click="openCreate">
<eb-icon name="plus" :size="14" />
新建会员
</eb-button>
<eb-button size="small" :disabled="!selection.length" @click="batchRemove">
批量删除{{ selection.length ? `(${selection.length})` : '' }}
</eb-button>
<eb-button size="small" @click="ioVisible = true">
<eb-icon name="swap" :size="14" />
导入 / 导出
</eb-button>
<eb-column-settings v-model="visibleProps" :columns="settingColumns" button-text="列设置" />
</template>
<template #level="{ row }">
<eb-tag :type="row.level === 'platinum' ? 'primary' : row.level === 'gold' ? 'warning' : 'info'" effect="plain">
{{ LEVEL_LABEL[row.level] }}
</eb-tag>
</template>
<template #balance="{ row }">
<span class="ml-money">¥{{ row.balance.toLocaleString() }}</span>
</template>
<template #status="{ row }">
<eb-status-tag :value="row.status" :statuses="MEMBER_STATUS" />
</template>
<template #operations="{ row }">
<eb-button text type="primary" size="small" @click="openEdit(row)">编辑</eb-button>
<eb-popconfirm title="删除后不可恢复,确认删除该会员?" icon-type="danger" @confirm="removeOne(row)">
<eb-button text type="danger" size="small">删除</eb-button>
</eb-popconfirm>
</template>
</eb-data-table>
</eb-section-card>
<eb-dialog
v-model="formVisible"
:title="editing ? `编辑会员 · ${editing.name}` : '新建会员'"
width="560px"
:close-on-click-modal="false"
>
<eb-form ref="formRef" :model="form" :rules="rules" label-width="88px">
<eb-form-item label="会员姓名" prop="name">
<eb-input v-model="form.name" placeholder="请输入姓名" maxlength="20" show-word-limit />
</eb-form-item>
<eb-form-item label="手机号" prop="phone">
<eb-input v-model="form.phone" placeholder="11 位手机号" />
</eb-form-item>
<eb-form-item label="邮箱" prop="email">
<eb-input v-model="form.email" placeholder="选填" />
</eb-form-item>
<eb-form-item label="会员等级" prop="level">
<eb-select v-model="form.level" style="width: 100%">
<eb-option v-for="l in MEMBER_LEVEL" :key="l.value" :label="l.label" :value="l.value" />
</eb-select>
</eb-form-item>
<eb-form-item label="所属部门" prop="dept">
<eb-select v-model="form.dept" style="width: 100%">
<eb-option v-for="d in DEPTS" :key="d" :label="d" :value="d" />
</eb-select>
</eb-form-item>
<eb-form-item label="账户状态" prop="status">
<eb-radio-group v-model="form.status">
<eb-radio v-for="s in MEMBER_STATUS" :key="s.value" :label="s.value">{{ s.label }}</eb-radio>
</eb-radio-group>
</eb-form-item>
<eb-form-item label="账户余额" prop="balance">
<eb-input-number v-model="form.balance" :min="0" :step="100" style="width: 200px" />
</eb-form-item>
</eb-form>
<template #footer>
<eb-button @click="formVisible = false">取消</eb-button>
<eb-button type="primary" :loading="saving" @click="save">保存</eb-button>
</template>
</eb-dialog>
<eb-dialog v-model="ioVisible" title="导入 / 导出会员" width="640px">
<eb-import-export-panel
ref="ioRef"
template-name="会员导入模板.xlsx"
import-tip="首列为会员姓名,手机号列必填;单次最多 2000 条。"
@import-file="onImportFile"
@export="onExport"
@download-template="EbMessage.info('示例:下载「会员导入模板.xlsx』')"
/>
</eb-dialog>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { EbMessage } from '@wil-works/evoke-business-ui'
import {
MEMBER_STATUS,
MEMBER_LEVEL,
LEVEL_LABEL,
DEPTS,
fetchMembers,
createMember,
updateMember,
removeMembers,
importMembers,
} from './mock.js'
const query = ref({ keyword: '', level: '', status: '' })
const filterFields = [
{ prop: 'keyword', label: '关键词', placeholder: '姓名 / 手机号 / 邮箱' },
{ prop: 'level', label: '会员等级', type: 'select', options: MEMBER_LEVEL },
{ prop: 'status', label: '账户状态', type: 'select', options: MEMBER_STATUS },
]
const allColumns = [
{ prop: 'name', label: '会员姓名', width: 110 },
{ prop: 'phone', label: '手机号', width: 130 },
{ prop: 'level', label: '等级', slot: 'level', width: 92 },
{ prop: 'owner', label: '负责人', stack: (row) => row.dept, width: 110 },
{ prop: 'balance', label: '账户余额', slot: 'balance', align: 'right', width: 100 },
{ prop: 'createdAt', label: '注册时间', width: 165 },
{ prop: 'status', label: '状态', slot: 'status', width: 88 },
]
const settingColumns = allColumns.map(({ prop, label }) => ({ prop, label }))
const visibleProps = ref(settingColumns.map((c) => c.prop))
const visibleColumns = computed(() =>
visibleProps.value.map((p) => allColumns.find((c) => c.prop === p)).filter(Boolean),
)
const tableRef = ref(null)
const rows = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const loading = ref(false)
let lastQuery = { ...query.value }
async function load() {
loading.value = true
try {
const res = await fetchMembers({ ...lastQuery, page: page.value, pageSize: pageSize.value })
rows.value = res.list
total.value = res.total
} finally {
loading.value = false
}
}
function onSearch(values) {
lastQuery = { ...values }
page.value = 1
load()
}
function handleRefresh() {
load()
EbMessage.success('列表已刷新')
}
const selection = ref([])
function onSelectionChange(rows) {
selection.value = rows
}
async function removeOne(row) {
removeMembers([row.id])
EbMessage.success(`已删除会员「${row.name}」`)
await load()
}
function batchRemove() {
const ids = selection.value.map((r) => r.id)
const count = removeMembers(ids)
EbMessage.success(`已批量删除 ${count} 位会员`)
tableRef.value?.clearSelection()
load()
}
const formVisible = ref(false)
const saving = ref(false)
const editing = ref(null)
const formRef = ref(null)
const emptyForm = () => ({
name: '',
phone: '',
email: '',
level: 'basic',
dept: DEPTS[0],
status: 'active',
balance: 0,
})
const form = reactive(emptyForm())
const rules = {
name: [{ required: true, message: '请输入会员姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
],
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
level: [{ required: true, message: '请选择会员等级', trigger: 'change' }],
}
function openCreate() {
editing.value = null
Object.assign(form, emptyForm())
formRef.value?.clearValidate()
formVisible.value = true
}
function openEdit(row) {
editing.value = row
Object.assign(form, {
name: row.name,
phone: row.phone,
email: row.email,
level: row.level,
dept: row.dept,
status: row.status,
balance: row.balance,
})
formRef.value?.clearValidate()
formVisible.value = true
}
async function save() {
let ok = true
try {
await formRef.value.validate()
} catch {
ok = false
}
if (!ok) {
EbMessage.warning('请先完善表单必填项')
return
}
saving.value = true
setTimeout(() => {
if (editing.value) {
updateMember(editing.value.id, { ...form })
EbMessage.success('会员信息已更新')
} else {
createMember({ ...form })
EbMessage.success('新建会员成功')
}
saving.value = false
formVisible.value = false
load()
}, 400)
}
const ioVisible = ref(false)
const ioRef = ref(null)
function onImportFile(file) {
if (!/\.(xlsx|xls|csv)$/i.test(file.name)) {
EbMessage.error('仅支持 xlsx / xls / csv 文件')
ioRef.value?.done('import')
return
}
setTimeout(() => {
const count = importMembers(3)
EbMessage.success(`导入完成:新增 ${count} 位会员`)
ioRef.value?.done('import')
ioVisible.value = false
load()
}, 800)
}
function onExport(format) {
setTimeout(() => {
EbMessage.success(`已按 ${String(format).toUpperCase()} 格式导出 ${total.value} 条数据`)
ioRef.value?.done('export')
}, 800)
}
onMounted(load)
</script>
<style scoped>
.ml-page {
padding: 16px;
}
.ml-card {
margin-top: 12px;
}
.ml-card :deep(.eb-search-filter) {
margin-bottom: 16px;
}
.ml-money {
font-variant-numeric: tabular-nums;
}
</style>