Files
2026-04-29 01:33:33 +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" allow-clear placeholder="搜索专家姓名/单位" 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"
:pagination="{ total, pageSize, current: currentPage, onChange: handlePageChange, showTotal: (t: number) => `共 ${t} 条` }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applicant'">
<div class="applicant-info">
<a-avatar :size="36" :src="record.avatar">{{ 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 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-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"
:confirm-loading="saving"
title="填写拒绝原因"
@ok="confirmReject"
>
<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"
:footer="null"
:title="`${currentRecord?.name} - 申请详情`"
width="700px"
>
<div v-if="currentRecord" class="detail-content">
<a-descriptions :column="2" bordered>
<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 :span="2" label="电子邮箱">{{ currentRecord.email }}</a-descriptions-item>
<a-descriptions-item :span="2" label="个人简介">{{ 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 lang="ts" setup>
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>