This commit is contained in:
2026-09-11 16:17:24 +08:00
parent 80e83a6668
commit ec7f76fede
18 changed files with 263 additions and 31 deletions
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } 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'
@@ -18,6 +19,7 @@ import {
const store = useTenantAdministrationStore()
const auth = useAuthStore()
const route = useRoute()
const companyApproved = computed(() => store.company.auditStatus === 'ACTIVE')
const auditStatusCopy = computed(() => ({
PROFILE_REQUIRED: { title: '请完善企业资质', detail: '上传营业执照并提交审核后,才能提交头程订单;法人身份证可按需补充。' },
@@ -41,6 +43,7 @@ const qualificationPage = ref(1)
const qualificationPageSize = ref(10)
const qualificationSubmitting = ref(false)
const reviewSubmitting = ref(false)
const deletingQualificationId = ref('')
const profileSubmitting = ref(false)
const loading = ref(false)
const loadError = ref('')
@@ -60,7 +63,7 @@ type QualificationDraftItem = QualificationTypeDefinition & {
const qualificationTypes: QualificationTypeDefinition[] = [
{ value: 'BUSINESS_LICENSE', label: '营业执照', description: '上传清晰完整的营业执照文件。', required: true },
{ value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '选填;如上传,正反面请合并为一个 PDF、JPG 或 PNG 文件。', required: false },
{ value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '选填;如上传,正反面请合并为一个 PDF、JPG、PNG 或 BMP 文件。', required: false },
{ value: 'FREIGHT_FORWARDER_CERTIFICATE', label: '货代资质证书', description: '如有,可上传当前有效的货代资质证明。', required: false },
{ value: 'BANK_ACCOUNT_LICENSE', label: '银行开户许可证', description: '如有,可上传企业对公账户开户证明。', required: false },
]
@@ -154,7 +157,7 @@ const profileFormRules: FormRules<TenantCompanyProfile> = {
contactEmail: [{ type: 'email', message: '请输入正确的联系邮箱', trigger: 'blur' }],
}
const qualificationColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
{ key: 'actions', label: '操作', width: 132, fixed: 'left', slotName: 'actions' },
{ key: 'type', prop: 'type', label: '文件类型', minWidth: 150 },
{ key: 'fileName', prop: 'fileName', label: '文件名称', minWidth: 220, showOverflowTooltip: true },
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
@@ -184,7 +187,13 @@ async function loadCompanyContext() {
loading.value = false
}
onMounted(() => void loadCompanyContext())
onMounted(async () => {
await loadCompanyContext()
if (route.query.qualification === 'upload' && canUploadQualification.value) {
activeTab.value = 'qualification'
openQualificationDialog()
}
})
onBeforeUnmount(() => qualificationDrafts.forEach(releaseQualificationPreview))
function startEdit() {
@@ -299,7 +308,7 @@ function handleQualificationFileChange(type: MerchantQualificationType, file: Up
item.fileName = raw.name
item.fileSize = raw.size
item.fileType = raw.type || raw.name.split('.').pop()?.toUpperCase() || '文件'
if (raw.type.startsWith('image/') || /\.(jpe?g|png)$/i.test(raw.name)) item.previewUrl = URL.createObjectURL(raw)
if (raw.type.startsWith('image/') || /\.(jpe?g|png|bmp)$/i.test(raw.name)) item.previewUrl = URL.createObjectURL(raw)
}
function removeSelectedFile(type: MerchantQualificationType) {
@@ -329,19 +338,19 @@ function resetQualificationDraft() {
qualificationCertificateNo.value = store.company.creditCode
}
async function submitQualification() {
if (qualificationSubmitting.value) return
async function submitQualification(closeDialog = true) {
if (qualificationSubmitting.value) return false
if (!canUploadQualification.value) {
qualificationOpen.value = false
await loadCompanyContext()
ElMessage.warning(qualificationUploadDisabledReason.value)
return
return false
}
if (!requiredPackageComplete.value) {
const businessLicense = qualificationDraftItem('BUSINESS_LICENSE')
if (businessLicense && !qualificationCreditCodeReady.value) businessLicense.error = qualificationCreditCodeError.value
ElMessage.warning(`请完整保存必填业务项:${missingRequiredPackageItems.value.join('、')}`)
return
return false
}
const optionalItems = qualificationDrafts.filter((item) => !item.required
&& item.file
@@ -418,20 +427,32 @@ async function submitQualification() {
if (!canUploadQualification.value) qualificationOpen.value = false
const uploadedCopy = uploadedCount ? `已保存 ${uploadedCount} 项;` : ''
ElMessage.error(`${uploadedCopy}${failures[0]}`)
return
return false
}
qualificationOpen.value = false
if (closeDialog) qualificationOpen.value = false
if (refreshed) ElMessage.success(includedLegalPersonId
? (requiredPackageChangeSelected.value
? '营业执照、统一社会信用代码和法人身份证已保存,尚未提交审核'
: '法人身份证已保存,尚未提交审核')
: '资质文件已保存,尚未提交审核')
else ElMessage.warning('资质文件已保存,但最新状态刷新失败,请点击“重新加载”确认状态')
return refreshed
} finally {
qualificationSubmitting.value = false
}
}
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
}
async function submitQualificationReview() {
if (!canSubmitQualificationReview.value || reviewSubmitting.value) return
reviewSubmitting.value = true
@@ -450,6 +471,34 @@ async function submitQualificationReview() {
}
}
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 = ''
}
}
async function previewQualification(record: TenantQualification) {
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
previewRecord.value = record
@@ -548,7 +597,10 @@ function closeQualificationPreview() {
:total="store.qualifications.length"
row-key="id"
>
<template #actions="{ row }"><el-button link type="primary" @click="previewQualification(row as TenantQualification)">查看</el-button></template>
<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>
<template #status="{ row }"><el-tag :type="qualificationStatusMeta(row.status).type" effect="light">{{ qualificationStatusMeta(row.status).label }}</el-tag></template>
</DataTable>
<div class="mobile-qualification-list">
@@ -556,7 +608,10 @@ function closeQualificationPreview() {
<header><strong>{{ record.type }}</strong><el-tag :type="qualificationStatusMeta(record.status).type" effect="light">{{ qualificationStatusMeta(record.status).label }}</el-tag></header>
<div class="qualification-file-name"><FileText :size="16" /><span>{{ record.fileName }}</span></div>
<dl><div><dt>有效期</dt><dd>{{ record.expiresAt }}</dd></div><div><dt>更新时间</dt><dd>{{ record.updatedAt }}</dd></div></dl>
<el-button link type="primary" @click="previewQualification(record)">查看文件信息</el-button>
<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>
</article>
<div v-if="store.qualifications.length === 0" class="client-empty">暂无企业资质记录。</div>
</div>
@@ -612,11 +667,11 @@ function closeQualificationPreview() {
:key="`${item.value}-${item.inputVersion}`"
:auto-upload="false"
:show-file-list="false"
accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png"
accept=".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp,image/x-ms-bmp"
@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,单个文件不超过 5MB。</div></template>
<template #tip><div class="upload-tip">支持 PDF、JPG、PNG、BMP,单个文件不超过 5MB。</div></template>
</el-upload>
<div v-if="item.fileName" class="selected-qualification-file" aria-live="polite">
<a v-if="item.previewUrl" class="selected-file-preview" :href="item.previewUrl" target="_blank" rel="noopener" :aria-label="`查看${item.label}图片预览`">
@@ -634,7 +689,7 @@ function closeQualificationPreview() {
<small class="qualification-credit-code-hint">{{ companyCreditCodeValid ? '已登记的统一社会信用代码不可直接修改。' : '与营业执照一致,保存后不可直接修改。' }}</small>
</el-form-item>
<p v-if="qualificationDialogHint" class="qualification-dialog-hint">{{ qualificationDialogHint }}</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>
<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>
</el-dialog>
<el-dialog v-model="previewOpen" :title="`${previewRecord?.type || '资质文件'}详情`" width="min(560px, 92vw)" @closed="closeQualificationPreview">
@@ -692,6 +747,7 @@ function closeQualificationPreview() {
.form-grid-two { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
.form-grid-two .full-row { grid-column: 1 / -1; }
.drawer-actions { display: flex; justify-content: flex-end; gap: 8px; }
.mobile-qualification-actions { display: flex; align-items: center; gap: 8px; }
.qualification-dialog-intro { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 16px; padding: 13px 15px; background: #f4f7f7; }
.qualification-dialog-intro strong { color: var(--navy); font-size: 13px; }
.qualification-dialog-intro span { max-width: 420px; color: var(--muted); font-size: 11px; line-height: 1.6; text-align: right; }