Forráskód Böngészése

fix(web-admin): align site/driver public brand qty with preference semantics

Ensure required mode keeps full brand options and multi-select entry.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 hónapja
szülő
commit
e6a1c1db2b
1 módosított fájl, 548 hozzáadás és 407 törlés
  1. 548 407
      web-admin/src/components/business/pl-plan-view-tabs/index.vue

+ 548 - 407
web-admin/src/components/business/pl-plan-view-tabs/index.vue

@@ -116,331 +116,11 @@
           <ViewKvTable :fields="freightViewFields" mode="sept" />
         </section>
 
-        <section class="pl-plan-form-section pl-plan-form-section--plain">
-          <div class="pl-plan-form-section-head">
-            <h3 class="pl-plan-form-section-title">司机回传信息</h3>
-            <div v-if="feedbackEditable" class="pl-plan-form-section-actions">
-              <el-button
-                type="primary"
-                class="btn-op-item"
-                :disabled="!canEditDriverFeedback"
-                @click="openDriverFeedbackDialog"
-              >
-                <el-icon><Edit /></el-icon>
-                {{ hasDriverFeedback ? '编辑' : '补充' }}
-              </el-button>
-              <el-button
-                v-if="arrangeDriverUrl"
-                class="btn-op-item"
-                @click="copyArrangeDriverUrl"
-              >
-                <el-icon><CopyDocument /></el-icon>
-                复制司机链接
-              </el-button>
-            </div>
-          </div>
-          <p v-if="!hasDriverFeedback" class="pl-plan-form-empty-hint">暂无司机回传</p>
-          <template v-else>
-            <div class="view-subtable-wrap">
-              <el-table :data="driverFeedbackTableRows" class="view-subtable list-table" stripe>
-                <el-table-column
-                  type="index"
-                  label="序号"
-                  min-width="68"
-                  align="center"
-                  header-align="center"
-                />
-                <el-table-column
-                  prop="productName"
-                  label="产品"
-                  min-width="140"
-                  show-overflow-tooltip
-                />
-                <el-table-column
-                  prop="actualQty"
-                  label="回传数量"
-                  min-width="100"
-                  align="right"
-                >
-                  <template #default="{ row }">
-                    {{ row.actualQty ?? '—' }}
-                  </template>
-                </el-table-column>
-                <el-table-column label="品牌数量" min-width="180" show-overflow-tooltip>
-                  <template #default="{ row }">
-                    {{ row.brandSummary || '—' }}
-                  </template>
-                </el-table-column>
-              </el-table>
-            </div>
-            <div class="pl-plan-io-site-meta">
-              <div class="pl-plan-io-site-meta__item">
-                <p class="pl-plan-form-empty-hint" style="margin-bottom: 6px">回传时间</p>
-                <p class="pl-plan-view-remark">{{ displayFeedbackTime }}</p>
-              </div>
-              <div class="pl-plan-io-site-meta__item">
-                <p class="pl-plan-form-empty-hint" style="margin-bottom: 6px">回传备注</p>
-                <p class="pl-plan-view-remark">{{ transport?.feedbackRemark || '—' }}</p>
-              </div>
-              <div class="pl-plan-io-site-meta__item">
-                <p class="pl-plan-form-empty-hint" style="margin-bottom: 6px">回传附件</p>
-                <AttachmentUploadField
-                  v-if="feedbackImageCsv"
-                  :model-value="feedbackImageCsv"
-                  disabled
-                  compact
-                />
-                <span v-else class="pl-plan-feedback-table__empty-cell">—</span>
-              </div>
-            </div>
-          </template>
-        </section>
-
-        <section class="pl-plan-form-section pl-plan-form-section--plain">
-          <div class="pl-plan-form-section-head">
-            <h3 class="pl-plan-form-section-title">
-              {{ ioOrder ? (ioOrder.kind === 'inbound' ? '入库单回传' : '出库单回传') : '现场回传信息' }}
-            </h3>
-            <div v-if="ioOrder" class="pl-plan-form-section-actions">
-              <el-button
-                v-if="feedbackEditable && ioOrder.status !== 'voided'"
-                type="primary"
-                class="btn-op-item"
-                @click="openIoOrderEditDialog"
-              >
-                <el-icon><Edit /></el-icon>
-                {{ ioOrderHasFeedback ? '编辑' : '补充' }}
-              </el-button>
-              <el-button
-                v-if="ioOrder.siteUrl"
-                class="btn-op-item"
-                @click="copyArrangeSiteUrl(ioOrder.siteUrl)"
-              >
-                <el-icon><CopyDocument /></el-icon>
-                复制现场链接
-              </el-button>
-            </div>
-          </div>
-          <!-- 新链路:入库/出库单权威展示 -->
-          <template v-if="ioOrder">
-            <ViewKvTable :fields="ioOrderViewFields" mode="quint" />
-            <div class="view-subtable-wrap" style="margin-top: 12px">
-              <el-table :data="ioOrder.items || []" class="view-subtable list-table" stripe>
-                <el-table-column type="index" label="序号" min-width="68" align="center" />
-                <el-table-column prop="productNo" label="产品编码" min-width="110" show-overflow-tooltip />
-                <el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
-                <el-table-column prop="specification" label="型号/规格" min-width="100" show-overflow-tooltip />
-                <el-table-column label="单位" min-width="72" align="center">
-                  <template #default="{ row }">{{ row.unitLabel || row.unit || '—' }}</template>
-                </el-table-column>
-                <el-table-column prop="planQty" label="计划数量" min-width="100" align="right" />
-                <el-table-column prop="actualQty" label="实际数量" min-width="100" align="right" />
-                <el-table-column label="品牌数量" min-width="180" show-overflow-tooltip>
-                  <template #default="{ row }">
-                    {{ resolveSiteBrandSummary(row) }}
-                  </template>
-                </el-table-column>
-                <el-table-column prop="packageSpec" label="包装规格" min-width="110" show-overflow-tooltip />
-                <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
-              </el-table>
-            </div>
-            <div class="pl-plan-io-site-meta">
-              <div class="pl-plan-io-site-meta__item">
-                <p class="pl-plan-form-empty-hint" style="margin-bottom: 6px">现场备注</p>
-                <p class="pl-plan-view-remark">{{ ioOrder.siteFeedbackRemark || '—' }}</p>
-              </div>
-              <div class="pl-plan-io-site-meta__item">
-                <p class="pl-plan-form-empty-hint" style="margin-bottom: 6px">现场附件</p>
-                <AttachmentUploadField
-                  v-if="ioOrderImageCsv"
-                  :model-value="ioOrderImageCsv"
-                  disabled
-                  compact
-                />
-                <span v-else class="pl-plan-feedback-table__empty-cell">—</span>
-              </div>
-            </div>
-          </template>
-          <!-- 旧链路:按产品现场 link -->
-          <template v-else>
-            <p v-if="!siteInventoryRows.length && !loading" class="pl-plan-form-empty-hint">
-              暂无现场链接
-            </p>
-            <div v-else class="view-subtable-wrap">
-              <el-table :data="siteFeedbackTableRows" class="view-subtable list-table" stripe>
-                <el-table-column type="index" label="序号" min-width="68" align="center" />
-                <el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
-                <el-table-column prop="siteFeedbackTime" label="保存时间" min-width="160" align="center" />
-                <el-table-column label="业务类型" min-width="100" align="center">
-                  <template #default="{ row }">
-                    <el-tag
-                      v-if="row.businessType && row.businessType !== '—'"
-                      :type="businessTypeTagType(row.businessType)"
-                      size="small"
-                    >
-                      {{ row.businessType }}
-                    </el-tag>
-                    <span v-else class="pl-plan-feedback-table__empty-cell">—</span>
-                  </template>
-                </el-table-column>
-                <el-table-column prop="siteFeedbackRemark" label="现场备注" min-width="160" show-overflow-tooltip />
-                <el-table-column label="现场附件" min-width="140" align="center">
-                  <template #default="{ row }">
-                    <div v-if="row.siteFeedbackImages" class="pl-plan-feedback-table__attachment">
-                      <AttachmentUploadField :model-value="row.siteFeedbackImages" disabled compact />
-                    </div>
-                    <span v-else class="pl-plan-feedback-table__empty-cell">—</span>
-                  </template>
-                </el-table-column>
-                <el-table-column label="操作" min-width="240" align="center" fixed="right">
-                  <template #default="{ row }">
-                    <el-button
-                      v-if="feedbackEditable"
-                      class="btn-op-item"
-                      @click="openSiteFeedbackDialog(row)"
-                    >
-                      <el-icon><Edit /></el-icon>
-                      {{ rowHasSiteFeedback(row) ? '编辑' : '补充' }}
-                    </el-button>
-                    <el-button class="btn-op-item" @click="copySiteSummaryForRow(row.sourceRow)">
-                      <el-icon><CopyDocument /></el-icon>
-                      复制给现场
-                    </el-button>
-                  </template>
-                </el-table-column>
-              </el-table>
-            </div>
-          </template>
-        </section>
-
-        <section class="pl-plan-form-section pl-plan-form-section--plain">
-          <h3 class="pl-plan-form-section-title">地磅单信息</h3>
-          <p v-if="!dibangPlanRows.length && !loading" class="pl-plan-form-empty-hint"
-            >暂无关联地磅单</p
-          >
-          <div
-            v-for="(row, idx) in dibangPlanRows"
-            :key="row.link.dbInventoryId || idx"
-            class="pl-plan-dibang-block"
-          >
-            <div class="pl-plan-dibang-head">
-              <span class="pl-plan-dibang-product">{{
-                row.link.productName || row.inventory?.product || '—'
-              }}</span>
-              <div class="pl-plan-dibang-head-actions">
-                <el-tag
-                  v-if="row.inventory?.status"
-                  :type="inventoryStatusTagType(row.inventory.status)"
-                  size="small"
-                >
-                  {{ row.inventory.status }}
-                </el-tag>
-              </div>
-            </div>
-
-            <ViewKvTable :fields="dibangBasicViewFields(row)" mode="hex" />
-
-            <template v-if="row.weighing">
-              <h5 class="pl-plan-dibang-weighing-title">地磅单</h5>
-              <div class="view-subtable-wrap">
-                <el-table :data="[row.weighing]" class="view-subtable list-table" stripe>
-                                    <el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
-                                    <el-table-column
-                    prop="batchNo"
-                    label="出入库单号"
-                    min-width="130"
-                    align="center"
-                  />
-                  <el-table-column prop="vehicleNo" label="车牌号" min-width="110" align="center" />
-                  <el-table-column
-                    prop="cargoName"
-                    label="货物名称"
-                    min-width="120"
-                    align="center"
-                    show-overflow-tooltip
-                  />
-                  <el-table-column
-                    prop="businessType"
-                    label="业务类型"
-                    min-width="100"
-                    align="center"
-                  >
-                    <template #default="{ row: weighingRow }">
-                      <el-tag :type="businessTypeTagType(weighingRow.businessType)" size="small">
-                        {{ weighingRow.businessType }}
-                      </el-tag>
-                    </template>
-                  </el-table-column>
-                  <el-table-column
-                    prop="supplier"
-                    label="供货单位"
-                    min-width="140"
-                    align="center"
-                    show-overflow-tooltip
-                  />
-                  <el-table-column
-                    prop="consignee"
-                    label="收货单位"
-                    min-width="140"
-                    align="center"
-                    show-overflow-tooltip
-                  />
-                  <el-table-column
-                    prop="grossWeight"
-                    label="毛重(kg)"
-                    min-width="100"
-                    align="right"
-                  />
-                  <el-table-column
-                    prop="grossWeightTime"
-                    label="毛重时间"
-                    min-width="160"
-                    align="center"
-                  >
-                    <template #default="{ row: weighingRow }">
-                      {{ formatDateTime(weighingRow.grossWeightTime) || '—' }}
-                    </template>
-                  </el-table-column>
-                  <el-table-column
-                    prop="tareWeight"
-                    label="皮重(kg)"
-                    min-width="100"
-                    align="right"
-                  />
-                  <el-table-column
-                    prop="tareWeightTime"
-                    label="皮重时间"
-                    min-width="160"
-                    align="center"
-                  >
-                    <template #default="{ row: weighingRow }">
-                      {{ formatDateTime(weighingRow.tareWeightTime) || '—' }}
-                    </template>
-                  </el-table-column>
-                  <el-table-column prop="deduction" label="扣杂(kg)" min-width="100" align="right">
-                    <template #default="{ row: weighingRow }">
-                      {{ weighingRow.deduction ?? 0 }}
-                    </template>
-                  </el-table-column>
-                  <el-table-column
-                    prop="netWeight"
-                    label="净重(kg)"
-                    min-width="100"
-                    align="right"
-                  />
-                  <el-table-column
-                    prop="remarks"
-                    label="备注"
-                    min-width="140"
-                    align="center"
-                    show-overflow-tooltip
-                  />
-                </el-table>
-              </div>
-            </template>
-            <p v-else class="pl-plan-form-empty-hint pl-plan-dibang-pending">尚未关联地磅单</p>
-          </div>
-        </section>
+        <PlPlanTransportFeedbackBlocks
+          v-if="!feedbackMirrorTo"
+          v-bind="feedbackBlocksBind"
+          v-on="feedbackBlocksOn"
+        />
       </el-tab-pane>
 
       <el-tab-pane label="合同信息" name="contract">
