From 79dbd82e9d31e659a5ecb58da7fd011c03a8d58f Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期日, 04 一月 2026 15:38:31 +0800
Subject: [PATCH] 修改工艺路线拖拽排序

---
 src/views/productionManagement/processRoute/ItemsForm.vue |  474 ++++++++++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 400 insertions(+), 74 deletions(-)

diff --git a/src/views/productionManagement/processRoute/ItemsForm.vue b/src/views/productionManagement/processRoute/ItemsForm.vue
index 7ac04f2..5538b2c 100644
--- a/src/views/productionManagement/processRoute/ItemsForm.vue
+++ b/src/views/productionManagement/processRoute/ItemsForm.vue
@@ -3,11 +3,30 @@
     <el-dialog
         v-model="isShow"
         title="宸ヨ壓璺嚎椤圭洰"
-        width="800"
+        width="800px"
         @close="closeModal"
     >
-      <el-button type="primary" @click="isShowProductSelectDialog = true" class="mb5">閫夋嫨浜у搧</el-button>
+      <div class="operate-button">
+        <el-button
+            type="primary"
+            @click="isShowProductSelectDialog = true"
+            class="mb5"
+            style="margin-bottom: 10px;"
+        >
+          閫夋嫨浜у搧
+        </el-button>
+
+        <el-switch
+            v-model="isTable"
+            inline-prompt
+            active-text="琛ㄦ牸"
+            inactive-text="鍒楄〃"
+            @change="handleViewChange"
+        />
+      </div>
+
       <el-table
+          v-if="isTable"
           ref="multipleTable"
           v-loading="tableLoading"
           border
@@ -16,10 +35,21 @@
           row-key="id"
           tooltip-effect="dark"
           class="lims-table"
+          style="cursor: move;"
       >
-        <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+        <el-table-column align="center" label="搴忓彿" width="60">
+          <template #default="scope">
+            {{ scope.$index + 1 }}
+          </template>
+        </el-table-column>
 
-        <el-table-column v-for="(item, index) in tableColumn" :key="index" :label="item.label" :width="item.width" show-overflow-tooltip>
+        <el-table-column
+            v-for="(item, index) in tableColumn"
+            :key="index"
+            :label="item.label"
+            :width="item.width"
+            show-overflow-tooltip
+        >
           <template #default="scope" v-if="item.dataType === 'action'">
             <el-button
                 v-for="(op, opIndex) in item.operation"
@@ -27,7 +57,7 @@
                 :type="op.type"
                 :link="op.link"
                 size="small"
-                @click="op.clickFun(scope.row)"
+                @click.stop="op.clickFun(scope.row)"
             >
               {{ op.name }}
             </el-button>
@@ -35,7 +65,11 @@
 
           <template #default="scope" v-else>
             <template v-if="item.prop === 'processId'">
-              <el-select v-model="scope.row[item.prop]" style="width: 100%">
+              <el-select
+                  v-model="scope.row[item.prop]"
+                  style="width: 100%;"
+                  @mousedown.stop
+              >
                 <el-option
                     v-for="process in processOptions"
                     :key="process.id"
@@ -45,12 +79,59 @@
               </el-select>
             </template>
             <template v-else>
-              {{ scope.row[item.prop] }}
+              {{ scope.row[item.prop] || '-' }}
             </template>
           </template>
-
         </el-table-column>
       </el-table>
+
+      <!-- 绠�鍖栧鍣ㄧ粨鏋勶紝鐩存帴缁檈l-steps鍔爎ef -->
+      <el-steps
+          v-else
+          ref="stepsContainer"
+          class="mb5 custom-steps"
+          :active="routeItems.length"
+          align-center
+          style="padding: 10px 0;"
+      >
+        <!-- 鍏抽敭锛氱粰el-step娣诲姞data-id锛岃�岄潪鍐呴儴鍗$墖 -->
+        <el-step
+            v-for="(item, index) in routeItems"
+            :key="item.id"
+            class="custom-step draggable-step"
+            :data-id="item.id"
+            style="cursor: move;"
+        >
+          <template #title>
+            <div class="step-content">
+              <div class="step-number">{{ index + 1 }}</div>
+              <el-card
+                  :header="item.productName"
+                  class="step-card"
+                  style="cursor: move;"
+              >
+                <p>{{ item.model }}</p>
+                <p>{{ item.unit }}</p>
+                <el-select
+                    v-model="item.processId"
+                    style="width: 100%;"
+                    @mousedown.stop
+                >
+                  <el-option
+                      v-for="process in processOptions"
+                      :key="process.id"
+                      :label="process.name"
+                      :value="process.id"
+                  />
+                </el-select>
+                <template #footer>
+                  <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">鍒犻櫎</el-button>
+                </template>
+              </el-card>
+            </div>
+          </template>
+        </el-step>
+      </el-steps>
 
       <template #footer>
         <div class="dialog-footer">
