From 3e3c7e237adabd8fecb30a2b96a62747f15f7474 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 27 Aug 2026 10:12:20 +0800 Subject: [PATCH] fix(admin): align supplier carrier master data --- .../pages/ContainersView.test.ts | 4 +- .../finance/pages/FinanceG3G4Views.test.ts | 2 +- .../api/platformPartners.test.ts | 3 +- .../counterparties/api/platformPartners.ts | 4 ++ .../counterparties/pages/PartnersView.test.ts | 20 ++++++ .../counterparties/pages/PartnersView.vue | 17 +++++ .../suppliers/api/platformSuppliers.test.ts | 8 +-- .../suppliers/api/platformSuppliers.ts | 3 +- .../suppliers/pages/SupplierDetailView.vue | 4 +- .../suppliers/pages/SupplierFormView.vue | 26 ++++--- .../pages/SupplierRouteReuse.behavior.test.ts | 70 ++++++++++++++++--- .../suppliers/pages/SuppliersView.test.ts | 22 ++++-- src/modules/suppliers/pages/SuppliersView.vue | 20 ++++-- src/modules/suppliers/types.ts | 9 ++- 14 files changed, 168 insertions(+), 44 deletions(-) diff --git a/src/modules/container-loading/pages/ContainersView.test.ts b/src/modules/container-loading/pages/ContainersView.test.ts index b2742151..f11fa1ae 100644 --- a/src/modules/container-loading/pages/ContainersView.test.ts +++ b/src/modules/container-loading/pages/ContainersView.test.ts @@ -361,7 +361,7 @@ describe('ContainersView', () => { }) it('backfills a legacy trucking supplier name and submits the frozen id-name pair', async () => { - vi.mocked(listSuppliers).mockResolvedValue({ records: [{ id: '9007199254740998', tenantId: '1', name: '测试拖车供应商', category: 'TRUCKING', region: 'CN', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-25T10:00:00', updatedBy: null, updatedAt: '2026-08-25T10:00:00', deleted: false, version: 0 }], page: 1, size: 200, total: 1, pages: 1 }) + vi.mocked(listSuppliers).mockResolvedValue({ records: [{ id: '9007199254740998', tenantId: '1', name: '测试拖车供应商', category: 'TRUCKING', carrierCodes: [], region: 'CN', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-25T10:00:00', updatedBy: null, updatedAt: '2026-08-25T10:00:00', deleted: false, version: 0 }], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(getContainer).mockResolvedValueOnce({ id: '9001', containerType: '40HC', warehouseId: '8', terminalTruckingCompanyId: null, terminalTruckingCompany: '测试拖车供应商' } as never) vi.mocked(updateContainer).mockResolvedValue({} as never) const router = createTestRouter() @@ -381,7 +381,7 @@ describe('ContainersView', () => { }) it('clears the trucking supplier id and name together', async () => { - vi.mocked(listSuppliers).mockResolvedValue({ records: [{ id: '9007199254740998', tenantId: '1', name: '测试拖车供应商', category: 'TRUCKING', region: 'CN', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-25T10:00:00', updatedBy: null, updatedAt: '2026-08-25T10:00:00', deleted: false, version: 0 }], page: 1, size: 200, total: 1, pages: 1 }) + vi.mocked(listSuppliers).mockResolvedValue({ records: [{ id: '9007199254740998', tenantId: '1', name: '测试拖车供应商', category: 'TRUCKING', carrierCodes: [], region: 'CN', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-25T10:00:00', updatedBy: null, updatedAt: '2026-08-25T10:00:00', deleted: false, version: 0 }], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(getContainer).mockResolvedValueOnce({ id: '9001', containerType: '40HC', warehouseId: '8', terminalTruckingCompanyId: '9007199254740998', terminalTruckingCompany: '测试拖车供应商' } as never) vi.mocked(updateContainer).mockResolvedValue({} as never) const router = createTestRouter() diff --git a/src/modules/finance/pages/FinanceG3G4Views.test.ts b/src/modules/finance/pages/FinanceG3G4Views.test.ts index 37d20deb..3c278d03 100644 --- a/src/modules/finance/pages/FinanceG3G4Views.test.ts +++ b/src/modules/finance/pages/FinanceG3G4Views.test.ts @@ -79,7 +79,7 @@ describe('Finance G3/G4 views', () => { document.body.innerHTML = '' const customer = { id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [customer], page: 1, size: 200, total: 1, pages: 1 }) - vi.mocked(listSuppliers).mockReset().mockResolvedValue({ records: [{ id: '301', tenantId: '0', name: '拖车供应商A', category: 'TRUCKING', scope: 'DOMESTIC', region: '宁波', legalRepresentative: '', contactName: '', phone: '', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-01', updatedBy: null, updatedAt: '2026-08-01', deleted: false, version: 0 } as Supplier], page: 1, size: 200, total: 1, pages: 1 }) + vi.mocked(listSuppliers).mockReset().mockResolvedValue({ records: [{ id: '301', tenantId: '0', name: '拖车供应商A', category: 'TRUCKING', carrierCodes: [], scope: 'DOMESTIC', region: '宁波', legalRepresentative: '', contactName: '', phone: '', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-01', updatedBy: null, updatedAt: '2026-08-01', deleted: false, version: 0 } as Supplier], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(listFinanceStatements).mockReset().mockResolvedValue([statement]) vi.mocked(getReconciliationStatement).mockReset().mockResolvedValue(statementDetail) vi.mocked(actionReconciliationStatement).mockReset().mockResolvedValue(statementDetail) diff --git a/src/modules/master-data/counterparties/api/platformPartners.test.ts b/src/modules/master-data/counterparties/api/platformPartners.test.ts index 96012acf..935308b8 100644 --- a/src/modules/master-data/counterparties/api/platformPartners.test.ts +++ b/src/modules/master-data/counterparties/api/platformPartners.test.ts @@ -21,6 +21,7 @@ const payload: SavePlatformPartnerRequest = { partnerName: '测试往来单位', partnerType: 'SHIPPER', types: ['SHIPPER'], + carrierCode: null, contactName: '测试联系人', contactPhone: '13800000000', contactEmail: null, @@ -66,7 +67,7 @@ describe('platform partner API', () => { vi.mocked(http.get) .mockResolvedValueOnce({ code: '200', message: 'success', data: { id: '19001', tenantId: 0, dataScopeId: 0, partnerCode: 'CUS-001', partnerName: '测试单位', - partnerType: 'CONSIGNEE', contactName: '张三', contactPhone: '13800000000', contactEmail: null, + partnerType: 'CONSIGNEE', carrierCode: null, contactName: '张三', contactPhone: '13800000000', contactEmail: null, countryCode: 'CN', province: null, city: '深圳市', district: null, addressLine: '测试地址', postalCode: null, status: 'ACTIVE', } }) diff --git a/src/modules/master-data/counterparties/api/platformPartners.ts b/src/modules/master-data/counterparties/api/platformPartners.ts index 9d71d926..8a772dbb 100644 --- a/src/modules/master-data/counterparties/api/platformPartners.ts +++ b/src/modules/master-data/counterparties/api/platformPartners.ts @@ -2,6 +2,7 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' export type PlatformPartnerType = 'CARRIER' | 'SHIPPER' | 'CONSIGNEE' export type PlatformPartnerStatus = 'ACTIVE' | 'INACTIVE' +export type PlatformPartnerCarrierCode = 'MATSON' | 'SML' | 'ZIM' | 'COSCO' | 'EMC' | 'HEDE' | 'OTHER' export interface PlatformPartnerResource { id: number | string @@ -11,6 +12,7 @@ export interface PlatformPartnerResource { partnerName: string partnerType: PlatformPartnerType types: PlatformPartnerType[] + carrierCode: PlatformPartnerCarrierCode | null contactName: string contactPhone: string contactEmail: string | null @@ -54,6 +56,7 @@ export interface SavePlatformPartnerRequest { partnerName: string partnerType: PlatformPartnerType types: PlatformPartnerType[] + carrierCode: PlatformPartnerCarrierCode | null contactName: string contactPhone: string contactEmail: string | null @@ -89,6 +92,7 @@ function normalizePartner(resource: PlatformPartnerResource): PlatformPartnerRes ...resource, dataScopeId: resource.dataScopeId ?? resource.tenantId, types: resource.types?.length ? resource.types : [resource.partnerType], + carrierCode: resource.carrierCode ?? null, remark: resource.remark ?? null, } } diff --git a/src/modules/master-data/counterparties/pages/PartnersView.test.ts b/src/modules/master-data/counterparties/pages/PartnersView.test.ts index c6a634e7..f5d4f5f5 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.test.ts +++ b/src/modules/master-data/counterparties/pages/PartnersView.test.ts @@ -52,6 +52,7 @@ const partner: PlatformPartnerResource = { partnerName: '深圳示例贸易有限公司', partnerType: 'SHIPPER', types: ['SHIPPER'], + carrierCode: null, contactName: '张三', contactPhone: '13800138000', contactEmail: 'contact@example.com', @@ -159,6 +160,25 @@ describe('PartnersView real API integration', () => { expect(document.body.textContent).not.toContain('供应商') }) + it('uses the fixed carrier-code selector when the unit is a shipping company', async () => { + const wrapper = await mountView() + await wrapper.findAll('button').find((button) => button.text().trim() === '新增往来单位')?.trigger('click') + await flushPromises() + const typeSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) => select.props('multiple')) + typeSelect!.vm.$emit('update:modelValue', ['CARRIER']) + await flushPromises() + + expect(document.body.textContent).toContain('船司标准编码') + const carrierValues = new Set(['MATSON', 'SML', 'ZIM', 'COSCO', 'EMC', 'HEDE', 'OTHER']) + const carrierOptions = wrapper.findAllComponents({ name: 'ElOption' }).filter((option) => carrierValues.has(String(option.props('value')))) + expect(carrierOptions.map((option) => option.props('value'))).toEqual(['MATSON', 'SML', 'ZIM', 'COSCO', 'EMC', 'HEDE', 'OTHER']) + expect(carrierOptions.map((option) => option.props('label'))).toContain('美森 (MATSON)') + expect(source).toContain('placeholder="请选择船司标准编码"') + expect(source).toContain("MATSON: '美森'") + expect(source).toContain("OTHER: '其他'") + expect(source).toContain("carrierCode: types.includes('CARRIER') ? form.carrierCode : null") + }) + it('renders a padded detail layout without an empty data-scope cell', async () => { const wrapper = await mountView() const detailButton = wrapper.findAll('button').find((button) => button.text().trim() === '详情') diff --git a/src/modules/master-data/counterparties/pages/PartnersView.vue b/src/modules/master-data/counterparties/pages/PartnersView.vue index 2385fe62..e7d1a86f 100644 --- a/src/modules/master-data/counterparties/pages/PartnersView.vue +++ b/src/modules/master-data/counterparties/pages/PartnersView.vue @@ -23,6 +23,7 @@ import { listPlatformPartners, updatePlatformPartner, type PlatformPartnerResource, + type PlatformPartnerCarrierCode, type PlatformPartnerStatus, type PlatformPartnerType, type SavePlatformPartnerRequest, @@ -94,6 +95,7 @@ const emptyForm = (): PartnerForm => ({ partnerName: '', partnerType: 'SHIPPER', types: ['SHIPPER'], + carrierCode: null, contactName: '', contactPhone: '', contactEmail: null, @@ -121,6 +123,7 @@ const rules: FormRules = { partnerCode: [{ required: true, whitespace: true, message: '请输入往来单位编码', trigger: 'blur' }], partnerName: [{ required: true, whitespace: true, message: '请输入往来单位名称', trigger: 'blur' }], types: [{ required: true, type: 'array', min: 1, message: '请至少选择一个单位类型', trigger: 'change' }], + carrierCode: [{ validator: (_rule, value, callback) => form.types.includes('CARRIER') && !value ? callback(new Error('请选择船司标准编码')) : callback(), trigger: 'change' }], contactName: [{ required: true, whitespace: true, message: '请输入联系人', trigger: 'blur' }], contactPhone: [{ required: true, whitespace: true, message: '请输入联系电话', trigger: 'blur' }], contactEmail: [{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }], @@ -139,6 +142,11 @@ const typeLabels: Record = { CONSIGNEE: '收货单位', } const typeOptions = Object.entries(typeLabels).map(([value, label]) => ({ value: value as PlatformPartnerType, label })) +const carrierCodeLabels: Record = { + MATSON: '美森', SML: 'SML', ZIM: '以星', COSCO: '中远', EMC: '长荣', HEDE: '合德', OTHER: '其他', +} +const carrierCodeOptions = (Object.entries(carrierCodeLabels) as [PlatformPartnerCarrierCode, string][]).map(([value, label]) => ({ value, label: `${label} (${value})` })) +function formatCarrierCode(value?: PlatformPartnerCarrierCode | null) { return value ? `${carrierCodeLabels[value]} (${value})` : '—' } function partnerTypeLabel(type: PlatformPartnerType) { return typeLabels[type] } @@ -225,6 +233,7 @@ function openEdit(row: PlatformPartnerResource) { partnerName: row.partnerName, partnerType: row.types[0], types: [...row.types], + carrierCode: row.carrierCode, contactName: row.contactName, contactPhone: row.contactPhone, contactEmail: row.contactEmail, @@ -249,6 +258,7 @@ function savePayload(): SavePlatformPartnerRequest { partnerName: form.partnerName.trim(), partnerType: types[0], types, + carrierCode: types.includes('CARRIER') ? form.carrierCode : null, contactName: form.contactName.trim(), contactPhone: form.contactPhone.trim(), contactEmail: nullable(form.contactEmail), @@ -342,6 +352,11 @@ function formatUpdatedAt(value?: string | null) { } watch([page, pageSize], () => { void loadPartners() }) +watch(() => form.types.includes('CARRIER'), (carrier) => { + if (carrier) return + form.carrierCode = null + formRef.value?.clearValidate('carrierCode') +}) watch(detailOpen, (open) => { if (open) return detailSequence += 1 @@ -415,6 +430,7 @@ onMounted(() => { 启用停用 +
@@ -446,6 +462,7 @@ onMounted(() => {
单位名称
{{ detail.partnerName }}
状态
{{ detail.status === 'ACTIVE' ? '启用' : '停用' }}
单位类型
{{ partnerTypeLabel(type) }}
+
船司标准编码
{{ formatCarrierCode(detail.carrierCode) }}
联系人
{{ detail.contactName }}
联系电话
{{ detail.contactPhone }}
diff --git a/src/modules/suppliers/api/platformSuppliers.test.ts b/src/modules/suppliers/api/platformSuppliers.test.ts index 5ccd8f3a..eed51af9 100644 --- a/src/modules/suppliers/api/platformSuppliers.test.ts +++ b/src/modules/suppliers/api/platformSuppliers.test.ts @@ -8,7 +8,7 @@ vi.mock('@/shared/api/http', async (original) => ({ http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() }, })) -const payload: SaveSupplierRequest = { name: '测试供应商', category: 'BOOKING', region: '宁波', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: '9007199254740995', status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null } +const payload: SaveSupplierRequest = { name: '测试供应商', category: 'BOOKING', carrierCodes: ['MATSON', 'SML'], region: '宁波', legalRepresentative: '张三', contactName: '李四', phone: '13800000000', email: null, address: null, businessLicenseNo: null, licenseFileId: '9007199254740995', status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null } const resource = { id: 9007199254740993, tenantId: 0, ...payload, createdBy: 9007199254740994, createdAt: '2026-08-13T10:00:00', updatedBy: null, updatedAt: '2026-08-13T10:00:00', deleted: false, version: 1 } describe('platform supplier API', () => { @@ -18,10 +18,10 @@ describe('platform supplier API', () => { vi.mocked(http.get) .mockResolvedValueOnce({ code: '200', data: { records: [resource], page: 1, size: 20, total: 1, pages: 1 } }) .mockResolvedValueOnce({ code: '200', data: { booking: 1, customs: 2, trucking: 3, certificate: 4 } }) - const page = await listSuppliers({ page: 1, size: 20, category: 'BOOKING', keyword: '测试' }) - expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/suppliers', { params: expect.objectContaining({ category: 'BOOKING', keyword: '测试' }) }) + const page = await listSuppliers({ page: 1, size: 20, category: 'BOOKING', keyword: '测试', carrierCode: 'MATSON' }) + expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/suppliers', { params: expect.objectContaining({ category: 'BOOKING', keyword: '测试', carrierCode: 'MATSON' }) }) expect(vi.mocked(http.get).mock.calls[0]?.[1]?.params).not.toHaveProperty('scope') - expect(page.records[0]).toMatchObject({ id: expect.any(String), tenantId: '0', licenseFileId: expect.any(String), createdBy: expect.any(String) }) + expect(page.records[0]).toMatchObject({ id: expect.any(String), tenantId: '0', carrierCodes: ['MATSON', 'SML'], licenseFileId: expect.any(String), createdBy: expect.any(String) }) expect(await getSupplierStats()).toEqual({ booking: 1, customs: 2, trucking: 3, certificate: 4 }) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/suppliers/stats') }) diff --git a/src/modules/suppliers/api/platformSuppliers.ts b/src/modules/suppliers/api/platformSuppliers.ts index ec361b31..53190823 100644 --- a/src/modules/suppliers/api/platformSuppliers.ts +++ b/src/modules/suppliers/api/platformSuppliers.ts @@ -9,7 +9,7 @@ function normalizeSupplier(raw: Supplier): Supplier { return { ...raw, id: String(raw.id), tenantId: String(raw.tenantId), - carrierCode: raw.carrierCode ?? null, + carrierCodes: [...raw.carrierCodes], licenseFileId: id(raw.licenseFileId), createdBy: id(raw.createdBy), updatedBy: id(raw.updatedBy), version: Number(raw.version), deleted: Boolean(raw.deleted), } @@ -21,6 +21,7 @@ export async function listSuppliers(query: SupplierQuery): Promise page: query.page ?? 1, size: query.size ?? 20, category: query.category, region: query.region?.trim() || undefined, status: query.status || undefined, keyword: query.keyword?.trim() || undefined, + carrierCode: query.carrierCode || undefined, }, }) const data = unwrapApiResponse(response) diff --git a/src/modules/suppliers/pages/SupplierDetailView.vue b/src/modules/suppliers/pages/SupplierDetailView.vue index f486e2d4..d0c7ecc2 100644 --- a/src/modules/suppliers/pages/SupplierDetailView.vue +++ b/src/modules/suppliers/pages/SupplierDetailView.vue @@ -11,7 +11,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { useAccessPreviewStore } from '@/core/access/accessPreview' import SupplierLicenseDialog from '@/modules/suppliers/components/SupplierLicenseDialog.vue' import { getSupplier } from '@/modules/suppliers/api/platformSuppliers' -import { categoryLabels, formatCarrierCode, settlementLabels, statusLabels, type Supplier } from '@/modules/suppliers/types' +import { categoryLabels, formatCarrierCodes, settlementLabels, statusLabels, type Supplier } from '@/modules/suppliers/types' const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore() const canView = computed(() => access.can('platform:supplier:list')) @@ -19,7 +19,7 @@ const supplierId = computed(() => typeof route.params.id === 'string' ? route.pa const supplier = ref(null); const loading = ref(false); const error = ref(null); const licenseOpen = ref(false) let loadSequence = 0 const items = computed(() => supplier.value ? [ - ['供应商名称', supplier.value.name], ['供应商类别', categoryLabels[supplier.value.category]], ['船司标准编码', formatCarrierCode(supplier.value.carrierCode)], ['地区', supplier.value.region], + ['供应商名称', supplier.value.name], ['供应商类别', categoryLabels[supplier.value.category]], ['可代理船公司', supplier.value.category === 'BOOKING' ? formatCarrierCodes(supplier.value.carrierCodes) : '—'], ['地区', supplier.value.region], ['法人代表', supplier.value.legalRepresentative], ['联系人', supplier.value.contactName], ['联系电话', supplier.value.phone], ['电子邮箱', supplier.value.email || '—'], ['营业执照号', supplier.value.businessLicenseNo || '—'], ['结算周期', settlementLabels[supplier.value.settlementPeriod]], ['公司地址', supplier.value.address || '—'], ['备注', supplier.value.remark || '—'], ] : []) diff --git a/src/modules/suppliers/pages/SupplierFormView.vue b/src/modules/suppliers/pages/SupplierFormView.vue index 55aab772..c0b48421 100644 --- a/src/modules/suppliers/pages/SupplierFormView.vue +++ b/src/modules/suppliers/pages/SupplierFormView.vue @@ -1,5 +1,5 @@