@@ -498,6 +178,11 @@
       </el-tab-pane>
     </el-tabs>
 
+    <!-- defer:目标在同次渲染的兄弟节点(归档左侧)时,等 flush 后再解析,避免 emitsOptions 空引用 -->
+    <Teleport v-if="feedbackMirrorTo" :to="feedbackMirrorTo" defer>
+      <PlPlanTransportFeedbackBlocks v-bind="feedbackBlocksBind" v-on="feedbackBlocksOn" />
+    </Teleport>
+
     <ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" hide-product-price />
     <PlPlanDriverFeedbackDialog
       v-model:visible="driverFeedbackDialogVisible"
@@ -525,56 +210,100 @@
     <el-dialog
       v-model="ioOrderEditVisible"
       :title="ioOrder?.kind === 'inbound' ? '编辑入库单回传' : '编辑出库单回传'"
-      width="1080px"
+      width="1280px"
       align-center
       destroy-on-close
-      class="dialog-form-layout"
+      append-to-body
+      class="dialog-form-layout pl-io-order-edit-dialog"
+      modal-class="order-module-dialog-mask"
       :close-on-click-modal="false"
+      style="backdrop-filter: none"
     >
-      <el-form v-if="ioOrderEditForm" label-position="top">
-        <el-form-item label="产品实际数量">
-          <div
-            v-for="(row, idx) in ioOrderEditForm.items || []"
-            :key="row.id"
-            class="pl-io-order-edit-item"
-          >
-            <div class="pl-io-order-edit-item__head">
-              <span class="pl-io-order-edit-item__name">{{ row.productName || '产品' }}</span>
-              <span class="pl-io-order-edit-item__badge">
-                产品 {{ idx + 1 }}/{{ ioOrderEditForm.items?.length || 0 }}
-              </span>
-            </div>
-            <el-table :data="[row]" size="small" border class="pl-io-order-edit-item__table">
-              <el-table-column prop="productNo" label="产品编码" min-width="110" show-overflow-tooltip />
-              <el-table-column prop="specification" label="型号/规格" min-width="100" show-overflow-tooltip />
-              <el-table-column label="单位" min-width="72" align="center">
-                <template #default="{ row: itemRow }">{{ itemRow.unitLabel || itemRow.unit || '—' }}</template>
-              </el-table-column>
-              <el-table-column prop="planQty" label="计划数量" min-width="100" align="right" />
-              <el-table-column label="实际数量" min-width="120">
-                <template #default="{ row: itemRow }">
+      <el-form v-if="ioOrderEditForm" label-position="top" class="pl-io-order-edit-form">
+        <el-form-item label="产品实际数量" class="pl-io-order-edit-products">
+          <div class="pl-io-order-edit-products__list">
+            <div
+              v-for="(row, idx) in ioOrderEditForm.items || []"
+              :key="row.id"
+              class="pl-io-order-edit-item"
+            >
+              <div class="pl-io-order-edit-item__head">
+                <div class="pl-io-order-edit-item__title">
+                  <span class="pl-io-order-edit-item__name">{{ row.productName || '产品' }}</span>
+                  <span v-if="row.productNo" class="pl-io-order-edit-item__no">{{
+                    row.productNo
+                  }}</span>
+                </div>
+                <span class="pl-io-order-edit-item__badge">
+                  产品 {{ idx + 1 }}/{{ ioOrderEditForm.items?.length || 0 }}
+                </span>
+              </div>
+              <div class="pl-io-order-edit-item__meta">
+                <div class="pl-io-order-edit-item__meta-item">
+                  <span class="pl-io-order-edit-item__meta-label">规格型号</span>
+                  <span class="pl-io-order-edit-item__meta-value">{{
+                    row.specification || '—'
+                  }}</span>
+                </div>
+                <div class="pl-io-order-edit-item__meta-item">
+                  <span class="pl-io-order-edit-item__meta-label">产品分类</span>
+                  <span class="pl-io-order-edit-item__meta-value">{{
+                    row.categoryName || '—'
+                  }}</span>
+                </div>
+                <div class="pl-io-order-edit-item__meta-item">
+                  <span class="pl-io-order-edit-item__meta-label">单位</span>
+                  <span class="pl-io-order-edit-item__meta-value">{{
+                    row.unitLabel || row.unit || '—'
+                  }}</span>
+                </div>
+                <div class="pl-io-order-edit-item__meta-item">
+                  <span class="pl-io-order-edit-item__meta-label">包装规格</span>
+                  <span class="pl-io-order-edit-item__meta-value">{{
+                    row.packageSpec || '—'
+                  }}</span>
+                </div>
+                <div class="pl-io-order-edit-item__meta-item">
+                  <span class="pl-io-order-edit-item__meta-label">送货方式</span>
+                  <span class="pl-io-order-edit-item__meta-value">{{
+                    getDeliveryMethodLabel(row.deliveryMethod)
+                  }}</span>
+                </div>
+              </div>
+              <div class="pl-io-order-edit-item__qty-row">
+                <div class="pl-io-order-edit-item__qty">
+                  <span class="pl-io-order-edit-item__qty-label">计划数量</span>
+                  <span class="pl-io-order-edit-item__qty-value">{{
+                    row.planQty != null ? row.planQty : '—'
+                  }}</span>
+                </div>
+                <div class="pl-io-order-edit-item__qty pl-io-order-edit-item__qty--actual">
+                  <span class="pl-io-order-edit-item__qty-label">实际数量</span>
                   <el-input-number
