From a853b1e0c238932cee83448bde8a96ce6730147f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 7 Oct 2026 16:46:58 +0800 Subject: [PATCH 1/4] feat(orders): show actual pooled freight fulfillment --- .../cross-border/api/businessOrders.ts | 1 + .../cross-border/businessOrders.test.mjs | 9 +++++ .../components/BusinessOrdersView.vue | 33 ++++++++++++++++--- src/modules/cross-border/fleetArrangement.ts | 28 ++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/modules/cross-border/fleetArrangement.ts diff --git a/src/modules/cross-border/api/businessOrders.ts b/src/modules/cross-border/api/businessOrders.ts index 3f1a894..39c3905 100644 --- a/src/modules/cross-border/api/businessOrders.ts +++ b/src/modules/cross-border/api/businessOrders.ts @@ -75,6 +75,7 @@ export interface ProcurementCreate { } export interface TransportSummary { + fleetArrangement: import('../fleetArrangement').FleetArrangement | null orderNo: string transportType: string transportMode?: string diff --git a/src/modules/cross-border/businessOrders.test.mjs b/src/modules/cross-border/businessOrders.test.mjs index 548c760..3a36ecb 100644 --- a/src/modules/cross-border/businessOrders.test.mjs +++ b/src/modules/cross-border/businessOrders.test.mjs @@ -6,6 +6,15 @@ const api = readFileSync(new URL('./api/businessOrders.ts', import.meta.url), 'u const routes = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8') const view = readFileSync(new URL('./components/BusinessOrdersView.vue', import.meta.url), 'utf8') +test('shows merchant-owned arrangement facts without a pool request or editable status control', () => { + assert.match(api, /fleetArrangement: import\('\.\.\/fleetArrangement'\)\.FleetArrangement \| null/) + assert.match(view, /aria-label="承运安排"/) + for (const field of ['signedPackages', 'unsignedPackages', 'returnedPackages', 'readOnlyReason']) assert.ok(view.includes(`fleetArrangement.${field}`)) + assert.doesNotMatch(api, /capacity-offers|fleet\/trips/) + assert.match(view, /if \(query\.page !== 1\) query\.page = 1\s+else void load\(\)/) + assert.match(view, /if \(reloadDirectly\) void load\(\)/) +}) + test('activates all three customer cross-border pages with frozen permissions', () => { for (const [path, permission] of [ ['cross-border/procurement', 'merchant:cross-border:procurement:list'], diff --git a/src/modules/cross-border/components/BusinessOrdersView.vue b/src/modules/cross-border/components/BusinessOrdersView.vue index e2404e0..60703d0 100644 --- a/src/modules/cross-border/components/BusinessOrdersView.vue +++ b/src/modules/cross-border/components/BusinessOrdersView.vue @@ -18,6 +18,7 @@ import { type TransportSummary, } from '../api/businessOrders' import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE } from '../containerSpecifications' +import { poolOrderStatusLabels } from '../fleetArrangement' const props = withDefaults(defineProps<{ kind: CrossBorderBusinessKind; createPage?: boolean; detailPage?: boolean }>(), { createPage: false, detailPage: false }) const auth = useAuthStore() @@ -105,6 +106,7 @@ function transportColumns(looseboard: boolean): ColumnDefinition[] { { key: 'budgetAmount', label: '预算总价', width: 110 }, { key: 'requestedDate', label: '要求时间', width: 120 }, { key: 'status', label: '状态', width: 130 }, + { key: 'fleetArrangement', label: '承运安排', width: 190 }, ] } @@ -115,6 +117,7 @@ const submitKey = ref('') const error = ref('') const rows = ref([]) const detail = ref(null) +const fleetArrangement = computed(() => detail.value && props.kind !== 'procurement' ? (detail.value as TransportDetail).summary.fleetArrangement : null) const formRef = ref() const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '', productName: '', purchaseCategory: '' }) const total = ref(0) @@ -157,6 +160,7 @@ const queryFields = computed(() => [ { label: '部分出库', value: 'PARTIALLY_OUTBOUND' }, { label: '全部出库', value: 'OUTBOUNDED' }, { label: '待接单', value: 'PENDING_ACCEPTANCE' }, + ...(props.kind === 'procurement' ? [] : Object.entries(poolOrderStatusLabels).filter(([value]) => !['IN_TRANSIT', 'COMPLETED', 'CANCELLED'].includes(value)).map(([value, label]) => ({ value, label }))), { label: '处理中', value: 'PROCESSING' }, { label: '运输中', value: 'IN_TRANSIT' }, { label: '已完成', value: 'COMPLETED' }, @@ -215,7 +219,7 @@ const tableColumns = computed(() => [ ...configuration.value.columns.map((column) => ({ ...column, prop: column.key, - slotName: ['orderNo', 'status'].includes(column.key) ? column.key : undefined, + slotName: ['orderNo', 'status', 'fleetArrangement'].includes(column.key) ? column.key : undefined, formatter: (row: object) => displayCell(row, column.key), })), ]) @@ -242,6 +246,7 @@ function yesNo(value: unknown) { return value === 'YES' ? '是' : value === 'NO' function transportTypeLabel(value: unknown) { return ({ PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } as Record)[String(value)] || text(value) } function transportModeLabel(value: unknown) { return ({ SHARED_TRUCK: '拼车', PRIORITY_CONSOLIDATION: '优先拼载' } as Record)[String(value)] || text(value) } function statusLabel(value: unknown) { + if (poolOrderStatusLabels[String(value)]) return poolOrderStatusLabels[String(value)] return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_OUTBOUND: '待出库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUNDED: '全部出库', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record)[String(value)] || text(value) } function statusTone(value: unknown) { @@ -283,13 +288,14 @@ async function load() { } function resetQuery() { + const reloadDirectly = query.page === 1 Object.assign(query, { page: 1, orderNo: '', routeKeyword: '', status: '', productName: '', purchaseCategory: '' }) - void load() + if (reloadDirectly) void load() } function searchOrders() { - query.page = 1 - void load() + if (query.page !== 1) query.page = 1 + else void load() } function getRowActions(candidate: object): TableAction[] { @@ -397,6 +403,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat + @@ -413,6 +420,24 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
{{ field[0] }}
{{ text(field[1]) }}
+
+

承运安排

{{ fleetArrangement.readOnlyReason }}

+
+
预订号
{{ fleetArrangement.bookingNo }}
+
计划发车
{{ dateTime(fleetArrangement.plannedDepartureAt) }}
+
起运仓
{{ text(fleetArrangement.originWarehouseName) }}
+
目的地
{{ text(fleetArrangement.destinationLabel) }}
+
交货方式
{{ fleetArrangement.deliveryMode === 'SELF_DELIVERY' ? '自行送至起运仓' : '线下安排提货至起运仓' }}
+
履约进度
{{ poolOrderStatusLabels[fleetArrangement.fulfillmentStage] || '待核对履约进度' }}
+
更新时间
{{ dateTime(fleetArrangement.stageOccurredAt) }}
+
约定运费(非实收)
{{ money(fleetArrangement.agreedFreightAmount, fleetArrangement.currency) }}
+
来货单号
{{ text(fleetArrangement.incomingNo) }}
+
派送单号
{{ text(fleetArrangement.deliveryNo) }}
+
已签 / 未签 / 退回箱数
{{ fleetArrangement.signedPackages ?? '—' }} / {{ fleetArrangement.unsignedPackages ?? '—' }} / {{ fleetArrangement.returnedPackages ?? '—' }}
+
交货说明
{{ text(fleetArrangement.deliveryInstructions) }}
+
履约说明
{{ fleetArrangement.exceptionSummary }}
+
+
diff --git a/src/modules/cross-border/fleetArrangement.ts b/src/modules/cross-border/fleetArrangement.ts new file mode 100644 index 0000000..4ad6d1a --- /dev/null +++ b/src/modules/cross-border/fleetArrangement.ts @@ -0,0 +1,28 @@ +export interface FleetArrangement { + sourceType: 'FLEET_CAPACITY_OFFER' + businessType: 'LOCAL_LOOSEBOARD' | 'US_TRUCK' + businessOrderNo: string + bookingNo: string + tripNo: string + plannedDepartureAt: string | null + originWarehouseName: string + destinationLabel: string + deliveryMode: 'SELF_DELIVERY' | 'MANUAL_PICKUP_TO_WAREHOUSE' + deliveryInstructions: string | null + fulfillmentStage: string + stageOccurredAt: string | null + agreedFreightAmount: number | null + currency: string | null + incomingNo: string | null + dispatchNo: string | null + deliveryNo: string | null + signedPackages: number | null + unsignedPackages: number | null + returnedPackages: number | null + exceptionSummary: string | null + readOnlyReason: string +} +export const poolOrderStatusLabels: Record = { + WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', + PENDING_DISPATCH: '待派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消', +} -- 2.54.0 From 47385d46f20b816d0948c80d534261723e895209 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 7 Oct 2026 19:44:44 +0800 Subject: [PATCH 2/4] fix(orders): retain nullable palletizing facts in customer summaries --- src/modules/cross-border/api/businessOrders.ts | 2 +- .../cross-border/businessOrders.test.mjs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/modules/cross-border/api/businessOrders.ts b/src/modules/cross-border/api/businessOrders.ts index 39c3905..5bb7ee3 100644 --- a/src/modules/cross-border/api/businessOrders.ts +++ b/src/modules/cross-border/api/businessOrders.ts @@ -79,7 +79,7 @@ export interface TransportSummary { orderNo: string transportType: string transportMode?: string - palletRequired?: string + palletRequired?: string | null origin: string destination: string containerType: string | null diff --git a/src/modules/cross-border/businessOrders.test.mjs b/src/modules/cross-border/businessOrders.test.mjs index 3a36ecb..01d11f0 100644 --- a/src/modules/cross-border/businessOrders.test.mjs +++ b/src/modules/cross-border/businessOrders.test.mjs @@ -1,11 +1,29 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' +import { runInNewContext } from 'node:vm' +import ts from 'typescript' const api = readFileSync(new URL('./api/businessOrders.ts', import.meta.url), 'utf8') const routes = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8') const view = readFileSync(new URL('./components/BusinessOrdersView.vue', import.meta.url), 'utf8') +test('renders unknown pallet service and container type as dashes while preserving explicit values', () => { + const helpers = view.slice(view.indexOf('function read('), view.indexOf('function idempotencyKey(')) + const compiled = ts.transpileModule(helpers, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText + const { displayCell, yesNo, text } = runInNewContext(`${compiled}\n({ displayCell, yesNo, text })`) + assert.equal(displayCell({ palletRequired: null }, 'palletRequired'), '—') + assert.equal(displayCell({ palletRequired: 'NO' }, 'palletRequired'), '否') + assert.equal(displayCell({ palletRequired: 'YES' }, 'palletRequired'), '是') + assert.equal(displayCell({ containerType: null }, 'containerType'), '—') + assert.equal(displayCell({ containerType: '40HQ' }, 'containerType'), '40HQ') + assert.equal(yesNo(null), '—') + assert.equal(text(null), '—') + assert.match(api, /palletRequired\?: string \| null/) + const create = api.slice(api.indexOf('export interface TransportCreate'), api.indexOf('export interface TransportUpdate')) + assert.doesNotMatch(create, /palletRequired\?: string \| null/) +}) + test('shows merchant-owned arrangement facts without a pool request or editable status control', () => { assert.match(api, /fleetArrangement: import\('\.\.\/fleetArrangement'\)\.FleetArrangement \| null/) assert.match(view, /aria-label="承运安排"/) -- 2.54.0 From 19f060727b953864d88c02af4a0097b2f15b9e10 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 7 Oct 2026 20:43:30 +0800 Subject: [PATCH 3/4] fix(client): localize transport mode in order detail --- src/modules/cross-border/components/BusinessOrdersView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/cross-border/components/BusinessOrdersView.vue b/src/modules/cross-border/components/BusinessOrdersView.vue index 60703d0..25a4427 100644 --- a/src/modules/cross-border/components/BusinessOrdersView.vue +++ b/src/modules/cross-border/components/BusinessOrdersView.vue @@ -202,7 +202,7 @@ const detailFields = computed(() => { } const current = detail.value as TransportDetail return [ - ['运输方式', read(summary, 'transportMode')], + ['运输方式', transportModeLabel(read(summary, 'transportMode'))], ['是否打盘', yesNo(read(summary, 'palletRequired'))], ['运输类型', transportTypeLabel(read(summary, 'transportType'))], ['起运地', read(summary, 'origin')], ['起运地址', current.originAddress], ['目的地', read(summary, 'destination')], ['目的地址', current.destinationAddress], -- 2.54.0 From 7759c7e9f30b4d11fb8f075938d61293741fa9b1 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 7 Oct 2026 20:59:34 +0800 Subject: [PATCH 4/4] fix(client): prevent arrangement labels overlapping values --- src/modules/cross-border/components/BusinessOrdersView.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/cross-border/components/BusinessOrdersView.vue b/src/modules/cross-border/components/BusinessOrdersView.vue index 25a4427..6ddbf6e 100644 --- a/src/modules/cross-border/components/BusinessOrdersView.vue +++ b/src/modules/cross-border/components/BusinessOrdersView.vue @@ -422,7 +422,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat

承运安排

{{ fleetArrangement.readOnlyReason }}

-
+
预订号
{{ fleetArrangement.bookingNo }}
计划发车
{{ dateTime(fleetArrangement.plannedDepartureAt) }}
起运仓
{{ text(fleetArrangement.originWarehouseName) }}
@@ -497,6 +497,8 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat .detail-grid div { display: grid; min-width: 0; min-height: 34px; grid-template-columns: 76px minmax(0, 1fr); align-items: center; gap: 7px; padding: 3px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); } .detail-grid div.is-wide { grid-column: span 2; } .detail-grid dt { color: var(--muted); font-size: 10px; white-space: nowrap; } +.arrangement-grid div { grid-template-columns: 124px minmax(0, 1fr); } +.arrangement-grid dt { white-space: normal; overflow-wrap: anywhere; } .detail-grid dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 600; line-height: 1.4; } .form-card :deep(.el-form) { max-width: 860px; } .form-card :deep(.el-input), .form-card :deep(.el-textarea), .form-card :deep(.el-select) { width: 100%; } -- 2.54.0