From 56776bb197ff7553bec38d7805c62ae55ce8cb98 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Tue, 6 Oct 2026 09:45:11 +0800 Subject: [PATCH] feat: split transport plan list columns into load and delivery product rows Co-authored-by: Cursor --- .../composables/usePlPlanMergeListTable.ts | 5 +- .../src/views/pl/plan/columnFilterDefs.ts | 55 +- web-admin/src/views/pl/plan/index.vue | 731 ++++++++++-------- 3 files changed, 466 insertions(+), 325 deletions(-) diff --git a/web-admin/src/composables/usePlPlanMergeListTable.ts b/web-admin/src/composables/usePlPlanMergeListTable.ts index d08bf533d..cfe26f7d1 100644 --- a/web-admin/src/composables/usePlPlanMergeListTable.ts +++ b/web-admin/src/composables/usePlPlanMergeListTable.ts @@ -28,6 +28,8 @@ export function usePlPlanMergeListTable(options: { archivePairMode?: boolean | Ref | ComputedRef loadCustomerKeywords?: Ref | ComputedRef deliveryCustomerKeywords?: Ref | ComputedRef + /** 产品名称关键字:只保留命中产品行并重算 rowspan */ + productKeywords?: Ref | ComputedRef }) { const displayRows = computed(() => { const pairMode = !!unref(options.archivePairMode) @@ -41,7 +43,8 @@ export function usePlPlanMergeListTable(options: { : null, customerKeywords: options.stopCustomerKeywords ? unref(options.stopCustomerKeywords) - : null + : null, + productKeywords: options.productKeywords ? unref(options.productKeywords) : null }) } return flattenPlanListRows(unref(options.planList), { diff --git a/web-admin/src/views/pl/plan/columnFilterDefs.ts b/web-admin/src/views/pl/plan/columnFilterDefs.ts index 05c494c66..39e353b07 100644 --- a/web-admin/src/views/pl/plan/columnFilterDefs.ts +++ b/web-admin/src/views/pl/plan/columnFilterDefs.ts @@ -54,7 +54,8 @@ export const plPlanColumnFilterDefs: ColumnFilterDef[] = [ type: 'textMulti', queryKey: 'deliveryCustomers' }, - { prop: 'stopAddressTime', label: '地址 / 时间', type: 'text', queryKey: 'stopAddress' }, + { prop: 'loadAddressTime', label: '装货地址/时间', type: 'text', queryKey: 'stopAddress' }, + { prop: 'deliveryAddressTime', label: '送货地址/时间', type: 'text', queryKey: 'stopAddress' }, { prop: 'productName', label: '产品名称', type: 'text', queryKey: 'productName' }, { prop: 'productSpec', label: '型号/规格', type: 'text', queryKey: 'productSpec' }, { @@ -64,29 +65,61 @@ export const plPlanColumnFilterDefs: ColumnFilterDef[] = [ queryKey: 'packageSpec' }, { prop: 'productUnit', label: '单位', type: 'text', queryKey: 'productUnit' }, - { prop: 'productPlanQty', label: '计划数量', type: 'number', queryKeyPrefix: 'productPlanQty' }, + { prop: 'loadPlanQty', label: '装货计划量', type: 'number', queryKeyPrefix: 'productPlanQty' }, + { prop: 'deliveryPlanQty', label: '送货计划量', type: 'number', queryKeyPrefix: 'productPlanQty' }, { - prop: 'productActualQty', - label: '实际数量', + prop: 'loadActualQty', + label: '装货实际量', type: 'number', queryKeyPrefix: 'productActualQty' }, - { prop: 'stopContract', label: '合同', type: 'text', queryKey: 'contractNo' }, { - prop: 'productInboundQty', - label: '入库数量', + prop: 'deliveryActualQty', + label: '送货实际量', + type: 'number', + queryKeyPrefix: 'productActualQty' + }, + { prop: 'loadContract', label: '装货合同', type: 'text', queryKey: 'contractNo' }, + { prop: 'deliveryContract', label: '送货合同', type: 'text', queryKey: 'contractNo' }, + { + prop: 'loadInboundQty', + label: '装货入库', type: 'number', queryKeyPrefix: 'productInboundQty' }, { - prop: 'productOutboundQty', - label: '出库数量', + prop: 'deliveryInboundQty', + label: '送货入库', + type: 'number', + queryKeyPrefix: 'productInboundQty' + }, + { + prop: 'loadOutboundQty', + label: '装货出库', type: 'number', queryKeyPrefix: 'productOutboundQty' }, { - prop: 'stopDibang', - label: '地磅', + prop: 'deliveryOutboundQty', + label: '送货出库', + type: 'number', + queryKeyPrefix: 'productOutboundQty' + }, + { + prop: 'loadDibang', + label: '装货地磅', + type: 'enum', + queryKey: 'stopDibang', + enumOptions: [ + { value: 'outbound', label: '出库' }, + { value: 'inbound', label: '入库' }, + { value: 'none', label: '无' }, + { value: 'off', label: '未生成' } + ] + }, + { + prop: 'deliveryDibang', + label: '送货地磅', type: 'enum', queryKey: 'stopDibang', enumOptions: [ diff --git a/web-admin/src/views/pl/plan/index.vue b/web-admin/src/views/pl/plan/index.vue index 2a71a6802..1fc597a0d 100644 --- a/web-admin/src/views/pl/plan/index.vue +++ b/web-admin/src/views/pl/plan/index.vue @@ -386,9 +386,9 @@ + + + + @@ -463,7 +479,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -1147,18 +1251,17 @@ formatStopDibangLabel, getDeliveryContractLinesFromRow, getDeliveryCustomerNameFromRow, - getDeliveryProductsFromRow, getLoadContractLinesFromRow, getLoadCustomerNameFromRow, - getLoadProductsFromRow, getPairDeliveryStop, getPairLoadStop, getPlanListDisplayIndex, getPlPlanListRowClassName, + getRowSideIoQty, + getRowSidePlanQty, + getStopArchiveActualQty, getStopDibangTagType, resolveStopCertProducts, - sumStopArchiveActualQty, - sumStopProductsField, type PlPlanListDisplayRow } from '@/utils/pl-plan-list-display-rows' import { @@ -1227,22 +1330,29 @@ { prop: 'arrangeCopy', label: '链接复制' }, { prop: 'loadCustomer', label: '装货客户' }, { prop: 'deliveryCustomer', label: '送货客户' }, - { prop: 'stopAddressTime', label: '地址 / 时间' }, + { prop: 'loadAddressTime', label: '装货地址/时间' }, + { prop: 'deliveryAddressTime', label: '送货地址/时间' }, { prop: 'productName', label: '产品名称' }, { prop: 'productSpec', label: '型号/规格' }, { prop: 'productPackageSpec', label: '包装规格' }, { prop: 'productUnit', label: '单位' }, - { prop: 'productPlanQty', label: '计划数量' }, - { prop: 'productActualQty', label: '实际数量' }, + { prop: 'loadPlanQty', label: '装货计划量' }, + { prop: 'deliveryPlanQty', label: '送货计划量' }, + { prop: 'loadActualQty', label: '装货实际量' }, + { prop: 'deliveryActualQty', label: '送货实际量' }, { prop: 'planAttachments', label: '计划附件' }, - { prop: 'stopContract', label: '合同' }, - { prop: 'productInboundQty', label: '入库数量', defaultVisible: false }, - { prop: 'productOutboundQty', label: '出库数量', defaultVisible: false }, - { prop: 'stopDibang', label: '地磅' } + { prop: 'loadContract', label: '装货合同' }, + { prop: 'deliveryContract', label: '送货合同' }, + { prop: 'loadInboundQty', label: '装货入库', defaultVisible: false }, + { prop: 'deliveryInboundQty', label: '送货入库', defaultVisible: false }, + { prop: 'loadOutboundQty', label: '装货出库', defaultVisible: false }, + { prop: 'deliveryOutboundQty', label: '送货出库', defaultVisible: false }, + { prop: 'loadDibang', label: '装货地磅' }, + { prop: 'deliveryDibang', label: '送货地磅' } ] const { visibleColumns, checkAll, isIndeterminate, isColumnVisible, resetToDefault } = - usePersistedListColumns('pl-plan-list-columns-v6', listColumnDefs) + usePersistedListColumns('pl-plan-list-columns-v7', listColumnDefs) const { filters, @@ -1298,6 +1408,34 @@ resetToInitial() } + function textFilterKeywords(value: unknown): string[] | null { + if (value == null || value === '') return null + if (typeof value === 'string') { + const text = value.trim() + return text ? [text] : null + } + if (typeof value === 'object') { + const obj = value as { value?: unknown; keyword?: unknown; selected?: string[] } + if (Array.isArray(obj.selected)) return selectedKeywordsFromFilter(obj) + const text = String(obj.value ?? obj.keyword ?? '').trim() + return text ? [text] : null + } + return null + } + + function isFirstLoadProductStatRow(row: PlPlanListDisplayRow): boolean { + const productKey = String( + row.loadProduct?.planProductId || row.loadProduct?.productId || '' + ).trim() + if (!row.plan?.id || !productKey) return false + const first = displayRows.value.find((item) => { + if (item.plan?.id !== row.plan.id) return false + const key = String(item.loadProduct?.planProductId || item.loadProduct?.productId || '').trim() + return key === productKey + }) + return first?.rowKey === row.rowKey + } + /** 装/送客户漏斗(配对行客户端过滤)。 */ const planLoadCustomerKeywords = computed(() => selectedKeywordsFromFilter(filters.loadCustomer as { selected?: string[] } | undefined) @@ -1305,11 +1443,13 @@ const planDeliveryCustomerKeywords = computed(() => selectedKeywordsFromFilter(filters.deliveryCustomer as { selected?: string[] } | undefined) ) + const planProductKeywords = computed(() => textFilterKeywords(filters.productName)) const displayRows = computed(() => flattenPlanArchivePairRows(planList.value, { loadCustomerKeywords: planLoadCustomerKeywords.value, - deliveryCustomerKeywords: planDeliveryCustomerKeywords.value + deliveryCustomerKeywords: planDeliveryCustomerKeywords.value, + productKeywords: planProductKeywords.value }) ) @@ -1386,10 +1526,10 @@ tableEl: planTableWrapRef, function getDisplayStatCellValue(row: PlPlanListDisplayRow, prop: string): unknown { const plan = row.plan - const loadProducts = getLoadProductsFromRow(row) - const deliveryProducts = getDeliveryProductsFromRow(row) const loadStop = getPairLoadStop(row) const deliveryStop = getPairDeliveryStop(row) + const deliveryProduct = row.deliveryProduct + const countLoadQty = isFirstLoadProductStatRow(row) if (prop === 'planNo') return plan.planNo if (prop === 'followStatus') return getPlPlanFollowStatusLabel(plan.followStatus) if (prop === 'planType') return getPlanTypeLabel(plan.planType) @@ -1406,72 +1546,44 @@ tableEl: planTableWrapRef, if (prop === 'followUserName') return plan.followUserName if (prop === 'loadCustomer') return getLoadCustomerNameFromRow(row) if (prop === 'deliveryCustomer') return getDeliveryCustomerNameFromRow(row) - if (prop === 'stopAddressTime') { - const loadPart = `${formatStopAddressText(loadStop)} ${formatLoadTimeText(loadStop)}`.trim() - const deliveryPart = - `${formatStopAddressText(deliveryStop)} ${formatDeliveryTimeText(deliveryStop)}`.trim() - return `装 ${loadPart} / 送 ${deliveryPart}` + if (prop === 'loadAddressTime') { + return `${formatStopAddressText(loadStop)} ${formatLoadTimeText(loadStop)}`.trim() } - if (prop === 'stopDibang') { - return `装 ${formatStopDibangLabel(loadStop!) || '—'} / 送 ${formatStopDibangLabel(deliveryStop!) || '—'}` + if (prop === 'deliveryAddressTime') { + return `${formatStopAddressText(deliveryStop)} ${formatDeliveryTimeText(deliveryStop)}`.trim() } - if (prop === 'stopContract') { - const loadLines = getLoadContractLinesFromRow(row) - .map((l) => `${l.side}${l.no}`) - .join(' ') - const deliveryLines = getDeliveryContractLinesFromRow(row) - .map((l) => `${l.side}${l.no}`) - .join(' ') - return `装 ${loadLines || '—'} / 送 ${deliveryLines || '—'}` - } - if (prop === 'productPlanQty') { + if (prop === 'loadDibang') return formatStopDibangLabel(loadStop) || '—' + if (prop === 'deliveryDibang') return formatStopDibangLabel(deliveryStop) || '—' + if (prop === 'loadContract') { return ( - sumStopProductsField(loadProducts, 'planQty') + - sumStopProductsField(deliveryProducts, 'planQty') + getLoadContractLinesFromRow(row) + .map((line) => `${line.side}${line.no}`) + .join(' ') || '—' ) } - if (prop === 'productInboundQty') { + if (prop === 'deliveryContract') { return ( - sumStopProductsField(loadProducts, 'inboundQty') + - sumStopProductsField(deliveryProducts, 'inboundQty') + getDeliveryContractLinesFromRow(row) + .map((line) => `${line.side}${line.no}`) + .join(' ') || '—' ) } - if (prop === 'productOutboundQty') { - return ( - sumStopProductsField(loadProducts, 'outboundQty') + - sumStopProductsField(deliveryProducts, 'outboundQty') - ) + if (prop === 'loadPlanQty') return countLoadQty ? getRowSidePlanQty(row, 'load') : 0 + if (prop === 'deliveryPlanQty') return getRowSidePlanQty(row, 'delivery') + if (prop === 'loadActualQty') { + return countLoadQty && row.loadProduct ? getStopArchiveActualQty(row.loadProduct, 'load') : 0 } - if (prop === 'productActualQty') { - return ( - sumStopArchiveActualQty(loadProducts, 'load') + - sumStopArchiveActualQty(deliveryProducts, 'delivery') - ) - } - if (prop === 'productName') { - return deliveryProducts - .map((p) => p.productName || '') - .filter(Boolean) - .join('\n') - } - if (prop === 'productSpec') { - return deliveryProducts - .map((p) => p.model || p.packageSpec || '') - .filter(Boolean) - .join('\n') - } - if (prop === 'productPackageSpec') { - return deliveryProducts - .map((p) => p.packageSpec || '') - .filter(Boolean) - .join('\n') - } - if (prop === 'productUnit') { - return deliveryProducts - .map((p) => p.unit || '') - .filter(Boolean) - .join('\n') + if (prop === 'deliveryActualQty') { + return deliveryProduct ? getStopArchiveActualQty(deliveryProduct, 'delivery') : 0 } + if (prop === 'loadInboundQty') return countLoadQty ? getRowSideIoQty(row, 'load', 'inbound') : 0 + if (prop === 'deliveryInboundQty') return getRowSideIoQty(row, 'delivery', 'inbound') + if (prop === 'loadOutboundQty') return countLoadQty ? getRowSideIoQty(row, 'load', 'outbound') : 0 + if (prop === 'deliveryOutboundQty') return getRowSideIoQty(row, 'delivery', 'outbound') + if (prop === 'productName') return deliveryProduct?.productName || '—' + if (prop === 'productSpec') return deliveryProduct?.model || deliveryProduct?.packageSpec || '—' + if (prop === 'productPackageSpec') return deliveryProduct?.packageSpec || '—' + if (prop === 'productUnit') return deliveryProduct?.unit || '—' if (prop === 'planAttachments' || prop === 'arrangeCopy') return '' return '' } @@ -1543,14 +1655,7 @@ tableEl: planTableWrapRef, } const planExportColumns = computed(() => { - const skip = new Set([ - 'arrangeCopy', - 'loadCustomer', - 'deliveryCustomer', - 'stopAddressTime', - 'stopDibang', - 'stopContract' - ]) + const skip = new Set(['arrangeCopy']) const cols = [...visibleColumns.value].filter((col) => !skip.has(col)) if (!cols.includes('planNo')) cols.unshift('planNo') return cols