From 21c1360819a78ab734046fe6e0aa91b4da9f510a Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 18 四月 2026 10:02:55 +0800
Subject: [PATCH] 生产订单工艺路线

---
 src/views/productionManagement/processRoute/processRouteItem/index.vue | 1340 +++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 960 insertions(+), 380 deletions(-)

diff --git a/src/views/productionManagement/processRoute/processRouteItem/index.vue b/src/views/productionManagement/processRoute/processRouteItem/index.vue
index 641ffff..c2c24b4 100644
--- a/src/views/productionManagement/processRoute/processRouteItem/index.vue
+++ b/src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -1,312 +1,695 @@
 <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>
+    <PageHeader content="浜у搧閮ㄤ欢" />
+    
+    <!-- 宸ヨ壓璺嚎淇℃伅灞曠ず -->
+    <!-- <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-switch
-          v-model="isTable"
-          inline-prompt
-          active-text="琛ㄦ牸"
-          inactive-text="鍒楄〃"
-          @change="handleViewChange"
-      />
+    </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="isTable"
-        ref="multipleTable"
+        v-if="viewMode === 'table'"
+        ref="tableRef"
         v-loading="tableLoading"
         border
-        :data="routeItems"
+        :data="tableData"
         :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">
+      <el-table-column align="center" label="搴忓彿" width="60" type="index" />
+      <el-table-column label="浜у搧鍚嶇О" prop="name" min-width="140" show-overflow-tooltip>
         <template #default="scope">
-          {{ scope.$index + 1 }}
+          {{ getProcessField(scope.row, 'name') }}
         </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>
+      <el-table-column label="浜у搧瑙勬牸" prop="productModel" min-width="120" show-overflow-tooltip>
+        <template #default="scope">
+          {{ getProcessField(scope.row, 'productModel') }}
         </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>
