<script setup>
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { auditGuideApplication, getGuideApplicationDetail } from '@/api/guideAudits'
|
import { getGuideUserDocumentBlob as getGuideDocumentBlob } from '@/api/users'
|
|
const route = useRoute()
|
const router = useRouter()
|
const loading = ref(false)
|
const submittingStatus = ref(null)
|
const application = ref(null)
|
const documentPreviewUrls = ref({})
|
|
const documentImages = [
|
{ key: 'idCardFrontUrl', label: '身份证(正)' },
|
{ key: 'idCardBackUrl', label: '身份证(反)' },
|
{ key: 'certificateUrl', label: '导游证' },
|
{ key: 'qualificationCertificateUrl', label: '导游资质证书' },
|
]
|
const statusMeta = {
|
0: { label: '待审核', type: 'warning' },
|
1: { label: '已通过', type: 'success' },
|
2: { label: '已驳回', type: 'danger' },
|
}
|
|
const canAudit = computed(() => application.value?.auditStatus === 0)
|
const teamName = computed(() => {
|
if (application.value?.guideType === 'personal') return '个人'
|
return application.value?.teamName || '-'
|
})
|
const genderLabel = computed(() => ({ 1: '男', 2: '女' }[application.value?.gender] || '-'))
|
const currentStatus = computed(() => statusMeta[application.value?.auditStatus] || statusMeta[0])
|
const formatDateTime = (value) => value ? value.replace('T', ' ').slice(0, 19) : '-'
|
const displayValue = (value) => value === null || value === undefined || value === '' ? '-' : value
|
|
const revokeDocumentPreviews = () => {
|
Object.values(documentPreviewUrls.value).forEach((url) => {
|
if (url?.startsWith('blob:')) URL.revokeObjectURL(url)
|
})
|
documentPreviewUrls.value = {}
|
}
|
|
const loadDocumentPreviews = async () => {
|
revokeDocumentPreviews()
|
await Promise.all(documentImages.map(async (item) => {
|
const documentUrl = application.value?.[item.key]
|
if (!documentUrl) return
|
try {
|
const blob = await getGuideDocumentBlob(documentUrl)
|
documentPreviewUrls.value[item.key] = blob ? URL.createObjectURL(blob) : documentUrl
|
} catch (error) {
|
delete documentPreviewUrls.value[item.key]
|
}
|
}))
|
}
|
|
const loadDetail = async () => {
|
loading.value = true
|
try {
|
application.value = await getGuideApplicationDetail(route.params.id)
|
await loadDocumentPreviews()
|
} finally {
|
loading.value = false
|
}
|
}
|
|
const submitAudit = async (auditStatus, auditRemark) => {
|
submittingStatus.value = auditStatus
|
try {
|
await auditGuideApplication(application.value.id, {
|
auditStatus,
|
auditRemark: auditRemark?.trim() || undefined,
|
})
|
ElMessage.success(auditStatus === 1 ? '审核通过' : '申请已驳回')
|
await router.push('/guide-audits')
|
} finally {
|
submittingStatus.value = null
|
}
|
}
|
|
const approve = async () => {
|
try {
|
await ElMessageBox.confirm('确认通过该导游申请吗?', '审核通过', {
|
type: 'warning',
|
confirmButtonText: '确认通过',
|
cancelButtonText: '取消',
|
})
|
await submitAudit(1)
|
} catch (error) {
|
if (error !== 'cancel' && error !== 'close') throw error
|
}
|
}
|
|
const reject = async () => {
|
try {
|
const { value } = await ElMessageBox.prompt('请输入审核不通过原因', '审核不通过', {
|
inputType: 'textarea',
|
inputPlaceholder: '请输入审核备注',
|
inputValidator: (text) => Boolean(text?.trim()),
|
inputErrorMessage: '审核不通过时必须填写原因',
|
confirmButtonText: '确认不通过',
|
cancelButtonText: '取消',
|
})
|
await submitAudit(2, value)
|
} catch (error) {
|
if (error !== 'cancel' && error !== 'close') throw error
|
}
|
}
|
|
onMounted(loadDetail)
|
onBeforeUnmount(revokeDocumentPreviews)
|
</script>
|
|
<template>
|
<section v-loading="loading" class="audit-detail-page">
|
<el-card class="audit-detail-card" shadow="never">
|
<template v-if="application">
|
<section class="detail-section basic-section">
|
<h3>基本信息</h3>
|
<div class="field-grid">
|
<div class="field-item">
|
<span class="field-label">导游手机号</span>
|
<div class="field-value">{{ displayValue(application.mobile) }}</div>
|
</div>
|
<div class="field-item">
|
<span class="field-label">注册时间</span>
|
<div class="field-value">{{ formatDateTime(application.createdAt) }}</div>
|
</div>
|
<div class="field-item">
|
<span class="field-label">姓名</span>
|
<div class="field-value">{{ displayValue(application.realName) }}</div>
|
</div>
|
<div class="field-item">
|
<span class="field-label">性别</span>
|
<el-select :model-value="genderLabel" disabled>
|
<el-option :label="genderLabel" :value="genderLabel" />
|
</el-select>
|
</div>
|
<div class="field-item">
|
<span class="field-label">年龄</span>
|
<div class="field-value">{{ displayValue(application.ageRange) }}</div>
|
</div>
|
<div class="field-item">
|
<span class="field-label">工作年限</span>
|
<el-select :model-value="displayValue(application.workYearRange)" disabled>
|
<el-option :label="displayValue(application.workYearRange)" :value="displayValue(application.workYearRange)" />
|
</el-select>
|
</div>
|
<div class="field-item">
|
<span class="field-label">身份证号</span>
|
<div class="field-value">{{ displayValue(application.idCardNo) }}</div>
|
</div>
|
</div>
|
|
<div class="document-grid">
|
<div v-for="item in documentImages" :key="item.key" class="document-item">
|
<span class="field-label document-label">{{ item.label }}</span>
|
<div class="document-control">
|
<div class="document-image">
|
<el-image
|
v-if="documentPreviewUrls[item.key]"
|
:src="documentPreviewUrls[item.key]"
|
fit="cover"
|
:preview-src-list="[documentPreviewUrls[item.key]]"
|
preview-teleported
|
/>
|
<span v-else>{{ application[item.key] ? '加载失败' : '暂无' }}</span>
|
</div>
|
<el-link
|
v-if="documentPreviewUrls[item.key]"
|
type="primary"
|
:href="documentPreviewUrls[item.key]"
|
target="_blank"
|
>查看大图</el-link>
|
<span v-else class="document-link-placeholder">查看大图</span>
|
</div>
|
</div>
|
</div>
|
</section>
|
|
<section class="detail-section business-section">
|
<h3>业务配置</h3>
|
<div class="business-row">
|
<span class="field-label">所属讲解队</span>
|
<el-select :model-value="teamName" disabled>
|
<el-option :label="teamName" :value="teamName" />
|
</el-select>
|
</div>
|
</section>
|
|
<section v-if="!canAudit" class="detail-section audit-record-section">
|
<h3>审核信息</h3>
|
<div class="audit-record-grid">
|
<div class="field-item">
|
<span class="field-label">审核结果</span>
|
<div><el-tag :type="currentStatus.type">{{ currentStatus.label }}</el-tag></div>
|
</div>
|
<div class="field-item">
|
<span class="field-label">审核人</span>
|
<div class="field-value">{{ displayValue(application.auditor) }}</div>
|
</div>
|
<div class="field-item">
|
<span class="field-label">审核时间</span>
|
<div class="field-value">{{ formatDateTime(application.auditedAt) }}</div>
|
</div>
|
<div class="field-item audit-remark-item">
|
<span class="field-label">审核备注</span>
|
<div class="field-value">{{ displayValue(application.auditRemark) }}</div>
|
</div>
|
</div>
|
</section>
|
|
<footer v-if="canAudit" class="audit-actions">
|
<el-button
|
type="primary"
|
:loading="submittingStatus === 1"
|
:disabled="submittingStatus === 2"
|
@click="approve"
|
>审核通过</el-button>
|
<el-button
|
type="primary"
|
:loading="submittingStatus === 2"
|
:disabled="submittingStatus === 1"
|
@click="reject"
|
>审核不通过</el-button>
|
</footer>
|
</template>
|
<el-empty v-else-if="!loading" description="导游申请不存在" />
|
</el-card>
|
</section>
|
</template>
|
|
<style scoped>
|
.audit-detail-page { min-height: 640px; }
|
.audit-detail-card { border: 0; border-radius: 4px; }
|
.audit-detail-card :deep(.el-card__body) { padding: 0; }
|
.detail-section { padding: 26px 38px 30px; }
|
.detail-section + .detail-section, .audit-actions { border-top: 10px solid #f4f7fb; }
|
.detail-section h3 { margin: 0 0 30px; color: #29476b; font-size: 14px; }
|
.field-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px 40px; }
|
.field-item { display: grid; grid-template-columns: 70px minmax(0, 1fr); align-items: center; gap: 12px; }
|
.field-label { color: #53657d; font-size: 13px; line-height: 1.5; white-space: nowrap; }
|
.field-value { min-height: 34px; padding: 8px 10px; overflow: hidden; color: #667085; background: #dedede; text-overflow: ellipsis; white-space: nowrap; }
|
.field-item :deep(.el-select) { width: 100%; }
|
.field-item :deep(.el-select__wrapper), .business-row :deep(.el-select__wrapper) { background: #dedede; }
|
.document-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 34px 70px; width: min(720px, 100%); margin-top: 42px; }
|
.document-item { display: grid; grid-template-columns: 70px minmax(0, 1fr); align-items: center; gap: 12px; }
|
.document-label { white-space: normal; }
|
.document-control { display: flex; align-items: center; gap: 32px; }
|
.document-image { display: grid; place-items: center; width: 64px; height: 58px; overflow: hidden; color: #8a96a6; background: #e4e6e8; font-size: 12px; }
|
.document-image :deep(.el-image) { width: 100%; height: 100%; }
|
.document-link-placeholder { color: #a8b0ba; font-size: 13px; }
|
.business-section { min-height: 140px; }
|
.business-row { display: grid; grid-template-columns: 70px 200px; align-items: center; gap: 12px; }
|
.business-row :deep(.el-select) { width: 200px; }
|
.audit-record-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px 40px; }
|
.audit-remark-item { grid-column: 1 / -1; grid-template-columns: 70px minmax(0, 1fr); }
|
.audit-remark-item .field-value { white-space: normal; }
|
.audit-actions { display: flex; gap: 36px; padding: 26px 38px; }
|
.audit-actions :deep(.el-button) { width: 146px; margin-left: 0; }
|
@media (max-width: 1200px) {
|
.field-grid, .audit-record-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
}
|
</style>
|