This commit is contained in:
2026-08-04 14:15:24 +08:00
parent 7af1d0250b
commit d5eaa75329
9 changed files with 62 additions and 46 deletions
+3 -2
View File
@@ -42,8 +42,9 @@ test('shows immediate login after registration and separates company review from
assert.match(registerSource, /企业账号注册成功/)
assert.match(registerSource, /无需等待注册审核/)
assert.match(registerSource, /提交订单前还需完成企业审核/)
assert.match(registerSource, /营业执照、法人身份证、货代资质证书和银行开户许可证/)
assert.match(registerSource, /一次补齐四类必填资质并提交审核/)
assert.match(registerSource, /营业执照和法人身份证/)
assert.match(registerSource, /货代资质证书、银行开户许可证可选填/)
assert.match(registerSource, /其他资质可选填/)
assert.match(registerSource, /立即登录/)
})
+2 -2
View File
@@ -116,7 +116,7 @@ async function submit() {
<span><RouterLink to="/legal/terms" target="_blank" rel="noopener">《用户服务协议》</RouterLink> 和 <RouterLink to="/legal/privacy" target="_blank" rel="noopener">《隐私政策》</RouterLink></span>
</div>
</el-form-item>
<p class="register-security-note">注册成功后账号可立即登录;首次登录需补齐营业执照、法人身份证、货代资质证书和银行开户许可证,提交平台审核通过后才能提交订单。</p>
<p class="register-security-note">注册成功后账号可立即登录;首次登录需上传营业执照和法人身份证,货代资质证书、银行开户许可证可选填。提交平台审核通过后才能提交订单。</p>
<div class="register-actions"><el-button @click="$router.push('/login')">取消</el-button><el-button native-type="submit" type="primary" :loading="submitting">注册企业账号</el-button></div>
</el-form>
</template>
@@ -126,7 +126,7 @@ async function submit() {
<h1>企业账号注册成功</h1>
<p><strong>{{ form.username }}</strong> 现在可以直接登录客户工作台,无需等待注册审核。</p>
<dl><div><dt>企业名称</dt><dd>{{ form.companyName }}</dd></div><div><dt>登录账号</dt><dd>{{ form.username }}</dd></div><div><dt>注册时间</dt><dd>{{ submittedAt }}</dd></div><div><dt>账号状态</dt><dd><el-tag type="success" effect="light">可登录</el-tag></dd></div></dl>
<div class="registration-next-step" role="note"><strong>提交订单前还需完成企业审核</strong><span>登录后请在“企业资料”一次补齐四类必填资质并提交审核。审核期间仍可维护资料、成员和订单草稿。</span></div>
<div class="registration-next-step" role="note"><strong>提交订单前还需完成企业审核</strong><span>登录后请在“企业资料”上传营业执照和法人身份证并提交审核,其他资质可选填。审核期间仍可维护资料、成员和订单草稿。</span></div>
<div class="registration-result-actions"><el-button type="primary" @click="router.push({ path: '/login', query: { account: form.username } })">立即登录</el-button></div>
</section>
</main>
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { formatQualificationFileSize, QUALIFICATION_MAX_FILE_SIZE, validateQualificationFile } from './qualificationFile.ts'
import { formatQualificationFileSize, qualificationUploadErrorMessage, QUALIFICATION_MAX_FILE_SIZE, validateQualificationFile } from './qualificationFile.ts'
test('accepts the backend-confirmed PDF, JPG and PNG formats up to 5MB', () => {
assert.equal(validateQualificationFile({ name: '营业执照.jpeg', size: QUALIFICATION_MAX_FILE_SIZE, type: 'image/jpeg' }), '')
@@ -20,3 +20,9 @@ test('formats selected file sizes for review', () => {
assert.equal(formatQualificationFileSize(1536), '1.5 KB')
assert.equal(formatQualificationFileSize(2 * 1024 * 1024), '2.0 MB')
})
test('turns gateway 413 responses into an actionable upload message', () => {
assert.match(qualificationUploadErrorMessage({ status: 413 }), /上传网关拒绝(413)/)
assert.match(qualificationUploadErrorMessage({ status: 413 }), /已小于 5MB.*检查网关上传限制/)
assert.equal(qualificationUploadErrorMessage(new Error('服务暂不可用')), '服务暂不可用')
})
@@ -23,3 +23,13 @@ export function formatQualificationFileSize(bytes: number) {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
export function qualificationUploadErrorMessage(error: unknown) {
const status = typeof error === 'object' && error !== null && 'status' in error
? (error as { status?: unknown }).status
: undefined
if (status === 413) {
return '文件被上传网关拒绝(413)。请压缩后重试;若文件已小于 5MB,请联系平台管理员检查网关上传限制'
}
return error instanceof Error ? error.message : '资质文件上传失败'
}
@@ -6,7 +6,7 @@ import { Building2, CheckCircle2, Clock3, FileCheck2, FileText, Pencil, Trash2,
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
import { useAuthStore } from '@/modules/auth/public'
import { formatQualificationFileSize, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
import { formatQualificationFileSize, qualificationUploadErrorMessage, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
import { qualificationStatusMeta } from '@/modules/tenant-management/utils/qualificationStatus'
import {
downloadQualificationImage,
@@ -19,10 +19,10 @@ const store = useTenantAdministrationStore()
const auth = useAuthStore()
const companyApproved = computed(() => store.company.auditStatus === 'ACTIVE')
const auditStatusCopy = computed(() => ({
PROFILE_REQUIRED: { title: '请完善企业资质', detail: '四类必填资质提交审核前不能提交头程订单。' },
PROFILE_REQUIRED: { title: '请完善企业资质', detail: '上传营业执照和法人身份证并提交审核后,才能提交头程订单。' },
PENDING: { title: '企业资料正在等待平台审核', detail: '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' },
ACTIVE: { title: '企业资料已审核,下单资格已开通', detail: '您可以创建、保存并提交头程订单。' },
REJECTED: { title: '企业资料审核未通过', detail: '请根据平台反馈替换对应资质,四类文件齐全后可重新提交审核。' },
REJECTED: { title: '企业资料审核未通过', detail: '请根据平台反馈替换对应资质,必填文件齐全后可重新提交审核。' },
DISABLED: { title: '企业账号已被平台停用', detail: '当前不能提交订单,请联系平台客服核实。' },
})[store.company.auditStatus])
const activeTab = ref('basic')
@@ -44,7 +44,7 @@ const loading = ref(false)
const loadError = ref('')
const draft = reactive<TenantCompanyProfile>({ ...store.company })
type QualificationUploadState = 'idle' | 'uploading' | 'uploaded' | 'error'
type QualificationTypeDefinition = { value: MerchantQualificationType; label: string; description: string }
type QualificationTypeDefinition = { value: MerchantQualificationType; label: string; description: string; required: boolean }
type QualificationDraftItem = QualificationTypeDefinition & {
file: File | null
fileName: string
@@ -57,11 +57,12 @@ type QualificationDraftItem = QualificationTypeDefinition & {
}
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: '上传企业对公账户开户证明。' },
{ value: 'BUSINESS_LICENSE', label: '营业执照', description: '上传清晰完整的营业执照文件。', required: true },
{ value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '正反面请合并为一个 PDF、JPG 或 PNG 文件。', required: true },
{ value: 'FREIGHT_FORWARDER_CERTIFICATE', label: '货代资质证书', description: '如有,可上传当前有效的货代资质证明。', required: false },
{ value: 'BANK_ACCOUNT_LICENSE', label: '银行开户许可证', description: '如有,可上传企业对公账户开户证明。', required: false },
]
const requiredQualificationTypes = qualificationTypes.filter((type) => type.required)
const qualificationDrafts = reactive<QualificationDraftItem[]>(qualificationTypes.map((type) => ({
...type,
file: null,
@@ -80,11 +81,11 @@ const normalizedQualificationCertificateNo = computed(() => qualificationCertifi
const businessLicenseFileSelected = computed(() => Boolean(
qualificationDrafts.find((item) => item.value === 'BUSINESS_LICENSE')?.file,
))
const qualificationCreditCodeReady = computed(() => companyCreditCodeValid.value
|| (businessLicenseFileSelected.value && unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value)))
const qualificationCreditCodeReady = computed(() => !businessLicenseFileSelected.value
|| companyCreditCodeValid.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))
@@ -98,22 +99,12 @@ const qualificationUploadDisabledReason = computed(() => {
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 ''
@@ -123,15 +114,15 @@ const qualificationDialogHint = computed(() => qualificationSaveDisabledReason.v
: qualificationSaveDisabledReason.value)
const canSubmitQualificationReview = computed(() => canUploadQualification.value
&& companyCreditCodeValid.value
&& qualificationTypes.every((type) => uploadedQualificationTypes.value.has(type.value)))
&& requiredQualificationTypes.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)
const missing = requiredQualificationTypes.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' }],
@@ -243,14 +234,14 @@ function qualificationSlotLabel(item: QualificationDraftItem) {
const record = qualificationRecord(item.value)
if (record?.status === 'PENDING') return '已上传'
if (record?.status === 'REJECTED') return '需重新上传'
return '待上传'
return item.required ? '待上传' : '选填'
}
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'
return item.required ? 'warning' : 'info'
}
function handleQualificationFileChange(type: MerchantQualificationType, file: UploadFile) {
@@ -323,13 +314,6 @@ async function submitQualification() {
ElMessage.warning(qualificationUploadDisabledReason.value)
return
}
if (missingQualificationTypes.value.length) {
qualificationDrafts.forEach((item) => {
if (!uploadedQualificationTypes.value.has(item.value) && !item.file) item.error = '请选择该项资质文件'
})
ElMessage.warning(`请补齐:${missingQualificationTypes.value.join('、')}`)
return
}
if (!qualificationCreditCodeReady.value) {
const businessLicense = qualificationDraftItem('BUSINESS_LICENSE')
if (businessLicense) businessLicense.error = qualificationCreditCodeError.value
@@ -366,7 +350,7 @@ async function submitQualification() {
item.uploadState = 'uploaded'
uploadedCount += 1
} catch (error) {
const message = error instanceof Error ? error.message : '资质文件上传失败'
const message = qualificationUploadErrorMessage(error)
item.error = message
item.uploadState = 'error'
failures.push(`${item.label}:${message}`)
@@ -542,14 +526,14 @@ function closeQualificationPreview() {
@closed="resetQualificationDraft"
>
<div class="qualification-dialog-intro">
<strong>一次补齐四类必填资质</strong>
<span>每类选择一个文件后统一保存;文件保存不会自动提交企业审核。</span>
<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>
<strong>{{ item.label }}({{ item.required ? '必填' : '选填' }})</strong>
<p>{{ item.description }}</p>
</div>
<el-tag :type="qualificationSlotTagType(item)" effect="light">{{ qualificationSlotLabel(item) }}</el-tag>
@@ -579,7 +563,7 @@ function closeQualificationPreview() {
<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-form-item label="统一社会信用代码" class="qualification-credit-code" :required="businessLicenseFileSelected && !companyCreditCodeValid" :error="qualificationCreditCodeReady ? '' : qualificationCreditCodeError">
<el-input v-model="qualificationCertificateNo" maxlength="18" placeholder="请输入18位统一社会信用代码" @input="qualificationCertificateNo = String($event).toUpperCase()" />
</el-form-item>
<p v-if="qualificationDialogHint" class="qualification-dialog-hint">{{ qualificationDialogHint }}</p>
@@ -47,7 +47,7 @@ test('implements company details, contact information and qualification workflow
assert.match(profileSource, /:close-on-click-modal="!qualificationSubmitting"/)
assert.match(profileSource, /:close-on-press-escape="!qualificationSubmitting"/)
assert.match(profileSource, /:show-close="!qualificationSubmitting"/)
assert.match(profileSource, /请重新选择营业执照,并填写18位统一社会信用代码/)
assert.match(profileSource, /const qualificationCreditCodeReady = computed\(\(\) => !businessLicenseFileSelected\.value/)
assert.match(profileSource, /companyCreditCodeValid\.value/)
assert.doesNotMatch(profileSource, /<el-select v-model="qualificationDraft\.type"/)
assert.match(profileSource, /class="mobile-qualification-list"/)
@@ -61,7 +61,12 @@ test('implements company details, contact information and qualification workflow
assert.match(profileSource, /ref="profileFormRef" :model="draft" :rules="profileFormRules"/)
assert.match(profileSource, /prop="companyName" required><el-input v-model="draft\.companyName" placeholder="请输入公司名称"/)
assert.match(profileSource, /const valid = await profileFormRef\.value\?\.validate\(\)/)
assert.match(profileSource, /请选择该项资质文件/)
assert.match(profileSource, /const requiredQualificationTypes = qualificationTypes\.filter\(\(type\) => type\.required\)/)
assert.match(profileSource, /货代资质证书'.*required: false/)
assert.match(profileSource, /银行开户许可证'.*required: false/)
assert.match(profileSource, /requiredQualificationTypes\.every/)
assert.doesNotMatch(profileSource, /四类必填资质/)
assert.match(profileSource, /qualificationUploadErrorMessage\(error\)/)
assert.match(profileSource, /submitTenantQualificationReview/)
assert.match(profileSource, /提交企业审核/)
assert.match(profileSource, /已保存.*项资质文件,尚未提交审核/)