@@ -80,6 +80,14 @@ const reviewLoadError = ref<ApiErrorDetails | null>(null)
const selectedReview = ref < EnterpriseReview | null > ( null )
const decisionOpen = ref ( false )
const reviewSubmitting = ref ( false )
const reviewQualifications = ref < PlatformTenantQualification [ ] > ( [ ] )
const reviewQualificationsLoading = ref ( false )
const reviewQualificationsError = ref < ApiErrorDetails | null > ( null )
const reviewPreviewOpen = ref ( false )
const reviewPreviewName = ref ( '' )
const reviewPreviewUrl = ref ( '' )
const reviewPreviewKind = ref < 'image' | 'pdf' > ( 'image' )
const reviewDocumentLoading = ref ( '' )
const approvalResultOpen = ref ( false )
const approvalResult = ref < EnterpriseApprovalResponse | null > ( null )
const credentialSource = ref < 'creation' | 'approval' > ( 'approval' )
@@ -252,6 +260,12 @@ const impersonationRules: FormRules = {
}
function displayText ( value ? : string | null ) { return value ? . trim ( ) || '—' }
function formatDate ( value ? : string | null ) { return value ? value . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) : '—' }
function qualificationLabel ( type ? : string | null ) {
return ( { BUSINESS _LICENSE : '营业执照' , LEGAL _PERSON _ID : '法人身份证' , FREIGHT _FORWARDER _CERTIFICATE : '货代资质证书' , BANK _ACCOUNT _LICENSE : '银行开户许可证' } as Record < string , string > ) [ type || '' ] || type || '企业资质'
}
function qualificationName ( item : PlatformTenantQualification ) {
try { return qualificationFileName ( item . documentUrl ) } catch { return qualificationLabel ( item . qualificationType ) }
}
function statusMeta ( status : PlatformTenantStatus ) {
if ( status === 'ACTIVE' ) return { label : '启用' , type : 'success' as const }
if ( status === 'PROFILE_REQUIRED' ) return { label : '待完善' , type : 'info' as const }
@@ -819,7 +833,7 @@ async function submitArchive() {
}
}
function openDecision ( row : PlatformTenantResource , type : 'approve' | 'reject' ) {
async function openDecision ( row : PlatformTenantResource , type : 'approve' | 'reject' ) {
if ( reviewSubmitting . value ) return
const review = pendingReviewByTenantId . value . get ( String ( row . id ) )
if ( ! review ) {
@@ -830,9 +844,60 @@ function openDecision(row: PlatformTenantResource, type: 'approve' | 'reject') {
decision . type = type
decision . comment = type === 'approve' ? '企业资料与必填资质完整、清晰,审核通过' : ''
decisionOpen . value = true
await loadReviewQualifications ( )
void nextTick ( ( ) => decisionFormRef . value ? . clearValidate ( 'comment' ) )
}
async function loadReviewQualifications ( ) {
const review = selectedReview . value
if ( ! review ) return
reviewQualifications . value = [ ]
reviewQualificationsError . value = null
reviewQualificationsLoading . value = true
try {
reviewQualifications . value = await listTenantQualifications ( review . tenantId )
} catch ( error ) {
reviewQualificationsError . value = getApiErrorDetails ( error , '客户资质加载失败,请重试后再审核' )
} finally {
reviewQualificationsLoading . value = false
}
}
function clearReviewPreview ( ) {
if ( reviewPreviewUrl . value ) URL . revokeObjectURL ( reviewPreviewUrl . value )
reviewPreviewOpen . value = false
reviewPreviewUrl . value = ''
reviewPreviewName . value = ''
}
async function loadReviewDocument ( item : PlatformTenantQualification , download : boolean ) {
const review = selectedReview . value
if ( ! review || reviewDocumentLoading . value ) return
const fileName = qualificationName ( item )
reviewDocumentLoading . value = String ( item . id )
try {
const blob = await downloadTenantQualification ( review . tenantId , item . documentUrl )
const url = URL . createObjectURL ( blob )
if ( download ) {
const anchor = document . createElement ( 'a' )
anchor . href = url
anchor . download = fileName
anchor . click ( )
URL . revokeObjectURL ( url )
return
}
clearReviewPreview ( )
reviewPreviewName . value = fileName
reviewPreviewKind . value = blob . type === 'application/pdf' || fileName . toLowerCase ( ) . endsWith ( '.pdf' ) ? 'pdf' : 'image'
reviewPreviewUrl . value = url
reviewPreviewOpen . value = true
} catch ( error ) {
ElMessage . error ( getApiErrorMessage ( error , '资质文件读取失败,请稍后重试' ) )
} finally {
reviewDocumentLoading . value = ''
}
}
async function submitDecision ( ) {
const review = selectedReview . value
if ( ! review || ! decisionFormRef . value || reviewSubmitting . value ) return
@@ -931,6 +996,7 @@ watch(() => decision.type, (type) => {
} )
onBeforeUnmount ( ( ) => {
clearQualificationPreviews ( )
clearReviewPreview ( )
impersonationCleanup ? . ( )
} )
onMounted ( ( ) => { void loadCustomers ( ) } )
@@ -951,8 +1017,8 @@ onMounted(() => { void loadCustomers() })
< 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 # actions = "{ row }" >
< div class = "table-actions" >
< el-button v-if = "access.can('platform:customer:view ')" 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:view') && access.can('platform:customer: edit')" class="customer-action-button customer-action-edit" size="small" @click="openEdit(row as PlatformTenantResource)"><Pencil :size="14" / > 编辑 < / 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)"><Eye :size="14" / > 详情 < / el-button >
< el-button v-if = "access.can('platform:customer:edit')" 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 customerSecondaryActions(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 >
< / div >
@@ -1043,9 +1109,12 @@ onMounted(() => { void loadCustomers() })
< / 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 class = "review-document-row" role = "row" > < span role = "cell" > { { selectedReview . qualificationType || '企业资质' } } < / span > < span role = "cell" > { { selectedReview . certificateNo || '—' } } < / span > < span role = "cell" > 待审核 < / span > < / div >
< 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 >
< / section >
< / div >
@@ -1056,6 +1125,10 @@ 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" >
< 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" >
< div v-if = "passwordTenant" class="decision-summary" > < div > < strong > 正在重置企业管理员登录密码 < / strong > < span > { { passwordTenant . tenantName } } < / span > < / div > < span > 客户编号 : { { passwordTenant . tenantCode } } < / span > < / div >
< el-alert v-if = "passwordError" type="error" :closable="false" show-icon role="alert"><template #title><div class="api-error-alert-title"><span>{{ passwordError.message }}</span><ApiErrorMeta :details="passwordError" / > < / div > < / template > < / el-alert >
@@ -1152,11 +1225,16 @@ onMounted(() => { void loadCustomers() })
. review - info - grid dt { color : # 64748 b ; font - size : 12 px ; }
. review - info - grid dd { min - width : 0 ; margin : 0 ; overflow - wrap : anywhere ; color : # 1 e293b ; font - size : 13 px ; }
. review - document - table { overflow : hidden ; border : 1 px solid # e4e4e7 ; border - radius : 6 px ; }
. review - document - header , . review - document - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) minmax ( 0 , 1 fr ) 76 px ; }
. review - document - header , . review - document - row { display : grid ; grid - template - columns : minmax ( 120 px , .8 fr ) minmax ( 140 px , 1 fr ) 150 px ; }
. review - document - header { background : rgba ( 102 , 126 , 234 , .08 ) ; color : # 52525 b ; font - size : 12 px ; font - weight : 600 ; }
. review - document - header > span , . review - document - row > span { min - width : 0 ; padding : 9 px 10 px ; overflow - wrap : anywhere ; border - right : 1 px solid # e4e4e7 ; }
. review - document - header > span : last - child , . review - document - row > span : last - child { border - right : 0 ; }
. review - document - row { border - top : 1 px solid # e4e4e7 ; color : # 334155 ; font - size : 13 px ; }
. review - document - actions { display : flex ; align - items : center ; gap : 8 px ; }
. review - document - actions : deep ( . el - button + . el - button ) { margin - left : 0 ; }
. review - document - state , . review - document - error , . review - document - empty { padding : 14 px ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
. review - document - error { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin - bottom : 8 px ; border : 1 px solid # fecdca ; border - radius : 6 px ; background : # fffafa ; color : # b42318 ; }
. review - document - empty { text - align : center ; }
. decision - summary > div > span { display : block ; margin - top : 4 px ; } . decision - type { margin : 0 0 16 px ; } . password - input - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto ; width : 100 % ; gap : 8 px ; } . dialog - hint { margin : 12 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.6 ; } . archive - confirm { display : grid ; gap : 8 px ; margin : 16 px 0 ; } . archive - confirm strong { color : var ( -- cc - text ) ; } . archive - confirm span { color : var ( -- cc - muted ) ; font - size : 12 px ; }
. approval - result - warning { display : flex ; align - items : flex - start ; gap : 10 px ; padding : 12 px 14 px ; border : 1 px solid # fecdca ; border - radius : 8 px ; background : # fffafa ; color : # b42318 ; } . approval - result - warning strong , . approval - result - warning span { display : block ; } . approval - result - warning strong { font - size : 14 px ; } . approval - result - warning span { margin - top : 4 px ; font - size : 12 px ; line - height : 1.5 ; } . approval - credentials { margin : 18 px 0 0 ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; } . approval - credentials > div { display : grid ; grid - template - columns : 90 px minmax ( 0 , 1 fr ) ; gap : 12 px ; padding : 12 px 14 px ; } . approval - credentials > div + div { border - top : 1 px solid var ( -- cc - border ) ; } . approval - credentials dt { color : var ( -- cc - muted ) ; font - size : 12 px ; } . approval - credentials dd { display : flex ; min - width : 0 ; align - items : center ; gap : 10 px ; margin : 0 ; } . approval - credentials code { min - width : 0 ; flex : 1 ; overflow - wrap : anywhere ; color : var ( -- cc - text ) ; font - size : 14 px ; font - weight : 700 ; }
@ 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 - dialog - grid { grid - template - columns : 1 fr ; gap : 18 px ; } . review - info - grid > div { grid - template - columns : 92 px minmax ( 0 , 1 fr ) ; } . password - input - row { grid - template - columns : 1 fr ; } . table - actions : deep ( . el - button ) { min - height : 40 px ; } }