This commit is contained in:
Mrx
2026-03-17 17:19:00 +08:00
parent 68da50984c
commit 792f8d6abe
9 changed files with 1180 additions and 58 deletions

View File

@@ -0,0 +1,441 @@
<template>
<div class="certification-reviews-page">
<div class="page-header">
<h1 class="page-title">企业审核</h1>
<p class="page-subtitle">审核用户提交的企业信息通过后可进入企业认证流程</p>
</div>
<div class="toolbar">
<el-select
v-model="filterStatus"
placeholder="审核状态"
clearable
style="width: 140px"
@change="loadList"
>
<el-option label="全部" value="" />
<el-option label="待审核" value="pending" />
<el-option label="已通过" value="approved" />
<el-option label="已拒绝" value="rejected" />
</el-select>
<el-button type="primary" @click="loadList">查询</el-button>
</div>
<el-table
v-loading="loading"
:data="list"
stripe
style="width: 100%"
class="reviews-table"
>
<el-table-column prop="submit_at" label="提交时间" width="170">
<template #default="{ row }">
{{ formatDate(row.submit_at) }}
</template>
</el-table-column>
<el-table-column prop="user_id" label="用户ID" width="280" show-overflow-tooltip />
<el-table-column prop="company_name" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="unified_social_code" label="统一社会信用代码" width="200" />
<el-table-column prop="legal_person_name" label="法人姓名" width="100" />
<el-table-column prop="manual_review_status" label="审核状态" width="100">
<template #default="{ row }">
<el-tag :type="statusTagType(row)" size="small">
{{ reviewStatusDisplay(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row.id)">查看详情</el-button>
<template v-if="canShowApproveReject(row)">
<el-button link type="success" @click="handleApprove(row)">通过</el-button>
<el-button link type="danger" @click="handleReject(row)">拒绝</el-button>
</template>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrap">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next"
@size-change="loadList"
@current-change="loadList"
/>
</div>
<!-- 详情抽屉 -->
<el-drawer
v-model="drawerVisible"
title="企业信息详情"
size="560"
direction="rtl"
:close-on-click-modal="false"
>
<template #header>
<span class="drawer-title">企业信息详情</span>
<div class="drawer-actions">
<template v-if="detail && canShowApproveReject(detail)">
<el-button type="success" size="small" @click="approveFromDrawer">通过</el-button>
<el-button type="danger" size="small" @click="rejectFromDrawer">拒绝</el-button>
</template>
</div>
</template>
<div v-if="detail" class="detail-content">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="企业名称">{{ detail.company_name }}</el-descriptions-item>
<el-descriptions-item label="统一社会信用代码">{{ detail.unified_social_code }}</el-descriptions-item>
<el-descriptions-item label="法人姓名">{{ detail.legal_person_name }}</el-descriptions-item>
<el-descriptions-item label="法人身份证号">{{ detail.legal_person_id }}</el-descriptions-item>
<el-descriptions-item label="法人手机号">{{ detail.legal_person_phone }}</el-descriptions-item>
<el-descriptions-item label="企业地址">{{ detail.enterprise_address }}</el-descriptions-item>
<el-descriptions-item label="授权代表姓名">{{ (detail.authorized_rep_name ?? detail.authorizedRepName) || '-' }}</el-descriptions-item>
<el-descriptions-item label="授权代表身份证号">{{ (detail.authorized_rep_id ?? detail.authorizedRepId) || '-' }}</el-descriptions-item>
<el-descriptions-item label="授权代表手机号">{{ (detail.authorized_rep_phone ?? detail.authorizedRepPhone) || '-' }}</el-descriptions-item>
<el-descriptions-item label="应用场景说明">{{ detail.api_usage || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatDate(detail.submit_at) }}</el-descriptions-item>
<el-descriptions-item label="审核状态">
<el-tag :type="statusTagType(detail)" size="small">
{{ reviewStatusDisplay(detail) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item v-if="detail.manual_review_remark" label="审核备注">
{{ detail.manual_review_remark }}
</el-descriptions-item>
</el-descriptions>
<div class="image-section">
<h4>营业执照</h4>
<div v-if="detail.business_license_image_url" class="image-list">
<a :href="detail.business_license_image_url" target="_blank" rel="noopener" class="image-link">
<img v-if="isImageUrl(detail.business_license_image_url)" :src="detail.business_license_image_url" alt="营业执照" class="thumb" />
<span v-else>查看链接</span>
</a>
</div>
<p v-else class="text-gray-500 text-sm"></p>
</div>
<div class="image-section">
<h4>办公场地照片</h4>
<div v-if="officePlaceUrls.length" class="image-list">
<a v-for="(url, i) in officePlaceUrls" :key="i" :href="url" target="_blank" rel="noopener" class="image-link">
<img v-if="isImageUrl(url)" :src="url" :alt="`场地${i + 1}`" class="thumb" />
<span v-else>链接{{ i + 1 }}</span>
</a>
</div>
<p v-else class="text-gray-500 text-sm"></p>
</div>
<div class="image-section">
<h4>应用场景附件</h4>
<div v-if="scenarioUrls.length" class="image-list">
<a v-for="(url, i) in scenarioUrls" :key="i" :href="url" target="_blank" rel="noopener" class="image-link">
<img v-if="isImageUrl(url)" :src="url" :alt="`场景${i + 1}`" class="thumb" />
<span v-else>链接{{ i + 1 }}</span>
</a>
</div>
<p v-else class="text-gray-500 text-sm"></p>
</div>
<div class="image-section">
<h4>授权代表身份证</h4>
<div v-if="authorizedRepIdUrls.length" class="image-list">
<a v-for="(url, i) in authorizedRepIdUrls" :key="i" :href="url" target="_blank" rel="noopener" class="image-link">
<img v-if="isImageUrl(url)" :src="url" :alt="i === 0 ? '人像面' : '国徽面'" class="thumb" />
<span v-else>{{ i === 0 ? '人像面' : '国徽面' }}</span>
</a>
</div>
<p v-else class="text-gray-500 text-sm"></p>
</div>
</div>
</el-drawer>
<!-- 通过弹窗 -->
<el-dialog v-model="approveDialogVisible" title="审核通过" width="400px">
<el-form label-width="80">
<el-form-item label="审核备注">
<el-input v-model="approveRemark" type="textarea" :rows="3" placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="approveDialogVisible = false">取消</el-button>
<el-button type="success" :loading="actionLoading" @click="confirmApprove">确认通过</el-button>
</template>
</el-dialog>
<!-- 拒绝弹窗 -->
<el-dialog v-model="rejectDialogVisible" title="审核拒绝" width="400px">
<el-form label-width="80">
<el-form-item label="拒绝原因" required>
<el-input v-model="rejectRemark" type="textarea" :rows="3" placeholder="必填" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="actionLoading" :disabled="!rejectRemark.trim()" @click="confirmReject">确认拒绝</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { certificationApi } from '@/api/index.js'
import { ElMessage } from 'element-plus'
const loading = ref(false)
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const filterStatus = ref('')
const drawerVisible = ref(false)
const detail = ref(null)
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const approveRemark = ref('')
const rejectRemark = ref('')
const actionLoading = ref(false)
const pendingRecordId = ref('')
function formatDate(val) {
if (!val) return '-'
try {
const d = new Date(val)
return Number.isNaN(d.getTime()) ? val : d.toLocaleString('zh-CN')
} catch {
return val
}
}
// 已完成企业认证的状态:展示「已审核」,不再展示通过/拒绝
const ENTERPRISE_VERIFIED_STATUSES = ['enterprise_verified', 'contract_applied', 'contract_signed', 'contract_rejected', 'contract_expired', 'completed']
function isEnterpriseVerified(certificationStatus) {
if (!certificationStatus) return false
return ENTERPRISE_VERIFIED_STATUSES.includes(certificationStatus)
}
function reviewStatusDisplay(row) {
if (isEnterpriseVerified(row?.certification_status)) return '已审核'
const m = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
return m[row?.manual_review_status] || row?.manual_review_status || '-'
}
function statusTagType(row) {
if (isEnterpriseVerified(row?.certification_status)) return 'info'
const m = { pending: 'warning', approved: 'success', rejected: 'danger' }
return m[row?.manual_review_status] || 'info'
}
function canShowApproveReject(row) {
if (!row) return false
if (row.manual_review_status !== 'pending') return false
if (isEnterpriseVerified(row.certification_status)) return false
return true
}
// 判断是否可作为图片展示(含七牛云等无扩展名的 CDN URL
function isImageUrl(url) {
if (!url || typeof url !== 'string') return false
if (url.startsWith('blob:') || url.startsWith('data:')) return true
if (url.startsWith('https://file.tianyuanapi.com')) return true
return /\.(jpe?g|png|webp|gif)(\?|$)/i.test(url)
}
const officePlaceUrls = computed(() => {
if (!detail.value?.office_place_image_urls) return []
try {
const v = detail.value.office_place_image_urls
return typeof v === 'string' ? JSON.parse(v) : v
} catch {
return []
}
})
const scenarioUrls = computed(() => {
if (!detail.value?.scenario_attachment_urls) return []
try {
const v = detail.value.scenario_attachment_urls
return typeof v === 'string' ? JSON.parse(v) : v
} catch {
return []
}
})
const authorizedRepIdUrls = computed(() => {
if (!detail.value?.authorized_rep_id_image_urls) return []
try {
const v = detail.value.authorized_rep_id_image_urls
return typeof v === 'string' ? JSON.parse(v) : v
} catch {
return []
}
})
async function loadList() {
loading.value = true
try {
const res = await certificationApi.adminListSubmitRecords({
page: page.value,
page_size: pageSize.value,
manual_review_status: filterStatus.value || undefined
})
const data = res?.data
list.value = data?.items ?? []
total.value = data?.total ?? 0
} catch (e) {
ElMessage.error(e?.message || '加载列表失败')
} finally {
loading.value = false
}
}
async function openDetail(id) {
try {
const res = await certificationApi.adminGetSubmitRecord(id)
detail.value = res?.data ?? res
drawerVisible.value = true
} catch (e) {
ElMessage.error(e?.message || '加载详情失败')
}
}
function handleApprove(row) {
pendingRecordId.value = row.id
approveRemark.value = ''
approveDialogVisible.value = true
}
function approveFromDrawer() {
if (!detail.value?.id) return
pendingRecordId.value = detail.value.id
approveRemark.value = ''
approveDialogVisible.value = true
}
async function confirmApprove() {
if (!pendingRecordId.value) return
actionLoading.value = true
try {
await certificationApi.adminApproveSubmitRecord(pendingRecordId.value, { remark: approveRemark.value })
ElMessage.success('已通过')
approveDialogVisible.value = false
drawerVisible.value = false
detail.value = null
loadList()
} catch (e) {
ElMessage.error(e?.message || '操作失败')
} finally {
actionLoading.value = false
}
}
function handleReject(row) {
pendingRecordId.value = row.id
rejectRemark.value = ''
rejectDialogVisible.value = true
}
function rejectFromDrawer() {
if (!detail.value?.id) return
pendingRecordId.value = detail.value.id
rejectRemark.value = ''
rejectDialogVisible.value = true
}
async function confirmReject() {
if (!pendingRecordId.value || !rejectRemark.value?.trim()) return
actionLoading.value = true
try {
await certificationApi.adminRejectSubmitRecord(pendingRecordId.value, { remark: rejectRemark.value.trim() })
ElMessage.success('已拒绝')
rejectDialogVisible.value = false
drawerVisible.value = false
detail.value = null
loadList()
} catch (e) {
ElMessage.error(e?.message || '操作失败')
} finally {
actionLoading.value = false
}
}
onMounted(() => {
loadList()
})
</script>
<style scoped>
.certification-reviews-page {
padding: 24px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #1e293b;
margin: 0 0 4px;
}
.page-subtitle {
font-size: 14px;
color: #64748b;
margin: 0;
}
.toolbar {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.pagination-wrap {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.drawer-title {
margin-right: 12px;
}
.drawer-actions {
display: flex;
gap: 8px;
align-items: center;
}
.detail-content {
padding-right: 8px;
}
.image-section {
margin-top: 20px;
}
.image-section h4 {
font-size: 14px;
color: #475569;
margin: 0 0 8px;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.image-link {
display: block;
width: 100px;
height: 100px;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
text-align: center;
line-height: 100px;
font-size: 12px;
color: #64748b;
}
.image-link .thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>