src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -30,14 +30,6 @@
            <span class="info-value">{{ routeInfo.model || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">BOM编号</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.bomNo || '-' }}</span>
          </div>
        </div>
        <div class="info-item"
             v-if="routeInfo.quantity && routeInfo.quantity !== 0">
          <div class="info-label-wrapper">
@@ -253,164 +245,6 @@
        </div>
      </div>
    </template>
    <!-- bom模块 -->
    <div class="section-header"
         style="margin-top: 20px;">
      <div class="section-title">BOM 结构</div>
      <div class="section-actions"
           v-if="pageType === 'order' && editable">
        <el-button v-if="!bomDataValue.isEdit"
                   type="primary"
                   @click="bomDataValue.isEdit = true">
          编辑
        </el-button>
        <el-button v-if="bomDataValue.isEdit"
                   @click="cancelEditBom">
          取消
        </el-button>
        <el-button v-if="bomDataValue.isEdit"
                   type="primary"
                   @click="handleSaveBom"
                   :loading="bomDataValue.loading">
          保存BOM
        </el-button>
      </div>
    </div>
    <el-table :data="bomTableData"
              border
              :preserve-expanded-content="false"
              :default-expand-all="true"
              style="width: 100%">
      <el-table-column type="expand">
        <template #default>
          <el-form ref="bomFormRef"
                   :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="产品" />
              <el-table-column prop="model"
                               label="规格">
                <template #default="{ row }">
                  <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
                                :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-select v-model="row.model"
                               placeholder="请选择规格"
                               clearable
                               :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)"
                               style="width: 100%"
                               @visible-change="(v) => { if (v) openBomDialog(row.tempId) }">
                      <el-option v-if="row.model"
                                 :label="row.model"
                                 :value="row.model" />
                    </el-select>
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="processName"
                               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' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
                               filterable
                               clearable
                               style="width: 100%"
                               @change="value => handleBomProcessChange(row, value)"
                               :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)">
                      <el-option v-for="item in bomDataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
                                 :value="item.id" />
                    </el-select>
                  </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"
                                     :precision="2"
                                     :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"
                                     :precision="2"
                                     :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"
                               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"
                             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>
            </el-table>
          </el-form>
        </template>
      </el-table-column>
      <el-table-column label="BOM编号"
                       prop="bomNo" />
      <el-table-column label="产品名称"
                       prop="productName" />
      <el-table-column label="规格型号"
                       prop="model" />
    </el-table>
    <ProductSelectDialog v-if="bomDataValue.showProductDialog"
                         v-model="bomDataValue.showProductDialog"
                         :single="true"
                         @confirm="handleBomProduct" />
    <!-- 上传组件弹窗 -->
    <el-dialog v-model="uploadDialogVisible"
               title="上传附件"
@@ -553,12 +387,6 @@
    sortRouteItem,
  } from "@/api/productionManagement/productProcessRoute.js";
  import { processList } from "@/api/productionManagement/productionProcess.js";
  import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
  import {
    queryList,
    queryList2,
    add2,
  } from "@/api/productionManagement/productStructure.js";
  import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue";
  import {
    attachmentList,
@@ -584,7 +412,6 @@
  const dialogVisible = ref(false);
  const operationType = ref("add"); // add | edit
  const formRef = ref(null);
  const bomFormRef = ref(null);
  const submitLoading = ref(false);
  const cardsContainer = ref(null);
  const tableRef = ref(null);
@@ -593,7 +420,6 @@
    processRouteCode: "",
    productName: "",
    model: "",
    bomNo: "",
    description: "",
    quantity: 0,
    technologyRoutingId: "",
@@ -770,7 +596,6 @@
    processList({ size: -1, current: -1 })
      .then(res => {
        processOptions.value = res.data.records || [];
        bomDataValue.value.processOptions = processOptions.value;
      })
      .catch(err => {
        console.error("获取工序失败:", err);
@@ -783,16 +608,11 @@
      processRouteCode: route.query.processRouteCode || "",
      productName: route.query.productName || "",
      model: route.query.model || "",
      bomNo: route.query.bomNo || "",
      bomId: route.query.bomId || "",
      description: route.query.description || "",
      quantity: route.query.quantity || 0,
      technologyRoutingId: route.query.technologyRoutingId || "",
      status: !(route.query.status == 1 || route.query.status === "false"),
    };
    bomTableData.value[0].productName = routeInfo.value.productName;
    bomTableData.value[0].model = routeInfo.value.model;
    bomTableData.value[0].bomNo = routeInfo.value.bomNo;
  };
  // 新增
@@ -1147,103 +967,6 @@
    }
  };
  // BOM相关状态和方法
  const bomTableData = ref([
    {
      productName: "",
      model: "",
      bomNo: "",
    },
  ]);
  const bomDataValue = ref({
    dataList: [],
    processOptions: [],
    showProductDialog: false,
    currentRowName: null,
    loading: false,
    isEdit: false,
  });
  const syncProcessOperationFields = item => {
    const processId =
      item.processId ?? item.operationId ?? item.technologyOperationId ?? "";
    if (!processId) {
      item.processId = "";
      return;
    }
    const option = bomDataValue.value.processOptions.find(
      p => p.id === processId
    );
    const processName =
      option?.name || item.processName || item.operationName || "";
    item.processId = processId;
    if (pageType.value === "order") {
      item.technologyOperationId = processId;
    } else {
      item.operationId = processId;
    }
    item.processName = processName;
    item.operationName = processName;
  };
  const normalizeTreeData = 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(2));
  };
  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 => {
    processOptions.value.forEach(item => {
      if (item.id == value) {
@@ -1254,340 +977,10 @@
    });
  };
  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 => {
    bomDataValue.value.currentRowName = tempId;
    bomDataValue.value.showProductDialog = true;
  };
  const fetchBomData = async () => {
    try {
      const isOrderPage = pageType.value === "order";
      const { data } = await (isOrderPage ? queryList2 : queryList)(
        routeInfo.value.bomId
      );
      bomDataValue.value.dataList = data || [];
      normalizeTreeData(bomDataValue.value.dataList);
      recalculateDemandedQuantities();
    } 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 => {
    if (!Array.isArray(row) || row.length === 0) {
      ElMessage.warning("请选择一个产品");
      return;
    }
    const productData = row[row.length - 1];
    const isTopLevel = bomDataValue.value.dataList.some(
      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;
        }
      }
    }
    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(
      item => item.tempId === tempId
    );
    if (topIndex !== -1) {
      bomDataValue.value.dataList.splice(topIndex, 1);
      return;
    }
    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 || "",
        productName: "",
        productId: "",
        model: undefined,
        productModelId: undefined,
        processId: "",
        processName: "",
        [pageType.value === "order" ? "technologyOperationId" : "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 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);
    });
  };
  const validateAllBom = () => {
    let isValid = true;
    const isOrderPage = pageType.value === "order";
    const validateItem = (item, isTopLevel = false) => {
      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 && !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;
        }
      }
      // 递归检查子级
      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;
    });
  };
  const cancelEditBom = () => {
    bomDataValue.value.isEdit = false;
    fetchBomData();
  };
  const handleSaveBom = () => {
    bomDataValue.value.loading = true;
    console.log(bomDataValue.value.dataList, "bomDataValue.value.dataList");
    normalizeTreeData(bomDataValue.value.dataList);
    recalculateDemandedQuantities();
    const valid = validateAllBom();
    if (valid) {
      add2({
        // bomId: Number(routeInfo.value.bomId),
        productionOrderBomId: Number(routeInfo.value.bomId) || null,
        children: buildSubmitTree(bomDataValue.value.dataList || []),
      })
        .then(() => {
          ElMessage.success("BOM保存成功");
          bomDataValue.value.isEdit = false;
          refreshCurrentPage();
        })
        .catch(() => {
          ElMessage.error("BOM保存失败");
        })
        .finally(() => {
          bomDataValue.value.loading = false;
        });
    } else {
      bomDataValue.value.loading = false;
    }
  };
  const refreshCurrentPage = () => {
    getRouteInfo();
    getList();
    getProcessList();
    fetchBomData();
    if (pageType.value === "order") {
      getAttachmentList();
    }