diff --git a/src/modules/customers/pages/CustomersView.test.ts b/src/modules/customers/pages/CustomersView.test.ts index 9174fa1c..fd7bc406 100644 --- a/src/modules/customers/pages/CustomersView.test.ts +++ b/src/modules/customers/pages/CustomersView.test.ts @@ -11,7 +11,6 @@ import { downloadTenantQualification, createPlatformTenantImpersonation, getPlatformTenant, - getPlatformTenantDimensionInitializationStatus, listEnterpriseReviews, listPlatformTenants, listTenantQualifications, @@ -24,8 +23,6 @@ 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' @@ -64,7 +61,6 @@ 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(), @@ -79,12 +75,6 @@ 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 { @@ -155,26 +145,6 @@ 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({ @@ -215,7 +185,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(1120px, calc(100vw - 28px))'") + expect(customersSource).toContain("'min(680px, 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 4e7e695e..785aca13 100644 --- a/src/modules/customers/pages/CustomersView.vue +++ b/src/modules/customers/pages/CustomersView.vue @@ -13,7 +13,6 @@ import { createPlatformTenantImpersonation, downloadTenantQualification, getPlatformTenant, - getPlatformTenantDimensionInitializationStatus, listEnterpriseReviews, listPlatformTenantOrderCounts, listPlatformTenants, @@ -33,14 +32,6 @@ import { type PlatformTenantResource, type PlatformTenantStatus, } from '@/modules/iam' -import { - getCustomerDimensionConfig, - updateCustomerDimensionConfig, - updateCustomerDimensionDefaultCalculationMode, - type CustomerDimensionBoxType, - type CustomerDimensionCalculationMode, -} from '@/modules/dimension-config' -import { createCustomerChargeRule, setCustomerChargeRuleDefault } from '@/modules/fba-pricing' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import CopyButton from '@/shared/components/CopyButton.vue' @@ -145,25 +136,6 @@ const queryFields: QueryField[] = [ ] type CustomerForm = CreatePlatformTenantRequest & { unifiedSocialCreditCode: string } -type CreationBillingRow = { - boxType: CustomerDimensionBoxType - boxTypeName: string - innerLengthCm: number - innerWidthCm: number - innerHeightCm: number - edgeAdditionCm: number - coefficient: number - weight: number - remark: string -} - -const newCreationBillingRows = (): CreationBillingRow[] => [ - { boxType: 'SMALL', boxTypeName: '小箱', innerLengthCm: 30, innerWidthCm: 20, innerHeightCm: 20, edgeAdditionCm: 2, coefficient: 1.1, weight: 5, remark: '标准小箱' }, - { boxType: 'MEDIUM', boxTypeName: '中箱', innerLengthCm: 40, innerWidthCm: 30, innerHeightCm: 30, edgeAdditionCm: 2, coefficient: 1.1, weight: 8, remark: '标准中箱' }, - { boxType: 'LARGE', boxTypeName: '大箱', innerLengthCm: 50, innerWidthCm: 40, innerHeightCm: 40, edgeAdditionCm: 2, coefficient: 1.1, weight: 12, remark: '标准大箱' }, - { boxType: 'EXTRA_LARGE', boxTypeName: '特大箱', innerLengthCm: 60, innerWidthCm: 50, innerHeightCm: 50, edgeAdditionCm: 2, coefficient: 1.1, weight: 15, remark: '特大箱' }, -] - const emptyForm = (): CustomerForm => ({ tenantName: '', legalName: '', legalRepresentative: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '', contactEmail: '', contactWechat: '', servicePackage: '基础版', salesOwnerName: '', paymentTermDays: 30, @@ -171,11 +143,6 @@ const emptyForm = (): CustomerForm => ({ registeredAddress: '', internalRemark: '', }) const form = ref(emptyForm()) -const creationBillingEnabled = ref(false) -const creationBillingDefaultMode = ref>('ADD') -const creationBillingRows = ref(newCreationBillingRows()) -const creationBillingSession = ref(false) -const savedCreationBillingSnapshot = ref('') const rules: FormRules = { tenantName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }], legalName: [{ required: true, message: '请输入营业执照上的企业名称', trigger: 'blur' }], @@ -247,13 +214,9 @@ let loadSequence = 0 let reviewLoadSequence = 0 let editorLoadSequence = 0 const editorTitle = computed(() => editingId.value ? '编辑客户' : '新增客户') -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 editorDialogWidth = computed(() => editingId.value + ? 'min(640px, calc(100vw - 28px))' + : 'min(680px, calc(100vw - 28px))') const canUploadQualifications = computed(() => !editorStatus.value || ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value)) const canSubmitReview = computed(() => Boolean(editingId.value) && ['PROFILE_REQUIRED', 'REJECTED'].includes(editorStatus.value || '')) const uploadingQualifications = computed(() => qualifications.value.some((item) => item.uploading)) @@ -423,110 +386,12 @@ function clearQualificationPreviews() { function resetEditor() { clearQualificationPreviews() form.value = emptyForm(); qualifications.value = newQualificationDrafts(); creditCodeEstablished.value = false; qualificationLoadError.value = ''; savedFormSnapshot.value = '' - creationBillingEnabled.value = false - creationBillingDefaultMode.value = 'ADD' - creationBillingRows.value = newCreationBillingRows() - creationBillingSession.value = false - savedCreationBillingSnapshot.value = '' } function openCreate() { editorLoadSequence += 1 - editingId.value = null; editorStatus.value = null; resetEditor(); creationBillingSession.value = true; editorOpen.value = true + editingId.value = null; editorStatus.value = null; resetEditor(); editorOpen.value = true nextTick(() => formRef.value?.clearValidate()) } - -function creationBillingSnapshot() { - return JSON.stringify({ - enabled: creationBillingEnabled.value, - defaultMode: creationBillingDefaultMode.value, - rows: creationBillingRows.value, - }) -} - -function validateCreationBillingRules() { - if (!creationBillingEnabled.value) return true - if (!canConfigureCreationBilling.value) { - ElMessage.error('当前账号缺少箱规尺寸或计费规则编辑权限,无法随客户一并保存') - return false - } - if (creationBillingRows.value.some((row) => ![ - row.innerLengthCm, row.innerWidthCm, row.innerHeightCm, - ].every((value) => Number.isFinite(value) && value >= 0.001))) { - ElMessage.error('四档箱规的内径长、宽、高必须大于 0') - return false - } - if (creationBillingRows.value.some((row) => !Number.isFinite(row.edgeAdditionCm) - || row.edgeAdditionCm < 0.001 || row.edgeAdditionCm > 100)) { - ElMessage.error('四档箱规的单边加宽必须在 0.001 至 100cm 之间') - return false - } - if (creationBillingRows.value.some((row) => !Number.isFinite(row.coefficient) || row.coefficient < 0.001)) { - ElMessage.error('四档箱规的权重系数必须大于 0') - return false - } - if (creationBillingRows.value.some((row) => !Number.isFinite(row.weight) || row.weight < 0.001)) { - ElMessage.error('四档箱规的重量必须大于 0') - return false - } - return true -} - -function wait(milliseconds: number) { - return new Promise((resolve) => window.setTimeout(resolve, milliseconds)) -} - -async function waitForDimensionInitialization(tenantId: string) { - for (let attempt = 0; attempt < 20; attempt += 1) { - const status = await getPlatformTenantDimensionInitializationStatus(tenantId) - if (status.status === 'READY') return - if (status.status === 'FAILED') { - throw new Error(status.lastErrorMessage || '客户默认箱规初始化失败') - } - await wait(500) - } - throw new Error('客户默认箱规仍在初始化,请稍后再次点击保存') -} - -async function saveCreationBillingRules(tenantId: string, customerName: string) { - if (!creationBillingEnabled.value || creationBillingSnapshot() === savedCreationBillingSnapshot.value) return - await waitForDimensionInitialization(tenantId) - const current = await getCustomerDimensionConfig(tenantId) - const versions = new Map(current.rules.map((rule) => [rule.boxType, rule.version])) - await updateCustomerDimensionConfig(tenantId, { - rules: creationBillingRows.value.map((row) => ({ - boxType: row.boxType, - innerLengthCm: row.innerLengthCm, - innerWidthCm: row.innerWidthCm, - innerHeightCm: row.innerHeightCm, - edgeAdditionCm: row.edgeAdditionCm, - remark: row.remark || null, - version: versions.get(row.boxType) ?? 0, - })), - }) - const firstCoefficient = creationBillingRows.value[0].coefficient - const created = await createCustomerChargeRule(tenantId, { - ruleCode: `RULE-${Date.now()}`, - customerId: tenantId, - customerName, - businessType: 'FBA', - chargeMode: 'CBM', - taxMode: 'TAX_INCLUDED', - conversionFactor: 167, - lengthFactor: firstCoefficient, - widthFactor: firstCoefficient, - heightFactor: firstCoefficient, - defaultRule: false, - roundingScale: 2, - roundingMode: 'HALF_UP', - currency: 'CNY', - effectiveFrom: new Date().toISOString().slice(0, 19), - effectiveTo: null, - remark: creationBillingRows.value.map((row) => `${row.boxType}:${row.innerLengthCm},${row.innerWidthCm},${row.innerHeightCm};系数:${row.coefficient};重量:${row.weight};${row.boxTypeName}:${row.remark || '—'}`).join(';'), - }) - await setCustomerChargeRuleDefault(tenantId, created.id) - await updateCustomerDimensionDefaultCalculationMode(tenantId, creationBillingDefaultMode.value) - savedCreationBillingSnapshot.value = creationBillingSnapshot() -} function mapTenantToForm(row: PlatformTenantResource): CustomerForm { return { tenantName: row.tenantName, legalName: row.legalName || '', legalRepresentative: row.legalRepresentative || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '', @@ -697,7 +562,6 @@ async function uploadSelectedQualifications(tenantId: string) { async function saveCustomer() { if (!formRef.value || !(await formRef.value.validate().catch(() => false))) return - if (!validateCreationBillingRules()) return if (!requiredQualificationPackageComplete.value) { ElMessage.warning(`请完整保存必填业务项:${missingQualificationPackageItems.value.join('、')}`) return @@ -728,15 +592,6 @@ async function saveCustomer() { editorStatus.value = saved.status savedFormSnapshot.value = JSON.stringify(form.value) const failures = hadSelectedQualifications ? await uploadSelectedQualifications(String(saved.id)) : [] - if (creationBillingSession.value && creationBillingEnabled.value) { - try { - await saveCreationBillingRules(String(saved.id), saved.tenantName) - } catch (error) { - ElMessage.error(`客户资料已保存,但箱规尺寸和权重系数未完整保存:${getApiErrorMessage(error, '请稍后再次点击保存')}`) - await loadCustomers() - return - } - } if (failures.length) { ElMessage.warning(`客户资料已保存,但${failures.join('、')}上传失败;失败文件已保留,请修正后再次保存`) } else if (hadBusinessLicense && hadLegalPersonId) { @@ -1235,26 +1090,6 @@ onMounted(() => { void loadCustomers() }) - -
- 按箱规尺寸计算 - - 箱规尺寸 - 权重系数 - -
-

当前账号需同时具备箱规尺寸和客户计费规则编辑权限,才能在创建时配置。

- -
-
{{ statusMeta(editorStatus).label }}
{{ creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照共同保存;法人身份证为选填。' }} @@ -1400,7 +1235,6 @@ onMounted(() => { void loadCustomers() }) :deep(.customer-list-table .el-tag--danger) { background:linear-gradient(135deg,rgba(239,68,68,.15),rgba(220,38,38,.1)); color:#dc2626; } :deep(.customer-list-table .el-tag--info) { background:linear-gradient(135deg,rgba(102,126,234,.15),rgba(118,75,162,.1)); color:#667eea; } .capital-input { display:grid; grid-template-columns:minmax(0,1fr) 130px; width:100%; gap:8px; } .capital-input :deep(.el-input-number),.capital-input :deep(.el-select) { width:100%; } -.creation-billing-heading { display:flex; min-height:36px; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; } .creation-billing-heading :deep(.el-checkbox) { margin-right:0; font-weight:650; } .creation-billing-hint,.creation-billing-permission-hint { margin:8px 0 0; color:var(--cc-muted); font-size:12px; line-height:1.6; } .creation-billing-permission-hint { color:var(--el-color-warning); } .creation-billing-table-wrap { overflow-x:auto; margin-top:12px; border:1px solid var(--cc-border); border-radius:8px; } .creation-billing-table { width:100%; min-width:1040px; border-collapse:collapse; } .creation-billing-table th,.creation-billing-table td { padding:9px 10px; border-bottom:1px solid var(--cc-border); text-align:left; white-space:nowrap; } .creation-billing-table thead th { background:#f7f8fc; color:var(--cc-muted); font-size:12px; } .creation-billing-table tbody tr:last-child th,.creation-billing-table tbody tr:last-child td { border-bottom:0; } .creation-billing-table tbody th { color:var(--cc-text); font-size:13px; } .creation-billing-table :deep(.el-input-number) { width:112px; } .creation-billing-table :deep(.el-input) { width:130px; } .qualification-heading { display:flex; justify-content:flex-end; margin-bottom:12px; } .qualification-hint { margin:12px 0 0; color:var(--cc-muted); font-size:12px; line-height:1.6; } .qualification-hint span,.qualification-label > span { color:var(--el-color-danger); } .qualification-credit-code { max-width:420px; margin-top:16px; } .qualification-credit-code small,.field-hint { display:block; margin-top:6px; color:var(--cc-muted); font-size:11px; 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; min-height:68px; align-items:center; padding:12px 14px; } .qualification-header { min-height:44px; background:#f7f8fc; color:var(--cc-muted); font-size:12px; font-weight:600; } .qualification-header > * { min-height:44px; } diff --git a/src/modules/iam/api/platformIam.ts b/src/modules/iam/api/platformIam.ts index 4d8aade7..2ff8f2c8 100644 --- a/src/modules/iam/api/platformIam.ts +++ b/src/modules/iam/api/platformIam.ts @@ -147,16 +147,6 @@ export interface PlatformTenantCreationResponse { temporaryPassword: string } -export interface CustomerDimensionInitializationStatus { - tenantId: number | string - status: 'PENDING' | 'READY' | 'FAILED' - attemptCount: number - nextRetryAt: string | null - lastErrorCode: string | null - lastErrorMessage: string | null - updatedAt: string | null -} - export interface ResetPlatformTenantOwnerPasswordRequest { newPassword: string forceChangePassword: boolean @@ -373,10 +363,6 @@ export function getPlatformTenant(tenantId: number | string) { return getData(`/platform/v1/iam/tenants/${tenantId}`) } -export function getPlatformTenantDimensionInitializationStatus(tenantId: number | string) { - return getData(`/platform/v1/iam/tenants/${tenantId}/dimension-initialization`) -} - export async function checkPlatformTenantContactPhone(contactPhone: string, excludeTenantId?: number | string) { const response = await http.get, ApiEnvelope>( '/platform/v1/iam/tenants/contact-phone-availability',