This commit is contained in:
2026-08-17 10:54:11 +08:00
parent 835d1d0216
commit f41eaa05a3
2 changed files with 73 additions and 47 deletions
@@ -168,7 +168,7 @@ describe('CustomersView', () => {
expect(customersSource).toContain("'min(640px, calc(100vw - 28px))'")
expect(customersSource).toContain("'min(680px, calc(100vw - 28px))'")
expect(customersSource).toContain('width="min(520px, calc(100vw - 28px))" class="customer-password-dialog"')
expect(customersSource).toContain('width="min(900px, calc(100vw - 28px))" class="review-decision-dialog"')
expect(customersSource).toContain('width="min(1040px, calc(100vw - 28px))" class="review-decision-dialog"')
expect(customersSource).not.toContain('<AppDrawer v-model="editorOpen"')
})
+72 -46
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { CheckCircle2, Download, Eye, FileText, FileUp, KeyRound, LogIn, MoreHorizontal, Pencil, Plus, Power, Save, ShieldCheck, ShieldX, Trash2 } from '@lucide/vue'
import { Building2, CheckCircle2, CircleCheck, Download, Eye, FileCheck2, FileText, FileUp, KeyRound, LogIn, MoreHorizontal, Pencil, Plus, Power, Save, ShieldCheck, ShieldX, Trash2 } from '@lucide/vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import { createPlatformExportTask } from '@/modules/downloads'
@@ -1112,35 +1112,44 @@ onMounted(() => { void loadCustomers() })
<template #footer><el-button type="primary" @click="closeApprovalResult">我已保存,关闭</el-button></template>
</el-dialog>
<el-dialog v-model="decisionOpen" title="客户入驻审核" width="min(900px, calc(100vw - 28px))" class="review-decision-dialog" :close-on-click-modal="false">
<div v-if="selectedReview" class="review-dialog-grid">
<section class="review-dialog-section">
<h3>企业基本信息</h3>
<dl class="review-info-grid">
<div><dt>客户名称</dt><dd>{{ selectedReview.tenantName || '—' }}</dd></div>
<div><dt>客户编号</dt><dd>{{ selectedReview.tenantCode || '—' }}</dd></div>
<div><dt>联系人</dt><dd>{{ selectedReview.contactName || '—' }}</dd></div>
<div><dt>联系电话</dt><dd>{{ selectedReview.contactPhone || '—' }}</dd></div>
<div><dt>联系邮箱</dt><dd>{{ selectedReview.contactEmail || '—' }}</dd></div>
<div><dt>提交时间</dt><dd>{{ formatDate(selectedReview.submittedAt || selectedReview.createdAt) }}</dd></div>
</dl>
</section>
<section class="review-dialog-section">
<h3>资质文件</h3>
<div v-if="reviewQualificationsLoading" class="review-document-state">正在加载资质文件...</div>
<div v-else-if="reviewQualificationsError" class="review-document-error" role="alert"><span>{{ reviewQualificationsError.message }}</span><el-button link type="primary" @click="loadReviewQualifications">重试</el-button></div>
<div class="review-document-table" role="table" aria-label="待审核客户资质">
<div class="review-document-header" role="row"><span role="columnheader">文件类型</span><span role="columnheader">文件名称</span><span role="columnheader">操作</span></div>
<div v-for="item in reviewQualifications" :key="String(item.id)" class="review-document-row" role="row"><span role="cell">{{ qualificationLabel(item.qualificationType) }}</span><span role="cell">{{ qualificationName(item) }}</span><span role="cell" class="review-document-actions"><el-button link type="primary" :loading="reviewDocumentLoading === String(item.id)" @click="loadReviewDocument(item, false)"><Eye :size="14" />预览</el-button><el-button link :disabled="Boolean(reviewDocumentLoading)" @click="loadReviewDocument(item, true)"><Download :size="14" />下载</el-button></span></div>
<div v-if="!reviewQualificationsLoading && !reviewQualificationsError && reviewQualifications.length === 0" class="review-document-empty">暂无资质文件</div>
</div>
<el-dialog v-model="decisionOpen" title="客户入驻审核" width="min(1040px, calc(100vw - 28px))" class="review-decision-dialog" :close-on-click-modal="false">
<div v-if="selectedReview" class="review-dialog-content">
<header class="review-dialog-header">
<div class="review-dialog-title"><span class="review-dialog-icon"><Building2 :size="18" /></span><div><strong>{{ selectedReview.tenantName || '未命名客户' }}</strong><span>客户编号:{{ selectedReview.tenantCode || '未生成' }}</span></div></div>
<el-tag type="warning" effect="plain">待审核</el-tag>
</header>
<div class="review-dialog-grid">
<section class="review-dialog-section review-dialog-card">
<div class="review-section-heading"><div><h3>企业基本信息</h3><span>核对客户提交的主体资料</span></div><CircleCheck :size="18" /></div>
<dl class="review-info-grid">
<div><dt>客户名称</dt><dd>{{ selectedReview.tenantName || '未填写' }}</dd></div>
<div><dt>客户编号</dt><dd>{{ selectedReview.tenantCode || '未生成' }}</dd></div>
<div><dt>联系人</dt><dd>{{ selectedReview.contactName || '未填写' }}</dd></div>
<div><dt>联系电话</dt><dd>{{ selectedReview.contactPhone || '未填写' }}</dd></div>
<div><dt>联系邮箱</dt><dd>{{ selectedReview.contactEmail || '未填写' }}</dd></div>
<div><dt>提交时间</dt><dd>{{ formatDate(selectedReview.submittedAt || selectedReview.createdAt) }}</dd></div>
</dl>
</section>
<section class="review-dialog-section review-dialog-card">
<div class="review-section-heading"><div><h3>资质文件</h3><span>共 {{ reviewQualifications.length }} 份,逐项查看后再作决定</span></div><FileCheck2 :size="18" /></div>
<div v-if="reviewQualificationsLoading" class="review-document-state">正在加载资质文件...</div>
<div v-else-if="reviewQualificationsError" class="review-document-error" role="alert"><span>{{ reviewQualificationsError.message }}</span><el-button link type="primary" @click="loadReviewQualifications">重试</el-button></div>
<div v-else class="review-document-table" role="table" aria-label="待审核客户资质">
<div class="review-document-header" role="row"><span role="columnheader">资质类型</span><span role="columnheader">已上传文件</span><span role="columnheader">操作</span></div>
<div v-for="item in reviewQualifications" :key="String(item.id)" class="review-document-row" role="row"><span role="cell" class="review-document-type">{{ qualificationLabel(item.qualificationType) }}</span><span role="cell" class="review-document-name" :title="qualificationName(item)"><FileText :size="15" />{{ qualificationName(item) }}</span><span role="cell" class="review-document-actions"><el-button link type="primary" :loading="reviewDocumentLoading === String(item.id)" @click="loadReviewDocument(item, false)"><Eye :size="14" />预览</el-button><el-button link :disabled="Boolean(reviewDocumentLoading)" @click="loadReviewDocument(item, true)"><Download :size="14" />下载</el-button></span></div>
<div v-if="reviewQualifications.length === 0" class="review-document-empty"><FileText :size="18" /><span>暂无资质文件</span></div>
</div>
</section>
</div>
<section class="review-decision-card">
<div class="review-section-heading"><div><h3>审核决定</h3><span>确认资料无误后提交审核结果</span></div><ShieldCheck :size="18" /></div>
<el-radio-group v-if="access.can('platform:customer:approve') && access.can('platform:customer:reject')" v-model="decision.type" class="decision-type" aria-label="审核决定"><el-radio-button label="approve">通过</el-radio-button><el-radio-button label="reject">驳回</el-radio-button></el-radio-group>
<el-form ref="decisionFormRef" :model="decision" :rules="decisionRules" label-position="top">
<el-form-item prop="comment" :label="decision.type === 'approve' ? '审核意见' : '驳回原因'" required><el-input v-model="decision.comment" type="textarea" :rows="3" maxlength="300" show-word-limit :placeholder="decision.type === 'approve' ? '可填写审核意见' : '请明确说明需要企业修改的内容'" /></el-form-item>
</el-form>
</section>
</div>
<el-radio-group v-if="access.can('platform:customer:approve') && access.can('platform:customer:reject')" v-model="decision.type" class="decision-type" aria-label="审核决定"><el-radio-button label="approve">通过</el-radio-button><el-radio-button label="reject">驳回</el-radio-button></el-radio-group>
<el-form ref="decisionFormRef" :model="decision" :rules="decisionRules" label-position="top">
<el-form-item prop="comment" :label="decision.type === 'approve' ? '审核意见' : '驳回原因'" required><el-input v-model="decision.comment" type="textarea" :rows="3" maxlength="300" show-word-limit :placeholder="decision.type === 'approve' ? '可填写审核意见' : '请明确说明需要企业修改的内容'" /></el-form-item>
</el-form>
<template #footer><div class="drawer-footer"><el-button :disabled="reviewSubmitting" @click="decisionOpen = false">返回</el-button><el-button v-if="access.can('platform:customer:reject') && decision.type === 'reject'" type="danger" :loading="reviewSubmitting" @click="submitDecision">确认驳回</el-button><el-button v-if="access.can('platform:customer:approve') && decision.type === 'approve'" type="primary" :loading="reviewSubmitting" @click="submitDecision">审核通过</el-button></div></template>
<template #footer><div class="drawer-footer"><el-button :disabled="reviewSubmitting" @click="decisionOpen = false">取消</el-button><el-button v-if="access.can('platform:customer:reject') && decision.type === 'reject'" type="danger" :loading="reviewSubmitting" @click="submitDecision">确认驳回</el-button><el-button v-if="access.can('platform:customer:approve') && decision.type === 'approve'" type="primary" :loading="reviewSubmitting" @click="submitDecision">审核通过</el-button></div></template>
</el-dialog>
<AppDialog v-model="reviewPreviewOpen" :title="reviewPreviewName || '查看资质文件'" width="min(860px, calc(100vw - 28px))" destroy-on-close :show-confirm="false" cancel-text="关闭预览" @closed="clearReviewPreview">
@@ -1232,29 +1241,46 @@ onMounted(() => { void loadCustomers() })
:global(.customer-editor-dialog .el-dialog__footer) { flex:0 0 auto; }
.customer-editor-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; } .customer-editor-footer > span { color:var(--cc-muted); font-size:12px; text-align:left; } .customer-editor-footer > div { display:flex; flex:0 0 auto; gap:8px; } .customer-editor-footer :deep(.el-button + .el-button) { margin-left:0; }
.decision-summary { display:flex; min-width:0; align-items:center; justify-content:space-between; gap:12px; margin-bottom:18px; padding:12px 14px; border-radius:8px; background:#f8fafc; } .decision-summary > * { min-width:0; overflow-wrap:anywhere; } .decision-summary strong { color:var(--cc-text); font-size:13px; } .decision-summary span { color:var(--cc-muted); font-size:11px; }
:global(.review-decision-dialog.el-dialog) { display:flex; max-height:min(90vh,820px); flex-direction:column; }
:global(.review-decision-dialog .el-dialog__body) { min-height:0; overflow-y:auto; padding:20px 28px; }
:global(.review-decision-dialog.el-dialog) { display:flex; max-height:min(90vh,860px); flex-direction:column; overflow:hidden; }
:global(.review-decision-dialog .el-dialog__header) { padding:22px 28px 16px; border-bottom:1px solid #eef1f6; }
:global(.review-decision-dialog .el-dialog__body) { min-height:0; overflow-y:auto; padding:20px 28px 24px; background:#fbfcfe; }
:global(.review-decision-dialog .el-dialog__footer) { flex:0 0 auto; }
.review-dialog-grid { display:grid; grid-template-columns:1fr 1fr; gap:24px; margin-bottom:18px; }
.review-dialog-section { min-width:0; }
.review-dialog-section h3 { margin:0 0 12px; padding-bottom:8px; border-bottom:2px solid #667eea; color:#1a1a2e; font-size:14px; font-weight:600; }
.review-info-grid { display:grid; gap:7px; margin:0; }
.review-info-grid > div { display:grid; grid-template-columns:110px minmax(0,1fr); gap:12px; }
.review-info-grid dt { color:#64748b; font-size:12px; }
.review-info-grid dd { min-width:0; margin:0; overflow-wrap:anywhere; color:#1e293b; font-size:13px; }
.review-document-table { overflow:hidden; border:1px solid #e4e4e7; border-radius:6px; }
.review-document-header,.review-document-row { display:grid; grid-template-columns:minmax(120px,.8fr) minmax(140px,1fr) 150px; }
.review-document-header { background:rgba(102,126,234,.08); color:#52525b; font-size:12px; font-weight:600; }
.review-document-header > span,.review-document-row > span { min-width:0; padding:9px 10px; overflow-wrap:anywhere; border-right:1px solid #e4e4e7; }
.review-dialog-content { display:grid; gap:16px; }
.review-dialog-header { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:14px 16px; border:1px solid #dbe4f0; border-radius:10px; background:linear-gradient(90deg,#f4f7ff,#fff); }
.review-dialog-title { display:flex; min-width:0; align-items:center; gap:11px; }
.review-dialog-title > div { display:grid; min-width:0; gap:3px; }
.review-dialog-title strong { overflow:hidden; color:#18243a; font-size:16px; text-overflow:ellipsis; white-space:nowrap; }
.review-dialog-title span:not(.review-dialog-icon) { overflow:hidden; color:#718096; font-size:12px; text-overflow:ellipsis; white-space:nowrap; }
.review-dialog-icon { display:grid; width:36px; height:36px; flex:0 0 36px; place-items:center; border-radius:9px; background:#e9efff; color:#526fe8; }
.review-dialog-grid { display:grid; grid-template-columns:minmax(270px,.85fr) minmax(390px,1.15fr); gap:16px; }
.review-dialog-section,.review-decision-card { min-width:0; padding:16px; border:1px solid #e5eaf1; border-radius:10px; background:#fff; box-shadow:0 2px 8px rgba(15,23,42,.03); }
.review-section-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:14px; }
.review-section-heading > div { min-width:0; }
.review-section-heading h3 { margin:0; color:#1a2438; font-size:14px; font-weight:700; }
.review-section-heading span { display:block; margin-top:4px; color:#8a94a6; font-size:11px; line-height:1.4; }
.review-section-heading > svg { flex:0 0 auto; color:#657bea; }
.review-info-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; margin:0; }
.review-info-grid > div { min-width:0; padding:10px 11px; border-radius:7px; background:#f7f9fc; }
.review-info-grid dt { margin-bottom:4px; color:#8792a5; font-size:11px; }
.review-info-grid dd { min-width:0; margin:0; overflow:hidden; color:#28364d; font-size:13px; font-weight:600; text-overflow:ellipsis; white-space:nowrap; }
.review-document-table { overflow:hidden; border:1px solid #e4e9f1; border-radius:8px; }
.review-document-header,.review-document-row { display:grid; grid-template-columns:118px minmax(0,1fr) 138px; }
.review-document-header { background:#f5f7fb; color:#657188; font-size:11px; font-weight:700; }
.review-document-header > span,.review-document-row > span { min-width:0; padding:9px 10px; overflow-wrap:anywhere; border-right:1px solid #e4e9f1; }
.review-document-header > span:last-child,.review-document-row > span:last-child { border-right:0; }
.review-document-row { border-top:1px solid #e4e4e7; color:#334155; font-size:13px; }
.review-document-actions { display:flex; align-items:center; gap:8px; }
.review-document-row { min-height:46px; border-top:1px solid #edf0f4; color:#334155; font-size:12px; }
.review-document-row > span { display:flex; align-items:center; }
.review-document-type { font-weight:650; }
.review-document-name { min-width:0; gap:6px; overflow:hidden; color:#4c5b72; text-overflow:ellipsis; white-space:nowrap; }
.review-document-name svg { flex:0 0 auto; color:#8798cf; }
.review-document-actions { justify-content:flex-end; gap:6px; }
.review-document-actions :deep(.el-button + .el-button) { margin-left:0; }
.review-document-state,.review-document-error,.review-document-empty { padding:14px; color:var(--cc-muted); font-size:12px; }
.review-document-error { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:8px; border:1px solid #fecdca; border-radius:6px; background:#fffafa; color:#b42318; }
.review-document-empty { text-align:center; }
.decision-summary > div > span { display:block; margin-top:4px; }.decision-type { margin:0 0 16px; }.password-input-row { display:grid; grid-template-columns:minmax(0,1fr) auto; width:100%; gap:8px; }.dialog-hint { margin:12px 0 0; color:var(--cc-muted); font-size:12px; line-height:1.6; }.archive-confirm { display:grid; gap:8px; margin:16px 0; }.archive-confirm strong { color:var(--cc-text); }.archive-confirm span { color:var(--cc-muted); font-size:12px; }
.review-document-empty { display:flex; align-items:center; justify-content:center; gap:7px; text-align:center; }.review-document-empty svg { color:#a5afc1; }
.review-decision-card :deep(.el-form-item) { margin-bottom:0; }.review-decision-card :deep(.el-form-item__label) { color:#4b5870; font-size:12px; font-weight:650; }.decision-summary > div > span { display:block; margin-top:4px; }.decision-type { margin:0 0 14px; }.password-input-row { display:grid; grid-template-columns:minmax(0,1fr) auto; width:100%; gap:8px; }.dialog-hint { margin:12px 0 0; color:var(--cc-muted); font-size:12px; line-height:1.6; }.archive-confirm { display:grid; gap:8px; margin:16px 0; }.archive-confirm strong { color:var(--cc-text); }.archive-confirm span { color:var(--cc-muted); font-size:12px; }
.approval-result-warning { display:flex; align-items:flex-start; gap:10px; padding:12px 14px; border:1px solid #fecdca; border-radius:8px; background:#fffafa; color:#b42318; } .approval-result-warning strong,.approval-result-warning span { display:block; } .approval-result-warning strong { font-size:14px; } .approval-result-warning span { margin-top:4px; font-size:12px; line-height:1.5; } .approval-credentials { margin:18px 0 0; border:1px solid var(--cc-border); border-radius:8px; } .approval-credentials > div { display:grid; grid-template-columns:90px minmax(0,1fr); gap:12px; padding:12px 14px; } .approval-credentials > div + div { border-top:1px solid var(--cc-border); } .approval-credentials dt { color:var(--cc-muted); font-size:12px; } .approval-credentials dd { display:flex; min-width:0; align-items:center; gap:10px; margin:0; } .approval-credentials code { min-width:0; flex:1; overflow-wrap:anywhere; color:var(--cc-text); font-size:14px; font-weight:700; }
@media (max-width:720px) { .customer-editor-footer,.review-sync-error { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; width:100%; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } .decision-summary { align-items:flex-start; flex-direction:column; }.review-dialog-grid { grid-template-columns:1fr; gap:18px; }.review-info-grid > div { grid-template-columns:92px minmax(0,1fr); }.password-input-row { grid-template-columns:1fr; }.table-actions :deep(.el-button) { min-height:40px; } }
@media (max-width:720px) { .customer-editor-footer,.review-sync-error { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; width:100%; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } .decision-summary { align-items:flex-start; flex-direction:column; }.review-dialog-grid { grid-template-columns:1fr; gap:12px; }.review-info-grid { grid-template-columns:1fr 1fr; }.review-document-header,.review-document-row { grid-template-columns:104px minmax(0,1fr) 124px; }.review-dialog-header { align-items:flex-start; }.password-input-row { grid-template-columns:1fr; }.table-actions :deep(.el-button) { min-height:40px; } }
@media (max-width:480px) { :global(.review-decision-dialog .el-dialog__header),:global(.review-decision-dialog .el-dialog__body) { padding-left:16px; padding-right:16px; }.review-info-grid { grid-template-columns:1fr; }.review-document-header { display:none; }.review-document-row { grid-template-columns:1fr; gap:6px; padding:10px; }.review-document-row > span { padding:0; border-right:0; }.review-document-actions { justify-content:flex-start; }.review-dialog-title strong { max-width:190px; } }
@media (max-width:360px) { .customers-table-actions,.capital-input { grid-template-columns:1fr; } }
</style>