@@ -59,31 +140,45 @@
         </div>
       </template>
     </el-dialog>
-  </div>
 
-  <ProductSelectDialog v-if="isShowProductSelectDialog" v-model:model-value="isShowProductSelectDialog" @confirm="handelSelectProducts" />
+    <ProductSelectDialog
+        v-model="isShowProductSelectDialog"
+        @confirm="handelSelectProducts"
+    />
+  </div>
 </template>
 
 <script setup>
-import {ref, computed, getCurrentInstance, onMounted} from "vue";
+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, addOrUpdateProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
 import { processList } from "@/api/productionManagement/productionProcess.js";
+import Sortable from 'sortablejs';
 
 const props = defineProps({
   visible: {
     type: Boolean,
     required: true,
+    default: false
   },
   record: {
     type: Object,
     required: true,
+    default: () => ({})
   }
 });
 
 const emit = defineEmits(['update:visible', 'completed']);
 
 const processOptions = ref([]);
+const tableLoading = ref(false);
+const isShowProductSelectDialog = ref(false);
+const routeItems = ref([]);
+let tableSortable = null;
+let stepsSortable = null;
+const multipleTable = ref(null);
+const stepsContainer = ref(null);
+const isTable = ref(true);
 
 const isShow = computed({
   get() {
@@ -91,119 +186,350 @@
   },
   set(val) {
     emit('update:visible', val);
-  },
+  }
 });
 
 const tableColumn = ref([
-  {
-    label: "浜у搧鍚嶇О",
-    prop: "productName",
-  },
-  {
-    label: "瑙勬牸鍚嶇О",
-    prop: "model",
-  },
-  {
-    label: "鍗曚綅",
-    prop: "unit",
-  },
-  {
-    label: "宸ュ簭鍚嶇О",
-    prop: "processId",
-  },
+  { label: "浜у搧鍚嶇О", prop: "productName", width: 180 },
+  { label: "瑙勬牸鍚嶇О", prop: "model", width: 150 },
+  { label: "鍗曚綅", prop: "unit", width: 80 },
+  { label: "宸ュ簭鍚嶇О", prop: "processId", width: 180 },
   {
     dataType: "action",
     label: "鎿嶄綔",
     align: "center",
     fixed: "right",
+    width: 100,
     operation: [
       {
         name: "鍒犻櫎",
         type: "danger",
         link: true,
         clickFun: (row) => {
-          const index = routeItems.value.indexOf(row);
-          if (index !== -1) {
-            routeItems.value.splice(index, 1);
+          const idx = routeItems.value.findIndex(item => item.id === row.id);
+          if (idx > -1) {
+            removeItem(idx)
           }
         }
-      },
+      }
     ]
   }
-])
+]);
 
-const tableLoading = ref(false);
+const removeItem = (index) => {
+  routeItems.value.splice(index, 1);
+  nextTick(() => initSortable());
+};
 
