From 17bff385d517ada8dd3ac8fc4b81c4c163a17aa2 Mon Sep 17 00:00:00 2001
From: yaowanxin <3588231647@qq.com>
Date: 星期三, 14 一月 2026 13:34:32 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_天津军泰伟业' into dev_天津军泰伟业

---
 src/views/productionManagement/processRoute/index.vue                      |  150 +++++-----
 src/views/procurementManagement/procurementInvoiceLedger/Form/EditForm.vue |  171 ++++++-----
 src/views/productionManagement/processRoute/processRouteItem/index.vue     |  503 +++++++++++++++++++++++++++++++++++
 3 files changed, 675 insertions(+), 149 deletions(-)

diff --git a/src/views/procurementManagement/procurementInvoiceLedger/Form/EditForm.vue b/src/views/procurementManagement/procurementInvoiceLedger/Form/EditForm.vue
index e26d73c..85f83ff 100644
--- a/src/views/procurementManagement/procurementInvoiceLedger/Form/EditForm.vue
+++ b/src/views/procurementManagement/procurementInvoiceLedger/Form/EditForm.vue
@@ -28,12 +28,22 @@
       </el-col>
       <el-col :span="12">
         <el-form-item label="鏉ョエ鏁帮細">
-          <el-input-number :step="0.1" :min="0" style="width: 100%" v-model="form.ticketsNum" @change="inputTicketsNum" :precision="2"/>
+          <el-input-number :step="0.1"
+                           :min="0"
+                           style="width: 100%"
+                           v-model="form.ticketsNum"
+                           @change="inputTicketsNum"
+                           :precision="2" />
         </el-form-item>
       </el-col>
       <el-col :span="12">
         <el-form-item label="鏈鏉ョエ閲戦(鍏�)锛�">
-					<el-input-number :step="0.1" :min="0" style="width: 100%" v-model="form.ticketsAmount" @change="inputTicketsAmount" :precision="2"/>
+          <el-input-number :step="0.1"
+                           :min="0"
+                           style="width: 100%"
+                           v-model="form.ticketsAmount"
+                           @change="inputTicketsAmount"
+                           :precision="2" />
         </el-form-item>
       </el-col>
       <el-col :span="12">
@@ -46,85 +56,92 @@
 </template>
 
 <script setup>
-import useFormData from "@/hooks/useFormData";
-import { getProductRecordById } from "@/api/procurementManagement/procurementInvoiceLedger";
-const { proxy } = getCurrentInstance()
+  import useFormData from "@/hooks/useFormData";
+  import { getProductRecordById } from "@/api/procurementManagement/procurementInvoiceLedger";
+  const { proxy } = getCurrentInstance();
 
-defineOptions({
-  name: "鏉ョエ鍙拌处琛ㄥ崟",
-});
-const temFutureTickets = ref(0)
-const { form, resetForm } = useFormData({
-  id: undefined,
-  purchaseContractNumber: undefined, // 閲囪喘鍚堝悓鍙�
-  salesContractNo: undefined, // 閿�鍞悎鍚屽彿
-  createdAt: undefined, // 鍒涘缓鏃堕棿
-  invoiceNumber: undefined, // 鍙戠エ鍙�
-  ticketsNum: undefined, // 鏉ョエ鏁�
-  ticketsAmount: undefined, // 鏉ョエ閲戦
-	taxInclusiveUnitPrice: undefined, // 鍚◣鍗曚环
-	ticketRegistrationId: undefined, // 鍚◣鍗曚环
-});
+  defineOptions({
+    name: "鏉ョエ鍙拌处琛ㄥ崟",
+  });
+  const temFutureTickets = ref(0);
+  const { form, resetForm } = useFormData({
+    id: undefined,
+    purchaseContractNumber: undefined, // 閲囪喘鍚堝悓鍙�
+    salesContractNo: undefined, // 閿�鍞悎鍚屽彿
+    createdAt: undefined, // 鍒涘缓鏃堕棿
+    invoiceNumber: undefined, // 鍙戠エ鍙�
+    ticketsNum: undefined, // 鏉ョエ鏁�
+    ticketsAmount: undefined, // 鏉ョエ閲戦
+    taxInclusiveUnitPrice: undefined, // 鍚◣鍗曚环
+    ticketRegistrationId: undefined, // 鍚◣鍗曚环
+  });
 
