@@ -7,17 +7,23 @@ import DataTable, { type DataTableColumn } from '@/components/common/DataTable.v
import { useTenantAdministrationStore , type TenantCompanyProfile , type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
import { useAuthStore } from '@/modules/auth/public'
import { formatQualificationFileSize , validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
import { downloadQualificationImage , uploadBusinessLicense } from '@/modules/tenant-management/api/merchantIam '
import { qualificationStatusMeta } from '@/modules/tenant-management/utils/qualificationStatus '
import {
downloadQualificationImage ,
submitTenantQualificationReview ,
uploadMerchantQualification ,
type MerchantQualificationType ,
} from '@/modules/tenant-management/api/merchantIam'
import { ApiError } from '@/api/http'
const store = useTenantAdministrationStore ( )
const auth = useAuthStore ( )
const companyApproved = computed ( ( ) => store . company . auditStatus === 'ACTIVE' )
const auditStatusCopy = computed ( ( ) => ( {
PROFILE _REQUIRED : { title : '请上传营业执照 ' , detail : '营业执照 提交审核前不能提交头程订单。' } ,
PROFILE _REQUIRED : { title : '请完善企业资质 ' , detail : '四类必填资质 提交审核前不能提交头程订单。' } ,
PENDING : { title : '企业资料正在等待平台审核' , detail : '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' } ,
ACTIVE : { title : '企业资料已审核,下单资格已开通' , detail : '您可以创建、保存并提交头程订单。' } ,
REJECTED : { title : '企业资料审核未通过' , detail : '请根据平台反馈修正资料;当前后端暂不支持同一企业重新上传 。' } ,
REJECTED : { title : '企业资料审核未通过' , detail : '请根据平台反馈替换对应资质,四类文件齐全后可重新提交审核 。' } ,
DISABLED : { title : '企业账号已被平台停用' , detail : '当前不能提交订单,请联系平台客服核实。' } ,
} ) [ store . company . auditStatus ] )
const activeTab = ref ( 'basic' )
@@ -26,6 +32,7 @@ const qualificationOpen = ref(false)
const previewOpen = ref ( false )
const previewRecord = ref < TenantQualification | null > ( null )
const previewUrl = ref ( '' )
const previewKind = ref < 'image' | 'pdf' > ( 'image' )
const previewError = ref ( '' )
const previewLoading = ref ( false )
const profileFormRef = ref < FormInstance > ( )
@@ -37,13 +44,31 @@ const selectedFileSize = ref(0)
const selectedFileType = ref ( '' )
const qualificationFileError = ref ( '' )
const qualificationSubmitting = ref ( false )
const reviewSubmitting = ref ( false )
const profileSubmitting = ref ( false )
const loading = ref ( false )
const loadError = ref ( '' )
const selectedFile = ref < File | null > ( null )
const draft = reactive < TenantCompanyProfile > ( { ... store . company } )
const qualificationDraft = reactive ( { type : '营业执照' , fileName : '' , certificateNo : '' } )
const qualificationTypes : Array < { value : MerchantQualificationType ; label : string } > = [
{ value : 'BUSINESS_LICENSE' , label : '营业执照' } ,
{ value : 'LEGAL_PERSON_ID' , label : '法人身份证' } ,
{ value : 'FREIGHT_FORWARDER_CERTIFICATE' , label : '货代资质证书' } ,
{ value : 'BANK_ACCOUNT_LICENSE' , label : '银行开户许可证' } ,
]
const qualificationDraft = reactive ( { type : 'BUSINESS_LICENSE' as MerchantQualificationType , fileName : '' , certificateNo : '' } )
const canUploadQualification = computed ( ( ) => [ 'PROFILE_REQUIRED' , 'REJECTED' ] . includes ( store . company . auditStatus ) )
const uploadedQualificationTypes = computed ( ( ) => new Set ( store . qualifications
. filter ( ( item ) => item . status === 'PENDING' )
. map ( ( item ) => qualificationTypes . find ( ( type ) => type . label === item . type ) ? . value )
. filter ( ( type ) : type is MerchantQualificationType => Boolean ( type ) ) ) )
const canSubmitQualificationReview = computed ( ( ) => canUploadQualification . value
&& qualificationTypes . every ( ( type ) => uploadedQualificationTypes . value . has ( type . value ) ) )
const qualificationReviewDisabledReason = computed ( ( ) => {
if ( ! canUploadQualification . value ) return qualificationUploadDisabledReason . value
const missing = qualificationTypes . filter ( ( type ) => ! uploadedQualificationTypes . value . has ( type . value ) ) . map ( ( type ) => type . label )
return missing . length ? ` 请先上传: ${ missing . join ( '、' ) } ` : ''
} )
const qualificationUploadDisabledReason = computed ( ( ) => {
if ( store . company . auditStatus === 'PENDING' ) return '企业资料正在审核,暂不能再次提交资质文件。'
if ( store . company . auditStatus === 'ACTIVE' ) return '当前企业已审核通过,资质替换请联系平台管理员处理。'
@@ -51,7 +76,7 @@ const qualificationUploadDisabledReason = computed(() => {
return ''
} )
const qualificationPanelHint = computed ( ( ) => canUploadQualification . value
? '一期仅支持提交营业执照;提交后将进入平台 审核。'
? '请先上传四类必填资质;文件上传不会自动送审,全部齐全后再提交企业 审核。'
: qualificationUploadDisabledReason . value )
const profileFormRules : FormRules < TenantCompanyProfile > = {
companyName : [ { required : true , whitespace : true , message : '请输入公司名称' , trigger : 'blur' } ] ,
@@ -61,7 +86,7 @@ const profileFormRules: FormRules<TenantCompanyProfile> = {
contactEmail : [ { type : 'email' , message : '请输入正确的联系邮箱' , trigger : 'blur' } ] ,
}
const qualificationFormRules : FormRules = {
fileName : [ { required : true , message : '请选择营业执照图片 ' , trigger : 'change' } ] ,
fileName : [ { required : true , message : '请选择资质文件 ' , trigger : 'change' } ] ,
}
const qualificationColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 90 , fixed : 'left' , slotName : 'actions' } ,
@@ -76,19 +101,24 @@ const pagedQualifications = computed(() => {
return store . qualifications . slice ( start , start + qualificationPageSize . value )
} )
async function loadCompanyContext ( ) {
loading . value = true
async function refreshCompanyContext ( ) {
loadError . value = ''
try {
await store . loadCompanyContext ( )
Object . assign ( draft , store . company )
return true
} catch ( error ) {
loadError . value = error instanceof Error ? error . message : '企业资料加载失败'
} finally {
loading . value = false
return false
}
}
async function loadCompanyContext ( ) {
loading . value = true
await refreshCompanyContext ( )
loading . value = false
}
onMounted ( ( ) => void loadCompanyContext ( ) )
function startEdit ( ) {
@@ -163,7 +193,7 @@ function removeSelectedFile() {
function resetQualificationDraft ( ) {
uploadRef . value ? . clearFiles ( )
qualificationDraft . type = '营业执照 '
qualificationDraft . type = 'BUSINESS_LICENSE '
qualificationDraft . fileName = ''
qualificationDraft . certificateNo = store . company . creditCode
selectedFileSize . value = 0
@@ -183,7 +213,7 @@ async function submitQualification() {
}
const valid = await qualificationFormRef . value ? . validate ( ) . then ( ( ) => true ) . catch ( ( ) => false )
if ( ! valid || ! selectedFile . value ) {
qualificationFileError . value = '请选择营业执照图片 '
qualificationFileError . value = '请选择资质文件 '
return
}
if ( qualificationFileError . value ) {
@@ -192,23 +222,54 @@ async function submitQualification() {
}
qualificationSubmitting . value = true
try {
await uploadBusinessLicense ( selectedFile . value , qualificationDraft . certificateNo . trim ( ) || undefined )
await store . loadCompanyContext ( )
qualificationOpen . value = false
ElMessage . success ( '营业执照已提交审核' )
} catch ( error ) {
if ( error instanceof ApiError && error . code === 'A0409' ) {
await loadCompanyContext ( )
try {
const existing = store . qualifications . find ( ( item ) => (
item . type === qualificationTypes . find ( ( type ) => type . value === qualificationDraft . type ) ? . label
&& item . status !== 'SUPERSEDED'
) )
await uploadMerchantQualification (
selectedFile . value ,
qualificationDraft . type ,
qualificationDraft . type === 'BUSINESS_LICENSE' ? qualificationDraft . certificateNo . trim ( ) || undefined : undefined ,
Boolean ( existing ) ,
)
qualificationOpen . value = false
ElMessage . warning ( '企业资料正在审核,不能再次提交资质文件' )
} catch ( error ) {
if ( error instanceof ApiError && error . code === 'A0409' ) {
await loadCompanyContext ( )
qualificationOpen . value = false
ElMessage . warning ( '企业资料正在审核,不能再次提交资质文件' )
return
}
ElMessage . error ( error instanceof Error ? error . message : '资质文件上传失败' )
return
}
ElMessage . error ( error instanceof Error ? error . message : '营业执照上传失败' )
const refreshed = await refreshCompanyContext ( )
if ( refreshed ) ElMessage . success ( '资质文件已上传,尚未提交审核' )
else ElMessage . warning ( '资质文件已上传,但最新状态刷新失败,请点击“重新加载”确认状态' )
} finally {
qualificationSubmitting . value = false
}
}
async function submitQualificationReview ( ) {
if ( ! canSubmitQualificationReview . value || reviewSubmitting . value ) return
reviewSubmitting . value = true
try {
try {
await submitTenantQualificationReview ( )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '企业审核提交失败' )
return
}
const refreshed = await refreshCompanyContext ( )
if ( refreshed ) ElMessage . success ( '企业资料已提交审核' )
else ElMessage . warning ( '企业资料已提交审核,但最新状态刷新失败,请点击“重新加载”确认状态' )
} finally {
reviewSubmitting . value = false
}
}
async function previewQualification ( record : TenantQualification ) {
if ( previewUrl . value ) URL . revokeObjectURL ( previewUrl . value )
previewRecord . value = record
@@ -222,6 +283,7 @@ async function previewQualification(record: TenantQualification) {
previewLoading . value = true
try {
const blob = await downloadQualificationImage ( record . documentUrl )
previewKind . value = blob . type === 'application/pdf' || record . fileName . toLowerCase ( ) . endsWith ( '.pdf' ) ? 'pdf' : 'image'
previewUrl . value = URL . createObjectURL ( blob )
} catch ( error ) {
previewError . value = error instanceof Error && error . message === '资质文件地址无效'
@@ -236,13 +298,11 @@ async function previewQualification(record: TenantQualification) {
function closeQualificationPreview ( ) {
if ( previewUrl . value ) URL . revokeObjectURL ( previewUrl . value )
previewUrl . value = ''
previewKind . value = 'image'
previewError . value = ''
previewRecord . value = null
}
function qualificationStatus ( status : string ) {
return status === 'VERIFIED' ? '已验证' : status === 'PENDING' ? '待审核' : status === 'REJECTED' ? '已驳回' : '已过期'
}
< / script >
< template >
@@ -287,9 +347,10 @@ function qualificationStatus(status: string) {
< div class = "client-card qualification-panel" >
< header >
< div > < h3 > 企业资质 < / h3 > < p > { { qualificationPanelHint } } < / p > < / div >
< el-tooltip v-if = "auth.hasPermission('company:qualification:upload')" :disabled="canUploadQualification" :content="qualificationUploadDisabledReason" placement="top " >
< span class = "qualification-update-control "> < el-button type = "primary" :disabled = "!canUploadQualification" @click ="openQualificationDialog" > < Upload :size = "16" / > 更新 资质< / el-button > < / span >
< / el-tooltip >
< div v-if = "auth.hasPermission('company:qualification:upload')" class="qualification-panel-actions " >
< el -tooltip :disabled = "canUploadQualification" :content = "qualificationUploadDisabledReason" placement = "top "> < span class = "qualification-update-control" > < el-button :disabled = "!canUploadQualification" @click ="openQualificationDialog" > < Upload :size = "16" / > 上传 资质< / el-button > < / span > < / el-tooltip >
< el-tooltip :disabled = "canSubmitQualificationReview" :content = "qualificationReviewDisabledReason" placement = "top" > < span class = "qualification-update-control" > < el-button type = "primary" :loading = "reviewSubmitting" :disabled = "!canSubmitQualificationReview" @click ="submitQualificationReview" > < FileCheck2 :size = "16" / > 提交企业审核 < / el-button > < / span > < / el-tooltip >
< / div >
< / header >
< DataTable
v -model :page = "qualificationPage"
@@ -303,11 +364,11 @@ function qualificationStatus(status: string) {
row -key = " id "
>
< template # actions = "{ row }" > < el-button link type = "primary" @click ="previewQualification(row as TenantQualification)" > 查看 < / el -button > < / template >
< template # status = "{ row }" > < el-tag : type = "row.status === 'VERIFIED' ? 'success' : row.status === 'PENDING' ? 'warning' : 'danger' " effect = "light" > { { qualificationStatus ( row . status ) } } < / el-tag > < / template >
< template # status = "{ row }" > < el-tag :type = "qualificationStatusMeta(row.status).type " effect = "light" > { { qualificationStatusMeta ( row . status ) . label } } < / el-tag > < / template >
< / DataTable >
< div class = "mobile-qualification-list" >
< article v-for = "record in store.qualifications" :key="record.id" >
< header > < strong > { { record . type } } < / strong > < el-tag : type = "record.status === 'VERIFIED' ? 'success' : record.status === 'PENDING' ? 'warning' : 'danger' " effect = "light" > { { qualificationStatus ( record . status ) } } < / el-tag > < / header >
< header > < strong > { { record . type } } < / strong > < el-tag :type = "qualificationStatusMeta(record.status).type " effect = "light" > { { qualificationStatusMeta ( record . status ) . label } } < / el-tag > < / header >
< div class = "qualification-file-name" > < FileText :size = "16" / > < span > { { record . fileName } } < / span > < / div >
< dl > < div > < dt > 有效期 < / dt > < dd > { { record . expiresAt } } < / dd > < / div > < div > < dt > 更新时间 < / dt > < dd > { { record . updatedAt } } < / dd > < / div > < / dl >
< el-button link type = "primary" @click ="previewQualification(record)" > 查看文件信息 < / el -button >
@@ -338,11 +399,11 @@ function qualificationStatus(status: string) {
< el-dialog v-model = "qualificationOpen" title="更新资质文件" width="min(520px, 92vw)" @closed="resetQualificationDraft" >
< el -form ref = "qualificationFormRef" :model = "qualificationDraft" :rules = "qualificationFormRules" label -position = " top " >
< el-form-item label = "文件类型" > < el-input v-model = "qualificationDraft.type" disabled / > < / el-form-item >
< el-form-item label = "文件类型" required > < el-select v-model = "qualificationDraft.type"><el-option v-for="type in qualificationTypes" :key="type.value" :label="type.label" :value="type.value" / > < / el-select > < / el-form-item >
< el-form-item label = "选择文件" prop = "fileName" required :error = "qualificationFileError" >
< el-upload ref = "uploadRef" :auto-upload = "false" :limit = "1" :show-file-list = "false" accept = ".jpg,.jpeg,.png" @change ="handleFileChange" >
< el-upload ref = "uploadRef" :auto-upload = "false" :limit = "1" :show-file-list = "false" accept = ".pdf,. jpg,.jpeg,.png,application/pdf,image/jpeg,image/png " @change ="handleFileChange" >
< el -button > < Upload :size = "16" / > 选择文件 < / el-button >
< template # tip > < div class = "upload-tip" > 仅 支持 JPG 、 PNG , 单张图片 不超过 5 MB 。 < / div > < / template >
< template # tip > < div class = "upload-tip" > 支持 PDF 、 JPG 、 PNG , 单个文件 不超过 5 MB 。 < / div > < / template >
< / el-upload >
< div v-if = "qualificationDraft.fileName" class="selected-qualification-file" aria-live="polite" >
< FileText :size = "20" / >
@@ -350,17 +411,18 @@ function qualificationStatus(status: string) {
< el-button link type = "danger" aria -label = " 移除所选资质文件 " @click ="removeSelectedFile" > < Trash2 :size = "16" / > 移除 < / el-button >
< / div >
< / el-form-item >
< el-form-item label = "统一社会信用代码" > < el-input v-model = "qualificationDraft.certificateNo" maxlength="18" placeholder="建议填写 18 位统一社会信用代码" / > < / el-form-item >
< el-form-item v-if = "qualificationDraft.type === 'BUSINESS_LICENSE'" label="统一社会信用代码"><el-input v-model= "qualificationDraft.certificateNo" maxlength="18" placeholder="建议填写 18 位统一社会信用代码" / > < / el-form-item >
< / el-form >
< template # footer > < el-button :disabled = "qualificationSubmitting" @click ="qualificationOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "qualificationSubmitting" @click ="submitQualification" > 提交审核 < / el -button > < / template >
< template # footer > < el-button :disabled = "qualificationSubmitting" @click ="qualificationOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "qualificationSubmitting" @click ="submitQualification" > 保存资质文件 < / el -button > < / template >
< / el-dialog >
< el-dialog v-model = "previewOpen" :title="`${previewRecord?.type || '资质文件'}详情`" width="min(560px, 92vw)" @closed="closeQualificationPreview" >
< div v-if = "previewRecord" v-loading="previewLoading" class="document-preview" >
< img v-if = "previewUrl" :src="previewUrl" :alt="`${previewRecord.type}预览`" class="qualification-preview-image" / >
< img v-if = "previewUrl && previewKind === 'image' " :src="previewUrl" :alt="`${previewRecord.type}预览`" class="qualification-preview-image" / >
< iframe v-else-if = "previewUrl && previewKind === 'pdf'" :src="previewUrl" :title="`${previewRecord.type} PDF 预览`" class="qualification-preview-frame" / >
< FileCheck2 :size = "42" / >
< strong > { { previewRecord . fileName } } < / strong >
< el-tag : type = "previewRecord.status === 'VERIFIED' ? 'success' : previewRecord.status === 'PENDING' ? 'warning' : 'danger' " effect = "light" > { { qualificationStatus ( previewRecord . status ) } } < / el-tag >
< el-tag :type = "qualificationStatusMeta(previewRecord.status).type " effect = "light" > { { qualificationStatusMeta ( previewRecord . status ) . label } } < / el-tag >
< dl > < div > < dt > 文件类型 < / dt > < dd > { { previewRecord . type } } < / dd > < / div > < div > < dt > 有效期 < / dt > < dd > { { previewRecord . expiresAt } } < / dd > < / div > < div > < dt > 更新时间 < / dt > < dd > { { previewRecord . updatedAt } } < / dd > < / div > < / dl >
< p v-if = "previewError" class="document-preview-error" role="alert" > {{ previewError }} < / p >
< span v-else > {{ previewUrl ? ' 文件通过当前商户会话临时加载 , 关闭弹窗后立即释放 。 ' : ' 当前文件暂不可预览 。 ' }} < / span >
@@ -382,6 +444,7 @@ function qualificationStatus(status: string) {
. company - audit - notice span { color : var ( -- muted ) ; font - size : 12 px ; line - height : 1.6 ; }
. company - audit - notice . is - approved { border - color : # bcd9d2 ; background : # eff8f5 ; color : # 20745 f ; }
. qualification - preview - image { display : block ; max - width : 100 % ; max - height : 360 px ; margin : 0 auto 14 px ; object - fit : contain ; }
. qualification - preview - frame { display : block ; width : 100 % ; min - height : min ( 60 vh , 560 px ) ; margin : 0 0 14 px ; border : 0 ; background : # fff ; }
. company - audit - notice . is - pending { border - color : # e5d2a5 ; background : # fff9eb ; color : # 9 a6a12 ; }
. qualification - panel header p { margin : 6 px 0 0 ; color : var ( -- muted ) ; font - size : 12 px ; }
. profile - tabs : deep ( . el - tabs _ _header ) { margin - bottom : 14 px ; padding : 0 16 px ; border : 1 px solid var ( -- border ) ; background : # fff ; }
@@ -399,6 +462,7 @@ function qualificationStatus(status: string) {
. qualification - panel { padding : 0 ; overflow : hidden ; }
. qualification - panel > header { padding : 20 px 22 px ; border - bottom : 1 px solid var ( -- border ) ; }
. qualification - panel h3 { margin : 0 ; }
. qualification - panel - actions { display : flex ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
. qualification - update - control { display : inline - flex ; }
. mobile - qualification - list { display : none ; }
. profile - form h3 { margin : 4 px 0 18 px ; padding - bottom : 10 px ; border - bottom : 1 px solid var ( -- border ) ; font - size : 15 px ; }
@@ -424,6 +488,8 @@ function qualificationStatus(status: string) {
. profile - grid , . form - grid - two { grid - template - columns : 1 fr ; }
. profile - grid > div : nth - child ( odd ) { border - right : 0 ; }
. qualification - panel > header { align - items : flex - start ; gap : 12 px ; }
. qualification - panel - actions { display : grid ; width : 100 % ; grid - template - columns : 1 fr ; }
. qualification - panel - actions . qualification - update - control , . qualification - panel - actions : deep ( . el - button ) { width : 100 % ; }
. qualification - panel > header . el - button { flex : 0 0 auto ; }
. desktop - qualification - table { display : none ; }
. mobile - qualification - list { display : grid ; gap : 10 px ; padding : 10 px ; }