From 0c2901e13dcc61ebae45aa7433d71b43bbf56d25 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期三, 22 四月 2026 15:42:17 +0800
Subject: [PATCH] Merge branch 'dev_New' into dev_NEW_pro

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

diff --git a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
new file mode 100644
index 0000000..9e1a852
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -0,0 +1,298 @@
+<template>
+  <div>
+    <el-dialog v-model="dialogVisible" title="棰嗘枡鍙拌处" width="1200px" @close="handleClose">
+      <div class="material-toolbar">
+        <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">
+          <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" />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="鍘熸枡鍚嶇О" min-width="160">
+          <template #default="{ row }">
+            <span v-if="row.bom === true">{{ row.materialName || "-" }}</span>
+            <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">
+          <template #default="{ row }">
+            {{ row.materialModel || "-" }}
+          </template>
+        </el-table-column>
+        <el-table-column label="闇�姹傛暟閲�" min-width="120">
+          <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)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="璁¢噺鍗曚綅" width="120">
+          <template #default="{ row }">
+            {{ row.unit || "-" }}
+          </template>
+        </el-table-column>
+        <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%;"
+            />
+          </template>
+        </el-table-column>
+        <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>
+          </template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button type="primary" :loading="materialSaving" @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"
+    />
+  </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";
+
+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,
+      });
+    }
+  });
+  processOptions.value = Array.from(processMap.values());
+};
+
+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));
+      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;
+  }
+};
+
+watch(
+  () => dialogVisible.value,
+  visible => {
+    if (visible) {
+      loadMaterialData();
+    }
+  }
+);
+
+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;
+}
+</style>

--
Gitblit v1.9.3