-const load = async (id) => {
-  const { code, data } = await getProductRecordById({ id });
-  if (code === 200) {
-    form.id = data.id;
-    form.purchaseContractNumber = data.purchaseContractNumber;
-    form.salesContractNo = data.salesContractNo;
-    form.createdAt = data.createdAt;
-    form.invoiceNumber = data.invoiceNumber;
-    form.ticketsNum = data.ticketsNum;
-    form.ticketsAmount = data.ticketsAmount.toFixed(2);
-    form.taxInclusiveUnitPrice = data.taxInclusiveUnitPrice;
-    form.futureTickets = data.futureTickets;
-    temFutureTickets.value = data.futureTickets;
-		form.ticketRegistrationId = data.ticketRegistrationId;
-  }
-};
+  const load = async id => {
+    const { code, data } = await getProductRecordById({ id });
+    if (code === 200) {
+      form.id = data.id;
+      form.purchaseContractNumber = data.purchaseContractNumber;
+      form.salesContractNo = data.salesContractNo;
+      form.createdAt = data.createdAt;
+      form.invoiceNumber = data.invoiceNumber;
+      form.ticketsNum = data.ticketsNum;
+      form.ticketsAmount = data.ticketsAmount.toFixed(2);
+      form.taxInclusiveUnitPrice = data.taxInclusiveUnitPrice;
+      form.futureTickets = data.futureTickets;
+      // temFutureTickets.value = data.futureTickets;
+      temFutureTickets.value = data.quantity;
 
-const inputTicketsNum = (val) => {
-	// 纭繚鍚◣鍗曚环瀛樺湪涓斾笉涓洪浂
-	if (!form.taxInclusiveUnitPrice || Number(form.taxInclusiveUnitPrice) === 0) {
-		proxy.$modal.msgWarning("鍚◣鍗曚环涓嶈兘涓洪浂鎴栨湭瀹氫箟");
-		return;
-	}
-	if (Number(form.ticketsNum) > Number(temFutureTickets.value)) {
-		proxy.$modal.msgWarning("寮�绁ㄦ暟涓嶅緱澶т簬鏈紑绁ㄦ暟");
-		form.ticketsNum = temFutureTickets.value
-	}
-	
-	// 纭繚鎵�鏈夋暟鍊奸兘杞崲涓烘暟瀛楃被鍨嬭繘琛岃绠�
-	const ticketsAmount = Number(form.ticketsNum) * Number(form.taxInclusiveUnitPrice);
-	const futureTickets = Number(temFutureTickets.value) - Number(form.ticketsNum);
-	form.futureTickets = Number(futureTickets.toFixed(2));
-	form.ticketsAmount = Number(ticketsAmount.toFixed(2));
-};
-const inputTicketsAmount = (val) => {
-	// 纭繚鍚◣鍗曚环瀛樺湪涓斾笉涓洪浂
-	if (!form.taxInclusiveUnitPrice || Number(form.taxInclusiveUnitPrice) === 0) {
-		proxy.$modal.msgWarning("鍚◣鍗曚环涓嶈兘涓洪浂鎴栨湭瀹氫箟");
-		return;
-	}
-	
-	if (Number(val) > Number(form.futureTickets*form.taxInclusiveUnitPrice)) {
-		proxy.$modal.msgWarning("鏈鏉ョエ閲戦涓嶅緱澶т簬鎬婚噾棰�");
-		form.ticketsAmount = (form.futureTickets*form.taxInclusiveUnitPrice).toFixed(2)
-		const ticketsNum = Number(form.ticketsAmount) / Number(form.taxInclusiveUnitPrice);
-		form.ticketsNum = Number(ticketsNum.toFixed(2))
-		return;
-	}
-	
-	// 纭繚鎵�鏈夋暟鍊奸兘杞崲涓烘暟瀛楃被鍨嬭繘琛岃绠�
-	const ticketsNum = Number(val) / Number(form.taxInclusiveUnitPrice);
-	form.ticketsNum = Number(ticketsNum.toFixed(2));
-};
+      form.ticketRegistrationId = data.ticketRegistrationId;
+    }
+  };
 