-const isShowProductSelectDialog = ref(false)
-const routeItems = ref([])
-
-let { proxy } = getCurrentInstance()
+const removeItemByID = (id) => {
+  const idx = routeItems.value.findIndex(item => item.id === id);
+  if (idx > -1) {
+    routeItems.value.splice(idx, 1);
+    nextTick(() => initSortable());
+  }
+};
 
 const closeModal = () => {
   isShow.value = false;
 };
 
 const handelSelectProducts = (products) => {
-  const data = products.map(({ id, ...product }) => ({
+  destroySortable();
+
+  const newData = products.map(({ id, ...product }) => ({
     ...product,
     productModelId: id,
-    routeId: props.record.id
+    routeId: props.record.id,
+    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+    processId: undefined
   }));
 
-  routeItems.value.push(...data);
-}
+  routeItems.value.push(...newData);
+  routeItems.value = [...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 => ({
-      ...item,
-      processId: item.processId === 0 ? undefined : item.processId
-    }))
-  }).catch(err => {
-    tableLoading.value = false;
-  })
+  findProcessRouteItemList({ routeId: props.record.id })
+      .then(res => {
+        tableLoading.value = false;
+        routeItems.value = res.data.map(item => ({
+          ...item,
+          processId: item.processId === 0 ? undefined : item.processId
+        }));
+        // 寤惰繜鍒濆鍖栵紝纭繚DOM瀹屽叏娓叉煋
+        nextTick(() => {
+          setTimeout(() => initSortable(), 100);
+        });
+      })
+      .catch(err => {
+        tableLoading.value = false;
+        console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+      });
 };
 
 const findProcessList = () => {
-  processList({}).then(res => {
-    processOptions.value = res.data
-  })
-}
+  processList({})
+      .then(res => {
+        processOptions.value = res.data;
+      })
+      .catch(err => {
+        console.error("鑾峰彇宸ュ簭澶辫触锛�", err);
+      });
+};
+
+const { proxy } = getCurrentInstance() || {};
 
 const handleSubmit = () => {
   if (routeItems.value.length === 0) {
-    proxy.$modal.msgError("璇锋坊鍔犺矾绾块」鐩�");
+    proxy?.$modal?.msgError("璇锋坊鍔犺矾绾块」鐩�");
     return;
   }
 
-  // 鏄惁鏈夋湭閫夋嫨鐨勫伐搴�
-  const hasUnselectedProcess = routeItems.value.some(item => !item.processId);
-  if (hasUnselectedProcess) {
-    proxy.$modal.msgError("璇烽�夋嫨宸ュ簭");
+  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
+  if (hasEmptyProcess) {
+    proxy?.$modal?.msgError("璇蜂负鎵�鏈夐」鐩�夋嫨宸ュ簭");
     return;
   }
 
-  addOrUpdateProcessRouteItem({routeId: props.record.id, processRouteItem: routeItems.value}).then(res => {
-    // 鍏抽棴妯℃�佹
-    isShow.value = false;
-    // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
-    emit('completed');
-    proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+  addOrUpdateProcessRouteItem({
+    routeId: props.record.id,
+    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
   })
+      .then(res => {
+        isShow.value = false;
+        emit('completed');
+        proxy?.$modal?.msgSuccess("鎻愪氦鎴愬姛");
+      })
+      .catch(err => {
+        proxy?.$modal?.msgError(`鎻愪氦澶辫触锛�${err.msg || "缃戠粶寮傚父"}`);
+      });
 };
 
+const destroySortable = () => {
+  if (tableSortable) {
+    tableSortable.destroy();
+    tableSortable = null;
+  }
+  if (stepsSortable) {
+    stepsSortable.destroy();
+    stepsSortable = null;
+  }
+};
+
+const initSortable = () => {
+  destroySortable();
+
+  if (isTable.value) {
+    if (!multipleTable.value) return;
+    const tbody = multipleTable.value.$el.querySelector('.el-table__body tbody') ||
+        multipleTable.value.$el.querySelector('.el-table__body-wrapper > table > tbody');
+    if (!tbody) return;
+
+    tableSortable = new Sortable(tbody, {
+      animation: 150,
+      ghostClass: 'sortable-ghost',
+      handle: '.el-table__row',
+      filter: '.el-button, .el-select',
+      onEnd: (evt) => {
+        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
+
+        // 浣跨敤鏁扮粍 splice 鏂规硶閲嶆柊鎺掑簭锛屼笌琛ㄦ牸妯″紡淇濇寔涓�鑷�
+        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 {
+    if (!stepsContainer.value) return;
+
+    // 鍏抽敭淇1锛氱簿鍑嗗畾浣嶆楠ゆ潯鍒楄〃瀹瑰櫒锛堝吋瀹笶lement Plus涓嶅悓鐗堟湰锛�
+    const stepsList = stepsContainer.value.$el.querySelector('.el-steps__items') ||
+        stepsContainer.value.$el ||
+        stepsContainer.value;
+    if (!stepsList) {
+      console.warn('鏈壘鍒版楠ゆ潯鎷栨嫿瀹瑰櫒');
+      return;
+    }
+
+    // 鍏抽敭淇2锛氭斁瀹芥嫋鎷借Е鍙戞潯浠讹紝鎭㈠鎷栨嫿鍔熻兘
+    stepsSortable = new Sortable(stepsList, {
+      animation: 150,
+      ghostClass: 'sortable-ghost',
+      draggable: '.draggable-step', // 鍙嫋鎷藉厓绱狅細el-step
+      handle: '.draggable-step, .step-card', // 鎷栨嫿鎵嬫焺锛歴tep鏈韩 + 鍗$墖锛堟墿澶цЕ鍙戝尯鍩燂級
+      filter: '.el-button, .el-select, .el-input', // 杩囨护鎸夐挳/閫夋嫨鍣紝閬垮厤璇Е鍙�
+      forceFallback: true, // 寮哄埗浣跨敤fallback妯″紡锛岄伩鍏嶅師鐢熸嫋鎷藉啿绐�
+      fallbackClass: 'sortable-fallback',
+      preventOnFilter: true, // 杩囨护鍏冪礌闃绘鎷栨嫿
+      scroll: true,
+      scrollSensitivity: 30,
+      scrollSpeed: 10,
+      bubbleScroll: true,
+      // 缁熶竴浣跨敤鏁扮粍 splice 鏂规硶閲嶆柊鎺掑簭锛屼笌琛ㄦ牸妯″紡淇濇寔涓�鑷�
+      onEnd: (evt) => {
+        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
+
+        // 浣跨敤鏁扮粍 splice 鏂规硶閲嶆柊鎺掑簭锛屼笌琛ㄦ牸妯″紡淇濇寔涓�鑷�
+        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
+        routeItems.value.splice(evt.newIndex, 0, moveItem);
+        routeItems.value = [...routeItems.value];
+      }
+    });
+
+    // 璋冭瘯锛氭墦鍗板鍣ㄥ拰瀹炰緥锛岀‘璁ょ粦瀹氭垚鍔�
+    console.log('姝ラ鏉℃嫋鎷藉鍣�:', stepsList);
+    console.log('Sortable瀹炰緥:', stepsSortable);
+  }
+};
+
+const handleViewChange = () => {
+  destroySortable();
+  // 寤惰繜鍒濆鍖栵紝纭繚瑙嗗浘鍒囨崲鍚嶥OM瀹屽叏娓叉煋
+  nextTick(() => {
+    setTimeout(() => initSortable(), 100);
+  });
+};
+
+onMounted(() => {
+  findProcessRouteItems();
+  findProcessList();
+});
+
+onUnmounted(() => {
+  destroySortable();
+});
 
 defineExpose({
   closeModal,
   handleSubmit,
-  isShow,
+  isShow
 });
-
-onMounted(() => {
-  findProcessRouteItems()
-  findProcessList()
-})
 </script>
+
+<style scoped>
+:deep(.sortable-ghost) {
+  opacity: 0.6;
+  background-color: #f5f7fa !important;
+}
+
+:deep(.el-table__row) {
+  transition: background-color 0.2s;
+}
+
+:deep(.el-table__row:hover) {
+  background-color: #f9fafc !important;
+}
+
+:deep(.el-card__footer){
+  padding: 0 !important;
+}
+
+.operate-button {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+/* 鍏抽敭淇锛氫紭鍖栨楠ゆ潯鎷栨嫿鏍峰紡锛岀‘淇濆彲鐐瑰嚮鍖哄煙 */
+:deep(.el-steps__items) {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: flex-start;
+  gap: 20px;
+  min-height: 100px; /* 纭繚瀹瑰櫒鏈夐珮搴� */
+}
+
+:deep(.draggable-step) {
+  cursor: move !important; /* 寮哄埗鏄剧ず鎷栨嫿鍏夋爣 */
+  padding: 8px;
+  position: relative;
+  transition: all 0.2s ease;
+  flex: 0 0 auto;
+  min-width: 220px;
+  touch-action: none; /* 绂佺敤瑙︽懜鍔ㄤ綔锛岄伩鍏嶇Щ鍔ㄧ鍐茬獊 */
+}
+
+/* 鎷栨嫿鎮诞鏍峰紡锛屾彁绀哄彲鎷栨嫿 */
+:deep(.draggable-step:hover) {
+  background-color: rgba(64, 158, 255, 0.05);
+  transform: translateY(-2px);
+}
+
+:deep(.sortable-ghost) {
+  opacity: 0.4;
+  background-color: #f5f7fa !important;
+  border: 2px dashed #409eff;
+  margin: 10px;
+  transform: scale(1.02);
+}
+
+:deep(.sortable-fallback) {
+  opacity: 0.9;
+  background-color: #f5f7fa;
+  border: 1px solid #409eff;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  transform: rotate(2deg);
+  margin: 10px;
+}
+
+:deep(.step-card) {
+  cursor: move !important;
+  transition: box-shadow 0.2s ease;
+  user-select: none;
+  -webkit-user-select: none;
+  pointer-events: auto; /* 纭繚鍗$墖鍙Е鍙戦紶鏍囦簨浠� */
+  margin: 10px;
+  height: 300px;
+}
+
+:deep(.step-card:hover) {
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.step-content {
+  width: 220px;
+  user-select: none;
+}
+
+/* 绂佺敤姝ラ鏉¢粯璁ょ殑澶撮儴鏍峰紡骞叉壈 */
+:deep(.el-step__head) {
+  display: none; /* 闅愯棌榛樿鐨勬楠ゅ渾鍦堝拰搴忓彿 */
+}
+
+/* 闅愯棌Element Plus鑷姩鐢熸垚鐨勮繛鎺ョ嚎 */
+:deep(.el-step__main::before) {
+  display: none; /* 闅愯棌杩炴帴绾� */
+}
+
+/* 鑷畾涔夊簭鍙锋牱寮忎紭鍖� */
+.step-number {
+  font-weight: bold;
+  text-align: center;
+  width: 36px;
+  height: 36px;
+  line-height: 36px;
+  margin: 0 auto 10px;
+  background: #409eff;
+  color: #fff;
+  border-radius: 50%;
+  font-size: 14px;
+}
+
+</style>
\ No newline at end of file

--
Gitblit v1.9.3