This commit is contained in:
2026-08-05 09:11:48 +08:00
parent db8774627e
commit 3ffa7ec55f
11 changed files with 141 additions and 41 deletions
@@ -12,6 +12,7 @@ import {
rejectEnterpriseReview,
updatePlatformTenant,
uploadPlatformTenantQualification,
uploadPlatformTenantRequiredQualificationPackage,
type EnterpriseReview,
type PlatformTenantResource,
} from '@/modules/iam'
@@ -45,6 +46,7 @@ vi.mock('@/modules/iam', () => ({
submitPlatformTenantReview: vi.fn(),
updatePlatformTenant: vi.fn(),
uploadPlatformTenantQualification: vi.fn(),
uploadPlatformTenantRequiredQualificationPackage: vi.fn(),
}))
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
@@ -107,6 +109,13 @@ describe('CustomersView', () => {
vi.mocked(uploadPlatformTenantQualification).mockResolvedValue({
qualificationId: 'qualification-1', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/files/license.png',
})
vi.mocked(uploadPlatformTenantRequiredQualificationPackage).mockResolvedValue({
businessLicenseQualificationId: 'qualification-business',
legalPersonIdQualificationId: 'qualification-legal',
tenantStatus: 'PROFILE_REQUIRED',
businessLicenseDocumentUrl: '/files/license.png',
legalPersonIdDocumentUrl: '/files/legal-person-new.png',
})
vi.mocked(createPlatformExportTask).mockResolvedValue({
id: 'export-1', taskName: '客户列表', taskNo: 'EXP2084', status: 'PENDING',
} as never)
@@ -198,7 +207,7 @@ describe('CustomersView', () => {
expect(getPlatformTenant).not.toHaveBeenCalled()
})
it('saves customer data and selected qualifications with one action', async () => {
it('saves both required files and the credit code through one package action', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID',
certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null,
@@ -209,9 +218,14 @@ describe('CustomersView', () => {
await flushPromises()
const licenseFile = new File(['license'], 'license.png', { type: 'image/png' })
const licenseInput = document.querySelector<HTMLInputElement>('.qualification-file input[type="file"]')
const legalPersonFile = new File(['legal-person'], 'legal-person.png', { type: 'image/png' })
const qualificationInputs = document.querySelectorAll<HTMLInputElement>('.qualification-file input[type="file"]')
const licenseInput = qualificationInputs[0]
const legalPersonInput = qualificationInputs[1]
Object.defineProperty(licenseInput, 'files', { configurable: true, value: [licenseFile] })
licenseInput?.dispatchEvent(new Event('change', { bubbles: true }))
Object.defineProperty(legalPersonInput, 'files', { configurable: true, value: [legalPersonFile] })
legalPersonInput?.dispatchEvent(new Event('change', { bubbles: true }))
await flushPromises()
const buttons = [...document.querySelectorAll<HTMLButtonElement>('button')]
@@ -220,14 +234,15 @@ describe('CustomersView', () => {
await flushPromises()
expect(updatePlatformTenant).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ tenantName: fullTenant.tenantName }))
expect(uploadPlatformTenantQualification).toHaveBeenCalledWith(
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledWith(
fullTenant.id,
'BUSINESS_LICENSE',
licenseFile,
undefined,
legalPersonFile,
fullTenant.unifiedSocialCreditCode,
false,
true,
)
expect(uploadPlatformTenantQualification).not.toHaveBeenCalled()
})
it('submits the current filters to the backend export service without loading every customer page', async () => {
+47 -13
View File
@@ -20,6 +20,7 @@ import {
submitPlatformTenantReview,
updatePlatformTenant,
uploadPlatformTenantQualification,
uploadPlatformTenantRequiredQualificationPackage,
type CreatePlatformTenantRequest,
type EnterpriseApprovalResponse,
type EnterpriseReview,
@@ -97,7 +98,8 @@ const queryFields: QueryField[] = [
] },
]
const emptyForm = (): CreatePlatformTenantRequest => ({
type CustomerForm = CreatePlatformTenantRequest & { unifiedSocialCreditCode: string }
const emptyForm = (): CustomerForm => ({
tenantName: '', legalName: '', legalRepresentative: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '',
contactEmail: '', contactWechat: '', servicePackage: '基础版', salesOwnerName: '', paymentTermDays: 30,
enterpriseType: '', establishedDate: null, registeredCapital: 100000, registeredCapitalCurrency: 'CNY',
@@ -174,17 +176,24 @@ const canSubmitReview = computed(() => Boolean(editingId.value) && ['PROFILE_REQ
const uploadingQualifications = computed(() => qualifications.value.some((item) => item.uploading))
const hasSelectedQualifications = computed(() => qualifications.value.some((item) => item.file))
const selectedBusinessLicense = computed(() => qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')?.file)
const creditCodeReadyForSave = computed(() => creditCodeEstablished.value
|| (Boolean(selectedBusinessLicense.value) && unifiedSocialCreditCodePattern.test(form.value.unifiedSocialCreditCode || '')))
const selectedLegalPersonId = computed(() => qualifications.value.find((item) => item.type === 'LEGAL_PERSON_ID')?.file)
const requiredPackageChangeSelected = computed(() => Boolean(selectedBusinessLicense.value || selectedLegalPersonId.value))
const requiredPackageFilesSelected = computed(() => Boolean(selectedBusinessLicense.value && selectedLegalPersonId.value))
const creditCodeReadyForSave = computed(() => requiredPackageChangeSelected.value
? requiredPackageFilesSelected.value && (creditCodeEstablished.value
|| unifiedSocialCreditCodePattern.test(form.value.unifiedSocialCreditCode || ''))
: creditCodeEstablished.value)
const requiredQualificationPackageComplete = computed(() => creditCodeReadyForSave.value
&& qualifications.value.filter((item) => item.required).every((item) => item.uploaded || Boolean(item.file)))
&& (requiredPackageChangeSelected.value
? requiredPackageFilesSelected.value
: qualifications.value.filter((item) => item.required).every((item) => item.uploaded)))
const missingQualificationPackageItems = computed(() => {
const businessLicense = qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')
const legalPersonId = qualifications.value.find((item) => item.type === 'LEGAL_PERSON_ID')
const missing: string[] = []
if (!businessLicense?.uploaded && !businessLicense?.file) missing.push('营业执照')
if (!creditCodeReadyForSave.value) missing.push('18位统一社会信用代码(需随营业执照保存)')
if (!legalPersonId?.uploaded && !legalPersonId?.file) missing.push('法人身份证')
if (requiredPackageChangeSelected.value ? !businessLicense?.file : !businessLicense?.uploaded) missing.push('营业执照')
if (!creditCodeReadyForSave.value) missing.push('18位统一社会信用代码(需随必填资质包保存)')
if (requiredPackageChangeSelected.value ? !legalPersonId?.file : !legalPersonId?.uploaded) missing.push('法人身份证')
return missing
})
const hasUnsavedChanges = computed(() => Boolean(editingId.value) && JSON.stringify(form.value) !== savedFormSnapshot.value)
@@ -322,7 +331,7 @@ function openCreate() {
editingId.value = null; editorStatus.value = null; resetEditor(); editorOpen.value = true
nextTick(() => formRef.value?.clearValidate())
}
function mapTenantToForm(row: PlatformTenantResource): CreatePlatformTenantRequest {
function mapTenantToForm(row: PlatformTenantResource): CustomerForm {
return {
tenantName: row.tenantName, legalName: row.legalName || '', legalRepresentative: row.legalRepresentative || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '',
contactName: row.contactName || '', contactPhone: row.contactPhone || '', contactEmail: row.contactEmail || '',
@@ -435,16 +444,40 @@ function handleQualificationPreviewError(draft: QualificationDraft) {
async function uploadSelectedQualifications(tenantId: string) {
const failures: string[] = []
for (const draft of qualifications.value.filter((item) => item.file)) {
const businessLicense = qualifications.value.find((item) => item.type === 'BUSINESS_LICENSE')
const legalPersonId = qualifications.value.find((item) => item.type === 'LEGAL_PERSON_ID')
if (requiredPackageChangeSelected.value && businessLicense?.file && legalPersonId?.file) {
businessLicense.uploading = true; legalPersonId.uploading = true
businessLicense.error = ''; legalPersonId.error = ''
try {
const uploaded = await uploadPlatformTenantRequiredQualificationPackage(
tenantId, businessLicense.file, legalPersonId.file, form.value.unifiedSocialCreditCode,
businessLicense.uploaded, legalPersonId.uploaded,
)
businessLicense.uploaded = true; legalPersonId.uploaded = true
creditCodeEstablished.value = true
businessLicense.uploadedName = businessLicense.file.name
legalPersonId.uploadedName = legalPersonId.file.name
businessLicense.documentUrl = uploaded.businessLicenseDocumentUrl
legalPersonId.documentUrl = uploaded.legalPersonIdDocumentUrl
businessLicense.file = null; legalPersonId.file = null
} catch (error) {
const message = getApiErrorMessage(error, '必填资质包上传失败,请重试')
businessLicense.error = message; legalPersonId.error = message
failures.push('必填资质包')
} finally {
businessLicense.uploading = false; legalPersonId.uploading = false
}
}
for (const draft of failures.length ? [] : qualifications.value.filter((item) => !item.required && item.file)) {
draft.uploading = true; draft.error = ''
try {
const uploaded = await uploadPlatformTenantQualification(
tenantId, draft.type, draft.file as File, draft.validTo || undefined,
draft.type === 'BUSINESS_LICENSE' ? form.value.unifiedSocialCreditCode : undefined,
undefined,
draft.uploaded,
)
draft.uploaded = true
if (draft.type === 'BUSINESS_LICENSE') creditCodeEstablished.value = true
draft.uploadedName = draft.file?.name || '已登记文件'
draft.documentUrl = uploaded.documentUrl
draft.file = null
@@ -470,7 +503,8 @@ async function saveCustomer() {
const { unifiedSocialCreditCode: _immutable, ...payload } = form.value
saved = await updatePlatformTenant(editingId.value, payload)
} else {
saved = await createPlatformTenant({ ...form.value, unifiedSocialCreditCode: undefined })
const { unifiedSocialCreditCode: _qualificationCreditCode, ...payload } = form.value
saved = await createPlatformTenant(payload)
editingId.value = String(saved.id)
}
editorStatus.value = saved.status
@@ -667,7 +701,7 @@ onMounted(async () => {
</div><el-form-item label="内部备注" prop="internalRemark"><el-input v-model="form.internalRemark" type="textarea" :rows="3" placeholder="请输入仅平台人员可见的客户备注" maxlength="1000" show-word-limit /></el-form-item></div>
<div class="form-section qualification-section"><div class="qualification-heading"><div><h3>企业认证必填业务项</h3><p>营业执照、18位统一社会信用代码、法人身份证需共同完整保存;货代资质证书和银行开户许可证选填。</p></div><el-tag v-if="editorStatus" :type="statusMeta(editorStatus).type" effect="light">{{ statusMeta(editorStatus).label }}</el-tag></div>
<el-form-item label="统一社会信用代码" prop="unifiedSocialCreditCode" required class="qualification-credit-code"><el-input v-model="form.unifiedSocialCreditCode" :disabled="creditCodeEstablished" placeholder="请输入18位大写字母或数字" maxlength="18" @input="form.unifiedSocialCreditCode = String($event).toUpperCase()" /><small>{{ creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照文件共同保存。' }}</small></el-form-item>
<el-form-item label="统一社会信用代码" prop="unifiedSocialCreditCode" required class="qualification-credit-code"><el-input v-model="form.unifiedSocialCreditCode" :disabled="creditCodeEstablished" placeholder="请输入18位大写字母或数字" maxlength="18" @input="form.unifiedSocialCreditCode = String($event).toUpperCase()" /><small>{{ creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照、法人身份证共同保存。' }}</small></el-form-item>
<div v-if="qualificationLoadError" class="qualification-load-error" role="alert">{{ qualificationLoadError }}<el-button v-if="editingId" link type="primary" @click="loadEditorQualifications(editingId)">重试</el-button></div>
<div class="qualification-table" role="table" aria-label="客户资质文件">
<div class="qualification-row qualification-header" role="row"><span role="columnheader">文件类型</span><span role="columnheader">上传文件</span><span role="columnheader">有效期</span></div>