gaoluyang
5 小时以前 04687ca035e6fa517e88470aac7247812f85eb95
Merge remote-tracking branch 'origin/dev_New' into dev_New
已修改11个文件
427 ■■■■■ 文件已修改
src/api/basicData/productModel.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/ProductSelectDialog.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/productModel.js
@@ -7,3 +7,11 @@
        params: query
    })
}
export function productModelListByUrl(url, query) {
    return request({
        url,
        method: 'get',
        params: query
    })
}
src/api/productionManagement/workOrder.js
@@ -37,7 +37,7 @@
// 工单-当前工序物料台账
export function listWorkOrderMaterialLedger(query) {
  return request({
    url: "/productWorkOrder/material/list",
    url: "/productOrderMaterial/reportMaterials",
    method: "get",
    params: query,
  });
@@ -69,3 +69,12 @@
    params: query,
  });
}
// 工单-领用(提交实际领用数量)
export function pickWorkOrderMaterial(data) {
  return request({
    url: "/productWorkOrder/material/pick",
    method: "post",
    data,
  });
}
src/views/basicData/product/ProductSelectDialog.vue
@@ -1,12 +1,12 @@
<template>
  <el-dialog v-model="visible" title="选择产品" width="900px" destroy-on-close :close-on-click-modal="false">
    <el-form :inline="true" :model="query" class="mb-2">
      <el-form-item label="产品大类">
        <el-input v-model="query.productName" placeholder="输入产品大类" clearable @keyup.enter="onSearch" />
      <el-form-item label="产品名称">
        <el-input v-model="query.productName" placeholder="输入产品名称" clearable @keyup.enter="onSearch" />
      </el-form-item>
      <el-form-item label="型号名称">
        <el-input v-model="query.model" placeholder="输入型号名称" clearable @keyup.enter="onSearch" />
      <el-form-item label="产品型号">
        <el-input v-model="query.model" placeholder="输入产品型号" clearable @keyup.enter="onSearch" />
      </el-form-item>
      <el-form-item>
@@ -20,8 +20,8 @@
      @selection-change="handleSelectionChange" @select="handleSelect">
      <el-table-column type="selection" width="55" />
      <el-table-column type="index" label="序号" width="60" />
      <el-table-column prop="productName" label="产品大类" min-width="160" />
      <el-table-column prop="model" label="型号名称" min-width="200" />
      <el-table-column prop="productName" label="产品名称" min-width="160" />
      <el-table-column prop="model" label="产品型号" min-width="200" />
      <el-table-column prop="unit" label="单位" min-width="160" />
    </el-table>
@@ -43,7 +43,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { productModelList } from '@/api/basicData/productModel'
import { productModelList, productModelListByUrl } from '@/api/basicData/productModel'
export type ProductRow = {
  id: number;
@@ -56,6 +56,7 @@
  modelValue: boolean;
  single?: boolean; // 是否只能选择一个,默认false(可选择多个)
  topProductParentId?: number; // 一级产品id
  requestUrl?: string; // 自定义查询接口
}>();
const emit = defineEmits(['update:modelValue', 'confirm']);
@@ -155,15 +156,19 @@
  loading.value = true;
  try {
    multipleSelection.value = []; // 翻页/搜索后清空选择更符合预期
    const res: any = await productModelList({
    const params = {
      productName: query.productName.trim(),
      model: query.model.trim(),
      current: page.pageNum,
      size: page.pageSize,
      topProductParentId: props.topProductParentId,
    });
    tableData.value = res.records;
    total.value = res.total;
    };
    const res: any = props.requestUrl
      ? await productModelListByUrl(props.requestUrl, params)
      : await productModelList(params);
    const records = res?.records || res?.data?.records || res?.data || [];
    tableData.value = Array.isArray(records) ? records : [];
    total.value = Number(res?.total ?? res?.data?.total ?? tableData.value.length);
  } finally {
    loading.value = false;
  }
