From d78815179b7d49509e6637d547ca8bbd525f7fcf Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期一, 07 九月 2026 11:45:41 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro_河南鹤壁' into dev_pro_河南鹤壁

---
 src/views/productionManagement/productStructure/Detail/index.vue |  283 +++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 266 insertions(+), 17 deletions(-)

diff --git a/src/views/productionManagement/productStructure/Detail/index.vue b/src/views/productionManagement/productStructure/Detail/index.vue
index ec18ddf..6899eb0 100644
--- a/src/views/productionManagement/productStructure/Detail/index.vue
+++ b/src/views/productionManagement/productStructure/Detail/index.vue
@@ -22,16 +22,34 @@
               border
               :preserve-expanded-content="false"
               :default-expand-all="true"
-              style="width: 100%">
+              style="width: 100%"
+              @expand-change="handleOuterExpandChange">
       <el-table-column type="expand">
         <template #default="props">
           <el-form ref="form"
                    :model="dataValue">
-            <el-table :data="dataValue.dataList"
+            <div v-if="dataValue.isEdit"
+                 class="drag-tip">
+              <el-icon><Rank /></el-icon>
+              <span>鎷栧姩鎵嬫焺璋冩暣缁撴瀯:鍚屽眰绾х洿鎺ユ嫋鍔ㄦ帓搴�;鎷栧埌鏌愯姝d笅鏂规垚涓哄叾瀛愮骇;鎷栧埌鍒楄〃鏈�鍓嶆垚涓轰竴绾�</span>
+            </div>
+            <el-table ref="tableRef"
+                      :data="dataValue.dataList"
                       row-key="tempId"
                       default-expand-all
                       :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+                      @expand-change="handleTreeExpandChange"
                       style="width: 100%">
+              <el-table-column v-if="dataValue.isEdit"
+                               width="44"
+                               align="center">
+                <template #default="{ row }">
+                  <div class="drag-handle"
+                       title="鎷栧姩璋冩暣椤哄簭:鎷栧埌鏌愯姝d笅鏂规垚涓哄叾瀛愮骇,鎷栧埌鍒楄〃鏈�鍓嶆垚涓轰竴绾�">
+                    <el-icon><Rank /></el-icon>
+                  </div>
+                </template>
+              </el-table-column>
               <el-table-column prop="productName"
                                label="浜у搧" />
               <el-table-column prop="model"
@@ -59,18 +77,20 @@
                   <el-form-item v-if="dataValue.isEdit"
                                 :rules="dataValue.dataList.some(item => (item as any).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 => handleProcessChange(row, value)"
-                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
-                      <el-option v-for="item in dataValue.processOptions"
-                                 :key="item.id"
-                                 :label="item.name"
-                                 :value="item.id" />
-                    </el-select>
+                    <el-input v-model="row.processName"
+                              readonly
+                              placeholder="璇烽�夋嫨娑堣�楀伐搴�"
+                              clearable
+                              style="width: 100%"
+                              :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
+                              @click="openProcessDialog(row, $event)"
+                              @clear="clearProcess(row)">
+                      <template #append>
+                        <el-button icon="Search"
+                                   :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
+                                   @click="openProcessDialog(row)" />
+                      </template>
+                    </el-input>
                   </el-form-item>
                 </template>
               </el-table-column>
@@ -150,6 +170,9 @@
                            v-model:model-value="dataValue.showProductDialog"
                            :single="true"
                            @confirm="handleProduct" />
+    <process-select-dialog v-if="dataValue.showProcessDialog"
+                           v-model:model-value="dataValue.showProcessDialog"
+                           @confirm="handleProcessConfirm" />
   </div>
 </template>
 
@@ -158,18 +181,23 @@
     computed,
     defineAsyncComponent,
     defineComponent,
+    nextTick,
+    onBeforeUnmount,
     onMounted,
     reactive,
     ref,
+    watch,
   } from "vue";
   import {
     queryList,
     addBomDetail,
   } from "@/api/productionManagement/productStructure.js";
   import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
-  import { list } from "@/api/productionManagement/productionProcess";
+  import { listAll } from "@/api/productionManagement/productionProcess";
   import { ElMessage } from "element-plus";
   import { useRoute, useRouter } from "vue-router";