-                    v-model="itemRow.actualQty"
+                    v-model="row.actualQty"
                     :min="0"
                     :precision="3"
                     :controls="false"
-                    :disabled="Boolean(itemRow.brandMode)"
+                    :disabled="Boolean(row.brandMode)"
+                    class="pl-io-order-edit-item__qty-input"
                   />
-                </template>
-              </el-table-column>
-              <el-table-column prop="packageSpec" label="包装规格" min-width="110" show-overflow-tooltip />
-            </el-table>
-            <BrandQtyChipsField
-              v-if="row.brandMode"
-              v-model="row.brandQtyRows"
-              :brand-mode="ioBrandModeForField(row)"
-              :required-brand-id="row.brandId"
-              :brand-options="row.brandOptions || []"
-              :unit="row.unitLabel || row.unit || '吨'"
-              variant="admin"
-              :actual-qty="Number(row.actualQty ?? 0)"
-              @update:actual-qty="(val) => syncIoRowActualQty(row, val)"
-            />
+                  <span class="pl-io-order-edit-item__unit">{{
+                    row.unitLabel || row.unit || '吨'
+                  }}</span>
+                </div>
+              </div>
+              <BrandQtyChipsField
+                v-if="row.brandMode"
+                v-model="row.brandQtyRows"
+                :brand-mode="ioBrandModeForField(row)"
+                :required-brand-id="row.brandId"
+                :brand-options="row.brandOptions || []"
+                :unit="row.unitLabel || row.unit || '吨'"
+                variant="admin"
+                :actual-qty="Number(row.actualQty ?? 0)"
+                @update:actual-qty="(val) => syncIoRowActualQty(row, val)"
+              />
+            </div>
           </div>
         </el-form-item>
         <el-form-item label="现场备注">
