Files
jczxw-pc/app/pages/admin/experts/review.vue
赵忠林 56aea4ad86 feat(about): 重构“关于我们”页面并丰富内容展示
- 采用左右分栏布局,左侧新增图标导航
- 全新设计顶部 Banner,提升视觉效果
- 添加学会简介数据亮点和主要职能展示
- 新增组织机构图、主要领导及专家委员会成员展示
- 引入学会章程章节分明条目展示
- 丰富咨询服务内容,新增服务项目卡片和联系方式
- “加入我们”板块支持企业与个人会员申请详情说明
- 支持资料下载并优化排版与交互体验
- 增强响应式支持,保证移动端体验一致
- 页面样式大幅调整,提升整体美观与可读性
2026-04-26 01:44:07 +08:00

338 lines
10 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="admin-experts-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" placeholder="搜索专家姓名/单位" allow-clear style="width: 200px" @press-enter="loadData" />
<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"
row-key="id"
:pagination="{ total, pageSize, current: currentPage, onChange: handlePageChange, showTotal: (t: number) => `共 ${t} 条` }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applicant'">
<div class="applicant-info">
<a-avatar :src="record.avatar" :size="36">{{ record.name?.charAt(0) }}</a-avatar>
<div class="applicant-detail">
<div class="applicant-name">{{ record.name }}</div>
<div class="applicant-org">{{ record.organization }}</div>
</div>
</div>
</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-space>
<a-button size="small" @click="previewFile(record, 'resume')">简历</a-button>
<a-button size="small" @click="previewFile(record, 'id')">身份证</a-button>
<a-button size="small" @click="previewFile(record, 'cert')">证书</a-button>
</a-space>
</template>
<template v-if="column.key === 'action'">
<a-space v-if="record.status === 'pending'">
<a-button type="primary" size="small" @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-space v-else>
<a-button size="small" @click="viewDetail(record)">详情</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 拒绝原因弹窗 -->
<a-modal
v-model:open="rejectModal"
title="填写拒绝原因"
@ok="confirmReject"
:confirm-loading="saving"
>
<a-form layout="vertical">
<a-form-item label="拒绝原因" required>
<a-textarea v-model:value="rejectReason" :rows="4" placeholder="请说明拒绝原因(将通知申请人)" />
</a-form-item>
</a-form>
</a-modal>
<!-- 详情弹窗 -->
<a-modal
v-model:open="detailModal"
:title="`${currentRecord?.name} - 申请详情`"
width="700px"
:footer="null"
>
<div v-if="currentRecord" class="detail-content">
<a-descriptions bordered :column="2">
<a-descriptions-item label="姓名">{{ currentRecord.name }}</a-descriptions-item>
<a-descriptions-item label="职称">{{ currentRecord.title }}</a-descriptions-item>
<a-descriptions-item label="所在单位">{{ currentRecord.organization }}</a-descriptions-item>
<a-descriptions-item label="研究领域">{{ currentRecord.researchArea }}</a-descriptions-item>
<a-descriptions-item label="学历">{{ currentRecord.education }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ currentRecord.phone }}</a-descriptions-item>
<a-descriptions-item label="电子邮箱" :span="2">{{ currentRecord.email }}</a-descriptions-item>
<a-descriptions-item label="个人简介" :span="2">{{ currentRecord.intro }}</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>
<div class="materials-section" style="margin-top:16px">
<h4 style="margin-bottom:12px">申请材料</h4>
<a-space wrap>
<a-button icon="📄" @click="previewFile(currentRecord, 'resume')">查看简历/研究成果</a-button>
<a-button icon="🪪" @click="previewFile(currentRecord, 'id')">查看身份证</a-button>
<a-button icon="🏆" @click="previewFile(currentRecord, 'cert')">查看职称证书/学历证书</a-button>
</a-space>
</div>
<div v-if="currentRecord.status === 'pending'" class="action-area" style="margin-top:16px">
<a-space>
<a-button type="primary" @click="handleApprove(currentRecord); detailModal = false">通过申请</a-button>
<a-button danger @click="handleReject(currentRecord); detailModal = false">拒绝申请</a-button>
</a-space>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
definePageMeta({ layout: 'admin' })
useHead({ title: '专家审核' })
const loading = ref(false)
const saving = ref(false)
const rejectModal = ref(false)
const detailModal = ref(false)
const rejectReason = ref('')
const currentRecord = ref<any>(null)
const pendingCount = ref(3)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(15)
const filters = reactive({
keyword: '',
status: '',
})
const columns = [
{ title: '申请人', key: 'applicant', width: 200 },
{ title: '职称', dataIndex: 'title', key: 'title' },
{ title: '研究领域', dataIndex: 'researchArea', key: 'researchArea' },
{ title: '申请时间', dataIndex: 'applyTime', key: 'applyTime', width: 150 },
{ title: '状态', key: 'status', width: 100 },
{ title: '材料', key: 'materials', width: 160 },
{ title: '操作', key: 'action', width: 160 },
]
const dataSource = ref<any[]>([
{
id: 1,
name: '张某某',
avatar: '',
organization: '广西大学',
title: '教授',
researchArea: '区域经济',
education: '博士',
phone: '138****0001',
email: 'zhang@gxu.edu.cn',
intro: '长期从事区域经济研究...',
applyTime: '2024-12-18 10:30',
status: 'pending',
},
{
id: 2,
name: '李某某',
avatar: '',
organization: '广西社科院',
title: '研究员',
researchArea: '产业政策',
education: '博士',
phone: '139****0002',
email: 'li@gxss.org',
intro: '专注产业政策研究...',
applyTime: '2024-12-17 15:00',
status: 'pending',
},
{
id: 3,
name: '王某某',
avatar: '',
organization: '广西师范大学',
title: '副教授',
researchArea: '金融经济',
education: '博士',
phone: '137****0003',
email: 'wang@gxnu.edu.cn',
intro: '从事金融经济研究...',
applyTime: '2024-12-15 09:00',
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
}
async function handleApprove(record: any) {
try {
// TODO: 调用API
record.status = 'approved'
pendingCount.value = Math.max(0, pendingCount.value - 1)
message.success(`已通过 ${record.name} 的专家申请`)
} catch (e: any) {
message.error(e?.message || '操作失败')
}
}
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'
pendingCount.value = Math.max(0, pendingCount.value - 1)
message.success('已拒绝申请并通知申请人')
rejectModal.value = false
} catch (e: any) {
message.error(e?.message || '操作失败')
} finally {
saving.value = false
}
}
function viewDetail(record: any) {
currentRecord.value = record
detailModal.value = true
}
function previewFile(record: any, type: string) {
message.info(`预览 ${record.name}${type === 'resume' ? '简历' : type === 'id' ? '身份证' : '证书'}材料`)
// TODO: 打开文件预览
}
function handlePageChange(page: number) {
currentPage.value = page
loadData()
}
async function loadData() {
loading.value = true
try {
// TODO: 接入实际API
} finally {
loading.value = false
}
}
onMounted(() => {
total.value = dataSource.value.length
})
</script>
<style scoped>
.admin-experts-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 {
background: #fff;
border-radius: 10px;
padding: 14px 20px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.table-card {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.applicant-info {
display: flex;
align-items: center;
gap: 10px;
}
.applicant-name {
font-weight: 600;
font-size: 14px;
color: #1f2937;
}
.applicant-org {
font-size: 12px;
color: #9ca3af;
}
</style>