From c464f47fb9649ab72e4526ae31be27b6517073bd Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 15 Aug 2026 10:03:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/FinanceCustomerAccountsView.vue | 2 +- .../pages/FinanceCustomerArapView.test.ts | 21 +++++++++++++++++-- .../finance/pages/FinanceCustomerArapView.vue | 18 ++++++++++------ .../pages/FinanceG3G4Operations.test.ts | 1 + .../pages/OceanBookingDetailView.test.ts | 9 ++++++-- .../pages/OceanBookingDetailView.vue | 16 ++++++-------- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.vue b/src/modules/finance/pages/FinanceCustomerAccountsView.vue index f9c548bb..550dc18a 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.vue +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.vue @@ -61,7 +61,7 @@ const columns: DataTableColumn[] = [ { key: 'receivedInPeriod', label: '本期已收(CNY)', width: 140 }, { key: 'outstandingPayable', label: '待付(CNY)', width: 130 }, { key: 'status', label: '客户状态', width: 110, slotName: 'status' }, - { key: 'lastChangedAt', label: '最近变动', width: 170 }, + { key: 'lastChangedAt', label: '最近变动', width: 170, slotName: 'lastChangedAt' }, ] const ledgerQueryFields: QueryField[] = [ { key: 'businessType', type: 'select', label: '流水类型', width: 180, placeholder: '全部类型', options: [ diff --git a/src/modules/finance/pages/FinanceCustomerArapView.test.ts b/src/modules/finance/pages/FinanceCustomerArapView.test.ts index ea7ed137..36f461b3 100644 --- a/src/modules/finance/pages/FinanceCustomerArapView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerArapView.test.ts @@ -167,15 +167,32 @@ describe('FinanceCustomerArapView G2 operations', () => { expect(wrapper.router.currentRoute.value.name).toBe('finance-customer-arap-detail') }) - it('resolves blank detail customer names from master data and preserves unknown source codes', async () => { + it('resolves blank detail customer names and keeps source labels business-facing', async () => { vi.mocked(getFinanceFeeSource).mockResolvedValueOnce({ ...detail, customerName: '', sourceType: 'NEW_SOURCE' }) const wrapper = await mountView(['platform:finance:customer-arap:list'], '/finance/customer-arap/42') expect(wrapper.text()).toContain('运费 · 客户A') - expect(wrapper.text()).toContain('未识别来源(NEW_SOURCE)') + expect(wrapper.text()).toContain('其他费用来源') expect(wrapper.text()).not.toContain('其他来源') + expect(wrapper.text()).not.toContain('NEW_SOURCE') expect(wrapper.text()).not.toContain('42 ·') }) + it('hides technical source identifiers and versions from list rows', async () => { + vi.mocked(listFinanceFeePool).mockResolvedValue([ + { poolItemId: 'PAYABLE:358', feeSourceId: '358', customerId: '10001', direction: 'PAYABLE', sourceType: 'CONTAINER_FEE', sourceId: '2088285842638942210', sourceVersion: 1, orderId: null, inboundId: null, containerId: '2088285834627821569', feeItemName: '吊机费', amount: 300, currency: 'CNY', status: 'AVAILABLE', createdAt: '2026-08-14T23:25:19.684' }, + ]) + const wrapper = await mountView(['platform:finance:customer-arap:list']) + await (wrapper.vm as unknown as { load(model: Record): Promise }).load({ customerId: '', direction: '', status: '', keyword: '' }) + const text = wrapper.text() + expect(text).toContain('应付费用 · 吊机费') + expect(text).toContain('柜次费用 · 关联柜次') + expect(text).toContain('2026/8/14 23:25:19') + expect(text).not.toContain('PAYABLE:358') + expect(text).not.toContain('2088285842638942210') + expect(text).not.toContain('/v1') + expect(text).not.toContain('CONTAINER_FEE') + }) + it.each(['10001', '客户 10001', '客户10001'])('never displays customer id placeholder %s as a customer name', async (customerName) => { vi.mocked(listPlatformTenants).mockResolvedValueOnce({ records: [], page: 1, size: 200, total: 0, pages: 1 }) vi.mocked(getFinanceFeeSource).mockResolvedValueOnce({ ...detail, customerName }) diff --git a/src/modules/finance/pages/FinanceCustomerArapView.vue b/src/modules/finance/pages/FinanceCustomerArapView.vue index 9eca5cd7..e62a8021 100644 --- a/src/modules/finance/pages/FinanceCustomerArapView.vue +++ b/src/modules/finance/pages/FinanceCustomerArapView.vue @@ -89,9 +89,9 @@ const queryFields = computed(() => [ ]) const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: (row: ArapRow) => row.direction === 'RECEIVABLE' && Boolean(row.feeSourceId) }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' }, - { key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' }, { key: 'documentNo', label: '费用单据', minWidth: 170 }, { key: 'sourceType', label: '来源类型', width: 120 }, + { key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' }, { key: 'documentNo', label: '费用单据', minWidth: 170 }, { key: 'sourceType', label: '来源类型', width: 120, slotName: 'sourceType' }, { key: 'sourceId', label: '来源追溯', minWidth: 200 }, { key: 'feeName', label: '费用项目', minWidth: 150 }, { key: 'amount', label: '金额(CNY)', width: 130 }, - { key: 'reviewStatus', label: '审核状态', width: 120, slotName: 'status' }, { key: 'settlementIncluded', label: '参与结算', width: 110, slotName: 'included' }, { key: 'statementGenerated', label: '已生成对账单', width: 140, slotName: 'generated' }, { key: 'businessDate', label: '业务发生日', width: 130 }, + { key: 'reviewStatus', label: '审核状态', width: 120, slotName: 'status' }, { key: 'settlementIncluded', label: '参与结算', width: 110, slotName: 'included' }, { key: 'statementGenerated', label: '已生成对账单', width: 140, slotName: 'generated' }, { key: 'businessDate', label: '业务发生日', width: 170, slotName: 'businessDate' }, ] function customerName(customerId: string, snapshot?: string) { const masterName = customers.value.find((item) => String(item.id) === String(customerId))?.tenantName?.trim() @@ -109,9 +109,15 @@ function sourceTypeDisplay(value: unknown) { } function feeDocumentLabel(direction: ArapRow['direction'], feeName: unknown) { const prefix = direction === 'RECEIVABLE' ? '应收费用' : '应付费用' - const name = String(feeName || '').trim() + const name = displayFeeName(feeName) return name ? `${prefix} · ${name}` : prefix } +function displayFeeName(value: unknown) { + const name = String(value || '').trim() + if (!name) return '' + if (/^ADMIN-API-\d+-整柜成本$/i.test(name)) return '整柜成本' + return name +} function sourceSummary(sourceType: unknown, orderId: unknown, inboundId: unknown, containerId: unknown) { const relations = [inboundId ? '关联入库单' : '', orderId ? '关联订单' : '', containerId ? '关联柜次' : ''].filter(Boolean) const type = sourceTypeDisplay(sourceType) @@ -150,7 +156,7 @@ async function load(model: QueryModel = filters.value) { try { const direction = String(filters.value.direction || '') as 'RECEIVABLE' | 'PAYABLE' | '' const items = await listFinanceFeePool(customerId || undefined, direction || undefined) - allRows.value = items.map((item): ArapRow => ({ id: item.poolItemId, feeSourceId: item.feeSourceId, customerId: item.customerId, customerName: customerName(item.customerId, item.customerName), direction: item.direction, documentNo: feeDocumentLabel(item.direction, item.feeItemName), sourceType: item.sourceType, sourceId: sourceSummary(item.sourceType, item.orderId, item.inboundId, item.containerId), orderId: item.orderId || '', inboundId: item.inboundId || '', containerId: item.containerId || '', feeName: item.feeItemName, amount: item.amount, status: item.status, occurredAt: item.createdAt, businessDate: item.createdAt, reviewStatus: item.status, settlementIncluded: null, statementGenerated: null })) + allRows.value = items.map((item): ArapRow => ({ id: item.poolItemId, feeSourceId: item.feeSourceId, customerId: item.customerId, customerName: customerName(item.customerId, item.customerName), direction: item.direction, documentNo: feeDocumentLabel(item.direction, item.feeItemName), sourceType: item.sourceType, sourceId: sourceSummary(item.sourceType, item.orderId, item.inboundId, item.containerId), orderId: item.orderId || '', inboundId: item.inboundId || '', containerId: item.containerId || '', feeName: displayFeeName(item.feeItemName), amount: item.amount, status: item.status, occurredAt: item.createdAt, businessDate: item.createdAt, reviewStatus: item.status, settlementIncluded: null, statementGenerated: null })) } catch (error) { loadError.value = getApiErrorDetails(error, '客户应收应付加载失败,请重试') } finally { loading.value = false } } @@ -217,7 +223,7 @@ async function restoreFeeSourceDetail(feeSourceId: string) { detailOpen.value = true; detailLoading.value = true; detailError.value = null; detail.value = null try { detail.value = await getFinanceFeeSource(feeSourceId) - selected.value = { id: detail.value.poolItemId, feeSourceId, customerId: detail.value.customerId, customerName: customerName(detail.value.customerId, detail.value.customerName), direction: detail.value.direction, documentNo: detail.value.feeItemName || detail.value.sourceId || detail.value.poolItemId, sourceType: detail.value.sourceType, sourceId: detail.value.sourceId, orderId: detail.value.orderId || '', inboundId: detail.value.inboundId || '', containerId: detail.value.containerId || '', feeName: detail.value.feeItemName, amount: detail.value.amount, status: detail.value.status, occurredAt: detail.value.createdAt, businessDate: detail.value.businessDate, reviewStatus: detail.value.reviewStatus, settlementIncluded: detail.value.settlementIncluded, statementGenerated: detail.value.statementGenerated } + selected.value = { id: detail.value.poolItemId, feeSourceId, customerId: detail.value.customerId, customerName: customerName(detail.value.customerId, detail.value.customerName), direction: detail.value.direction, documentNo: feeDocumentLabel(detail.value.direction, detail.value.feeItemName), sourceType: detail.value.sourceType, sourceId: detail.value.sourceId, orderId: detail.value.orderId || '', inboundId: detail.value.inboundId || '', containerId: detail.value.containerId || '', feeName: displayFeeName(detail.value.feeItemName), amount: detail.value.amount, status: detail.value.status, occurredAt: detail.value.createdAt, businessDate: detail.value.businessDate, reviewStatus: detail.value.reviewStatus, settlementIncluded: detail.value.settlementIncluded, statementGenerated: detail.value.statementGenerated } } catch (error) { detailError.value = getApiErrorDetails(error, '费用来源详情加载失败,请重试') } finally { detailLoading.value = false } } @@ -257,7 +263,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
返回客户应收应付

费用来源详情

{{ selected?.documentNo || '费用来源' }} · {{ selected ? customerName(selected.customerId, selected.customerName) : '客户名称暂不可用' }}

审核参与结算
diff --git a/src/modules/finance/pages/FinanceG3G4Operations.test.ts b/src/modules/finance/pages/FinanceG3G4Operations.test.ts index 2b12693e..bd96399d 100644 --- a/src/modules/finance/pages/FinanceG3G4Operations.test.ts +++ b/src/modules/finance/pages/FinanceG3G4Operations.test.ts @@ -185,6 +185,7 @@ describe('finance G3/G4 page operations', () => { expect(wrapper.emitted('openDetail')).toEqual([['801']]) expect(getFinanceSettlement).not.toHaveBeenCalled() expect(vm.customerName('2088', '上海客户有限公司')).toBe('上海客户有限公司') + expect(vm.customerName('2088', '客户2088')).toBe('—') expect(vm.customerName('2088')).toBe('—') }) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts index be2b3784..076e8416 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts @@ -3,13 +3,18 @@ import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingDetailView.vue'), 'utf8') +const routesSource = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/routes.ts'), 'utf8') describe('OceanBookingDetailView interaction contract', () => { it('recognizes the dedicated create route as an editable new booking', () => { expect(source).toContain("route.name === 'ocean-booking-create' || id.value === 'new'") expect(source).toContain('const editable = computed(() => isNew.value)') + expect(source).toContain("const canSave = computed(() => isNew.value && access.can('platform:firstleg:ocean-booking:create'))") + expect(source).toContain(':disabled="!editable"') expect(source).not.toContain("status === 'DRAFT'") - expect(source).toContain("? access.can('platform:firstleg:ocean-booking:create')") + expect(routesSource.indexOf("path: 'ocean-bookings/new'")).toBeLessThan(routesSource.indexOf("path: 'ocean-bookings/:bookingId'")) + expect(routesSource).toContain("name: 'ocean-booking-create'") + expect(routesSource).toContain("permission: 'platform:firstleg:ocean-booking:create'") }) it('selects business objects and automatically keeps their ids and names together', () => { @@ -73,7 +78,7 @@ describe('OceanBookingDetailView interaction contract', () => { expect(source).toContain('v-model="releaseReason"') expect(source).not.toContain('v-model="releaseForm.') expect(source).toContain('线下订舱资料已登记并确认') - expect(source).toContain("{{ isNew ? '登记订舱' : '保存' }}") + expect(source).toContain('登记订舱') expect(source).toContain('登记后直接确认') }) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index ffa69ad4..5afbc040 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -111,9 +111,7 @@ const rules: FormRules = { freightTerm: [{ required: true, message: '请输入运费条款 Freight Term', trigger: 'blur' }], } const editable = computed(() => isNew.value) -const canSave = computed(() => isNew.value - ? access.can('platform:firstleg:ocean-booking:create') - : editable.value && access.can('platform:firstleg:ocean-booking:edit')) +const canSave = computed(() => isNew.value && access.can('platform:firstleg:ocean-booking:create')) const availableActions = computed(() => (actionsByStatus[detail.value?.status || 'CONFIRMED'] || []) .filter((action) => access.can(actionPermissions[action]))) @@ -245,12 +243,10 @@ async function save() { actionError.value = null try { const payload = { ...form.data } - if (isNew.value) { - const created = await createOceanBooking({ expectedVersion: 0, data: payload }) - ElMessage.success('线下订舱资料已登记并确认') - await router.replace(`/ocean-bookings/${created.id}`) - applyDetail(created) - } + const created = await createOceanBooking({ expectedVersion: 0, data: payload }) + ElMessage.success('线下订舱资料已登记并确认') + await router.replace(`/ocean-bookings/${created.id}`) + applyDetail(created) } catch (cause) { actionError.value = getApiErrorDetails(cause) } finally { saving.value = false } } @@ -362,7 +358,7 @@ onMounted(() => { void load(); void loadOptions() })

订舱信息

已确认订舱资料仅可查看,船期变更请使用“申请改船”。

{{ actionLabels[item] }} - {{ isNew ? '登记订舱' : '保存' }} + 登记订舱
重新加载选项