@@ -1,7 +1,9 @@
< script setup lang = "ts" >
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref , watch , type Component } from 'vue '
import '@/styles/adminDensity.css '
import '@/modules/customers/customerDensity.css'
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue'
import { useRoute , useRouter } from 'vue-router'
import { Building2 , CheckCircle2 , CircleCheck , Download , Eye , FileCheck2 , FileText , FileUp , KeyRound , LogIn , MoreHorizontal , Pencil , Plus , Power , Save , ShieldCheck , ShieldX , Trash2 } from '@lucide/vue'
import { Building2 , CheckCircle2 , CircleCheck , FileCheck2 , FileText , MoreHorizontal , ShieldCheck , ShieldX } 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'
@@ -225,9 +227,7 @@ let loadSequence = 0
let reviewLoadSequence = 0
let editorLoadSequence = 0
const editorTitle = computed ( ( ) => editingId . value ? '编辑客户' : '新增客户' )
const editorDialogWidth = computed ( ( ) => editingId . value
? 'min(640px, calc(100vw - 28px))'
: 'min(680px, calc(100vw - 28px))' )
const editorDialogWidth = 'min(1120px, calc(100vw - 28px))'
const canUploadQualifications = computed ( ( ) => ! editorStatus . value || [ 'PROFILE_REQUIRED' , 'REJECTED' ] . includes ( editorStatus . value ) )
const canSubmitReview = computed ( ( ) => Boolean ( editingId . value ) && [ 'PROFILE_REQUIRED' , 'REJECTED' ] . includes ( editorStatus . value || '' ) )
const uploadingQualifications = computed ( ( ) => qualifications . value . some ( ( item ) => item . uploading ) )
@@ -636,7 +636,7 @@ async function submitReview() {
const missing = qualifications . value . filter ( ( item ) => item . required && ! item . uploaded )
for ( const item of missing ) item . error = ` ${ item . label } 为提交审核必填文件 `
if ( missing . length ) { ElMessage . warning ( ` 请先上传 ${ missing . map ( ( item ) => item . label ) . join ( '、' ) } ` ) ; return }
await ElMessageBox . confirm ( '提交后客户资料与资质将进入企业审核,审核完成前不能继续替换文件。确认提交吗?' , '提交企业审核' , { type : 'warning' , confirmButtonText : '确认提交' , cancelButtonText : '继续完善' } )
await ElMessageBox . confirm ( '提交后客户资料与资质将进入企业审核,审核完成前不能继续替换文件。确认提交吗?' , '提交企业审核' , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认提交' , cancelButtonText : '继续完善' } )
const saved = await submitPlatformTenantReview ( editingId . value )
editorStatus . value = saved . status ; ElMessage . success ( '审核单已提交' )
editorOpen . value = false ; await loadCustomers ( )
@@ -661,12 +661,6 @@ function canArchive(row: PlatformTenantResource) {
return access . can ( 'platform:customer:delete' ) && [ 'PROFILE_REQUIRED' , 'REJECTED' ] . includes ( row . status )
}
type CustomerRowAction = 'reset-password' | 'impersonate' | 'status' | 'delete'
const customerRowActionIcons : Record < CustomerRowAction , Component > = {
'reset-password' : KeyRound ,
impersonate : LogIn ,
status : Power ,
delete : Trash2 ,
}
function hasReviewAction ( row : PlatformTenantResource ) {
return pendingReviewByTenantId . value . has ( String ( row . id ) )
&& access . can ( 'platform:customer:review' )
@@ -721,7 +715,7 @@ function handleMoreAction(row: PlatformTenantResource, command: string) {
async function toggleStatus ( row : PlatformTenantResource ) {
const next = row . status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE' ; const verb = next === 'ACTIVE' ? '启用' : '停用'
try {
await ElMessageBox . confirm ( ` 确认 ${ verb } 客户“ ${ row . tenantName } ”吗? ` , ` ${ verb } 客户 ` , { type : 'warning' , confirmButtonText : ` 确认 ${ verb } ` , cancelButtonText : '取消' } )
await ElMessageBox . confirm ( ` 确认 ${ verb } 客户“ ${ row . tenantName } ”吗? ` , ` ${ verb } 客户 ` , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : ` 确认 ${ verb } ` , cancelButtonText : '取消' } )
rowActionId . value = String ( row . id ) ; await changePlatformTenantStatus ( row . id , next )
ElMessage . success ( ` 客户已 ${ verb } ` ) ; await loadCustomers ( )
} catch ( error ) {
@@ -1048,7 +1042,7 @@ onMounted(() => { void loadCustomers() })
< / script >
< template >
< AdminPage as = "div" class = "customers-page" >
< AdminPage as = "div" class = "customers-page admin-density customer-density " >
< OperationErrorToast : details = "passwordError || impersonationError || archiveError" / >
< ListPageLayout aria -label = " 客户管理 " >
< template # toolbar > < QueryToolbar v-model = "filters" class="surface-panel" ariaLabel="客户筛选" :fields="queryFields" preference-key="customers-query" @search="applyFilters" / > < / template >
@@ -1060,16 +1054,16 @@ onMounted(() => { void loadCustomers() })
< section v-if = "!loadError" class="surface-panel table-panel" :aria-busy="loading" >
< div v-if = "reviewLoadError" class="review-sync-error" role="alert"><div><ShieldX :size="17" /><span>{{ reviewLoadError.message }}</span></div><el-button link type="primary" @click="reloadPendingReviews" > 重新加载待审核客户 < / el -button > < / div >
< DataTable v -model :page = "page" v -model :page-size = "pageSize" :data = "result.records" :columns = "customerColumns" preference -key = " customers -table " :loading = "loading" :total = "result.total" aria -label = " 客户列表 " row -key = " id " empty -text = " 暂无符合筛选条件的客户 " class = "customer-list-table" :stripe = "true" border >
< template # toolbar -actions > < div class = "customers-table-actions" > < el-button v-if = "access.can('platform:customer:export') && access.can('platform:download:list')" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportCustomers"><Download :size="16" /> 导出Excel</el-button><el-button v-if="access.can('platform:customer:create')" type="primary" :disabled="loading" @click="openCreate"><Plus :size="16" / > 新增客户 < / el-button > < / div > < / template >
< template # toolbar -actions > < div class = "customers-table-actions" > < el-button v-if = "access.can('platform:customer:export') && access.can('platform:download:list')" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportCustomers">导出Excel</el-button><el-button v-if="access.can('platform:customer:create')" type="primary" :disabled="loading" @click="openCreate" > 新增客户 < / el -button > < / div > < / template >
< template # actions = "{ row }" >
< div class = "table-actions" >
< el-button v-if = "access.can('platform:customer:list')" class="customer-action-button customer-action-detail" size="small" @click="showDetail(row as PlatformTenantResource)"><Eye :size="14" / > 详情 < / el-button >
< el-button v-if = "access.can('platform:customer:edit') && (row as PlatformTenantResource).status !== 'ARCHIVED'" class="customer-action-button customer-action-edit" size="small" @click="openEdit(row as PlatformTenantResource)"><Pencil :size="14" / > 编辑 < / el-button >
< el-button v-if = "hasReviewAction(row as PlatformTenantResource)" class="customer-action-button customer-action-review" size="small" :disabled="reviewSubmitting" @click="openDecision(row as PlatformTenantResource, access.can('platform:customer:approve') ? 'approve' : 'reject')"><ShieldCheck :size="14" / > 审核 < / el-button >
< el-button v-for = "action in customerDirectSecondaryActions(row as PlatformTenantResource)" :key="action" :class="[`customer-action-button`, `customer-action-${action}`, { 'is-enable': action === 'status' && (row as PlatformTenantResource).status !== 'ACTIVE', 'is-disable': action === 'status' && (row as PlatformTenantResource).status === 'ACTIVE' }]" size="small" :disabled="customerActionDisabled(action)" @click="handleMoreAction(row as PlatformTenantResource, action)"><component :is="customerRowActionIcons[action]" :size="14" / > { { customerActionLabel ( row as PlatformTenantResource , action ) } } < / el-button >
< el-button v-if = "access.can('platform:customer:list')" class="customer-action-button customer-action-detail" size="small" @click="showDetail(row as PlatformTenantResource)" > 详情 < / el -button >
< el-button v-if = "access.can('platform:customer:edit') && (row as PlatformTenantResource).status !== 'ARCHIVED'" class="customer-action-button customer-action-edit" size="small" @click="openEdit(row as PlatformTenantResource)" > 编辑 < / el -button >
< el-button v-if = "hasReviewAction(row as PlatformTenantResource)" class="customer-action-button customer-action-review" size="small" :disabled="reviewSubmitting" @click="openDecision(row as PlatformTenantResource, access.can('platform:customer:approve') ? 'approve' : 'reject')" > 审核 < / el -button >
< el-button v-for = "action in customerDirectSecondaryActions(row as PlatformTenantResource)" :key="action" :class="[`customer-action-button`, `customer-action-${action}`, { 'is-enable': action === 'status' && (row as PlatformTenantResource).status !== 'ACTIVE', 'is-disable': action === 'status' && (row as PlatformTenantResource).status === 'ACTIVE' }]" size="small" :disabled="customerActionDisabled(action)" @click="handleMoreAction(row as PlatformTenantResource, action)" > {{ customerActionLabel ( row as PlatformTenantResource , action ) }} < / el -button >
< el-dropdown v-if = "customerOverflowActions(row as PlatformTenantResource).length" trigger="click" popper-class="customer-action-menu" @command="handleMoreAction(row as PlatformTenantResource, $event as string)" >
< el -button class = "customer-action-button customer-action-more" size = "small" plain : aria -label = " customerMoreActionsLabel ( row as PlatformTenantResource ) " : title = "customerMoreActionsLabel(row as PlatformTenantResource)" > < MoreHorizontal :size = "16 " / > < / el-button >
< template # dropdown > < el-dropdown-menu > < el-dropdown-item v-for = "action in customerOverflowActions(row as PlatformTenantResource)" :key="action" :command="action" :disabled="customerActionDisabled(action)" :class="{ 'customer-action-menu__danger': customerActionType(row as PlatformTenantResource, action) === 'danger' }"><component :is="customerRowActionIcons[action]" :size="15" / > { { customerActionLabel ( row as PlatformTenantResource , action ) } } < / el-dropdown-item > < / el-dropdown-menu > < / template >
< el -button class = "customer-action-button customer-action-more" size = "small" plain : aria -label = " customerMoreActionsLabel ( row as PlatformTenantResource ) " : title = "customerMoreActionsLabel(row as PlatformTenantResource)" > < MoreHorizontal :size = "17 " aria -hidden = " true " / > < / el-button >
< template # dropdown > < el-dropdown-menu > < el-dropdown-item v-for = "action in customerOverflowActions(row as PlatformTenantResource)" :key="action" :command="action" :disabled="customerActionDisabled(action)" :class="{ 'customer-action-menu__danger': customerActionType(row as PlatformTenantResource, action) === 'danger' }" > {{ customerActionLabel ( row as PlatformTenantResource , action ) }} < / el -dropdown -item > < / el-dropdown-menu > < / template >
< / el-dropdown >
< / div >
< / template >
@@ -1081,9 +1075,9 @@ onMounted(() => { void loadCustomers() })
< / section >
< / ListPageLayout >
< AppDialog v-model = "editorOpen" :title="editorTitle" :width="editorDialogWidth" class="customer-editor-dialog" :loading="editorLoading" :confirm-loading="saving" destroy -on -close >
< AppDialog v-model = "editorOpen" :title="editorTitle" :width="editorDialogWidth" class="admin-density-dialog customer-density-dialog customer-editor-dialog" :loading="editorLoading" :confirm-loading="saving" destroy -on -close >
< el-form ref = "formRef" v-loading = "editorLoading" :model="form" :rules="rules" label-position="top" class="drawer-form" :disabled="editorLoading || saving || submitting || uploadingQualifications" >
< FormSection title = "基础资料" description = "维护工商登记和企业主体信息。" > < div class = "form-grid two -cols" >
< FormSection title = "基础资料" description = "维护工商登记和企业主体信息。" > < div class = "form-grid three -cols" >
< el-form-item label = "公司名称" prop = "tenantName" required > < el-input v-model = "form.tenantName" placeholder="请输入工商登记公司名称" maxlength="128" / > < / el-form-item >
< el-form-item label = "客户昵称" prop = "customerNickname" > < el-input v-model = "form.customerNickname" placeholder="不填默认生成中文拼音大写首字母,如 SZWL" maxlength="32" /><small class="field-hint" > 创建后仍可在客户资料中修改 。 < / small > < / el -form -item >
< el-form-item label = "营业执照企业名称" prop = "legalName" required > < el-input v-model = "form.legalName" placeholder="请输入营业执照上的企业名称" maxlength="128" / > < / el-form-item >
@@ -1093,7 +1087,7 @@ onMounted(() => { void loadCustomers() })
< el-form-item label = "注册资本" prop = "registeredCapital" > < div class = "capital-input" > < el-input-number v-model = "form.registeredCapital" :min="0" :precision="2" controls-position="right" placeholder="请输入注册资本(选填)" /><el-select v-model="form.registeredCapitalCurrency" aria-label="注册资本币种"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /><el-option label="港币 HKD" value="HKD" / > < / el-select > < / div > < / el-form-item >
< / div > < el-form-item label = "注册地址" prop = "registeredAddress" > < el-input v-model = "form.registeredAddress" placeholder="请输入省市区、街道及门牌信息" maxlength="500" / > < / el-form-item > < / FormSection >
< FormSection title = "联系与商务" description = "维护客户联系人、账期和内部跟进信息。" > < div class = "form-grid two -cols" >
< FormSection title = "联系与商务" description = "维护客户联系人、账期和内部跟进信息。" > < div class = "form-grid three -cols" >
< el-form-item label = "联系人" prop = "contactName" required > < el-input v-model = "form.contactName" placeholder="请输入主要联系人姓名" maxlength="64" / > < / el-form-item >
< el-form-item label = "公司联系电话" prop = "contactPhone" required > < el-input v-model = "form.contactPhone" placeholder="请输入公司联系电话" maxlength="32" /><small class="field-hint" > 仅用于企业业务联系 , 不会修改企业管理员登录手机号 ; 登录手机号请在成员 / 账号管理中单独维护 , 修改会影响登录 。 < / small > < / el -form -item >
< el-form-item label = "联系邮箱" prop = "contactEmail" > < el-input v-model = "form.contactEmail" placeholder="如:contact@example.com" maxlength="128" / > < / el-form-item >
@@ -1114,9 +1108,9 @@ onMounted(() => { void loadCustomers() })
< button v-if = "draft.file || draft.uploaded" type="button" class="qualification-preview-trigger" :class="{ 'is-loading': draft.previewLoading }" :disabled="draft.previewLoading" :aria-label="`预览${draft.label}: ${draft.file?.name || draft.uploadedName}`" @click="openQualificationPreview(draft)" >
< img v-if = "draft.previewUrl && draft.previewKind === 'image' && !draft.previewFailed" :src="draft.previewUrl" :alt="`${draft.label}预览`" @error="handleQualificationPreviewError(draft)" / >
< span v-else class = "qualification-preview-placeholder" > < FileText :size = "20" / > < small > { { draft . previewKind === 'pdf' ? 'PDF' : '文件' } } < / small > < / span >
< span class = "qualification-preview-action" > < Eye :size = "14" / > { { draft . previewLoading ? '读取中' : ( draft . previewFailed ? '重试' : '查看' ) } } < / span >
< span class = "qualification-preview-action" > { { draft . previewLoading ? '读取中' : ( draft . previewFailed ? '重试' : '查看' ) } } < / span >
< / button >
< div class = "qualification-file-control" > < label class = "file-picker" : class = "{ 'is-disabled': !canUploadQualifications || draft.uploading }" > < FileUp :size = "16" / > < span > { { draft . uploading ? '上传中' : ( draft . file ? '已选择新文件' : ( draft . uploaded ? '更换文件' : '选择文件' ) ) } } < / span > < input type = "file" accept = ".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" : disabled = "!canUploadQualifications || draft.uploading" @change ="selectQualificationFile($event, draft)" / > < / label > < span class = "qualification-file-name" : title = "draft.file?.name || draft.uploadedName" > { { draft . file ? . name || draft . uploadedName || '未选择文件' } } < / span > < small v-if = "draft.error" class="qualification-error" role="alert" > {{ draft.error }} < / small > < / div >
< div class = "qualification-file-control" > < label class = "file-picker" : class = "{ 'is-disabled': !canUploadQualifications || draft.uploading }" > < span > { { draft . uploading ? '上传中' : ( draft . file ? '已选择新文件' : ( draft . uploaded ? '更换文件' : '选择文件' ) ) } } < / span > < input type = "file" accept = ".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" : disabled = "!canUploadQualifications || draft.uploading" @change ="selectQualificationFile($event, draft)" / > < / label > < span class = "qualification-file-name" : title = "draft.file?.name || draft.uploadedName" > { { draft . file ? . name || draft . uploadedName || '未选择文件' } } < / span > < small v-if = "draft.error" class="qualification-error" role="alert" > {{ draft.error }} < / small > < / div >
< / div >
< div class = "qualification-validity" role = "cell" > < el-date-picker v-model = "draft.validTo" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期" :disabled="!canUploadQualifications || draft.uploaded" / > < / div >
< / div >
@@ -1124,17 +1118,17 @@ onMounted(() => { void loadCustomers() })
< p class = "qualification-hint" > 支持 PDF 、 JPG 、 PNG , 单个文件不超过 5 MB ; < span > * < / span > 为提交审核必填项 。 < / p >
< / FormSection >
< / el-form >
< template # footer > < div class = "customer-editor-footer" > < span > 保存资料时会一并上传已选资质 ; 进入审核仍需点击 “ 提交审核 ” 并再次确认 < / span > < div > < el-button : disabled = "editorLoading || saving || submitting || uploadingQualifications" @click ="editorOpen = false" > 关闭 < / el -button > < el-button :loading = "saving" : disabled = "editorLoading || submitting || uploadingQualifications" @click ="saveCustomer" > < Save :size = "16" / > 保存资料和资质 < / el-button > < el-button v-if = "canSubmitReview" type="primary" :loading="submitting" :disabled="editorLoading || saving || uploadingQualifications" @click="submitReview" > 提交审核 < / el -button > < / div > < / div > < / template >
< template # footer > < div class = "customer-editor-footer" > < span > 保存资料时会一并上传已选资质 ; 进入审核仍需点击 “ 提交审核 ” 并再次确认 < / span > < div > < el-button : disabled = "editorLoading || saving || submitting || uploadingQualifications" @click ="editorOpen = false" > 关闭 < / el -button > < el-button :loading = "saving" : disabled = "editorLoading || submitting || uploadingQualifications" @click ="saveCustomer" > 保存资料和资质 < / el -button > < el-button v-if = "canSubmitReview" type="primary" :loading="submitting" :disabled="editorLoading || saving || uploadingQualifications" @click="submitReview" > 提交审核 < / el -button > < / div > < / div > < / template >
< / AppDialog >
< AppDialog v-model = "qualificationPreviewOpen" :title="qualificationPreviewName || '查看资质文件'" width="min(860px, calc(100vw - 28px))" class="qualification-preview-dialog" destroy-on-close :show-confirm="false" cancel-text="关闭预览" >
< AppDialog v-model = "qualificationPreviewOpen" :title="qualificationPreviewName || '查看资质文件'" width="min(860px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog qualification-preview-dialog" destroy-on-close :show-confirm="false" cancel-text="关闭预览" >
< div class = "qualification-preview" >
< img v-if = "qualificationPreviewUrl && qualificationPreviewKind === 'image'" :src="qualificationPreviewUrl" :alt="`${qualificationPreviewName}预览`" / >
< iframe v-else-if = "qualificationPreviewUrl && qualificationPreviewKind === 'pdf'" :src="qualificationPreviewUrl" :title="`${qualificationPreviewName} PDF 预览`" / >
< / div >
< / AppDialog >
< el-dialog v-model = "approvalResultOpen" :title="credentialSource === 'creation' ? '客户已创建,管理员账号已生成' : '审核通过,管理员账号已创建'" width="min(560px, calc(100vw - 28px))" class="approval-result-dialog" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" >
< el-dialog v-model = "approvalResultOpen" :title="credentialSource === 'creation' ? '客户已创建,管理员账号已生成' : '审核通过,管理员账号已创建'" width="min(560px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog approval-result-dialog" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" >
< div v-if = "approvalResult" class="approval-result-content" >
< div class = "approval-result-warning" role = "alert" > < ShieldCheck :size = "20" / > < div > < strong > 请立即保存以下一次性凭证 < / strong > < span > 临时密码只在本次 { { credentialSource === 'creation' ? '创建' : '审核' } } 响应中返回 , 关闭后无法再次查看 。 < / span > < / div > < / div >
< dl class = "approval-credentials" >
@@ -1145,7 +1139,7 @@ onMounted(() => { void loadCustomers() })
< template # footer > < el-button type = "primary" @click ="closeApprovalResult" > 我已保存 , 关闭 < / el -button > < / template >
< / el-dialog >
< el-dialog v-model = "decisionOpen" title="客户入驻审核" width="min(1040px, calc(100vw - 28px))" class="review-decision-dialog" :close-on-click-modal="false" >
< el-dialog v-model = "decisionOpen" title="客户入驻审核" width="min(1040px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog 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 >
@@ -1169,7 +1163,7 @@ onMounted(() => { void loadCustomers() })
< 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-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)">预览</el-button><el-button link :disabled="Boolean(reviewDocumentLoading)" @click="loadReviewDocument(item, true)" > 下载 < / el -button > < / span > < / div >
< div v-if = "reviewQualifications.length === 0" class="review-document-empty"><FileText :size="18" / > < span > 暂无资质文件 < / span > < / div >
< / div >
< / section >
@@ -1185,11 +1179,11 @@ onMounted(() => { void loadCustomers() })
< 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" >
< AppDialog class = "admin-density-dialog customer-density-dialog" v-model = "reviewPreviewOpen" :title="reviewPreviewName || '查看资质文件'" width="min(860px, calc(100vw - 28px))" destroy-on-close :show-confirm="false" cancel-text="关闭预览" @closed="clearReviewPreview" >
< div class = "qualification-preview" > < img v-if = "reviewPreviewUrl && reviewPreviewKind === 'image'" :src="reviewPreviewUrl" :alt="`${reviewPreviewName}预览`" /><iframe v-else-if="reviewPreviewUrl" :src="reviewPreviewUrl" :title="`${reviewPreviewName} PDF 预览`" / > < / div >
< / AppDialog >
< el-dialog v-model = "passwordDialogOpen" title="重置客户密码" width="min(520px, calc(100vw - 28px))" class="customer-password-dialog" :close-on-click-modal="false" :close-on-press-escape="!passwordSubmitting" :show-close="!passwordSubmitting" >
< el-dialog v-model = "passwordDialogOpen" title="重置客户密码" width="min(520px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog customer-password-dialog" :close-on-click-modal="false" :close-on-press-escape="!passwordSubmitting" :show-close="!passwordSubmitting" >
< div v-if = "passwordTenant" class="decision-summary" > < div > < strong > 正在重置企业管理员登录密码 < / strong > < span > { { passwordTenant . tenantName } } < / span > < / div > < span > 客户编号 : { { passwordTenant . tenantCode } } < / span > < / div >
< el-form ref = "passwordFormRef" :model = "passwordForm" :rules = "passwordRules" label -position = " top " >
< el-form-item label = "新密码" prop = "newPassword" required > < div class = "password-input-row" > < el-input v-model = "passwordForm.newPassword" type="password" show-password autocomplete="new-password" placeholder="请输入 8-20 位新密码" /><el-button @click="randomPassword" > 随机生成 < / el -button > < / div > < / el-form-item >
@@ -1200,14 +1194,14 @@ onMounted(() => { void loadCustomers() })
< template # footer > < div class = "drawer-footer" > < el-button :disabled = "passwordSubmitting" @click ="passwordDialogOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "passwordSubmitting" @click ="submitResetPassword" > 确认重置 < / el -button > < / div > < / template >
< / el-dialog >
< el-dialog v-model = "impersonationDialogOpen" title="跳转登录客户工作台" width="min(560px, calc(100vw - 28px))" :close-on-click-modal="false" :close-on-press-escape="!impersonationSubmitting" :show-close="!impersonationSubmitting" >
< el-dialog class = "admin-density-dialog customer-density-dialog" v-model = "impersonationDialogOpen" title="跳转登录客户工作台" width="min(560px, calc(100vw - 28px))" :close-on-click-modal="false" :close-on-press-escape="!impersonationSubmitting" :show-close="!impersonationSubmitting" >
< div v-if = "impersonationTenant" class="decision-summary" > < div > < strong > { { impersonationTenant . tenantName } } < / strong > < span > 将以企业管理员身份打开客户工作台 < / span > < / div > < span > 客户编号 : { { impersonationTenant . tenantCode } } < / span > < / div >
< el-alert type = "warning" :closable = "false" show -icon > 本操作会生成一次性 、 短时有效的客户会话 , 并记录代登录原因 ; 不会替换当前 Admin 会话 。 < / el-alert >
< el-form ref = "impersonationFormRef" :model = "impersonationForm" :rules = "impersonationRules" label -position = " top " > < el-form-item label = "代登录原因" prop = "reason" required > < el-input v-model = "impersonationForm.reason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请填写 5-500 字的业务原因" / > < / el-form-item > < / el-form >
< template # footer > < div class = "drawer-footer" > < el-button :disabled = "impersonationSubmitting" @click ="impersonationDialogOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "impersonationSubmitting" @click ="submitImpersonation" > 确认跳转 < / el -button > < / div > < / template >
< / el-dialog >
< el-dialog v-model = "archiveDialogOpen" title="删除客户" width="min(560px, calc(100vw - 28px))" :close-on-click-modal="false" :close-on-press-escape="!archiveSubmitting" :show-close="!archiveSubmitting" >
< el-dialog class = "admin-density-dialog customer-density-dialog" v-model = "archiveDialogOpen" title="删除客户" width="min(560px, calc(100vw - 28px))" :close-on-click-modal="false" :close-on-press-escape="!archiveSubmitting" :show-close="!archiveSubmitting" >
< el -alert type = "warning" :closable = "false" show -icon > 删除是逻辑归档 : 历史业务数据会保留 , 客户账号会被停用 。 是否允许归档由服务端最终判断 。 < / el-alert >
< div v-if = "archiveTenant" class="archive-confirm"><strong>{{ archiveTenant.tenantName }}</strong><span>请输入客户编号 {{ archiveTenant.tenantCode }} 以确认。</span><el-input v-model="archiveConfirmCode" :disabled="archiveSubmitting" :placeholder="archiveTenant.tenantCode" autocomplete="off" / > < / div >
< template # footer > < div class = "drawer-footer" > < el-button :disabled = "archiveSubmitting" @click ="archiveDialogOpen = false" > 取消 < / el -button > < el-button type = "danger" :loading = "archiveSubmitting" : disabled = "!archiveTenant || archiveConfirmCode.trim() !== archiveTenant.tenantCode" @click ="submitArchive" > 确认删除 < / el -button > < / div > < / template >
@@ -1223,25 +1217,15 @@ onMounted(() => { void loadCustomers() })
: global ( . customer - action - menu . customer - action - menu _ _danger ) { color : var ( -- el - color - danger ) ; }
. review - sync - error { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 12 px ; padding : 10 px 18 px ; border - bottom : 1 px solid # fecdca ; background : # fffafa ; color : # b42318 ; font - size : 12 px ; } . review - sync - error > div { display : flex ; min - width : 0 ; align - items : center ; gap : 8 px ; } . review - sync - error span { overflow - wrap : anywhere ; }
. customer - company - name { display : block ; overflow : hidden ; color : # 334155 ; font - weight : 500 ; text - overflow : ellipsis ; white - space : nowrap ; }
. customers - table - actions : deep ( . el - button -- primary ) { border : 0 ; background : linear - gradient ( 135 deg , # 667 eea 0 % , # 764 ba2 50 % , # f093fb 100 % ) ; box - shadow : 0 4 px 12 px rgba ( 102 , 126 , 234 , .3 ) ; }
. customers - table - actions : deep ( . el - button -- default ) { border - color : rgba ( 102 , 126 , 234 , .22 ) ; }
: deep ( . customer - list - table . el - table _ _header - wrapper th ) { background : # f7f9fc ; }
: deep ( . customer - list - table . el - table ) { -- el - table - border - color : # e5eaf1 ; -- el - table - row - hover - bg - color : rgba ( 102 , 126 , 234 , .06 ) ; border : 1 px solid # e5eaf1 ; border - radius : 0 ; overflow : hidden ; }
: deep ( . customer - list - table . el - table _ _inner - wrapper : : before ) { background : # e5eaf1 ; }
: deep ( . customer - list - table . el - table _ _body tr . el - table _ _row -- striped > td ) { background : # fbfcff ; }
: deep ( . customer - list - table . el - table _ _body tr : hover > td ) { background : rgba ( 102 , 126 , 234 , .06 ) ! important ; }
: deep ( . customer - list - table . el - table _ _body tr : hover > td ) { background : rgba ( 102 , 126 , 234 , .06 ) ; }
: deep ( . customer - list - table . el - table _ _body td ) , : deep ( . customer - list - table . el - table _ _header th ) { border - right : 1 px solid # edf1f5 ; }
: deep ( . customer - list - table . el - table _ _body td : last - child ) , : deep ( . customer - list - table . el - table _ _header th : last - child ) { border - right : 0 ; }
: deep ( . customer - list - table . customer - code - column . cell ) { white - space : nowrap ; }
: deep ( . customer - list - table . customer - action - button ) { min - height : 28 px ; margin : 0 ; padding : 5 px 9 px ; border - radius : 8 px ; font - size : 12 px ; font - weight : 650 ; box - shadow : 0 2 px 6 px rgba ( 15 , 23 , 42 , .06 ) ; }
: deep ( . customer - list - table . customer - action - detail ) { border - color : # bfdbfe ; background : # eff6ff ; color : # 2563 eb ; }
: deep ( . customer - list - table . customer - action - edit ) { border - color : # c7d2fe ; background : # eef2ff ; color : # 667 eea ; }
: deep ( . customer - list - table . customer - action - review ) { border - color : # fed7aa ; background : # fff7ed ; color : # ea580c ; }
: deep ( . customer - list - table . customer - action - reset - password ) { border - color : # ddd6fe ; background : # f5f3ff ; color : # 7 c3aed ; }
: deep ( . customer - list - table . customer - action - impersonate ) { border - color : # bae6fd ; background : # f0f9ff ; color : # 0369 a1 ; }
: deep ( . customer - list - table . customer - action - status . is - enable ) { border - color : # bbf7d0 ; background : # f0fdf4 ; color : # 16 a34a ; }
: deep ( . customer - list - table . customer - action - status . is - disable ) , : deep ( . customer - list - table . customer - action - delete ) { border - color : # fecaca ; background : # fef2f2 ; color : # dc2626 ; }
: deep ( . customer - list - table . customer - action - button : hover ) { filter : brightness ( .97 ) ; transform : none ; box - shadow : 0 2 px 6 px rgba ( 15 , 23 , 42 , .06 ) ; }
: deep ( . customer - list - table . el - tag ) { border : 0 ; border - radius : 12 px ; font - weight : 650 ; }
: deep ( . customer - list - table . el - tag -- success ) { background : linear - gradient ( 135 deg , rgba ( 34 , 197 , 94 , .15 ) , rgba ( 22 , 163 , 74 , .1 ) ) ; color : # 16 a34a ; }
: deep ( . customer - list - table . el - tag -- warning ) { background : linear - gradient ( 135 deg , rgba ( 245 , 158 , 11 , .15 ) , rgba ( 234 , 88 , 12 , .1 ) ) ; color : # ea580c ; }