新版官网模板
This commit is contained in:
392
app/pages/admin/members/index.vue
Normal file
392
app/pages/admin/members/index.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<div class="members-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">💼 会员管理</h2>
|
||||
<p class="page-desc">管理企业会员和个人会员,支持入会申请审核</p>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button :loading="loading" @click="loadMembers">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col v-for="stat in statCards" :key="stat.key" :sm="6" :xs="12">
|
||||
<div
|
||||
:class="[stat.color, { active: filterStatus === stat.key }]"
|
||||
class="stat-card"
|
||||
@click="handleStatFilter(stat.key)"
|
||||
>
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 会员类型切换 -->
|
||||
<a-radio-group v-model:value="memberType" button-style="solid" class="mb-4">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="enterprise">企业会员</a-radio-button>
|
||||
<a-radio-button value="personal">个人会员</a-radio-button>
|
||||
</a-radio-group>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">📋 会员列表</span>
|
||||
<a-space wrap>
|
||||
<a-select v-model:value="filterStatus" style="width: 120px" @change="handleSearch">
|
||||
<a-select-option :value="undefined">全部状态</a-select-option>
|
||||
<a-select-option :value="0">待审核</a-select-option>
|
||||
<a-select-option :value="1">已通过</a-select-option>
|
||||
<a-select-option :value="2">已拒绝</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索姓名 / 企业名称"
|
||||
style="width: 240px"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="pagedMembers"
|
||||
:loading="loading"
|
||||
:pagination="tablePagination"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'info'">
|
||||
<div class="member-info-cell">
|
||||
<div :class="record.type === 1 ? 'enterprise' : 'personal'" class="member-avatar">
|
||||
{{ record.type === 1 ? '🏢' : '👤' }}
|
||||
</div>
|
||||
<div class="member-info-text">
|
||||
<div class="member-name">{{ record.name }}</div>
|
||||
<div class="member-meta">
|
||||
<a-tag :color="record.type === 1 ? 'blue' : 'green'" size="small">
|
||||
{{ record.type === 1 ? '企业会员' : '个人会员' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'contact'">
|
||||
<div class="contact-cell">
|
||||
<div v-if="record.contact">📞 {{ record.contact }}</div>
|
||||
<div v-if="record.phone">📱 {{ record.phone }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'createTime'">
|
||||
<span class="text-sm text-gray">{{ record.createTime?.substring(0, 10) || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" type="link" @click="handleView(record)">查看</a-button>
|
||||
<a-button v-if="record.status === 0" size="small" type="link" @click="handleReview(record)">审核</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showDetailModal"
|
||||
:footer="null"
|
||||
title="会员详情"
|
||||
width="700px"
|
||||
>
|
||||
<template v-if="currentMember">
|
||||
<a-tag :color="currentMember.type === 1 ? 'blue' : 'green'" style="margin-bottom: 16px">
|
||||
{{ currentMember.type === 1 ? '企业会员' : '个人会员' }}
|
||||
</a-tag>
|
||||
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="姓名/企业名">{{ currentMember.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系人">{{ currentMember.contact || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">{{ currentMember.phone || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="邮箱">{{ currentMember.email || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="statusColor(currentMember.status)">{{ statusText(currentMember.status) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="申请时间">{{ currentMember.createTime?.substring(0, 10) || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item :span="2" label="简介">{{ currentMember.bio || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div v-if="currentMember.attachments?.length" class="attachments-section">
|
||||
<h4>附件材料</h4>
|
||||
<div class="attachment-list">
|
||||
<a v-for="(file, idx) in currentMember.attachments" :key="idx" :href="file.url" target="_blank">
|
||||
📎 {{ file.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentMember.status === 0" class="review-actions">
|
||||
<a-divider />
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleApprove(currentMember)">通过审核</a-button>
|
||||
<a-button danger @click="handleReject(currentMember)">拒绝</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
useHead({ title: '会员管理 - 后台管理' })
|
||||
|
||||
interface Member {
|
||||
id?: number
|
||||
name?: string
|
||||
type?: number // 1: 企业, 2: 个人
|
||||
contact?: string
|
||||
phone?: string
|
||||
email?: string
|
||||
bio?: string
|
||||
status?: number
|
||||
createTime?: string
|
||||
attachments?: { name: string; url: string }[]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const members = ref<Member[]>([])
|
||||
const memberType = ref('all')
|
||||
const filterStatus = ref<number | undefined>(undefined)
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
})
|
||||
|
||||
const statCards = reactive([
|
||||
{ key: 0, icon: '⏳', label: '待审核', value: 0, color: 'orange' },
|
||||
{ key: 1, icon: '✅', label: '已通过', value: 0, color: 'green' },
|
||||
{ key: 2, icon: '❌', label: '已拒绝', value: 0, color: 'red' },
|
||||
{ key: -1, icon: '👥', label: '全部会员', value: 0, color: 'blue' },
|
||||
])
|
||||
|
||||
const columns = [
|
||||
{ title: '会员信息', key: 'info', width: 260 },
|
||||
{ title: '联系方式', key: 'contact', width: 180 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '申请时间', key: 'createTime', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 120 },
|
||||
]
|
||||
|
||||
const showDetailModal = ref(false)
|
||||
const currentMember = ref<Member | null>(null)
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||||
return members.value
|
||||
.filter(item => {
|
||||
if (memberType.value === 'enterprise') return item.type === 1
|
||||
if (memberType.value === 'personal') return item.type === 2
|
||||
return true
|
||||
})
|
||||
.filter(item => filterStatus.value === undefined || item.status === filterStatus.value)
|
||||
.filter(item => {
|
||||
if (!keyword) return true
|
||||
return [item.name, item.contact]
|
||||
.some(val => String(val || '').toLowerCase().includes(keyword))
|
||||
})
|
||||
.sort((a, b) => (b.id || 0) - (a.id || 0))
|
||||
})
|
||||
|
||||
const pagedMembers = computed(() => {
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
return filteredMembers.value.slice(start, start + pagination.pageSize)
|
||||
})
|
||||
|
||||
const tablePagination = computed(() => ({
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: filteredMembers.value.length,
|
||||
showSizeChanger: pagination.showSizeChanger,
|
||||
showQuickJumper: pagination.showQuickJumper,
|
||||
}))
|
||||
|
||||
function updateStats() {
|
||||
const filtered = members.value.filter(item => {
|
||||
if (memberType.value === 'enterprise') return item.type === 1
|
||||
if (memberType.value === 'personal') return item.type === 2
|
||||
return true
|
||||
})
|
||||
statCards[0].value = filtered.filter(i => i.status === 0).length
|
||||
statCards[1].value = filtered.filter(i => i.status === 1).length
|
||||
statCards[2].value = filtered.filter(i => i.status === 2).length
|
||||
statCards[3].value = filtered.length
|
||||
}
|
||||
|
||||
async function loadMembers() {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 接入实际API
|
||||
updateStats()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载会员列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatFilter(key: number) {
|
||||
filterStatus.value = key === -1 ? undefined : key
|
||||
pagination.current = 1
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
}
|
||||
|
||||
function handleTableChange(pag: { current: number; pageSize: number }) {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
}
|
||||
|
||||
function handleView(record: Member) {
|
||||
currentMember.value = record
|
||||
showDetailModal.value = true
|
||||
}
|
||||
|
||||
function handleReview(record: Member) {
|
||||
currentMember.value = record
|
||||
showDetailModal.value = true
|
||||
}
|
||||
|
||||
async function handleApprove(member: Member) {
|
||||
try {
|
||||
// TODO: 接入实际API
|
||||
message.success('已通过审核')
|
||||
showDetailModal.value = false
|
||||
await loadMembers()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject(member: Member) {
|
||||
try {
|
||||
// TODO: 接入实际API
|
||||
message.success('已拒绝')
|
||||
showDetailModal.value = false
|
||||
await loadMembers()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
function statusText(status?: number) {
|
||||
const map: Record<number, string> = { 0: '待审核', 1: '已通过', 2: '已拒绝' }
|
||||
return map[status ?? -1] || '-'
|
||||
}
|
||||
|
||||
function statusColor(status?: number) {
|
||||
const map: Record<number, string> = { 0: 'orange', 1: 'success', 2: 'error' }
|
||||
return map[status ?? -1] || 'default'
|
||||
}
|
||||
|
||||
watch(memberType, () => {
|
||||
updateStats()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadMembers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.members-page { min-height: 100%; }
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
|
||||
.page-desc { font-size: 13px; color: #9ca3af; margin: 2px 0 0; }
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
||||
.stat-card.active { box-shadow: 0 4px 12px rgba(0,0,0,0.12); }
|
||||
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
|
||||
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
|
||||
.stat-icon { font-size: 28px; flex-shrink: 0; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: rgba(0,0,0,0.85); line-height: 1.2; }
|
||||
.stat-label { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 2px; }
|
||||
|
||||
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
|
||||
|
||||
.member-info-cell { display: flex; align-items: flex-start; gap: 12px; }
|
||||
.member-avatar {
|
||||
width: 48px; height: 48px; border-radius: 12px;
|
||||
font-size: 24px;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.member-avatar.enterprise { background: #eff6ff; }
|
||||
.member-avatar.personal { background: #f0fdf4; }
|
||||
.member-info-text { flex: 1; min-width: 0; }
|
||||
.member-name { font-size: 14px; font-weight: 500; color: rgba(0,0,0,0.85); }
|
||||
.member-meta { margin-top: 4px; }
|
||||
.contact-cell { font-size: 12px; color: rgba(0,0,0,0.65); line-height: 1.7; }
|
||||
|
||||
.attachments-section { margin-top: 16px; }
|
||||
.attachments-section h4 { font-size: 14px; margin-bottom: 8px; }
|
||||
.attachment-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.attachment-list a { color: #1890ff; }
|
||||
.review-actions { text-align: right; }
|
||||
|
||||
.text-sm { font-size: 12px; }
|
||||
.text-gray { color: rgba(0,0,0,0.45); }
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
.mb-4 { margin-bottom: 16px; }
|
||||
</style>
|
||||
324
app/pages/admin/members/review.vue
Normal file
324
app/pages/admin/members/review.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<div class="admin-members-review">
|
||||
<div class="page-header">
|
||||
<h3>会员审核</h3>
|
||||
<span class="pending-count">待审核:{{ pendingCount }} 条</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索过滤 -->
|
||||
<div class="filter-bar">
|
||||
<a-space wrap>
|
||||
<a-input v-model:value="filters.keyword" allow-clear placeholder="搜索申请人姓名/单位" style="width: 200px" @press-enter="loadData" />
|
||||
<a-select v-model:value="filters.type" style="width: 130px" @change="loadData">
|
||||
<a-select-option value="">全部类型</a-select-option>
|
||||
<a-select-option value="enterprise">企业会员</a-select-option>
|
||||
<a-select-option value="personal">个人会员</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filters.status" style="width: 130px" @change="loadData">
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="loadData">搜索</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="{ total, pageSize, current: currentPage, onChange: handlePageChange, showTotal: (t: number) => `共 ${t} 条` }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="record.memberType === 'enterprise' ? 'blue' : 'green'">
|
||||
{{ record.memberType === 'enterprise' ? '企业会员' : '个人会员' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'materials'">
|
||||
<a-button size="small" @click="viewMaterials(record)">查看材料</a-button>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space v-if="record.status === 'pending'">
|
||||
<a-button size="small" type="primary" @click="handleApprove(record)">通过</a-button>
|
||||
<a-button danger size="small" @click="handleReject(record)">拒绝</a-button>
|
||||
<a-button size="small" @click="viewDetail(record)">详情</a-button>
|
||||
</a-space>
|
||||
<a-button v-else size="small" @click="viewDetail(record)">详情</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 材料预览弹窗 -->
|
||||
<a-modal v-model:open="materialsModal" :footer="null" :title="`${currentRecord?.applicantName} 的申请材料`" width="700px">
|
||||
<div v-if="currentRecord">
|
||||
<!-- 企业会员材料 -->
|
||||
<div v-if="currentRecord.memberType === 'enterprise'">
|
||||
<h4>企业会员申请材料</h4>
|
||||
<div class="materials-list">
|
||||
<div class="material-item">
|
||||
<span class="material-icon">📄</span>
|
||||
<span class="material-name">入会申请表(盖章)</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
<div class="material-item">
|
||||
<span class="material-icon">🏢</span>
|
||||
<span class="material-name">营业执照副本</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
<div class="material-item">
|
||||
<span class="material-icon">🪪</span>
|
||||
<span class="material-name">法人身份证</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
<div class="material-item">
|
||||
<span class="material-icon">📝</span>
|
||||
<span class="material-name">单位简介</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 个人会员材料 -->
|
||||
<div v-else>
|
||||
<h4>个人会员申请材料</h4>
|
||||
<div class="materials-list">
|
||||
<div class="material-item">
|
||||
<span class="material-icon">📄</span>
|
||||
<span class="material-name">入会申请表(签字)</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
<div class="material-item">
|
||||
<span class="material-icon">📖</span>
|
||||
<span class="material-name">个人简介</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
<div class="material-item">
|
||||
<span class="material-icon">🎓</span>
|
||||
<span class="material-name">职称证书/学历证书</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
<div class="material-item">
|
||||
<span class="material-icon">🪪</span>
|
||||
<span class="material-name">身份证复印件</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
<div class="material-item">
|
||||
<span class="material-icon">🏆</span>
|
||||
<span class="material-name">研究成果/获奖证明</span>
|
||||
<a-button ghost size="small" type="primary">预览/下载</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentRecord.status === 'pending'" class="action-area">
|
||||
<a-divider />
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleApprove(currentRecord); materialsModal = false">通过申请</a-button>
|
||||
<a-button danger @click="handleReject(currentRecord); materialsModal = false">拒绝申请</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<a-modal v-model:open="detailModal" :footer="null" :title="`会员申请详情`" width="700px">
|
||||
<a-descriptions v-if="currentRecord" :column="2" bordered>
|
||||
<a-descriptions-item label="申请人">{{ currentRecord.applicantName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="会员类型">
|
||||
<a-tag :color="currentRecord.memberType === 'enterprise' ? 'blue' : 'green'">
|
||||
{{ currentRecord.memberType === 'enterprise' ? '企业会员' : '个人会员' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="currentRecord.memberType === 'enterprise'" label="单位/组织">{{ currentRecord.organization }}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系方式">{{ currentRecord.phone }}</a-descriptions-item>
|
||||
<a-descriptions-item :span="2" label="电子邮箱">{{ currentRecord.email }}</a-descriptions-item>
|
||||
<a-descriptions-item label="申请时间">{{ currentRecord.applyTime }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(currentRecord.status)">{{ getStatusText(currentRecord.status) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="currentRecord.rejectReason" :span="2" label="拒绝原因">{{ currentRecord.rejectReason }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
|
||||
<!-- 拒绝弹窗 -->
|
||||
<a-modal v-model:open="rejectModal" :confirm-loading="saving" title="填写拒绝原因" @ok="confirmReject">
|
||||
<a-textarea v-model:value="rejectReason" :rows="4" placeholder="请说明拒绝原因" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
useHead({ title: '会员审核' })
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const materialsModal = ref(false)
|
||||
const detailModal = ref(false)
|
||||
const rejectModal = ref(false)
|
||||
const rejectReason = ref('')
|
||||
const currentRecord = ref<any>(null)
|
||||
const pendingCount = ref(5)
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
|
||||
const filters = reactive({ keyword: '', type: '', status: '' })
|
||||
|
||||
const columns = [
|
||||
{ title: '申请人', dataIndex: 'applicantName', key: 'applicantName' },
|
||||
{ title: '会员类型', key: 'type', width: 110 },
|
||||
{ title: '单位/联系方式', dataIndex: 'orgOrContact', key: 'orgOrContact' },
|
||||
{ title: '申请时间', dataIndex: 'applyTime', key: 'applyTime', width: 150 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '材料', key: 'materials', width: 100 },
|
||||
{ title: '操作', key: 'action', width: 180 },
|
||||
]
|
||||
|
||||
const dataSource = ref([
|
||||
{ id: 1, applicantName: '广西某科技公司', memberType: 'enterprise', orgOrContact: '王总 139****0001', phone: '139****0001', email: 'enterprise@xx.com', organization: '广西某科技有限公司', applyTime: '2024-12-19 10:00', status: 'pending' },
|
||||
{ id: 2, applicantName: '张某某', memberType: 'personal', orgOrContact: '广西大学', phone: '138****0001', email: 'zhang@gxu.edu.cn', applyTime: '2024-12-18 14:30', status: 'pending' },
|
||||
{ id: 3, applicantName: '南宁某咨询机构', memberType: 'enterprise', orgOrContact: '李经理 137****0002', phone: '137****0002', email: 'nn@xx.com', organization: '南宁某咨询有限公司', applyTime: '2024-12-15 09:20', status: 'approved' },
|
||||
])
|
||||
|
||||
function getStatusColor(status: string) {
|
||||
const map: Record<string, string> = { pending: 'orange', approved: 'green', rejected: 'red' }
|
||||
return map[status] || 'default'
|
||||
}
|
||||
|
||||
function getStatusText(status: string) {
|
||||
const map: Record<string, string> = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function viewMaterials(record: any) {
|
||||
currentRecord.value = record
|
||||
materialsModal.value = true
|
||||
}
|
||||
|
||||
function viewDetail(record: any) {
|
||||
currentRecord.value = record
|
||||
detailModal.value = true
|
||||
}
|
||||
|
||||
async function handleApprove(record: any) {
|
||||
// TODO: 调用API
|
||||
record.status = 'approved'
|
||||
pendingCount.value = Math.max(0, pendingCount.value - 1)
|
||||
message.success(`已通过 ${record.applicantName} 的会员申请`)
|
||||
}
|
||||
|
||||
function handleReject(record: any) {
|
||||
currentRecord.value = record
|
||||
rejectReason.value = ''
|
||||
rejectModal.value = true
|
||||
}
|
||||
|
||||
async function confirmReject() {
|
||||
if (!rejectReason.value.trim()) { message.warning('请填写拒绝原因'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
// TODO: 调用API
|
||||
currentRecord.value.status = 'rejected'
|
||||
currentRecord.value.rejectReason = rejectReason.value
|
||||
pendingCount.value = Math.max(0, pendingCount.value - 1)
|
||||
message.success('已拒绝申请并通知申请人')
|
||||
rejectModal.value = false
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
// TODO: 接入实际API
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
total.value = dataSource.value.length
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-members-review {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 20px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.page-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pending-count {
|
||||
padding: 4px 12px;
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-bar, .table-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.materials-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.material-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.material-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.material-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.action-area {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user