From 9840e565b021ca4ed199ea40d2c8b47bc9feeb3f Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 03 九月 2026 14:50:49 +0800
Subject: [PATCH] fix: 金额优化

---
 src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue |  733 +++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 488 insertions(+), 245 deletions(-)

diff --git a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
index 9e1a852..ca25853 100644
--- a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
+++ b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -1,298 +1,541 @@
 <template>
   <div>
-    <el-dialog v-model="dialogVisible" title="棰嗘枡鍙拌处" width="1200px" @close="handleClose">
+    <el-dialog v-model="dialogVisible"
+               title="棰嗘枡鍙拌处"
+               width="1450px"
+               @close="handleClose">
       <div class="material-toolbar">
-        <el-button type="primary" @click="handleAddMaterialRow">鏂板</el-button>
+        <el-button type="primary"
+                   @click="handleAddMaterialRow">鏂板</el-button>
       </div>
-      <el-table v-loading="materialTableLoading" :data="materialTableData" border row-key="tempId">
-        <el-table-column label="宸ュ簭鍚嶇О" min-width="180">
+      <el-table v-loading="materialTableLoading"
+                :data="materialTableData"
+                border
+                row-key="tempId">
+        <el-table-column label="宸ュ簭鍚嶇О"
+                         min-width="100">
           <template #default="{ row }">
-            <span v-if="row.bom === true">{{ row.processName || "-" }}</span>
-            <el-select
-              v-else
-              v-model="row.processName"
-              placeholder="璇烽�夋嫨宸ュ簭"
-              clearable
-              filterable
-              style="width: 100%;"
-              @change="val => handleProcessNameChange(row, val)"
-            >
-              <el-option v-for="item in processOptions" :key="item.id" :label="item.name" :value="item.name" />
+            <span v-if="row.bom === true">{{ row.operationName || "-" }}</span>
+            <el-select v-else
+                       v-model="row.operationName"
+                       placeholder="璇烽�夋嫨宸ュ簭"
+                       clearable
+                       filterable
+                       style="width: 100%;"
+                       @change="val => handleProcessNameChange(row, val)">
+              <el-option v-for="item in processOptions"
+                         :key="item.technologyOperationId"
+                         :label="item.name"
+                         :value="item.name" />
             </el-select>
           </template>
         </el-table-column>
-        <el-table-column label="鍘熸枡鍚嶇О" min-width="160">
+        <el-table-column label="鍘熸枡鍚嶇О"
+                         min-width="120">
           <template #default="{ row }">
             <span v-if="row.bom === true">{{ row.materialName || "-" }}</span>
-            <el-button v-else type="primary" link @click="openMaterialProductSelect(row)">
+            <el-button v-else
+                       type="primary"
+                       link
+                       @click="openMaterialProductSelect(row)">
               {{ row.materialName || "閫夋嫨鍘熸枡" }}
             </el-button>
           </template>
         </el-table-column>
-        <el-table-column label="鍘熸枡鍨嬪彿" min-width="180">
+        <el-table-column label="鍘熸枡鍨嬪彿"
+                         min-width="110">
           <template #default="{ row }">
             {{ row.materialModel || "-" }}
           </template>
         </el-table-column>
-        <el-table-column label="闇�姹傛暟閲�" min-width="120">
+        <!-- 鎵瑰彿澶氶�� -->
+        <el-table-column min-width="180"
+                         label="鎵瑰彿">
           <template #default="{ row }">
-            <span v-if="row.bom === true">{{ row.requiredQty ?? "-" }}</span>
-            <el-input-number
-              v-else
-              v-model="row.requiredQty"
-              :min="0"
-              :precision="3"
-              :step="1"
-              controls-position="right"
-              style="width: 100%;"
-              @change="val => handleRequiredQtyChange(row, val)"
-            />
+            <el-select v-model="row.batchNo"
+                       multiple
+                       collapse-tags
+                       collapse-tags-indicator
+                       placeholder="璇烽�夋嫨鎵瑰彿"
+                       style="width: 100%;">
+              <el-option v-for="item in row.batchNoList"
+                         :key="item"
+                         :label="item"
+                         :value="item" />
+            </el-select>
           </template>
         </el-table-column>