+  import Sortable from "sortablejs";
+  import { Rank } from "@element-plus/icons-vue";
 
   defineComponent({
     name: "StructureEdit",
@@ -178,8 +206,16 @@
   const ProductSelectDialog = defineAsyncComponent(
     () => import("@/views/basicData/product/ProductSelectDialog.vue")
   );
+  const ProcessSelectDialog = defineAsyncComponent(
+    () =>
+      import(
+        "@/views/productionManagement/productionProcess/ProcessSelectDialog.vue"
+      )
+  );
   const emit = defineEmits(["update:router"]);
   const form = ref();
+  const tableRef = ref();
+  let tableSortable = null;
 
   const route = useRoute();
   const router = useRouter();
@@ -210,8 +246,10 @@
     productOptions: [],
     processOptions: [],
     showProductDialog: false,
+    showProcessDialog: false,
     currentRowIndex: null,
     currentRowName: null,
+    currentProcessRow: null,
     loading: false,
     isEdit: false,
   });
@@ -303,6 +341,155 @@
     syncDemandedQuantityTree(dataValue.dataList);
   };
 
+  // ===== 鎷栨嫿鎺掑簭 =====
+  // 鎸夊綋鍓� DOM 琛岄『搴忓睍寮�鎴愭墎骞冲垪琛�(缂栬緫妯″紡涓嬪己鍒跺叏閮ㄥ睍寮�,淇濊瘉琛岄『搴忎笌鏁版嵁涓�鑷�)
+  const buildFlatTree = () => {
+    const flat = [];
+    const walk = (items, depth, parentNode) => {
+      items.forEach(node => {
+        flat.push({ node, depth, parentArray: items, parentNode });
+        if (Array.isArray(node.children) && node.children.length > 0) {
+          walk(node.children, depth + 1, node);
+        }
+      });
+    };
+    walk(dataValue.dataList, 0, null);
+    return flat;
+  };
+
+  const removeNodeByTempId = (items, tempId) => {
+    for (let i = 0; i < items.length; i++) {
+      if (String(items[i].tempId) === String(tempId)) {
+        items.splice(i, 1);
+        return true;
+      }
+      if (
+        Array.isArray(items[i].children) &&
+        removeNodeByTempId(items[i].children, tempId)
+      ) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  const updateParentRef = (node, parent) => {
+    node.parentTempId = parent?.tempId ?? "";
+    node.parentId = parent?.id ?? "";
+  };
+
+  const handleSortEnd = evt => {
+    const { oldIndex, newIndex } = evt;
+    if (
+      typeof oldIndex !== "number" ||
+      typeof newIndex !== "number" ||
+      oldIndex === newIndex
+    ) {
+      return;
+    }
+    const flat = buildFlatTree();
+    if (oldIndex < 0 || oldIndex >= flat.length) return;
+    const dragged = flat[oldIndex];
+
+    // 鎷栨斁鍚庝綅浜庤鎷栬涓婃柟涓�琛岀殑鏁版嵁(鎸夋渶缁� DOM 椤哄簭鎹㈢畻鍒板師鎵佸钩鍒楄〃)
+    let aEntry = null;
+    if (newIndex > oldIndex) {
+      aEntry = flat[newIndex];
+    } else if (newIndex > 0) {
+      aEntry = flat[newIndex - 1];
+    }
+
+    if (!aEntry) {
+      // 鎷栧埌鍒楄〃鏈�鍓� 鈫� 鎴愪负涓�绾�
+      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
+      dataValue.dataList.unshift(dragged.node);
+      updateParentRef(dragged.node, null);
+    } else if (aEntry.depth < dragged.depth) {
+      // 鎷栧埌鏌愯姝d笅鏂� 鈫� 鎴愪负璇ヨ鐨勫瓙绾�
+      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
+      if (!aEntry.node.children) {
+        aEntry.node.children = [];
+      }
+      aEntry.node.children.push(dragged.node);
+      updateParentRef(dragged.node, aEntry.node);
+    } else {
+      // 鍚岀骇鎻掑叆:鍚戜笂鎵剧涓�涓繁搴︿笉瓒呰繃琚嫋琛岀殑鑺傜偣
+      let i = flat.indexOf(aEntry);
+      while (i >= 0 && flat[i].depth > dragged.depth) {
+        i--;
+      }
+      const eEntry = flat[i];
+      // 鎷栧洖鑷繁鐨勫瓙鏍戝唴 鈫� 涓嶅鐞�(闃叉褰㈡垚寰幆)
+      if (!eEntry || eEntry.node.tempId === dragged.node.tempId) {
+        return;
+      }
+      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
+      const siblings = eEntry.parentArray;
+      const idx = siblings.indexOf(eEntry.node);
+      siblings.splice(idx + 1, 0, dragged.node);
+      updateParentRef(dragged.node, eEntry.parentNode);
+    }
+
+    recalculateDemandedQuantities();
+  };
+
+  const initSortable = () => {
+    if (!tableRef.value) return;
+    const tbody = tableRef.value.$el.querySelector(".el-table__body tbody");
+    if (!tbody) return;
+    tableSortable?.destroy();
+    tableSortable = new Sortable(tbody, {
+      animation: 150,
+      handle: ".drag-handle",
+      disabled: !dataValue.isEdit,
+      ghostClass: "sortable-ghost",
+      onEnd: handleSortEnd,
+    });
+  };
+
+  const expandAllTree = () => {
+    const table = tableRef.value;
+    if (!table) return;
+    const walk = items => {
+      items.forEach(item => {
+        if (Array.isArray(item.children) && item.children.length > 0) {
+          table.toggleRowExpansion(item, true);
+          walk(item.children);
+        }
+      });
+    };
+    walk(dataValue.dataList);
+  };
+
+  // 缂栬緫妯″紡涓嬩笉鍏佽鎶樺彔,淇濊瘉鎷栨嫿鏃� DOM 琛岄『搴忎笌 dataList 涓�鑷�
+  const handleTreeExpandChange = (row, expanded) => {
+    if (dataValue.isEdit && expanded === false) {
+      nextTick(() => {
+        tableRef.value?.toggleRowExpansion(row, true);
+      });
+    }
+  };
+
+  // 澶栧眰琛屾姌鍙犲悗鍐呭浼氶攢姣�,閲嶆柊灞曞紑鏃堕噸鏂板垵濮嬪寲鎷栨嫿
+  const handleOuterExpandChange = (row, expandedRows) => {
+    if (Array.isArray(expandedRows) && expandedRows.some(r => r === row)) {
+      nextTick(() => initSortable());
+    }
+  };
+
+  watch(
+    () => dataValue.isEdit,
+    val => {
+      tableSortable?.option("disabled", !val);
+      if (val) {
+        nextTick(() => {
+          initSortable();
+          expandAllTree();
+        });
+      }
+    }
+  );
+
   const buildSubmitTree = (items: any[]) => {
     return items.map((item: any) => {
       const current = { ...item };
@@ -346,6 +533,35 @@
     }
   };
 
+  const openProcessDialog = (row: any, event?: Event) => {
+    // 鐐瑰嚮娓呴櫎鎸夐挳鏃朵笉寮圭獥
+    const target = event?.target as HTMLElement | undefined;
+    if (target && target.closest(".el-input__clear")) {
+      return;
+    }
+    dataValue.currentProcessRow = row;
+    dataValue.showProcessDialog = true;
+  };
+
+  const clearProcess = (row: any) => {
+    row.processId = "";
+    row.operationId = "";
+    row.processName = "";
+    row.operationName = "";
+  };
+
+  const handleProcessConfirm = (processRow: any) => {
+    const row = dataValue.currentProcessRow;
+    if (!row) {
+      return;
+    }
+    row.processId = processRow.id;
+    row.operationId = processRow.id;
+    row.processName = processRow.name;
+    row.operationName = processRow.name;
+    handleProcessChange(row, processRow.id);
+  };
+
   const handleUnitQuantityChange = () => {
     recalculateDemandedQuantities();
   };
@@ -382,7 +598,7 @@
   };
 
   const fetchProcessOptions = async () => {
-    const { data } = await list({});
+    const { data } = await listAll();
     console.log(data, "dataValue.dataList");
     dataValue.processOptions = normalizeListData(data);
   };
@@ -693,5 +909,38 @@
     // 鍏堝姞杞藉伐搴忛�夐」锛屽啀鍔犺浇鏁版嵁锛岀‘淇漞l-select鑳藉姝g‘鍥炴樉
     await fetchProcessOptions();
     await fetchData();
+    await nextTick();
+    initSortable();
   });
-</script>
\ No newline at end of file
+
+  onBeforeUnmount(() => {
+    tableSortable?.destroy();
+    tableSortable = null;
+  });
+</script>
+
+<style scoped>
+  .drag-tip {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    margin-bottom: 8px;
+    font-size: 12px;
+    color: #909399;
+  }
+
+  .drag-handle {
+    cursor: move;
+    color: #909399;
+    text-align: center;
+  }
+
+  .drag-handle:hover {
+    color: var(--el-color-primary);
+  }
+
+  :deep(.sortable-ghost) {
+    opacity: 0.6;
+    background-color: #f5f7fa !important;
+  }
+</style>
\ No newline at end of file

--
Gitblit v1.9.3