@@ -585,8 +314,12 @@
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button @click="ioOrderEditVisible = false">取消</el-button>
-        <el-button type="primary" :loading="ioOrderSaving" @click="saveIoOrderEdit">确定</el-button>
+        <el-button @click="ioOrderEditVisible = false">
+          <el-icon><Close /></el-icon>取消
+        </el-button>
+        <el-button type="primary" :loading="ioOrderSaving" @click="saveIoOrderEdit">
+          <el-icon><Check /></el-icon>确定
+        </el-button>
       </template>
     </el-dialog>
     <PlVehicleViewDialog
@@ -598,7 +331,7 @@
 </template>
 
 <script setup lang="ts">
-  import { CopyDocument, Edit, View } from '@element-plus/icons-vue'
+  import { Check, Close, CopyDocument, View } from '@element-plus/icons-vue'
   import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
   import BrandQtyChipsField, {
     type BrandQtyRow
@@ -607,14 +340,14 @@
   import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
   import PlPlanHazardCertViewTable from '@/components/business/pl-plan-hazard-cert-view-table/index.vue'
   import PlPlanSiteFeedbackDialog from '@/components/business/pl-plan-site-feedback-dialog/index.vue'
+  import PlPlanTransportFeedbackBlocks, {
+    type SiteFeedbackTableRow
+  } from '@/components/business/pl-plan-transport-feedback-blocks/index.vue'
   import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
   import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
   import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
   import type { ViewField } from '@/components/business/system-view-dialog/types'
-  import {
-    businessTypeTagType,
-    inventoryStatusTagType
-  } from '@/components/business/db/db-related-view'
+  import { businessTypeTagType } from '@/components/business/db/db-related-view'
   import type { ContractVO } from '@/api/crm/contract/types'
   import { inventoryApi } from '@/api/db/inventory'
   import {
@@ -623,7 +356,12 @@
     type PlIoOrder,
     type TransportSiteLink
   } from '@/api/pl'
-  import { mapContractProducts, type PlanFormProductRow } from '@/composables/usePlContractToPlan'
+  import {
+    enrichPlanProductLinesAlias,
+    getDeliveryMethodLabel,
+    mapContractProducts,
+    type PlanFormProductRow
+  } from '@/composables/usePlContractToPlan'
   import { mapPlanProductToFormRow, usePlPlanViewDetail, type PlPlanTransportDibangRow, type PlPlanTransportSiteRow } from '@/composables/usePlPlanViewDetail'
   import { resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
   import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
@@ -641,8 +379,7 @@
     getPlPlanCompanyLabel,
     getPlPlanTypeLabel,
     getPlYesNoLabel,
-    parsePlFeedbackImages,
-    resolveSiteBrandSummary
+    parsePlFeedbackImages
   } from '@/utils/pl-display'
   import { formatMoneyDisplay } from '@/utils/money-format'
   import type { Transport, TransportDriverProductFeedback } from '@/api/pl'
@@ -661,13 +398,16 @@
       arrangeDriverUrl?: string
       arrangeSiteRecords?: TransportSiteLink[]
       feedbackEditable?: boolean
+      /** 将司机/现场/地磅回传区块镜像渲染到归档左侧等目标节点 */
+      feedbackMirrorTo?: string | HTMLElement | null
     }>(),
     {
       embedded: false,
       showArrangeLinks: false,
       arrangeDriverUrl: '',
       arrangeSiteRecords: () => [],
-      feedbackEditable: false
+      feedbackEditable: false,
+      feedbackMirrorTo: null
     }
   )
 
