Files
danyang-match/admin-web/src/modules/review/ReviewView.vue
T

289 lines
14 KiB
Vue
Raw 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="page-container page-shell review-page">
<header class="page-heading">
<div>
<span class="page-kicker">用户与资料审核</span>
<h1>资料审核</h1>
<p>按提交顺序核验必要资料,审核操作保留清晰意见和审计记录。</p>
</div>
<div class="stat-tags">
<el-tag type="warning">待审核 {{ stats.pending }}</el-tag>
<el-tag type="info">审核中 {{ stats.processing }}</el-tag>
</div>
</header>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" show-icon>
<template #default><el-button link type="primary" @click="loadPage">重新加载</el-button></template>
</el-alert>
<section v-else class="surface-card review-panel">
<el-tabs v-model="activeTab">
<el-tab-pane :label="`待审核队列(${pendingTasks.length})`" name="pending">
<el-table :data="pendingTasks" v-loading="loading" row-key="taskId">
<el-table-column prop="userId" label="用户编号" min-width="170" show-overflow-tooltip />
<el-table-column prop="reviewType" label="审核类型" min-width="130">
<template #default="{ row }">{{ reviewTypeText(row.reviewType) }}</template>
</el-table-column>
<el-table-column prop="submittedAt" label="提交时间" min-width="180">
<template #default="{ row }">{{ formatTime(row.submittedAt) }}</template>
</el-table-column>
<el-table-column label="状态" width="110">
<template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button v-if="canClaim(row)" type="primary" link @click="handleClaim(row)">领取审核</el-button>
<span v-else class="muted">只读</span>
</template>
</el-table-column>
<template #empty><el-empty description="暂无待审核任务" /></template>
</el-table>
</el-tab-pane>
<el-tab-pane :label="`我的审核(${myTasks.length})`" name="my">
<el-table :data="myTasks" v-loading="loading" row-key="taskId">
<el-table-column prop="userId" label="用户编号" min-width="170" show-overflow-tooltip />
<el-table-column prop="reviewType" label="审核类型" min-width="130">
<template #default="{ row }">{{ reviewTypeText(row.reviewType) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
</el-table-column>
<el-table-column prop="submittedAt" label="提交时间" min-width="180">
<template #default="{ row }">{{ formatTime(row.submittedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }"><el-button link type="primary" @click="handleView(row)">查看</el-button></template>
</el-table-column>
<template #empty><el-empty description="暂无已领取的审核任务" /></template>
</el-table>
</el-tab-pane>
</el-tabs>
</section>
<el-drawer v-model="detailVisible" title="审核详情" size="min(560px, 92vw)">
<div v-if="detailLoading" v-loading="true" class="drawer-loading"></div>
<template v-else-if="currentTask">
<el-descriptions :column="1" border>
<el-descriptions-item label="用户编号">{{ currentTask.userId }}</el-descriptions-item>
<el-descriptions-item label="审核类型">{{ reviewTypeText(currentTask.reviewType) }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatTime(currentTask.submittedAt) }}</el-descriptions-item>
<el-descriptions-item label="当前状态">
<el-tag :type="statusType(currentTask.status)">{{ statusText(currentTask.status) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<h3 class="drawer-heading">用户资料</h3>
<el-descriptions v-if="currentTask.userProfile" :column="2" border>
<el-descriptions-item label="显示名称">{{ currentTask.userProfile.displayName || '-' }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ genderText(currentTask.userProfile.gender) }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ currentTask.userProfile.age || '-' }}</el-descriptions-item>
<el-descriptions-item label="身高">{{ currentTask.userProfile.heightCm ? `${currentTask.userProfile.heightCm} cm` : '-' }}</el-descriptions-item>
<el-descriptions-item label="区域">{{ currentTask.userProfile.regionName || '-' }}</el-descriptions-item>
<el-descriptions-item label="学历">{{ currentTask.userProfile.educationCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="自我介绍" :span="2">{{ currentTask.userProfile.selfIntro || '未填写' }}</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无资料快照" />
<template v-if="currentTask.reviewType === 'local_eligibility'">
<h3 class="drawer-heading">本地资格佐证</h3>
<el-alert title="佐证属于敏感数据,仅在审核必要时查看;每次访问都会记录目的和结果。" type="warning" :closable="false" show-icon />
<el-table :data="currentTask.localEligibilityEvidences || []" class="evidence-table">
<el-table-column prop="evidenceType" label="佐证类型" min-width="140" />
<el-table-column prop="createdAt" label="上传时间" min-width="170">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button v-if="canViewEvidence" link type="primary" @click="viewEvidence(row)">查看</el-button>
<span v-else class="muted">无权限</span>
</template>
</el-table-column>
</el-table>
</template>
<template v-if="canProcessCurrent">
<h3 class="drawer-heading">审核操作</h3>
<el-form label-position="top">
<el-form-item label="审核意见">
<el-input v-model.trim="reviewForm.comment" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请写明判断依据或需要补充的内容" />
</el-form-item>
<el-form-item label="原因标签">
<el-select v-model="reviewForm.reasonCodes" multiple class="full-width" placeholder="按需选择">
<el-option v-for="reason in reasonOptions" :key="reason.code" :label="reason.name" :value="reason.code" />
</el-select>
</el-form-item>
<div class="review-actions">
<el-button type="success" :loading="submitting" @click="runDecision('approve', '审核通过')">通过</el-button>
<el-button type="warning" :loading="submitting" @click="runDecision('need-edit', currentTask.reviewType === 'local_eligibility' ? '要求补充佐证' : '退回修改')">{{ currentTask.reviewType === 'local_eligibility' ? '补充佐证' : '需修改' }}</el-button>
<el-button type="danger" :loading="submitting" @click="runDecision('reject', '拒绝资料')">拒绝</el-button>
</div>
</el-form>
</template>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { get, post } from '@/api/client'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const activeTab = ref('pending')
const loading = ref(false)
const detailLoading = ref(false)
const submitting = ref(false)
const detailVisible = ref(false)
const errorMessage = ref('')
const stats = ref({ pending: 0, processing: 0 })
const tasks = ref<any[]>([])
const currentTask = ref<any>(null)
const reasonOptions = ref<any[]>([])
const reviewForm = ref({ comment: '', reasonCodes: [] as string[] })
const canProcess = computed(() => authStore.permissions.includes('review.process'))
const canViewEvidence = computed(() => authStore.permissions.includes('local_eligibility.evidence.view'))
const canProcessCurrent = computed(() => currentTask.value?.reviewType === 'local_eligibility'
? authStore.permissions.includes('local_eligibility.review')
: canProcess.value)
function canClaim(task: any) {
return task.reviewType === 'local_eligibility'
? authStore.permissions.includes('local_eligibility.review')
: canProcess.value
}
const pendingTasks = computed(() => tasks.value.filter((task) => !task.assignedOperatorId))
const myTasks = computed(() => tasks.value.filter((task) => task.assignedOperatorId === authStore.operator?.operatorId))
async function loadPage() {
loading.value = true
errorMessage.value = ''
try {
const [statsResponse, tasksResponse, reasonsResponse]: any[] = await Promise.all([
get('/operator/reviews/stats'),
get('/operator/reviews/pending', { page: 1, pageSize: 20 }),
get('/operator/reviews/reasons'),
])
stats.value = statsResponse.data || { pending: 0, processing: 0 }
tasks.value = tasksResponse.data || []
reasonOptions.value = reasonsResponse.data || []
} catch (error: any) {
tasks.value = []
errorMessage.value = error.response?.status === 403
? '您没有查看资料审核的权限'
: '审核任务加载失败,请检查服务状态后重试'
} finally {
loading.value = false
}
}
async function handleClaim(row: any) {
try {
const response: any = await post(`/operator/reviews/${row.taskId || row.id}/claim`)
if (!response.success) throw new Error(response.message || '领取失败')
ElMessage.success('审核任务领取成功')
await loadPage()
await openDetail(row.taskId || row.id)
} catch (error: any) {
ElMessage.error(error.message || '领取失败,任务可能已被其他审核员领取')
}
}
async function openDetail(taskId: string) {
detailVisible.value = true
detailLoading.value = true
currentTask.value = null
reviewForm.value = { comment: '', reasonCodes: [] }
try {
const response: any = await get(`/operator/reviews/${taskId}`)
currentTask.value = response.data
} finally {
detailLoading.value = false
}
}
async function handleView(row: any) {
await openDetail(row.taskId || row.id)
}
async function runDecision(action: 'approve' | 'need-edit' | 'reject', label: string) {
if (!currentTask.value || submitting.value) return
if (action !== 'approve' && !reviewForm.value.comment) {
ElMessage.warning('请填写审核意见,说明需要修改或拒绝的原因')
return
}
if (action === 'reject') {
await ElMessageBox.confirm('拒绝后用户需要重新处理资料,确认继续吗?', '确认拒绝资料', {
type: 'warning',
confirmButtonText: '确认拒绝',
cancelButtonText: '取消',
})
}
submitting.value = true
try {
const localActions = {
approve: 'approve-local-eligibility',
'need-edit': 'need-evidence',
reject: 'reject-local-eligibility',
}
const endpointAction = currentTask.value.reviewType === 'local_eligibility' ? localActions[action] : action
const response: any = await post(`/operator/reviews/${currentTask.value.taskId}/${endpointAction}`, {
comment: reviewForm.value.comment,
reasonCodes: reviewForm.value.reasonCodes,
})
if (!response.success) throw new Error(response.message || `${label}失败`)
ElMessage.success(label)
detailVisible.value = false
await loadPage()
} catch (error: any) {
ElMessage.error(error.message || `${label}失败,请重试`)
} finally {
submitting.value = false
}
}
async function viewEvidence(row: any) {
if (!currentTask.value) return
try {
const { value: purpose } = await ElMessageBox.prompt('请填写本次查看佐证的业务目的', '敏感数据访问确认', {
confirmButtonText: '记录并查看',
cancelButtonText: '取消',
inputPattern: /\S+/,
inputErrorMessage: '访问目的不能为空',
})
const response: any = await post(`/operator/reviews/${currentTask.value.taskId}/evidences/${row.evidenceId}/access`, { purpose })
window.open(response.data.url, '_blank', 'noopener,noreferrer')
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(error.message || '佐证访问失败')
}
}
function reviewTypeText(value?: string) {
return value ? ({ identity: '实名认证', profile: '婚恋资料', local_eligibility: '本地资格' }[value] || value) : '-'
}
function genderText(value?: string) {
return value === 'male' ? '男' : value === 'female' ? '女' : '-'
}
function statusType(status: string) {
return ({ pending: 'warning', processing: 'info', approved: 'success', need_edit: 'warning', rejected: 'danger' } as Record<string, string>)[status] || 'info'
}
function statusText(status: string) {
return ({ pending: '待审核', processing: '审核中', approved: '已通过', need_edit: '需修改', rejected: '已拒绝' } as Record<string, string>)[status] || status || '-'
}
function formatTime(time?: string) {
return time ? new Date(time).toLocaleString('zh-CN') : '-'
}
onMounted(loadPage)
</script>
<style scoped>
.page-container{padding:28px}.page-kicker{color:var(--admin-primary);font-size:12px;font-weight:650;letter-spacing:1px}.stat-tags{display:flex;gap:8px}.review-panel{padding:20px 24px}.muted{color:var(--color-text-tertiary);font-size:12px}.drawer-loading{min-height:240px}.drawer-heading{margin:24px 0 12px;font-size:16px}.full-width{width:100%}.review-actions{display:flex;flex-wrap:wrap;gap:8px}@media(max-width:760px){.page-container{padding:18px}}
</style>