Explorar o código

feat: 添加计划表展开/收起功能,并优化数据排序逻辑为树形结构

xieyong hai 3 días
pai
achega
39c954cfb8

+ 71 - 1
src/utils/batchNoSort.js

@@ -26,4 +26,74 @@ export function compareBatchNo(a, b) {
     return aInfo.base.localeCompare(bInfo.base);
   }
   return aInfo.suffix - bInfo.suffix;
-}
+}
+
+// 把扁平数据按 batchNo / code 折叠成树形:子项(如 batchNo="20260914-1"
+// 或 code="P001-1")会挂到基础号父项的 children 下;父节点本身不存在时,
+// 子项保持独立,不创建虚拟父节点。
+// 根节点和子节点都按 startTime 降序排(开始时间越大越靠前)。
+export function buildBatchNoTree(rows = []) {
+  const list = Array.isArray(rows) ? rows : [];
+  // 按开始时间降序比较的辅助函数
+  const byStartTimeDesc = (a, b) => {
+    const ta = new Date(a?.startTime || 0).getTime();
+    const tb = new Date(b?.startTime || 0).getTime();
+    return tb - ta;
+  };
+
+  // 索引:batchNo/code -> 行(用于 O(1) 查找父节点)
+  const batchNoIndex = new Map();
+  const codeIndex = new Map();
+  list.forEach((row) => {
+    if (row?.batchNo) batchNoIndex.set(String(row.batchNo), row);
+    if (row?.code) codeIndex.set(String(row.code), row);
+  });
+
+  // 找到行的父节点:需要 batchNo 和 code 两个字段同时能匹配到
+  // 同一个父节点(AND 关系),任一字段缺失或匹配到不同父都返回 null
+  const findParent = (row) => {
+    const batchInfo = splitBatchNo(row?.batchNo);
+    const codeInfo = splitBatchNo(row?.code);
+    // 任一字段没有 -N 后缀,就不满足 AND 条件
+    if (batchInfo.suffix < 0 || codeInfo.suffix < 0) return null;
+    if (!batchInfo.base || !codeInfo.base) return null;
+    const batchParent = batchNoIndex.get(batchInfo.base);
+    const codeParent = codeIndex.get(codeInfo.base);
+    if (!batchParent || !codeParent) return null;
+    if (batchParent === row || codeParent === row) return null;
+    // 两个字段必须匹配到同一个父节点
+    return batchParent === codeParent ? batchParent : null;
+  };
+
+  // 先建临时节点(带 children),再挂父子关系
+  const nodeMap = new Map();
+  list.forEach((row) => {
+    nodeMap.set(row, { ...row, children: undefined });
+  });
+
+  const rootRows = [];
+  nodeMap.forEach((node, row) => {
+    const parent = findParent(row);
+    if (parent) {
+      const parentNode = nodeMap.get(parent);
+      if (!Array.isArray(parentNode.children)) {
+        parentNode.children = [];
+      }
+      parentNode.children.push(node);
+    } else {
+      rootRows.push(node);
+    }
+  });
+
+  // 父子层内各自按 startTime 降序
+  const sortTree = (nodes) => {
+    nodes.forEach((n) => {
+      if (Array.isArray(n.children) && n.children.length) {
+        sortTree(n.children);
+      }
+    });
+    nodes.sort(byStartTimeDesc);
+  };
+  sortTree(rootRows);
+  return rootRows;
+}

+ 30 - 10
src/views/productionPlan/components/newFactoryProductionScheduling.vue

@@ -72,15 +72,25 @@
               :label="item.label"
             />
           </el-tabs>
+          <el-button
+            size="mini"
+            plain
+            class="plan-table-expand-toggle"
+            @click="togglePlanTableExpandAll"
+          >
+            {{ planTableExpandAll ? '收起全部' : '展开全部' }}
+          </el-button>
         </div>
         <ele-pro-table
-          :key="planTableCacheKey"
+          :key="planTableCacheKey + '-' + (planTableExpandAll ? 'expanded' : 'collapsed')"
           width="100%"
           ref="planTable"
           :columns="columns"
           :cache-key="planTableCacheKey"
           :datasource="sortedPlanDataList"
           row-key="id"
+          :default-expand-all="planTableExpandAll"
+          :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
           :selection.sync="selection"
           :height="planTableHeight"
           :page-size="20"
@@ -263,7 +273,7 @@
     savePlanDotLine
   } from '@/api/productionPlan/planDotLine';
   import { deepClone } from '@/utils';
-  import { compareBatchNo } from '@/utils/batchNoSort';
+  import { compareBatchNo, buildBatchNoTree } from '@/utils/batchNoSort';
   import {
     EXEC_TYPE,
     EXEC_TYPE_OPTIONS,
@@ -317,6 +327,8 @@
         planDataList: [],
         allPlanDataList: [],
         selection: [],
+        // 计划表是否展开所有父子节点(按 batchNo 折叠的树形结构)
+        planTableExpandAll: false,
         activeTab: 'factory',
         dateRange: [startDate, endDate],
         planPickerVisible: false,
@@ -443,18 +455,13 @@
           : 'aps-factory-scheduling-plan-table';
       },
       // 不论 planDataList 由哪条路径赋值,最终给表格用的数据都按 batchNo 排序:
-      // 基础号相同的,"20260914-1" 这类带 "-N" 后缀的按数字升序排在基础号之后
+      // 子 batchNo(20260914-1)会挂到基础号(20260914)的 children 下,
+      // 仅当父节点本身存在于数据中时才会折叠;父节点不存在时子项独立显示
       sortedPlanDataList() {
         if (!Array.isArray(this.planDataList)) {
           return this.planDataList;
         }
-        return [...this.planDataList].sort((a, b) =>
-          compareBatchNo(a?.batchNo, b?.batchNo)
-        ).sort((a, b) =>{
-          const startTimeA = new Date(a?.startTime || 0).getTime();
-          const startTimeB = new Date(b?.startTime || 0).getTime();
-          return startTimeB - startTimeA
-        });
+        return buildBatchNoTree(this.planDataList);
       },
       cacheKeyUrl() {
         return this.planTableCacheKey;
@@ -1260,6 +1267,9 @@
           }, 0);
         });
       },
+      togglePlanTableExpandAll() {
+        this.planTableExpandAll = !this.planTableExpandAll;
+      },
 
       // Plan picker flow
       async planPickerDatasource({ page, limit, where }) {
@@ -3611,6 +3621,16 @@
     box-sizing: border-box;
     border-bottom: 1px solid #e5edf6;
     background: linear-gradient(180deg, #f9fcff 0%, #f3f7fb 100%);
+    display: flex;
+    align-items: flex-end;
+    justify-content: space-between;
+  }
+
+  .plan-table-expand-toggle {
+    margin-bottom: 4px;
+    height: 28px;
+    padding: 0 12px;
+    font-size: 12px;
   }
 
   .plan-table-status-tabs ::v-deep {