fix: 领料功能修改,移除订单领料,添加工序领料功能,支持按工序查询物料与已领料记录
已修改10个文件
1903 ■■■■ 文件已修改
src/api/productionManagement/processRouteItem.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/ItemsForm.vue 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 391 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 484 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue 235 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue 474 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteItem.js
@@ -17,6 +17,15 @@
    data: data,
  });
}
// 全量保存工艺路线工序(按提交顺序确定工序顺序)
export function saveProcessRouteItems(data) {
  return request({
    url: "/technologyRoutingOperation/saveRouteOperations",
    method: "post",
    data: data,
  });
}
export function addOrUpdateProcessRouteItem1(data) {
  return request({
    url: "/technologyRoutingOperation",
src/api/productionManagement/productionOrder.js
@@ -62,12 +62,12 @@
  });
}
// 生产订单-查询产品结构列表
// 生产订单-查询订单BOM快照的扁平物料明细(物料规格 + 消耗工序)
export function listProcessBom(query) {
  const orderId = typeof query === "object" && query !== null ? query.orderId : query;
  return request({
    url: "/productOrder/listProcessBom",
    url: "/productionBomStructure/listByOrderId/" + orderId,
    method: "get",
    params: query,
  });
}
src/api/productionManagement/workOrder.js
@@ -42,51 +42,40 @@
  });
}
// 工单-当前工序物料台账
export function listWorkOrderMaterialLedger(query) {
// 工单-当前工序可领用物料 + 已领料记录
export function listWorkOrderMaterialLedger(productionOperationTaskId) {
  return request({
    url: "/productOrderMaterial/reportMaterials",
    url: "/productionOrderPick/taskMaterials/" + productionOperationTaskId,
    method: "get",
    params: query,
  });
}
// 工单-补料
export function addWorkOrderMaterialSupplement(data) {
  return request({
    url: "/productionOperationTask/material/supplement",
    method: "post",
    data,
  });
}
// 工单-退料
export function addWorkOrderMaterialReturn(data) {
  return request({
    url: "/productionOperationTask/material/return",
    method: "post",
    data,
  });
}
// 工单-补料记录
export function listWorkOrderMaterialSupplementRecord(query) {
  return request({
    url: "/productionOperationTask/material/supplementRecord",
    method: "get",
    params: query,
  });
}
// 工单-领用(提交实际领用数量)
// 工单-工序统一领料(立即扣减库存,可重复领料)
export function pickWorkOrderMaterial(data) {
  return request({
    url: "/productionOperationTask/material/pick",
    url: "/productionOrderPick/savePickByTask",
    method: "post",
    data,
  });
}
// 工单-退回当前工序已领物料
export function returnWorkOrderMaterial(data) {
  return request({
    url: "/productionOrderPick/updatePick",
    method: "post",
    data,
  });
}
// 工单-当前工序是否已有有效领料(报工前置校验,仅用于前端提示)
export function checkWorkOrderMaterialPicked(productionOperationTaskId) {
  return request({
    url: "/productionOrderPick/taskPicked/" + productionOperationTaskId,
    method: "get",
  });
}
// 获取工序统计数据
export function getOperationStatistics(query) {
  return request({
src/views/productionManagement/processRoute/ItemsForm.vue
@@ -7,15 +7,7 @@
        @close="closeModal"
    >
      <div class="operate-button">
        <el-button
            type="primary"
            @click="isShowProductSelectDialog = true"
            class="mb5"
            style="margin-bottom: 10px;"
        >
          选择产品
        </el-button>
        <span class="route-tip">后端根据BOM生成工艺路线,可在此调整工序和顺序</span>
        <el-switch
            v-model="isTable"
            inline-prompt
@@ -68,6 +60,7 @@
              <el-select
                  v-model="scope.row[item.prop]"
                  style="width: 100%;"
                  @change="value => handleProcessChange(scope.row, value)"
                  @mousedown.stop
              >
                <el-option
@@ -102,16 +95,17 @@
          <div class="step-content">
            <div class="step-number">{{ index + 1 }}</div>
            <el-card
                :header="item.productName"
                :header="getProcessName(item)"
                class="step-card"
                style="cursor: move;"
            >
              <div class="step-card-content">
                <p>{{ item.model }}</p>
                <p>{{ item.unit }}</p>
                <p>{{ item.productName || '-' }}</p>
                <p>{{ item.model || '-' }}</p>
                <el-select
                    v-model="item.processId"
                    style="width: 100%;"
                    @change="value => handleProcessChange(item, value)"
                    @mousedown.stop
                >
                  <el-option
@@ -139,18 +133,12 @@
        </div>
      </template>
    </el-dialog>
    <ProductSelectDialog
        v-model="isShowProductSelectDialog"
        @confirm="handelSelectProducts"
    />
  </div>
</template>
<script setup>
import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
import { findProcessRouteItemList, addOrUpdateProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
import { findProcessRouteItemList, saveProcessRouteItems } from "@/api/productionManagement/processRouteItem.js";
import { processList } from "@/api/productionManagement/productionProcess.js";
import Sortable from 'sortablejs';
@@ -171,7 +159,6 @@
const processOptions = ref([]);
const tableLoading = ref(false);
const isShowProductSelectDialog = ref(false);
const routeItems = ref([]);
let tableSortable = null;
let stepsSortable = null;
@@ -191,8 +178,7 @@
const tableColumn = ref([
  { label: "产品名称", prop: "productName", width: 180 },
  { label: "规格名称", prop: "model", width: 150 },
  { label: "单位", prop: "unit", width: 80 },
  { label: "工序名称", prop: "processId", width: 180 },
  { label: "工序名称", prop: "processId", width: 220 },
  {
    dataType: "action",
    label: "操作",
@@ -215,6 +201,21 @@
  }
]);
const getProcessName = item => {
  if (item.processName || item.operationName) {
    return item.processName || item.operationName;
  }
  const process = processOptions.value.find(process => process.id === item.processId);
  return process?.name || "请选择工序";
};
const handleProcessChange = (row, value) => {
  const process = processOptions.value.find(process => process.id === value);
  row.processName = process?.name || "";
  row.operationName = row.processName;
  row.operationId = value;
};
const removeItem = (index) => {
  routeItems.value.splice(index, 1);
  nextTick(() => initSortable());
@@ -232,46 +233,18 @@
  isShow.value = false;
};
const handelSelectProducts = (products) => {
  destroySortable();
  const newData = products.map(({ id, ...product }) => ({
    ...product,
    productModelId: id,
    routeId: props.record.id,
    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
    processId: undefined
  }));
  console.log('选择产品前数组:', routeItems.value);
  routeItems.value.push(...newData);
  routeItems.value = [...routeItems.value];
  console.log('选择产品后数组:', routeItems.value);
  // 延迟初始化,确保DOM完全渲染
  nextTick(() => {
    // 强制重新渲染组件
    if (proxy?.$forceUpdate) {
      proxy.$forceUpdate();
    }
    const temp = [...routeItems.value];
    routeItems.value = [];
    nextTick(() => {
      routeItems.value = temp;
      initSortable();
    });
  });
};
const findProcessRouteItems = () => {
  tableLoading.value = true;
  findProcessRouteItemList({ routeId: props.record.id })
      .then(res => {
        tableLoading.value = false;
        routeItems.value = res.data.map(item => ({
        // 后端返回 technologyOperationId / technologyOperationName,页面统一用 processId / processName
        routeItems.value = (res.data || []).map(item => ({
          ...item,
          processId: item.processId === 0 ? undefined : item.processId
          productName: item.productName || "",
          model: item.model || "",
          processName: item.technologyOperationName || item.processName || "",
          processId: item.technologyOperationId || (item.processId === 0 ? undefined : item.processId)
        }));
        // 延迟初始化,确保DOM完全渲染
        nextTick(() => {
@@ -302,10 +275,20 @@
    proxy?.$modal?.msgError("请为所有项目选择工序");
    return;
  }
  const processIds = routeItems.value.map(item => item.processId);
  if (new Set(processIds).size !== processIds.length) {
    proxy?.$modal?.msgError("同一道工序不能重复出现在工艺路线中");
    return;
  }
  addOrUpdateProcessRouteItem({
    routeId: props.record.id,
    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
  // 全量提交:工序顺序即当前列表顺序,未回传的工序会被后端删除
  saveProcessRouteItems({
    technologyRoutingId: props.record.id,
    operationList: routeItems.value.map((item, index) => ({
      id: item.id,
      technologyOperationId: item.processId,
      dragSort: index + 1
    }))
  })
      .then(res => {
        isShow.value = false;
@@ -349,7 +332,6 @@
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
        console.log('排序后数组:', routeItems.value);
      }
    });
  } else {
@@ -385,10 +367,6 @@
        routeItems.value = [...routeItems.value];
      }
    });
    // 调试:打印容器和实例,确认绑定成功
    console.log('步骤条拖拽容器:', stepsList);
    console.log('Sortable实例:', stepsSortable);
  }
};
@@ -438,6 +416,12 @@
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.route-tip {
  color: #606266;
  font-size: 13px;
}
/* 修改:自定义步骤条容器样式 */
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -265,6 +265,12 @@
          编辑
        </el-button>
        <el-button v-if="bomDataValue.isEdit"
                   type="primary"
                   plain
                   @click="addBomItem()">
          添加物料
        </el-button>
        <el-button v-if="bomDataValue.isEdit"
                   @click="cancelEditBom">
          取消
        </el-button>
@@ -287,8 +293,6 @@
                   :model="bomDataValue">
            <el-table :data="bomDataValue.dataList"
                      row-key="tempId"
                      default-expand-all
                      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
                      style="width: 100%">
              <el-table-column prop="productName"
                               label="产品" />
@@ -301,7 +305,7 @@
                    <el-select v-model="row.model"
                               placeholder="请选择规格"
                               clearable
                               :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)"
                               :disabled="!bomDataValue.isEdit"
                               style="width: 100%"
                               @visible-change="(v) => { if (v) openBomDialog(row.tempId) }">
                      <el-option v-if="row.model"
@@ -315,7 +319,7 @@
                               label="消耗工序">
                <template #default="{ row }">
                  <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
                                :rules="bomDataValue.dataList.some(item => (item).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                :rules="[{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
@@ -323,7 +327,7 @@
                               clearable
                               style="width: 100%"
                               @change="value => handleBomProcessChange(row, value)"
                               :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)">
                               :disabled="!bomDataValue.isEdit">
                      <el-option v-for="item in bomDataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
@@ -332,65 +336,14 @@
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unitQuantity"
                               label="单位产出所需数量">
                <template #default="{ row }">
                  <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
                                :rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.unitQuantity"
                                     :min="0"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     @change="handleUnitQuantityChange"
                                     :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column v-if="pageType === 'order'"
                               prop="demandedQuantity"
                               label="需求总量">
                <template #default="{ row }">
                  <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
                                :rules="[{ required: true, message: '请输入需求总量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.demandedQuantity"
                                     :min="0"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="true" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unit"
                               label="单位">
                <template #default="{ row }">
                  <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
                                :rules="[{ required: true, message: '请输入单位', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input v-model="row.unit"
                              placeholder="请输入单位"
                              clearable
                              :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column label="操作"
                               fixed="right"
                               width="200"
                               width="100"
                               v-if="pageType === 'order' && bomDataValue.isEdit">
                <template #default="{ row }">
                  <el-button v-if="bomDataValue.isEdit && !bomDataValue.dataList.some(item => (item).tempId === row.tempId)"
                             type="danger"
                  <el-button type="danger"
                             text
                             @click="removeBomItem(row.tempId)">删除
                  </el-button>
                  <el-button v-if="bomDataValue.isEdit"
                             type="primary"
                             text
                             @click="addBomItem(row.tempId)">添加
                  </el-button>
                </template>
              </el-table-column>
@@ -1186,60 +1139,11 @@
    item.operationName = processName;
  };
  const normalizeTreeData = items => {
  const normalizeBomData = items => {
    items.forEach(item => {
      item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`;
      syncProcessOperationFields(item);
      if (Array.isArray(item.children) && item.children.length > 0) {
        normalizeTreeData(item.children);
      }
    });
  };
  const toQuantityNumber = value => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(4));
  };
  const syncDemandedQuantityTree = (items, parentDemandedQuantity = null) => {
    items.forEach(item => {
      if (parentDemandedQuantity !== null) {
        item.demandedQuantity = toQuantityNumber(
          parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
        );
      }
      if (Array.isArray(item.children) && item.children.length > 0) {
        syncDemandedQuantityTree(
          item.children,
          toQuantityNumber(item.demandedQuantity)
        );
      }
    });
  };
  const recalculateDemandedQuantities = () => {
    if (pageType.value !== "order") {
      return;
    }
    const rootDemandedQuantity = routeInfo.value.quantity;
    if (
      rootDemandedQuantity === undefined ||
      rootDemandedQuantity === null ||
      rootDemandedQuantity === ""
    ) {
      syncDemandedQuantityTree(bomDataValue.value.dataList);
      return;
    }
    syncDemandedQuantityTree(
      bomDataValue.value.dataList,
      toQuantityNumber(rootDemandedQuantity)
    );
  };
  const processChange = value => {
@@ -1252,40 +1156,9 @@
    });
  };
  const findSiblings = (items, tempId) => {
    if (!items || items.length === 0) return null;
    // 检查当前层级
    if (items.some(item => item.tempId === tempId)) {
      return items;
    }
    // 递归查找子级
    for (const item of items) {
      if (item.children && item.children.length > 0) {
        const result = findSiblings(item.children, tempId);
        if (result) return result;
      }
    }
    return null;
  };
  const handleBomProcessChange = (row, value) => {
    row.processId = value || "";
    syncProcessOperationFields(row);
    // 检查同一层级是否已经有其他不同的工序被选中
    const siblings = findSiblings(bomDataValue.value.dataList, row.tempId);
    if (siblings && value) {
      const hasDifferentProcess = siblings.some(sibling => {
        return (
          sibling.tempId !== row.tempId &&
          sibling.processId &&
          sibling.processId !== value
        );
      });
      if (hasDifferentProcess) {
        ElMessage.warning("同一层级已存在不同的工序,请先统一工序后再进行修改");
      }
    }
  };
  const openBomDialog = tempId => {
@@ -1300,29 +1173,10 @@
        routeInfo.value.bomId
      );
      bomDataValue.value.dataList = data || [];
      normalizeTreeData(bomDataValue.value.dataList);
      recalculateDemandedQuantities();
      normalizeBomData(bomDataValue.value.dataList);
    } catch (err) {
      console.error("获取BOM数据失败:", err);
    }
  };
  const childItem = (item, tempId, productData) => {
    if (item.tempId === tempId) {
      item.productName = productData.productName;
      item.model = productData.model;
      item.productModelId = productData.id;
      item.unit = productData.unit || "";
      return true;
    }
    if (item.children && item.children.length > 0) {
      for (let child of item.children) {
        if (childItem(child, tempId, productData)) {
          return true;
        }
      }
    }
    return false;
  };
  const handleBomProduct = row => {
@@ -1331,219 +1185,51 @@
      return;
    }
    const productData = row[row.length - 1];
    const isTopLevel = bomDataValue.value.dataList.some(
    const currentItem = bomDataValue.value.dataList.find(
      item => item.tempId === bomDataValue.value.currentRowName
    );
    if (isTopLevel) {
      if (
        productData.productName === bomTableData.value[0].productName &&
        productData.model === bomTableData.value[0].model
      ) {
        const hasOther = bomDataValue.value.dataList.some(
          item =>
            item.tempId !== bomDataValue.value.currentRowName &&
            item.productName === bomTableData.value[0].productName &&
            item.model === bomTableData.value[0].model
        );
        if (hasOther) {
          ElMessage.warning("最外层和当前产品一样的一级只能有一个");
          return;
    if (currentItem) {
      currentItem.productName = productData.productName;
      currentItem.model = productData.model;
      currentItem.productModelId = productData.id;
        }
      }
    }
    bomDataValue.value.dataList.forEach(item => {
      if (item.tempId === bomDataValue.value.currentRowName) {
        item.productName = productData.productName;
        item.model = productData.model;
        item.productModelId = productData.id;
        item.unit = productData.unit || "";
        return;
      }
      childItem(item, bomDataValue.value.currentRowName, productData);
    });
    bomDataValue.value.showProductDialog = false;
  };
  const removeBomItem = tempId => {
    const topIndex = bomDataValue.value.dataList.findIndex(
    const index = bomDataValue.value.dataList.findIndex(
      item => item.tempId === tempId
    );
    if (topIndex !== -1) {
      bomDataValue.value.dataList.splice(topIndex, 1);
      return;
    if (index !== -1) {
      bomDataValue.value.dataList.splice(index, 1);
    }
    const delchildItem = (items, tempId) => {
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.tempId === tempId) {
          items.splice(i, 1);
          return true;
        }
        if (item.children && item.children.length > 0) {
          if (delchildItem(item.children, tempId)) {
            return true;
          }
        }
      }
      return false;
    };
    bomDataValue.value.dataList.forEach(item => {
      if (item.children && item.children.length > 0) {
        delchildItem(item.children, tempId);
      }
    });
  };
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
  const addchildItem = (item, tempId) => {
    if (item.tempId === tempId) {
      if (!item.children) {
        item.children = [];
      }
      item.children.push({
        parentId: item.id || "",
        parentTempId: item.tempId || "",
  const addBomItem = () => {
    bomDataValue.value.dataList.push({
        productName: "",
        productId: "",
        model: undefined,
        productModelId: undefined,
        processId: "",
        processName: "",
        [pageType.value === "order" ? "technologyOperationId" : "operationId"]:
          "",
      technologyOperationId: "",
        operationName: "",
        unitQuantity: 1,
        demandedQuantity: 0,
        children: [],
        unit: "",
        tempId: new Date().getTime(),
      });
      recalculateDemandedQuantities();
      return true;
    }
    if (item.children && item.children.length > 0) {
      for (let child of item.children) {
        if (addchildItem(child, tempId)) {
          return true;
        }
      }
    }
    return false;
  };
  const addBomItem = tempId => {
    bomDataValue.value.dataList.forEach(item => {
      if (item.tempId === tempId) {
        if (!item.children) {
          item.children = [];
        }
        item.children.push({
          parentId: item.id || "",
          parentTempId: item.tempId || "",
          productName: "",
          productId: "",
          model: undefined,
          productModelId: undefined,
          processId: "",
          processName: "",
          [pageType.value === "order" ? "technologyOperationId" : "operationId"]:
            "",
          operationName: "",
          unitQuantity: 1,
          demandedQuantity: 0,
          unit: "",
          children: [],
          tempId: new Date().getTime(),
        });
        recalculateDemandedQuantities();
        return;
      }
      addchildItem(item, tempId);
      tempId: `${Date.now()}_${Math.random()}`,
    });
  };
  const validateAllBom = () => {
    let isValid = true;
    const isOrderPage = pageType.value === "order";
    const validateItem = (item, isTopLevel = false) => {
      if (!item.model) {
        ElMessage.error("请选择规格");
        isValid = false;
        return;
    for (const item of bomDataValue.value.dataList) {
      if (!item.productModelId) {
        ElMessage.error("请选择物料规格");
        return false;
      }
      if (!isTopLevel && !item.processId) {
      if (!item.processId) {
        ElMessage.error("请选择消耗工序");
        isValid = false;
        return;
      }
      if (!item.unitQuantity) {
        ElMessage.error("请输入单位产出所需数量");
        isValid = false;
        return;
      }
      if (isOrderPage && !item.demandedQuantity) {
        ElMessage.error("请输入需求总量");
        isValid = false;
        return;
      }
      if (item.children && item.children.length > 0) {
        item.children.forEach(child => {
          validateItem(child, false);
        });
      }
    };
    // 校验同一层级的工序是否一致
    const validateProcessConsistency = items => {
      if (!items || items.length === 0) return;
      // 检查当前层级
      const processes = items
        .filter(item => item.processId)
        .map(item => item.processId);
      if (processes.length > 1) {
        const uniqueProcesses = [...new Set(processes)];
        if (uniqueProcesses.length > 1) {
          ElMessage.error("同一层级的工序必须一致");
          isValid = false;
          return;
        return false;
        }
      }
      // 递归检查子级
      items.forEach(item => {
        if (item.children && item.children.length > 0) {
          validateProcessConsistency(item.children);
        }
      });
    };
    bomDataValue.value.dataList.forEach(item => {
      validateItem(item, true);
    });
    validateProcessConsistency(bomDataValue.value.dataList);
    return isValid;
  };
  const buildSubmitTree = items => {
    return items.map(item => {
      const current = { ...item };
      syncProcessOperationFields(current);
      current.children = Array.isArray(current.children)
        ? buildSubmitTree(current.children)
        : [];
      return current;
    });
    return true;
  };
  const cancelEditBom = () => {
@@ -1553,17 +1239,18 @@
  const handleSaveBom = () => {
    bomDataValue.value.loading = true;
    console.log(bomDataValue.value.dataList, "bomDataValue.value.dataList");
    normalizeTreeData(bomDataValue.value.dataList);
    recalculateDemandedQuantities();
    normalizeBomData(bomDataValue.value.dataList);
    const valid = validateAllBom();
    if (valid) {
      add2({
        // bomId: Number(routeInfo.value.bomId),
        productionOrderBomId: Number(routeInfo.value.bomId) || null,
        children: buildSubmitTree(bomDataValue.value.dataList || []),
        productionOrderId: Number(orderId.value) || null,
        detailList: bomDataValue.value.dataList.map(item => ({
          id: item.id || undefined,
          productModelId: item.productModelId,
          technologyOperationId: item.technologyOperationId || item.processId,
        })),
      })
        .then(() => {
          ElMessage.success("BOM保存成功");
src/views/productionManagement/productStructure/Detail/index.vue
@@ -27,23 +27,27 @@
        <template #default="props">
          <el-form ref="form"
                   :model="dataValue">
            <div v-if="dataValue.isEdit && !isOrderPage"
                 class="mb10"
                 style="text-align: right;">
              <el-button type="primary"
                         @click="addItem">添加物料</el-button>
            </div>
            <el-table :data="dataValue.dataList"
                      row-key="tempId"
                      default-expand-all
                      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
                      style="width: 100%">
              <el-table-column prop="productName"
                               label="产品" />
                               label="物料名称" />
              <el-table-column prop="model"
                               label="规格">
                <template #default="{ row, $index }">
                               label="物料规格">
                <template #default="{ row }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
                                :rules="[{ required: true, message: '请选择物料规格', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-select v-model="row.model"
                               placeholder="请选择规格"
                               placeholder="请选择物料规格"
                               clearable
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                               :disabled="!dataValue.isEdit"
                               style="width: 100%"
                               @visible-change="(v) => { if (v) openDialog(row.tempId) }">
                      <el-option v-if="row.model"
@@ -51,13 +55,14 @@
                                 :value="row.model" />
                    </el-select>
                  </el-form-item>
                  <span v-else>{{ row.model }}</span>
                </template>
              </el-table-column>
              <el-table-column prop="processName"
                               label="消耗工序">
                <template #default="{ row, $index }">
                <template #default="{ row }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                :rules="[{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
@@ -65,73 +70,24 @@
                               clearable
                               style="width: 100%"
                               @change="value => handleProcessChange(row, value)"
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
                               :disabled="!dataValue.isEdit">
                      <el-option v-for="item in dataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
                                 :value="item.id" />
                    </el-select>
                  </el-form-item>
                  <span v-else>{{ row.processName }}</span>
                </template>
              </el-table-column>
              <el-table-column prop="unitQuantity"
                               label="单位产出所需数量">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.unitQuantity"
                                     :min="0"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     @change="handleUnitQuantityChange"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column v-if="isOrderPage"
                               prop="demandedQuantity"
                               label="需求总量">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="[{ required: true, message: '请输入需求总量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.demandedQuantity"
                                     :min="0"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="true" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unit"
                               label="单位">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="[{ required: true, message: '请输入单位', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input v-model="row.unit"
                              placeholder="请输入单位"
                              clearable
                              :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column label="操作"
              <el-table-column v-if="dataValue.isEdit && !isOrderPage"
                               label="操作"
                               fixed="right"
                               width="200">
                <template #default="{ row, $index }">
                  <el-button v-if="dataValue.isEdit && !dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                             type="danger"
                               width="120">
                <template #default="{ row }">
                  <el-button type="danger"
                             text
                             @click="removeItem(row.tempId)">删除
                  </el-button>
                  <el-button v-if="dataValue.isEdit"
                             type="primary"
                             text
                             @click="addItem2(row.tempId)">添加
                  </el-button>
                </template>
              </el-table-column>
@@ -238,7 +194,7 @@
  };
  const syncProcessOperationFields = (item: any) => {
    const processId = item.processId ?? item.operationId ?? "";
    const processId = item.processId ?? item.operationId ?? item.technologyOperationId ?? "";
    if (!processId) {
      item.processId = "";
      item.operationId = "";
@@ -257,97 +213,27 @@
    item.operationName = processName;
  };
  const normalizeTreeData = (items: any[]) => {
    items.forEach((item: any) => {
      item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`;
      syncProcessOperationFields(item);
      if (Array.isArray(item.children) && item.children.length > 0) {
        normalizeTreeData(item.children);
      }
    });
  const createTempId = () => `${Date.now()}_${Math.random()}`;
  // BOM 只维护“物料规格 + 消耗工序”,tempId 仅作为表格本地行键,不表达任何层级关系
  const normalizeMaterialItem = (item: any) => {
    const material = {
      id: item.id ?? null,
      tempId: item.tempId || item.id || createTempId(),
      productName: item.productName || item.materialName || "",
      model: item.model || item.materialModel || undefined,
      productModelId: item.productModelId || item.materialModelId || undefined,
      processId: item.processId ?? item.operationId ?? item.technologyOperationId ?? "",
      processName: item.processName || item.operationName || "",
      operationId: item.operationId ?? item.processId ?? item.technologyOperationId ?? "",
      operationName: item.operationName || item.processName || "",
    };
    syncProcessOperationFields(material);
    return material;
  };
  const toQuantityNumber = (value: any) => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(4));
  };
  const syncDemandedQuantityTree = (
    items: any[],
    parentDemandedQuantity: number | null = null
  ) => {
    items.forEach((item: any) => {
      if (parentDemandedQuantity !== null) {
        item.demandedQuantity = toQuantityNumber(
          parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
        );
      }
      if (Array.isArray(item.children) && item.children.length > 0) {
        syncDemandedQuantityTree(
          item.children,
          toQuantityNumber(item.demandedQuantity)
        );
      }
    });
  };
  const recalculateDemandedQuantities = () => {
    if (!isOrderPage.value) {
      return;
    }
    syncDemandedQuantityTree(dataValue.dataList);
  };
  const buildSubmitTree = (items: any[]) => {
    return items.map((item: any) => {
      const current = { ...item };
      syncProcessOperationFields(current);
      current.children = Array.isArray(current.children)
        ? buildSubmitTree(current.children)
        : [];
      return current;
    });
  };
  const findSiblings = (items: any[], tempId: string): any[] | null => {
    if (!items || items.length === 0) return null;
    // 检查当前层级
    if (items.some(item => item.tempId === tempId)) {
      return items;
    }
    // 递归查找子级
    for (const item of items) {
      if (item.children && item.children.length > 0) {
        const result = findSiblings(item.children, tempId);
        if (result) return result;
      }
    }
    return null;
  };
  const handleProcessChange = (row: any, value: any) => {
    row.processId = value || "";
    syncProcessOperationFields(row);
    // 检查同一层级是否已经有其他不同的工序被选中
    const siblings = findSiblings(dataValue.dataList, row.tempId);
    if (siblings && value) {
      const hasDifferentProcess = siblings.some(sibling => {
        return sibling.tempId !== row.tempId && sibling.processId && sibling.processId !== value;
      });
      if (hasDifferentProcess) {
        ElMessage.warning("同一层级已存在不同的工序,请先统一工序后再进行修改");
      }
    }
  };
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  const normalizeMaterialList = (items: any[]) => {
    return normalizeListData(items).map(item => normalizeMaterialItem(item));
  };
  const tableData = reactive([
@@ -359,7 +245,6 @@
  ]);
  const openDialog = (tempId: any) => {
    console.log(tempId, "tempId");
    dataValue.currentRowName = tempId;
    dataValue.showProductDialog = true;
  };
@@ -368,188 +253,94 @@
    if (isOrderPage.value) {
      // 订单情况:使用订单的产品结构接口
      const { data } = await listProcessBom({ orderId: routeOrderId.value });
      dataValue.dataList = (data as any) || [];
      normalizeTreeData(dataValue.dataList);
      recalculateDemandedQuantities();
      dataValue.dataList = normalizeMaterialList((data as any) || []);
    } else {
      // 非订单情况:使用原来的接口
      const { data } = await queryList(routeId.value);
      dataValue.dataList = (data as any) || [];
      console.log(dataValue);
      normalizeTreeData(dataValue.dataList);
      console.log(dataValue.dataList, "dataValue.dataList");
      dataValue.dataList = normalizeMaterialList((data as any) || []);
    }
  };
  const fetchProcessOptions = async () => {
    const { data } = await list({});
    console.log(data, "dataValue.dataList");
    dataValue.processOptions = normalizeListData(data);
  };
  const handleProduct = (row: any) => {
    if (!Array.isArray(row) || row.length === 0) {
      ElMessage.warning("请选择一个产品");
      ElMessage.warning("请选择一个物料");
      return;
    }
    // 只允许一个:如果上游返回了多个,默认使用最后一次选择并覆盖当前值
    const productData = row[row.length - 1];
    //  最外层组件中,与当前产品相同的产品只能有一个
    const isTopLevel = dataValue.dataList.some(
      item => (item as any).tempId === dataValue.currentRowName
    const current = dataValue.dataList.find(
      item => item.tempId === dataValue.currentRowName
    );
    if (isTopLevel) {
      if (
        productData.productName === tableData[0].productName &&
        productData.model === tableData[0].model
      ) {
        //  查找是否已经有其他顶层行已经是这个产品
        const hasOther = dataValue.dataList.some(
          item =>
            (item as any).tempId !== dataValue.currentRowName &&
            (item as any).productName === tableData[0].productName &&
            (item as any).model === tableData[0].model
        );
        if (hasOther) {
          ElMessage.warning("最外层和当前产品一样的一级只能有一个");
          return;
    if (current) {
      current.productName = productData.productName;
      current.model = productData.model;
      current.productModelId = productData.id;
        }
      }
    }
    // dataValue.dataList[dataValue.currentRowIndex].productName =
    //   row[0].productName;
    // dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
    // dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
    // dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || "";
    dataValue.dataList.map(item => {
      if (item.tempId === dataValue.currentRowName) {
        item.productName = productData.productName;
        item.model = productData.model;
        item.productModelId = productData.id;
        item.unit = productData.unit || "";
        return;
      }
      childItem(item, dataValue.currentRowName, productData);
    });
    dataValue.showProductDialog = false;
  };
  const childItem = (item: any, tempId: any, productData: any) => {
    if (item.tempId === tempId) {
      item.productName = productData.productName;
      item.model = productData.model;
      item.productModelId = productData.id;
      item.unit = productData.unit || "";
      return true;
    }
    if (item.children && item.children.length > 0) {
      for (let child of item.children) {
        if (childItem(child, tempId, productData)) {
          return true;
        }
      }
    }
    return false;
  };
  // 递归校验所有层级的表单数据
  const validateAll = () => {
    let isValid = true;
    if (dataValue.dataList.length === 0) {
      ElMessage.error("请添加物料");
      return false;
    }
    // 校验一组兄弟节点的工序是否都相同
    const checkProcessUniqueness = (items: any[]) => {
      if (!items || items.length === 0 || !isValid) return;
      // 获取第一个非空的工序ID作为参考
      const firstProcessId = items.find(item => item.processId)?.processId;
      // 如果有工序ID,检查所有项是否都使用相同的工序
      if (firstProcessId) {
        for (const item of items) {
          if (item.processId && item.processId !== firstProcessId) {
            const option1 = getProcessOptionById(firstProcessId);
            const option2 = getProcessOptionById(item.processId);
            const processName1 = option1?.name || "未知工序";
            const processName2 = option2?.name || "未知工序";
            ElMessage.error(
              `当前层级下工序不一致,请使用相同的工序。存在「${processName1}」和「${processName2}」`
    const invalidItem = dataValue.dataList.find(
      item => !item.model || !item.productModelId || !item.processId
            );
            isValid = false;
            return;
          }
        }
    if (invalidItem) {
      ElMessage.error("请选择物料规格和消耗工序");
      return false;
      }
      // 递归校验子级的兄弟节点
      for (const item of items) {
        if (item.children && item.children.length > 0) {
          checkProcessUniqueness(item.children);
    const relationKeys = new Set();
    const duplicated = dataValue.dataList.some(item => {
      const key = `${item.productModelId}#${item.processId}`;
      if (relationKeys.has(key)) {
        return true;
        }
      relationKeys.add(key);
      return false;
    });
    if (duplicated) {
      ElMessage.error("同一物料规格在同一工序下不能重复配置");
      return false;
      }
    return true;
    };
    // 校验函数
    const validateItem = (item: any, isTopLevel = false) => {
      if (!isValid) return;
      // 校验当前项的必填字段
      if (!item.model) {
        ElMessage.error("请选择规格");
        isValid = false;
        return;
      }
      if (!isTopLevel && !item.processId) {
        ElMessage.error("请选择消耗工序");
        isValid = false;
        return;
      }
      if (!item.unitQuantity) {
        ElMessage.error("请输入单位产出所需数量");
        isValid = false;
        return;
      }
      if (isOrderPage.value && !item.demandedQuantity) {
        ElMessage.error("请输入需求总量");
        isValid = false;
        return;
      }
      // if (!item.unit) {
      //   ElMessage.error("请输入单位");
      //   isValid = false;
      //   return;
      // }
      // 递归校验子项字段
      if (item.children && item.children.length > 0) {
        item.children.forEach(child => {
          validateItem(child, false);
  // 提交给后端的只有物料规格和消耗工序,不含数量、单位和层级字段
  const buildSubmitList = (items: any[]) => {
    return items.map((item: any) => {
      const current = normalizeMaterialItem(item);
      return {
        id: current.id || undefined,
        productModelId: current.productModelId,
        operationId: current.operationId,
      };
        });
      }
    };
    // 1. 首先校验同一父级下的同层消耗工序是否唯一
    checkProcessUniqueness(dataValue.dataList);
    if (!isValid) return false;
    // 2. 然后遍历校验所有顶层项的字段必填情况
    dataValue.dataList.forEach(item => {
      validateItem(item, true);
    });
    return isValid;
  const handleProcessChange = (row: any, value: any) => {
    row.processId = value || "";
    syncProcessOperationFields(row);
  };
  const submit = () => {
    dataValue.loading = true;
    normalizeTreeData(dataValue.dataList);
    recalculateDemandedQuantities();
    // 先进行表单校验
    const valid = validateAll();
    console.log(dataValue.dataList, "dataValue.dataList");
    if (valid) {
      addBomDetail({
        bomId: routeId.value,
        children: buildSubmitTree(dataValue.dataList || []),
        detailList: buildSubmitList(dataValue.dataList || []),
      })
        .then(res => {
          router.go(-1);
@@ -565,117 +356,28 @@
  };
  const removeItem = (tempId: string) => {
    // 先尝试从顶层删除
    const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
    if (topIndex !== -1) {
      dataValue.dataList.splice(topIndex, 1);
      return;
    const index = dataValue.dataList.findIndex(item => item.tempId === tempId);
    if (index !== -1) {
      dataValue.dataList.splice(index, 1);
    }
    // 递归删除子项
    const delchildItem = (items: any[], tempId: any) => {
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.tempId === tempId) {
          items.splice(i, 1);
          return true;
        }
        if (item.children && item.children.length > 0) {
          if (delchildItem(item.children, tempId)) {
            return true;
          }
        }
      }
      return false;
    };
    dataValue.dataList.forEach(item => {
      if (item.children && item.children.length > 0) {
        delchildItem(item.children, tempId);
      }
    });
  };
  const addItem2 = tempId => {
    dataValue.dataList.map(item => {
      if (item.tempId === tempId) {
        if (!item.children) {
          item.children = [];
        }
        item.children.push({
          parentId: item.id || "",
          parentTempId: item.tempId || "",
  const addItem = () => {
    dataValue.dataList.push({
      id: null,
          productName: "",
          productId: "",
          model: undefined,
          productModelId: undefined,
          processId: "",
          processName: "",
          operationId: "",
          operationName: "",
          unitQuantity: 1,
          demandedQuantity: 0,
          unit: "",
          children: [],
          tempId: new Date().getTime(),
      tempId: createTempId(),
        });
        recalculateDemandedQuantities();
        return;
      }
      addchildItem(item, tempId);
    });
  };
  const addchildItem = (item: any, tempId: any) => {
    if (item.tempId === tempId) {
      console.log(item, "item");
      if (!item.children) {
        item.children = [];
      }
      item.children.push({
        parentId: item.id || "",
        parentTempId: item.tempId || "",
        productName: "",
        productId: "",
        model: undefined,
        productModelId: undefined,
        processId: "",
        processName: "",
        operationId: "",
        operationName: "",
        unitQuantity: 1,
        demandedQuantity: 0,
        children: [],
        unit: "",
        tempId: new Date().getTime(),
      });
      recalculateDemandedQuantities();
      return true;
    }
    if (item.children && item.children.length > 0) {
      for (let child of item.children) {
        if (addchildItem(child, tempId)) {
          return true;
        }
      }
    }
    return false;
  };
  const getPropPath = (row, field) => {
    // 为每个row生成唯一的路径
    // 使用row.id或索引作为唯一标识
    let path = "dataList";
    // 简单实现:使用row的id或一个唯一标识
    const uniqueId = row.id || Math.floor(Math.random() * 10000);
    path += `.${uniqueId}`;
    return path + `.${field}`;
  };
  const cancelEdit = () => {
    dataValue.isEdit = false;
    // dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
    fetchData();
  };
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -4,28 +4,28 @@
               title="领料详情"
               width="1400px"
               @close="handleClose">
      <div class="material-detail-tip">
        展示该生产订单下所有工序的领料记录。
      </div>
      <el-table v-loading="materialDetailLoading"
                :data="materialDetailTableData"
                border
                row-key="id">
        <el-table-column label="工序名称"
                         prop="operationName"
                         min-width="180" />
                         min-width="160" />
        <el-table-column label="工单编号"
                         prop="workOrderNo"
                         min-width="150" />
        <el-table-column label="原料名称"
                         prop="productName"
                         prop="materialName"
                         min-width="160" />
        <el-table-column label="原料型号"
                         prop="model"
                         prop="materialModel"
                         min-width="180" />
        <el-table-column label="批号"
                         prop="batchNo"
                         min-width="150" />
        <el-table-column label="需求数量"
                         min-width="110">
          <template #default="{ row }">
            {{ stripTrailingZeros(row.demandedQuantity) }}
          </template>
        </el-table-column>
        <el-table-column label="计量单位"
                         prop="unit"
                         width="100" />
@@ -35,105 +35,28 @@
            {{ stripTrailingZeros(row.pickQuantity) }}
          </template>
        </el-table-column>
        <el-table-column label="补料数量"
                         min-width="120">
          <template #default="{ row }">
            <el-button type="primary"
                       link
                       @click="handleViewSupplementRecord(row)">
              {{ stripTrailingZeros(row.feedingQty) ?? 0 }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column label="退料数量"
                         min-width="110">
          <template #default="{ row }">
            {{ stripTrailingZeros(row.returnQty) ?? 0 }}
            {{ stripTrailingZeros(row.returnQty) || 0 }}
          </template>
        </el-table-column>
        <el-table-column label="实际数量"
                         min-width="140">
        <el-table-column label="净领料数量"
                         min-width="130">
          <template #default="{ row }">
            <el-input-number v-model="row.actualQty"
                             :min="0"
                             :step="1"
                             controls-position="right"
                             placeholder="输入实际数量"
                             :formatter="value => stripTrailingZeros(value)"
                             :parser="value => parseFloat(value) || 0"
                             style="width: 100%;"
                             :disabled="row.returned || orderRow?.end"
                             @change="val => handleActualQtyChange(row, val)" />
            {{ stripTrailingZeros(row.effectiveQuantity) || 0 }}
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button v-if="!orderRow?.end"
                     type="warning"
                     :loading="materialReturnConfirming"
                     :disabled="!canOpenReturnSummary"
                     @click="openReturnSummaryDialog">
            退料确认
          </el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog v-model="supplementRecordDialogVisible"
               title="补料记录"
               width="800px">
      <el-table v-loading="supplementRecordLoading"
                :data="supplementRecordTableData"
                border
                row-key="id">
        <el-table-column label="补料数量"
                         prop="pickQuantity"
                         min-width="120" />
        <el-table-column label="补料人"
                         prop="supplementUserName"
                         min-width="120" />
        <el-table-column label="补料日期"
                         prop="supplementTime"
                         min-width="160" />
        <el-table-column label="补料原因"
                         prop="feedingReason"
                         min-width="200" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="supplementRecordDialogVisible = false">关闭</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog v-model="returnSummaryDialogVisible"
               title="退料汇总确认"
               width="900px">
      <el-table :data="returnSummaryList"
                border
                row-key="summaryKey">
        <el-table-column label="原料名称"
                         prop="materialName"
                         min-width="180" />
        <el-table-column label="原料型号"
                         prop="materialModel"
                         min-width="180" />
        <el-table-column label="计量单位"
                         prop="unit"
                         min-width="100" />
        <el-table-column label="退料汇总数量"
                         min-width="140">
        <el-table-column label="领料时间"
                         min-width="120">
          <template #default="{ row }">
            {{ stripTrailingZeros(row.returnQtyTotal) }}
            {{ formatDate(row.createTime) }}
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     :loading="materialReturnConfirming"
                     @click="handleReturnConfirm">确认提交</el-button>
          <el-button @click="returnSummaryDialogVisible = false">取消</el-button>
          <el-button @click="dialogVisible = false">关闭</el-button>
        </span>
      </template>
    </el-dialog>
@@ -142,12 +65,9 @@
<script setup>
  import { computed, ref, watch } from "vue";
  import { ElMessage } from "element-plus";
  import {
    listMaterialPickingDetail,
    listMaterialSupplementRecord,
    updateMaterialPickingLedger,
  } from "@/api/productionManagement/productionOrder.js";
  import { listMaterialPickingDetail } from "@/api/productionManagement/productionOrder.js";
  const formatDate = value => (value ? String(value).slice(0, 10) : "-");
  const stripTrailingZeros = val => {
    const str = String(val ?? "");
@@ -170,21 +90,6 @@
  const materialDetailLoading = ref(false);
  const materialDetailTableData = ref([]);
  const materialReturnConfirming = ref(false);
  const supplementRecordDialogVisible = ref(false);
  const supplementRecordLoading = ref(false);
  const supplementRecordTableData = ref([]);
  const returnSummaryDialogVisible = ref(false);
  const returnSummaryList = ref([]);
  const calcReturnQty = item =>
    Number(item.pickQuantity || 0) +
    Number(item.feedingQty || 0) -
    Number(item.actualQty || 0);
  const canOpenReturnSummary = computed(() =>
    materialDetailTableData.value.some(
      item => item.returned !== true && calcReturnQty(item) > 0
    )
  );
  const loadDetailList = async () => {
    if (!props.orderRow?.id) return;
@@ -192,12 +97,15 @@
    materialDetailTableData.value = [];
    try {
      const res = await listMaterialPickingDetail(props.orderRow.id);
      materialDetailTableData.value = (res.data || []).map(item => ({
      const records = Array.isArray(res.data) ? res.data : res.data?.records || [];
      materialDetailTableData.value = records.map(item => ({
        ...item,
        actualQty:
          item.actualQty ??
          Number(item.pickQuantity || 0) + Number(item.feedingQty || 0),
        operationName: item.operationName || item.processName || "",
        materialName: item.productName || item.materialName || "",
        materialModel: item.model || item.materialModel || "",
        pickQuantity: item.pickQuantity ?? item.quantity ?? 0,
        returnQty: item.returnQty ?? 0,
        effectiveQuantity: item.effectiveQuantity ?? 0,
      }));
    } finally {
      materialDetailLoading.value = false;
@@ -216,87 +124,12 @@
  const handleClose = () => {
    materialDetailTableData.value = [];
  };
  const handleActualQtyChange = (row, val) => {
    row.returnQty = calcReturnQty(row);
  };
  const handleViewSupplementRecord = async row => {
    if (!row?.id) return;
    supplementRecordDialogVisible.value = true;
    supplementRecordLoading.value = true;
    supplementRecordTableData.value = [];
    try {
      const res = await listMaterialSupplementRecord({
        pickId: row.id,
        productionOrderId: props.orderRow.id,
      });
      supplementRecordTableData.value = res.data || [];
    } finally {
      supplementRecordLoading.value = false;
    }
  };
  const buildReturnSummary = () => {
    const map = new Map();
    materialDetailTableData.value.forEach(item => {
      const returnQty = calcReturnQty(item);
      if (returnQty <= 0) return;
      const key = `${item.productModelId || ""}_${item.productName || ""}_${
        item.model || ""
      }_${item.unit || ""}`;
      const old = map.get(key) || {
        summaryKey: key,
        materialName: item.productName || "",
        materialModel: item.model || "",
        unit: item.unit || "",
        returnQtyTotal: 0,
      };
      old.returnQtyTotal += returnQty;
      map.set(key, old);
    });
    return Array.from(map.values());
  };
  const openReturnSummaryDialog = async () => {
    if (!canOpenReturnSummary.value) {
      ElMessage.warning("退料数量=领用数量+补料数量-实际数量,且需大于0");
      return;
    }
    returnSummaryList.value = buildReturnSummary();
    returnSummaryDialogVisible.value = true;
  };
  const handleReturnConfirm = async () => {
    if (!props.orderRow?.id) return;
    materialReturnConfirming.value = true;
    try {
      await updateMaterialPickingLedger({
        productionOrderId: props.orderRow.id,
        productionOrderPickDto: materialDetailTableData.value.map(item => ({
          id: item.id,
          technologyOperationId: item.technologyOperationId,
          operationName: item.operationName,
          bom: item.bom === true,
          productModelId: item.productModelId,
          demandedQuantity: item.demandedQuantity,
          unit: item.unit,
          pickQuantity: item.pickQuantity,
          batchNo: item.batchNo,
          feedingQty: item.feedingQty,
          returnQty: item.returnQty,
          actualQty: item.actualQty,
          feedingReason: item.feedingReason,
          returned: true,
        })),
      });
      returnSummaryDialogVisible.value = false;
      dialogVisible.value = false;
      emit("confirmed");
    } finally {
      materialReturnConfirming.value = false;
    }
  };
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
  .material-detail-tip {
    margin-bottom: 12px;
    color: #606266;
    font-size: 13px;
  }
</style>
src/views/productionManagement/productionOrder/index.vue
@@ -192,24 +192,12 @@
        </div>
      </div>
    </el-dialog>
    <MaterialLedgerDialog v-model="materialDialogVisible"
                          :order-row="currentMaterialOrder"
                           />
    <MaterialDetailDialog v-model="materialDetailDialogVisible"
                          :order-row="currentMaterialDetailOrder"
                          @confirmed="getList" />
    <MaterialSupplementDialog v-model="materialSupplementDialogVisible"
                              :order-row="currentMaterialSupplementOrder"
                              @saved="getList" />
    <new-product-order v-if="isShowNewModal"
                       v-model:visible="isShowNewModal"
                       @completed="handleQuery" />
    <!-- 打印领料单组件 -->
    <div class="print-requisition-wrapper">
      <PrintMaterialRequisition ref="printRef"
                                :order-row="printOrderRow"
                                :material-list="printMaterialList" />
    </div>
  </div>
</template>
@@ -236,16 +224,9 @@
    updateProductOrder,
  } from "@/api/productionManagement/productionOrder.js";
  import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
  import MaterialLedgerDialog from "@/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue";
  import MaterialDetailDialog from "@/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue";
  import MaterialSupplementDialog from "@/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue";
  import PrintMaterialRequisition from "@/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import { listPage } from "@/api/productionManagement/processRoute.js";
  import {
    listMaterialPickingDetail,
    listMaterialPickingBom,
  } from "@/api/productionManagement/productionOrder.js";
  const NewProductOrder = defineAsyncComponent(() =>
    import("@/views/productionManagement/productionOrder/New.vue")
  );
@@ -398,38 +379,11 @@
          },
        },
        {
          name: "领料",
          type: "text",
          color: "#5EC7AB",
          showHide: row => !row.endOrder && !row.returned,
          clickFun: row => {
            openMaterialDialog(row);
          },
        },
        {
          name: "补料",
          type: "text",
          color: "#5EC7AB",
          showHide: row => !row.endOrder && !row.returned,
          clickFun: row => {
            openMaterialSupplementDialog(row);
          },
        },
        {
          name: "领料详情",
          type: "text",
          color: "#5EC7AB",
          clickFun: row => {
            openMaterialDetailDialog(row);
          },
        },
        {
          name: "打印领料单",
          type: "text",
          color: "#5EC7AB",
          showHide: row => !row.endOrder,
          clickFun: row => {
            handlePrint(row);
          },
        },
        {
@@ -524,48 +478,8 @@
    orderId: null,
    routeId: null,
  });
  const materialDialogVisible = ref(false);
  const currentMaterialOrder = ref(null);
  const materialDetailDialogVisible = ref(false);
  const currentMaterialDetailOrder = ref(null);
  const materialSupplementDialogVisible = ref(false);
  const currentMaterialSupplementOrder = ref(null);
  // 打印相关
  const printOrderRow = ref(null);
  const printMaterialList = ref([]);
  const handlePrint = async row => {
    printOrderRow.value = row;
    proxy.$modal.loading("正在获取领料数据...");
    try {
      printMaterialList.value = [];
      const detailRes = await listMaterialPickingDetail(row.id);
      const detailList = Array.isArray(detailRes?.data)
        ? detailRes.data
        : detailRes?.data?.records || [];
      if (detailList.length > 0) {
        printMaterialList.value = detailList;
      }
      if (printMaterialList.value.length === 0) {
        proxy.$modal.msgWarning("暂无领料数据");
        return;
      }
      // 等待 DOM 更新后执行打印
      proxy.$nextTick(() => {
        setTimeout(() => {
          window.print();
        }, 800);
      });
    } catch (e) {
      console.error("获取领料数据失败:", e);
      proxy.$modal.msgError("获取领料数据失败");
    } finally {
      proxy.$modal.closeLoading();
    }
  };
  const openBindRouteDialog = async (row, type) => {
    bindForm.orderId = row.id;
@@ -611,19 +525,9 @@
    }
  };
  const openMaterialDialog = row => {
    currentMaterialOrder.value = row;
    materialDialogVisible.value = true;
  };
  const openMaterialDetailDialog = async row => {
    currentMaterialDetailOrder.value = row;
    materialDetailDialogVisible.value = true;
  };
  const openMaterialSupplementDialog = row => {
    currentMaterialSupplementOrder.value = row;
    materialSupplementDialogVisible.value = true;
  };
  const handleReset = () => {
src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue
@@ -1,35 +1,62 @@
<template>
  <div>
    <el-dialog v-model="dialogVisible"
               title="物料"
               title="工序领料"
               width="1200px"
               @close="handleCloseMaterialDialog">
      <div class="material-toolbar">
        <span>
          按当前工序的 BOM 物料领用,也可添加 BOM 外的物料;填写本次领料数量并选择批号后领用,库存立即扣减。
        </span>
        <el-button type="primary"
                   link
                   @click="openProductDialog">添加物料</el-button>
      </div>
      <el-table v-loading="materialTableLoading"
                :data="materialTableData"
                :data="pickRows"
                border
                row-key="id">
                row-key="rowKey">
        <el-table-column label="工序名称"
                         prop="processName"
                         prop="operationName"
                         min-width="120" />
        <el-table-column label="物料名称"
                         prop="productName"
                         min-width="140" />
        <el-table-column label="原料名称"
                         prop="materialName"
                         min-width="140" />
        <el-table-column label="原料型号"
                         prop="materialModel"
        <el-table-column label="物料规格"
                         prop="model"
                         min-width="140" />
        <el-table-column label="计量单位"
                         prop="unit"
                         min-width="100" />
        <el-table-column label="线边仓数量"
                         prop="pickQty"
                         min-width="100" />
        <el-table-column label="补料数量"
                         prop="supplementQty"
                         min-width="100" />
        <el-table-column label="实际数量"
                         min-width="140">
                         width="90" />
        <el-table-column label="来源"
                         width="90">
          <template #default="{ row }">
            <el-input-number v-model="row.actualQty"
            <el-tag :type="row.bom ? 'success' : 'info'"
                    size="small">{{ row.bom ? "BOM" : "手工" }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="可用库存"
                         prop="stockQuantity"
                         min-width="100" />
        <el-table-column label="批号"
                         min-width="200">
          <template #default="{ row }">
            <el-select v-model="row.batchNo"
                       placeholder="请选择批号"
                       filterable
                       clearable
                       style="width: 100%;">
              <el-option v-for="batchNo in row.batchNoList || []"
                         :key="batchNo"
                         :label="batchNo"
                         :value="batchNo" />
            </el-select>
          </template>
        </el-table-column>
        <el-table-column label="本次领料数量"
                         min-width="150">
          <template #default="{ row }">
            <el-input-number v-model="row.pickQuantity"
                             :min="0"
                             :precision="3"
                             :step="1"
@@ -40,17 +67,75 @@
        <el-table-column label="操作"
                         align="center"
                         fixed="right"
                         width="180">
          <template #default="{ row }">
            <el-button type="primary"
                         width="90">
          <template #default="{ row, $index }">
            <el-button v-if="!row.bom"
                       type="danger"
                       link
                       @click="openSupplementDialog(row)">补料</el-button>
            <el-button type="info"
                       link
                       @click="openSupplementRecordDialog(row)">补料记录</el-button>
                       @click="removePickRow($index)">移除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="material-section-title">本工序已领料记录</div>
      <el-table v-loading="materialTableLoading"
                :data="pickedRows"
                border
                row-key="id">
        <el-table-column label="物料名称"
                         prop="productName"
                         min-width="140" />
        <el-table-column label="物料规格"
                         prop="model"
                         min-width="140" />
        <el-table-column label="批号"
                         prop="batchNo"
                         min-width="150" />
        <el-table-column label="计量单位"
                         prop="unit"
                         width="90" />
        <el-table-column label="领料数量"
                         prop="pickQuantity"
                         min-width="110" />
        <el-table-column label="退料数量"
                         min-width="110">
          <template #default="{ row }">{{ row.returnQty || 0 }}</template>
        </el-table-column>
        <el-table-column label="净领料数量"
                         prop="effectiveQuantity"
                         min-width="120" />
        <el-table-column label="领料时间"
                         min-width="120">
          <template #default="{ row }">
            {{ formatDate(row.createTime) }}
          </template>
        </el-table-column>
        <el-table-column label="本次退料数量"
                         min-width="150">
          <template #default="{ row }">
            <el-input-number v-model="row.currentReturnQty"
                             :min="0"
                             :max="Number(row.effectiveQuantity || 0)"
                             :precision="3"
                             :step="1"
                             controls-position="right"
                             style="width: 100%;" />
          </template>
        </el-table-column>
        <el-table-column label="操作"
                         align="center"
                         fixed="right"
                         width="90">
          <template #default="{ row }">
            <el-button type="primary"
                       link
                       :loading="returningId === row.id"
                       :disabled="Number(row.effectiveQuantity || 0) <= 0"
                       @click="handleReturnMaterial(row)">退料</el-button>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
@@ -60,80 +145,25 @@
        </span>
      </template>
    </el-dialog>
    <FormDialog v-model="supplementDialogVisible"
                title="补料"
                width="500px"
                @confirm="handleSubmitSupplement">
      <el-form ref="supplementFormRef"
               :model="supplementForm"
               :rules="supplementRules"
               label-width="100px">
        <el-form-item label="补料数量"
                      prop="supplementQty">
          <el-input-number v-model="supplementForm.supplementQty"
                           :min="0.001"
                           :precision="3"
                           :step="1"
                           style="width: 100%;" />
        </el-form-item>
        <el-form-item label="补料原因"
                      prop="supplementReason">
          <el-input v-model="supplementForm.supplementReason"
                    type="textarea"
                    :rows="3"
                    maxlength="200"
                    show-word-limit
                    placeholder="请输入补料原因" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     :loading="supplementSubmitting"
                     @click="handleSubmitSupplement">确定</el-button>
          <el-button @click="supplementDialogVisible = false">取消</el-button>
        </span>
      </template>
    </FormDialog>
    <el-dialog v-model="supplementRecordDialogVisible"
               title="补料记录"
               width="900px">
      <el-table v-loading="supplementRecordLoading"
                :data="supplementRecordTableData"
                border
                row-key="id">
        <el-table-column label="补料数量"
                         prop="supplementQty"
                         min-width="100" />
        <el-table-column label="补料原因"
                         prop="supplementReason"
                         min-width="200" />
        <el-table-column label="补料人"
                         prop="supplementUserName"
                         min-width="120" />
        <el-table-column label="补料日期"
                         prop="supplementTime"
                         min-width="160" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="supplementRecordDialogVisible = false">关闭</el-button>
        </span>
      </template>
    </el-dialog>
    <ProductSelectDialog v-if="productDialogVisible"
                         v-model:model-value="productDialogVisible"
                         :single="true"
                         @confirm="handleProductConfirm" />
  </div>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from "vue";
  import { ElMessage } from "element-plus";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import { computed, defineAsyncComponent, ref, watch } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import {
    listWorkOrderMaterialLedger,
    addWorkOrderMaterialSupplement,
    listWorkOrderMaterialSupplementRecord,
    pickWorkOrderMaterial,
    returnWorkOrderMaterial,
  } from "@/api/productionManagement/workOrder.js";
  const ProductSelectDialog = defineAsyncComponent(() =>
    import("@/views/basicData/product/ProductSelectDialog.vue")
  );
  const props = defineProps({
    modelValue: {
@@ -154,46 +184,54 @@
  });
  const materialTableLoading = ref(false);
  const materialTableData = ref([]);
  const currentMaterialRow = ref(null);
  const currentMaterialOrderRow = ref(null);
  // 本次可领用的物料行:BOM 建议 + 手工添加
  const pickRows = ref([]);
  // 当前工序已产生的领料记录
  const pickedRows = ref([]);
  const productDialogVisible = ref(false);
  const currentWorkOrderRow = ref(null);
  const pickSubmitting = ref(false);
  const returningId = ref(null);
  const supplementDialogVisible = ref(false);
  const supplementSubmitting = ref(false);
  const supplementFormRef = ref(null);
  const supplementForm = reactive({
    supplementQty: null,
    supplementReason: "",
  });
  const formatDate = value => (value ? String(value).slice(0, 10) : "-");
  const supplementRecordDialogVisible = ref(false);
  const supplementRecordLoading = ref(false);
  const supplementRecordTableData = ref([]);
  const createRowKey = () => `${Date.now()}_${Math.random()}`;
  const supplementRules = {
    supplementQty: [
      { required: true, message: "请输入补料数量", trigger: "blur" },
    ],
    supplementReason: [
      { required: true, message: "请输入补料原因", trigger: "blur" },
    ],
  };
  const loadMaterialTable = async row => {
    if (!row?.id) return;
    currentMaterialOrderRow.value = row;
    currentWorkOrderRow.value = row;
    materialTableLoading.value = true;
    materialTableData.value = [];
    pickRows.value = [];
    pickedRows.value = [];
    try {
      const res = await listWorkOrderMaterialLedger({
        workOrderId: row.id,
        processId: row.processId,
        productProcessRouteItemId: row.productProcessRouteItemId,
      });
      materialTableData.value = res.data || [];
      const res = await listWorkOrderMaterialLedger(row.id);
      const records = Array.isArray(res.data) ? res.data : res.data?.records || [];
      pickRows.value = records
        .filter(item => item.picked === false || !item.id)
        .map(item => ({
          rowKey: createRowKey(),
          bom: item.bom !== false,
          productModelId: item.productModelId,
          productName: item.productName || "",
          model: item.model || "",
          unit: item.unit || "",
          operationName: item.operationName || "",
          stockQuantity: item.stockQuantity ?? 0,
          batchNoList: item.batchNoList || [],
          batchNo: "",
          pickQuantity: 0,
        }));
      pickedRows.value = records
        .filter(item => item.picked === true || item.id)
        .map(item => ({
          ...item,
          pickQuantity: item.pickQuantity ?? item.quantity ?? 0,
          effectiveQuantity: item.effectiveQuantity ?? 0,
          currentReturnQty: 0,
        }));
    } catch (e) {
      console.error("获取物料台账失败", e);
      ElMessage.error("获取物料台账失败");
      console.error("获取工序物料失败", e);
      ElMessage.error("获取工序物料失败");
    } finally {
      materialTableLoading.value = false;
    }
@@ -209,112 +247,144 @@
  );
  const handleCloseMaterialDialog = () => {
    materialTableData.value = [];
    currentMaterialRow.value = null;
    currentMaterialOrderRow.value = null;
    pickRows.value = [];
    pickedRows.value = [];
    currentWorkOrderRow.value = null;
  };
  const openSupplementDialog = row => {
    currentMaterialRow.value = row;
    supplementForm.supplementQty = null;
    supplementForm.supplementReason = "";
    supplementDialogVisible.value = true;
    nextTick(() => {
      supplementFormRef.value?.clearValidate();
    });
  const openProductDialog = () => {
    productDialogVisible.value = true;
  };
  const handleSubmitSupplement = () => {
    supplementFormRef.value?.validate(async valid => {
      if (!valid || !currentMaterialRow.value?.id) {
        ElMessage.warning("缺少物料明细ID");
  const handleProductConfirm = products => {
    const product = Array.isArray(products) ? products[0] : null;
    if (!product) return;
    const productModelId = product.id || product.productModelId || product.modelId;
    const exists = pickRows.value.some(
      item => String(item.productModelId || "") === String(productModelId || "")
    );
    if (exists) {
      ElMessage.warning("该物料已在领料列表中");
        return;
      }
      supplementSubmitting.value = true;
      try {
        await addWorkOrderMaterialSupplement({
          materialLedgerId: currentMaterialRow.value.id,
          supplementQty: Number(supplementForm.supplementQty),
          supplementReason: supplementForm.supplementReason,
          workOrderId: currentMaterialOrderRow.value?.id,
    pickRows.value.push({
      rowKey: createRowKey(),
      bom: false,
      productModelId,
      productName: product.productName || product.materialName || "",
      model: product.model || product.materialModel || "",
      unit: product.unit || "",
      operationName:
        currentWorkOrderRow.value?.processName ||
        currentWorkOrderRow.value?.operationName ||
        "",
      stockQuantity: product.stockQuantity ?? 0,
      batchNoList: product.batchNoList || [],
      batchNo: "",
      pickQuantity: 0,
        });
        supplementDialogVisible.value = false;
        await loadMaterialTable(currentMaterialOrderRow.value);
        ElMessage.success("补料成功");
  };
  const removePickRow = index => {
    pickRows.value.splice(index, 1);
  };
  const handleReturnMaterial = async row => {
    const returnQty = Number(row.currentReturnQty || 0);
    if (returnQty <= 0) {
      ElMessage.warning("请输入本次退料数量");
      return;
    }
    if (returnQty > Number(row.effectiveQuantity || 0)) {
      ElMessage.warning("退料数量不能超过净领料数量");
      return;
    }
    await ElMessageBox.confirm(
      `确认退回 ${row.productName || "该物料"} ${returnQty}${row.unit || ""}?`,
      "退料确认",
      { type: "warning" }
    );
    returningId.value = row.id;
    try {
      await returnWorkOrderMaterial({
        productionOrderId: row.productionOrderId,
        pickList: [
          {
            id: row.id,
            productionOrderId: row.productionOrderId,
            returned: true,
            returnQty,
          },
        ],
      });
      ElMessage.success("退料成功");
      await loadMaterialTable(currentWorkOrderRow.value);
        emit("refresh");
      } catch (e) {
        console.error("补料失败", e);
        ElMessage.error("补料失败");
      if (e !== "cancel" && e !== "close") {
        console.error("退料失败", e);
      }
      } finally {
        supplementSubmitting.value = false;
      returningId.value = null;
      }
    });
  };
  const openSupplementRecordDialog = async row => {
    supplementRecordDialogVisible.value = true;
    supplementRecordLoading.value = true;
    supplementRecordTableData.value = [];
    try {
      const res = await listWorkOrderMaterialSupplementRecord({
        materialLedgerId: row.id,
      });
      supplementRecordTableData.value = res.data || [];
    } catch (e) {
      console.error("获取补料记录失败", e);
      ElMessage.error("获取补料记录失败");
    } finally {
      supplementRecordLoading.value = false;
    }
  };
  const validatePickRows = () => {
    if (materialTableData.value.length === 0) {
      return { valid: false, message: "暂无可领用物料" };
    }
    const invalidRow = materialTableData.value.find(
      item =>
        item.actualQty === null ||
        item.actualQty === undefined ||
        item.actualQty === ""
    );
    if (invalidRow) {
      return { valid: false, message: "请填写实际数量后再领用" };
    }
    const exceedRow = materialTableData.value.find(item => {
      const maxQty = Number(item.pickQty || 0) + Number(item.supplementQty || 0);
      return Number(item.actualQty || 0) > maxQty;
    });
    if (exceedRow) {
      return { valid: false, message: "实际数量不能大于领用数量+补料数量" };
    }
    return { valid: true, message: "" };
  };
  const handleSubmitPick = async () => {
    if (!currentMaterialOrderRow.value?.id) return;
    const validateResult = validatePickRows();
    if (!validateResult.valid) {
      ElMessage.warning(validateResult.message);
    if (!currentWorkOrderRow.value?.id) return;
    const submitRows = pickRows.value.filter(
      item => Number(item.pickQuantity || 0) > 0
    );
    if (submitRows.length === 0) {
      ElMessage.warning("请至少填写一个物料的本次领料数量");
      return;
    }
    const missingBatchRow = submitRows.find(
      item => (item.batchNoList || []).length > 0 && !item.batchNo
    );
    if (missingBatchRow) {
      ElMessage.warning("请选择领料批号");
      return;
    }
    pickSubmitting.value = true;
    try {
      await pickWorkOrderMaterial({
        workOrderId: currentMaterialOrderRow.value.id,
        items: materialTableData.value.map(item => ({
          materialLedgerId: item.id,
          actualQty: Number(item.actualQty || 0),
        productionOperationTaskId: currentWorkOrderRow.value.id,
        pickList: submitRows.map(item => ({
          productModelId: item.productModelId,
          batchNo: item.batchNo || undefined,
          pickQuantity: Number(item.pickQuantity || 0),
        })),
      });
      ElMessage.success("领用成功");
      await loadMaterialTable(currentMaterialOrderRow.value);
      await loadMaterialTable(currentWorkOrderRow.value);
      emit("refresh");
    } catch (e) {
      console.error("领用失败", e);
      ElMessage.error("领用失败");
    } finally {
      pickSubmitting.value = false;
    }
  };
</script>
<style scoped>
  .material-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    color: #606266;
    font-size: 13px;
  }
  .material-section-title {
    margin: 16px 0 8px;
    color: #303133;
    font-size: 14px;
    font-weight: 600;
  }
</style>
src/views/productionManagement/workOrderManagement/index.vue
@@ -270,8 +270,8 @@
    productWorkOrderPage,
    addProductMain,
    downProductWorkOrder,
    checkWorkOrderMaterialPicked,
  } from "@/api/productionManagement/workOrder.js";
  import { listMaterialPickingDetail } from "@/api/productionManagement/productionOrder.js";
  import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js";
  import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
  import { getDicts } from "@/api/system/dict/data";
@@ -376,12 +376,13 @@
            openWorkOrderFiles(row);
          },
        },
        // {
        //   name: "物料",
        //   clickFun: row => {
        //     openMaterialDialog(row);
        //   },
        // },
        {
          name: "领料",
          clickFun: row => {
            openMaterialDialog(row);
          },
          showHide: row => !row.endOrder,
        },
        {
          name: "报工",
          clickFun: row => {
@@ -643,20 +644,26 @@
    fileDialogVisible.value = true;
  };
  const showReportDialog = async row => {
    if (row.productionOrderId) {
  // 仅用于即时提示:报工权限最终由后端在报工事务内校验当前工序是否已领料
  const checkCurrentProcessPicked = async row => {
      try {
        const res = await listMaterialPickingDetail(row.productionOrderId);
        const records = Array.isArray(res.data)
          ? res.data
          : res.data?.records || [];
        if (res.code === 200 && records.length === 0) {
          proxy.$modal.msgError("未领料无法报工");
          return;
      const res = await checkWorkOrderMaterialPicked(row.id);
      if (res.code === 200 && res.data !== true) {
        proxy.$modal.msgError("当前工序未领料,无法报工");
        return false;
        }
      return true;
      } catch (error) {
        console.error("查询领料详情失败:", error);
      console.error("查询当前工序领料失败:", error);
      proxy.$modal.msgError("查询当前工序领料失败");
      return false;
      }
  };
  const showReportDialog = async row => {
    const picked = await checkCurrentProcessPicked(row);
    if (!picked) {
      return;
    }
    currentReportRowData.value = row;
    reportForm.planQuantity = subtractQuantity(