From 7543b32e5c64bb415af4368123f2c1cbefe94549 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期四, 15 一月 2026 14:44:37 +0800
Subject: [PATCH] fix: 完成工艺路线项目重构

---
 src/views/productionManagement/productStructure/Detail/index.vue       |    5 
 src/views/productionManagement/processRoute/index.vue                  |    7 
 src/api/productionManagement/processRouteItem.js                       |   41 ++-
 src/api/productionManagement/processRoute.js                           |    8 
 src/views/productionManagement/processRoute/processRouteItem/index.vue |  546 ++++++++++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 581 insertions(+), 26 deletions(-)

diff --git a/src/api/productionManagement/processRoute.js b/src/api/productionManagement/processRoute.js
index 4d16775..c13b2fc 100644
--- a/src/api/productionManagement/processRoute.js
+++ b/src/api/productionManagement/processRoute.js
@@ -31,4 +31,12 @@
     method: 'put',
     data: data,
   })
+}
+
+// 鑾峰彇璇︽儏
+export function getById(id) {
+  return request({
+    url: `/processRoute/${id}`,
+    method: 'get',
+  })
 }
\ No newline at end of file
diff --git a/src/api/productionManagement/processRouteItem.js b/src/api/productionManagement/processRouteItem.js
index ad4861c..9e81406 100644
--- a/src/api/productionManagement/processRouteItem.js
+++ b/src/api/productionManagement/processRouteItem.js
@@ -3,17 +3,36 @@
 
 // 鍒楄〃鏌ヨ
 export function findProcessRouteItemList(query) {
-    return request({
-        url: "/processRouteItem/list",
-        method: "get",
-        params: query,
-    });
+  return request({
+    url: "/processRouteItem/list",
+    method: "get",
+    params: query,
+  });
 }
 
 export function addOrUpdateProcessRouteItem(data) {
-    return request({
-        url: "/processRouteItem",
-        method: "post",
-        data: data,
-    });
-}
\ No newline at end of file
+  return request({
+    url: "/processRouteItem",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鎺掑簭鎺ュ彛
+export function sortProcessRouteItem(data) {
+  return request({
+    url: "/processRouteItem/sort",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鎵归噺鍒犻櫎鎺ュ彛
+export function batchDeleteProcessRouteItem(ids) {
+  // 灏唅d鏁扮粍杞崲涓洪�楀彿鍒嗛殧鐨勫瓧绗︿覆锛屾嫾鎺ュ埌URL鍚庨潰
+  const idsStr = Array.isArray(ids) ? ids.join(",") : ids;
+  return request({
+    url: `/processRouteItem/batchDelete/${idsStr}`,
+    method: "delete",
+  });
+}
diff --git a/src/views/productionManagement/processRoute/index.vue b/src/views/productionManagement/processRoute/index.vue
index 74d2d23..6bd4900 100644
--- a/src/views/productionManagement/processRoute/index.vue
+++ b/src/views/productionManagement/processRoute/index.vue
@@ -170,7 +170,12 @@
   router.push({
     path: '/productionManagement/processRouteItem',
     query: {
-      id: row.id
+      id: row.id,
+      processRouteCode: row.processRouteCode || '',
+      productName: row.productName || '',
+      model: row.model || '',
+      bomNo: row.bomNo || '',
+      description: row.description || ''
     }
   })
 };
diff --git a/src/views/productionManagement/processRoute/processRouteItem/index.vue b/src/views/productionManagement/processRoute/processRouteItem/index.vue
index 7e32396..31b4a74 100644
--- a/src/views/productionManagement/processRoute/processRouteItem/index.vue
+++ b/src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -1,12 +1,71 @@
 <template>
   <div class="app-container">
-    <PageHeader content="宸ヨ壓璺嚎椤圭洰">
-      <template #right-button>
-        <el-button type="primary" @click="handleAdd">鏂板</el-button>
-      </template>
-    </PageHeader>
+    <PageHeader content="宸ヨ壓璺嚎椤圭洰" />
     
+    <!-- 宸ヨ壓璺嚎淇℃伅灞曠ず -->
+    <div v-if="routeInfo.processRouteCode" class="section-title" style="margin-bottom: 12px;">宸ヨ壓璺嚎淇℃伅</div>
+    <el-card v-if="routeInfo.processRouteCode" class="route-info-card" shadow="hover">
+      <div class="route-info">
+        <div class="info-item">
+          <div class="info-label-wrapper">
+            <span class="info-label">宸ヨ壓璺嚎缂栧彿</span>
+          </div>
+          <div class="info-value-wrapper">
+            <span class="info-value">{{ routeInfo.processRouteCode }}</span>
+          </div>
+        </div>
+        <div class="info-item">
+          <div class="info-label-wrapper">
+            <span class="info-label">浜у搧鍚嶇О</span>
+          </div>
+          <div class="info-value-wrapper">
+            <span class="info-value">{{ routeInfo.productName || '-' }}</span>
+          </div>
+        </div>
+        <div class="info-item">
+          <div class="info-label-wrapper">
+            <span class="info-label">瑙勬牸鍚嶇О</span>
+          </div>
+          <div class="info-value-wrapper">
+            <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 full-width" v-if="routeInfo.description">
+          <div class="info-label-wrapper">
+            <span class="info-label">鎻忚堪</span>
+          </div>
+          <div class="info-value-wrapper">
+            <span class="info-value">{{ routeInfo.description }}</span>
+          </div>
+        </div>
+      </div>
+    </el-card>
+    
+    <!-- 琛ㄦ牸瑙嗗浘 -->
+    <div v-if="viewMode === 'table'" class="section-header">
+      <div class="section-title">宸ヨ壓璺嚎椤圭洰鍒楄〃</div>
+      <div class="section-actions">
+        <el-button 
+            icon="Grid" 
+            @click="toggleView"
+            style="margin-right: 10px;"
+        >
+          鍗$墖瑙嗗浘
+        </el-button>
+        <el-button type="primary" @click="handleAdd">鏂板</el-button>
+      </div>
+    </div>
     <el-table
+        v-if="viewMode === 'table'"
+        ref="tableRef"
         v-loading="tableLoading"
         border
         :data="tableData"
@@ -31,6 +90,60 @@
         </template>
       </el-table-column>
     </el-table>
+    
+    <!-- 鍗$墖瑙嗗浘 -->
+    <template v-else>
+      <div class="section-header">
+        <div class="section-title">宸ヨ壓璺嚎椤圭洰鍒楄〃</div>
+        <div class="section-actions">
+          <el-button 
+              icon="Menu" 
+              @click="toggleView"
+              style="margin-right: 10px;"
+          >
+            琛ㄦ牸瑙嗗浘
+          </el-button>
+          <el-button type="primary" @click="handleAdd">鏂板</el-button>
+        </div>
+      </div>
+      <div v-loading="tableLoading" class="card-container">
+        <div 
+            ref="cardsContainer" 
+            class="cards-wrapper"
+        >
+        <div
+            v-for="(item, index) in tableData"
+            :key="item.id || index"
+            class="process-card"
+            :data-index="index"
+        >
+          <!-- 搴忓彿鍦嗗湀 -->
+          <div class="card-header">
+            <div class="card-number">{{ index + 1 }}</div>
+            <div class="card-process-name">{{ getProcessName(item.processId) || '-' }}</div>
+          </div>
+          
+          <!-- 浜у搧淇℃伅 -->
+          <div class="card-content">
+            <div v-if="item.productName" class="product-info">
+              <div class="product-name">{{ item.productName }}</div>
+              <div v-if="item.model" class="product-model">
+                {{ item.model }}
+                <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> -->
+              </div>
+            </div>
+            <div v-else class="product-info empty">鏆傛棤浜у搧淇℃伅</div>
+          </div>
+          
+          <!-- 鎿嶄綔鎸夐挳 -->
+          <div class="card-footer">
+            <el-button type="primary" link size="small" @click="handleEdit(item)">缂栬緫</el-button>
+            <el-button type="danger" link size="small" @click="handleDelete(item)">鍒犻櫎</el-button>
+          </div>
+        </div>
+      </div>
+      </div>
+    </template>
 
     <!-- 鏂板/缂栬緫寮圭獥 -->
     <el-dialog
@@ -95,12 +208,13 @@
 </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, sortProcessRouteItem, batchDeleteProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
 import { processList } from "@/api/productionManagement/productionProcess.js";
 import { useRoute } from 'vue-router'
 import { ElMessageBox } from 'element-plus'
+import Sortable from 'sortablejs'
 
 const route = useRoute()
 const { proxy } = getCurrentInstance() || {};
@@ -113,9 +227,30 @@
 const operationType = ref('add'); // add | edit
 const formRef = ref(null);
 const submitLoading = ref(false);
+const cardsContainer = ref(null);
+const tableRef = ref(null);
+const viewMode = ref('table'); // table | card
+const routeInfo = ref({
+  processRouteCode: '',
+  productName: '',
+  model: '',
+  bomNo: '',
+  description: ''
+});
 
 const processOptions = ref([]);
 const showProductSelectDialog = ref(false);
+let tableSortable = null;
+let cardSortable = null;
+
+// 鍒囨崲瑙嗗浘
+const toggleView = () => {
+  viewMode.value = viewMode.value === 'table' ? 'card' : 'table';
+  // 鍒囨崲瑙嗗浘鍚庨噸鏂板垵濮嬪寲鎷栨嫿鎺掑簭
+  nextTick(() => {
+    initSortable();
+  });
+};
 
 const form = ref({
   id: undefined,
@@ -146,6 +281,10 @@
     .then(res => {
       tableData.value = res.data || [];
       tableLoading.value = false;
+      // 鍒楄〃鍔犺浇瀹屾垚鍚庡垵濮嬪寲鎷栨嫿鎺掑簭
+      nextTick(() => {
+        initSortable();
+      });
     })
     .catch(err => {
       tableLoading.value = false;
@@ -163,6 +302,17 @@
     .catch(err => {
       console.error("鑾峰彇宸ュ簭澶辫触锛�", err);
     });
+};
+
+// 鑾峰彇宸ヨ壓璺嚎璇︽儏锛堜粠璺敱鍙傛暟鑾峰彇锛�
+const getRouteInfo = () => {
+  routeInfo.value = {
+    processRouteCode: route.query.processRouteCode || '',
+    productName: route.query.productName || '',
+    model: route.query.model || '',
+    bomNo: route.query.bomNo || '',
+    description: route.query.description || ''
+  };
 };
 
 // 鏂板
@@ -195,11 +345,8 @@
     type: 'warning'
   })
     .then(() => {
-      // 璋冪敤鍒犻櫎鎺ュ彛锛屼紶鍗曚釜瀵硅薄锛堝寘鍚玦d锛�
-      addOrUpdateProcessRouteItem({
-        id: row.id,
-        routeId: routeId.value,
-      })
+      // 璋冪敤鎵归噺鍒犻櫎鎺ュ彛锛屼紶閫抜d鏁扮粍
+      batchDeleteProcessRouteItem([row.id])
         .then(() => {
           proxy?.$modal?.msgSuccess('鍒犻櫎鎴愬姛');
           getList();
@@ -239,8 +386,13 @@
       };
 
       if (operationType.value === 'add') {
-        // 鏂板锛氫紶鍗曚釜瀵硅薄
-        addOrUpdateProcessRouteItem(submitData)
+        // 鏂板锛氫紶鍗曚釜瀵硅薄锛屽寘鍚玠ragSort瀛楁
+        // dragSort = 褰撳墠鍒楄〃闀垮害 + 1锛岃〃绀烘柊澧炶褰曟帓鍦ㄦ渶鍚�
+        const dragSort = tableData.value.length + 1;
+        addOrUpdateProcessRouteItem({
+          ...submitData,
+          dragSort: dragSort
+        })
           .then(() => {
             proxy?.$modal?.msgSuccess('鏂板鎴愬姛');
             closeDialog();
@@ -294,18 +446,384 @@
   resetForm();
 };
 
+// 鍒濆鍖栨嫋鎷芥帓搴�
+const initSortable = () => {
+  destroySortable();
+  
+  if (viewMode.value === 'table') {
+    // 琛ㄦ牸瑙嗗浘鐨勬嫋鎷芥帓搴�
+    if (!tableRef.value) return;
+    
+    const tbody = tableRef.value.$el.querySelector('.el-table__body tbody') ||
+        tableRef.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 || !tableData.value[evt.oldIndex]) return;
+
+        // 閲嶆柊鎺掑簭鏁扮粍
+        const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
+        tableData.value.splice(evt.newIndex, 0, moveItem);
+        
+        // 璁$畻鏂扮殑搴忓彿锛坉ragSort浠�1寮�濮嬶級
+        const newIndex = evt.newIndex;
+        const dragSort = newIndex + 1;
+        
+        // 璋冪敤鎺掑簭鎺ュ彛
+        if (moveItem.id) {
+          sortProcessRouteItem({
+            id: moveItem.id,
+            dragSort: dragSort
+          })
+            .then(() => {
+              // 鏇存柊鎵�鏈夎鐨刣ragSort
+              tableData.value.forEach((item, index) => {
+                if (item.id) {
+                  item.dragSort = index + 1;
+                }
+              });
+              proxy?.$modal?.msgSuccess('鎺掑簭鎴愬姛');
+            })
+            .catch((err) => {
+              // 鎺掑簭澶辫触锛屾仮澶嶅師鏁扮粍
+              tableData.value.splice(newIndex, 1);
+              tableData.value.splice(evt.oldIndex, 0, moveItem);
+              proxy?.$modal?.msgError('鎺掑簭澶辫触');
+              console.error("鎺掑簭澶辫触锛�", err);
+            });
+        }
+      }
+    });
+  } else {
+    // 鍗$墖瑙嗗浘鐨勬嫋鎷芥帓搴�
+    if (!cardsContainer.value) return;
+
+    cardSortable = new Sortable(cardsContainer.value, {
+      animation: 150,
+      ghostClass: 'sortable-ghost',
+      handle: '.process-card',
+      filter: '.el-button',
+      onEnd: (evt) => {
+        if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex]) return;
+
+        // 閲嶆柊鎺掑簭鏁扮粍
+        const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
+        tableData.value.splice(evt.newIndex, 0, moveItem);
+        
+        // 璁$畻鏂扮殑搴忓彿锛坉ragSort浠�1寮�濮嬶級
+        const newIndex = evt.newIndex;
+        const dragSort = newIndex + 1;
+        
+        // 璋冪敤鎺掑簭鎺ュ彛
+        if (moveItem.id) {
+          sortProcessRouteItem({
+            id: moveItem.id,
+            dragSort: dragSort
+          })
+            .then(() => {
+              // 鏇存柊鎵�鏈夎鐨刣ragSort
+              tableData.value.forEach((item, index) => {
+                if (item.id) {
+                  item.dragSort = index + 1;
+                }
+              });
+              proxy?.$modal?.msgSuccess('鎺掑簭鎴愬姛');
+            })
+            .catch((err) => {
+              // 鎺掑簭澶辫触锛屾仮澶嶅師鏁扮粍
+              tableData.value.splice(newIndex, 1);
+              tableData.value.splice(evt.oldIndex, 0, moveItem);
+              proxy?.$modal?.msgError('鎺掑簭澶辫触');
+              console.error("鎺掑簭澶辫触锛�", err);
+            });
+        }
+      }
+    });
+  }
+};
+
+// 閿�姣佹嫋鎷芥帓搴�
+const destroySortable = () => {
+  if (tableSortable) {
+    tableSortable.destroy();
+    tableSortable = null;
+  }
+  if (cardSortable) {
+    cardSortable.destroy();
+    cardSortable = null;
+  }
+};
+
 onMounted(() => {
+  getRouteInfo();
   getList();
   getProcessList();
+});
+
+onUnmounted(() => {
+  destroySortable();
 });
 </script>
 
 <style scoped>
+.card-container {
+  padding: 20px 0;
+}
+
+.cards-wrapper {
+  display: flex;
+  gap: 16px;
+  overflow-x: auto;
+  padding: 10px 0;
+  min-height: 200px;
+}
+
+.cards-wrapper::-webkit-scrollbar {
+  height: 8px;
+}
+
+.cards-wrapper::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 4px;
+}
+
+.cards-wrapper::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 4px;
+}
+
+.cards-wrapper::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+.process-card {
+  flex-shrink: 0;
+  width: 220px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  cursor: move;
+  transition: all 0.3s;
+}
+
+.process-card:hover {
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  transform: translateY(-2px);
+}
+
+.card-header {
+  text-align: center;
+  margin-bottom: 12px;
+}
+
+.card-number {
+  width: 36px;
+  height: 36px;
+  line-height: 36px;
+  border-radius: 50%;
+  background: #409eff;
+  color: #fff;
+  font-weight: bold;
+  font-size: 16px;
+  margin: 0 auto 8px;
+}
+
+.card-process-name {
+  font-size: 14px;
+  color: #333;
+  font-weight: 500;
+  word-break: break-all;
+}
+
+.card-content {
+  flex: 1;
+  margin-bottom: 12px;
+  min-height: 60px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.product-info {
+  font-size: 13px;
+  color: #666;
+  text-align: center;
+  width: 100%;
+}
+
+.product-info.empty {
+  color: #999;
+  text-align: center;
+  padding: 20px 0;
+}
+
+.product-name {
+  margin-bottom: 6px;
+  word-break: break-all;
+  line-height: 1.5;
+  text-align: center;
+}
+
+.product-model {
+  color: #909399;
+  font-size: 12px;
+  word-break: break-all;
+  line-height: 1.5;
+  text-align: center;
+}
+
+.product-unit {
+  margin-left: 4px;
+  color: #409eff;
+}
+
+.card-footer {
+  display: flex;
+  justify-content: space-around;
+  padding-top: 12px;
+  border-top: 1px solid #f0f0f0;
+}
+
+.card-footer .el-button {
+  padding: 0;
+  font-size: 12px;
+}
+
+:deep(.sortable-ghost) {
+  opacity: 0.5;
+  background-color: #f5f7fa !important;
+}
+
+:deep(.sortable-drag) {
+  opacity: 0.8;
+}
+
+/* 琛ㄦ牸瑙嗗浘鏍峰紡 */
 :deep(.el-table__row) {
   transition: background-color 0.2s;
+  cursor: move;
 }
 
 :deep(.el-table__row:hover) {
   background-color: #f9fafc !important;
 }
+
+/* 鍖哄煙鏍囬鏍峰紡 */
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  padding-left: 12px;
+  position: relative;
+  margin-bottom: 0;
+}
+
+.section-title::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 3px;
+  height: 16px;
+  background: #409eff;
+  border-radius: 2px;
+}
+
+.section-actions {
+  display: flex;
+  align-items: center;
+}
+
+/* 宸ヨ壓璺嚎淇℃伅鍗$墖鏍峰紡 */
+.route-info-card {
+  margin-bottom: 20px;
+  border: 1px solid #e4e7ed;
+  background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.route-info {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+  gap: 16px;
+  padding: 4px;
+}
+
+.info-item {
+  display: flex;
+  flex-direction: column;
+  background: #ffffff;
+  border-radius: 6px;
+  padding: 14px 16px;
+  border: 1px solid #f0f2f5;
+  transition: all 0.3s ease;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+.info-item:hover {
+  border-color: #409eff;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
+  transform: translateY(-1px);
+}
+
+.info-item.full-width {
+  grid-column: 1 / -1;
+}
+
+.info-label-wrapper {
+  margin-bottom: 8px;
+}
+
+.info-label {
+  display: inline-block;
+  color: #909399;
+  font-size: 12px;
+  font-weight: 500;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  padding: 2px 0;
+  position: relative;
+}
+
+.info-label::after {
+  content: '';
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  width: 20px;
+  height: 2px;
+  background: linear-gradient(90deg, #409eff, transparent);
+  border-radius: 1px;
+}
+
+.info-value-wrapper {
+  flex: 1;
+}
+
+.info-value {
+  display: block;
+  color: #303133;
+  font-size: 15px;
+  font-weight: 500;
+  line-height: 1.5;
+  word-break: break-all;
+}
 </style>
diff --git a/src/views/productionManagement/productStructure/Detail/index.vue b/src/views/productionManagement/productStructure/Detail/index.vue
index dda0834..b702eb7 100644
--- a/src/views/productionManagement/productStructure/Detail/index.vue
+++ b/src/views/productionManagement/productStructure/Detail/index.vue
@@ -271,6 +271,11 @@
 };
 
 onMounted(() => {
+  // 浠庤矾鐢卞弬鏁板洖鏄炬暟鎹�
+  tableData[0].productName = routeProductName.value;
+  tableData[0].model = routeProductModelName.value;
+  tableData[0].bomNo = routeBomNo.value;
+  
   fetchData();
   fetchProcessOptions();
 });

--
Gitblit v1.9.3