From d618683467243eef004acc25de72fcea35269c23 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 17 Aug 2026 19:13:25 +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.test.ts | 8 ++++ .../pages/FinanceCustomerArapView.test.ts | 11 +++++ .../FinanceCustomerStatementsView.test.ts | 10 +++++ .../pages/FinanceCustomerStatementsView.vue | 43 +++++++++++++------ .../pages/FinanceSettlementPayablesView.vue | 17 +++++++- .../FinanceSettlementReceivablesView.vue | 17 +++++++- .../FinanceSettlementRouteLayout.test.ts | 19 ++++++++ .../components/InboundTaskList.test.ts | 25 +++++++++++ .../inbounds/components/InboundTaskList.vue | 6 ++- src/modules/inbounds/core/api.test.ts | 12 +++++- .../inbounds/fee/InboundFeePanel.test.ts | 11 +++-- src/modules/inbounds/fee/InboundFeePanel.vue | 39 ++++++++++++----- .../components/LoadingListsView.test.ts | 1 + .../pages/ConfirmedLoadingListDetailView.vue | 1 + .../pages/PreloadPlanDetailView.test.ts | 1 + .../pages/PreloadPlanDetailView.vue | 1 + .../unified-orders/api/unifiedOrders.test.ts | 4 +- .../unified-orders/api/unifiedOrders.ts | 8 +++- .../InboundFeeDetailManager.test.ts | 10 +++++ .../components/InboundFeeDetailManager.vue | 25 ++++++++--- .../pages/UnifiedOrdersView.test.ts | 13 +++--- .../pages/UnifiedOrdersView.vue | 31 ++++++++++++- 22 files changed, 263 insertions(+), 50 deletions(-) create mode 100644 src/modules/finance/pages/FinanceSettlementRouteLayout.test.ts diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts index c9795b34..51f0bac7 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts @@ -82,6 +82,14 @@ describe('FinanceCustomerAccountsView delivery', () => { expect(page).toContain('white-space: nowrap') }) + it('keeps account detail and term editing readable across the 1920 workspace', () => { + expect(page).toContain('width: min(100%, 1920px)') + expect(page).toContain('@media (min-width: 1920px)') + expect(page).toContain('.finance-account-detail__layout :deep(.detail-page-layout__content)') + expect(page).toContain('padding: 0 14px 10px') + expect(page).not.toContain('max-width: 520px') + }) + it('shows only derived CNY summaries and no wallet actions', () => { for (const marker of ['未收', '待出账', '本期已收', '应付', 'CNY']) expect(page).toContain(marker) for (const forbidden of ['手动充值', '钱包', '外币', '发票', '利润']) expect(page).not.toContain(forbidden) diff --git a/src/modules/finance/pages/FinanceCustomerArapView.test.ts b/src/modules/finance/pages/FinanceCustomerArapView.test.ts index ce17878d..92daecea 100644 --- a/src/modules/finance/pages/FinanceCustomerArapView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerArapView.test.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import ElementPlus from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { createMemoryHistory, createRouter } from 'vue-router' @@ -189,6 +191,15 @@ describe('FinanceCustomerArapView G2 operations', () => { expect(wrapper.find('.arap-information').text()).not.toContain('T00:00:00') }) + it('keeps the fee-source detail readable across the 1920 workspace', () => { + const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerArapView.vue'), 'utf8') + expect(page).toContain('width: min(100%, 1920px)') + expect(page).toContain('@media (min-width: 1920px)') + expect(page).toContain('grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr)') + expect(page).toContain('.finance-arap-detail__layout :deep(.detail-page-layout__content)') + expect(page).not.toContain('max-width: 520px') + }) + 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') diff --git a/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts b/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts index ffe85fdb..eae8418e 100644 --- a/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import ElementPlus from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { createMemoryHistory, createRouter } from 'vue-router' @@ -99,12 +101,20 @@ describe('FinanceCustomerStatementsView', () => { expect(document.querySelector('[aria-label="客户对账单列表"]')).toBeNull() expect(document.querySelector('[aria-label="所选对账单联动明细"]')).toBeNull() expect(document.querySelector('[aria-label="对账单摘要"]')?.textContent).toContain('200.00 CNY') + expect(document.querySelectorAll('.statement-summary-grid .statement-summary-item')).toHaveLength(11) expect(document.querySelector('.statement-detail-head')?.textContent).toContain('REC-501') expect(document.querySelector('[aria-label="客户对账单账单明细"]')).toBeTruthy() expect(document.querySelector('.detail-table-scroll [aria-label="客户对账单账单明细"]')).toBeTruthy() expect(document.querySelector('.el-drawer')).toBeNull() }) + it('uses the prototype-style wide summary grid at 1920px', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerStatementsView.vue'), 'utf8') + expect(source).toContain('grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))') + expect(source).toContain('grid-template-columns: repeat(5, minmax(0, 1fr))') + expect(source).not.toContain('') + }) + it('keeps the payment history in the desktop detail layout without duplicating a card container', async () => { const { wrapper } = await mountPage() const vm = wrapper.vm as unknown as { openDetail(row: FinanceStatementReadModel): Promise; activeDetailTab: 'lines' | 'payments' } diff --git a/src/modules/finance/pages/FinanceCustomerStatementsView.vue b/src/modules/finance/pages/FinanceCustomerStatementsView.vue index 7c7c8b07..a459ba67 100644 --- a/src/modules/finance/pages/FinanceCustomerStatementsView.vue +++ b/src/modules/finance/pages/FinanceCustomerStatementsView.vue @@ -456,19 +456,19 @@ watch(detailRouteId, (statementId) => { {{ financeStatusLabel(detail.status) }} - - {{ detail.totalAmount.toFixed(2) }} CNY - {{ detail.paidAmount.toFixed(2) }} CNY - {{ detail.outstandingAmount.toFixed(2) }} CNY - {{ detail.creditDays }} 天 - {{ detail.currency }} - {{ detail.settled ? '是' : '否' }} - {{ operatorDisplayName(detail.createdByName) }} - {{ formatDateTime(detail.createdAt) }} - {{ operatorDisplayName(detail.updatedByName) }} - {{ formatDateTime(detail.updatedAt) }} - {{ detail.remark || '—' }} - +
+
总应收金额{{ detail.totalAmount.toFixed(2) }} CNY
+
已收金额{{ detail.paidAmount.toFixed(2) }} CNY
+
未收金额{{ detail.outstandingAmount.toFixed(2) }} CNY
+
账期{{ detail.creditDays }} 天
+
货币{{ detail.currency }}
+
是否结清{{ detail.settled ? '是' : '否' }}
+
创建人{{ operatorDisplayName(detail.createdByName) }}
+
创建时间{{ formatDateTime(detail.createdAt) }}
+
更新人{{ operatorDisplayName(detail.updatedByName) }}
+
更新时间{{ formatDateTime(detail.updatedAt) }}
+
备注{{ detail.remark || '—' }}
+
@@ -566,6 +566,12 @@ watch(detailRouteId, (statementId) => { .statement-detail-head h2 { margin: 2px 0 0; font-size: 16px; line-height: 1.4; overflow-wrap: anywhere; } .statement-detail-head p { margin: 5px 0 0; color: var(--cc-text-secondary); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; } .statement-summary-section { padding: 10px 16px; border-bottom: 1px solid var(--cc-border-subtle); } +.statement-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px 24px; } +.statement-summary-item { min-width: 0; } +.statement-summary-item span, .statement-summary-item strong { display: block; overflow-wrap: anywhere; } +.statement-summary-item span { margin-bottom: 3px; color: var(--cc-text-secondary); font-size: 12px; } +.statement-summary-item strong { color: var(--cc-text-primary); font-size: 14px; line-height: 1.45; } +.statement-summary-item--remark { grid-column: span 2; } .statement-outstanding { color: var(--el-color-warning-dark-2); } .statement-detail-tabs { min-width: 0; } .statement-detail-table { min-width: 0; } @@ -601,6 +607,15 @@ watch(detailRouteId, (statementId) => { padding: 8px 14px; } + .statement-summary-grid { + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px 24px; + } + + .statement-summary-item--remark { + grid-column: span 2; + } + .statement-detail-tabs :deep(.el-tabs__header) { padding: 0 14px; } @@ -624,5 +639,7 @@ watch(detailRouteId, (statementId) => { @media (max-width: 640px) { .statement-detail-page__header { align-items: stretch; flex-direction: column; } + .statement-summary-grid { grid-template-columns: 1fr; } + .statement-summary-item--remark { grid-column: auto; } } diff --git a/src/modules/finance/pages/FinanceSettlementPayablesView.vue b/src/modules/finance/pages/FinanceSettlementPayablesView.vue index e30b08d7..997e470b 100644 --- a/src/modules/finance/pages/FinanceSettlementPayablesView.vue +++ b/src/modules/finance/pages/FinanceSettlementPayablesView.vue @@ -9,4 +9,19 @@ const settlementId = computed(() => String(route.params.settlementId || '')) function openDetail(id: string) { void router.push({ name: 'finance-settlement-payable-detail', params: { settlementId: id } }) } function back() { void router.push({ name: 'finance-settlement-payables' }) } - + + + diff --git a/src/modules/finance/pages/FinanceSettlementReceivablesView.vue b/src/modules/finance/pages/FinanceSettlementReceivablesView.vue index 756b6a01..4c56d224 100644 --- a/src/modules/finance/pages/FinanceSettlementReceivablesView.vue +++ b/src/modules/finance/pages/FinanceSettlementReceivablesView.vue @@ -9,4 +9,19 @@ const settlementId = computed(() => String(route.params.settlementId || '')) function openDetail(id: string) { void router.push({ name: 'finance-settlement-receivable-detail', params: { settlementId: id } }) } function back() { void router.push({ name: 'finance-settlement-receivables' }) } - + + + diff --git a/src/modules/finance/pages/FinanceSettlementRouteLayout.test.ts b/src/modules/finance/pages/FinanceSettlementRouteLayout.test.ts new file mode 100644 index 00000000..8376b6b1 --- /dev/null +++ b/src/modules/finance/pages/FinanceSettlementRouteLayout.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const receivableSource = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceSettlementReceivablesView.vue'), 'utf8') +const payableSource = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceSettlementPayablesView.vue'), 'utf8') + +describe('Finance settlement route 1920px layout', () => { + it.each([ + ['应收', receivableSource, 'finance-settlement-route--receivable'], + ['应付', payableSource, 'finance-settlement-route--payable'], + ])('uses the full detail workspace and a readable two-field information grid for %s', (_label, source, routeClass) => { + expect(source).toContain(routeClass) + expect(source).toContain('@media (min-width: 1920px)') + expect(source).toContain('.finance-settlement-route :deep(.settlement-detail-page) { width: 100%; max-width: none; }') + expect(source).toContain('grid-template-columns: minmax(110px, .35fr) minmax(0, 1fr) minmax(110px, .35fr) minmax(0, 1fr)') + expect(source).toContain('> tbody > tr) { display: contents; }') + }) +}) diff --git a/src/modules/inbounds/components/InboundTaskList.test.ts b/src/modules/inbounds/components/InboundTaskList.test.ts index 153c2093..fce47f2a 100644 --- a/src/modules/inbounds/components/InboundTaskList.test.ts +++ b/src/modules/inbounds/components/InboundTaskList.test.ts @@ -95,6 +95,31 @@ describe('InboundTaskList measurement scope', () => { expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }") }) + it('shows authoritative measurement totals with operation first and no internal IDs as primary columns', () => { + const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8') + const columnsSource = source.slice(source.indexOf('const columns'), source.indexOf('function text')) + + expect(columnsSource.indexOf("key: 'actions'")).toBeLessThan(columnsSource.indexOf("key: 'inboundCode'")) + expect(columnsSource).toContain("key: 'customerName', label: '客户'") + expect(columnsSource).toContain("key: 'channelName', label: '渠道'") + expect(columnsSource).toContain("key: 'totalPackageCount', label: '总箱数'") + expect(columnsSource).toContain("key: 'palletCount', label: '托盘数'") + expect(columnsSource).toContain("key: 'actualGrossWeightKg', label: '实测毛重(kg)'") + expect(columnsSource).toContain("key: 'actualNetWeightKg', label: '货物重量(kg)'") + expect(columnsSource).toContain("key: 'actualVolumeCbm', label: '实测体积(m³)'") + expect(columnsSource).toContain("key: 'measureCompletedAt', label: '提交时间'") + expect(columnsSource).not.toContain("key: 'id'") + expect(columnsSource).not.toContain("key: 'orderId'") + }) + + it('renders missing list facts as an explicit dash instead of guessing values', () => { + const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8') + + expect(source).toContain("value === null || value === undefined || value === '' ? '—'") + expect(source).toContain("value === null || value === undefined ? '—'") + expect(source).toContain("if (!value) return '—'") + }) + it('refreshes cached task lists when users return from measurement work', () => { const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8') diff --git a/src/modules/inbounds/components/InboundTaskList.vue b/src/modules/inbounds/components/InboundTaskList.vue index c50cea29..b47d487d 100644 --- a/src/modules/inbounds/components/InboundTaskList.vue +++ b/src/modules/inbounds/components/InboundTaskList.vue @@ -93,9 +93,11 @@ const columns = computed(() => [ { key: 'businessType', label: '业务类型', minWidth: 120, slotName: 'businessType' }, { key: 'channelName', label: '渠道', minWidth: 140, slotName: 'channelName' }, { key: 'totalPackageCount', label: '总箱数', width: 92, slotName: 'totalPackageCount' }, + { key: 'palletCount', label: '托盘数', width: 92, slotName: 'palletCount' }, { key: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 128, slotName: 'actualGrossWeightKg' }, + { key: 'actualNetWeightKg', label: '货物重量(kg)', minWidth: 128, slotName: 'actualNetWeightKg' }, { key: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 128, slotName: 'actualVolumeCbm' }, - { key: 'measureCompletedAt', label: '测量完成时间', minWidth: 164, slotName: 'measureCompletedAt' }, + { key: 'measureCompletedAt', label: '提交时间', minWidth: 164, slotName: 'measureCompletedAt' }, ]) function text(value: unknown) { @@ -334,7 +336,9 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria + + diff --git a/src/modules/inbounds/core/api.test.ts b/src/modules/inbounds/core/api.test.ts index 0e3074b6..e871d88a 100644 --- a/src/modules/inbounds/core/api.test.ts +++ b/src/modules/inbounds/core/api.test.ts @@ -30,7 +30,11 @@ describe('platform inbound query API', () => { it('uses the implemented non-v1 list path and normalizes page values and IDs', async () => { vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { - records: [{ id: 19001, tenantId: 10002, orderId: 18001, customerId: 10001, totalPackageCount: '6', holdFlag: 'false' }], + records: [{ + id: 19001, tenantId: 10002, orderId: 18001, customerId: 10001, + totalPackageCount: '6', palletCount: '2', actualGrossWeightKg: '21.5', + actualNetWeightKg: '18.25', actualVolumeCbm: '0.456', holdFlag: 'false', + }], page: '1', size: '20', total: '1', pages: '1', }, }) @@ -38,7 +42,11 @@ describe('platform inbound query API', () => { await expect(listPlatformInbounds('10001', { page: 1, size: 20, tab: 'WAIT_MEASURE', inboundCode: 'RK', holdFlag: false, })).resolves.toMatchObject({ - records: [{ id: '19001', tenantId: '10002', orderId: '18001', customerId: '10001', totalPackageCount: 6, holdFlag: false }], + records: [{ + id: '19001', tenantId: '10002', orderId: '18001', customerId: '10001', + totalPackageCount: 6, palletCount: 2, actualGrossWeightKg: 21.5, + actualNetWeightKg: 18.25, actualVolumeCbm: 0.456, holdFlag: false, + }], total: 1, pages: 1, }) expect(http.get).toHaveBeenCalledWith('/platform/inbounds', { diff --git a/src/modules/inbounds/fee/InboundFeePanel.test.ts b/src/modules/inbounds/fee/InboundFeePanel.test.ts index 378fe6fe..eb0afca9 100644 --- a/src/modules/inbounds/fee/InboundFeePanel.test.ts +++ b/src/modules/inbounds/fee/InboundFeePanel.test.ts @@ -75,7 +75,7 @@ describe('InboundFeePanel', () => { expect(getLatestInboundFee).toHaveBeenCalledWith('10001', '88') expect(document.body.textContent).not.toContain('租户') - expect(document.body.textContent).toContain('对客费用总额 · CNY') + expect(document.body.textContent).toContain('头程自动费用 · CNY') expect(document.body.textContent).toContain('¥1,235.60') expect(document.body.textContent).toContain('基础运费小计 ¥1,135.60') expect(document.body.textContent).toContain('附加费 ¥100.00') @@ -130,8 +130,10 @@ describe('InboundFeePanel', () => { const content = wrapper.find('.fee-content').text() expect(wrapper.findAll('.fee-summary > div > span').map((node) => node.text())).toEqual([ - '对客费用总额 · CNY', + '头程自动费用 · CNY', '最终计费量', + '单价', + '附加费', '快照版本', ]) expect(wrapper.findAll('.calculation-flow > div > span').map((node) => node.text())).toEqual([ @@ -169,12 +171,13 @@ describe('InboundFeePanel', () => { expect(document.querySelector('[role="alert"]')).toBeNull() }) - it('shows adjustment only for authorized published inbound fees', async () => { + it('shows adjustment only for authorized calculated inbound fees', async () => { mountPanel(['firstleg:fba-pricing:view']) await flushPromises() expect(document.querySelector('[data-testid="adjust-fee-button"]')).toBeNull() document.body.innerHTML = '' + vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' }) mountPanel() await flushPromises() expect(document.querySelector('[data-testid="adjust-fee-button"]')).not.toBeNull() @@ -189,6 +192,7 @@ describe('InboundFeePanel', () => { }) it('submits a reasoned adjustment and emits the refreshed snapshot', async () => { + vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' }) const wrapper = mountPanel() await flushPromises() ;(document.querySelector('[data-testid="adjust-fee-button"]') as HTMLButtonElement).click() @@ -233,6 +237,7 @@ describe('InboundFeePanel', () => { }) it('refreshes the latest snapshot after a conflict while retaining the dialog', async () => { + vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' }) vi.mocked(adjustPublishedInboundFee).mockRejectedValue( new ApiResponseError('费用已被其他操作员修改,请刷新后重试', 'A0409', 409), ) diff --git a/src/modules/inbounds/fee/InboundFeePanel.vue b/src/modules/inbounds/fee/InboundFeePanel.vue index 16e913b0..738bb344 100644 --- a/src/modules/inbounds/fee/InboundFeePanel.vue +++ b/src/modules/inbounds/fee/InboundFeePanel.vue @@ -22,10 +22,12 @@ const props = defineProps<{ customerId?: string tenantId?: string inboundId: string + compact?: boolean }>() const emit = defineEmits<{ changed: [snapshot: InboundFeeSnapshot] + loaded: [snapshot: InboundFeeSnapshot | null] }>() const access = useAccessPreviewStore() @@ -64,7 +66,7 @@ const canView = computed(() => access.can('firstleg:fba-pricing:view')) const canAdjust = computed(() => access.can('firstleg:fba-pricing:adjust')) const customerContextId = computed(() => props.customerId || props.tenantId || '') const adjustmentEligible = computed(() => ( - ['CALCULATED', 'CONFIRMED', 'SENT'].includes(snapshot.value?.feeStatus || '') + snapshot.value?.feeStatus === 'CALCULATED' && snapshot.value?.nextProcessStatus === 'WAIT_SPLIT' )) const chargeModeLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '按重量计费' : '按体积计费') @@ -133,10 +135,12 @@ async function loadFee(options: { preserveAdjustment?: boolean } = {}) { const result = await getLatestInboundFee(customerContextId.value, props.inboundId) if (sequence !== loadSequence) return snapshot.value = result + emit('loaded', result) if (!options.preserveAdjustment && adjustOpen.value) fillAdjustmentForm(result) } catch (error) { if (sequence !== loadSequence) return snapshot.value = null + emit('loaded', null) if (error instanceof ApiResponseError && error.code === 'A0404') notFound.value = true else loadError.value = getApiErrorDetails(error, '费用快照加载失败,请稍后重试') } finally { @@ -228,8 +232,8 @@ defineExpose({ reload: loadFee }) + diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index bcd4522a..fb3bfac0 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -95,21 +95,21 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('v-model:page-size="pageSize"') const columnSource = source.slice(source.indexOf('const columns ='), source.indexOf('function emptyFilters')) const expectedColumnKeys = [ - 'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName', - 'destinationWarehouseName', 'totalPackageCount', 'measurementReviewStatus', 'actualGrossWeightKg', + 'actions', 'orderNo', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName', + 'destinationWarehouseName', 'channelName', 'totalPackageCount', 'recommendedPalletType', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg', 'actualVolumeCbm', 'chargeableWeightKg', 'expectedShipDate', 'loadingStatus', 'containerNo', 'cabinetNo', 'sailingAt', 'portArrivalAt', 'customerRemark', 'latestInternalNote', 'createdAt', 'updatedAt', ] for (const key of expectedColumnKeys) expect(columnSource).toContain(`key: '${key}'`) for (const uncontractedKey of [ 'internalOrderStatus', 'expectedArrivalAt', 'feeReviewStatus', 'holdCargo', 'companyName', - 'pickupMethod', 'chargeableVolumeCbm', 'recommendedPalletType', 'locationCodes', 'associatedFiles', 'createdBy', + 'pickupMethod', 'chargeableVolumeCbm', 'associatedFiles', 'createdBy', ]) expect(columnSource).not.toContain(`key: '${uncontractedKey}'`) - expect(columnSource).not.toContain("key: 'orderNo'") - expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'")) + expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'orderNo'")) + expect(columnSource.indexOf("key: 'orderNo'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'")) expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'") expect(columnSource).toContain("{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field', defaultVisible: false }") - expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(8) + expect(columnSource.match(/defaultVisible: false/g)).toHaveLength(10) expect(source).toContain('.table-card { overflow: hidden; }') expect(source).toContain('slotName: \'measurementReviewStatus\'') expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')') @@ -288,6 +288,7 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('InboundFeePanel') expect(source).toContain('InboundFeeDetailManager') expect(source).toContain(':customer-id="String(detail.summary.tenantId)"') + expect(source).toContain('compact') expect(source).toContain('头程自动初算') expect(source).toContain('其他费用明细') expect(source).toContain('@changed="loadDetail(true)"') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 607a8a42..4be92303 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -103,6 +103,18 @@ const detail = ref(null) const detailLoading = ref(false) const detailError = ref(null) const activeDetailTab = ref('basic') +const automaticFee = ref(null) +const otherFeeSummary = ref({ currency: 'CNY', approvedReceivable: 0, approvedPayable: 0 }) +const customerFeeTotal = computed(() => ( + (automaticFee.value?.feeStatus === 'SENT' ? Number(automaticFee.value.totalAmount || 0) : 0) + + otherFeeSummary.value.approvedReceivable +)) +function feeMoney(value: number) { + return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: otherFeeSummary.value.currency || 'CNY' }).format(value) +} + +type OtherFeeSummary = { currency: string; approvedReceivable: number; approvedPayable: number } +type AutomaticFeeSummary = { feeStatus: string; totalAmount: number } const approvalOpen = ref(false) const approvalLoadingId = ref('') const approvalSaving = ref(false) @@ -226,12 +238,16 @@ const columns = computed(() => [ key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: (row: UnifiedOrderResource) => canApprove(row) && !batchApprovalSaving.value, } satisfies DataTableColumn] : []), + { key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 180, slotName: 'field' }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, slotName: 'inboundCode' }, { key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' }, { key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '平台收货仓', minWidth: 150, slotName: 'field' }, { key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '目的仓', minWidth: 150, slotName: 'field' }, + { key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 140, slotName: 'field' }, { key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' }, + { key: 'recommendedPalletType', prop: 'recommendedPalletType', label: '推荐托盘类型', minWidth: 140, slotName: 'field', defaultVisible: false }, + { key: 'locationCodes', prop: 'locationCodes', label: '库位位置', minWidth: 160, slotName: 'field', defaultVisible: false }, { key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 170, slotName: 'measurementReviewStatus' }, { key: 'actualGrossWeightKg', prop: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 130, slotName: 'field' }, { key: 'actualVolumeCbm', prop: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 130, slotName: 'field' }, @@ -351,7 +367,6 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) { measurementReviewStatus: 'measurement.reviewStatus', expectedArrivalAt: 'expectedArrivalAt', loadingStatus: 'transport.loadingStatus', containerNo: 'transport.containerNo', cabinetNo: 'transport.cabinetNo', sailingAt: 'transport.sailingAt', portArrivalAt: 'transport.portArrivalAt', - actualGrossWeightKg: 'measurement.actualGrossWeightKg', actualVolumeCbm: 'measurement.actualVolumeCbm', chargeableWeightKg: 'measurement.chargeableWeightKg', measurementCompletedAt: 'measurement.completedAt', expressTrackingNo: 'delivery.trackingNo', } @@ -978,16 +993,23 @@ onBeforeUnmount(() => { @@ -1295,6 +1317,12 @@ onBeforeUnmount(() => { .panel-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; } .panel-title > svg { color: var(--cc-primary); } .fee-section-title { margin: 14px 0 8px; color: #344054; font-size: 13px; line-height: 1.4; } +.fee-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 8px 0 0; border: 1px solid var(--cc-border); } +.fee-overview > div { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 4px 12px; padding: 10px 12px; } +.fee-overview > div + div { border-left: 1px solid var(--cc-border); } +.fee-overview dt, .fee-overview small { color: var(--cc-muted); font-size: 11px; } +.fee-overview dd { margin: 0; font-size: 18px; font-weight: 700; text-align: right; } +.fee-overview small { grid-column: 1 / -1; } .fee-section-title--secondary { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--cc-border); } .order-info-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; margin: 0; overflow: hidden; border: 1px solid #edf0f4; background: #fff; } .order-info-grid > div, .measurement-grid > div { display: flex; min-width: 0; min-height: 38px; align-items: baseline; gap: 8px; padding: 8px 12px; border-right: 1px solid #edf0f4; border-bottom: 1px solid #edf0f4; background: #fff; } @@ -1346,5 +1374,6 @@ onBeforeUnmount(() => { @media (max-width: 960px) { .detail-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-overview article:nth-child(2) { border-right: 0; } .detail-overview article:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); } .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { display: grid; grid-template-columns: auto 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .detail-overview, .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .detail-overview article { border-right: 0; border-bottom: 1px solid var(--cc-border); } .detail-overview article:last-child { border-bottom: 0; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } } @media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } } +@media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--cc-border); border-left: 0; } } @media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } }