From 2fd817ab50faa08111ce6e64c6d22a54807d08a4 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 09 六月 2026 15:16:48 +0800
Subject: [PATCH] pro 1.bom编辑页面样式优化

---
 src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue |  224 ++++++++++++++
 src/views/productionManagement/productStructure/DetailNew/index.vue        |  617 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 841 insertions(+), 0 deletions(-)

diff --git a/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue b/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue
new file mode 100644
index 0000000..038d3e8
--- /dev/null
+++ b/src/views/productionManagement/productStructure/DetailNew/MaterialCard.vue
@@ -0,0 +1,224 @@
+<template>
+  <div class="material-node">
+    <!-- 褰撳墠鑺傜偣鍗$墖 -->
+    <div :class="['node-card', isRoot ? 'root-card' : 'child-card']">
+      <div class="node-header">
+        <div class="node-label">
+          <el-tag :type="isRoot ? '' : 'success'" size="small" effect="dark">
+            {{ isRoot ? '鎴愬搧' : '鍘熸枡' }}
+          </el-tag>
+          <span class="node-title">{{ row.productName || '鏈�夋嫨浜у搧' }}</span>
+          <span v-if="row.model" class="node-sub">瑙勬牸: {{ row.model }}</span>
+          <span v-if="row.unit" class="node-sub">鍗曚綅: {{ row.unit }}</span>
+        </div>
+        <div class="node-actions">
+          <el-button v-if="editable"
+                     type="primary"
+                     text
+                     size="small"
+                     @click="handleAdd">
+            + 娣诲姞{{ isRoot ? '鍘熸枡' : '瀛愮骇鍘熸枡' }}
+          </el-button>
+          <el-button v-if="editable"
+                     type="danger"
+                     text
+                     size="small"
+                     @click="$emit('remove', row.tempId)">
+            鍒犻櫎
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 缂栬緫妯″紡涓嬬殑琛ㄥ崟 -->
+      <div v-if="editable" class="node-body">
+        <el-row :gutter="12">
+          <el-col :span="7">
+            <el-form-item label="浜у搧" :rules="[{ required: true, message: '璇烽�夋嫨浜у搧' }]" style="margin:0">
+              <el-input :model-value="row.productName || ''"
+                        readonly
+                        placeholder="鐐瑰嚮閫夋嫨浜у搧"
+                        @click="openSelect"
+                        style="width:100%">
+                <template #suffix>
+                  <el-icon><component :is="SearchIcon" /></el-icon>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="5">
+            <el-form-item label="瑙勬牸" style="margin:0">
+              <el-select v-model="row.model"
+                         placeholder="璇烽�夋嫨瑙勬牸"
+                         clearable
+                         style="width:100%"
+                         @visible-change="(v:boolean) => { if (v) openSelect() }">
+                <el-option v-if="row.model" :label="row.model" :value="row.model" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col v-if="!isRoot" :span="5">
+            <el-form-item label="宸ュ簭" :rules="[{ required: true, message: '璇烽�夋嫨宸ュ簭' }]" style="margin:0">
+              <el-select v-model="row.processId"
+                         placeholder="璇烽�夋嫨"
+                         filterable
+                         clearable
+                         style="width:100%"
+                         @change="(v:any) => $emit('processChange', row, v)">
+                <el-option v-for="item in processOptions"
+                           :key="item.id"
+                           :label="item.name"
+                           :value="item.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item label="鏁伴噺" :rules="[{ required: true, message: '璇峰~鍐欐暟閲�' }]" style="margin:0">
+              <el-input-number v-model="row.unitQuantity"
+                               :min="0"
+                               :precision="2"
+                               :step="1"
+                               controls-position="right"
+                               style="width:100%"
+                               @change="$emit('quantityChange')" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="3">
+            <el-form-item label="鍗曚綅" style="margin:0">
+              <el-input v-model="row.unit" placeholder="鍗曚綅" clearable style="width:100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+
+      <!-- 闈炵紪杈戞ā寮忥細绠�娲佹樉绀� -->
+      <div v-else class="node-view">
+        <span v-if="!isRoot && row.processName">宸ュ簭: {{ row.processName }} | </span>
+        <span>鏁伴噺: {{ row.unitQuantity || '-' }}</span>
+      </div>
+    </div>
+
+    <!-- 閫掑綊娓叉煋瀛愯妭鐐� -->
+    <div v-if="row.children && row.children.length > 0" class="node-children">
+      <MaterialCard
+        v-for="child in row.children"
+        :key="child.tempId"
+        :row="child"
+        :depth="depth + 1"
+        :editable="editable"
+        :process-options="processOptions"
+        @remove="(id:string) => $emit('remove', id)"
+        @add="(id:string) => $emit('add', id)"
+        @select-product="(tempId: string, data: any) => $emit('selectProduct', tempId, data)"
+        @process-change="(row: any, v: any) => $emit('processChange', row, v)"
+        @quantity-change="$emit('quantityChange')"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+
+const SearchIcon = Search
+
+const props = defineProps<{
+  row: any
+  depth: number
+  editable: boolean
+  processOptions: any[]
+}>()
+
+const emit = defineEmits<{
+  remove: [tempId: string]
+  add: [tempId: string]
+  selectProduct: [tempId: string, data: any]
+  processChange: [row: any, value: any]
+  quantityChange: []
+}>()
+
+const isRoot = computed(() => props.depth === 0)
+
+const openSelect = () => {
+  emit('selectProduct', props.row.tempId, null)
+}
+
+const handleAdd = () => {
+  emit('add', props.row.tempId)
+}
+</script>
+
+<script lang="ts">
+export default { name: 'MaterialCard' }
+</script>
+
+<style scoped>
+.material-node {
+  margin: 4px 0;
+}
+
+.node-card {
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.root-card {
+  border-color: #409eff;
+  border-left: 4px solid #409eff;
+  background-color: #f0f5ff;
+}
+
+.child-card {
+  border-left: 4px solid #67c23a;
+  background-color: #f0f9eb;
+}
+
+.node-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 12px;
+  background-color: rgba(0,0,0,0.03);
+  flex-wrap: wrap;
+  gap: 4px;
+}
+
+.node-label {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.node-title {
+  font-weight: 600;
+  color: #303133;
+}
+
+.node-sub {
+  font-size: 12px;
+  color: #909399;
+}
+
+.node-actions {
+  display: flex;
+  gap: 4px;
+}
+
+.node-body {
+  padding: 10px 12px;
+}
+
+.node-view {
+  padding: 6px 12px;
+  font-size: 13px;
+  color: #606266;
+}
+
+.node-children {
+  margin-left: 36px;
+  padding-left: 16px;
+  border-left: 2px dashed #dcdfe6;
+}
+</style>
diff --git a/src/views/productionManagement/productStructure/DetailNew/index.vue b/src/views/productionManagement/productStructure/DetailNew/index.vue
new file mode 100644
index 0000000..33b31f7
--- /dev/null
+++ b/src/views/productionManagement/productStructure/DetailNew/index.vue
@@ -0,0 +1,617 @@
+<template>
+  <div class="app-container">
+    <PageHeader content="浜у搧缁撴瀯璇︽儏">
+      <template #right-button>
+        <el-button v-if="!dataValue.isEdit && !isOrderPage"
+                   type="primary"
+                   @click="dataValue.isEdit = true">缂栬緫
+        </el-button>
+        <el-button v-if="dataValue.isEdit && !isOrderPage"
+                   type="primary"
+                   @click="cancelEdit">鍙栨秷
+        </el-button>
+        <el-button v-if="!isOrderPage"
+                   type="primary"
+                   :loading="dataValue.loading"
+                   @click="submit"
+                   :disabled="!dataValue.isEdit">纭
+        </el-button>
+      </template>
+    </PageHeader>
+    <el-table :data="tableData"
+              border
+              :preserve-expanded-content="false"
+              :default-expand-all="true"
+              style="width: 100%">
+      <el-table-column type="expand">
+        <template #default="props">
+          <el-form ref="form" :model="dataValue">
+            <div class="tree-container">
+              <div class="tree-legend">
+                <el-tag type="" size="small" effect="dark">鎴愬搧</el-tag>
+                <span style="margin:0 4px">鈫� 鏈�涓婂眰锛堜骇鍑虹墿锛�</span>
+                <el-divider direction="vertical" />
+                <span style="margin:0 4px">鏈�涓嬪眰锛堟姇鍏ョ墿锛夆啋</span>
+                <el-tag type="success" size="small" effect="dark">鍘熸枡</el-tag>
+              </div>
+
+              <div v-if="dataValue.dataList.length === 0 && dataValue.isEdit" class="empty-hint">
+                璇风偣鍑讳笅鏂规寜閽坊鍔犳垚鍝�
+              </div>
+
+              <MaterialCard
+                v-for="(item, index) in dataValue.dataList"
+                :key="item.tempId"
+                :row="item"
+                :depth="0"
+                :editable="dataValue.isEdit"
+                :process-options="dataValue.processOptions"
+                @remove="(id: string) => removeItem(id)"
+                @add="(id: string) => addChildItem(id)"
+                @select-product="(tempId: string, _data: any) => { dataValue.currentRowName = tempId; dataValue.showProductDialog = true }"
+                @process-change="(row: any, v: any) => handleProcessChange(row, v)"
+                @quantity-change="handleUnitQuantityChange"
+              />
+
+              <el-button v-if="dataValue.isEdit"
+                         type="primary"
+                         plain
+                         style="margin-top:12px"
+                         @click="addRootItem">
+                + 娣诲姞鎴愬搧
+              </el-button>
+            </div>
+          </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>
+    <product-select-dialog v-if="dataValue.showProductDialog"
+                           v-model:model-value="dataValue.showProductDialog"
+                           :single="true"
+                           @confirm="handleProduct" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import {
+    computed,
+    defineAsyncComponent,
+    defineComponent,
+    onMounted,
+    reactive,
+    ref,
+  } from "vue";
+  import {
+    queryList,
+    addBomDetail,
+  } from "@/api/productionManagement/productStructure.js";
+  import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
+  import { list } from "@/api/productionManagement/productionProcess";
+  import { ElMessage } from "element-plus";
+  import { useRoute, useRouter } from "vue-router";
+
+  defineComponent({
+    name: "StructureEdit",
+  });
+
+  const ProductSelectDialog = defineAsyncComponent(
+    () => import("@/views/basicData/product/ProductSelectDialog.vue")
+  );
+  import MaterialCard from "./MaterialCard.vue";
+  const emit = defineEmits(["update:router"]);
+  const form = ref();
+
+  const route = useRoute();
+  const router = useRouter();
+  const routeId = computed({
+    get() {
+      return route.query.id;
+    },
+
+    set(val) {
+      emit("update:router", val);
+    },
+  });
+
+  // 浠庤矾鐢卞弬鏁拌幏鍙栦骇鍝佷俊鎭�
+  const routeBomNo = computed(() => route.query.bomNo || "");
+  const routeProductName = computed(() => route.query.productName || "");
+  const routeProductModelName = computed(
+    () => route.query.productModelName || ""
+  );
+  const routeOrderId = computed(() => route.query.orderId);
+  const pageType = computed(() => route.query.type);
+  const isOrderPage = computed(
+    () => pageType.value === "order" && routeOrderId.value
+  );
+
+  const dataValue = reactive({
+    dataList: [],
+    productOptions: [],
+    processOptions: [],
+    showProductDialog: false,
+    currentRowIndex: null,
+    currentRowName: null,
+    loading: false,
+    isEdit: false,
+  });
+
+  const normalizeListData = (source: any) => {
+    if (Array.isArray(source)) {
+      return source;
+    }
+    if (Array.isArray(source?.records)) {
+      return source.records;
+    }
+    return [];
+  };
+
+  const getProcessOptionById = (id: any) => {
+    if (id === undefined || id === null || id === "") {
+      return null;
+    }
+    return (
+      normalizeListData(dataValue.processOptions).find(
+        option => String(option.id) === String(id)
+      ) || null
+    );
+  };
+
+  const syncProcessOperationFields = (item: any) => {
+    const processId = item.processId ?? item.operationId ?? "";
+    if (!processId) {
+      item.processId = "";
+      item.operationId = "";
+      item.processName = "";
+      item.operationName = "";
+      return;
+    }
+
+    const option = getProcessOptionById(processId);
+    const processName =
+      option?.name || item.processName || item.operationName || "";
+
+    item.processId = processId;
+    item.operationId = processId;
+    item.processName = processName;
+    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 toQuantityNumber = (value: any) => {
+    const numberValue = Number(value);
+    if (!Number.isFinite(numberValue)) {
+      return 0;
+    }
+    return Number(numberValue.toFixed(2));
+  };
+
+  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 tableData = reactive([
+    {
+      productName: "",
+      model: "",
+      bomNo: "",
+    },
+  ]);
+
+  const openDialog = (tempId: any) => {
+    console.log(tempId, "tempId");
+    dataValue.currentRowName = tempId;
+    dataValue.showProductDialog = true;
+  };
+
+  const fetchData = async () => {
+    if (isOrderPage.value) {
+      // 璁㈠崟鎯呭喌锛氫娇鐢ㄨ鍗曠殑浜у搧缁撴瀯鎺ュ彛
+      const { data } = await listProcessBom({ orderId: routeOrderId.value });
+      dataValue.dataList = (data as any) || [];
+      normalizeTreeData(dataValue.dataList);
+      recalculateDemandedQuantities();
+    } else {
+      // 闈炶鍗曟儏鍐碉細浣跨敤鍘熸潵鐨勬帴鍙�
+      const { data } = await queryList(routeId.value);
+      dataValue.dataList = (data as any) || [];
+      console.log(dataValue);
+      normalizeTreeData(dataValue.dataList);
+      console.log(dataValue.dataList, "dataValue.dataList");
+    }
+  };
+
+  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("璇烽�夋嫨涓�涓骇鍝�");
+      return;
+    }
+    // 鍙厑璁镐竴涓細濡傛灉涓婃父杩斿洖浜嗗涓紝榛樿浣跨敤鏈�鍚庝竴娆¢�夋嫨骞惰鐩栧綋鍓嶅��
+    const productData = row[row.length - 1];
+
+    //  鏈�澶栧眰缁勪欢涓紝涓庡綋鍓嶄骇鍝佺浉鍚岀殑浜у搧鍙兘鏈変竴涓�
+    const isTopLevel = dataValue.dataList.some(
+      item => (item as any).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;
+        }
+      }
+    }
+    // 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;
+
+    // 鏍¢獙涓�缁勫厔寮熻妭鐐圭殑宸ュ簭鏄惁閮界浉鍚�
+    const checkProcessUniqueness = (items: any[]) => {
+      if (!items || items.length === 0 || !isValid) return;
+
+      // 鑾峰彇绗竴涓潪绌虹殑宸ュ簭ID浣滀负鍙傝��
+      const firstProcessId = items.find(item => item.processId)?.processId;
+      
+      // 濡傛灉鏈夊伐搴廔D锛屾鏌ユ墍鏈夐」鏄惁閮戒娇鐢ㄧ浉鍚岀殑宸ュ簭
+      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}銆峘
+            );
+            isValid = false;
+            return;
+          }
+        }
+      }
+
+      // 閫掑綊鏍¢獙瀛愮骇鐨勫厔寮熻妭鐐�
+      for (const item of items) {
+        if (item.children && item.children.length > 0) {
+          checkProcessUniqueness(item.children);
+        }
+      }
+    };
+
+    // 鏍¢獙鍑芥暟
+    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);
+        });
+      }
+    };
+
+    // 1. 棣栧厛鏍¢獙鍚屼竴鐖剁骇涓嬬殑鍚屽眰娑堣�楀伐搴忔槸鍚﹀敮涓�
+    checkProcessUniqueness(dataValue.dataList);
+    if (!isValid) return false;
+
+    // 2. 鐒跺悗閬嶅巻鏍¢獙鎵�鏈夐《灞傞」鐨勫瓧娈靛繀濉儏鍐�
+    dataValue.dataList.forEach(item => {
+      validateItem(item, true);
+    });
+
+    return isValid;
+  };
+
+  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 || []),
+      })
+        .then(res => {
+          router.go(-1);
+          ElMessage.success("淇濆瓨鎴愬姛");
+          dataValue.loading = false;
+        })
+        .catch(() => {
+          dataValue.loading = false;
+        });
+    } else {
+      dataValue.loading = false;
+    }
+  };
+
+  const removeItem = (tempId: string) => {
+    const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
+    if (topIndex !== -1) {
+      dataValue.dataList.splice(topIndex, 1);
+      return;
+    }
+
+    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 newChildNode = (parentItem: any) => ({
+    parentId: parentItem.id || "",
+    parentTempId: parentItem.tempId || "",
+    productName: "",
+    productId: "",
+    model: undefined,
+    productModelId: undefined,
+    processId: "",
+    processName: "",
+    operationId: "",
+    operationName: "",
+    unitQuantity: 1,
+    demandedQuantity: 0,
+    unit: "",
+    children: [],
+    tempId: new Date().getTime(),
+  });
+
+  const addRootItem = () => {
+    dataValue.dataList.push(newChildNode({ id: "", tempId: "" }));
+  };
+
+  const addChildItem = (parentTempId: string) => {
+    const addToItem = (items: any[]): boolean => {
+      for (const item of items) {
+        if (item.tempId === parentTempId) {
+          if (!item.children) item.children = [];
+          item.children.push(newChildNode(item));
+          recalculateDemandedQuantities();
+          return true;
+        }
+        if (item.children?.length > 0) {
+          if (addToItem(item.children)) return true;
+        }
+      }
+      return false;
+    };
+    addToItem(dataValue.dataList);
+  };
+
+  const getPropPath = (row, field) => {
+    // 涓烘瘡涓猺ow鐢熸垚鍞竴鐨勮矾寰�
+    // 浣跨敤row.id鎴栫储寮曚綔涓哄敮涓�鏍囪瘑
+    let path = "dataList";
+
+    // 绠�鍗曞疄鐜帮細浣跨敤row鐨刬d鎴栦竴涓敮涓�鏍囪瘑
+    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();
+  };
+
+  onMounted(async () => {
+    // 浠庤矾鐢卞弬鏁板洖鏄炬暟鎹�
+    tableData[0].productName = routeProductName.value as string;
+    tableData[0].model = routeProductModelName.value as string;
+    tableData[0].bomNo = routeBomNo.value as string;
+
+    // 璁㈠崟鎯呭喌涓嬬鐢ㄧ紪杈�
+    if (isOrderPage.value) {
+      dataValue.isEdit = false;
+    }
+
+    // 鍏堝姞杞藉伐搴忛�夐」锛屽啀鍔犺浇鏁版嵁锛岀‘淇漞l-select鑳藉姝g‘鍥炴樉
+    await fetchProcessOptions();
+    await fetchData();
+  });
+</script>
+
+<style scoped>
+.tree-container {
+  padding: 8px 0;
+}
+.tree-legend {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+  padding: 8px 12px;
+  background: #f5f7fa;
+  border-radius: 6px;
+  font-size: 13px;
+  color: #606266;
+}
+.empty-hint {
+  text-align: center;
+  color: #909399;
+  padding: 24px 0;
+}
+</style>
\ No newline at end of file

--
Gitblit v1.9.3