2026-07-30 17:59:42 +08:00
< script setup lang = "ts" >
2026-08-04 10:31:53 +08:00
import { computed , onBeforeUnmount , onMounted , reactive , ref } from 'vue'
2026-09-11 16:17:24 +08:00
import { useRoute } from 'vue-router'
import { ElMessage , ElMessageBox } from 'element-plus'
2026-08-04 09:05:01 +08:00
import type { FormInstance , FormRules , UploadFile } from 'element-plus'
2026-07-30 17:59:42 +08:00
import { Building2 , CheckCircle2 , Clock3 , FileCheck2 , FileText , Pencil , Trash2 , Upload , UserRound } from '@lucide/vue'
2026-08-01 12:17:33 +08:00
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
2026-08-03 18:52:30 +08:00
import { useTenantAdministrationStore , type TenantCompanyProfile , type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
import { useAuthStore } from '@/modules/auth/public'
2026-08-04 14:15:24 +08:00
import { formatQualificationFileSize , qualificationUploadErrorMessage , validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
2026-08-03 20:50:32 +08:00
import { qualificationStatusMeta } from '@/modules/tenant-management/utils/qualificationStatus'
import {
downloadQualificationImage ,
submitTenantQualificationReview ,
uploadMerchantQualification ,
2026-08-05 09:11:41 +08:00
uploadMerchantRequiredQualificationPackage ,
2026-08-03 20:50:32 +08:00
type MerchantQualificationType ,
} from '@/modules/tenant-management/api/merchantIam'
2026-07-30 17:59:42 +08:00
const store = useTenantAdministrationStore ()
const auth = useAuthStore ()
2026-09-11 16:17:24 +08:00
const route = useRoute ()
2026-07-30 17:59:42 +08:00
const companyApproved = computed (() => store . company . auditStatus === 'ACTIVE' )
const auditStatusCopy = computed (() => ({
2026-09-01 19:34:37 +08:00
PROFILE_REQUIRED : { title : '请完善企业资质' , detail : '上传营业执照并提交审核后,才能提交头程订单;法人身份证可按需补充。' },
2026-08-03 10:51:53 +08:00
PENDING : { title : '企业资料正在等待平台审核' , detail : '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' },
ACTIVE : { title : '企业资料已审核,下单资格已开通' , detail : '您可以创建、保存并提交头程订单。' },
2026-08-04 14:15:24 +08:00
REJECTED : { title : '企业资料审核未通过' , detail : '请根据平台反馈替换对应资质,必填文件齐全后可重新提交审核。' },
2026-08-03 10:51:53 +08:00
DISABLED : { title : '企业账号已被平台停用' , detail : '当前不能提交订单,请联系平台客服核实。' },
2026-08-14 16:06:33 +08:00
ARCHIVED : { title : '企业已归档' , detail : '该企业已结束使用,资料和历史业务仅供查看,不能再提交或修改。' },
2026-07-30 17:59:42 +08:00
})[ store . company . auditStatus ])
const activeTab = ref ( 'basic' )
const editing = ref ( false )
const qualificationOpen = ref ( false )
const previewOpen = ref ( false )
const previewRecord = ref < TenantQualification | null >( null )
const previewUrl = ref ( '' )
2026-08-03 20:50:32 +08:00
const previewKind = ref < 'image' | 'pdf' > ( 'image' )
2026-08-03 18:15:34 +08:00
const previewError = ref ( '' )
2026-07-30 17:59:42 +08:00
const previewLoading = ref ( false )
2026-08-01 12:17:33 +08:00
const profileFormRef = ref < FormInstance >()
const qualificationPage = ref ( 1 )
const qualificationPageSize = ref ( 10 )
2026-07-30 17:59:42 +08:00
const qualificationSubmitting = ref ( false )
2026-08-03 20:50:32 +08:00
const reviewSubmitting = ref ( false )
2026-09-11 16:17:24 +08:00
const deletingQualificationId = ref ( '' )
2026-07-30 17:59:42 +08:00
const profileSubmitting = ref ( false )
const loading = ref ( false )
2026-07-31 15:34:22 +08:00
const loadError = ref ( '' )
2026-07-30 17:59:42 +08:00
const draft = reactive < TenantCompanyProfile >({ ... store . company })
2026-08-04 09:05:01 +08:00
type QualificationUploadState = 'idle' | 'uploading' | 'uploaded' | 'error'
2026-08-04 14:15:24 +08:00
type QualificationTypeDefinition = { value : MerchantQualificationType ; label : string ; description : string ; required : boolean }
2026-08-04 09:05:01 +08:00
type QualificationDraftItem = QualificationTypeDefinition & {
file : File | null
fileName : string
fileSize : number
fileType : string
2026-08-04 10:31:53 +08:00
previewUrl : string
2026-08-04 09:05:01 +08:00
inputVersion : number
error : string
uploadState : QualificationUploadState
}
const qualificationTypes : QualificationTypeDefinition [] = [
2026-08-04 14:15:24 +08:00
{ value : 'BUSINESS_LICENSE' , label : '营业执照' , description : '上传清晰完整的营业执照文件。' , required : true },
2026-09-11 16:17:24 +08:00
{ value : 'LEGAL_PERSON_ID' , label : '法人身份证' , description : '选填;如上传,正反面请合并为一个 PDF、JPG、PNG 或 BMP 文件。' , required : false },
2026-08-04 14:15:24 +08:00
{ value : 'FREIGHT_FORWARDER_CERTIFICATE' , label : '货代资质证书' , description : '如有,可上传当前有效的货代资质证明。' , required : false },
{ value : 'BANK_ACCOUNT_LICENSE' , label : '银行开户许可证' , description : '如有,可上传企业对公账户开户证明。' , required : false },
2026-08-03 20:50:32 +08:00
]
2026-08-04 14:15:24 +08:00
const requiredQualificationTypes = qualificationTypes . filter (( type ) => type . required )
2026-08-04 09:05:01 +08:00
const qualificationDrafts = reactive < QualificationDraftItem [] >( qualificationTypes . map (( type ) => ({
... type ,
file : null ,
fileName : '' ,
fileSize : 0 ,
fileType : '' ,
2026-08-04 10:31:53 +08:00
previewUrl : '' ,
2026-08-04 09:05:01 +08:00
inputVersion : 0 ,
error : '' ,
uploadState : 'idle' ,
})))
const qualificationCertificateNo = ref ( '' )
const unifiedSocialCreditCodePattern = /^[0-9A-Z]{18}$/
const companyCreditCodeValid = computed (() => unifiedSocialCreditCodePattern . test ( store . company . creditCode ))
const normalizedQualificationCertificateNo = computed (() => qualificationCertificateNo . value . trim (). toUpperCase ())
2026-08-04 21:01:34 +08:00
const draftCreditCodeValid = computed (() => unifiedSocialCreditCodePattern . test ( normalizedQualificationCertificateNo . value ))
const businessLicenseFileSelected = computed (() => Boolean ( qualificationDraftItem ( 'BUSINESS_LICENSE' ) ? . file ))
2026-08-05 09:11:41 +08:00
const legalPersonIdFileSelected = computed (() => Boolean ( qualificationDraftItem ( 'LEGAL_PERSON_ID' ) ? . file ))
2026-09-01 19:34:37 +08:00
const requiredPackageChangeSelected = computed (() => businessLicenseFileSelected . value )
const requiredPackageFilesSelected = computed (() => businessLicenseFileSelected . value )
2026-08-05 09:11:41 +08:00
const qualificationCreditCodeReady = computed (() => requiredPackageChangeSelected . value
? requiredPackageFilesSelected . value && ( companyCreditCodeValid . value || draftCreditCodeValid . value )
: companyCreditCodeValid . value )
2026-08-04 09:05:01 +08:00
const qualificationCreditCodeError = computed (() => {
if ( qualificationCreditCodeReady . value ) return ''
2026-09-01 19:34:37 +08:00
if ( draftCreditCodeValid . value && ! requiredPackageFilesSelected . value ) return '统一社会信用代码必须随营业执照共同保存'
2026-08-04 09:05:01 +08:00
return '请输入18位字母或数字组成的统一社会信用代码'
})
2026-08-03 18:15:34 +08:00
const canUploadQualification = computed (() => [ 'PROFILE_REQUIRED' , 'REJECTED' ]. includes ( store . company . auditStatus ))
2026-08-03 20:50:32 +08:00
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 ))))
2026-08-05 09:11:41 +08:00
const requiredPackageFilesReady = computed (() => requiredPackageChangeSelected . value
? requiredPackageFilesSelected . value
: requiredQualificationTypes . every (( type ) => uploadedQualificationTypes . value . has ( type . value )))
2026-08-04 21:01:34 +08:00
const requiredPackageComplete = computed (() => qualificationCreditCodeReady . value && requiredPackageFilesReady . value )
const missingRequiredPackageItems = computed (() => {
const missing = requiredQualificationTypes
2026-08-05 09:11:41 +08:00
. filter (( type ) => requiredPackageChangeSelected . value
? ! qualificationDraftItem ( type . value ) ? . file
: ! uploadedQualificationTypes . value . has ( type . value ))
2026-08-04 21:01:34 +08:00
. map (( type ) => type . label )
if ( ! qualificationCreditCodeReady . value ) missing . splice ( 1 , 0 , '18位统一社会信用代码' )
return missing
})
2026-08-03 18:15:34 +08:00
const qualificationUploadDisabledReason = computed (() => {
if ( store . company . auditStatus === 'PENDING' ) return '企业资料正在审核,暂不能再次提交资质文件。'
if ( store . company . auditStatus === 'ACTIVE' ) return '当前企业已审核通过,资质替换请联系平台管理员处理。'
if ( store . company . auditStatus === 'DISABLED' ) return '企业账号已停用,暂不能提交资质文件。'
2026-08-14 16:06:33 +08:00
if ( store . company . auditStatus === 'ARCHIVED' ) return '企业已归档,不能再提交资质文件。'
2026-08-03 18:15:34 +08:00
return ''
})
2026-08-04 09:05:01 +08:00
const selectedQualificationCount = computed (() => qualificationDrafts . filter (( item ) => Boolean ( item . file )). length )
const canSaveQualificationBatch = computed (() => canUploadQualification . value
&& selectedQualificationCount . value > 0
2026-08-04 21:01:34 +08:00
&& requiredPackageComplete . value )
2026-08-04 09:05:01 +08:00
const qualificationSaveDisabledReason = computed (() => {
if ( ! canUploadQualification . value ) return qualificationUploadDisabledReason . value
2026-08-04 21:01:34 +08:00
if ( ! requiredPackageComplete . value ) return `请完整保存必填业务项: ${ missingRequiredPackageItems . value . join ( '、' ) } `
if ( ! selectedQualificationCount . value ) return '当前没有需要保存的变更'
2026-08-04 09:05:01 +08:00
return ''
})
2026-08-04 10:31:53 +08:00
const qualificationDialogHint = computed (() => qualificationSaveDisabledReason . value === qualificationCreditCodeError . value
? ''
: qualificationSaveDisabledReason . value )
2026-08-04 09:05:01 +08:00
const canSubmitQualificationReview = computed (() => canUploadQualification . value
&& companyCreditCodeValid . value
2026-08-04 14:15:24 +08:00
&& requiredQualificationTypes . every (( type ) => uploadedQualificationTypes . value . has ( type . value )))
2026-08-04 09:05:01 +08:00
const qualificationReviewDisabledReason = computed (() => {
if ( ! canUploadQualification . value ) return qualificationUploadDisabledReason . value
if ( ! companyCreditCodeValid . value ) return '请先通过营业执照填写18位统一社会信用代码'
2026-08-04 14:15:24 +08:00
const missing = requiredQualificationTypes . filter (( type ) => ! uploadedQualificationTypes . value . has ( type . value )). map (( type ) => type . label )
2026-08-04 09:05:01 +08:00
return missing . length ? `请先上传: ${ missing . join ( '、' ) } ` : ''
})
2026-08-03 18:15:34 +08:00
const qualificationPanelHint = computed (() => canUploadQualification . value
2026-09-01 19:34:37 +08:00
? '营业执照为必填;法人身份证、货代资质证书、银行开户许可证可选填。文件保存后需另行提交审核。'
2026-08-03 18:15:34 +08:00
: qualificationUploadDisabledReason . value )
2026-08-01 12:17:33 +08:00
const profileFormRules : FormRules < TenantCompanyProfile > = {
companyName : [{ required : true , whitespace : true , message : '请输入公司名称' , trigger : 'blur' }],
legalRepresentative : [{ required : true , whitespace : true , message : '请输入法人姓名' , trigger : 'blur' }],
contactName : [{ required : true , whitespace : true , message : '请输入联系人姓名' , trigger : 'blur' }],
2026-09-02 10:15:51 +08:00
contactPhone : [
{ required : true , whitespace : true , message : '请输入公司联系电话' , trigger : 'blur' },
{ pattern : /^[0-9-]{4,32}$/ , message : '公司联系电话仅支持 4–32 位数字或连字符' , trigger : 'blur' },
],
2026-08-01 12:17:33 +08:00
contactEmail : [{ type : 'email' , message : '请输入正确的联系邮箱' , trigger : 'blur' }],
}
const qualificationColumns : DataTableColumn [] = [
2026-09-11 16:17:24 +08:00
{ key : 'actions' , label : '操作' , width : 132 , fixed : 'left' , slotName : 'actions' },
2026-08-01 12:17:33 +08:00
{ key : 'type' , prop : 'type' , label : '文件类型' , minWidth : 150 },
{ key : 'fileName' , prop : 'fileName' , label : '文件名称' , minWidth : 220 , showOverflowTooltip : true },
{ key : 'status' , label : '状态' , width : 110 , slotName : 'status' },
{ key : 'expiresAt' , prop : 'expiresAt' , label : '有效期' , width : 130 },
{ key : 'updatedAt' , prop : 'updatedAt' , label : '更新时间' , minWidth : 160 },
]
const pagedQualifications = computed (() => {
const start = ( qualificationPage . value - 1 ) * qualificationPageSize . value
return store . qualifications . slice ( start , start + qualificationPageSize . value )
})
2026-07-30 17:59:42 +08:00
2026-08-03 20:50:32 +08:00
async function refreshCompanyContext () {
2026-07-31 15:34:22 +08:00
loadError . value = ''
2026-07-30 17:59:42 +08:00
try {
await store . loadCompanyContext ()
Object . assign ( draft , store . company )
2026-08-03 20:50:32 +08:00
return true
2026-07-30 17:59:42 +08:00
} catch ( error ) {
2026-07-31 15:34:22 +08:00
loadError . value = error instanceof Error ? error . message : '企业资料加载失败'
2026-08-03 20:50:32 +08:00
return false
2026-07-30 17:59:42 +08:00
}
2026-07-31 15:34:22 +08:00
}
2026-08-03 20:50:32 +08:00
async function loadCompanyContext () {
loading . value = true
await refreshCompanyContext ()
loading . value = false
}
2026-09-11 16:17:24 +08:00
onMounted ( async () => {
await loadCompanyContext ()
if ( route . query . qualification === 'upload' && canUploadQualification . value ) {
activeTab . value = 'qualification'
openQualificationDialog ()
}
})
2026-08-04 10:31:53 +08:00
onBeforeUnmount (() => qualificationDrafts . forEach ( releaseQualificationPreview ))
2026-07-30 17:59:42 +08:00
function startEdit () {
2026-08-14 16:06:33 +08:00
if ( store . company . auditStatus === 'ARCHIVED' ) return
2026-07-30 17:59:42 +08:00
Object . assign ( draft , store . company )
2026-08-01 12:17:33 +08:00
profileFormRef . value ? . clearValidate ()
2026-07-30 17:59:42 +08:00
editing . value = true
}
function cancelEdit () {
Object . assign ( draft , store . company )
editing . value = false
2026-08-01 12:17:33 +08:00
profileFormRef . value ? . clearValidate ()
2026-07-30 17:59:42 +08:00
}
async function saveProfile () {
if ( profileSubmitting . value ) return
2026-08-01 12:17:33 +08:00
const valid = await profileFormRef . value ? . validate (). then (() => true ). catch (() => false )
if ( ! valid ) return
2026-07-30 17:59:42 +08:00
profileSubmitting . value = true
try {
await store . updateCompany ({ ... draft })
Object . assign ( draft , store . company )
editing . value = false
ElMessage . success ( '企业资料已保存' )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '企业资料保存失败' )
} finally {
profileSubmitting . value = false
}
}
function openQualificationDialog () {
2026-08-03 18:15:34 +08:00
if ( ! canUploadQualification . value ) {
ElMessage . warning ( qualificationUploadDisabledReason . value )
return
}
2026-07-30 17:59:42 +08:00
resetQualificationDraft ()
qualificationOpen . value = true
}
2026-08-04 09:05:01 +08:00
function qualificationRecord ( type : MerchantQualificationType ) {
const label = qualificationTypes . find (( item ) => item . value === type ) ? . label
const records = store . qualifications . filter (( item ) => item . type === label && item . status !== 'SUPERSEDED' )
return records . find (( item ) => item . status === 'PENDING' )
?? records . find (( item ) => item . status === 'VERIFIED' )
?? records . find (( item ) => item . status === 'REJECTED' )
}
function replaceableQualification ( type : MerchantQualificationType ) {
const record = qualificationRecord ( type )
return record && record . status !== 'REJECTED' ? record : undefined
}
function qualificationDraftItem ( type : MerchantQualificationType ) {
return qualificationDrafts . find (( item ) => item . value === type )
}
2026-08-04 10:31:53 +08:00
function releaseQualificationPreview ( item : QualificationDraftItem ) {
if ( ! item . previewUrl ) return
URL . revokeObjectURL ( item . previewUrl )
item . previewUrl = ''
}
2026-08-04 09:05:01 +08:00
function qualificationSlotLabel ( item : QualificationDraftItem ) {
if ( item . uploadState === 'uploading' ) return '上传中'
if ( item . uploadState === 'uploaded' ) return '已保存'
if ( item . uploadState === 'error' ) return '上传失败'
if ( item . file ) return '待保存'
const record = qualificationRecord ( item . value )
if ( record ? . status === 'PENDING' ) return '已上传'
if ( record ? . status === 'REJECTED' ) return '需重新上传'
2026-08-04 14:15:24 +08:00
return item . required ? '待上传' : '选填'
2026-08-04 09:05:01 +08:00
}
function qualificationSlotTagType ( item : QualificationDraftItem ) {
if ( item . file ) return 'warning'
if ( item . uploadState === 'uploaded' || qualificationRecord ( item . value ) ? . status === 'PENDING' ) return 'success'
if ( item . uploadState === 'error' || qualificationRecord ( item . value ) ? . status === 'REJECTED' ) return 'danger'
2026-08-04 14:15:24 +08:00
return item . required ? 'warning' : 'info'
2026-08-04 09:05:01 +08:00
}
function handleQualificationFileChange ( type : MerchantQualificationType , file : UploadFile ) {
const item = qualificationDraftItem ( type )
if ( ! item ) return
2026-08-04 10:31:53 +08:00
releaseQualificationPreview ( item )
2026-07-30 17:59:42 +08:00
const raw = file . raw
if ( ! raw ) {
2026-08-04 09:05:01 +08:00
item . error = '未能读取所选文件,请重新选择'
item . uploadState = 'error'
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . file = null
item . inputVersion += 1
2026-07-30 17:59:42 +08:00
return
}
const error = validateQualificationFile ({ name : raw . name , size : raw . size , type : raw . type })
if ( error ) {
2026-08-04 09:05:01 +08:00
item . error = error
item . uploadState = 'error'
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . file = null
item . inputVersion += 1
2026-07-30 17:59:42 +08:00
return
}
2026-08-04 09:05:01 +08:00
item . error = ''
item . uploadState = 'idle'
item . file = raw
item . fileName = raw . name
item . fileSize = raw . size
item . fileType = raw . type || raw . name . split ( '.' ). pop () ? . toUpperCase () || '文件'
2026-09-11 16:17:24 +08:00
if ( raw . type . startsWith ( 'image/' ) || /\.(jpe?g|png|bmp)$/i . test ( raw . name )) item . previewUrl = URL . createObjectURL ( raw )
2026-07-30 17:59:42 +08:00
}
2026-08-04 09:05:01 +08:00
function removeSelectedFile ( type : MerchantQualificationType ) {
const item = qualificationDraftItem ( type )
if ( ! item ) return
2026-08-04 10:31:53 +08:00
releaseQualificationPreview ( item )
2026-08-04 09:05:01 +08:00
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . error = ''
item . file = null
item . inputVersion += 1
item . uploadState = 'idle'
2026-07-30 17:59:42 +08:00
}
function resetQualificationDraft () {
2026-08-04 09:05:01 +08:00
qualificationDrafts . forEach (( item ) => {
2026-08-04 10:31:53 +08:00
releaseQualificationPreview ( item )
2026-08-04 09:05:01 +08:00
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . error = ''
item . file = null
item . inputVersion += 1
item . uploadState = 'idle'
})
qualificationCertificateNo . value = store . company . creditCode
2026-07-30 17:59:42 +08:00
}
2026-09-11 16:17:24 +08:00
async function submitQualification ( closeDialog = true ) {
if ( qualificationSubmitting . value ) return false
2026-08-03 18:15:34 +08:00
if ( ! canUploadQualification . value ) {
qualificationOpen . value = false
await loadCompanyContext ()
ElMessage . warning ( qualificationUploadDisabledReason . value )
2026-09-11 16:17:24 +08:00
return false
2026-08-03 18:15:34 +08:00
}
2026-08-04 21:01:34 +08:00
if ( ! requiredPackageComplete . value ) {
2026-08-04 09:05:01 +08:00
const businessLicense = qualificationDraftItem ( 'BUSINESS_LICENSE' )
2026-08-04 21:01:34 +08:00
if ( businessLicense && ! qualificationCreditCodeReady . value ) businessLicense . error = qualificationCreditCodeError . value
ElMessage . warning ( `请完整保存必填业务项: ${ missingRequiredPackageItems . value . join ( '、' ) } ` )
2026-09-11 16:17:24 +08:00
return false
2026-08-04 09:05:01 +08:00
}
2026-09-02 13:34:56 +08:00
const optionalItems = qualificationDrafts . filter (( item ) => ! item . required
&& item . file
&& ( item . value !== 'LEGAL_PERSON_ID' || ! requiredPackageChangeSelected . value ))
2026-09-01 19:34:37 +08:00
const includedLegalPersonId = legalPersonIdFileSelected . value
2026-07-30 17:59:42 +08:00
qualificationSubmitting . value = true
2026-08-04 09:05:01 +08:00
let uploadedCount = 0
const failures : string [] = []
2026-07-30 17:59:42 +08:00
try {
2026-08-05 09:11:41 +08:00
if ( requiredPackageChangeSelected . value ) {
const businessLicense = qualificationDraftItem ( 'BUSINESS_LICENSE' )
const legalPersonId = qualificationDraftItem ( 'LEGAL_PERSON_ID' )
2026-09-01 19:34:37 +08:00
for ( const item of [ businessLicense , ...( legalPersonId ? . file ? [ legalPersonId ] : [])]) {
2026-08-05 09:11:41 +08:00
if ( ! item ) continue
item . error = ''
item . uploadState = 'uploading'
}
try {
await uploadMerchantRequiredQualificationPackage (
businessLicense ? . file as File ,
2026-09-01 19:34:37 +08:00
legalPersonId ? . file || undefined ,
2026-08-05 09:11:41 +08:00
normalizedQualificationCertificateNo . value ,
Boolean ( replaceableQualification ( 'BUSINESS_LICENSE' )),
2026-09-01 19:34:37 +08:00
legalPersonId ? . file ? Boolean ( replaceableQualification ( 'LEGAL_PERSON_ID' )) : undefined ,
2026-08-05 09:11:41 +08:00
)
2026-09-01 19:34:37 +08:00
const uploadedRequiredItems = [ businessLicense , ...( legalPersonId ? . file ? [ legalPersonId ] : [])]
for ( const item of uploadedRequiredItems ) {
2026-08-05 09:11:41 +08:00
if ( ! item ) continue
releaseQualificationPreview ( item )
item . file = null
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . inputVersion += 1
item . uploadState = 'uploaded'
}
2026-09-01 19:34:37 +08:00
uploadedCount += uploadedRequiredItems . filter ( Boolean ). length
2026-08-05 09:11:41 +08:00
} catch ( error ) {
const message = qualificationUploadErrorMessage ( error )
if ( businessLicense ) { businessLicense . error = message ; businessLicense . uploadState = 'error' }
2026-09-01 19:34:37 +08:00
if ( legalPersonId ? . file ) { legalPersonId . error = message ; legalPersonId . uploadState = 'error' }
2026-08-05 09:11:41 +08:00
failures . push ( `必填资质包: ${ message } ` )
}
}
for ( const item of failures . length ? [] : optionalItems ) {
2026-08-04 09:05:01 +08:00
const file = item . file
if ( ! file ) continue
item . error = ''
item . uploadState = 'uploading'
try {
await uploadMerchantQualification (
file ,
item . value ,
2026-08-05 09:11:41 +08:00
undefined ,
2026-08-04 09:05:01 +08:00
Boolean ( replaceableQualification ( item . value )),
)
2026-08-04 10:31:53 +08:00
releaseQualificationPreview ( item )
2026-08-04 09:05:01 +08:00
item . file = null
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . inputVersion += 1
item . uploadState = 'uploaded'
uploadedCount += 1
} catch ( error ) {
2026-08-04 14:15:24 +08:00
const message = qualificationUploadErrorMessage ( error )
2026-08-04 09:05:01 +08:00
item . error = message
item . uploadState = 'error'
failures . push ( ` ${ item . label } : ${ message } ` )
2026-08-03 20:50:32 +08:00
}
2026-08-03 18:15:34 +08:00
}
2026-08-03 20:50:32 +08:00
const refreshed = await refreshCompanyContext ()
2026-08-04 09:05:01 +08:00
if ( failures . length ) {
if ( ! canUploadQualification . value ) qualificationOpen . value = false
const uploadedCopy = uploadedCount ? `已保存 ${ uploadedCount } 项;` : ''
ElMessage . error ( ` ${ uploadedCopy }${ failures [ 0 ] } ` )
2026-09-11 16:17:24 +08:00
return false
2026-08-04 09:05:01 +08:00
}
2026-09-11 16:17:24 +08:00
if ( closeDialog ) qualificationOpen . value = false
2026-09-01 19:34:37 +08:00
if ( refreshed ) ElMessage . success ( includedLegalPersonId
2026-09-02 13:34:56 +08:00
? ( requiredPackageChangeSelected . value
? '营业执照、统一社会信用代码和法人身份证已保存,尚未提交审核'
: '法人身份证已保存,尚未提交审核' )
: '资质文件已保存,尚未提交审核' )
2026-08-04 09:05:01 +08:00
else ElMessage . warning ( '资质文件已保存,但最新状态刷新失败,请点击“重新加载”确认状态' )
2026-09-11 16:17:24 +08:00
return refreshed
2026-07-30 17:59:42 +08:00
} finally {
qualificationSubmitting . value = false
}
}
2026-09-11 16:17:24 +08:00
async function submitQualificationAndReview () {
if ( qualificationSubmitting . value || reviewSubmitting . value ) return
if ( selectedQualificationCount . value > 0 && ! await submitQualification ( false )) return
if ( ! canSubmitQualificationReview . value ) {
ElMessage . warning ( qualificationReviewDisabledReason . value )
return
}
await submitQualificationReview ()
if ( store . company . auditStatus === 'PENDING' ) qualificationOpen . value = false
}
2026-08-03 20:50:32 +08:00
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
}
}
2026-09-11 16:17:24 +08:00
function qualificationDeleteDisabled ( record : TenantQualification ) {
return deletingQualificationId . value === record . id
|| ! auth . hasPermission ( 'company:qualification:upload' )
|| ! canUploadQualification . value
}
async function deleteQualification ( record : TenantQualification ) {
if ( qualificationDeleteDisabled ( record )) return
try {
await ElMessageBox . confirm (
`确认删除资质文件“ ${ record . fileName } ”?删除后如需送审必须重新上传。` ,
'删除资质文件' ,
{ type : 'warning' , confirmButtonText : '确认删除' , cancelButtonText : '取消' },
)
} catch {
return
}
deletingQualificationId . value = record . id
try {
await store . deleteQualification ( record . id )
ElMessage . success ( '资质文件已删除' )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '资质文件删除失败' )
} finally {
deletingQualificationId . value = ''
}
}
2026-07-30 17:59:42 +08:00
async function previewQualification ( record : TenantQualification ) {
2026-08-03 18:15:34 +08:00
if ( previewUrl . value ) URL . revokeObjectURL ( previewUrl . value )
2026-07-30 17:59:42 +08:00
previewRecord . value = record
previewOpen . value = true
2026-08-03 18:15:34 +08:00
previewUrl . value = ''
previewError . value = ''
if ( ! record . documentUrl ) {
previewError . value = '当前资质记录没有可查看的文件地址,请联系平台管理员处理。'
return
}
2026-07-30 17:59:42 +08:00
previewLoading . value = true
try {
const blob = await downloadQualificationImage ( record . documentUrl )
2026-08-03 20:50:32 +08:00
previewKind . value = blob . type === 'application/pdf' || record . fileName . toLowerCase (). endsWith ( '.pdf' ) ? 'pdf' : 'image'
2026-07-30 17:59:42 +08:00
previewUrl . value = URL . createObjectURL ( blob )
} catch ( error ) {
2026-08-03 18:15:34 +08:00
previewError . value = error instanceof Error && error . message === '资质文件地址无效'
? '当前资质文件尚未迁移到安全预览地址,请联系平台管理员处理。'
: '资质文件加载失败,请稍后重试。'
ElMessage . error ( previewError . value )
2026-07-30 17:59:42 +08:00
} finally {
previewLoading . value = false
}
}
function closeQualificationPreview () {
if ( previewUrl . value ) URL . revokeObjectURL ( previewUrl . value )
previewUrl . value = ''
2026-08-03 20:50:32 +08:00
previewKind . value = 'image'
2026-08-03 18:15:34 +08:00
previewError . value = ''
2026-07-30 17:59:42 +08:00
previewRecord . value = null
}
</ script >
< template >
< section v-loading = "loading" class="profile-page" >
2026-07-31 15:34:22 +08:00
< div v-if = "loadError" class="client-card profile-load-error" role="alert" >
< div >< strong > 企业资料加载失败 </ strong >< span >{{ loadError }}</ span ></ div >
< el-button :loading = "loading" @click ="loadCompanyContext" > 重新加载 </ el -button >
</ div >
< template v-else >
2026-07-30 17:59:42 +08:00
< div class = "company-audit-notice" : class = "companyApproved ? 'is-approved' : 'is-pending'" role = "status" >
< component : is = "companyApproved ? CheckCircle2 : Clock3" :size = "21" />
< div >< strong >{{ auditStatusCopy . title }}</ strong >< span >{{ auditStatusCopy . detail }}</ span ></ div >
</ div >
< el-tabs v-model = "activeTab" class="profile-tabs" >
< el-tab-pane name = "basic" >
< template # label >< span class = "tab-label" >< Building2 :size = "16" /> 企业基本信息 </ span ></ template >
< div class = "client-card info-panel" >
2026-08-14 16:06:33 +08:00
< header class = "info-panel-header" >< h3 > 企业基本信息 </ h3 >< el-button v-if = "auth.hasPermission('company:profile:edit') && store.company.auditStatus !== 'ARCHIVED'" type="primary" @click="startEdit"><Pencil :size="16" /> 编辑企业资料 </ el-button ></ header >
2026-07-30 17:59:42 +08:00
< dl class = "profile-grid" >
2026-07-31 15:34:22 +08:00
< div >< dt > 公司名称 </ dt >< dd >{{ store . company . companyName || '-' }}</ dd ></ div >
< div >< dt > 统一社会信用代码 </ dt >< dd >{{ store . company . creditCode || '-' }}</ dd ></ div >
2026-08-17 19:52:08 +08:00
< div >< dt > 企业类型 </ dt >< dd >{{ store . company . enterpriseType || '-' }}</ dd ></ div >
< div >< dt > 注册地 </ dt >< dd >{{ store . company . registeredAddress || '-' }}</ dd ></ div >
< div >< dt > 成立日期 </ dt >< dd >{{ store . company . establishedDate || '-' }}</ dd ></ div >
< div >< dt > 注册资本 </ dt >< dd >{{ store . company . registeredCapital == null ? '-' : ` ${ store . company . registeredCapital } ${ store . company . registeredCapitalCurrency } ` . trim () }}</ dd ></ div >
2026-07-31 15:34:22 +08:00
< div >< dt > 法人姓名 </ dt >< dd >{{ store . company . legalRepresentative || '-' }}</ dd ></ div >
2026-07-30 17:59:42 +08:00
</ dl >
</ div >
</ el-tab-pane >
< el-tab-pane name = "contact" >
< template # label >< span class = "tab-label" >< UserRound :size = "16" /> 联系信息 </ span ></ template >
< div class = "client-card info-panel" >
2026-08-03 10:51:53 +08:00
< header class = "info-panel-header" >< h3 > 联系信息 </ h3 ></ header >
2026-07-30 17:59:42 +08:00
< dl class = "profile-grid" >
2026-07-31 15:34:22 +08:00
< div >< dt > 联系人 </ dt >< dd >{{ store . company . contactName || '-' }}</ dd ></ div >
2026-09-02 10:04:21 +08:00
< div >< dt > 公司联系电话 </ dt >< dd >{{ store . company . contactPhone || '-' }}</ dd ></ div >
2026-07-31 15:34:22 +08:00
< div >< dt > 联系邮箱 </ dt >< dd >{{ store . company . contactEmail || '-' }}</ dd ></ div >
2026-08-17 19:52:08 +08:00
< div >< dt > 联系微信 </ dt >< dd >{{ store . company . contactWechat || '-' }}</ dd ></ div >
2026-07-30 17:59:42 +08:00
</ dl >
</ div >
</ el-tab-pane >
< el-tab-pane name = "qualification" >
< template # label >< span class = "tab-label" >< FileCheck2 :size = "16" /> 资质文件 </ span ></ template >
< div class = "client-card qualification-panel" >
2026-08-03 18:15:34 +08:00
< header >
< div >< h3 > 企业资质 </ h3 >< p >{{ qualificationPanelHint }}</ p ></ div >
2026-08-03 20:50:32 +08:00
< 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 >
2026-08-03 18:15:34 +08:00
</ header >
2026-08-01 12:17:33 +08:00
< DataTable
v -model:page = "qualificationPage"
v -model:page-size = "qualificationPageSize"
class = "desktop-qualification-table"
:columns = "qualificationColumns"
:data = "pagedQualifications"
:empty-text = "'暂无企业资质记录'"
: page -sizes ="[ 10 , 20 , 50 ]"
:total = "store.qualifications.length"
row -key =" id "
>
2026-09-11 16:17:24 +08:00
< template # actions = "{ row }" >
< el-button link type = "primary" @click ="previewQualification(row as TenantQualification)" > 查看 </ el -button >
< el-button link type = "danger" : loading = "deletingQualificationId === (row as TenantQualification).id" : disabled = "qualificationDeleteDisabled(row as TenantQualification)" @click ="deleteQualification(row as TenantQualification)" > 删除 </ el -button >
</ template >
2026-08-03 20:50:32 +08:00
< template # status = "{ row }" >< el-tag :type = "qualificationStatusMeta(row.status).type" effect = "light" >{{ qualificationStatusMeta ( row . status ). label }}</ el-tag ></ template >
2026-08-01 12:17:33 +08:00
</ DataTable >
2026-07-30 17:59:42 +08:00
< div class = "mobile-qualification-list" >
< article v-for = "record in store.qualifications" :key="record.id" >
2026-08-03 20:50:32 +08:00
< header >< strong >{{ record . type }}</ strong >< el-tag :type = "qualificationStatusMeta(record.status).type" effect = "light" >{{ qualificationStatusMeta ( record . status ). label }}</ el-tag ></ header >
2026-07-30 17:59:42 +08:00
< 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 >
2026-09-11 16:17:24 +08:00
< div class = "mobile-qualification-actions" >
< el-button link type = "primary" @click ="previewQualification(record)" > 查看文件信息 </ el -button >
< el-button link type = "danger" : loading = "deletingQualificationId === record.id" :disabled = "qualificationDeleteDisabled(record)" @click ="deleteQualification(record)" > 删除 </ el -button >
</ div >
2026-07-30 17:59:42 +08:00
</ article >
2026-07-31 15:34:22 +08:00
< div v-if = "store.qualifications.length === 0" class="client-empty" > 暂无企业资质记录 。 </ div >
2026-07-30 17:59:42 +08:00
</ div >
</ div >
</ el -tab-pane >
</ el-tabs >
< el-drawer v-model = "editing" title="编辑企业资料" size="min(620px, 94vw)" @closed="cancelEdit" >
2026-08-01 12:17:33 +08:00
< el-form ref = "profileFormRef" :model = "draft" :rules = "profileFormRules" label -position =" top " class = "profile-form" >
2026-07-30 17:59:42 +08:00
< h3 > 企业基本信息 </ h3 >
< div class = "form-grid-two" >
2026-08-01 12:17:33 +08:00
< el-form-item label = "公司名称" prop = "companyName" required >< el-input v-model = "draft.companyName" placeholder="请输入公司名称" /></ el-form-item >
2026-07-30 17:59:42 +08:00
< el-form-item label = "统一社会信用代码" >< el-input v-model = "draft.creditCode" disabled /></ el-form-item >
2026-08-01 12:17:33 +08:00
< el-form-item label = "法人姓名" prop = "legalRepresentative" required >< el-input v-model = "draft.legalRepresentative" placeholder="请输入法定代表人姓名" /></ el-form-item >
2026-07-30 17:59:42 +08:00
</ div >
< h3 > 联系信息 </ h3 >
< div class = "form-grid-two" >
2026-08-01 12:17:33 +08:00
< el-form-item label = "联系人" prop = "contactName" required >< el-input v-model = "draft.contactName" placeholder="请输入联系人姓名" /></ el-form-item >
2026-09-02 10:15:51 +08:00
< el-form-item label = "公司联系电话" prop = "contactPhone" required >< el-input v-model = "draft.contactPhone" maxlength="32" inputmode="tel" placeholder="请输入公司联系电话" /><small class="profile-field-hint" > 仅用于公司业务联系 , 不会修改任何成员的登录手机号 。 </ small > </ el -form-item >
2026-08-01 12:17:33 +08:00
< el-form-item label = "联系邮箱" prop = "contactEmail" >< el-input v-model = "draft.contactEmail" placeholder="请输入联系邮箱(选填)" /></ el-form-item >
2026-07-30 17:59:42 +08:00
</ div >
</ el-form >
< template # footer >< div class = "drawer-actions" >< el-button :disabled = "profileSubmitting" @click ="cancelEdit" > 取消 </ el -button >< el-button type = "primary" :loading = "profileSubmitting" @click ="saveProfile" > 保存修改 </ el -button ></ div ></ template >
</ el-drawer >
2026-08-04 09:05:01 +08:00
< el-dialog
v-model = "qualificationOpen"
title = "更新资质文件"
width = "min(760px, 92vw)"
:close-on-click-modal = "!qualificationSubmitting"
:close-on-press-escape = "!qualificationSubmitting"
:show-close = "!qualificationSubmitting"
@closed ="resetQualificationDraft"
>
< div class = "qualification-dialog-intro" >
2026-08-04 21:01:34 +08:00
< strong > 企业认证必填业务项 </ strong >
2026-09-01 19:34:37 +08:00
< span > 营业执照和18位统一社会信用代码需共同保存 ; 法人身份证及其他资质可按需上传 。 </ span >
2026-08-04 09:05:01 +08:00
</ div >
< div class = "qualification-upload-grid" >
< article v-for = "item in qualificationDrafts" :key="item.value" class="qualification-upload-item" >
< header >
< div >
2026-08-04 14:15:24 +08:00
< strong >{{ item . label }} ( {{ item . required ? '必填' : '选填' }} ) </ strong >
2026-08-04 09:05:01 +08:00
< p >{{ item . description }}</ p >
</ div >
< el-tag :type = "qualificationSlotTagType(item)" effect = "light" >{{ qualificationSlotLabel ( item ) }}</ el-tag >
</ header >
< div v-if = "qualificationRecord(item.value)" class="existing-qualification-file" >
< FileCheck2 :size = "16" />
< span > 当前文件 : {{ qualificationRecord ( item . value ) ? . fileName }}</ span >
</ div >
< el-upload
:key = "`${item.value}-${item.inputVersion}`"
:auto-upload = "false"
:show-file-list = "false"
2026-09-11 16:17:24 +08:00
accept = ".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp,image/x-ms-bmp"
2026-08-04 09:05:01 +08:00
@change ="handleQualificationFileChange(item.value, $event)"
>
< el-button :disabled = "qualificationSubmitting" >< Upload :size = "16" />{{ qualificationRecord ( item . value ) ? '替换文件' : '选择文件' }}</ el-button >
2026-09-11 16:17:24 +08:00
< template # tip >< div class = "upload-tip" > 支持 PDF 、 JPG 、 PNG 、 BMP , 单个文件不超过 5 MB 。 </ div ></ template >
2026-07-30 17:59:42 +08:00
</ el-upload >
2026-08-04 09:05:01 +08:00
< div v-if = "item.fileName" class="selected-qualification-file" aria-live="polite" >
2026-08-04 10:31:53 +08:00
< a v-if = "item.previewUrl" class="selected-file-preview" :href="item.previewUrl" target="_blank" rel="noopener" :aria-label="`查看${item.label}图片预览`" >
< img :src = "item.previewUrl" :alt = "`${item.label}待上传图片预览`" />
</ a >
< div v-else class = "selected-file-type" aria -hidden =" true " >< FileText :size = "18" />< span > PDF </ span ></ div >
< div class = "selected-file-meta" >< strong >{{ item . fileName }}</ strong >< span >{{ item . fileType }} · {{ formatQualificationFileSize ( item . fileSize ) }}</ span ></ div >
2026-08-04 09:05:01 +08:00
< el-button link type = "danger" :disabled = "qualificationSubmitting" :aria-label = "`移除${item.label}文件`" @click ="removeSelectedFile(item.value)" >< Trash2 :size = "16" /> 移除 </ el-button >
2026-07-30 17:59:42 +08:00
</ div >
2026-08-04 09:05:01 +08:00
< p v-if = "item.error" class="qualification-upload-error" role="alert" >{{ item.error }}</ p >
</ article >
</ div >
2026-08-04 21:01:34 +08:00
< el-form-item label = "统一社会信用代码" class = "qualification-credit-code" required : error = "qualificationCreditCodeReady ? '' : qualificationCreditCodeError" >
< el-input v-model = "qualificationCertificateNo" maxlength="18" :disabled="companyCreditCodeValid" placeholder="请输入18位统一社会信用代码" @input="qualificationCertificateNo = String($event).toUpperCase()" />
< small class = "qualification-credit-code-hint" >{{ companyCreditCodeValid ? '已登记的统一社会信用代码不可直接修改。' : '与营业执照一致,保存后不可直接修改。' }}</ small >
2026-08-04 09:05:01 +08:00
</ el-form-item >
2026-08-04 10:31:53 +08:00
< p v-if = "qualificationDialogHint" class="qualification-dialog-hint" >{{ qualificationDialogHint }}</ p >
2026-09-11 16:17:24 +08:00
< template # footer >< el-button : disabled = "qualificationSubmitting || reviewSubmitting" @click ="qualificationOpen = false" > 取消 </ el -button >< el-button :loading = "qualificationSubmitting" : disabled = "!canSaveQualificationBatch || reviewSubmitting" @click ="submitQualification()" > 保存草稿 </ el -button >< el-button type = "primary" : loading = "qualificationSubmitting || reviewSubmitting" : disabled = "selectedQualificationCount > 0 ? !canSaveQualificationBatch : !canSubmitQualificationReview" @click ="submitQualificationAndReview" > 提交审核 </ el -button ></ template >
2026-07-30 17:59:42 +08:00
</ 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" >
2026-08-03 20:50:32 +08:00
< 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" />
2026-07-30 17:59:42 +08:00
< FileCheck2 :size = "42" />
< strong >{{ previewRecord . fileName }}</ strong >
2026-08-03 20:50:32 +08:00
< el-tag :type = "qualificationStatusMeta(previewRecord.status).type" effect = "light" >{{ qualificationStatusMeta ( previewRecord . status ). label }}</ el-tag >
2026-07-30 17:59:42 +08:00
< 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 >
2026-08-03 18:15:34 +08:00
< p v-if = "previewError" class="document-preview-error" role="alert" >{{ previewError }}</ p >
< span v-else >{{ previewUrl ? ' 文件通过当前商户会话临时加载 , 关闭弹窗后立即释放 。' : ' 当前文件暂不可预览 。' }}</ span >
2026-07-30 17:59:42 +08:00
</ div >
</ el -dialog >
2026-07-31 15:34:22 +08:00
</ template >
2026-07-30 17:59:42 +08:00
</ section >
</ template >
< style scoped >
. profile - page { display : flex ; flex - direction : column ; gap : 16 px ; }
2026-07-31 15:34:22 +08:00
. profile - load - error { display : flex ; min - height : 180 px ; align - items : center ; justify - content : center ; gap : 22 px ; text - align : left ; }
. profile - load - error > div { display : flex ; max - width : 560 px ; flex - direction : column ; gap : 6 px ; }
. profile - load - error span { color : var ( -- muted ); font - size : 12 px ; line - height : 1.6 ; }
2026-07-30 17:59:42 +08:00
. company - audit - notice { display : flex ; align - items : flex - start ; gap : 12 px ; padding : 15 px 17 px ; border : 1 px solid ; }
. company - audit - notice svg { flex : 0 0 21 px ; margin - top : 1 px ; }
. company - audit - notice div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 4 px ; }
. company - audit - notice strong { color : var ( -- navy ); font - size : 13 px ; }
. 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 ; }
2026-08-03 20:50:32 +08:00
. qualification - preview - frame { display : block ; width : 100 % ; min - height : min ( 60 vh , 560 px ); margin : 0 0 14 px ; border : 0 ; background : # fff ; }
2026-07-30 17:59:42 +08:00
. company - audit - notice . is - pending { border - color : # e5d2a5 ; background : # fff9eb ; color : # 9 a6a12 ; }
2026-08-03 10:51:53 +08:00
. qualification - panel header p { margin : 6 px 0 0 ; color : var ( -- muted ); font - size : 12 px ; }
2026-07-30 17:59:42 +08:00
. profile - tabs : deep (. el - tabs__header ) { margin - bottom : 14 px ; padding : 0 16 px ; border : 1 px solid var ( -- border ); background : # fff ; }
. profile - tabs : deep (. el - tabs__nav - wrap :: after ) { display : none ; }
. tab - label { display : inline - flex ; align - items : center ; gap : 7 px ; }
. info - panel { padding : 0 ; }
2026-08-03 10:51:53 +08:00
. info - panel - header { min - height : 66 px ; padding : 14 px 22 px ; border - bottom : 1 px solid var ( -- border ); }
. info - panel - header h3 { margin : 0 ; font - size : 15 px ; }
2026-07-30 17:59:42 +08:00
. profile - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); margin : 0 ; }
. profile - grid > div { min - height : 86 px ; padding : 20 px 22 px ; border - bottom : 1 px solid var ( -- border ); }
. profile - grid > div : nth - child ( odd ) { border - right : 1 px solid var ( -- border ); }
. profile - grid dt { color : var ( -- muted ); font - size : 11 px ; }
. profile - grid dd { margin : 9 px 0 0 ; color : var ( -- navy ); font - size : 14 px ; font - weight : 650 ; }
. profile - grid . full - row { grid - column : 1 / - 1 ; border - right : 0 ; }
. 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 ; }
2026-08-03 20:50:32 +08:00
. qualification - panel - actions { display : flex ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
2026-08-03 18:15:34 +08:00
. qualification - update - control { display : inline - flex ; }
2026-07-30 17:59:42 +08:00
. 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 ; }
. profile - form h3 : not ( : first - child ) { margin - top : 22 px ; }
2026-09-02 10:04:21 +08:00
. profile - field - hint { display : block ; margin - top : 5 px ; color : var ( -- muted ); font - size : 10 px ; line - height : 1.5 ; }
2026-07-30 17:59:42 +08:00
. form - grid - two { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 0 16 px ; }
. form - grid - two . full - row { grid - column : 1 / - 1 ; }
. drawer - actions { display : flex ; justify - content : flex - end ; gap : 8 px ; }
2026-09-11 16:17:24 +08:00
. mobile - qualification - actions { display : flex ; align - items : center ; gap : 8 px ; }
2026-08-04 09:05:01 +08:00
. qualification - dialog - intro { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 18 px ; margin - bottom : 16 px ; padding : 13 px 15 px ; background : # f4f7f7 ; }
. qualification - dialog - intro strong { color : var ( -- navy ); font - size : 13 px ; }
. qualification - dialog - intro span { max - width : 420 px ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.6 ; text - align : right ; }
. qualification - upload - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); border - top : 1 px solid var ( -- border ); border - left : 1 px solid var ( -- border ); }
. qualification - upload - item { min - width : 0 ; padding : 16 px ; border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
. qualification - upload - item > header { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; margin - bottom : 14 px ; }
. qualification - upload - item > header > div { min - width : 0 ; }
. qualification - upload - item > header strong { color : var ( -- navy ); font - size : 13 px ; }
. qualification - upload - item > header p { margin : 5 px 0 0 ; color : var ( -- muted ); font - size : 10 px ; line - height : 1.55 ; }
. qualification - upload - item > header . el - tag { flex : 0 0 auto ; }
. existing - qualification - file { display : flex ; min - width : 0 ; align - items : center ; gap : 7 px ; margin - bottom : 12 px ; color : var ( -- primary ); font - size : 10 px ; }
. existing - qualification - file svg { flex : 0 0 16 px ; }
. existing - qualification - file span { min - width : 0 ; overflow : hidden ; color : var ( -- muted ); text - overflow : ellipsis ; white - space : nowrap ; }
2026-07-30 17:59:42 +08:00
. upload - tip { margin - top : 8 px ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.55 ; }
. selected - qualification - file { display : flex ; width : 100 % ; min - width : 0 ; align - items : center ; gap : 10 px ; margin - top : 12 px ; padding : 12 px ; border : 1 px solid # cfe1dd ; background : # f3f8f7 ; color : var ( -- primary ); }
2026-08-04 10:31:53 +08:00
. selected - file - preview { display : block ; width : 54 px ; height : 54 px ; flex : 0 0 54 px ; overflow : hidden ; border : 1 px solid # bfd4cf ; background : # fff ; }
. selected - file - preview img { display : block ; width : 100 % ; height : 100 % ; object - fit : cover ; }
. selected - file - preview : focus - visible { outline : 2 px solid var ( -- primary ); outline - offset : 2 px ; }
. selected - file - type { display : grid ; width : 54 px ; height : 54 px ; flex : 0 0 54 px ; place - items : center ; align - content : center ; gap : 2 px ; border : 1 px solid # cfe1dd ; background : # fff ; }
. selected - file - type span { color : var ( -- muted ); font - size : 9 px ; font - weight : 700 ; }
. selected - file - meta { display : flex ; min - width : 0 ; flex : 1 ; flex - direction : column ; gap : 3 px ; }
2026-07-30 17:59:42 +08:00
. selected - qualification - file strong { overflow : hidden ; color : var ( -- navy ); font - size : 12 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. selected - qualification - file span { color : var ( -- muted ); font - size : 10 px ; }
2026-08-04 09:05:01 +08:00
. qualification - upload - error { margin : 9 px 0 0 ; color : # b42318 ; font - size : 11 px ; line - height : 1.5 ; }
. qualification - credit - code { max - width : 420 px ; margin : 18 px 0 0 ; }
2026-08-04 21:01:34 +08:00
. qualification - credit - code - hint { display : block ; margin - top : 6 px ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.5 ; }
2026-08-04 09:05:01 +08:00
. qualification - dialog - hint { margin : 10 px 0 0 ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.5 ; }
2026-07-30 17:59:42 +08:00
. document - preview { display : grid ; min - height : 260 px ; place - items : center ; align - content : center ; gap : 12 px ; border : 1 px dashed # b8c8c5 ; background : # f6faf9 ; color : var ( -- primary ); }
. document - preview > strong { max - width : min ( 420 px , calc ( 100 % - 32 px )); overflow - wrap : anywhere ; color : var ( -- navy ); text - align : center ; }
. document - preview dl { display : grid ; width : min ( 420 px , calc ( 100 % - 32 px )); grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); margin : 6 px 0 ; border - top : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
. document - preview dl div { min - width : 0 ; padding : 12 px ; text - align : center ; }
. document - preview dt { color : var ( -- muted ); font - size : 10 px ; }
. document - preview dd { margin : 5 px 0 0 ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 12 px ; font - weight : 650 ; }
. document - preview > span { max - width : 440 px ; padding : 0 16 px ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.6 ; text - align : center ; }
2026-08-03 18:15:34 +08:00
. document - preview - error { max - width : 440 px ; margin : 0 ; padding : 0 16 px ; color : # 9 a5a09 ; font - size : 11 px ; line - height : 1.6 ; text - align : center ; }
2026-07-30 17:59:42 +08:00
@ media ( max - width : 720 px ) {
. profile - grid , . form - grid - two { grid - template - columns : 1 fr ; }
2026-08-04 09:05:01 +08:00
. qualification - dialog - intro { flex - direction : column ; gap : 5 px ; }
. qualification - dialog - intro span { text - align : left ; }
. qualification - upload - grid { grid - template - columns : 1 fr ; }
2026-07-30 17:59:42 +08:00
. profile - grid > div : nth - child ( odd ) { border - right : 0 ; }
. qualification - panel > header { align - items : flex - start ; gap : 12 px ; }
2026-08-03 20:50:32 +08:00
. 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 % ; }
2026-07-30 17:59:42 +08:00
. 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 ; }
. mobile - qualification - list article { min - width : 0 ; padding : 14 px ; border : 1 px solid var ( -- border ); background : # fff ; }
. mobile - qualification - list article > header { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; }
. qualification - file - name { display : flex ; min - width : 0 ; align - items : center ; gap : 8 px ; margin : 14 px 0 ; color : var ( -- primary ); }
. qualification - file - name svg { flex : 0 0 16 px ; }
. qualification - file - name span { min - width : 0 ; overflow : hidden ; color : var ( -- navy ); font - size : 12 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. mobile - qualification - list dl { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 12 px ; margin : 0 0 10 px ; padding - top : 12 px ; border - top : 1 px solid var ( -- border ); }
. mobile - qualification - list dt { color : var ( -- muted ); font - size : 10 px ; }
. mobile - qualification - list dd { margin : 4 px 0 0 ; overflow - wrap : anywhere ; font - size : 11 px ; font - weight : 600 ; }
. document - preview dl { grid - template - columns : 1 fr ; }
. document - preview dl div + div { border - top : 1 px solid var ( -- border ); }
. selected - qualification - file { align - items : flex - start ; flex - wrap : wrap ; }
2026-08-04 10:31:53 +08:00
. selected - qualification - file > . el - button { margin - left : 64 px ; }
2026-07-30 17:59:42 +08:00
}
</ style >