2026-07-30 17:59:42 +08:00
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { UploadFile , UploadInstance } from 'element-plus'
import { Building2 , CheckCircle2 , Clock3 , FileCheck2 , FileText , Pencil , Trash2 , Upload , UserRound } from '@lucide/vue'
import { useTenantAdministrationStore , type TenantCompanyProfile , type TenantQualification } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
import { formatQualificationFileSize , validateQualificationFile } from '@/utils/qualificationFile'
import { downloadQualificationImage , uploadBusinessLicense } from '@/api/merchantIam'
const store = useTenantAdministrationStore ()
const auth = useAuthStore ()
const companyApproved = computed (() => store . company . auditStatus === 'ACTIVE' )
const auditStatusCopy = computed (() => ({
PROFILE_REQUIRED : { label : '待完善资料' , title : '请上传营业执照' , detail : '营业执照提交审核前不能提交头程订单。' },
PENDING : { label : '审核中' , title : '企业资料正在等待平台审核' , detail : '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' },
ACTIVE : { label : '已认证' , title : '企业资料已审核,下单资格已开通' , detail : '您可以创建、保存并提交头程订单。' },
REJECTED : { label : '审核驳回' , title : '企业资料审核未通过' , detail : '请根据平台反馈修正资料;当前后端暂不支持同一企业重新上传。' },
DISABLED : { label : '企业已停用' , title : '企业账号已被平台停用' , detail : '当前不能提交订单,请联系平台客服核实。' },
})[ 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 ( '' )
const previewLoading = ref ( false )
const uploadRef = ref < UploadInstance >()
const selectedFileSize = ref ( 0 )
const selectedFileType = ref ( '' )
const qualificationFileError = ref ( '' )
const qualificationSubmitting = ref ( false )
const profileSubmitting = ref ( false )
const loading = ref ( false )
const selectedFile = ref < File | null >( null )
const draft = reactive < TenantCompanyProfile >({ ... store . company })
const qualificationDraft = reactive ({ type : '营业执照' , fileName : '' , certificateNo : '' })
onMounted ( async () => {
loading . value = true
try {
await store . loadCompanyContext ()
Object . assign ( draft , store . company )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '企业资料加载失败' )
} finally {
loading . value = false
}
})
function startEdit () {
Object . assign ( draft , store . company )
editing . value = true
}
function cancelEdit () {
Object . assign ( draft , store . company )
editing . value = false
}
async function saveProfile () {
if ( profileSubmitting . value ) return
if ( ! draft . companyName || ! draft . legalRepresentative || ! draft . contactName || ! draft . contactPhone ) {
ElMessage . warning ( '请完整填写企业名称、法人及联系人信息' )
return
}
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 () {
resetQualificationDraft ()
qualificationOpen . value = true
}
function handleFileChange ( file : UploadFile ) {
const raw = file . raw
if ( ! raw ) {
qualificationFileError . value = '未能读取所选文件,请重新选择'
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 ()
return
}
qualificationFileError . value = ''
selectedFile . value = raw
qualificationDraft . fileName = raw . name
selectedFileSize . value = raw . size
selectedFileType . value = 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 resetQualificationDraft () {
uploadRef . value ? . clearFiles ()
qualificationDraft . type = '营业执照'
qualificationDraft . fileName = ''
qualificationDraft . certificateNo = store . company . creditCode
selectedFileSize . value = 0
selectedFileType . value = ''
qualificationFileError . value = ''
selectedFile . value = null
}
async function submitQualification () {
if ( qualificationSubmitting . value ) return
if ( ! qualificationDraft . fileName || ! selectedFile . value ) {
ElMessage . warning ( '请选择营业执照图片' )
return
}
if ( qualificationFileError . value ) {
ElMessage . warning ( qualificationFileError . value )
return
}
qualificationSubmitting . value = true
try {
await uploadBusinessLicense ( selectedFile . value , qualificationDraft . certificateNo . trim () || undefined )
await store . loadCompanyContext ()
qualificationOpen . value = false
ElMessage . success ( '营业执照已提交审核' )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '营业执照上传失败' )
} finally {
qualificationSubmitting . value = false
}
}
async function previewQualification ( record : TenantQualification ) {
previewRecord . value = record
previewOpen . value = true
if ( ! record . documentUrl ) return
previewLoading . value = true
try {
const blob = await downloadQualificationImage ( record . documentUrl )
previewUrl . value = URL . createObjectURL ( blob )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '资质文件加载失败' )
} finally {
previewLoading . value = false
}
}
function closeQualificationPreview () {
if ( previewUrl . value ) URL . revokeObjectURL ( previewUrl . value )
previewUrl . value = ''
previewRecord . value = null
}
function qualificationStatus ( status : string ) {
return status === 'VERIFIED' ? '已验证' : status === 'PENDING' ? '待审核' : status === 'REJECTED' ? '已驳回' : '已过期'
}
</ script >
< template >
< section v-loading = "loading" class="profile-page" >
< div class = "profile-summary-card client-card" >
< div class = "company-mark" >< Building2 :size = "28" /></ div >
< div class = "company-summary" >
< div >< h2 >{{ store . company . companyName }}</ h2 >< el-tag : type = "companyApproved ? 'success' : store.company.auditStatus === 'REJECTED' || store.company.auditStatus === 'DISABLED' ? 'danger' : 'warning'" effect = "light" >{{ auditStatusCopy . label }}</ el-tag ></ div >
< p >{{ store . company . creditCode }} · {{ store . company . companyType }}</ p >
</ div >
< el-button v-if = "auth.hasPermission('company:profile:edit')" type="primary" @click="startEdit"><Pencil :size="16" /> 编辑企业资料 </ el-button >
</ div >
< 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" >
< dl class = "profile-grid" >
< div >< dt > 公司名称 </ dt >< dd >{{ store . company . companyName }}</ dd ></ div >
< div >< dt > 统一社会信用代码 </ dt >< dd >{{ store . company . creditCode }}</ dd ></ div >
< div >< dt > 企业类型 </ dt >< dd >{{ store . company . companyType }}</ 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 }}</ dd ></ div >
< div >< dt > 法人姓名 </ dt >< dd >{{ store . company . legalRepresentative }}</ dd ></ div >
< div >< dt > 法人电话 </ dt >< dd >{{ store . company . legalRepresentativePhone }}</ dd ></ div >
</ 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" >
< dl class = "profile-grid" >
< div >< dt > 联系人 </ dt >< dd >{{ store . company . contactName }}</ dd ></ div >
< div >< dt > 联系电话 </ dt >< dd >{{ store . company . contactPhone }}</ dd ></ div >
< div >< dt > 联系邮箱 </ dt >< dd >{{ store . company . contactEmail }}</ dd ></ div >
< div >< dt > 微信 </ dt >< dd >{{ store . company . contactWechat }}</ dd ></ div >
< div class = "full-row" >< dt > 业务地址 </ dt >< dd >{{ store . company . businessAddress }}</ dd ></ div >
</ 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" >
< header >< div >< h3 > 企业资质 </ h3 >< p > 文件更新后需等待平台重新审核 , 历史业务不受影响 。 </ p ></ div >< el-button v-if = "auth.hasPermission('company:qualification:upload')" type="primary" @click="openQualificationDialog"><Upload :size="16" /> 更新资质 </ el-button ></ header >
< el-table class = "desktop-qualification-table" :data = "store.qualifications" row -key =" id " >
2026-07-30 21:23:06 +08:00
< el-table-column label = "操作" width = "90" fixed = "left" >< template # default = "{ row }" >< el-button link type = "primary" @click ="previewQualification(row as TenantQualification)" > 查看 </ el -button ></ template ></ el-table-column >
2026-07-30 17:59:42 +08:00
< el-table-column prop = "type" label = "文件类型" min -width =" 150 " />
< el-table-column prop = "fileName" label = "文件名称" min -width =" 220 " show -overflow-tooltip />
< el-table-column label = "状态" width = "110" >< template # default = "{ row }" >< el-tag : type = "row.status === 'VERIFIED' ? 'success' : row.status === 'PENDING' ? 'warning' : 'danger'" effect = "light" >{{ qualificationStatus ( row . status ) }}</ el-tag ></ template ></ el-table-column >
< el-table-column prop = "expiresAt" label = "有效期" width = "130" />
< el-table-column prop = "updatedAt" label = "更新时间" min -width =" 160 " />
</ el-table >
< 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 >
< 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 >
</ article >
</ div >
</ div >
</ el-tab-pane >
</ el-tabs >
< el-drawer v-model = "editing" title="编辑企业资料" size="min(620px, 94vw)" @closed="cancelEdit" >
< el-form label -position =" top " class = "profile-form" >
< h3 > 企业基本信息 </ h3 >
< div class = "form-grid-two" >
< el-form-item label = "公司名称" required >< el-input v-model = "draft.companyName" /></ el-form-item >
< el-form-item label = "统一社会信用代码" >< el-input v-model = "draft.creditCode" disabled /></ el-form-item >
< el-form-item label = "企业类型" >< el-input v-model = "draft.companyType" /></ el-form-item >
< el-form-item label = "注册地" >< el-input v-model = "draft.registeredAddress" /></ el-form-item >
< el-form-item label = "法人姓名" required >< el-input v-model = "draft.legalRepresentative" /></ el-form-item >
< el-form-item label = "法人电话" >< el-input v-model = "draft.legalRepresentativePhone" /></ el-form-item >
</ div >
< h3 > 联系信息 </ h3 >
< div class = "form-grid-two" >
< el-form-item label = "联系人" required >< el-input v-model = "draft.contactName" /></ el-form-item >
< el-form-item label = "联系电话" required >< el-input v-model = "draft.contactPhone" /></ el-form-item >
< el-form-item label = "联系邮箱" >< el-input v-model = "draft.contactEmail" /></ el-form-item >
< el-form-item label = "微信" >< el-input v-model = "draft.contactWechat" /></ el-form-item >
< el-form-item label = "业务地址" class = "full-row" >< el-input v-model = "draft.businessAddress" /></ el-form-item >
</ 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 >
< el-dialog v-model = "qualificationOpen" title="更新资质文件" width="min(520px, 92vw)" @closed="resetQualificationDraft" >
< el-form label -position =" top " >
< el-form-item label = "文件类型" >< el-input v-model = "qualificationDraft.type" disabled /></ el-form-item >
< el-form-item label = "选择文件" required :error = "qualificationFileError" >
< el-upload ref = "uploadRef" :auto-upload = "false" :limit = "1" :show-file-list = "false" accept = ".jpg,.jpeg,.png" @change ="handleFileChange" >
< el-button >< Upload :size = "16" /> 选择文件 </ el-button >
< template # tip >< div class = "upload-tip" > 仅支持 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 >
</ el-form-item >
< el-form-item 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 >
</ 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" />
< 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 >
< 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 >
< span >{{ previewUrl ? '文件通过当前商户会话临时加载,关闭弹窗后立即释放。' : '当前文件暂不可预览。' }}</ span >
</ div >
</ el-dialog >
</ section >
</ template >
< style scoped >
. profile - page { display : flex ; flex - direction : column ; gap : 16 px ; }
. 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 ; }
. company - audit - notice . is - pending { border - color : # e5d2a5 ; background : # fff9eb ; color : # 9 a6a12 ; }
. profile - summary - card { display : flex ; align - items : center ; gap : 16 px ; }
. company - mark { display : grid ; width : 58 px ; height : 58 px ; flex : 0 0 58 px ; place - items : center ; background : # e7f3f1 ; color : var ( -- primary ); }
. company - summary { flex : 1 ; min - width : 0 ; }
. company - summary > div { display : flex ; align - items : center ; gap : 10 px ; }
. company - summary h2 { overflow : hidden ; margin : 0 ; color : var ( -- navy ); font - size : 20 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. company - summary p , . 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 ; }
. 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 ; }
. 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 ; }
. 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 ; }
. 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 ; }
. 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 ; }
. 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 ; }
@ media ( max - width : 720 px ) {
. profile - summary - card { align - items : flex - start ; flex - wrap : wrap ; }
. profile - summary - card > . el - button { width : 100 % ; }
. 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 > 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 ; }
. selected - qualification - file > . el - button { margin - left : 30 px ; }
}
</ style >