@@ -755,6 +495,21 @@
     row.actualQty = Number(val)
   }
 
+  function resolveIoBrandOptions(it: PlIoOrder['items'][number]) {
+    let options = [...(it.brandOptions || [])]
+    if (
+      it.brandMode === 'required' &&
+      it.brandId &&
+      !options.some((o) => o.brandId === it.brandId)
+    ) {
+      options = [
+        { brandId: it.brandId, brandName: it.brandName || '指定品牌' },
+        ...options
+      ]
+    }
+    return options
+  }
+
   function mapIoItemBrandQtyRows(it: PlIoOrder['items'][number]): BrandQtyRow[] {
     const saved = (it.brandQtys || [])
       .filter(r => Number(r.qty) > 0)
@@ -765,10 +520,14 @@
       }))
     if (saved.length) return saved
     if (it.brandMode === 'required' && it.brandId) {
+      const brandName =
+        it.brandName ||
+        (it.brandOptions || []).find((o) => o.brandId === it.brandId)?.brandName ||
+        '指定品牌'
       return [
         {
           brandId: it.brandId,
-          brandName: it.brandName || '指定品牌',
+          brandName,
           qty: Number(it.actualQty ?? 0)
         }
       ]
@@ -785,17 +544,25 @@
 
   function openIoOrderEditDialog() {
     if (!ioOrder.value) return
+    const planProductById = new Map((products.value || []).map(p => [p.id, p]))
     ioOrderEditForm.value = {
       ...ioOrder.value,
       items: (ioOrder.value.items || []).map(it => {
-        const brandQtyRows = mapIoItemBrandQtyRows(it)
+        const brandOptions = resolveIoBrandOptions(it)
+        const brandQtyRows = mapIoItemBrandQtyRows({ ...it, brandOptions })
         const actualQty = it.brandMode
           ? Number(
               brandQtyRows.reduce((sum, r) => sum + Number(r.qty || 0), 0).toFixed(3)
             )
           : it.actualQty
+        const planProduct = planProductById.get(it.planProductId)
         return {
           ...it,
+          categoryName: it.categoryName || planProduct?.categoryName || '',
+          specification: it.specification || planProduct?.model || '',
+          packageSpec: it.packageSpec || planProduct?.packageSpec || '',
+          deliveryMethod: it.deliveryMethod || planProduct?.deliveryMethod || '',
+          brandOptions,
           brandQtyRows,
           actualQty
         }
@@ -874,26 +641,237 @@
     return Array.isArray(imgs) ? imgs.filter(Boolean).join(',') : ''
   }
 
+  function mergeTaxRatesFromContract(
+    rows: PlanFormProductRow[],
+    contract: ContractVO | null
+  ): PlanFormProductRow[] {
+    if (!contract?.products?.length) return rows
+    const rateByProductId = new Map<string, number>()
+    for (const item of contract.products) {
+      const key = String(item.productId ?? item.id ?? '')
+      if (key && item.taxRate != null) rateByProductId.set(key, Number(item.taxRate))
+    }
+    return rows.map((row) => ({
+      ...row,
+      taxRate: row.taxRate ?? rateByProductId.get(row.productId)
+    }))
+  }
+
+  const productRows = ref<PlanFormProductRow[]>([])
+  let productRowsEnrichSeq = 0
+
+  async function refreshProductRows() {
+    const seq = ++productRowsEnrichSeq
+    const rows = products.value.map((item, index) => mapPlanProductToFormRow(item, index))
+    const withTax = mergeTaxRatesFromContract(rows, contractSnapshot.value)
+    // 先同步写入计划产品字段,避免别名异步回填完成前表格全是 —
+    productRows.value = withTax
+    const enriched = await enrichPlanProductLinesAlias(withTax.map((row) => ({ ...row })))
+    if (seq !== productRowsEnrichSeq) return
+    productRows.value = enriched
+  }
+
+  watch([products, contractSnapshot], () => {
+    void refreshProductRows()
+  }, { immediate: true, deep: true })
+
+  const planProductById = computed(() => {
+    const map = new Map<string, (typeof products.value)[number]>()
+    for (const row of products.value) {
+      if (row.id) map.set(String(row.id), row)
+    }
+    return map
+  })
+
+  const planProductFormById = computed(() => {
+    const map = new Map<string, PlanFormProductRow>()
+    for (const row of productRows.value) {
+      if (row.id) map.set(String(row.id), row)
+    }
+    return map
+  })
+
+  function resolveFeedbackPlanProductId(row: TransportDriverProductFeedback | Record<string, unknown>) {
+    const raw =
+      (row as TransportDriverProductFeedback).planProductId ||
+      (row as { plan_product_id?: string }).plan_product_id ||
+      ''
+    return String(raw || '').trim()
+  }
+
+  function resolveDisplayBrandLabel(options: {
+    brandMode?: string | null
+    brandName?: string | null
+  }) {
+    const mode = (options.brandMode || '').trim()
+    const name = (options.brandName || '').trim()
+    if (mode === 'required') return name || '指定品牌'
+    if (mode === 'any') return name || '不限品牌'
+    return name
+  }
+
+  function buildDriverFeedbackDisplayRow(
+    pp: (typeof products.value)[number] | undefined,
+    fb: TransportDriverProductFeedback | undefined,
+    formRow?: PlanFormProductRow
+  ) {
+    const unitCode = String(fb?.unit || pp?.unit || formRow?.unit || '').trim()
+    const unitLabel = dictStore.getLabel('crm_product_unit', unitCode) || unitCode
+    const brandLabel = resolveDisplayBrandLabel({
+      brandMode: fb?.brandMode || pp?.brandMode || formRow?.brandMode,
+      brandName: fb?.brandName || pp?.brandName || formRow?.brandName
+    })
+    return {
+      productName:
+        formRow?.standardProductName ||
+        pp?.productName ||
+        formRow?.productName ||
+        fb?.productName ||
+        '—',
+      aliasName: formRow?.aliasName || '',
+      model: fb?.model || pp?.model || formRow?.model || '',
+      unit: unitCode,
+      unitLabel,
+      packageSpec: fb?.packageSpec || pp?.packageSpec || formRow?.packageSpec || '',
+      brandName: brandLabel,
+      actualQty: fb?.actualQty,
+      brandSummary: fb?.brandSummary || formatBrandQtySummary(fb?.brandQtys) || '—'
+    }
+  }
+
   const driverFeedbackTableRows = computed(() => {
-    const rows = transport.value?.driverProductFeedbacks || []
-    if (rows.length) {
-      return rows.map((row: TransportDriverProductFeedback) => ({
-        productName: row.productName || '—',
-        actualQty: row.actualQty,
-        brandSummary: row.brandSummary || formatBrandQtySummary(row.brandQtys) || '—'
-      }))
+    const feedbacks = transport.value?.driverProductFeedbacks || []
+    const planList = products.value || []
+
+    if (feedbacks.length) {
+      const feedbackById = new Map(
+        feedbacks.map((fb) => [resolveFeedbackPlanProductId(fb), fb] as const)
+      )
+      const usedIds = new Set<string>()
+      const rows: ReturnType<typeof buildDriverFeedbackDisplayRow>[] = []
+
+      // 以计划产品为主键拼接,保证名称/规格/单位等一定能带出
+      for (const pp of planList) {
+        const fb = feedbackById.get(String(pp.id))
+        if (!fb) continue
+        usedIds.add(String(pp.id))
+        rows.push(buildDriverFeedbackDisplayRow(pp, fb, planProductFormById.value.get(String(pp.id))))
+      }
+
+      // 兼容:planProductId 对不上时按序号兜底
+      if (!rows.length && feedbacks.length === planList.length) {
+        return feedbacks.map((fb, index) =>
+          buildDriverFeedbackDisplayRow(
+            planList[index],
+            fb,
+            planProductFormById.value.get(String(planList[index]?.id || ''))
+          )
+        )
+      }
+
+      for (const fb of feedbacks) {
+        const id = resolveFeedbackPlanProductId(fb)
+        if (id && usedIds.has(id)) continue
+        const pp = id ? planProductById.value.get(id) : undefined
+        rows.push(buildDriverFeedbackDisplayRow(pp, fb, id ? planProductFormById.value.get(id) : undefined))
+      }
+      return rows
     }
+
     if (!hasDriverFeedback.value) return []
     // 兼容旧数据:仅有运输单总量、无按产品明细
     return [
       {
         productName: '(合计)',
+        aliasName: '',
+        model: '',
+        unit: '',
+        unitLabel: '',
+        packageSpec: '',
+        brandName: '',
         actualQty: transport.value?.feedbackQty,
         brandSummary: '—'
       }
     ]
   })
 
+  function resolveIoLinkedPlanProduct(
+    it: {
+      planProductId?: string
+      productId?: string
+      productName?: string
+      plan_product_id?: string
+      product_id?: string
+    },
+    index: number
+  ) {
+    const planList = products.value || []
+    const planProductId = String(it.planProductId || it.plan_product_id || '').trim()
+    if (planProductId) {
+      const byId = planProductById.value.get(planProductId)
+      if (byId) return byId
+    }
+    const crmId = String(it.productId || it.product_id || '').trim()
+    if (crmId) {
+      const byCrm = planList.find((p) => String(p.productId || '') === crmId)
+      if (byCrm) return byCrm
+    }
+    if (planList.length === (ioOrder.value?.items || []).length) {
+      const byIndex = planList[index]
+      if (byIndex) return byIndex
+    }
+    const name = String(it.productName || '').trim()
+    if (name) {
+      const byName = planList.find((p) => String(p.productName || '').trim() === name)
+      if (byName) return byName
+    }
+    return undefined
+  }
+
+  const ioOrderDisplayItems = computed(() => {
+    const items = ioOrder.value?.items || []
+    return items.map((it, index) => {
+      const pp = resolveIoLinkedPlanProduct(it, index)
+      const formRow = pp?.id
+        ? planProductFormById.value.get(String(pp.id))
+        : planProductFormById.value.get(String(it.planProductId || '').trim())
+      const productId = String(formRow?.productId || pp?.productId || it.productId || '').trim()
+      const candidates = [
+        String(it.productNo || '').trim(),
+        String(formRow?.productNo || '').trim(),
+        String(pp?.productNo || '').trim()
+      ].filter(Boolean)
+      const productNo =
+        candidates.find((no) => no && no !== productId) || candidates[0] || ''
+      const brandLabel = resolveDisplayBrandLabel({
+        brandMode: it.brandMode || pp?.brandMode || formRow?.brandMode,
+        brandName: it.brandName || pp?.brandName || formRow?.brandName
+      })
+      const unitCode = String(it.unit || pp?.unit || formRow?.unit || '').trim()
+      return {
+        ...it,
+        productNo: productNo || undefined,
+        productName:
+          formRow?.standardProductName ||
+          pp?.productName ||
+          it.productName ||
+          formRow?.productName ||
+          '',
+        aliasName: formRow?.aliasName || '',
+        brandName: brandLabel,
+        specification: it.specification || pp?.model || formRow?.model || '',
+        packageSpec: it.packageSpec || pp?.packageSpec || formRow?.packageSpec || '',
+        remark: (it.remark || pp?.remark || formRow?.remark || '').trim(),
+        unit: unitCode || it.unit,
+        unitLabel:
+          it.unitLabel ||
+          dictStore.getLabel('crm_product_unit', unitCode) ||
+          unitCode ||
+          ''
+      }
+    })
+  })
+
   const siteFeedbackTableRows = computed(() =>
     siteInventoryRows.value.map((row) => ({
       sourceRow: row,
@@ -1036,6 +1014,41 @@
 
   const displayFeedbackTime = computed(() => formatDateTime(transport.value?.feedbackTime) || '—')
 
+  const feedbackBlocksBind = computed(() => ({
+    feedbackEditable: props.feedbackEditable,
+    loading: loading.value,
+    canEditDriverFeedback: canEditDriverFeedback.value,
+    hasDriverFeedback: hasDriverFeedback.value,
+    arrangeDriverUrl: props.arrangeDriverUrl,
+    driverFeedbackTableRows: driverFeedbackTableRows.value,
+    displayFeedbackTime: displayFeedbackTime.value,
+    feedbackRemark: transport.value?.feedbackRemark,
+    feedbackImageCsv: feedbackImageCsv.value,
+    ioOrder: ioOrder.value,
+    ioOrderDisplayItems: ioOrderDisplayItems.value,
+    planProducts: products.value,
+    planProductRows: productRows.value,
+    ioOrderHasFeedback: ioOrderHasFeedback.value,
+    ioOrderViewFields: ioOrderViewFields.value,
+    ioOrderImageCsv: ioOrderImageCsv.value,
+    siteFeedbackTableRows: siteFeedbackTableRows.value,
+    dibangPlanRows: dibangPlanRows.value,
+    dibangBasicViewFields
+  }))
+
+  const feedbackBlocksOn = {
+    'edit-driver': openDriverFeedbackDialog,
+    'copy-driver-url': copyArrangeDriverUrl,
+    'edit-io-order': openIoOrderEditDialog,
+    'copy-site-url': (url?: string) => {
+      void copyArrangeSiteUrl(url)
+    },
+    'edit-site-feedback': openSiteFeedbackDialog,
+    'copy-site-summary': (row: SiteFeedbackTableRow) => {
+      void copySiteSummaryForRow(row.sourceRow)
+    }
+  }
+
   const displayCarrierName = computed(() => {
     if (transport.value?.carrierName) return transport.value.carrierName
     const vehicle = vehicleBundle.value?.detail
@@ -1083,22 +1096,6 @@
     return getPlHasInvoiceLabelFromSettlement(freightSettlement.value)
   })
 
-  function mergeTaxRatesFromContract(
-    rows: PlanFormProductRow[],
-    contract: ContractVO | null
-  ): PlanFormProductRow[] {
-    if (!contract?.products?.length) return rows
-    const rateByProductId = new Map<string, number>()
-    for (const item of contract.products) {
-      const key = String(item.productId ?? item.id ?? '')
-      if (key && item.taxRate != null) rateByProductId.set(key, Number(item.taxRate))
-    }
-    return rows.map((row) => ({
-      ...row,
-      taxRate: row.taxRate ?? rateByProductId.get(row.productId)
-    }))
-  }
-
   const overviewBasicViewFields = computed<ViewField[]>(() => [
     { label: '计划编号', value: plan.value?.planNo },
     { label: '计划日期', value: plan.value?.planDate },
@@ -1211,11 +1208,6 @@
     mapContractProducts(contractSnapshot.value?.products)
   )
 
-  const productRows = computed<PlanFormProductRow[]>(() => {
-    const rows = products.value.map((item, index) => mapPlanProductToFormRow(item, index))
-    return mergeTaxRatesFromContract(rows, contractSnapshot.value)
-  })
-
   const hazardProducts = computed(() => products.value.filter((item) => isHazardProduct(item)))
 
   async function handleViewContract() {
@@ -1292,6 +1284,11 @@
     }
   }
 
+  /* 回传区块有外包层,无法命中 section + section,补与上方运费表格的间距 */
+  .pl-plan-form-section + :deep(.pl-plan-transport-feedback-blocks) {
+    margin-top: 12px;
+  }
+
   .pl-plan-form-section-head {
     display: flex;
     align-items: center;
@@ -1637,4 +1634,148 @@
   .pl-plan-dibang-pending {
     margin: 8px 0 0;
   }
+
+  .pl-io-order-edit-form {
+    width: 100%;
+  }
+
+  .pl-io-order-edit-products {
+    width: 100%;
+
+    :deep(.el-form-item__content) {
+      display: block;
+      width: 100%;
+    }
+  }
+
+  .pl-io-order-edit-products__list {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    width: 100%;
+  }
+
+  .pl-io-order-edit-item {
+    width: 100%;
+    padding: 12px;
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    background: #f8fafc;
+  }
+
+  .pl-io-order-edit-item__head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 8px;
+    margin-bottom: 10px;
+  }
+
+  .pl-io-order-edit-item__title {
+    display: flex;
+    align-items: baseline;
+    gap: 8px;
+    min-width: 0;
+  }
+
+  .pl-io-order-edit-item__name {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1e293b;
+  }
+
+  .pl-io-order-edit-item__no {
+    font-size: 12px;
+    color: #64748b;
+  }
+
+  .pl-io-order-edit-item__badge {
+    font-size: 12px;
+    color: #64748b;
+    flex-shrink: 0;
+  }
+
+  .pl-io-order-edit-item__meta {
+    display: grid;
+    grid-template-columns: repeat(5, minmax(0, 1fr));
+    gap: 8px 12px;
+    margin-bottom: 12px;
+    padding: 10px 12px;
+    border-radius: 8px;
+    background: #fff;
+    border: 1px solid #e2e8f0;
+  }
+
+  .pl-io-order-edit-item__meta-item {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    min-width: 0;
+  }
+
+  .pl-io-order-edit-item__meta-label {
+    font-size: 11px;
+    color: #94a3b8;
+    line-height: 1.2;
+  }
+
+  .pl-io-order-edit-item__meta-value {
+    font-size: 13px;
+    color: #334155;
+    line-height: 1.4;
+    word-break: break-all;
+  }
+
+  .pl-io-order-edit-item__qty-row {
+    display: grid;
+    grid-template-columns: minmax(140px, 1fr) minmax(240px, 2fr);
+    gap: 12px;
+    align-items: center;
+  }
+
+  .pl-io-order-edit-item__qty {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    min-width: 0;
+  }
+
+  .pl-io-order-edit-item__qty--actual {
+    display: grid;
+    grid-template-columns: 72px 1fr auto;
+    gap: 8px;
+    align-items: center;
+  }
+
+  .pl-io-order-edit-item__qty-label {
+    font-size: 13px;
+    color: #64748b;
+    white-space: nowrap;
+  }
+
+  .pl-io-order-edit-item__qty-value {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1e293b;
+  }
+
+  .pl-io-order-edit-item__qty-input {
+    width: 100%;
+  }
+
+  .pl-io-order-edit-item__unit {
+    font-size: 12px;
+    font-weight: 600;
+    color: #64748b;
+  }
+
+  @media (max-width: 900px) {
+    .pl-io-order-edit-item__meta {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+
+    .pl-io-order-edit-item__qty-row {
+      grid-template-columns: 1fr;
+    }
+  }
 </style>