This commit is contained in:
wujinliang
2026-09-18 09:15:40 +08:00
parent d54f857929
commit 9f1ea75504
4 changed files with 57 additions and 10 deletions
@@ -194,9 +194,13 @@ describe('CustomersView', () => {
expect(customersSource).not.toContain('<AppDrawer v-model="editorOpen"')
})
it('reserves enough space for qualification validity dates without widening the upload column', () => {
it('keeps qualification file controls usable when the editor dialog is narrow', () => {
expect(customersSource).toContain('class="qualification-validity" role="cell"')
expect(customersSource).toContain('grid-template-columns:170px minmax(0,1fr) 220px')
expect(customersSource).toContain('.qualification-table { container-type:inline-size;')
expect(customersSource).toContain('@container (max-width:660px)')
expect(customersSource).toContain('.qualification-row { grid-template-columns:112px minmax(0,1fr); }')
expect(customersSource).toContain('.qualification-label { grid-row:span 2; align-items:center; }')
expect(customersSource).toContain('.qualification-validity :deep(.el-date-editor) { width:100%; min-width:0; }')
expect(customersSource).toContain('.qualification-file-name { min-width:0;')
expect(readFileSync(resolve(process.cwd(), 'src/modules/customers/customerDensity.css'), 'utf8')).toContain('max-width: none; overflow-wrap: anywhere; white-space: normal;')
@@ -313,8 +317,41 @@ describe('CustomersView', () => {
expect(updatePlatformTenant).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ contactPhone: '0755-12345678' }))
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(false)
expect(document.body.textContent).toContain('客户资料已保存')
expect(document.body.textContent).not.toContain('尚未提交审核')
expect(document.body.textContent).toContain('不会修改企业管理员登录手机号')
expect(customersSource).not.toContain('联系电话(登录用户名)')
expect(customersSource.indexOf('if (!failures.length) editorOpen.value = false')).toBeLessThan(customersSource.indexOf('await loadCustomers()'))
})
it('allows an active customer to replace qualifications and select a validity date', async () => {
const activeTenant = { ...fullTenant, status: 'ACTIVE' as const }
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [activeTenant], page: 1, size: 10, total: 1, pages: 1 })
vi.mocked(getPlatformTenant).mockResolvedValue(activeTenant)
vi.mocked(listTenantQualifications).mockResolvedValue([
{
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'APPROVED', reviewComment: null, validTo: null,
},
{
id: 'qualification-freight', tenantId: fullTenant.id, qualificationType: 'FREIGHT_FORWARDER_CERTIFICATE',
certificateNo: null, documentUrl: '/files/freight.png', status: 'APPROVED', reviewComment: null, validTo: null,
},
])
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('编辑')?.click()
await flushPromises()
const fileInputs = [...document.querySelectorAll<HTMLInputElement>('.qualification-file input[type="file"]')]
const validityInputs = [...document.querySelectorAll<HTMLInputElement>('.qualification-validity input')]
expect(fileInputs).toHaveLength(4)
expect(fileInputs.every((input) => !input.disabled)).toBe(true)
expect(validityInputs.map((input) => input.disabled)).toEqual([true, false, true, false])
selectQualificationFile(0, new File(['license-new'], 'license-new.png', { type: 'image/png' }))
await flushPromises()
expect(validityInputs[0]?.disabled).toBe(false)
})
it('does not count rejected or superseded history as a current qualification when resubmitting', async () => {
+17 -7
View File
@@ -228,7 +228,8 @@ let reviewLoadSequence = 0
let editorLoadSequence = 0
const editorTitle = computed(() => editingId.value ? '编辑客户' : '新增客户')
const editorDialogWidth = 'min(1120px, calc(100vw - 28px))'
const canUploadQualifications = computed(() => !editorStatus.value || ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value))
const canUploadQualifications = computed(() => !editorStatus.value
|| ['PROFILE_REQUIRED', 'REJECTED', 'ACTIVE', 'DISABLED'].includes(editorStatus.value))
const canSubmitReview = computed(() => Boolean(editingId.value) && ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value || ''))
const uploadingQualifications = computed(() => qualifications.value.some((item) => item.uploading))
const hasSelectedQualifications = computed(() => qualifications.value.some((item) => item.file))
@@ -603,19 +604,22 @@ async function saveCustomer() {
editorStatus.value = saved.status
savedFormSnapshot.value = JSON.stringify(form.value)
const failures = hadSelectedQualifications ? await uploadSelectedQualifications(String(saved.id)) : []
const canSubmitSavedCustomerForReview = ['PROFILE_REQUIRED', 'REJECTED'].includes(saved.status)
if (failures.length) {
ElMessage.warning(`客户资料已保存,但${failures.join('、')}上传失败;失败文件已保留,请修正后再次保存`)
} else if (hadBusinessLicense && hadLegalPersonId) {
} else if (canSubmitSavedCustomerForReview && hadBusinessLicense && hadLegalPersonId) {
ElMessage.success('营业执照、统一社会信用代码和选填的法人身份证已保存,尚未提交审核')
} else if (hadBusinessLicense) {
} else if (canSubmitSavedCustomerForReview && hadBusinessLicense) {
ElMessage.success('营业执照和统一社会信用代码已保存,法人身份证可按需补充,尚未提交审核')
} else if (hadSelectedQualifications) {
} else if (canSubmitSavedCustomerForReview && hadSelectedQualifications) {
ElMessage.success('客户资料和已选资质已保存,尚未提交审核')
} else if (hadSelectedQualifications) {
ElMessage.success('客户资料和已选资质已保存')
} else {
ElMessage.success('客户资料已保存')
}
await loadCustomers()
if (!failures.length) editorOpen.value = false
await loadCustomers()
} catch (error) { ElMessage.error(getApiErrorMessage(error, '客户资料保存失败,请稍后重试')) }
finally { saving.value = false }
}
@@ -1112,7 +1116,7 @@ onMounted(() => { void loadCustomers() })
</button>
<div class="qualification-file-control"><label class="file-picker" :class="{ 'is-disabled': !canUploadQualifications || draft.uploading }"><span>{{ draft.uploading ? '上传中' : (draft.file ? '已选择新文件' : (draft.uploaded ? '更换文件' : '选择文件')) }}</span><input type="file" accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" :disabled="!canUploadQualifications || draft.uploading" @change="selectQualificationFile($event, draft)" /></label><span class="qualification-file-name" :title="draft.file?.name || draft.uploadedName">{{ draft.file?.name || draft.uploadedName || '未选择文件' }}</span><small v-if="draft.error" class="qualification-error" role="alert">{{ draft.error }}</small></div>
</div>
<div class="qualification-validity" role="cell"><el-date-picker v-model="draft.validTo" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期" :disabled="!canUploadQualifications || draft.uploaded" /></div>
<div class="qualification-validity" role="cell"><el-date-picker v-model="draft.validTo" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期" :disabled="!canUploadQualifications || draft.uploading || (draft.uploaded && !draft.file)" /></div>
</div>
</div>
<p class="qualification-hint">支持 PDF、JPG、PNG,单个文件不超过 5MB;<span>*</span> 为提交审核必填项。</p>
@@ -1222,7 +1226,7 @@ onMounted(() => { void loadCustomers() })
.capital-input { display:grid; grid-template-columns:minmax(0,1fr) 130px; width:100%; gap:var(--admin-space-3, 8px); } .capital-input :deep(.el-input-number) { width:100%; } .capital-input :deep(.el-select) { width:100%; }
.qualification-heading { display:flex; justify-content:flex-end; margin-bottom:var(--admin-space-5, 12px); } .qualification-hint { margin:var(--admin-space-5, 12px) 0 0; color:var(--cc-muted); font-size:var(--admin-font-caption, 12px); line-height:1.6; } .qualification-hint span { color:var(--el-color-danger); } .qualification-label > span { color:var(--el-color-danger); }
.qualification-credit-code { max-width:420px; margin-top:var(--admin-space-6, 16px); } .qualification-credit-code small { display:block; margin-top:var(--admin-space-2, 6px); color:var(--cc-muted); font-size:var(--admin-font-caption, 11px); line-height:1.5; } .field-hint { display:block; margin-top:var(--admin-space-2, 6px); color:var(--cc-muted); line-height:1.5; }
.qualification-table { overflow:hidden; margin-top:14px; border:1px solid var(--cc-border); border-radius:8px; } .qualification-row { display:grid; grid-template-columns:170px minmax(0,1fr) 220px; min-width:0; border-top:1px solid var(--cc-border); } .qualification-row:first-child { border-top:0; } .qualification-row > * { display:flex; min-width:0; align-items:center; } .qualification-header { min-height:44px; background:#f7f8fc; color:var(--cc-muted); font-size:var(--admin-font-caption, 12px); font-weight:600; }
.qualification-table { container-type:inline-size; overflow:hidden; margin-top:14px; border:1px solid var(--cc-border); border-radius:8px; } .qualification-row { display:grid; grid-template-columns:170px minmax(0,1fr) 220px; min-width:0; border-top:1px solid var(--cc-border); } .qualification-row:first-child { border-top:0; } .qualification-row > * { display:flex; min-width:0; align-items:center; } .qualification-header { min-height:44px; background:#f7f8fc; color:var(--cc-muted); font-size:var(--admin-font-caption, 12px); font-weight:600; }
.qualification-label { gap:5px; font-weight:600; } .qualification-label svg { margin-left:auto; color:var(--el-color-success); }
.qualification-optional { padding:2px 5px; border-radius:var(--admin-radius, 4px); background:var(--el-fill-color-light); color:var(--cc-muted); font-size:var(--admin-font-caption, 10px); font-weight:500; line-height:1.4; }
.qualification-file { gap:var(--admin-space-5, 12px); } .qualification-file-control { display:flex; min-width:0; flex:1; flex-wrap:wrap; align-items:center; gap:var(--admin-space-3, 8px); } .file-picker { position:relative; display:inline-flex; flex:0 0 auto; align-items:center; gap:7px; overflow:hidden; padding:0 var(--admin-space-5, 12px); border:1px solid var(--cc-border); border-radius:6px; background:var(--admin-surface, #fff); color:var(--cc-text); cursor:pointer; } .file-picker:focus-within { outline:2px solid color-mix(in srgb,var(--cc-primary) 35%,transparent); outline-offset:2px; } .file-picker.is-disabled { cursor:not-allowed; opacity:.6; } .file-picker input { position:absolute; inset:0; width:100%; height:100%; opacity:0; cursor:inherit; } .qualification-file-name { min-width:0; overflow:hidden; color:var(--cc-muted); font-size:var(--admin-font-caption, 12px); text-overflow:ellipsis; } .qualification-validity :deep(.el-date-editor) { width:100%; min-width:0; } .qualification-error { width:100%; color:var(--el-color-danger); line-height:1.4; }
@@ -1230,6 +1234,12 @@ onMounted(() => { void loadCustomers() })
.qualification-preview-trigger:hover { border-color:var(--cc-primary); color:var(--cc-primary); }
.qualification-preview-trigger:focus-visible { outline:2px solid color-mix(in srgb,var(--cc-primary) 42%,transparent); outline-offset:2px; }
.qualification-preview-trigger:disabled { cursor:wait; opacity:.72; }
@container (max-width:660px) {
.qualification-row { grid-template-columns:112px minmax(0,1fr); }
.qualification-header { display:none; }
.qualification-label { grid-row:span 2; align-items:center; }
.qualification-file,.qualification-validity { min-height:44px; }
}
.qualification-preview-trigger img { width:100%; height:100%; object-fit:contain; background:var(--admin-surface, #fff); }
.qualification-preview-placeholder { display:grid; place-items:center; gap:2px; }
.qualification-preview-placeholder small { font-size:var(--admin-font-caption, 10px); font-weight:700; }
@@ -53,7 +53,7 @@ async function submit() {
if (Object.values(variables).some((value) => !value.trim())) return ElMessage.warning('请填写全部模板变量')
submitting.value = true
try {
await createNotification({ tenantId: form.tenantId, templateCode: form.templateCode, recipientType: form.recipientType, ...(needsIds.value ? { recipientIds } : {}), ...(needsRole.value ? { roleCode: form.roleCode.trim() } : {}), variables: { ...variables }, scheduledAt: form.scheduledAt ? new Date(form.scheduledAt).toISOString() : null, idempotencyKey: idempotencyKey() })
await createNotification({ tenantId: form.tenantId, templateCode: form.templateCode, recipientType: form.recipientType, ...(needsIds.value ? { recipientIds } : {}), ...(needsRole.value ? { roleCode: form.roleCode.trim() } : {}), variables: { ...variables }, scheduledAt: form.scheduledAt || null, idempotencyKey: idempotencyKey() })
ElMessage.success(form.scheduledAt ? '消息已定时' : '消息发送任务已创建')
open.value = false; reset(); emit('success')
} catch (reason) { error.value = getApiErrorDetails(reason, '消息发送失败') } finally { submitting.value = false }
@@ -129,7 +129,7 @@ describe('notification density in native lists and overlays', () => {
await wrapper.get('[role="switch"]').trigger('click')
await flushPromises()
await click(wrapper, '确认定时发送')
expect(api.createNotification).toHaveBeenCalledWith(expect.objectContaining({ tenantId: '0', templateCode: template.templateCode, recipientType: 'PLATFORM_ROLE', roleCode: 'TEST_OPERATOR', variables: {}, scheduledAt: expect.any(String), idempotencyKey: expect.any(String) }))
expect(api.createNotification).toHaveBeenCalledWith(expect.objectContaining({ tenantId: '0', templateCode: template.templateCode, recipientType: 'PLATFORM_ROLE', roleCode: 'TEST_OPERATOR', variables: {}, scheduledAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/), idempotencyKey: expect.any(String) }))
expect(wrapper.emitted('success')).toHaveLength(1)
})
})