src/views/productionManagement/processRoute/index.vue
@@ -171,6 +171,7 @@
    path: '/productionManagement/processRouteItem',
    query: {
      id: row.id,
      bomId: row.bomId,
      processRouteCode: row.processRouteCode || '',
      productName: row.productName || '',
      model: row.model || '',
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -47,7 +47,13 @@
        </div>
      </div>
    </el-card>
    <div class="section-title" style="margin-bottom: 10px;">产品结构</div>
    <ProductStructureDetail
      class="product-structure-panel"
      style="margin-bottom: 20px;"
      embedded
      :bom-id="route.query.bomId"
    />
    <!-- 表格视图 -->
    <div v-if="viewMode === 'table'" class="section-header">
      <div class="section-title">工艺路线项目列表</div>
@@ -231,7 +237,7 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue";
import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick, defineAsyncComponent } from "vue";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
import { findProcessRouteItemList, addOrUpdateProcessRouteItem, sortProcessRouteItem, batchDeleteProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
import { findProductProcessRouteItemList, deleteRouteItem, addRouteItem, addOrUpdateProductProcessRouteItem, sortRouteItem } from "@/api/productionManagement/productProcessRoute.js";
@@ -242,6 +248,7 @@
const route = useRoute()
const { proxy } = getCurrentInstance() || {};
const ProductStructureDetail = defineAsyncComponent(() => import("@/views/productionManagement/productStructure/Detail/index.vue"));
const routeId = computed(() => route.query.id);
const orderId = computed(() => route.query.orderId);
@@ -841,6 +848,10 @@
  align-items: center;
}
.product-structure-panel {
  margin: 12px 0 20px;
}
/* 工艺路线信息卡片样式 */
.route-info-card {
  margin-bottom: 20px;
src/views/productionManagement/productStructure/Detail/index.vue
@@ -1,6 +1,6 @@
<template>
  <div class="app-container">
    <PageHeader content="产品结构详情">
  <div :class="embedded ? 'embedded-container' : 'app-container'">
    <PageHeader v-if="!embedded" content="产品结构详情">
      <template #right-button>
        <el-button v-if="!dataValue.isEdit && !isOrderPage"
                   type="primary"
@@ -119,7 +119,7 @@
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column label="操作"
              <el-table-column v-if="!embedded" label="操作"
                               fixed="right"
                               width="200">
                <template #default="{ row, $index }">
@@ -174,6 +174,18 @@
  const ProductSelectDialog = defineAsyncComponent(
    () => import("@/views/basicData/product/ProductSelectDialog.vue")
  );
  const props = defineProps({
    embedded: {
      type: Boolean,
      default: false,
    },
    // 显式指定BOM主键(用于嵌入到“工艺路线项目”等页面时,路由 query.id 不是 bomId 的情况)
    bomId: {
      type: [String, Number],
      default: undefined,
    },
  });
  const embedded = computed(() => props.embedded);
  const emit = defineEmits(["update:router"]);
  const form = ref();
@@ -181,7 +193,8 @@
  const router = useRouter();
  const routeId = computed({
    get() {
      return route.query.id;
      // 优先使用外部传入的 bomId,其次使用路由的 bomId,最后回退到路由的 id(兼容原页面)
      return props.bomId ?? route.query.bomId ?? route.query.id;
    },
    set(val) {
@@ -227,29 +240,27 @@
  };
  const fetchData = async () => {
    if (isOrderPage.value) {
      // 订单情况:使用订单的产品结构接口
      const { data } = await listProcessBom({ orderId: routeOrderId.value });
      dataValue.dataList = (data as any) || [];
    } else {
      // 非订单情况:使用原来的接口
      const { data } = await queryList(routeId.value);
      dataValue.dataList = (data as any) || [];
      // 为所有项及其子项设置name属性
      const setNameRecursively = (items: any[]) => {
        items.forEach((item: any) => {
          item.tempId = item.id;
        item.tempId = item.tempId || item.id || new Date().getTime() + Math.random();
          item.processName =
            dataValue.processOptions.find(option => option.id === item.processId)
              ?.name || "";
          dataValue.processOptions.find(option => option.id === item.processId)?.name || item.processName || "";
          if (item.children && item.children.length > 0) {
            setNameRecursively(item.children);
          }
        });
      };
      setNameRecursively(dataValue.dataList);
      console.log(dataValue.dataList, "dataValue.dataList");
    // 统一使用 BOM 查询产品结构:/productStructure/listBybomId/{bomId}
    // 说明:订单页也会从路由/父组件带入 bomId(route.query.bomId 或 props.bomId)
    const bomId = routeId.value;
    if (!bomId) {
      dataValue.dataList = [];
      return;
    }
    const { data } = await queryList(bomId);
    dataValue.dataList = (data as any) || [];
    setNameRecursively(dataValue.dataList);
  };
  const fetchProcessOptions = async () => {
@@ -519,3 +530,10 @@
    await fetchData();
  });
</script>
<style scoped>
.embedded-container {
  padding: 0;
  margin: 0;
}
</style>
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -36,8 +36,9 @@
    <el-dialog v-model="supplementRecordDialogVisible" title="补料记录" width="800px">
      <el-table v-loading="supplementRecordLoading" :data="supplementRecordTableData" border row-key="id">
        <el-table-column label="补料数量" prop="supplementQty" min-width="120" />
        <el-table-column label="补料时间" prop="supplementTime" min-width="180" />
        <el-table-column label="备注" prop="remark" min-width="200" />
        <el-table-column label="补料人" prop="supplementUserName" min-width="120" />
        <el-table-column label="补料日期" prop="supplementTime" min-width="160" />
        <el-table-column label="补料原因" prop="supplementReason" min-width="200" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
@@ -88,8 +89,10 @@
const supplementRecordTableData = ref([]);
const returnSummaryDialogVisible = ref(false);
const returnSummaryList = ref([]);
const calcReturnQty = item =>
  Number(item.pickQty || 0) + Number(item.supplementQty || 0) - Number(item.actualQty || 0);
const canOpenReturnSummary = computed(() =>
  materialDetailTableData.value.some(item => Number(item.returnQty || 0) > 0)
  materialDetailTableData.value.some(item => calcReturnQty(item) > 0)
);
const loadDetailList = async () => {
@@ -133,6 +136,8 @@
const buildReturnSummary = () => {
  const map = new Map();
  materialDetailTableData.value.forEach(item => {
    const returnQty = calcReturnQty(item);
    if (returnQty <= 0) return;
    const key = `${item.materialModelId || ""}_${item.materialName || ""}_${item.materialModel || ""}_${item.unit || ""}`;
    const old = map.get(key) || {
      summaryKey: key,
@@ -141,7 +146,7 @@
      unit: item.unit || "",
      returnQtyTotal: 0,
    };
    old.returnQtyTotal += Number(item.returnQty || 0);
    old.returnQtyTotal += returnQty;
    map.set(key, old);
  });
  return Array.from(map.values());
@@ -149,7 +154,7 @@
const openReturnSummaryDialog = async () => {
  if (!canOpenReturnSummary.value) {
    ElMessage.warning("退料数量大于0时才能退料确认");
    ElMessage.warning("退料数量=领用数量+补料数量-实际数量,且需大于0");
    return;
  }
  returnSummaryList.value = buildReturnSummary();
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -7,21 +7,24 @@
      <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-model="row.processId"
              v-else
              v-model="row.processName"
              placeholder="请选择工序"
              clearable
              filterable
              style="width: 100%;"
              @change="val => handleProcessChange(row, val)"
              @change="val => handleProcessNameChange(row, val)"
            >
              <el-option v-for="item in processOptions" :key="item.id" :label="item.name" :value="item.id" />
              <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 }">
            <el-button type="primary" link @click="openMaterialProductSelect(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>
@@ -33,7 +36,9 @@
        </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"
@@ -62,8 +67,8 @@
          </template>
        </el-table-column>
        <el-table-column label="操作" width="90" fixed="right">
          <template #default="{ $index }">
            <el-button type="danger" link @click="handleDeleteMaterialRow($index)">删除</el-button>
          <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>
@@ -79,15 +84,21 @@
      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 { processList } from "@/api/productionManagement/productionProcess.js";
import { listMaterialPickingLedger, saveMaterialPickingLedger } from "@/api/productionManagement/productionOrder.js";
import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js";
import {
  listMaterialPickingDetail,
  listMaterialPickingLedger,
  saveMaterialPickingLedger,
} from "@/api/productionManagement/productionOrder.js";
const props = defineProps({
  modelValue: { type: Boolean, default: false },
@@ -112,7 +123,9 @@
  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 || "",
@@ -122,9 +135,23 @@
});
const getProcessOptions = async () => {
  if (processOptions.value.length > 0) return;
  const res = await processList({});
  processOptions.value = res.data || [];
  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 () => {
@@ -133,8 +160,19 @@
  materialTableData.value = [];
  await getProcessOptions();
  try {
    const res = await listMaterialPickingLedger({ orderId: props.orderRow.id });
    materialTableData.value = (res.data || []).map(item => createMaterialRow(item));
    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;
  }
@@ -162,9 +200,9 @@
  materialTableData.value.splice(index, 1);
};
const handleProcessChange = (row, processId) => {
  const process = processOptions.value.find(item => item.id === processId);
  row.processName = process?.name || "";
const handleProcessNameChange = (row, processName) => {
  const process = processOptions.value.find(item => item.name === processName);
  row.productProcessId = process?.id;
};
const handleRequiredQtyChange = (row, val) => {
@@ -186,37 +224,56 @@
  if (index < 0 || !materialTableData.value[index]) return;
  const product = products[0];
  const row = materialTableData.value[index];
  row.materialModelId = product.id;
  row.materialName = product.productName || "";
  row.materialModel = product.model || "";
  row.unit = product.unit || "";
  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 false;
  return !materialTableData.value.find(
  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.processId ||
      !item.materialModelId ||
      !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 || !validateMaterialRows()) return;
  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.processId,
        processId: item.processName,
        productProcessId: item.productProcessId,
        processName: item.processName,
        bom: item.bom === true,
        materialModelId: item.materialModelId,
        materialName: item.materialName,
        materialModel: item.materialModel,
src/views/productionManagement/productionOrder/index.vue
@@ -48,7 +48,7 @@
                     @click="handleQuery">搜索</el-button>
        </el-form-item>
      </el-form>
      <div>
      <div class="action-buttons">
        <el-button type="primary" @click="isShowNewModal = true">新增</el-button>
        <el-button type="danger" @click="handleDelete">删除</el-button>
        <el-button @click="handleOut">导出</el-button>
@@ -224,13 +224,13 @@
            openBindRouteDialog(row);
          },
        },
        {
          name: "产品结构",
          type: "text",
          clickFun: row => {
            showProductStructure(row);
          },
        },
        // {
        //   name: "产品结构",
        //   type: "text",
        //   clickFun: row => {
        //     showProductStructure(row);
        //   },
        // },
        {
          name: "领料",
          type: "text",
@@ -421,6 +421,7 @@
        path: "/productionManagement/processRouteItem",
        query: {
          id: data.id,
          bomId: data.bomId,
          processRouteCode: data.processRouteCode || "",
          productName: data.productName || "",
          model: data.model || "",
@@ -504,6 +505,12 @@
  align-items: start;
}
.action-buttons {
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
}
:deep(.yellow) {
  background-color: #FAF0DE;
}
src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue
@@ -11,18 +11,33 @@
        <el-table-column label="原料名称" prop="materialName" min-width="140" />
        <el-table-column label="原料型号" prop="materialModel" 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="pickQty" min-width="100" />
        <el-table-column label="补料数量" prop="supplementQty" min-width="100" />
        <el-table-column label="退料数量" prop="returnQty" min-width="100" />
        <el-table-column label="实际数量" prop="actualQty" min-width="100" />
        <el-table-column label="操作" align="center" fixed="right" width="220">
        <el-table-column label="实际数量" min-width="140">
          <template #default="{ row }">
            <el-input-number
              v-model="row.actualQty"
              :min="0"
              :precision="3"
              :step="1"
              controls-position="right"
              style="width: 100%;"
            />
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" fixed="right" width="180">
          <template #default="{ row }">
            <el-button type="primary" link @click="openSupplementDialog(row)">补料</el-button>
            <el-button type="warning" link @click="openReturnDialog(row)">退料</el-button>
            <el-button type="info" link @click="openSupplementRecordDialog(row)">补料记录</el-button>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" :loading="pickSubmitting" @click="handleSubmitPick">领用</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    <FormDialog
@@ -60,31 +75,6 @@
      </template>
    </FormDialog>
    <FormDialog
      v-model="returnDialogVisible"
      title="退料"
      width="500px"
      @confirm="handleSubmitReturn"
    >
      <el-form ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="120px">
        <el-form-item label="退料数量" prop="returnQty">
          <el-input-number
            v-model="returnForm.returnQty"
            :min="0.001"
            :precision="3"
            :step="1"
            style="width: 100%;"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" :loading="returnSubmitting" @click="handleSubmitReturn">确定</el-button>
          <el-button @click="returnDialogVisible = 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" />
@@ -108,8 +98,8 @@
import {
  listWorkOrderMaterialLedger,
  addWorkOrderMaterialSupplement,
  addWorkOrderMaterialReturn,
  listWorkOrderMaterialSupplementRecord,
  pickWorkOrderMaterial,
} from "@/api/productionManagement/workOrder.js";
const props = defineProps({
@@ -134,6 +124,7 @@
const materialTableData = ref([]);
const currentMaterialRow = ref(null);
const currentMaterialOrderRow = ref(null);
const pickSubmitting = ref(false);
const supplementDialogVisible = ref(false);
const supplementSubmitting = ref(false);
@@ -141,13 +132,6 @@
const supplementForm = reactive({
  supplementQty: null,
  supplementReason: "",
});
const returnDialogVisible = ref(false);
const returnSubmitting = ref(false);
const returnFormRef = ref(null);
const returnForm = reactive({
  returnQty: null,
});
const supplementRecordDialogVisible = ref(false);
@@ -158,10 +142,6 @@
  supplementQty: [{ required: true, message: "请输入补料数量", trigger: "blur" }],
  supplementReason: [{ required: true, message: "请输入补料原因", trigger: "blur" }],
};
const returnRules = {
  returnQty: [{ required: true, message: "请输入退料数量", trigger: "blur" }],
};
const loadMaterialTable = async row => {
  if (!row?.id) return;
  currentMaterialOrderRow.value = row;
@@ -234,49 +214,6 @@
  });
};
const openReturnDialog = row => {
  currentMaterialRow.value = row;
  returnForm.returnQty = null;
  returnDialogVisible.value = true;
  nextTick(() => {
    returnFormRef.value?.clearValidate();
  });
};
const handleSubmitReturn = () => {
  returnFormRef.value?.validate(async valid => {
    if (!valid || !currentMaterialRow.value?.id) {
      ElMessage.warning("缺少物料明细ID");
      return;
    }
    const returnQty = Number(returnForm.returnQty);
    const minQty =
      Number(currentMaterialRow.value.pickQty || 0) +
      Number(currentMaterialRow.value.supplementQty || 0);
    if (returnQty < minQty) {
      ElMessage.warning(`退料数量不能低于领用数量+补料数量(${minQty})`);
      return;
    }
    returnSubmitting.value = true;
    try {
      await addWorkOrderMaterialReturn({
        materialLedgerId: currentMaterialRow.value.id,
        returnQty,
        workOrderId: currentMaterialOrderRow.value?.id,
      });
      returnDialogVisible.value = false;
      await loadMaterialTable(currentMaterialOrderRow.value);
      ElMessage.success("退料成功");
      emit("refresh");
    } catch (e) {
      console.error("退料失败", e);
      ElMessage.error("退料失败");
    } finally {
      returnSubmitting.value = false;
    }
  });
};
const openSupplementRecordDialog = async row => {
  supplementRecordDialogVisible.value = true;
  supplementRecordLoading.value = true;
@@ -293,4 +230,49 @@
    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);
    return;
  }
  pickSubmitting.value = true;
  try {
    await pickWorkOrderMaterial({
      workOrderId: currentMaterialOrderRow.value.id,
      items: materialTableData.value.map(item => ({
        materialLedgerId: item.id,
        actualQty: Number(item.actualQty || 0),
      })),
    });
    ElMessage.success("领用成功");
    await loadMaterialTable(currentMaterialOrderRow.value);
    emit("refresh");
  } catch (e) {
    console.error("领用失败", e);
    ElMessage.error("领用失败");
  } finally {
    pickSubmitting.value = false;
  }
};
</script>
src/views/productionManagement/workOrderManagement/index.vue
@@ -289,17 +289,17 @@
          },
        },
        {
          name: "物料",
          clickFun: row => {
            openMaterialDialog(row);
          },
        },
        {
          name: "报工",
          clickFun: row => {
            showReportDialog(row);
          },
          disabled: row => row.planQuantity <= 0,
        },
        {
          name: "物料",
          clickFun: row => {
            openMaterialDialog(row);
          },
        },
      ],
    },