-        <el-table-column label="璁¢噺鍗曚綅" width="120">
+        <el-table-column label="闇�姹傛暟閲�"
+                         min-width="95">
+          <template #default="{ row }">
+            <span v-if="row.bom === true">{{ stripTrailingZeros(row.demandedQuantity) ?? "-" }}</span>
+            <el-input-number v-else
+                             v-model="row.demandedQuantity"
+                             :min="0"
+                             :step="1"
+                             controls-position="right"
+                             :formatter="value => stripTrailingZeros(value)"
+                             :parser="value => parseFloat(value) || 0"
+                             style="width: 100%;"
+                             @change="val => handleRequiredQtyChange(row, val)" />
+          </template>
+        </el-table-column>
+        <el-table-column label="搴撳瓨浣欓噺"
+                         min-width="90">
+          <template #default="{ row }">
+            <span v-if="row.stockQty === null">-</span>
+            <span v-else>{{ stripTrailingZeros(row.stockQty) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="宸查鏁伴噺"
+                         min-width="90">
+          <template #default="{ row }">
+            {{ stripTrailingZeros(row.pickedQty) ?? 0 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="鍓╀綑鍙"
+                         min-width="90">
+          <template #default="{ row }">
+            <span :class="{ 'remaining-empty': Number(row.remainingQty) <= 0 }">
+              {{ stripTrailingZeros(row.remainingQty) ?? 0 }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="璁¢噺鍗曚綅"
+                         width="80">
           <template #default="{ row }">
             {{ row.unit || "-" }}
           </template>
         </el-table-column>
-        <el-table-column label="棰嗙敤鏁伴噺" min-width="120">
+        <el-table-column label="棰嗙敤鏁伴噺"
+                         min-width="120">
           <template #default="{ row }">
-            <el-input-number
-              v-model="row.pickQty"
-              :min="0"
-              :precision="3"
-              :step="1"
-              controls-position="right"
-              style="width: 100%;"
-            />
+            <el-input-number v-model="row.pickQty"
+                             :min="0"
+                             :step="1"
+                             controls-position="right"
+                             :formatter="value => stripTrailingZeros(value)"
+                             :parser="value => parseFloat(value) || 0"
+                             style="width: 100%;" />
           </template>
         </el-table-column>
-        <el-table-column label="鎿嶄綔" width="90" fixed="right">
+        <el-table-column label="鎿嶄綔"
+                         width="90"
+                         fixed="right">
           <template #default="{ $index, row }">
-            <el-button v-if="row.bom !== true" type="danger" link @click="handleDeleteMaterialRow($index)">鍒犻櫎</el-button>
+            <el-button v-if="row.bom !== true || Number(row.pickQty) > 0"
+                       type="danger"
+                       link
+                       @click="handleDeleteMaterialRow($index)">鍒犻櫎</el-button>
           </template>
         </el-table-column>
       </el-table>
       <template #footer>
         <span class="dialog-footer">
-          <el-button type="primary" :loading="materialSaving" @click="handleMaterialSave">淇濆瓨</el-button>
+          <el-button type="primary"
+                     :loading="materialSaving"
+                     :disabled="isSaveDisabled"
+                     @click="handleMaterialSave">淇濆瓨</el-button>
           <el-button @click="dialogVisible = false">鍙栨秷</el-button>
         </span>
       </template>
     </el-dialog>
-
-    <ProductSelectDialog
-      v-model="materialProductDialogVisible"
-      @confirm="handleMaterialProductConfirm"
-      single
-      request-url="/stockInventory/rawMaterials"
-    />
+    <ProductSelectDialog v-model="materialProductDialogVisible"
+                         @confirm="handleMaterialProductConfirm"
+                         single />
+    <!-- request-url="/stockInventory/rawMaterials" -->
   </div>
 </template>
 
 <script setup>
-import { computed, ref, watch } from "vue";
-import { ElMessage } from "element-plus";
-import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
-import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js";
-import {
-  listMaterialPickingDetail,
-  listMaterialPickingLedger,
-  saveMaterialPickingLedger,
-} from "@/api/productionManagement/productionOrder.js";
+  import { computed, ref, watch } from "vue";
+  import { ElMessage } from "element-plus";
+  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+  import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js";
+  import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
+  import {
+    listMaterialPickingDetail,
+    listMaterialPickingBom,
+    saveMaterialPickingLedger,
+  } from "@/api/productionManagement/productionOrder.js";
 
-const props = defineProps({
-  modelValue: { type: Boolean, default: false },
-  orderRow: { type: Object, default: null },
-});
-const emit = defineEmits(["update:modelValue", "saved"]);
-
-const dialogVisible = computed({
-  get: () => props.modelValue,
-  set: val => emit("update:modelValue", val),
-});
-
-const materialProductDialogVisible = ref(false);
-const materialTableLoading = ref(false);
-const materialSaving = ref(false);
-const materialTableData = ref([]);
-const processOptions = ref([]);
-const currentMaterialSelectRowIndex = ref(-1);
-let materialTempId = 0;
-
-const createMaterialRow = (row = {}) => ({
-  tempId: row.id || `temp_${++materialTempId}`,
-  id: row.id,
-  processId: row.processId,
-  productProcessId: row.productProcessId || row.processId,
-  processName: row.processName || "",
-  bom: row.bom === true,
-  materialModelId: row.materialModelId,
-  materialName: row.materialName || "",
-  materialModel: row.materialModel || "",
-  requiredQty: Number(row.requiredQty ?? 0),
-  unit: row.unit || "",
-  pickQty: Number(row.pickQty ?? row.requiredQty ?? 0),
-});
-
-const getProcessOptions = async () => {
-  if (!props.orderRow?.id) return;
-  const res = await findProductProcessRouteItemList({ orderId: props.orderRow.id });
-  const routeList = Array.isArray(res?.data) ? res.data : res?.data?.records || [];
-  const processMap = new Map();
-  routeList.forEach(item => {
-    const processId = item.processId;
-    const processName = item.processName;
-    if (!processId || !processName) return;
-    const key = `${processId}_${processName}`;
-    if (!processMap.has(key)) {
-      processMap.set(key, {
-        id: processId,
-        name: processName,
-      });
-    }
+  const props = defineProps({
+    modelValue: { type: Boolean, default: false },
+    orderRow: { type: Object, default: null },
   });
-  processOptions.value = Array.from(processMap.values());
-};
+  const emit = defineEmits(["update:modelValue", "saved"]);
 
-const loadMaterialData = async () => {
-  if (!props.orderRow?.id) return;
-  materialTableLoading.value = true;
-  materialTableData.value = [];
-  await getProcessOptions();
-  try {
-    const detailRes = await listMaterialPickingDetail({ orderId: props.orderRow.id });
-    const detailList = Array.isArray(detailRes?.data)
-      ? detailRes.data
-      : detailRes?.data?.records || [];
-    if (detailList.length > 0) {
-      materialTableData.value = detailList.map(item => createMaterialRow(item));
+  const dialogVisible = computed({
+    get: () => props.modelValue,
+    set: val => emit("update:modelValue", val),
+  });
+
+  const materialProductDialogVisible = ref(false);
+  const materialTableLoading = ref(false);
+  const materialSaving = ref(false);
+  const materialTableData = ref([]);
+
+  const isSaveDisabled = computed(() => {
+    if (materialTableLoading.value) return true;
+    if (materialTableData.value.length === 0) return true;
+    // 鍙湁瀛樺湪鏈棰嗙敤鏁伴噺澶т簬0鐨勮鎵嶅厑璁镐繚瀛�
+    return !materialTableData.value.some(item => Number(item.pickQty) > 0);
+  });
+
+  const processOptions = ref([]);
+  const currentMaterialSelectRowIndex = ref(-1);
+  let materialTempId = 0;
+
+  const toNumber = val => {
+    const n = Number(val);
+    return Number.isFinite(n) ? n : 0;
+  };
+
+  // 淇濈暀6浣嶅皬鏁帮紝閬垮厤娴偣璇樊
+  const roundQuantity = val => Math.round(toNumber(val) * 1e6) / 1e6;
+
+  // 闇�姹傛暟閲忓垎缁勯敭锛氫笌鍚庣淇濇寔涓�鑷达紙宸ュ簭ID浼樺厛锛岀己澶辨椂鐢ㄥ伐搴忓悕绉帮級
+  const buildDemandKey = (technologyOperationId, operationName, productModelId) =>
+    `${technologyOperationId != null && technologyOperationId !== undefined ? "T" + technologyOperationId : "N" + (operationName || "")}#${productModelId ?? ""}`;
+
+  const stripTrailingZeros = val => {
+    if (val === null || val === undefined || val === "") return "0";
+    const str = String(val);
+    if (str.includes(".")) {
+      return parseFloat(str).toString();
+    }
+    return str;
+  };
+
+  const createMaterialRow = (row = {}) => {
+    const batchNoList = Array.isArray(row.batchNoList) ? row.batchNoList : [];
+    const pickedEntry = row.pickedEntry || null;
+    const pickedQty = pickedEntry ? roundQuantity(pickedEntry.pickedQty) : 0;
+    const returnedQty = pickedEntry ? roundQuantity(pickedEntry.returnedQty) : 0;
+    const demandedQuantity = roundQuantity(row.demandedQuantity ?? 0);
+    const remainingQty = Math.max(roundQuantity(demandedQuantity - pickedQty + returnedQty), 0);
+
+    return {
+      tempId: `temp_${++materialTempId}`,
+      id: null,
+      bom: row.bom === true,
+      technologyOperationId: row.technologyOperationId ?? null,
+      operationName: row.operationName || "",
+      materialModelId: row.materialModelId ?? null,
+      materialName: row.materialName || "",
+      materialModel: row.materialModel || "",
+      unit: row.unit || "",
+      demandedQuantity,
+      pickedQty,
+      returnedQty,
+      remainingQty,
+      pickQty: 0,
+      stockQty: null,
+      batchNo: [],
+      batchNoList,
+    };
+  };
+
+  const getProcessOptions = async () => {
+    if (!props.orderRow?.id) return;
+    const res = await findProductProcessRouteItemList({
+      orderId: props.orderRow.id,
+    });
+    const routeList = Array.isArray(res?.data)
+      ? res.data
+      : res?.data?.records || [];
+    const processMap = new Map();
+    routeList.forEach(item => {
+      const processId = item.technologyOperationId;
+      const operationName = item.operationName;
+      if (!processId || !operationName) return;
+      const key = `${processId}_${operationName}`;
+      if (!processMap.has(key)) {
+        processMap.set(key, {
+          id: processId,
+          technologyOperationId: processId,
+          name: operationName,
+        });
+      }
+    });
+    processOptions.value = Array.from(processMap.values());
+  };
+
+  const loadMaterialData = async () => {
+    if (!props.orderRow?.id) return;
+    materialTableLoading.value = true;
+    materialTableData.value = [];
+    try {
+      await getProcessOptions();
+      // 鍚屾椂鑾峰彇BOM闇�姹傛暟鎹笌宸查鏂欐槑缁嗭紝鐢ㄤ簬璁$畻鈥滃凡棰嗘暟閲�/鍓╀綑鍙鈥�
+      const [bomRes, detailRes] = await Promise.all([
+        listMaterialPickingBom(props.orderRow.id),
+        listMaterialPickingDetail(props.orderRow.id),
+      ]);
+      const bomList = Array.isArray(bomRes?.data)
+        ? bomRes.data
+        : bomRes?.data?.records || [];
+      const detailList = Array.isArray(detailRes?.data)
+        ? detailRes.data
+        : detailRes?.data?.records || [];
+
+      // 鎸夆�滃伐搴�+鍘熸枡鈥濇眹鎬诲巻鍙查鏂欙細宸查鏁伴噺銆侀��鏂欐暟閲�
+      const pickedMap = new Map();
+      detailList.forEach(item => {
+        if (!item) return;
+        const key = buildDemandKey(
+          item.technologyOperationId,
+          item.operationName,
+          item.productModelId
+        );
+        const entry = pickedMap.get(key) || {
+          pickedQty: 0,
+          returnedQty: 0,
+          manualDemand: 0,
+          technologyOperationId: item.technologyOperationId,
+          operationName: item.operationName || "",
+          materialModelId: item.productModelId,
+          materialName: item.productName || "",
+          materialModel: item.model || "",
+          unit: item.unit || "",
+          batchNoList: Array.isArray(item.batchNoList)
+            ? item.batchNoList
+            : [],
+        };
+        entry.pickedQty = roundQuantity(entry.pickedQty + toNumber(item.pickQuantity));
+        entry.returnedQty = roundQuantity(entry.returnedQty + toNumber(item.returnQty));
+        // 鎵嬪伐鏂板琛岋紙闈濨OM锛夌殑闇�姹傛暟閲忎互棰嗘枡蹇収涓哄噯
+        if (item.bom !== true && item.demandedQuantity != null) {
+          entry.manualDemand = Math.max(
+            toNumber(entry.manualDemand),
+            toNumber(item.demandedQuantity)
+          );
+        }
+        pickedMap.set(key, entry);
+      });
+
+      const rows = [];
+      const bomKeySet = new Set();
+      // BOM琛岋細闇�姹傛暟閲忓彇BOM锛屽凡棰嗘暟閲忓彇鍘嗗彶棰嗘枡姹囨��
+      bomList.forEach(item => {
+        const materialModelId = item.productModelId ?? item.materialModelId;
+        const key = buildDemandKey(
+          item.technologyOperationId,
+          item.operationName,
+          materialModelId
+        );
+        bomKeySet.add(key);
+        rows.push(
+          createMaterialRow({
+            bom: true,
+            technologyOperationId: item.technologyOperationId,
+            operationName: item.operationName || "",
+            materialModelId,
+            materialName: item.productName || item.materialName || "",
+            materialModel: item.model || item.materialModel || "",
+            unit: item.unit || "",
+            demandedQuantity: item.demandedQuantity ?? item.requiredQty ?? 0,
+            pickedEntry: pickedMap.get(key),
+            batchNoList: item.batchNoList || [],
+          })
+        );
+      });
+      // 涓嶅湪BOM涓殑鍘嗗彶棰嗘枡锛堟墜宸ユ柊澧炶锛夛紝涔熷睍绀哄嚭鏉ユ敮鎸佺户缁鏂�
+      pickedMap.forEach((entry, key) => {
+        if (bomKeySet.has(key)) return;
+        rows.push(
+          createMaterialRow({
+            bom: false,
+            technologyOperationId: entry.technologyOperationId,
+            operationName: entry.operationName,
+            materialModelId: entry.materialModelId,
+            materialName: entry.materialName,
+            materialModel: entry.materialModel,
+            unit: entry.unit,
+            demandedQuantity: entry.manualDemand,
+            pickedEntry: entry,
+            batchNoList: entry.batchNoList,
+          })
+        );
+      });
+      // 鍏堟煡鍑哄簱瀛樹綑閲忓啀鍏ヨ〃锛屼繚璇侀娆℃墦寮�鍗冲睍绀猴紙rows 灏氭湭杞负鍝嶅簲寮忎唬鐞嗭紝
+      // 鍏ヨ〃鍚庡啀鏀瑰師濮嬪璞′笉浼氳Е鍙戣鍥炬洿鏂帮級
+      await loadStockQtyForRows(rows);
+      materialTableData.value = rows;
+    } finally {
+      materialTableLoading.value = false;
+    }
+  };
+
+  watch(
+    () => dialogVisible.value,
+    visible => {
+      if (visible) {
+        loadMaterialData();
+      }
+    }
+  );
+
+  const handleClose = () => {
+    materialTableData.value = [];
+    currentMaterialSelectRowIndex.value = -1;
+  };
+
+  // 鎸夊瀷鍙锋眹鎬绘墍鏈夋壒娆$殑鍘熸枡鍙敤搴撳瓨浣欓噺
+  const loadStockQtyForRows = async rows => {
+    const modelIds = [
+      ...new Set(rows.map(row => row.materialModelId).filter(Boolean)),
+    ];
+    const stockMap = new Map();
+    await Promise.all(
+      modelIds.map(async modelId => {
+        try {
+          const res = await getStockInventoryByModelId(modelId);
+          const list = Array.isArray(res?.data)
+            ? res.data
+            : res?.data?.records || res?.data?.rows || [];
+          const total = list.reduce(
+            (sum, item) => sum + toNumber(item?.qualitity ?? item?.quantity),
+            0
+          );
+          stockMap.set(modelId, roundQuantity(total));
+        } catch (e) {
+          stockMap.set(modelId, 0);
+        }
+      })
+    );
+    rows.forEach(row => {
+      if (!row.materialModelId) {
+        row.stockQty = null;
+        return;
+      }
+      row.stockQty = stockMap.has(row.materialModelId)
+        ? stockMap.get(row.materialModelId)
+        : 0;
+    });
+  };
+
+  const handleAddMaterialRow = () => {
+    materialTableData.value.push(createMaterialRow());
+  };
+
+  const handleDeleteMaterialRow = index => {
+    materialTableData.value.splice(index, 1);
+  };
+
+  const handleProcessNameChange = (row, operationName) => {
+    const process = processOptions.value.find(
+      item => item.name === operationName
+    );
+    row.technologyOperationId = process?.technologyOperationId;
+  };
+
+  const handleRequiredQtyChange = (row, val) => {
+    const required = roundQuantity(val);
+    row.demandedQuantity = required;
+    row.remainingQty = Math.max(
+      roundQuantity(required - toNumber(row.pickedQty) + toNumber(row.returnedQty)),
+      0
+    );
+  };
+
+  const openMaterialProductSelect = row => {
+    currentMaterialSelectRowIndex.value = materialTableData.value.findIndex(
+      item => item.tempId === row.tempId
+    );
+    materialProductDialogVisible.value = true;
+  };
+
+  const handleMaterialProductConfirm = products => {
+    if (!products || products.length === 0) return;
+    const index = currentMaterialSelectRowIndex.value;
+    if (index < 0 || !materialTableData.value[index]) return;
+    const product = products[0];
+    const row = materialTableData.value[index];
+    row.materialModelId =
+      product.materialModelId || product.modelId || product.id;
+    row.materialName =
+      product.materialName || product.productName || product.name || "";
+    row.materialModel = product.materialModel || product.model || "";
+    row.unit = product.unit || product.measureUnit || "";
+    row.batchNoList = Array.isArray(product.batchNoList) ? product.batchNoList : [];
+    row.batchNo = [];
+    currentMaterialSelectRowIndex.value = -1;
+    materialProductDialogVisible.value = false;
+    loadStockQtyForRows([row]);
+  };
+
+  const validateMaterialRows = () => {
+    if (materialTableData.value.length === 0) {
+      return { valid: false, message: "璇峰厛鏂板棰嗘枡鏁版嵁" };
+    }
+    const pickRows = materialTableData.value.filter(
+      item => toNumber(item.pickQty) > 0
+    );
+    if (pickRows.length === 0) {
+      return { valid: false, message: "璇疯嚦灏戝~鍐欎竴琛屽ぇ浜�0鐨勯鐢ㄦ暟閲�" };
+    }
+    for (let i = 0; i < materialTableData.value.length; i++) {
+      const item = materialTableData.value[i];
+      const rowNo = i + 1;
+      if (toNumber(item.pickQty) <= 0) continue;
+      if (item.bom !== true && (!item.operationName || !item.materialName)) {
+        return { valid: false, message: `绗�${rowNo}琛岀殑宸ュ簭鍚嶇О鍜屽師鏂欏悕绉颁负蹇呭~椤筦 };
+      }
+      if (!Array.isArray(item.batchNo) || item.batchNo.length === 0) {
+        return { valid: false, message: `绗�${rowNo}琛岃閫夋嫨鎵瑰彿` };
+      }
+    }
+    return { valid: true, message: "" };
+  };
+
+  const handleMaterialSave = async () => {
+    if (!props.orderRow?.id) return;
+    const validateResult = validateMaterialRows();
+    if (!validateResult.valid) {
+      ElMessage.warning(validateResult.message);
       return;
     }
-    const ledgerRes = await listMaterialPickingLedger({ orderId: props.orderRow.id });
-    const ledgerList = Array.isArray(ledgerRes?.data)
-      ? ledgerRes.data
-      : ledgerRes?.data?.records || [];
-    materialTableData.value = ledgerList.map(item => createMaterialRow(item));
-  } finally {
-    materialTableLoading.value = false;
-  }
-};
+    materialSaving.value = true;
+    try {
+      // 姣忔淇濆瓨閮芥槸涓�娆$嫭绔嬬殑棰嗘枡锛氬彧鎻愪氦鏈棰嗙敤鏁伴噺澶т簬0鐨勮锛�
+      // 鍑哄簱鍙拌处鎸夋湰娆¢鏂欐暟閲忚璐︼紝鍘嗗彶棰嗘枡璁板綍淇濇寔涓嶅彉銆�
+      const pickRows = materialTableData.value.filter(
+        item => toNumber(item.pickQty) > 0
+      );
+      await saveMaterialPickingLedger({
+        productionOrderId: props.orderRow.id,
+        productionOrderPickDto: pickRows.map(item => ({
+          technologyOperationId: item.technologyOperationId,
+          operationName: item.operationName,
+          bom: item.bom === true,
+          productModelId: item.materialModelId,
+          demandedQuantity: item.demandedQuantity,
+          unit: item.unit,
+          pickQuantity: toNumber(item.pickQty),
+          batchNo: Array.isArray(item.batchNo)
+            ? item.batchNo.join(",")
+            : item.batchNo,
+        })),
+      });
 
-watch(
-  () => dialogVisible.value,
-  visible => {
-    if (visible) {
-      loadMaterialData();
+      ElMessage({ message: "棰嗘枡鎴愬姛", type: "success" });
+      emit("saved");
+      dialogVisible.value = false;
+    } finally {
+      materialSaving.value = false;
     }
-  }
-);
-
-const handleClose = () => {
-  materialTableData.value = [];
-  currentMaterialSelectRowIndex.value = -1;
-};
-
-const handleAddMaterialRow = () => {
-  materialTableData.value.push(createMaterialRow());
-};
-
-const handleDeleteMaterialRow = index => {
-  materialTableData.value.splice(index, 1);
-};
-
-const handleProcessNameChange = (row, processName) => {
-  const process = processOptions.value.find(item => item.name === processName);
-  row.productProcessId = process?.id;
-};
-
-const handleRequiredQtyChange = (row, val) => {
-  const required = Number(val ?? 0);
-  row.requiredQty = required;
-  if (!row.pickQty || Number(row.pickQty) === 0) {
-    row.pickQty = required;
-  }
-};
-
-const openMaterialProductSelect = row => {
-  currentMaterialSelectRowIndex.value = materialTableData.value.findIndex(item => item.tempId === row.tempId);
-  materialProductDialogVisible.value = true;
-};
-
-const handleMaterialProductConfirm = products => {
-  if (!products || products.length === 0) return;
-  const index = currentMaterialSelectRowIndex.value;
-  if (index < 0 || !materialTableData.value[index]) return;
-  const product = products[0];
-  const row = materialTableData.value[index];
-  row.materialModelId = product.materialModelId || product.modelId || product.id;
-  row.materialName = product.materialName || product.productName || product.name || "";
-  row.materialModel = product.materialModel || product.model || "";
-  row.unit = product.unit || product.measureUnit || "";
-  currentMaterialSelectRowIndex.value = -1;
-  materialProductDialogVisible.value = false;
-};
-
-const validateMaterialRows = () => {
-  if (materialTableData.value.length === 0) {
-    return { valid: false, message: "璇峰厛鏂板棰嗘枡鏁版嵁" };
-  }
-  const invalidNewRow = materialTableData.value.find(
-    item => item.bom !== true && (!item.processName || !item.materialName)
-  );
-  if (invalidNewRow) {
-    return { valid: false, message: "鏂板琛岀殑宸ュ簭鍚嶇О鍜屽師鏂欏悕绉颁负蹇呭~椤�" };
-  }
-  const invalidRow = materialTableData.value.find(
-    item =>
-      !item.processName ||
-      !item.materialName ||
-      item.requiredQty === null ||
-      item.requiredQty === undefined ||
-      item.pickQty === null ||
-      item.pickQty === undefined
-  );
-  if (invalidRow) {
-    return { valid: false, message: "璇峰畬鍠勫伐搴忋�佸師鏂欏拰鏁伴噺鍚庡啀淇濆瓨" };
-  }
-  return { valid: true, message: "" };
-};
-
-const handleMaterialSave = async () => {
-  if (!props.orderRow?.id) return;
-  const validateResult = validateMaterialRows();
-  if (!validateResult.valid) {
-    ElMessage.warning(validateResult.message);
-    return;
-  }
-  materialSaving.value = true;
-  try {
-    await saveMaterialPickingLedger({
-      orderId: props.orderRow.id,
-      items: materialTableData.value.map(item => ({
-        id: item.id,
-        processId: item.processName,
-        productProcessId: item.productProcessId,
-        processName: item.processName,
-        bom: item.bom === true,
-        materialModelId: item.materialModelId,
-        materialName: item.materialName,
-        materialModel: item.materialModel,
-        requiredQty: item.requiredQty,
-        unit: item.unit,
-        pickQty: item.pickQty,
-      })),
-    });
-    emit("saved");
-    dialogVisible.value = false;
-  } finally {
-    materialSaving.value = false;
-  }
-};
+  };
 </script>
 
 <style scoped lang="scss">
-.material-toolbar {
-  margin-bottom: 12px;
-  text-align: right;
-}
+  .material-toolbar {
+    margin-bottom: 12px;
+    text-align: right;
+  }
+
+  .remaining-empty {
+    color: #f56c6c;
+    font-weight: 600;
+  }
 </style>

--
Gitblit v1.9.3