@@ -1,4 +1,5 @@
< script setup lang = "ts" >
import '@/styles/adminWorkbench.css'
import InlineHelp from '@/shared/components/InlineHelp.vue'
import '@/styles/adminDensity.css'
import '@/modules/customers/customerDensity.css'
@@ -643,7 +644,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 ( '提交后客户资料与资质将进入企业审核,审核完成前不能继续替换文件。确认提交吗?' , customerDialogTitle ( '提交企业审核' , editorCustomer . value ) , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : '确认提交' , cancelButtonText : '继续完善' } )
await ElMessageBox . confirm ( '提交后客户资料与资质将进入企业审核,审核完成前不能继续替换文件。确认提交吗?' , customerDialogTitle ( '提交企业审核' , editorCustomer . value ) , { customClass : 'admin-density-confirm admin-workbench ' , type : 'warning' , confirmButtonText : '确认提交' , cancelButtonText : '继续完善' } )
const saved = await submitPlatformTenantReview ( editingId . value )
editorStatus . value = saved . status ; ElMessage . success ( '审核单已提交' )
editorOpen . value = false ; await loadCustomers ( )
@@ -732,7 +733,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 } ”吗? ` , customerDialogTitle ( ` ${ verb } 客户 ` , row ) , { customClass : 'admin-density-confirm' , type : 'warning' , confirmButtonText : ` 确认 ${ verb } ` , cancelButtonText : '取消' } )
await ElMessageBox . confirm ( ` 确认 ${ verb } 客户“ ${ row . tenantName } ”吗? ` , customerDialogTitle ( ` ${ verb } 客户 ` , row ) , { customClass : 'admin-density-confirm admin-workbench ' , type : 'warning' , confirmButtonText : ` 确认 ${ verb } ` , cancelButtonText : '取消' } )
rowActionId . value = String ( row . id ) ; await changePlatformTenantStatus ( row . id , next )
ElMessage . success ( ` 客户已 ${ verb } ` ) ; await loadCustomers ( )
} catch ( error ) {
@@ -1059,12 +1060,12 @@ onMounted(() => { void loadCustomers() })
< / script >
< template >
< AdminPage as = "div" class = "customers-page admin-density customer-density" >
< AdminPage as = "div" class = "customers-page admin-density customer-density admin-workbench " >
< OperationErrorToast : details = "passwordError || impersonationError || archiveError" / >
< ListPageLayout aria -label = " 客户管理 " >
< section :aria-busy = "loading" > < DataTable appearance = "integrated" :stripe = "false" column -settings -mode = " show " :results-visible = "!loadError" 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" border >
< section class = "workbench-table is-page-surface" :aria-busy = "loading" > < DataTable appearance = "integrated" :stripe = "false" column -settings -mode = " show " :results-visible = "!loadError" 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" border >
< template # toolbar -start > < h1 > 客户列表 < / h1 > < / template >
< template # filters > < QueryToolbar v-model = "filters" ariaLabel="客户筛选" :fields="queryFields" preference-key="customers-query" @search="applyFilters" / > < / template >
< template # notices > < template v-if = "loadError" >
@@ -1083,9 +1084,9 @@ onMounted(() => { void loadCustomers() })
< / DataTable > < / section >
< / ListPageLayout >
< AppDialog v-model = "editorOpen" :title="editorTitle" :width="editorDialogWidth" class="admin-density-dialog customer-density-dialog customer-editor-dialog" :loading="editorLoading" :confirm-loading="saving" fullscreenable destroy -on -close >
< AppDialog v-model = "editorOpen" :title="editorTitle" :width="editorDialogWidth" class="admin-density-dialog customer-density-dialog customer-editor-dialog admin-workbench is-dialog " :loading="editorLoading" :confirm-loading="saving" fullscreenable destroy -on -close >
< el-form ref = "formRef" v-loading = "editorLoading" :model="form" :rules="rules" label-position="left" label-width="144px" class="density-form density-dialog-sections" :disabled="editorLoading || saving || submitting || uploadingQualifications" >
< section class = "density-dialog-section" data -tone = " work " > < h3 class = "density-dialog-section__title" > 基础资料 < / h3 >
< section class = "density-dialog-section workbench-evidence " data -tone = " work " > < h3 class = "density-dialog-section__title" > 基础资料 < / h3 >
< table class = "density-table density-table--editable customer-editor-table" aria -label = " 基础资料 " > < tbody >
< tr > < td > < el-form-item label = "公司名称" prop = "tenantName" required > < el-input v-model = "form.tenantName" placeholder="请输入工商登记公司名称" maxlength="128" /></el-form-item></td><td><el-form-item label="客户昵称" prop="customerNickname"><el-input v-model="form.customerNickname" placeholder="不填默认生成中文拼音大写首字母,如 SZWL" maxlength="32" / > < / el-form-item > < / td > < / tr >
< tr > < td > < el-form-item label = "营业执照企业名称" prop = "legalName" required > < el-input v-model = "form.legalName" placeholder="请输入营业执照上的企业名称" maxlength="128" /></el-form-item></td><td><el-form-item label="法定代表人" prop="legalRepresentative"><el-input v-model="form.legalRepresentative" placeholder="请输入法定代表人姓名(选填)" maxlength="64" / > < / el-form-item > < / td > < / tr >
@@ -1095,7 +1096,7 @@ onMounted(() => { void loadCustomers() })
< / tbody > < / table >
< / section >
< section class = "density-dialog-section" data -tone = " work " > < h3 class = "density-dialog-section__title" > 联系与商务 < InlineHelp title = "企业联系信息说明" > < p > 仅用于企业业务联系 , 不会修改企业管理员登录手机号 ; 登录手机号请在成员 / 账号管理中单独维护 , 修改会影响登录 。 < / p > < / InlineHelp > < / h3 >
< section class = "density-dialog-section workbench-evidence " data -tone = " work " > < h3 class = "density-dialog-section__title" > 联系与商务 < InlineHelp title = "企业联系信息说明" > < p > 仅用于企业业务联系 , 不会修改企业管理员登录手机号 ; 登录手机号请在成员 / 账号管理中单独维护 , 修改会影响登录 。 < / p > < / InlineHelp > < / h3 >
< table class = "density-table density-table--editable customer-editor-table" aria -label = " 联系与商务 " > < tbody >
< tr > < td > < el-form-item label = "联系人" prop = "contactName" required > < el-input v-model = "form.contactName" placeholder="请输入主要联系人姓名" maxlength="64" /></el-form-item></td><td><el-form-item label="公司联系电话" prop="contactPhone" required><el-input v-model="form.contactPhone" placeholder="请输入公司联系电话" maxlength="32" / > < / el-form-item > < / td > < / tr >
< tr > < td > < el-form-item label = "联系邮箱" prop = "contactEmail" > < el-input v-model = "form.contactEmail" placeholder="如:contact@example.com" maxlength="128" /></el-form-item></td><td><el-form-item label="联系微信" prop="contactWechat"><el-input v-model="form.contactWechat" placeholder="请输入联系人微信号" maxlength="64" / > < / el-form-item > < / td > < / tr >
@@ -1104,7 +1105,7 @@ onMounted(() => { void loadCustomers() })
< / tbody > < / table >
< / section >
< section class = "density-dialog-section qualification-section" data -tone = " work " > < h3 class = "density-dialog-section__title" > 企业认证资料 < InlineHelp title = "资质提交规则" > < p > 营业执照和18位统一社会信用代码为必填业务项 ; 法人身份证 、 货代资质证书和银行开户许可证均为选填 。 < / p > < p > 支持 PDF 、 JPG 、 PNG , 单个文件不超过 5 MB ; < span > * < / span > 为提交审核必填项 。 < / p > < / InlineHelp > < / h3 > < div v-if = "editorStatus" class="qualification-heading"><el-tag :type="statusMeta(editorStatus).type" effect="light" > {{ statusMeta ( editorStatus ) .label }} < / el -tag > < / div >
< section class = "density-dialog-section qualification-section workbench-evidence " data -tone = " work " > < h3 class = "density-dialog-section__title" > 企业认证资料 < InlineHelp title = "资质提交规则" > < p > 营业执照和18位统一社会信用代码为必填业务项 ; 法人身份证 、 货代资质证书和银行开户许可证均为选填 。 < / p > < p > 支持 PDF 、 JPG 、 PNG , 单个文件不超过 5 MB ; < span > * < / span > 为提交审核必填项 。 < / p > < / InlineHelp > < / h3 > < div v-if = "editorStatus" class="qualification-heading"><el-tag :type="statusMeta(editorStatus).type" effect="light" > {{ statusMeta ( editorStatus ) .label }} < / el -tag > < / div >
< table class = "density-table density-table--editable" aria -label = " 企业信用代码 " > < tbody > < tr > < td > < el-form-item label = "统一社会信用代码" prop = "unifiedSocialCreditCode" required class = "qualification-credit-code" > < span v-if = "creditCodeEstablished">{{ form.unifiedSocialCreditCode }}</span><el-input v-else v-model="form.unifiedSocialCreditCode" placeholder="请输入18位大写字母或数字" maxlength="18" @input="form.unifiedSocialCreditCode = String($event).toUpperCase()" / > < small > { { creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照共同保存;法人身份证为选填。' } } < / small > < / el-form-item > < / td > < / tr > < / tbody > < / table >
< div v-if = "qualificationLoadError" class="qualification-load-error" role="alert">{{ qualificationLoadError }}<el-button v-if="editingId" link type="primary" @click="loadEditorQualifications(editingId)" > 重试 < / el -button > < / div >
< div class = "qualification-table" role = "table" aria -label = " 客户资质文件 " >
@@ -1128,7 +1129,7 @@ onMounted(() => { void loadCustomers() })
< template # footer > < div class = "customer-editor-footer" > < span > 保存资料时会一并上传已选资质 ; 进入审核仍需点击 “ 提交审核 ” 并再次确认 < / span > < div class = "density-actions density-actions--footer" > < 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="customerDialogTitle('查看资质文件', editorCustomer)" width="min(860px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog qualification-preview-dialog customer-qualification-preview" fullscreenable destroy-on-close :show-confirm="false" cancel-text="关闭预览" >
< AppDialog v-model = "qualificationPreviewOpen" :title="customerDialogTitle('查看资质文件', editorCustomer)" width="min(860px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog qualification-preview-dialog customer-qualification-preview admin-workbench is-dialog " fullscreenable destroy-on-close :show-confirm="false" cancel-text="关闭预览" >
< p class = "density-dialog-note customer-preview-filename" > { { qualificationPreviewName } } < / p >
< div class = "density-dialog-section qualification-preview" data -tone = " source " >
< img v-if = "qualificationPreviewUrl && qualificationPreviewKind === 'image'" :src="qualificationPreviewUrl" :alt="`${qualificationPreviewName}预览`" / >
@@ -1136,10 +1137,10 @@ onMounted(() => { void loadCustomers() })
< / div >
< / AppDialog >
< AppDialog v-model = "approvalResultOpen" :title="customerDialogTitle(credentialSource === 'creation' ? '客户已创建,管理员账号已生成' : '审核通过,管理员账号已创建', credentialSource === 'creation' ? editorCustomer : selectedReview)" 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" >
< AppDialog v-model = "approvalResultOpen" :title="customerDialogTitle(credentialSource === 'creation' ? '客户已创建,管理员账号已生成' : '审核通过,管理员账号已创建', credentialSource === 'creation' ? editorCustomer : selectedReview)" width="min(560px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog approval-result-dialog admin-workbench is-dialog " :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" >
< div v-if = "approvalResult" class="density-dialog-sections" >
< div class = "approval-result-warning" role = "alert" > < ShieldCheck :size = "20" / > < div > < strong > 请立即保存以下一次性凭证 < / strong > < span > 临时密码只在本次 { { credentialSource === 'creation' ? '创建' : '审核' } } 响应中返回 , 关闭后无法再次查看 。 < / span > < / div > < / div >
< section class = "density-dialog-section" data -tone = " summary " > < table class = "density-table" aria -label = " 一次性管理员凭据 " > < tbody >
< section class = "density-dialog-section workbench-evidence " data -tone = " summary " > < table class = "density-table" aria -label = " 一次性管理员凭据 " > < tbody >
< tr > < th scope = "row" > 登录账号 < / th > < td > < div class = "customer-credential-value" > < code > { { approvalResult . administratorUsername || '未返回' } } < / code > < CopyButton v-if = "approvalResult.administratorUsername" :value="approvalResult.administratorUsername" label="登录账号" size="small" / > < / div > < / td > < / tr >
< tr > < th scope = "row" > 临时密码 < / th > < td > < div class = "customer-credential-value" > < code > { { approvalResult . temporaryPassword || '未返回' } } < / code > < CopyButton v-if = "approvalResult.temporaryPassword" :value="approvalResult.temporaryPassword" label="临时密码" size="small" / > < / div > < / td > < / tr >
< / tbody > < / table > < / section >
@@ -1147,9 +1148,9 @@ onMounted(() => { void loadCustomers() })
< template # footer > < el-button type = "primary" @click ="closeApprovalResult" > 我已保存 , 关闭 < / el -button > < / template >
< / AppDialog >
< AppDialog v-model = "decisionOpen" :title="customerDialogTitle('客户入驻审核', selectedReview)" fullscreenable :confirm-loading="reviewSubmitting" :lock-while-submitting="false" width="min(1040px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog review-decision-dialog" :close-on-click-modal="false" >
< AppDialog v-model = "decisionOpen" :title="customerDialogTitle('客户入驻审核', selectedReview)" fullscreenable :confirm-loading="reviewSubmitting" :lock-while-submitting="false" width="min(1040px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog review-decision-dialog admin-workbench is-dialog " :close-on-click-modal="false" >
< div v-if = "selectedReview" class="density-dialog-sections" >
< section class = "density-dialog-section" data -tone = " source " >
< section class = "density-dialog-section workbench-evidence " data -tone = " source " >
< h3 class = "density-dialog-section__title" > 企业基本信息 < el-tag type = "warning" effect = "plain" > 待审核 < / el-tag > < / h3 >
< table class = "density-table customer-review-table" aria -label = " 待审核企业资料 " > < tbody >
< tr > < th scope = "row" > 客户名称 < / th > < td > { { selectedReview . tenantName || '未填写' } } < / td > < th scope = "row" > 客户编号 < / th > < td > { { selectedReview . tenantCode || '未生成' } } < / td > < / tr >
@@ -1157,7 +1158,7 @@ onMounted(() => { void loadCustomers() })
< tr > < th scope = "row" > 联系邮箱 < / th > < td > { { selectedReview . contactEmail || '未填写' } } < / td > < th scope = "row" > 提交时间 < / th > < td > { { formatDate ( selectedReview . submittedAt || selectedReview . createdAt ) } } < / td > < / tr >
< / tbody > < / table >
< / section >
< section class = "density-dialog-section" data -tone = " source " >
< section class = "density-dialog-section workbench-evidence " data -tone = " source " >
< h3 class = "density-dialog-section__title" > 资质文件 < / h3 >
< p class = "density-dialog-note" > 营业执照为必填 ; 法人身份证及其他资质为选填 。 共 { { reviewQualifications . length } } 份 < / p >
< div v-if = "reviewQualificationsLoading" class="review-document-state" > 正在加载资质文件... < / div >
@@ -1179,12 +1180,12 @@ onMounted(() => { void loadCustomers() })
< template # footer > < div class = "density-actions density-actions--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 >
< / AppDialog >
< AppDialog class = "admin-density-dialog customer-density-dialog customer-qualification-preview" v-model = "reviewPreviewOpen" :title="customerDialogTitle('查看审核资质', selectedReview)" fullscreenable width="min(860px, calc(100vw - 28px))" destroy-on-close :show-confirm="false" cancel-text="关闭预览" @closed="clearReviewPreview" >
< AppDialog class = "admin-density-dialog customer-density-dialog customer-qualification-preview admin-workbench is-dialog " v-model = "reviewPreviewOpen" :title="customerDialogTitle('查看审核资质', selectedReview)" fullscreenable width="min(860px, calc(100vw - 28px))" destroy-on-close :show-confirm="false" cancel-text="关闭预览" @closed="clearReviewPreview" >
< p class = "density-dialog-note customer-preview-filename" > { { reviewPreviewName } } < / p >
< div class = "density-dialog-section qualification-preview" data -tone = " source " > < img v-if = "reviewPreviewUrl && reviewPreviewKind === 'image'" :src="reviewPreviewUrl" :alt="`${reviewPreviewName}预览`" /><iframe v-else-if="reviewPreviewUrl" :src="reviewPreviewUrl" :title="`${reviewPreviewName} PDF 预览`" / > < / div >
< / AppDialog >
< AppDialog v-model = "passwordDialogOpen" :title="customerDialogTitle('重置客户密码', passwordTenant)" :confirm-loading="passwordSubmitting" 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" >
< AppDialog v-model = "passwordDialogOpen" :title="customerDialogTitle('重置客户密码', passwordTenant)" :confirm-loading="passwordSubmitting" width="min(520px, calc(100vw - 28px))" class="admin-density-dialog customer-density-dialog customer-password-dialog admin-workbench is-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 " class = "density-form" >
< 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 >
@@ -1195,14 +1196,14 @@ onMounted(() => { void loadCustomers() })
< template # footer > < div class = "density-actions density-actions--footer" > < el-button :disabled = "passwordSubmitting" @click ="passwordDialogOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "passwordSubmitting" @click ="submitResetPassword" > 确认重置 < / el -button > < / div > < / template >
< / AppDialog >
< AppDialog class = "admin-density-dialog customer-density-dialog" v-model = "impersonationDialogOpen" :title="customerDialogTitle('跳转登录客户工作台', impersonationTenant)" :confirm-loading="impersonationSubmitting" width="min(560px, calc(100vw - 28px))" :close-on-click-modal="false" :close-on-press-escape="!impersonationSubmitting" :show-close="!impersonationSubmitting" >
< AppDialog class = "admin-density-dialog customer-density-dialog admin-workbench is-dialog " v-model = "impersonationDialogOpen" :title="customerDialogTitle('跳转登录客户工作台', impersonationTenant)" :confirm-loading="impersonationSubmitting" 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 " class = "density-form" > < 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 = "density-actions density-actions--footer" > < el-button :disabled = "impersonationSubmitting" @click ="impersonationDialogOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "impersonationSubmitting" @click ="submitImpersonation" > 确认跳转 < / el -button > < / div > < / template >
< / AppDialog >
< AppDialog class = "admin-density-dialog customer-density-dialog" v-model = "archiveDialogOpen" :title="customerDialogTitle('删除客户', archiveTenant)" :confirm-loading="archiveSubmitting" width="min(560px, calc(100vw - 28px))" :close-on-click-modal="false" :close-on-press-escape="!archiveSubmitting" :show-close="!archiveSubmitting" >
< AppDialog class = "admin-density-dialog customer-density-dialog admin-workbench is-dialog " v-model = "archiveDialogOpen" :title="customerDialogTitle('删除客户', archiveTenant)" :confirm-loading="archiveSubmitting" 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 = "density-actions density-actions--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 >
@@ -1211,25 +1212,24 @@ onMounted(() => { void loadCustomers() })
< / template >
< style scoped >
. customers - table - heading { display : flex ; min - width : 0 ; align - items : baseline ; gap : var ( -- admin - space - 4 , 10 px ) ; } . customers - table - heading h2 { margin : 0 ; color : var ( -- cc - text ) ; font - size : var ( -- admin - font - title , 16 px ) ; } . customers - table - heading span { color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; white - space : nowrap ; }
. customers - table - heading { display : flex ; min - width : 0 ; align - items : baseline ; gap : var ( -- admin - space - 4 , 10 px ) ; } . customers - table - heading h2 { margin : 0 ; color : var ( -- workbench - text ) ; font - size : var ( -- admin - font - title , 16 px ) ; } . customers - table - heading span { color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; white - space : nowrap ; }
. customers - table - actions { display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 3 , 8 px ) ; } . customers - table - actions : deep ( . el - button + . el - button ) { margin - left : 0 ; }
. table - actions { display : flex ; flex - wrap : wrap ; align - items : center ; } . table - actions : deep ( . el - button ) { margin - left : 0 ; }
: global ( . customer - action - menu . el - dropdown - menu _ _item ) { min - height : 40 px ; gap : var ( -- admin - space - 3 , 8 px ) ; }
: 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 : var ( -- admin - space - 5 , 12 px ) ; padding : var ( -- admin - space - 4 , 10 px ) 18 px ; border - bottom : 1 px solid # fecdca ; background : # fffafa ; color : # b42318 ; font - size : var ( -- admin - font - caption , 12 px ) ; } . review - sync - error > div { display : flex ; min - width : 0 ; align - items : center ; gap : var ( -- admin - space - 3 , 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 -- default ) { border - color : rgba ( 102 , 126 , 234 , .22 ) ; }
. customer - company - name { display : block ; overflow : hidden ; color : var ( -- workbench - text ) ; font - weight : 500 ; text - overflow : ellipsis ; white - space : nowrap ; }
: deep ( . customer - list - table . customer - code - column . cell ) { white - space : nowrap ; }
. capital - input { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) 130 px ; width : 100 % ; gap : var ( -- admin - space - 3 , 8 px ) ; } . capital - input : deep ( . el - input - number ) { width : 100 % ; } . capital - input : deep ( . el - select ) { width : 100 % ; }
. qualification - heading { display : flex ; justify - content : flex - end ; margin - bottom : var ( -- admin - space - 5 , 12 px ) ; } . qualification - hint { margin : var ( -- admin - space - 5 , 12 px ) 0 0 ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; line - height : 1.6 ; } . qualification - hint span { color : var ( -- el - color - danger ) ; } . qualification - label > span { color : var ( -- el - color - danger ) ; }
. qualification - credit - code small { display : block ; margin - top : var ( -- admin - space - 2 , 6 px ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 11 px ) ; line - height : 1.5 ; } . field - hint { display : block ; margin - top : var ( -- admin - space - 2 , 6 px ) ; color : var ( -- cc - muted ) ; line - height : 1.5 ; }
. qualification - table { container - type : inline - size ; overflow : hidden ; margin - top : 14 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; } . qualification - row { display : grid ; grid - template - columns : 170 px minmax ( 0 , 1 fr ) 220 px ; min - width : 0 ; border - top : 1 px solid var ( -- cc - border ) ; } . qualification - row : first - child { border - top : 0 ; } . qualification - row > * { display : flex ; min - width : 0 ; align - items : center ; } . qualification - header { min - height : 44 px ; background : # f7f8fc ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; font - weight : 600 ; }
. qualification - heading { display : flex ; justify - content : flex - end ; margin - bottom : var ( -- admin - space - 5 , 12 px ) ; } . qualification - hint { margin : var ( -- admin - space - 5 , 12 px ) 0 0 ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; line - height : 1.6 ; } . qualification - hint span { color : var ( -- el - color - danger ) ; } . qualification - label > span { color : var ( -- el - color - danger ) ; }
. qualification - credit - code small { display : block ; margin - top : var ( -- admin - space - 2 , 6 px ) ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 11 px ) ; line - height : 1.5 ; } . field - hint { display : block ; margin - top : var ( -- admin - space - 2 , 6 px ) ; color : var ( -- workbench - muted ) ; line - height : 1.5 ; }
. qualification - table { container - type : inline - size ; overflow : hidden ; margin - top : 14 px ; border : 1 px solid var ( -- workbench - border ) ; border - radius : 8 px ; } . qualification - row { display : grid ; grid - template - columns : 170 px minmax ( 0 , 1 fr ) 220 px ; min - width : 0 ; border - top : 1 px solid var ( -- workbench - border ) ; } . qualification - row : first - child { border - top : 0 ; } . qualification - row > * { display : flex ; min - width : 0 ; align - items : center ; } . qualification - header { min - height : 44 px ; background : var ( -- workbench - surface - soft ) ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; font - weight : 600 ; }
. qualification - label { gap : 5 px ; font - weight : 600 ; } . qualification - label svg { margin - left : auto ; color : var ( -- el - color - success ) ; }
. qualification - optional { padding : 2 px 5 px ; border - radius : var ( -- admin - radius , 4 px ) ; background : var ( -- el - fill - color - light ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 10 px ) ; font - weight : 500 ; line - height : 1.4 ; }
. qualification - file { gap : var ( -- admin - space - 5 , 12 px ) ; } . qualification - file - control { display : flex ; min - width : 0 ; flex : 1 ; flex - wrap : wrap ; align - items : center ; gap : var ( -- admin - space - 3 , 8 px ) ; } . file - picker { position : relative ; display : inline - flex ; flex : 0 0 auto ; align - items : center ; gap : 7 px ; overflow : hidden ; padding : 0 var ( -- admin - space - 5 , 12 px ) ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px ; background : var ( -- admin - surface , # fff ) ; color : var ( -- cc - text ) ; cursor : pointer ; } . file - picker : focus - within { outline : 2 px solid color - mix ( in srgb , var ( -- cc - primary ) 35 % , transparent ) ; outline - offset : 2 px ; } . file - picker . is - disabled { cursor : not - allowed ; opacity : .6 ; } . file - picker input { position : absolute ; inset : 0 ; width : 100 % ; height : 100 % ; opacity : 0 ; cursor : inherit ; } . qualification - file - name { min - width : 0 ; overflow : hidden ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; text - overflow : ellipsis ; } . qualification - validity : deep ( . el - date - editor ) { width : 100 % ; min - width : 0 ; } . qualification - error { width : 100 % ; color : var ( -- el - color - danger ) ; line - height : 1.4 ; }
. qualification - preview - trigger { position : relative ; display : grid ; width : 68 px ; height : 68 px ; flex : 0 0 68 px ; overflow : hidden ; place - items : center ; padding : 0 ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px ; background : var ( -- admin - label - bg , # f8fafc ) ; color : var ( -- cc - muted ) ; cursor : pointer ; }
. qualification - preview - trigger : hover { border - color : var ( -- cc - primary ) ; color : var ( -- cc - primary ) ; }
. qualification - preview - trigger : focus - visible { outline : 2 px solid color - mix ( in srgb , var ( -- cc - primary ) 42 % , transparent ) ; outline - offset : 2 px ; }
. qualification - optional { padding : 2 px 5 px ; border - radius : var ( -- admin - radius , 4 px ) ; background : var ( -- el - fill - color - light ) ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 10 px ) ; font - weight : 500 ; line - height : 1.4 ; }
. qualification - file { gap : var ( -- admin - space - 5 , 12 px ) ; } . qualification - file - control { display : flex ; min - width : 0 ; flex : 1 ; flex - wrap : wrap ; align - items : center ; gap : var ( -- admin - space - 3 , 8 px ) ; } . file - picker { position : relative ; display : inline - flex ; flex : 0 0 auto ; align - items : center ; gap : 7 px ; overflow : hidden ; padding : 0 var ( -- admin - space - 5 , 12 px ) ; border : 1 px solid var ( -- workbench - border ) ; border - radius : 6 px ; background : var ( -- workbench - surface ) ; color : var ( -- workbench - text ) ; cursor : pointer ; } . file - picker : focus - within { outline : 2 px solid color - mix ( in srgb , var ( -- workbench - primary ) 35 % , transparent ) ; outline - offset : 2 px ; } . file - picker . is - disabled { cursor : not - allowed ; opacity : .6 ; } . file - picker input { position : absolute ; inset : 0 ; width : 100 % ; height : 100 % ; opacity : 0 ; cursor : inherit ; } . qualification - file - name { min - width : 0 ; overflow : hidden ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; text - overflow : ellipsis ; } . qualification - validity : deep ( . el - date - editor ) { width : 100 % ; min - width : 0 ; } . qualification - error { width : 100 % ; color : var ( -- el - color - danger ) ; line - height : 1.4 ; }
. qualification - preview - trigger { position : relative ; display : grid ; width : 68 px ; height : 68 px ; flex : 0 0 68 px ; overflow : hidden ; place - items : center ; padding : 0 ; border : 1 px solid var ( -- workbench - border ) ; border - radius : 6 px ; background : var ( -- workbench - surface - soft ) ; color : var ( -- workbench - muted ) ; cursor : pointer ; }
. qualification - preview - trigger : hover { border - color : var ( -- workbench - primary ) ; color : var ( -- workbench - primary ) ; }
. qualification - preview - trigger : focus - visible { outline : 2 px solid color - mix ( in srgb , var ( -- workbench - primary ) 42 % , transparent ) ; outline - offset : 2 px ; }
. qualification - preview - trigger : disabled { cursor : wait ; opacity : .72 ; }
@ container ( max - width : 660 px ) {
. qualification - row { grid - template - columns : 112 px minmax ( 0 , 1 fr ) ; }
@@ -1237,19 +1237,19 @@ onMounted(() => { void loadCustomers() })
. qualification - label { grid - row : span 2 ; align - items : center ; }
. qualification - file , . qualification - validity { min - height : 44 px ; }
}
. qualification - preview - trigger img { width : 100 % ; height : 100 % ; object - fit : contain ; background : var ( -- admin - surface , # fff ) ; }
. qualification - preview - trigger img { width : 100 % ; height : 100 % ; object - fit : contain ; background : var ( -- workbench - surface ) ; }
. qualification - preview - placeholder { display : grid ; place - items : center ; gap : 2 px ; }
. qualification - preview - placeholder small { font - size : var ( -- admin - font - caption , 10 px ) ; font - weight : 700 ; }
. qualification - preview - action { position : absolute ; inset : auto 0 0 ; display : flex ; min - height : 22 px ; align - items : center ; justify - content : center ; gap : 3 px ; background : rgba ( 23 , 32 , 51 , .78 ) ; color : var ( -- admin - surface , # fff ) ; }
. qualification - preview - action { position : absolute ; inset : auto 0 0 ; display : flex ; min - height : 22 px ; align - items : center ; justify - content : center ; gap : 3 px ; background : rgba ( 23 , 32 , 51 , .78 ) ; color : var ( -- workbench - surface ) ; }
. qualification - load - error { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 4 , 10 px ) ; margin - top : var ( -- admin - space - 5 , 12 px ) ; padding : var ( -- admin - space - 4 , 10 px ) var ( -- admin - space - 5 , 12 px ) ; border : 1 px solid # fecdca ; border - radius : 6 px ; background : # fffafa ; color : # b42318 ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. customer - editor - footer { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 6 , 16 px ) ; } . customer - editor - footer > span { color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; text - align : left ; }
. decision - summary { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 , 12 px ) ; background : var ( -- admin - label - bg , # f8fafc ) ; } . decision - summary > * { min - width : 0 ; overflow - wrap : anywhere ; } . decision - summary strong { color : var ( -- cc - text ) ; font - size : var ( -- admin - font - body , 13 px ) ; } . decision - summary span { color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 11 px ) ; }
. review - document - state { padding : 14 px ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. review - document - error { padding : 14 px ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. review - document - empty { padding : 14 px ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. customer - editor - footer { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 6 , 16 px ) ; } . customer - editor - footer > span { color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; text - align : left ; }
. decision - summary { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 , 12 px ) ; background : var ( -- workbench - surface - soft ) ; } . decision - summary > * { min - width : 0 ; overflow - wrap : anywhere ; } . decision - summary strong { color : var ( -- workbench - text ) ; font - size : var ( -- admin - font - body , 13 px ) ; } . decision - summary span { color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 11 px ) ; }
. review - document - state { padding : 14 px ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. review - document - error { padding : 14 px ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. review - document - empty { padding : 14 px ; color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. review - document - error { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 , 12 px ) ; margin - bottom : var ( -- admin - space - 3 , 8 px ) ; border : 1 px solid # fecdca ; border - radius : 6 px ; background : # fffafa ; color : # b42318 ; }
. review - document - empty { display : flex ; align - items : center ; justify - content : center ; gap : 7 px ; text - align : center ; } . review - document - empty svg { color : # a5afc1 ; }
. decision - summary > div > span { display : block ; margin - top : var ( -- admin - space - 1 , 4 px ) ; } . decision - type { margin : 0 0 14 px ; } . password - input - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto ; width : 100 % ; gap : var ( -- admin - space - 3 , 8 px ) ; } . archive - confirm { display : grid ; gap : var ( -- admin - space - 3 , 8 px ) ; margin : var ( -- admin - space - 6 , 16 px ) 0 ; } . archive - confirm strong { color : var ( -- cc - text ) ; } . archive - confirm span { color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. review - document - empty { display : flex ; align - items : center ; justify - content : center ; gap : 7 px ; text - align : center ; } . review - document - empty svg { color : var ( -- workbench - muted ) ; }
. decision - summary > div > span { display : block ; margin - top : var ( -- admin - space - 1 , 4 px ) ; } . decision - type { margin : 0 0 14 px ; } . password - input - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto ; width : 100 % ; gap : var ( -- admin - space - 3 , 8 px ) ; } . archive - confirm { display : grid ; gap : var ( -- admin - space - 3 , 8 px ) ; margin : var ( -- admin - space - 6 , 16 px ) 0 ; } . archive - confirm strong { color : var ( -- workbench - text ) ; } . archive - confirm span { color : var ( -- workbench - muted ) ; font - size : var ( -- admin - font - caption , 12 px ) ; }
. approval - result - warning { display : flex ; align - items : flex - start ; gap : var ( -- admin - space - 4 , 10 px ) ; padding : var ( -- admin - space - 5 , 12 px ) 14 px ; border : 1 px solid # fecdca ; border - radius : 8 px ; background : # fffafa ; color : # b42318 ; } . approval - result - warning strong { display : block ; } . approval - result - warning span { display : block ; } . approval - result - warning strong { font - size : var ( -- admin - font - section , 14 px ) ; } . approval - result - warning span { margin - top : var ( -- admin - space - 1 , 4 px ) ; font - size : var ( -- admin - font - caption , 12 px ) ; line - height : 1.5 ; }
@ media ( max - width : 720 px ) { . customer - editor - footer , . review - sync - error { align - items : stretch ; flex - direction : column ; } . customers - table - actions { display : grid ; grid - template - columns : 1 fr 1 fr ; width : 100 % ; } . customers - table - actions : deep ( . el - button ) { min - height : 40 px ; margin : 0 ; } . qualification - table { overflow - x : auto ; } . qualification - row { min - width : 640 px ; } . customer - editor - footer > div { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 110 px , 1 fr ) ) ; width : 100 % ; } . customer - editor - footer : deep ( . el - button ) { min - height : 40 px ; margin : 0 ; } . decision - summary { align - items : flex - start ; flex - direction : column ; } . review - document - header , . password - input - row { grid - template - columns : 1 fr ; } . table - actions : deep ( . el - button ) { min - height : 40 px ; } }
@ media ( max - width : 480 px ) { : global ( . review - decision - dialog . el - dialog _ _header ) , : global ( . review - decision - dialog . el - dialog _ _body ) { padding - left : 16 px ; padding - right : 16 px ; } }