+      </el-table-column>
+      <el-table-column label="閮ㄤ欢缂栧彿" prop="no" width="120" show-overflow-tooltip>
+        <template #default="scope">
+          {{ getProcessField(scope.row, 'no') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="閮ㄤ欢绫诲瀷" prop="typeText" width="120" show-overflow-tooltip>
+        <template #default="scope">
+          {{ getProcessTypeText(getProcessRaw(scope.row)?.type) || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="璁″垝宸ユ椂(灏忔椂)" prop="salaryQuota" width="130" align="center">
+        <template #default="scope">
+          {{ getProcessField(scope.row, 'salaryQuota') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="璁″垝浜哄憳" prop="plannerName" width="100" show-overflow-tooltip>
+        <template #default="scope">
+          {{ getProcessField(scope.row, 'plannerName') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="鏄惁璐ㄦ" prop="isQuality" width="90" align="center">
+        <template #default="scope">
+          {{ scope.row.isQuality ? '鏄�' : '鍚�' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="澶囨敞" prop="remark" min-width="100" show-overflow-tooltip>
+        <template #default="scope">
+          {{ getProcessField(scope.row, 'remark') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="鏇存柊鏃堕棿" prop="updateTime" width="160" align="center">
+        <template #default="scope">
+          {{ getProcessField(scope.row, 'updateTime') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="鎿嶄綔" align="center" fixed="right" width="150">
+        <template #default="scope">
+          <el-button type="primary" link size="small" @click="handleEdit(scope.row)" :disabled="scope.row.isComplete">缂栬緫</el-button>
+          <el-button type="danger" link size="small" @click="handleDelete(scope.row)" :disabled="scope.row.isComplete">鍒犻櫎</el-button>
         </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;"
+    
+    <!-- 鍗$墖瑙嗗浘 -->
+    <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;"
           >
-            <div class="step-card-content">
-              <p>{{ item.model }}</p>
-              <p>{{ item.unit }}</p>
+            琛ㄦ牸瑙嗗浘
+          </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">{{ getProcessField(item, 'name') }}</div>
+          </div>
+          
+          <!-- 涓庡伐搴忎富琛ㄤ竴鑷寸殑绠�瑕佷俊鎭� -->
+          <div class="card-content">
+            <div class="product-info">
+              <div class="product-name">{{ getProcessField(item, 'productModel') }}</div>
+              <div v-if="getProcessRaw(item)?.no" class="product-model">缂栧彿 {{ getProcessRaw(item)?.no }}</div>
+              <div v-if="getProcessTypeText(getProcessRaw(item)?.type)" class="product-model">
+                {{ getProcessTypeText(getProcessRaw(item)?.type) }}
+              </div>
+              <el-tag type="primary" class="product-tag" v-if="item.isQuality">璐ㄦ</el-tag>
+            </div>
+          </div>
+          
+          <!-- 鎿嶄綔鎸夐挳 -->
+          <div class="card-footer">
+            <el-button type="primary" link size="small" @click="handleEdit(item)" :disabled="item.isComplete">缂栬緫</el-button>
+            <el-button type="danger" link size="small" @click="handleDelete(item)" :disabled="item.isComplete">鍒犻櫎</el-button>
+          </div>
+        </div>
+      </div>
+      </div>
+    </template>
+
+    <!-- 鏂板/缂栬緫寮圭獥锛堝竷灞�銆佸瓧娈典笌宸ュ簭/閮ㄤ欢椤� New銆丒dit 涓�鑷达紱鎻愪氦鍙傛暟浠嶄负鍘熸帴鍙e瓧娈碉級 -->
+    <el-dialog
+        v-model="dialogVisible"
+        :title="operationType === 'add' ? '鏂板宸ヨ壓璺嚎椤圭洰' : '缂栬緫宸ヨ壓璺嚎椤圭洰'"
+        width="760"
+        @close="closeDialog"
+    >
+      <el-form
+          ref="formRef"
+          :model="form"
+          :rules="rules"
+          label-width="140px"
+          label-position="top"
+      >
+        <el-row :gutter="16">
+          <el-col :span="24">
+            <el-form-item label="閮ㄤ欢" prop="processId">
               <el-select
-                  v-model="item.processId"
-                  style="width: 100%;"
-                  @mousedown.stop
+                  v-model="form.processId"
+                  placeholder="璇烽�夋嫨閮ㄤ欢"
+                  clearable
+                  filterable
+                  style="width: 100%"
               >
                 <el-option
                     v-for="process in processOptions"
                     :key="process.id"
-                    :label="process.name"
+                    :label="formatProcessOptionLabel(process)"
                     :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>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item
+                label="浜у搧鍚嶇О锛�"
+                prop="productId"
+            >
+              <el-tree-select
+                  v-model="form.productId"
+                  placeholder="璇烽�夋嫨浜у搧鍚嶇О"
+                  clearable
+                  filterable
+                  check-strictly
+                  :data="productCategoryOptions"
+                  :render-after-expand="false"
+                  style="width: 100%"
+                  @change="handleProductChange"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item
+                label="浜у搧瑙勬牸锛�"
+                prop="productModelId"
+            >
+              <el-select
+                  v-model="form.productModelId"
+                  placeholder="璇烽�夋嫨浜у搧瑙勬牸"
+                  clearable
+                  filterable
+                  :disabled="!form.productId"
+                  style="width: 100%"
+              >
+                <el-option
+                    v-for="item in modelOptions"
+                    :key="item.id"
+                    :label="item.model"
+                    :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="閮ㄤ欢缂栧彿">
+              <el-input :model-value="selectedProcess?.no ?? ''" readonly />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="閮ㄤ欢绫诲瀷">
+              <el-input :model-value="getProcessTypeText(selectedProcess?.type) || '-'" readonly />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璁″垝宸ユ椂(灏忔椂)">
+              <el-input :model-value="selectedProcess?.salaryQuota != null && selectedProcess?.salaryQuota !== '' ? String(selectedProcess.salaryQuota) : ''" readonly>
+                <template #append>灏忔椂</template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="璁″垝浜哄憳">
+              <el-input :model-value="selectedProcess?.plannerName ?? ''" readonly />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鏄惁璐ㄦ" prop="isQuality">
+              <el-switch v-model="form.isQuality" :active-value="true" inactive-value="false"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="澶囨敞">
+              <el-input :model-value="selectedProcess?.remark ?? ''" type="textarea" readonly />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
 
-    <ProductSelectDialog
-        v-model="isShowProductSelectDialog"
-        @confirm="handelSelectProducts"
-    />
+      <template #footer>
+        <el-button type="primary" @click="handleSubmit" :loading="submitLoading">纭畾</el-button>
+        <el-button @click="closeDialog">鍙栨秷</el-button>
+      </template>
+    </el-dialog>
   </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 { findProcessRouteItemList, addOrUpdateProcessRouteItem, sortProcessRouteItem, batchDeleteProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
+import { findProductProcessRouteItemList, deleteRouteItem, addRouteItem, addOrUpdateProductProcessRouteItem, sortRouteItem } from "@/api/productionManagement/productProcessRoute.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);
+import { modelListPage, productTreeList } from "@/api/basicData/product";
+import { useRoute } from 'vue-router'
+import { ElMessageBox } from 'element-plus'
+import Sortable from 'sortablejs'
 
 const route = useRoute()
-const router = useRouter()
-const routeId = computed({
-  get() {
-    return route.query.id;
-  },
+const { proxy } = getCurrentInstance() || {};
 
-  set(val) {
-    emit('update:router', val)
-  }
+const routeId = computed(() => route.query.id);
+const orderId = computed(() => route.query.orderId);
+const pageType = computed(() => route.query.type);
+
+const tableLoading = ref(false);
+const tableData = ref([]);
+const dialogVisible = ref(false);
+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 productCategoryOptions = ref([]);
+const modelOptions = ref([]);
+let tableSortable = null;
+let cardSortable = null;
 
-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瀹屽叏娓叉煋
+// 鍒囨崲瑙嗗浘
+const toggleView = () => {
+  viewMode.value = viewMode.value === 'table' ? 'card' : 'table';
+  // 鍒囨崲瑙嗗浘鍚庨噸鏂板垵濮嬪寲鎷栨嫿鎺掑簭
   nextTick(() => {
-    // 寮哄埗閲嶆柊娓叉煋缁勪欢
-    if (proxy?.$forceUpdate) {
-      proxy.$forceUpdate();
-    }
-
-    const temp = [...routeItems.value];
-    routeItems.value = [];
-    nextTick(() => {
-      routeItems.value = temp;
-      initSortable();
-    });
+    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 form = ref({
+  id: undefined,
+  routeId: routeId.value,
+  processId: undefined,
+  productId: undefined,
+  productModelId: undefined,
+  productName: "",
+  model: "",
+  isQuality: false,
+});
+
+const rules = {
+  processId: [{ required: true, message: '璇烽�夋嫨閮ㄤ欢', trigger: 'change' }],
+  productId: [{ required: true, message: '璇烽�夋嫨浜у搧鍚嶇О', trigger: 'change' }],
+  productModelId: [{ required: true, message: '璇烽�夋嫨浜у搧瑙勬牸', trigger: 'change' }],
 };
 
-const findProcessList = () => {
-  processList({})
-      .then(res => {
-        processOptions.value = res.data;
-      })
-      .catch(err => {
-        console.error("鑾峰彇宸ュ簭澶辫触锛�", err);
-      });
+const selectedProcess = computed(() => {
+  if (form.value.processId === undefined || form.value.processId === null || form.value.processId === '') return null;
+  return processOptions.value.find(p => String(p.id) === String(form.value.processId)) || null;
+});
+
+const getProcessTypeText = (type) => {
+  if (type === undefined || type === null) return '';
+  const map = {
+    1: '鍔犲伐',
+    2: '鍒澘鍐疯姱鍒朵綔',
+    3: '绠¤矾缁勫',
+    4: '缃愪綋杩炴帴鍙婅皟璇�',
+    5: '娴嬭瘯鎵撳帇',
+    6: '鍏朵粬',
+  };
+  return map[type] || '';
 };
 
-const { proxy } = getCurrentInstance() || {};
+const getProcessRaw = (row) => {
+  if (!row?.processId) return null;
+  return processOptions.value.find(p => String(p.id) === String(row.processId)) || null;
+};
 
-const handleSubmit = () => {
-  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
-  if (hasEmptyProcess) {
-    proxy?.$modal?.msgError("璇蜂负鎵�鏈夐」鐩�夋嫨宸ュ簭");
+const getProcessField = (row, key) => {
+  const p = getProcessRaw(row);
+  const fromProcess = p ? p[key] : undefined;
+  if (fromProcess !== undefined && fromProcess !== null && fromProcess !== '') return fromProcess;
+  if (key === 'name' && row.productName) return row.productName;
+  if (key === 'productModel' && row.model) return row.model;
+  const fromRow = row[key];
+  if (fromRow !== undefined && fromRow !== null && fromRow !== '') return fromRow;
+  return '-';
+};
+
+const formatProcessOptionLabel = (process) => {
+  if (!process) return '';
+  const no = process.no ? String(process.no).trim() : '';
+  const name = process.name || '';
+  if (no && name) return `${no} ${name}`;
+  return name || no || '';
+};
+
+const convertProductTree = (list) => {
+  return (list || []).map(item => {
+    const children = convertProductTree(item.children || item.childList || []);
+    return {
+      ...item,
+      value: item.id,
+      label: item.name || item.label,
+      children,
+      disabled: children.length > 0,
+    };
+  });
+};
+
+const findNodeById = (nodes, targetId) => {
+  for (const node of nodes || []) {
+    if (String(node.value) === String(targetId)) {
+      return node;
+    }
+    if (node.children && node.children.length > 0) {
+      const found = findNodeById(node.children, targetId);
+      if (found) return found;
+    }
+  }
+  return null;
+};
+
+const findNodeIdByLabel = (nodes, targetLabel) => {
+  for (const node of nodes || []) {
+    if (node.label === targetLabel) {
+      return node.value;
+    }
+    if (node.children && node.children.length > 0) {
+      const found = findNodeIdByLabel(node.children, targetLabel);
+      if (found !== null && found !== undefined) return found;
+    }
+  }
+  return undefined;
+};
+
+const getProductCategoryOptions = async () => {
+  try {
+    const res = await productTreeList();
+    const list = Array.isArray(res) ? res : res?.data || [];
+    productCategoryOptions.value = convertProductTree(list);
+  } catch (e) {
+    productCategoryOptions.value = [];
+  }
+};
+
+const getModelOptions = async (productId) => {
+  if (!productId) {
+    modelOptions.value = [];
     return;
   }
+  try {
+    const res = await modelListPage({
+      id: productId,
+      current: 1,
+      size: 999,
+    });
+    const records = res?.records || res?.data?.records || [];
+    modelOptions.value = records;
+  } catch (e) {
+    modelOptions.value = [];
+  }
+};
 
-  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 handleProductChange = async (value) => {
+  const selectedNode = findNodeById(productCategoryOptions.value, value);
+  form.value.productName = selectedNode?.label || '';
+  form.value.productModelId = undefined;
+  await getModelOptions(value);
+};
+
+// 鑾峰彇鍒楄〃
+const getList = () => {
+  tableLoading.value = true;
+  const listPromise =
+    pageType.value === "order"
+      ? findProductProcessRouteItemList({ orderId: orderId.value })
+      : findProcessRouteItemList({ routeId: routeId.value });
+
+  listPromise
+    .then(res => {
+      tableData.value = res.data || [];
+      tableLoading.value = false;
+      // 鍒楄〃鍔犺浇瀹屾垚鍚庡垵濮嬪寲鎷栨嫿鎺掑簭
+      nextTick(() => {
+        initSortable();
       });
+    })
+    .catch(err => {
+      tableLoading.value = false;
+      console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+      proxy?.$modal?.msgError("鑾峰彇鍒楄〃澶辫触");
+    });
 };
 
-const destroySortable = () => {
-  if (tableSortable) {
-    tableSortable.destroy();
-    tableSortable = null;
-  }
-  if (stepsSortable) {
-    stepsSortable.destroy();
-    stepsSortable = null;
-  }
+// 鑾峰彇宸ュ簭鍒楄〃
+const getProcessList = () => {
+  processList({})
+    .then(res => {
+      processOptions.value = res.data || [];
+    })
+    .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 || ''
+  };
+};
+
+// 鏂板
+const handleAdd = () => {
+  operationType.value = 'add';
+  resetForm();
+  dialogVisible.value = true;
+  getProductCategoryOptions();
+};
+
+// 缂栬緫
+const handleEdit = async (row) => {
+  operationType.value = 'edit';
+  form.value = {
+    id: row.id,
+    routeId: routeId.value,
+    processId: row.processId,
+    productId: row.productId,
+    productModelId: row.productModelId,
+    productName: row.productName || "",
+    model: row.model || "",
+    isQuality: row.isQuality,
+  };
+  dialogVisible.value = true;
+  await getProductCategoryOptions();
+  if (!form.value.productId && form.value.productName) {
+    form.value.productId = findNodeIdByLabel(productCategoryOptions.value, form.value.productName);
+  }
+  await getModelOptions(form.value.productId);
+};
+
+// 鍒犻櫎
+const handleDelete = (row) => {
+  ElMessageBox.confirm('纭鍒犻櫎璇ュ伐鑹鸿矾绾块」鐩紵', '鎻愮ず', {
+    confirmButtonText: '纭',
+    cancelButtonText: '鍙栨秷',
+    type: 'warning'
+  })
+    .then(() => {
+      // 鐢熶骇璁㈠崟涓嬩娇鐢� productProcessRoute 鐨勫垹闄ゆ帴鍙o紙璺敱鍚庢嫾鎺� id锛夛紝鍏跺畠鎯呭喌浣跨敤宸ヨ壓璺嚎椤圭洰鎵归噺鍒犻櫎鎺ュ彛
+      const deletePromise =
+        pageType.value === 'order'
+          ? deleteRouteItem(row.id)
+          : batchDeleteProcessRouteItem([row.id]);
+
+      deletePromise
+        .then(() => {
+          proxy?.$modal?.msgSuccess('鍒犻櫎鎴愬姛');
+          getList();
+        })
+        .catch(() => {
+          proxy?.$modal?.msgError('鍒犻櫎澶辫触');
+        });
+    })
+    .catch(() => {});
+};
+
+// 鎻愪氦
+const handleSubmit = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      submitLoading.value = true;
+      
+      if (operationType.value === 'add') {
+        // 鏂板锛氫紶鍗曚釜瀵硅薄锛屽寘鍚玠ragSort瀛楁
+        // dragSort = 褰撳墠鍒楄〃闀垮害 + 1锛岃〃绀烘柊澧炶褰曟帓鍦ㄦ渶鍚�
+        const dragSort = tableData.value.length + 1;
+        const isOrderPage = pageType.value === 'order';
+
+        const addPromise = isOrderPage
+          ? addRouteItem({
+              productOrderId: orderId.value,
+              productRouteId: routeId.value,
+              processId: form.value.processId,
+              productModelId: form.value.productModelId,
+              isQuality: form.value.isQuality,
+              dragSort,
+            })
+          : addOrUpdateProcessRouteItem({
+              routeId: routeId.value,
+              processId: form.value.processId,
+              productModelId: form.value.productModelId,
+              isQuality: form.value.isQuality,
+              dragSort,
+            });
+
+        addPromise
+          .then(() => {
+            proxy?.$modal?.msgSuccess('鏂板鎴愬姛');
+            closeDialog();
+            getList();
+          })
+          .catch(() => {
+            proxy?.$modal?.msgError('鏂板澶辫触');
+          })
+          .finally(() => {
+            submitLoading.value = false;
+          });
+      } else {
+        // 缂栬緫锛氱敓浜ц鍗曚笅浣跨敤 productProcessRoute/updateRouteItem锛屽叾瀹冩儏鍐典娇鐢ㄥ伐鑹鸿矾绾块」鐩洿鏂版帴鍙�
+        const isOrderPage = pageType.value === 'order';
+        
+        const updatePromise = isOrderPage
+          ? addOrUpdateProductProcessRouteItem({
+              id: form.value.id,
+              processId: form.value.processId,
+              productModelId: form.value.productModelId,
+              isQuality: form.value.isQuality,
+            })
+          : addOrUpdateProcessRouteItem({
+              routeId: routeId.value,
+              processId: form.value.processId,
+              productModelId: form.value.productModelId,
+              id: form.value.id,
+              isQuality: form.value.isQuality,
+            });
+
+        updatePromise
+          .then(() => {
+            proxy?.$modal?.msgSuccess('淇敼鎴愬姛');
+            closeDialog();
+            getList();
+          })
+          .catch(() => {
+            proxy?.$modal?.msgError('淇敼澶辫触');
+          })
+          .finally(() => {
+            submitLoading.value = false;
+          });
+      }
+    }
+  });
+};
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+  form.value = {
+    id: undefined,
+    routeId: routeId.value,
+    processId: undefined,
+    productId: undefined,
+    productModelId: undefined,
+    productName: "",
+    model: "",
+    isQuality: false,
+  };
+  modelOptions.value = [];
+  nextTick(() => formRef.value?.clearValidate());
+};
+
+// 鍏抽棴寮圭獥
+const closeDialog = () => {
+  dialogVisible.value = false;
+  resetForm();
+};
+
+// 鍒濆鍖栨嫋鎷芥帓搴�
 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 (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, {
@@ -315,189 +698,386 @@
       handle: '.el-table__row',
       filter: '.el-button, .el-select',
       onEnd: (evt) => {
-        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
+        if (evt.oldIndex === evt.newIndex || !tableData.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);
+        // 閲嶆柊鎺掑簭鏁扮粍
+        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) {
+          const isOrderPage = pageType.value === 'order';
+          const sortPromise = isOrderPage
+            ? sortRouteItem({
+                id: moveItem.id,
+                dragSort: dragSort
+              })
+            : sortProcessRouteItem({
+                id: moveItem.id,
+                dragSort: dragSort
+              });
+
+          sortPromise
+            .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 (!stepsContainer.value) return;
+    // 鍗$墖瑙嗗浘鐨勬嫋鎷芥帓搴�
+    if (!cardsContainer.value) return;
 
-    // 淇敼锛氱洿鎺ヤ娇鐢╯tepsContainer.value浣滀负鎷栨嫿瀹瑰櫒
-    const stepsList = stepsContainer.value;
-    if (!stepsList) {
-      console.warn('鏈壘鍒版楠ゆ潯鎷栨嫿瀹瑰櫒');
-      return;
-    }
-
-    // 淇敼锛氱畝鍖栨嫋鎷介厤缃�
-    stepsSortable = new Sortable(stepsList, {
+    cardSortable = new Sortable(cardsContainer.value, {
       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,
+      handle: '.process-card',
+      filter: '.el-button',
       onEnd: (evt) => {
-        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
+        if (evt.oldIndex === evt.newIndex || !tableData.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];
+        // 閲嶆柊鎺掑簭鏁扮粍
+        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) {
+          const isOrderPage = pageType.value === 'order';
+          const sortPromise = isOrderPage
+            ? sortRouteItem({
+                id: moveItem.id,
+                dragSort: dragSort
+              })
+            : sortProcessRouteItem({
+                id: moveItem.id,
+                dragSort: dragSort
+              });
+
+          sortPromise
+            .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);
+            });
+        }
       }
     });
-
-    // 璋冭瘯锛氭墦鍗板鍣ㄥ拰瀹炰緥锛岀‘璁ょ粦瀹氭垚鍔�
-    console.log('姝ラ鏉℃嫋鎷藉鍣�:', stepsList);
-    console.log('Sortable瀹炰緥:', stepsSortable);
   }
 };
 
-const handleViewChange = () => {
-  destroySortable();
-  // 寤惰繜鍒濆鍖栵紝纭繚瑙嗗浘鍒囨崲鍚嶥OM瀹屽叏娓叉煋
-  nextTick(() => {
-    setTimeout(() => initSortable(), 100);
-  });
+// 閿�姣佹嫋鎷芥帓搴�
+const destroySortable = () => {
+  if (tableSortable) {
+    tableSortable.destroy();
+    tableSortable = null;
+  }
+  if (cardSortable) {
+    cardSortable.destroy();
+    cardSortable = null;
+  }
 };
 
 onMounted(() => {
-  findProcessRouteItems();
-  findProcessList();
+  getRouteInfo();
+  getList();
+  getProcessList();
+  getProductCategoryOptions();
 });
 
 onUnmounted(() => {
   destroySortable();
 });
-
-defineExpose({
-  handleSubmit,
-});
 </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;
+}
+
+.product-tag {
+  margin: 10px 0;
+}
+
+.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.6;
+  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;
 }
 
-:deep(.el-card__footer){
-  padding: 0 !important;
+/* 鍖哄煙鏍囬鏍峰紡 */
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
 }
 
-.operate-button {
+.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;
-  justify-content: space-between;
 }
 
-/* 淇敼锛氳嚜瀹氫箟姝ラ鏉″鍣ㄦ牱寮� */
-.custom-steps {
-  min-height: 100px;
-  padding: 10px 0;
-  display: flex;
-  flex-wrap: wrap;
-  gap: 20px;
-  align-items: flex-start;
+/* 宸ヨ壓璺嚎淇℃伅鍗$墖鏍峰紡 */
+.route-info-card {
+  margin-bottom: 20px;
+  border: 1px solid #e4e7ed;
+  background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
+  border-radius: 8px;
+  overflow: hidden;
 }
 
-/* 淇敼锛氳嚜瀹氫箟姝ラ椤规牱寮� */
-.custom-step {
-  cursor: move !important;
-  padding: 8px;
-  position: relative;
-  transition: all 0.2s ease;
-  flex: 0 0 auto;
-  min-width: 220px;
-  touch-action: none;
+.route-info {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+  gap: 16px;
+  padding: 4px;
 }
 
-/* 鎷栨嫿鎮诞鏍峰紡锛屾彁绀哄彲鎷栨嫿 */
-.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 {
+.info-item {
   display: flex;
   flex-direction: column;
-  align-items: center;
-  height: 140px;
+  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);
 }
 
-.step-card-footer {
-  display: flex;
-  justify-content: flex-end;
-  align-items: center;
-  padding: 10px;
+.info-item:hover {
+  border-color: #409eff;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
+  transform: translateY(-1px);
 }
 
-/* 鑷畾涔夊簭鍙锋牱寮忎紭鍖� */
-.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;
+.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>

--
Gitblit v1.9.3