Przeglądaj źródła

feat: split transport plan list columns into load and delivery product rows

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 3 tygodni temu
rodzic
commit
56776bb197

+ 4 - 1
web-admin/src/composables/usePlPlanMergeListTable.ts

@@ -28,6 +28,8 @@ export function usePlPlanMergeListTable(options: {
   archivePairMode?: boolean | Ref<boolean> | ComputedRef<boolean>
   loadCustomerKeywords?: Ref<string[] | null> | ComputedRef<string[] | null>
   deliveryCustomerKeywords?: Ref<string[] | null> | ComputedRef<string[] | null>
+  /** 产品名称关键字:只保留命中产品行并重算 rowspan */
+  productKeywords?: Ref<string[] | null> | ComputedRef<string[] | null>
 }) {
   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), {

+ 44 - 11
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: 'deliveryOutboundQty',
+    label: '送货出库',
     type: 'number',
     queryKeyPrefix: 'productOutboundQty'
   },
   {
-    prop: 'stopDibang',
-    label: '地磅',
+    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: [

+ 414 - 309
web-admin/src/views/pl/plan/index.vue

@@ -386,9 +386,9 @@
               </template>
             </el-table-column>
             <el-table-column
-              v-if="isColumnVisible('stopAddressTime')"
-              prop="stopAddressTime"
-              label="地址 / 时间"
+              v-if="isColumnVisible('loadAddressTime')"
+              prop="loadAddressTime"
+              label="装货地址/时间"
               min-width="240"
               align="left"
               header-align="left"
@@ -397,42 +397,58 @@
             >
               <template #header>
                 <div class="col-header-with-filter">
-                  <span class="col-header-with-filter__label is-left">地址 / 时间</span>
+                  <span class="col-header-with-filter__label is-left">装货地址/时间</span>
                   <ColumnHeaderFilter
-                    prop="stopAddressTime"
-                    label="地址 / 时间"
+                    prop="loadAddressTime"
+                    label="装货地址/时间"
                     type="text"
-                    :is-active="isFilterActive('stopAddressTime')"
-                    :model-value="filters.stopAddressTime || null"
-                    @update:model-value="(v) => setFilterState('stopAddressTime', v)"
+                    :is-active="isFilterActive('loadAddressTime')"
+                    :model-value="filters.loadAddressTime || null"
+                    @update:model-value="(v) => setFilterState('loadAddressTime', v)"
                   />
                 </div>
               </template>
               <template #default="{ row }">
-                <div class="pl-plan-pair-stack">
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--load">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--load">装</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <div class="pl-plan-pair-stack__name">
-                        {{ formatStopAddressText(getPairLoadStop(row)) }}
-                      </div>
-                      <div class="pl-plan-pair-stack__sub">
-                        {{ formatLoadTimeText(getPairLoadStop(row)) }}
-                      </div>
-                    </div>
+                <div class="pl-plan-stop-address-time">
+                  <div class="pl-plan-stop-address-time__addr">
+                    {{ formatStopAddressText(getPairLoadStop(row)) }}
                   </div>
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--delivery">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--delivery"
-                      >送</span
-                    >
-                    <div class="pl-plan-pair-stack__body">
-                      <div class="pl-plan-pair-stack__name">
-                        {{ formatStopAddressText(getPairDeliveryStop(row)) }}
-                      </div>
-                      <div class="pl-plan-pair-stack__sub">
-                        {{ formatDeliveryTimeText(getPairDeliveryStop(row)) }}
-                      </div>
-                    </div>
+                  <div class="pl-plan-stop-address-time__time">
+                    {{ formatLoadTimeText(getPairLoadStop(row)) }}
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="isColumnVisible('deliveryAddressTime')"
+              prop="deliveryAddressTime"
+              label="送货地址/时间"
+              min-width="240"
+              align="left"
+              header-align="left"
+              class-name="col-name-wrap pl-plan-stop-col"
+              label-class-name="col-name-wrap"
+            >
+              <template #header>
+                <div class="col-header-with-filter">
+                  <span class="col-header-with-filter__label is-left">送货地址/时间</span>
+                  <ColumnHeaderFilter
+                    prop="deliveryAddressTime"
+                    label="送货地址/时间"
+                    type="text"
+                    :is-active="isFilterActive('deliveryAddressTime')"
+                    :model-value="filters.deliveryAddressTime || null"
+                    @update:model-value="(v) => setFilterState('deliveryAddressTime', v)"
+                  />
+                </div>
+              </template>
+              <template #default="{ row }">
+                <div class="pl-plan-stop-address-time">
+                  <div class="pl-plan-stop-address-time__addr">
+                    {{ formatStopAddressText(getPairDeliveryStop(row)) }}
+                  </div>
+                  <div class="pl-plan-stop-address-time__time">
+                    {{ formatDeliveryTimeText(getPairDeliveryStop(row)) }}
                   </div>
                 </div>
               </template>
@@ -463,7 +479,7 @@
 
               <template #default="{ row }">
                 <PlPlanListProductCell
-                  :stop-products="getDeliveryProductsFromRow(row)"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
                   :plan-products="row.plan.products"
                   field="name"
                 />
@@ -494,7 +510,7 @@
               </template>
               <template #default="{ row }">
                 <PlPlanListProductCell
-                  :stop-products="getDeliveryProductsFromRow(row)"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
                   :plan-products="row.plan.products"
                   field="spec"
                 />
@@ -525,7 +541,7 @@
               </template>
               <template #default="{ row }">
                 <PlPlanListProductCell
-                  :stop-products="getDeliveryProductsFromRow(row)"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
                   :plan-products="row.plan.products"
                   field="packageSpec"
                 />
@@ -555,16 +571,16 @@
               </template>
               <template #default="{ row }">
                 <PlPlanListProductCell
-                  :stop-products="getDeliveryProductsFromRow(row)"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
                   :plan-products="row.plan.products"
                   field="unit"
                 />
               </template>
             </el-table-column>
             <el-table-column
-              v-if="isColumnVisible('productPlanQty')"
-              prop="productPlanQty"
-              label="计划数量"
+              v-if="isColumnVisible('loadPlanQty')"
+              prop="loadPlanQty"
+              label="装货计划量"
               min-width="100"
               align="right"
               header-align="right"
@@ -572,48 +588,61 @@
             >
               <template #header>
                 <div class="col-header-with-filter">
-                  <span class="col-header-with-filter__label is-right">计划数量</span>
+                  <span class="col-header-with-filter__label is-right">装货计划量</span>
                   <ColumnHeaderFilter
-                    prop="productPlanQty"
-                    label="计划数量"
+                    prop="loadPlanQty"
+                    label="装货计划量"
                     type="number"
-                    :is-active="isFilterActive('productPlanQty')"
-                    :model-value="filters.productPlanQty || null"
-                    @update:model-value="(v) => setFilterState('productPlanQty', v)"
+                    :is-active="isFilterActive('loadPlanQty')"
+                    :model-value="filters.loadPlanQty || null"
+                    @update:model-value="(v) => setFilterState('loadPlanQty', v)"
                   />
                 </div>
               </template>
               <template #default="{ row }">
-                <div class="pl-plan-pair-stack">
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--load">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--load">装</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="load"
-                        :stop-products="getLoadProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="planQty"
-                      />
-                    </div>
-                  </div>
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--delivery">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--delivery">送</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="delivery"
-                        :stop-products="getDeliveryProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="planQty"
-                      />
-                    </div>
-                  </div>
+                <PlPlanListProductCell
+                  stop-kind="load"
+                  :stop-products="row.loadProduct ? [row.loadProduct] : []"
+                  :plan-products="row.plan.products"
+                  field="planQty"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="isColumnVisible('deliveryPlanQty')"
+              prop="deliveryPlanQty"
+              label="送货计划量"
+              min-width="100"
+              align="right"
+              header-align="right"
+              class-name="pl-plan-stop-col"
+            >
+              <template #header>
+                <div class="col-header-with-filter">
+                  <span class="col-header-with-filter__label is-right">送货计划量</span>
+                  <ColumnHeaderFilter
+                    prop="deliveryPlanQty"
+                    label="送货计划量"
+                    type="number"
+                    :is-active="isFilterActive('deliveryPlanQty')"
+                    :model-value="filters.deliveryPlanQty || null"
+                    @update:model-value="(v) => setFilterState('deliveryPlanQty', v)"
+                  />
                 </div>
               </template>
+              <template #default="{ row }">
+                <PlPlanListProductCell
+                  stop-kind="delivery"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
+                  :plan-products="row.plan.products"
+                  field="planQty"
+                />
+              </template>
             </el-table-column>
             <el-table-column
-              v-if="isColumnVisible('productActualQty')"
-              prop="productActualQty"
-              label="实际数量"
+              v-if="isColumnVisible('loadActualQty')"
+              prop="loadActualQty"
+              label="装货实际量"
               min-width="100"
               align="right"
               header-align="right"
@@ -621,43 +650,56 @@
             >
               <template #header>
                 <div class="col-header-with-filter">
-                  <span class="col-header-with-filter__label is-right">实际数量</span>
+                  <span class="col-header-with-filter__label is-right">装货实际量</span>
                   <ColumnHeaderFilter
-                    prop="productActualQty"
-                    label="实际数量"
+                    prop="loadActualQty"
+                    label="装货实际量"
                     type="number"
-                    :is-active="isFilterActive('productActualQty')"
-                    :model-value="filters.productActualQty || null"
-                    @update:model-value="(v) => setFilterState('productActualQty', v)"
+                    :is-active="isFilterActive('loadActualQty')"
+                    :model-value="filters.loadActualQty || null"
+                    @update:model-value="(v) => setFilterState('loadActualQty', v)"
                   />
                 </div>
               </template>
               <template #default="{ row }">
-                <div class="pl-plan-pair-stack">
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--load">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--load">装</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="load"
-                        :stop-products="getLoadProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="actualQty"
-                      />
-                    </div>
-                  </div>
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--delivery">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--delivery">送</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="delivery"
-                        :stop-products="getDeliveryProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="actualQty"
-                      />
-                    </div>
-                  </div>
+                <PlPlanListProductCell
+                  stop-kind="load"
+                  :stop-products="row.loadProduct ? [row.loadProduct] : []"
+                  :plan-products="row.plan.products"
+                  field="actualQty"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="isColumnVisible('deliveryActualQty')"
+              prop="deliveryActualQty"
+              label="送货实际量"
+              min-width="100"
+              align="right"
+              header-align="right"
+              class-name="pl-plan-stop-col"
+            >
+              <template #header>
+                <div class="col-header-with-filter">
+                  <span class="col-header-with-filter__label is-right">送货实际量</span>
+                  <ColumnHeaderFilter
+                    prop="deliveryActualQty"
+                    label="送货实际量"
+                    type="number"
+                    :is-active="isFilterActive('deliveryActualQty')"
+                    :model-value="filters.deliveryActualQty || null"
+                    @update:model-value="(v) => setFilterState('deliveryActualQty', v)"
+                  />
                 </div>
               </template>
+              <template #default="{ row }">
+                <PlPlanListProductCell
+                  stop-kind="delivery"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
+                  :plan-products="row.plan.products"
+                  field="actualQty"
+                />
+              </template>
             </el-table-column>
             <el-table-column
               v-if="isColumnVisible('planAttachments')"
@@ -669,16 +711,21 @@
             >
               <template #default="{ row }">
                 <PlPlanListCertCell
-                  :products="resolveStopCertProducts(row.plan, getDeliveryProductsFromRow(row))"
+                  :products="
+                    resolveStopCertProducts(
+                      row.plan,
+                      row.deliveryProduct ? [row.deliveryProduct] : []
+                    )
+                  "
                   compact
                   hide-product
                 />
               </template>
             </el-table-column>
             <el-table-column
-              v-if="isColumnVisible('stopContract')"
-              prop="stopContract"
-              label="合同"
+              v-if="isColumnVisible('loadContract')"
+              prop="loadContract"
+              label="装货合同"
               min-width="180"
               align="left"
               header-align="left"
@@ -687,44 +734,58 @@
             >
               <template #header>
                 <div class="col-header-with-filter">
-                  <span class="col-header-with-filter__label is-left">合同</span>
+                  <span class="col-header-with-filter__label is-left">装货合同</span>
                   <ColumnHeaderFilter
-                    prop="stopContract"
-                    label="合同"
+                    prop="loadContract"
+                    label="装货合同"
                     type="text"
-                    :is-active="isFilterActive('stopContract')"
-                    :model-value="filters.stopContract || null"
-                    @update:model-value="(v) => setFilterState('stopContract', v)"
+                    :is-active="isFilterActive('loadContract')"
+                    :model-value="filters.loadContract || null"
+                    @update:model-value="(v) => setFilterState('loadContract', v)"
                   />
                 </div>
               </template>
               <template #default="{ row }">
-                <div class="pl-plan-pair-stack">
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--load">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--load">装</span>
-                    <div class="pl-plan-pair-stack__body pl-plan-pair-stack__contract">
-                      <PlPlanContractCell
-                        :lines="getLoadContractLinesFromRow(row)"
-                        @open="openContractDetail"
-                      />
-                    </div>
-                  </div>
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--delivery">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--delivery">送</span>
-                    <div class="pl-plan-pair-stack__body pl-plan-pair-stack__contract">
-                      <PlPlanContractCell
-                        :lines="getDeliveryContractLinesFromRow(row)"
-                        @open="openContractDetail"
-                      />
-                    </div>
-                  </div>
+                <PlPlanContractCell
+                  :lines="getLoadContractLinesFromRow(row)"
+                  @open="openContractDetail"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="isColumnVisible('deliveryContract')"
+              prop="deliveryContract"
+              label="送货合同"
+              min-width="180"
+              align="left"
+              header-align="left"
+              class-name="col-name-wrap pl-plan-stop-col"
+              label-class-name="col-name-wrap"
+            >
+              <template #header>
+                <div class="col-header-with-filter">
+                  <span class="col-header-with-filter__label is-left">送货合同</span>
+                  <ColumnHeaderFilter
+                    prop="deliveryContract"
+                    label="送货合同"
+                    type="text"
+                    :is-active="isFilterActive('deliveryContract')"
+                    :model-value="filters.deliveryContract || null"
+                    @update:model-value="(v) => setFilterState('deliveryContract', v)"
+                  />
                 </div>
               </template>
+              <template #default="{ row }">
+                <PlPlanContractCell
+                  :lines="getDeliveryContractLinesFromRow(row)"
+                  @open="openContractDetail"
+                />
+              </template>
             </el-table-column>
             <el-table-column
-              v-if="isColumnVisible('productInboundQty')"
-              prop="productInboundQty"
-              label="入库数量"
+              v-if="isColumnVisible('loadInboundQty')"
+              prop="loadInboundQty"
+              label="装货入库"
               min-width="100"
               align="right"
               header-align="right"
@@ -732,48 +793,63 @@
             >
               <template #header>
                 <div class="col-header-with-filter">
-                  <span class="col-header-with-filter__label is-right">入库数量</span>
+                  <span class="col-header-with-filter__label is-right">装货入库</span>
                   <ColumnHeaderFilter
-                    prop="productInboundQty"
-                    label="入库数量"
+                    prop="loadInboundQty"
+                    label="装货入库"
                     type="number"
-                    :is-active="isFilterActive('productInboundQty')"
-                    :model-value="filters.productInboundQty || null"
-                    @update:model-value="(v) => setFilterState('productInboundQty', v)"
+                    :is-active="isFilterActive('loadInboundQty')"
+                    :model-value="filters.loadInboundQty || null"
+                    @update:model-value="(v) => setFilterState('loadInboundQty', v)"
                   />
                 </div>
               </template>
               <template #default="{ row }">
-                <div class="pl-plan-pair-stack">
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--load">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--load">装</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="load"
-                        :stop-products="getLoadProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="inboundQty"
-                      />
-                    </div>
-                  </div>
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--delivery">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--delivery">送</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="delivery"
-                        :stop-products="getDeliveryProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="inboundQty"
-                      />
-                    </div>
-                  </div>
+                <PlPlanListProductCell
+                  stop-kind="load"
+                  :stop-products="row.loadProduct ? [row.loadProduct] : []"
+                  :plan-products="row.plan.products"
+                  :stop-customer-name="getPairLoadStop(row)?.customerName"
+                  field="inboundQty"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="isColumnVisible('deliveryInboundQty')"
+              prop="deliveryInboundQty"
+              label="送货入库"
+              min-width="100"
+              align="right"
+              header-align="right"
+              class-name="pl-plan-stop-col"
+            >
+              <template #header>
+                <div class="col-header-with-filter">
+                  <span class="col-header-with-filter__label is-right">送货入库</span>
+                  <ColumnHeaderFilter
+                    prop="deliveryInboundQty"
+                    label="送货入库"
+                    type="number"
+                    :is-active="isFilterActive('deliveryInboundQty')"
+                    :model-value="filters.deliveryInboundQty || null"
+                    @update:model-value="(v) => setFilterState('deliveryInboundQty', v)"
+                  />
                 </div>
               </template>
+              <template #default="{ row }">
+                <PlPlanListProductCell
+                  stop-kind="delivery"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
+                  :plan-products="row.plan.products"
+                  :stop-customer-name="getPairDeliveryStop(row)?.customerName"
+                  field="inboundQty"
+                />
+              </template>
             </el-table-column>
             <el-table-column
-              v-if="isColumnVisible('productOutboundQty')"
-              prop="productOutboundQty"
-              label="出库数量"
+              v-if="isColumnVisible('loadOutboundQty')"
+              prop="loadOutboundQty"
+              label="装货出库"
               min-width="100"
               align="right"
               header-align="right"
@@ -781,96 +857,124 @@
             >
               <template #header>
                 <div class="col-header-with-filter">
-                  <span class="col-header-with-filter__label is-right">出库数量</span>
+                  <span class="col-header-with-filter__label is-right">装货出库</span>
                   <ColumnHeaderFilter
-                    prop="productOutboundQty"
-                    label="出库数量"
+                    prop="loadOutboundQty"
+                    label="装货出库"
                     type="number"
-                    :is-active="isFilterActive('productOutboundQty')"
-                    :model-value="filters.productOutboundQty || null"
-                    @update:model-value="(v) => setFilterState('productOutboundQty', v)"
+                    :is-active="isFilterActive('loadOutboundQty')"
+                    :model-value="filters.loadOutboundQty || null"
+                    @update:model-value="(v) => setFilterState('loadOutboundQty', v)"
                   />
                 </div>
               </template>
               <template #default="{ row }">
-                <div class="pl-plan-pair-stack">
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--load">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--load">装</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="load"
-                        :stop-products="getLoadProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="outboundQty"
-                      />
-                    </div>
-                  </div>
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--delivery">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--delivery">送</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <PlPlanListProductCell
-                        stop-kind="delivery"
-                        :stop-products="getDeliveryProductsFromRow(row)"
-                        :plan-products="row.plan.products"
-                        field="outboundQty"
-                      />
-                    </div>
-                  </div>
+                <PlPlanListProductCell
+                  stop-kind="load"
+                  :stop-products="row.loadProduct ? [row.loadProduct] : []"
+                  :plan-products="row.plan.products"
+                  :stop-customer-name="getPairLoadStop(row)?.customerName"
+                  field="outboundQty"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="isColumnVisible('deliveryOutboundQty')"
+              prop="deliveryOutboundQty"
+              label="送货出库"
+              min-width="100"
+              align="right"
+              header-align="right"
+              class-name="pl-plan-stop-col"
+            >
+              <template #header>
+                <div class="col-header-with-filter">
+                  <span class="col-header-with-filter__label is-right">送货出库</span>
+                  <ColumnHeaderFilter
+                    prop="deliveryOutboundQty"
+                    label="送货出库"
+                    type="number"
+                    :is-active="isFilterActive('deliveryOutboundQty')"
+                    :model-value="filters.deliveryOutboundQty || null"
+                    @update:model-value="(v) => setFilterState('deliveryOutboundQty', v)"
+                  />
                 </div>
               </template>
+              <template #default="{ row }">
+                <PlPlanListProductCell
+                  stop-kind="delivery"
+                  :stop-products="row.deliveryProduct ? [row.deliveryProduct] : []"
+                  :plan-products="row.plan.products"
+                  :stop-customer-name="getPairDeliveryStop(row)?.customerName"
+                  field="outboundQty"
+                />
+              </template>
             </el-table-column>
             <el-table-column
-              v-if="isColumnVisible('stopDibang')"
-              prop="stopDibang"
-              label="地磅"
+              v-if="isColumnVisible('loadDibang')"
+              prop="loadDibang"
+              label="装货地磅"
               min-width="120"
               align="center"
               class-name="pl-plan-stop-col"
             >
               <template #header>
                 <div class="col-header-with-filter">
-                  <span class="col-header-with-filter__label">地磅</span>
+                  <span class="col-header-with-filter__label">装货地磅</span>
                   <ColumnHeaderFilter
-                    prop="stopDibang"
-                    label="地磅"
+                    prop="loadDibang"
+                    label="装货地磅"
                     type="enum"
-                    :is-active="isFilterActive('stopDibang')"
-                    :model-value="filters.stopDibang || null"
-                    :options="enumOptionsFor('stopDibang')"
-                    @update:model-value="(v) => setFilterState('stopDibang', v)"
+                    :is-active="isFilterActive('loadDibang')"
+                    :model-value="filters.loadDibang || null"
+                    :options="enumOptionsFor('loadDibang')"
+                    @update:model-value="(v) => setFilterState('loadDibang', v)"
                   />
                 </div>
               </template>
               <template #default="{ row }">
-                <div class="pl-plan-pair-stack">
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--load">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--load">装</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <el-tag
-                        v-if="formatStopDibangLabel(getPairLoadStop(row)!)"
-                        :type="getStopDibangTagType(getPairLoadStop(row)!)"
-                        size="small"
-                      >
-                        {{ formatStopDibangLabel(getPairLoadStop(row)!) }}
-                      </el-tag>
-                      <span v-else class="muted">—</span>
-                    </div>
-                  </div>
-                  <div class="pl-plan-pair-stack__row pl-plan-pair-stack__row--delivery">
-                    <span class="pl-plan-pair-stack__label pl-plan-pair-stack__label--delivery">送</span>
-                    <div class="pl-plan-pair-stack__body">
-                      <el-tag
-                        v-if="formatStopDibangLabel(getPairDeliveryStop(row)!)"
-                        :type="getStopDibangTagType(getPairDeliveryStop(row)!)"
-                        size="small"
-                      >
-                        {{ formatStopDibangLabel(getPairDeliveryStop(row)!) }}
-                      </el-tag>
-                      <span v-else class="muted">—</span>
-                    </div>
-                  </div>
+                <el-tag
+                  v-if="formatStopDibangLabel(getPairLoadStop(row)!)"
+                  :type="getStopDibangTagType(getPairLoadStop(row)!)"
+                  size="small"
+                >
+                  {{ formatStopDibangLabel(getPairLoadStop(row)!) }}
+                </el-tag>
+                <span v-else class="muted">—</span>
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="isColumnVisible('deliveryDibang')"
+              prop="deliveryDibang"
+              label="送货地磅"
+              min-width="120"
+              align="center"
+              class-name="pl-plan-stop-col"
+            >
+              <template #header>
+                <div class="col-header-with-filter">
+                  <span class="col-header-with-filter__label">送货地磅</span>
+                  <ColumnHeaderFilter
+                    prop="deliveryDibang"
+                    label="送货地磅"
+                    type="enum"
+                    :is-active="isFilterActive('deliveryDibang')"
+                    :model-value="filters.deliveryDibang || null"
+                    :options="enumOptionsFor('deliveryDibang')"
+                    @update:model-value="(v) => setFilterState('deliveryDibang', v)"
+                  />
                 </div>
               </template>
+              <template #default="{ row }">
+                <el-tag
+                  v-if="formatStopDibangLabel(getPairDeliveryStop(row)!)"
+                  :type="getStopDibangTagType(getPairDeliveryStop(row)!)"
+                  size="small"
+                >
+                  {{ formatStopDibangLabel(getPairDeliveryStop(row)!) }}
+                </el-tag>
+                <span v-else class="muted">—</span>
+              </template>
             </el-table-column>
           </ListTable>
           </div>
@@ -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 === 'stopDibang') {
-      return `装 ${formatStopDibangLabel(loadStop!) || '—'} / 送 ${formatStopDibangLabel(deliveryStop!) || '—'}`
-    }
-    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') {
-      return (
-        sumStopProductsField(loadProducts, 'planQty') +
-        sumStopProductsField(deliveryProducts, 'planQty')
-      )
+    if (prop === 'loadAddressTime') {
+      return `${formatStopAddressText(loadStop)} ${formatLoadTimeText(loadStop)}`.trim()
     }
-    if (prop === 'productInboundQty') {
-      return (
-        sumStopProductsField(loadProducts, 'inboundQty') +
-        sumStopProductsField(deliveryProducts, 'inboundQty')
-      )
+    if (prop === 'deliveryAddressTime') {
+      return `${formatStopAddressText(deliveryStop)} ${formatDeliveryTimeText(deliveryStop)}`.trim()
     }
-    if (prop === 'productOutboundQty') {
+    if (prop === 'loadDibang') return formatStopDibangLabel(loadStop) || '—'
+    if (prop === 'deliveryDibang') return formatStopDibangLabel(deliveryStop) || '—'
+    if (prop === 'loadContract') {
       return (
-        sumStopProductsField(loadProducts, 'outboundQty') +
-        sumStopProductsField(deliveryProducts, 'outboundQty')
+        getLoadContractLinesFromRow(row)
+          .map((line) => `${line.side}${line.no}`)
+          .join(' ') || '—'
       )
     }
-    if (prop === 'productActualQty') {
+    if (prop === 'deliveryContract') {
       return (
-        sumStopArchiveActualQty(loadProducts, 'load') +
-        sumStopArchiveActualQty(deliveryProducts, 'delivery')
+        getDeliveryContractLinesFromRow(row)
+          .map((line) => `${line.side}${line.no}`)
+          .join(' ') || '—'
       )
     }
-    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 === '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 === '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