@@ -1,7 +1,7 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance , FormRules , UploadFile , UploadInstance } from 'element-plus'
import type { FormInstance , FormRules , UploadFile } from 'element-plus'
import { Building2 , CheckCircle2 , Clock3 , FileCheck2 , FileText , Pencil , Trash2 , Upload , UserRound } from '@lucide/vue'
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
import { useTenantAdministrationStore , type TenantCompanyProfile , type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
@@ -14,7 +14,6 @@ import {
uploadMerchantQualification ,
type MerchantQualificationType ,
} from '@/modules/tenant-management/api/merchantIam'
import { ApiError } from '@/api/http'
const store = useTenantAdministrationStore ( )
const auth = useAuthStore ( )
@@ -36,47 +35,98 @@ const previewKind = ref<'image' | 'pdf'>('image')
const previewError = ref ( '' )
const previewLoading = ref ( false )
const profileFormRef = ref < FormInstance > ( )
const qualificationFormRef = ref < FormInstance > ( )
const uploadRef = ref < UploadInstance > ( )
const qualificationPage = ref ( 1 )
const qualificationPageSize = ref ( 10 )
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 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 : '银行开户许可证' } ,
type QualificationUploadState = 'idle' | 'uploading' | 'uploaded' | 'error'
type QualificationTypeDefinition = { value : MerchantQualificationType ; label : string ; description : string }
type QualificationDraftItem = QualificationTypeDefinition & {
file : File | null
fileName : string
fileSize : number
fileType : string
inputVersion : number
error : string
uploadState : QualificationUploadState
}
const qualificationTypes : QualificationTypeDefinition [ ] = [
{ value : 'BUSINESS_LICENSE' , label : '营业执照' , description : '上传清晰完整的营业执照文件。' } ,
{ value : 'LEGAL_PERSON_ID' , label : '法人身份证' , description : '正反面请合并为一个 PDF、JPG 或 PNG 文件。' } ,
{ value : 'FREIGHT_FORWARDER_CERTIFICATE' , label : '货代资质证书' , description : '上传当前有效的货代资质证明。' } ,
{ value : 'BANK_ACCOUNT_LICENSE' , label : '银行开户许可证' , description : '上传企业对公账户开户证明。' } ,
]
const qualificationDraft = reactive ( { type : 'BUSINESS_LICENSE' as MerchantQualificationType , fileName : '' , certificateNo : '' } )
const qualificationDrafts = reactive < QualificationDraftItem [ ] > ( qualificationTypes . map ( ( type ) => ( {
... type ,
file : null ,
fileName : '' ,
fileSize : 0 ,
fileType : '' ,
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 ( ) )
const businessLicenseFileSelected = computed ( ( ) => Boolean (
qualificationDrafts . find ( ( item ) => item . value === 'BUSINESS_LICENSE' ) ? . file ,
) )
const qualificationCreditCodeReady = computed ( ( ) => companyCreditCodeValid . value
|| ( businessLicenseFileSelected . value && unifiedSocialCreditCodePattern . test ( normalizedQualificationCertificateNo . value ) ) )
const qualificationCreditCodeError = computed ( ( ) => {
if ( qualificationCreditCodeReady . value ) return ''
if ( ! businessLicenseFileSelected . value ) return '请重新选择营业执照,并填写18位统一社会信用代码'
return '请输入18位字母或数字组成的统一社会信用代码'
} )
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 '当前企业已审核通过,资质替换请联系平台管理员处理。'
if ( store . company . auditStatus === 'DISABLED' ) return '企业账号已停用,暂不能提交资质文件。'
return ''
} )
const missingQualificationTypes = computed ( ( ) => qualificationTypes
. filter ( ( type ) => {
const draftItem = qualificationDrafts . find ( ( item ) => item . value === type . value )
return ! uploadedQualificationTypes . value . has ( type . value )
&& ! draftItem ? . file
&& draftItem ? . uploadState !== 'uploaded'
} )
. map ( ( type ) => type . label ) )
const selectedQualificationCount = computed ( ( ) => qualificationDrafts . filter ( ( item ) => Boolean ( item . file ) ) . length )
const canSaveQualificationBatch = computed ( ( ) => canUploadQualification . value
&& selectedQualificationCount . value > 0
&& missingQualificationTypes . value . length === 0
&& qualificationCreditCodeReady . value )
const qualificationSaveDisabledReason = computed ( ( ) => {
if ( ! canUploadQualification . value ) return qualificationUploadDisabledReason . value
if ( missingQualificationTypes . value . length ) return ` 还需选择: ${ missingQualificationTypes . value . join ( '、' ) } `
if ( ! qualificationCreditCodeReady . value ) return qualificationCreditCodeError . value
if ( ! selectedQualificationCount . value ) return '请选择需要新增或替换的资质文件'
return ''
} )
const canSubmitQualificationReview = computed ( ( ) => canUploadQualification . value
&& companyCreditCodeValid . value
&& qualificationTypes . every ( ( type ) => uploadedQualificationTypes . value . has ( type . value ) ) )
const qualificationReviewDisabledReason = computed ( ( ) => {
if ( ! canUploadQualification . value ) return qualificationUploadDisabledReason . value
if ( ! companyCreditCodeValid . value ) return '请先通过营业执照填写18位统一社会信用代码'
const missing = qualificationTypes . filter ( ( type ) => ! uploadedQualificationTypes . value . has ( type . value ) ) . map ( ( type ) => type . label )
return missing . length ? ` 请先上传: ${ missing . join ( '、' ) } ` : ''
} )
const qualificationPanelHint = computed ( ( ) => canUploadQualification . value
? '请先上传 四类必填资质;文件上传 不会自动送审,全部齐全后再提交企业审核。'
? '一次选择 四类必填资质;文件保存 不会自动送审,全部齐全后再提交企业审核。'
: qualificationUploadDisabledReason . value )
const profileFormRules : FormRules < TenantCompanyProfile > = {
companyName : [ { required : true , whitespace : true , message : '请输入公司名称' , trigger : 'blur' } ] ,
@@ -85,9 +135,6 @@ const profileFormRules: FormRules<TenantCompanyProfile> = {
contactPhone : [ { required : true , whitespace : true , message : '请输入联系电话' , trigger : 'blur' } ] ,
contactEmail : [ { type : 'email' , message : '请输入正确的联系邮箱' , trigger : 'blur' } ] ,
}
const qualificationFormRules : FormRules = {
fileName : [ { required : true , message : '请选择资质文件' , trigger : 'change' } ] ,
}
const qualificationColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 90 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'type' , prop : 'type' , label : '文件类型' , minWidth : 150 } ,
@@ -159,48 +206,97 @@ function openQualificationDialog() {
qualificationOpen . value = true
}
function handleFileChange ( file : UploadFile ) {
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 )
}
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 '需重新上传'
return '待上传'
}
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'
return 'warning'
}
function handleQualificationFileChange ( type : MerchantQualificationType , file : UploadFile ) {
const item = qualificationDraftItem ( type )
if ( ! item ) return
const raw = file . raw
if ( ! raw ) {
qualificationFileError . value = '未能读取所选文件,请重新选择'
item . error = '未能读取所选文件,请重新选择'
item . uploadState = 'error'
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . file = null
item . inputVersion += 1
return
}
const error = validateQualificationFile ( { name : raw . name , size : raw . size , type : raw . type } )
if ( error ) {
qualificationFileError . value = error
qualificationDraft . fileName = ''
selectedFileSize . value = 0
selectedFileType . value = ''
selectedFile . value = null
uploadRef . value ? . clearFiles ( )
item . error = error
item . uploadState = 'error '
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . file = null
item . inputVersion += 1
return
}
qualificationFileError . value = ''
selectedFile . value = raw
qualificationDraft . fileName = raw . name
selectedFileSize . value = raw . size
selectedFileType . value = raw . type || raw . name . split ( '.' ) . pop ( ) ? . toUpperCase ( ) || '文件'
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 ( ) || '文件'
}
function removeSelectedFile ( ) {
uploadRef . value ? . clearFiles ( )
qualificationDraft . fileName = ''
selectedFileSize . value = 0
selectedFileType . value = ''
qualificationFileError . value = ''
selectedFile . value = null
function removeSelectedFile ( type : MerchantQualificationType ) {
const item = qualificationDraftItem ( type )
if ( ! item ) return
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . error = ''
item . file = null
item . inputVersion += 1
item . uploadState = 'idle'
}
function resetQualificationDraft ( ) {
uploadRef . value ? . clearFiles ( )
qualificationDraft . type = 'BUSINESS_LICENSE '
qualificationDraft . fileName = ''
qualificationDraft . certificateNo = store . company . creditCode
selectedFileSize . value = 0
selectedFileType . value = ''
qualificationFileError . value = ''
selectedFile . value = null
qualificationFormRef . value ? . clearValidate ( )
qualificationDrafts . forEach ( ( item ) => {
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . error = ''
item . file = null
item . inputVersion + = 1
item . uploadState = 'idle'
} )
qualificationCertificateNo . value = store . company . creditCode
}
async function submitQualification ( ) {
@@ -211,42 +307,64 @@ async function submitQualification() {
ElMessage . warning ( qualificationUploadDisabledReason . value )
return
}
const valid = await qualificationFormRef . value ? . validate ( ) . then ( ( ) => true ) . catch ( ( ) => false )
if ( ! valid || ! selectedFile . value ) {
qualificationFileError . value = '请选择资质文件'
if ( missingQualificationTypes . value . length ) {
qualificationDrafts . forEach ( ( item ) => {
if ( ! uploadedQualificationTypes . value . has ( item . value ) && ! item . file ) item . error = '请选择该项 资质文件'
} )
ElMessage . warning ( ` 请补齐: ${ missingQualificationTypes . value . join ( '、' ) } ` )
return
}
if ( qualificationFileError . value ) {
ElMessage . warning ( qualificationFileError . value )
if ( ! qualificationCreditCodeReady . value ) {
const businessLicense = qualificationDraftItem ( 'BUSINESS_LICENSE' )
if ( businessLicense ) businessLicense . error = qualificationCreditCodeError . value
ElMessage . warning ( qualificationCreditCodeError . value )
return
}
const pendingItems = qualificationDrafts . filter ( ( item ) => item . file )
if ( ! pendingItems . length ) {
ElMessage . warning ( '请选择需要新增或替换的资质文件' )
return
}
qualificationSubmitting . value = true
let uploadedCount = 0
const failures : string [ ] = [ ]
try {
for ( const item of pendingItems ) {
const file = item . file
if ( ! file ) continue
item . error = ''
item . uploadState = 'uploading'
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 ) ,
file ,
item . value ,
item . value === 'BUSINESS_LICENSE' ? normalizedQualificationCertificateNo . value : undefined ,
Boolean ( replaceableQualification ( item . value ) ) ,
)
qualificationOpen . value = false
item . file = null
item . fileName = ''
item . fileSize = 0
item . fileType = ''
item . inputVersion += 1
item . uploadState = 'uploaded'
uploadedCount += 1
} catch ( error ) {
if ( error instanceof ApiError && error . code === 'A0409' ) {
await loadCompanyContext ( )
qualificationOpen . value = false
ElMessage . warning ( '企业资料正在审核,不能再次提交资质文件' )
return
const message = error instanceof Error ? error . message : '资质文件上传失败'
item . error = message
item . uploadState = 'error'
failures . push ( ` ${ item . label } : ${ message } ` )
}
ElMessage . error ( error instanceof Error ? error . message : '资质文件上传失败' )
return
}
const refreshed = await refreshCompanyContext ( )
if ( refreshed ) ElMessage . success ( '资质文件已上传,尚未提交审核' )
else ElMessage . warning ( '资质文件已上传,但最新状态刷新失败,请点击“重新加载”确认状态' )
if ( failures . length ) {
if ( ! canUploadQualification . value ) qualificationOpen . value = false
const uploadedCopy = uploadedCount ? ` 已保存 ${ uploadedCount } 项; ` : ''
ElMessage . error ( ` ${ uploadedCopy } ${ failures [ 0 ] } ` )
return
}
qualificationOpen . value = false
if ( refreshed ) ElMessage . success ( ` 已保存 ${ uploadedCount } 项资质文件,尚未提交审核 ` )
else ElMessage . warning ( '资质文件已保存,但最新状态刷新失败,请点击“重新加载”确认状态' )
} finally {
qualificationSubmitting . value = false
}
@@ -397,23 +515,55 @@ function closeQualificationPreview() {
< 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 >
< 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 = "文件类型" 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 = ".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" @change ="handleFileCha nge ">
< el -button > < Upload :size = "16" / > 选择文件 < / el-button >
< el-dialog
v-model = "qualificationOpen"
title = "更新资质文件"
width = "min(760px, 92vw)"
:close-on-click-modal = "!qualificationSubmitti ng"
:close-on-press-escape = "!qualificationSubmitting"
:show-close = "!qualificationSubmitting"
@closed ="resetQualificationDraft"
>
< div class = "qualification-dialog-intro" >
< strong > 一次补齐四类必填资质 < / strong >
< span > 每类选择一个文件后统一保存 ; 文件保存不会自动提交企业审核 。 < / span >
< / div >
< div class = "qualification-upload-grid" >
< article v-for = "item in qualificationDrafts" :key="item.value" class="qualification-upload-item" >
< header >
< div >
< strong > { { item . label } } < / strong >
< 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"
accept = ".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png"
@change ="handleQualificationFileChange(item.value, $event)"
>
< el-button :disabled = "qualificationSubmitting" > < Upload :size = "16" / > { { qualificationRecord ( item . value ) ? '替换文件' : '选择文件' } } < / el-button >
< 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 " / >
< div > < strong > { { qualificationDraft . fileName } } < / strong > < span > { { selectedFileType } } · { { formatQualificationFileSize ( selectedFileSize ) } } < / span > < / div >
< el-button link type = "danger" aria -label = " 移除所选资质文件 " @click ="removeSelectedFile" > < Trash2 :size = "16" / > 移除 < / el-button >
< div v-if = "item .fileName" class="selected-qualification-file" aria-live="polite" >
< FileText :size = "18 " / >
< div > < strong > { { item . fileName } } < / strong > < span > { { item . fileType } } · { { formatQualificationFileSize ( item . fileSize ) } } < / span > < / div >
< el-button link type = "danger" :disabled = "qualificationSubmitting" :aria-label = "`移除${item.label}文件`" @click ="removeSelectedFile(item.value) " > < Trash2 :size = "16" / > 移除 < / el-button >
< / div >
< p v-if = "item.error" class="qualification-upload-error" role="alert" > {{ item.error }} < / p >
< / article >
< / div >
< el -form -item label = "统一社会信用代码" class = "qualification-credit-code" :required = "!companyCreditCodeValid" : error = "qualificationCreditCodeReady ? '' : qualificationCreditCodeError" >
< el-input v-model = "qualificationCertificateNo" maxlength="18" placeholder="请输入18位统一社会信用代码" @input="qualificationCertificateNo = String($event).toUpperCase()" / >
< / 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 >
< p v-if = "qualificationSaveDisabledReason" class="qualification-dialog-hint " > {{ qualificationSaveDisabledReason }} < / p >
< template # footer > < el-button :disabled = "qualificationSubmitting" @click ="qualificationOpen = false" > 取消 < / el -button > < el-button type = "primary" :loading = "qualificationSubmitting" :disabled = "!canSaveQualificationBatch" @click ="submitQualification" > 保存全部资质 < / el -button > < / template >
< / el-dialog >
< el-dialog v-model = "previewOpen" :title="`${previewRecord?.type || '资质文件'}详情`" width="min(560px, 92vw)" @closed="closeQualificationPreview" >
@@ -470,12 +620,28 @@ function closeQualificationPreview() {
. 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 ; }
. 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 ; }
. 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 ) ; }
. selected - qualification - file > svg { flex : 0 0 20 px ; }
. selected - qualification - file > div { display : flex ; min - width : 0 ; flex : 1 ; flex - direction : column ; gap : 3 px ; }
. 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 ; }
. 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 ; }
. qualification - dialog - hint { margin : 10 px 0 0 ; color : var ( -- muted ) ; font - size : 11 px ; line - height : 1.5 ; }
. 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 ) ; }
@@ -486,6 +652,9 @@ function closeQualificationPreview() {
. 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 ; }
@ media ( max - width : 720 px ) {
. profile - grid , . form - grid - two { grid - template - columns : 1 fr ; }
. qualification - dialog - intro { flex - direction : column ; gap : 5 px ; }
. qualification - dialog - intro span { text - align : left ; }
. qualification - upload - grid { 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 ; }
@@ -504,6 +673,6 @@ function closeQualificationPreview() {
. 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 ; }
. selected - qualification - file > . el - button { margin - left : 30 px ; }
. selected - qualification - file > . el - button { margin - left : 28 px ; }
}
< / style >