-defineExpose({
-  load,
-  form,
-  resetForm,
-});
+  const inputTicketsNum = val => {
+    // 纭繚鍚◣鍗曚环瀛樺湪涓斾笉涓洪浂
+    if (!form.taxInclusiveUnitPrice || Number(form.taxInclusiveUnitPrice) === 0) {
+      proxy.$modal.msgWarning("鍚◣鍗曚环涓嶈兘涓洪浂鎴栨湭瀹氫箟");
+      return;
+    }
+    if (Number(form.ticketsNum) > Number(temFutureTickets.value)) {
+      proxy.$modal.msgWarning("寮�绁ㄦ暟涓嶅緱澶т簬鏈紑绁ㄦ暟");
+      form.ticketsNum = temFutureTickets.value;
+    }
+
+    // 纭繚鎵�鏈夋暟鍊奸兘杞崲涓烘暟瀛楃被鍨嬭繘琛岃绠�
+    const ticketsAmount =
+      Number(form.ticketsNum) * Number(form.taxInclusiveUnitPrice);
+    const futureTickets =
+      Number(temFutureTickets.value) - Number(form.ticketsNum);
+    form.futureTickets = Number(futureTickets.toFixed(2));
+    form.ticketsAmount = Number(ticketsAmount.toFixed(2));
+  };
+  const inputTicketsAmount = val => {
+    // 纭繚鍚◣鍗曚环瀛樺湪涓斾笉涓洪浂
+    if (!form.taxInclusiveUnitPrice || Number(form.taxInclusiveUnitPrice) === 0) {
+      proxy.$modal.msgWarning("鍚◣鍗曚环涓嶈兘涓洪浂鎴栨湭瀹氫箟");
+      return;
+    }
+
+    if (Number(val) > Number(form.futureTickets * form.taxInclusiveUnitPrice)) {
+      proxy.$modal.msgWarning("鏈鏉ョエ閲戦涓嶅緱澶т簬鎬婚噾棰�");
+      form.ticketsAmount = (
+        form.futureTickets * form.taxInclusiveUnitPrice
+      ).toFixed(2);
+      const ticketsNum =
+        Number(form.ticketsAmount) / Number(form.taxInclusiveUnitPrice);
+      form.ticketsNum = Number(ticketsNum.toFixed(2));
+      return;
+    }
+
+    // 纭繚鎵�鏈夋暟鍊奸兘杞崲涓烘暟瀛楃被鍨嬭繘琛岃绠�
+    const ticketsNum = Number(val) / Number(form.taxInclusiveUnitPrice);
+    form.ticketsNum = Number(ticketsNum.toFixed(2));
+  };
+
+  defineExpose({
+    load,
+    form,
+    resetForm,
+  });
 </script>
 
 <style lang="scss" scoped></style>
diff --git a/src/views/productionManagement/processRoute/index.vue b/src/views/productionManagement/processRoute/index.vue
index 10bd0d9..7d5ab5d 100644
--- a/src/views/productionManagement/processRoute/index.vue
+++ b/src/views/productionManagement/processRoute/index.vue
@@ -1,45 +1,45 @@
 <template>
-	<div class="app-container">
-		<div class="search_form">
-			<el-form :model="searchForm" :inline="true">
-				<el-form-item label="瑙勬牸鍚嶇О:">
-					<el-input v-model="searchForm.model" placeholder="璇疯緭鍏�" clearable prefix-icon="Search"
-										style="width: 200px;"
-										@change="handleQuery" />
-				</el-form-item>
-				<el-form-item>
-					<el-button type="primary" @click="handleQuery">鎼滅储</el-button>
-				</el-form-item>
-			</el-form>
-		</div>
-		<div class="table_list">
-			<div style="text-align: right" class="mb10">
-				<el-button type="primary" @click="showNewModal">鏂板宸ヨ壓璺嚎</el-button>
-				<el-button type="danger" @click="handleDelete" :disabled="selectedRows.length === 0" plain>鍒犻櫎宸ヨ壓璺嚎</el-button>
-			</div>
-			<PIMTable
-				rowKey="id"
-				:column="tableColumn"
-				:tableData="tableData"
-				:page="page"
-				:isSelection="true"
-				@selection-change="handleSelectionChange"
-				:tableLoading="tableLoading"
-				@pagination="pagination"
-				:total="page.total"
-			/>
-		</div>
-		<new-process
-      v-if="isShowNewModal"
-      v-model:visible="isShowNewModal"
-      @completed="getList"
+  <div class="app-container">
+    <div class="search_form">
+      <el-form :model="searchForm" :inline="true">
+        <el-form-item label="瑙勬牸鍚嶇О:">
+          <el-input v-model="searchForm.model" placeholder="璇疯緭鍏�" clearable prefix-icon="Search"
+                    style="width: 200px;"
+                    @change="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="table_list">
+      <div style="text-align: right" class="mb10">
+        <el-button type="primary" @click="showNewModal">鏂板宸ヨ壓璺嚎</el-button>
+        <el-button type="danger" @click="handleDelete" :disabled="selectedRows.length === 0" plain>鍒犻櫎宸ヨ壓璺嚎</el-button>
+      </div>
+      <PIMTable
+          rowKey="id"
+          :column="tableColumn"
+          :tableData="tableData"
+          :page="page"
+          :isSelection="true"
+          @selection-change="handleSelectionChange"
+          :tableLoading="tableLoading"
+          @pagination="pagination"
+          :total="page.total"
+      />
+    </div>
+    <new-process
+        v-if="isShowNewModal"
+        v-model:visible="isShowNewModal"
+        @completed="getList"
     />
 
     <edit-process
-      v-if="isShowEditModal"
-      v-model:visible="isShowEditModal"
-      :record="record"
-      @completed="getList"
+        v-if="isShowEditModal"
+        v-model:visible="isShowEditModal"
+        :record="record"
+        @completed="getList"
     />
 
     <route-item-form
@@ -48,7 +48,7 @@
         :record="record"
         @completed="getList"
     />
-	</div>
+  </div>
 </template>
 
 <script setup>
@@ -57,11 +57,13 @@
 import EditProcess from "@/views/productionManagement/processRoute/Edit.vue";
 import RouteItemForm from "@/views/productionManagement/processRoute/ItemsForm.vue";
 import {listPage, del} from "@/api/productionManagement/processRoute.js";
+import { useRouter } from 'vue-router'
 
+const router = useRouter()
 const data = reactive({
-	searchForm: {
+  searchForm: {
     model: "",
-	},
+  },
 });
 const { searchForm } = toRefs(data);
 const tableColumn = ref([
@@ -73,14 +75,14 @@
     label: "浜у搧鍚嶇О",
     prop: "productName",
   },
-	{
-		label: "瑙勬牸鍚嶇О",
-		prop: "model",
-	},
-	{
-		label: "鎻忚堪",
-		prop: "description",
-	},
+  {
+    label: "瑙勬牸鍚嶇О",
+    prop: "model",
+  },
+  {
+    label: "鎻忚堪",
+    prop: "description",
+  },
   {
     dataType: "action",
     label: "鎿嶄綔",
@@ -113,41 +115,41 @@
 const isShowItemModal = ref(false);
 const record = ref({});
 const page = reactive({
-	current: 1,
-	size: 100,
-	total: 0,
+  current: 1,
+  size: 100,
+  total: 0,
 });
 const { proxy } = getCurrentInstance()
 
 // 鏌ヨ鍒楄〃
 /** 鎼滅储鎸夐挳鎿嶄綔 */
 const handleQuery = () => {
-	page.current = 1;
-	getList();
+  page.current = 1;
+  getList();
 };
 
 const pagination = (obj) => {
-	page.current = obj.page;
-	page.size = obj.limit;
-	getList();
+  page.current = obj.page;
+  page.size = obj.limit;
+  getList();
 };
 const getList = () => {
-	tableLoading.value = true;
-	const params = { ...searchForm.value, ...page };
-	params.entryDate = undefined
+  tableLoading.value = true;
+  const params = { ...searchForm.value, ...page };
+  params.entryDate = undefined
   listPage(params).then(res => {
-		tableLoading.value = false;
-		tableData.value = res.data.records.map(item => ({
-			...item,
-		}));
-		page.total = res.data.total;
-	}).catch(err => {
-		tableLoading.value = false;
-	})
+    tableLoading.value = false;
+    tableData.value = res.data.records.map(item => ({
+      ...item,
+    }));
+    page.total = res.data.total;
+  }).catch(err => {
+    tableLoading.value = false;
+  })
 };
 // 琛ㄦ牸閫夋嫨鏁版嵁
 const handleSelectionChange = (selection) => {
-	selectedRows.value = selection;
+  selectedRows.value = selection;
 };
 
 // 鎵撳紑鏂板寮规
