diff --git a/src/modules/customers/pages/CustomersView.test.ts b/src/modules/customers/pages/CustomersView.test.ts index fd7bc406..9174fa1c 100644 --- a/src/modules/customers/pages/CustomersView.test.ts +++ b/src/modules/customers/pages/CustomersView.test.ts @@ -11,6 +11,7 @@ import { downloadTenantQualification, createPlatformTenantImpersonation, getPlatformTenant, + getPlatformTenantDimensionInitializationStatus, listEnterpriseReviews, listPlatformTenants, listTenantQualifications, @@ -23,6 +24,8 @@ import { type EnterpriseReview, type PlatformTenantResource, } from '@/modules/iam' +import { getCustomerDimensionConfig, updateCustomerDimensionConfig, updateCustomerDimensionDefaultCalculationMode } from '@/modules/dimension-config' +import { createCustomerChargeRule, setCustomerChargeRuleDefault } from '@/modules/fba-pricing' import { createPlatformExportTask } from '@/modules/downloads' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' @@ -61,6 +64,7 @@ vi.mock('@/modules/iam', () => ({ createPlatformTenantImpersonation: vi.fn(), downloadTenantQualification: vi.fn(), getPlatformTenant: vi.fn(), + getPlatformTenantDimensionInitializationStatus: vi.fn(), listEnterpriseReviews: vi.fn(), listPlatformTenantOrderCounts: vi.fn(), listPlatformTenants: vi.fn(), @@ -75,6 +79,12 @@ vi.mock('@/modules/iam', () => ({ })) vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() })) +vi.mock('@/modules/dimension-config', () => ({ + getCustomerDimensionConfig: vi.fn(), + updateCustomerDimensionConfig: vi.fn(), + updateCustomerDimensionDefaultCalculationMode: vi.fn(), +})) +vi.mock('@/modules/fba-pricing', () => ({ createCustomerChargeRule: vi.fn(), setCustomerChargeRuleDefault: vi.fn() })) vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() })) class ResizeObserverStub implements ResizeObserver { @@ -145,6 +155,26 @@ describe('CustomersView', () => { page: 1, size: 10, total: 1, pages: 1, }) vi.mocked(getPlatformTenant).mockResolvedValue(fullTenant) + vi.mocked(getPlatformTenantDimensionInitializationStatus).mockResolvedValue({ + tenantId: fullTenant.id, status: 'READY', attemptCount: 0, nextRetryAt: null, + lastErrorCode: null, lastErrorMessage: null, updatedAt: '2026-08-01T10:00:00', + }) + const dimensionRules = [ + ['SMALL', '小箱', 30, 20, 20], + ['MEDIUM', '中箱', 40, 30, 30], + ['LARGE', '大箱', 50, 40, 40], + ['EXTRA_LARGE', '特大箱', 60, 50, 50], + ].map(([boxType, boxTypeName, length, width, height], index) => ({ + id: `dimension-${index}`, boxType, boxTypeName, innerLengthCm: length, innerWidthCm: width, + innerHeightCm: height, edgeAdditionCm: 2, outerLengthCm: Number(length) + 4, + outerWidthCm: Number(width) + 4, outerHeightCm: Number(height) + 4, volumeCbm: 0.01, + remark: String(boxTypeName), version: 0, createdAt: null, updatedAt: null, + })) + vi.mocked(getCustomerDimensionConfig).mockResolvedValue({ tenantId: fullTenant.id, defaultCalculationMode: 'ACTUAL', rules: dimensionRules } as never) + vi.mocked(updateCustomerDimensionConfig).mockResolvedValue({ tenantId: fullTenant.id, defaultCalculationMode: 'ACTUAL', rules: dimensionRules } as never) + vi.mocked(updateCustomerDimensionDefaultCalculationMode).mockResolvedValue({ tenantId: fullTenant.id, defaultCalculationMode: 'ADD', rules: dimensionRules } as never) + vi.mocked(createCustomerChargeRule).mockResolvedValue({ id: 'charge-rule-1' } as never) + vi.mocked(setCustomerChargeRuleDefault).mockResolvedValue(undefined) vi.mocked(listTenantQualifications).mockResolvedValue([]) vi.mocked(listEnterpriseReviews).mockResolvedValue([]) vi.mocked(createPlatformTenant).mockResolvedValue({ @@ -185,7 +215,7 @@ describe('CustomersView', () => { it('matches the frozen prototype containers for customer operations', () => { expect(customersSource).toContain("router.push({ name: 'customer-detail'") expect(customersSource).toContain("'min(640px, calc(100vw - 28px))'") - expect(customersSource).toContain("'min(680px, calc(100vw - 28px))'") + expect(customersSource).toContain("'min(1120px, calc(100vw - 28px))'") expect(customersSource).toContain('width="min(520px, calc(100vw - 28px))" class="customer-password-dialog"') expect(customersSource).toContain('width="min(1040px, calc(100vw - 28px))" class="review-decision-dialog"') expect(customersSource).not.toContain(' { ) }) + it('saves carton dimensions and all four weight coefficients during customer creation', async () => { + const auth = useAuthStore() + auth.permissions = [...auth.permissions, 'platform:customer:create', 'firstleg:dimension-config:view', 'firstleg:dimension-config:edit', 'firstleg:fba-pricing:rule:edit'] + useAccessPreviewStore().initializeAfterLogin() + const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) + await flushPromises() + ;[...document.querySelectorAll('button')] + .find((button) => button.textContent?.trim() === '新增客户')?.click() + await flushPromises() + + await wrapper.get('input[placeholder="请输入工商登记公司名称"]').setValue(fullTenant.tenantName) + await wrapper.get('input[placeholder="请输入营业执照上的企业名称"]').setValue(fullTenant.legalName) + await wrapper.get('input[placeholder="请输入法定代表人姓名"]').setValue(fullTenant.legalRepresentative) + await wrapper.get('input[placeholder="请输入主要联系人姓名"]').setValue(fullTenant.contactName) + await wrapper.get('input[placeholder="请输入公司联系电话"]').setValue(fullTenant.contactPhone) + await wrapper.get('input[placeholder="请输入18位大写字母或数字"]').setValue(fullTenant.unifiedSocialCreditCode) + const licenseFile = new File(['license'], 'license.png', { type: 'image/png' }) + selectQualificationFile(0, licenseFile) + const billingCheckbox = [...document.querySelectorAll('.el-checkbox')] + .find((checkbox) => checkbox.textContent?.includes('按箱规尺寸计算')) + billingCheckbox?.click() + await flushPromises() + + expect(document.body.textContent).toContain('权重系数') + expect(document.querySelectorAll('.creation-billing-table tbody tr')).toHaveLength(4) + ;[...document.querySelectorAll('button')] + .find((button) => button.textContent?.includes('保存资料和资质'))?.click() + await flushPromises() + + expect(updateCustomerDimensionConfig).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ + rules: expect.arrayContaining([expect.objectContaining({ boxType: 'SMALL', innerLengthCm: 30, edgeAdditionCm: 2 })]), + })) + expect(createCustomerChargeRule).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ + customerId: fullTenant.id, + remark: expect.stringMatching(/SMALL:30,20,20;系数:1\.1;重量:5/), + })) + expect(setCustomerChargeRuleDefault).toHaveBeenCalledWith(fullTenant.id, 'charge-rule-1') + expect(updateCustomerDimensionDefaultCalculationMode).toHaveBeenCalledWith(fullTenant.id, 'ADD') + }) + it('keeps the company contact phone editable without changing a member login phone', async () => { const activeTenant = { ...fullTenant, status: 'ACTIVE' as const } vi.mocked(listPlatformTenants).mockResolvedValue({ records: [activeTenant], page: 1, size: 10, total: 1, pages: 1 }) diff --git a/src/modules/customers/pages/CustomersView.vue b/src/modules/customers/pages/CustomersView.vue index 812dfa27..4e7e695e 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -247,10 +247,12 @@ let loadSequence = 0 let reviewLoadSequence = 0 let editorLoadSequence = 0 const editorTitle = computed(() => editingId.value ? '编辑客户' : '新增客户') -const editorDialogWidth = computed(() => editingId.value - ? 'min(640px, calc(100vw - 28px))' - : 'min(1120px, calc(100vw - 28px))') -const canConfigureCreationBilling = computed(() => access.can('firstleg:dimension-config:edit') +const editorDialogWidth = computed(() => creationBillingSession.value + ? 'min(1120px, calc(100vw - 28px))' + : 'min(640px, calc(100vw - 28px))') +const canConfigureCreationBilling = computed(() => access.can('platform:customer:list') + && access.can('firstleg:dimension-config:view') + && access.can('firstleg:dimension-config:edit') && access.can('firstleg:fba-pricing:rule:edit')) const canUploadQualifications = computed(() => !editorStatus.value || ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value)) const canSubmitReview = computed(() => Boolean(editingId.value) && ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value || ''))