@@ -161,8 +163,12 @@
 };
 
 const showItemModal = (row) => {
-  isShowItemModal.value = true
-  record.value = row
+  router.push({
+    path: '/productionManagement/processRouteItem',
+    query: {
+      id: row.id
+    }
+  })
 };
 
 // 鍒犻櫎
@@ -181,7 +187,7 @@
 }
 
 onMounted(() => {
-	getList();
+  getList();
 });
 </script>
 
diff --git a/src/views/productionManagement/processRoute/processRouteItem/index.vue b/src/views/productionManagement/processRoute/processRouteItem/index.vue
new file mode 100644
index 0000000..641ffff
--- /dev/null
+++ b/src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -0,0 +1,503 @@
+<template>
+  <div class="app-container">
+    <div class="operate-button">
+      <div style="margin-bottom: 15px;">
+        <el-button
+            type="primary"
+            @click="isShowProductSelectDialog = true"
+        >
+          閫夋嫨浜у搧
+        </el-button>
+        <el-button type="primary" @click="handleSubmit">纭</el-button>
+      </div>
+
+      <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
+        :data="routeItems"
+        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+        row-key="id"
+        tooltip-effect="dark"
+        class="lims-table"
+        style="cursor: move;"
+    >
+      <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
+      >
+        <template #default="scope" v-if="item.dataType === 'action'">
+          <el-button
+              v-for="(op, opIndex) in item.operation"
+              :key="opIndex"
+              :type="op.type"
+              :link="op.link"
+              size="small"
+              @click.stop="op.clickFun(scope.row)"
+          >
+            {{ op.name }}
+          </el-button>
+        </template>
+
+        <template #default="scope" v-else>
+          <template v-if="item.prop === 'processId'">
+            <el-select
+                v-model="scope.row[item.prop]"
+                style="width: 100%;"
+                @mousedown.stop
+            >
+              <el-option
+                  v-for="process in processOptions"
+                  :key="process.id"
+                  :label="process.name"
+                  :value="process.id"
+              />
+            </el-select>
+          </template>
+          <template v-else>
+            {{ scope.row[item.prop] || '-' }}
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 浣跨敤鏅�歞iv鏇夸唬el-steps -->
+    <div
+        v-else
+        ref="stepsContainer"
+        class="mb5 custom-steps"
+    >
+      <div
+          v-for="(item, index) in routeItems"
+          :key="item.id"
+          class="custom-step draggable-step"
+          :data-id="item.id"
+          style="cursor: move; flex: 0 0 auto; min-width: 220px;"
+      >
+        <div class="step-content">
+          <div class="step-number">{{ index + 1 }}</div>
+          <el-card
+              :header="item.productName"
+              class="step-card"
+              style="cursor: move;"
+          >
+            <div class="step-card-content">
+              <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>
+            </div>
+            <template #footer>
+              <div class="step-card-footer">
+                <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">鍒犻櫎</el-button>
+              </div>
+            </template>
+          </el-card>
+        </div>
+      </div>
+    </div>
+
+    <ProductSelectDialog
+        v-model="isShowProductSelectDialog"
+        @confirm="handelSelectProducts"
+    />
+  </div>
+</template>
+
+<script setup>
+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 { processList } from "@/api/productionManagement/productionProcess.js";
+import Sortable from 'sortablejs';
+import { useRoute, useRouter } from 'vue-router'
+
+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 route = useRoute()
+const router = useRouter()
+const routeId = computed({
+  get() {
+    return route.query.id;
+  },
+
+  set(val) {
+    emit('update:router', val)
+  }
+});
+
+
+const tableColumn = ref([
+  { label: "浜у搧鍚嶇О", prop: "productName"},
+  { label: "瑙勬牸鍚嶇О", prop: "model" },
+  { label: "鍗曚綅", prop: "unit" },
+  { label: "宸ュ簭鍚嶇О", prop: "processId", width: 200 },
+  {
+    dataType: "action",
+    label: "鎿嶄綔",
+    align: "center",
+    fixed: "right",
+    width: 100,
+    operation: [
+      {
+        name: "鍒犻櫎",
+        type: "danger",
+        link: true,
+        clickFun: (row) => {
+          const idx = routeItems.value.findIndex(item => item.id === row.id);
+          if (idx > -1) {
+            removeItem(idx)
+          }
+        }
+      }
+    ]
+  }
+]);
+
+const removeItem = (index) => {
+  routeItems.value.splice(index, 1);
+  nextTick(() => initSortable());
+};
+
+const removeItemByID = (id) => {
+  const idx = routeItems.value.findIndex(item => item.id === id);
+  if (idx > -1) {
+    routeItems.value.splice(idx, 1);
+    nextTick(() => initSortable());
+  }
+};
+
+const handelSelectProducts = (products) => {
+  destroySortable();
+
+  const newData = products.map(({ id, ...product }) => ({
+    ...product,
+    productModelId: id,
+    routeId: routeId.value,
+    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+    processId: undefined
+  }));
+
+  console.log('閫夋嫨浜у搧鍓嶆暟缁�:', routeItems.value);
+  routeItems.value.push(...newData);
+  routeItems.value = [...routeItems.value];
+  console.log('閫夋嫨浜у搧鍚庢暟缁�:', 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: routeId.value })
+      .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;
+      })
+      .catch(err => {
+        console.error("鑾峰彇宸ュ簭澶辫触锛�", err);
+      });
+};
+
+const { proxy } = getCurrentInstance() || {};
+
+const handleSubmit = () => {
+  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
+  if (hasEmptyProcess) {
+    proxy?.$modal?.msgError("璇蜂负鎵�鏈夐」鐩�夋嫨宸ュ簭");
+    return;
+  }
+
+  addOrUpdateProcessRouteItem({
+    routeId: routeId.value,
+    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
+  })
+      .then(res => {
+        router.push({
+          path: '/productionManagement/processRoute',
+        })
+        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;
+
+    // 淇敼锛氱洿鎺ヤ娇鐢╯tepsContainer.value浣滀负鎷栨嫿瀹瑰櫒
+    const stepsList = stepsContainer.value;
+    if (!stepsList) {
+      console.warn('鏈壘鍒版楠ゆ潯鎷栨嫿瀹瑰櫒');
+      return;
+    }
+
+    // 淇敼锛氱畝鍖栨嫋鎷介厤缃�
+    stepsSortable = new Sortable(stepsList, {
+      animation: 150,
+      ghostClass: 'sortable-ghost',
+      draggable: '.draggable-step', // 鍙嫋鎷藉厓绱�
+      handle: '.draggable-step, .step-card', // 鎷栨嫿鎵嬫焺
+      filter: '.el-button, .el-select, .el-input', // 杩囨护鎸夐挳/閫夋嫨鍣�
+      forceFallback: true,
+      fallbackClass: 'sortable-fallback',
+      preventOnFilter: true,
+      scroll: true,
+      scrollSensitivity: 30,
+      scrollSpeed: 10,
+      bubbleScroll: true,
+      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({
+  handleSubmit,
+});
+</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;
+}
+
+/* 淇敼锛氳嚜瀹氫箟姝ラ鏉″鍣ㄦ牱寮� */
+.custom-steps {
+  min-height: 100px;
+  padding: 10px 0;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  align-items: flex-start;
+}
+
+/* 淇敼锛氳嚜瀹氫箟姝ラ椤规牱寮� */
+.custom-step {
+  cursor: move !important;
+  padding: 8px;
+  position: relative;
+  transition: all 0.2s ease;
+  flex: 0 0 auto;
+  min-width: 220px;
+  touch-action: none;
+}
+
+/* 鎷栨嫿鎮诞鏍峰紡锛屾彁绀哄彲鎷栨嫿 */
+.custom-step:hover {
+  background-color: rgba(64, 158, 255, 0.05);
+  transform: translateY(-2px);
+}
+
+.sortable-ghost {
+  opacity: 0.4;
+  background-color: #f5f7fa !important;
+  border: 2px dashed #409eff;
+  margin: 10px;
+  transform: scale(1.02);
+}
+
+.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;
+}
+
+.step-card {
+  cursor: move !important;
+  transition: box-shadow 0.2s ease;
+  user-select: none;
+  -webkit-user-select: none;
+  pointer-events: auto;
+  margin: 10px;
+  height: 260px;
+}
+
+.step-card:hover {
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.step-content {
+  width: 245px;
+  user-select: none;
+}
+
+.step-card-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  height: 140px;
+}
+
+.step-card-footer {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 10px;
+}
+
+/* 鑷畾涔夊簭鍙锋牱寮忎紭鍖� */
+.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>

--
Gitblit v1.9.3