基于Vue + element制作扁平化树形结构渲染表格(动态行列合并,树结构算法应用)

前置条件,后端的数据为扁平化后的树形结构

达到效果:

1、方法介绍及其用处

1)扁平转树和树形转扁平,element-ui的表格组件从数组索引0开始渲染一直到最后,如果后端没有按照顺序传数据会导致表格错乱,所以我们需要将数据二次处理(扁平转树再转扁平),达到整理数据的效果

//扁平转树
    toTreeData(flatData) {
      const idToNodeMap = {};
      const roots = [];
      flatData.forEach((node) => {
        const { id, pid } = node;
        if (!idToNodeMap[id]) {
          idToNodeMap[id] = { ...node, children: [] };
        } else {
          Object.assign(idToNodeMap[id], node);
        }
      });
      Object.values(idToNodeMap).forEach((node) => {
        if (node.pid === 0) {
          roots.push(node);
        } else {
          const parent = idToNodeMap[node.pid];
          if (parent) {
            parent.children.push(node);
          }
        }
      });
      return roots;
    },
    //树转扁平
    flattenTree(tree) {
      const result = [];

      function traverse(node) {
        result.push(node); // 将当前节点添加到结果数组中

        if (node.children && node.children.length > 0) {
          for (const child of node.children) {
            traverse(child); // 递归遍历子节点
          }
        }
      }

      for (const node of tree) {
        traverse(node); // 从每个根节点开始遍历
      }

      return result;
    },
//重构数据
    generateTableData(list) {
      this.treeData = this.toTreeData(list);
      this.tableData = this.flattenTree(this.treeData);
    },

2)根据id获取树的某个节点(深度优先遍历),这个方法对于获取某个单元格占用的列或行至关重要

 //根据id获取节点(深度优先遍历)
    getNodeById(targetId) {
      let foundNode = null;

      function findNode(node) {
        if (node.id === targetId) {
          foundNode = node;
          return;
        }

        for (const child of node.children) {
          findNode(child);
        }
      }

      for (const node of this.treeData) {
        findNode(node);
      }

      return foundNode;
    },

3)节点应该占用行(深度优先遍历),对于这种需求来说,一个单元格应该占用多少行其实指的就是这个树的这个节点下有多少个没有children的节点,其中node传的就是我们用id查到的节点

 //根据节点判断需要占用多少行
    countLeafNodes(node) {
      if (!node) {
        return 0;
      }

      if (!node.children || node.children.length === 0) {
        return 1;
      }

      let count = 0;
      for (const child of node.children) {
        count += this.countLeafNodes(child);
      }

      return count;
    },

4)实际占用行(树的节点总数),算出节点应该占用行不不代表实际,elementUI合并表格并不会删除原有行(父节点依然占用了多余的一行),所以之后我们得手动隐藏

 // 计算树中的节点数量
    countAllNodes(tree) {
      let count = 0;

      function traverse(node) {
        count++; // 每次访问节点时增加计数

        if (node.children && node.children.length > 0) {
          for (const child of node.children) {
            traverse(child); // 递归遍历子节点
          }
        }
      }

      // 遍历树中的每个根节点
      for (const node of tree) {
        traverse(node);
      }

      return count;
    },

5)获取树的深度,这个方法对于合并列至关重要,深度为1表示后去没有其他内容了,它自己可以占用后续所有单元格,反之只占用应有的1个单元格,将其他留给后续内容

getDeep(tree) {
      let maxDepth = 0;

      function traverse(node, depth) {
        if (!node || !node.children) {
          return;
        }

        maxDepth = Math.max(maxDepth, depth);

        for (const child of node.children) {
          traverse(child, depth + 1);
        }
      }

      for (const node of tree) {
        traverse(node, 1);
      }

      return maxDepth;
    },

2、完整代码

<template>
  <div>
    {{ getHideId() }}
    <!-- {{ tableData }} -->
    <el-table
      :data="tableData"
      style="width: 100%"
      border
      :span-method="objectSpanMethod"
    >
      <el-table-column
        v-for="level in max_level"
        :key="`level_${level}`"
        :label="`${level}级商户`"
        :prop="`name_${level}`"
      >
        <template slot-scope="scope">
          {{ scope.row[`name_${level}`]}}
        </template>
      </el-table-column>

      <el-table-column v-for="item in thConfig" :key="'th'+item.prop" :prop="item.prop" :label="item.label"> </el-table-column>
      
    </el-table>
  </div>
</template>
  
  <script>
export default {
  props: {
    max_level: {
      type: Array,
      default: [],
    },
    list: {
      type: Array,
      default: [],
    },
    thConfig: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      treeData: [],
      tableData: [], // 用于渲染表格的数据
      hideIndex: [],
      flexTotal:0,//可灵活使用的列数
    };
  },
  created() {
    this.$nextTick(() => {
      this.generateTableData(this.list);
      this.flexTotal = this.max_level.length
    });
  },
  methods: {
    //需要隱藏的行的id
    getHideId() {
      let arr = [];
      this.list.forEach((item, index) => {
        if (this.countLeafNodes(this.getNodeById(item.id)) > 1) {
          arr.push(item.id);
        }
      });
      return arr;
    },
    //重构数据
    generateTableData(list) {
      this.treeData = this.toTreeData(list);
      this.tableData = this.flattenTree(this.treeData);
    },
    //扁平转树
    toTreeData(flatData) {
      const idToNodeMap = {};
      const roots = [];
      flatData.forEach((node) => {
        const { id, pid } = node;
        if (!idToNodeMap[id]) {
          idToNodeMap[id] = { ...node, children: [] };
        } else {
          Object.assign(idToNodeMap[id], node);
        }
      });
      Object.values(idToNodeMap).forEach((node) => {
        if (node.pid === 0) {
          roots.push(node);
        } else {
          const parent = idToNodeMap[node.pid];
          if (parent) {
            parent.children.push(node);
          }
        }
      });
      return roots;
    },
    //树转扁平
    flattenTree(tree) {
      const result = [];

      function traverse(node) {
        result.push(node); // 将当前节点添加到结果数组中

        if (node.children && node.children.length > 0) {
          for (const child of node.children) {
            traverse(child); // 递归遍历子节点
          }
        }
      }

      for (const node of tree) {
        traverse(node); // 从每个根节点开始遍历
      }

      return result;
    },

    //根据id获取节点(深度优先遍历)
    getNodeById(targetId) {
      let foundNode = null;

      function findNode(node) {
        if (node.id === targetId) {
          foundNode = node;
          return;
        }

        for (const child of node.children) {
          findNode(child);
        }
      }

      for (const node of this.treeData) {
        findNode(node);
      }

      return foundNode;
    },
    //根据节点判断需要占用多少行
    countLeafNodes(node) {
      if (!node) {
        return 0;
      }

      if (!node.children || node.children.length === 0) {
        return 1;
      }

      let count = 0;
      for (const child of node.children) {
        count += this.countLeafNodes(child);
      }

      return count;
    },
    // 计算树中的节点数量
    countAllNodes(tree) {
      let count = 0;

      function traverse(node) {
        count++; // 每次访问节点时增加计数

        if (node.children && node.children.length > 0) {
          for (const child of node.children) {
            traverse(child); // 递归遍历子节点
          }
        }
      }

      // 遍历树中的每个根节点
      for (const node of tree) {
        traverse(node);
      }

      return count;
    },
    getDeep(tree) {
      let maxDepth = 0;

      function traverse(node, depth) {
        if (!node || !node.children) {
          return;
        }

        maxDepth = Math.max(maxDepth, depth);

        for (const child of node.children) {
          traverse(child, depth + 1);
        }
      }

      for (const node of tree) {
        traverse(node, 1);
      }

      return maxDepth;
    },
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex < this.flexTotal) {
        let totalnode = 0;
        if (this.getNodeById(row.id)) {
          totalnode = this.countAllNodes([this.getNodeById(row.id)]); //节点数
        }
        let deep = this.getDeep([this.getNodeById(row.id)]);//深度
        let flexSum = this.flexTotal - columnIndex//可灵活分配列单元格数量
          if (row["name_" + (columnIndex + 1)]) {
            return {
              rowspan: totalnode,
              colspan: deep > 1 ? 1 : flexSum,
            };
          }
          return {
            rowspan: 0,
            colspan: 0,
        }
      }
      if (
        this.getHideId().includes(row.id) ||
        this.getDeep([this.getNodeById(row.id)]) > 1
      ) {
        return {
          rowspan: 0,
          colspan: 0,
        };
      }
    },
  },
};
</script>

3、使用示例和参数标准

<table-test :list="res.list" :max_level="res.max_level" :thConfig="thConfig"></table-test>


thConfig:[
        {
          prop:"order_counts",
          label:"交易笔数"
        },
        {
          prop:"amount_incurred",
          label:"发生金额"
        }
      ]


max_level:[1,2,3,4]


list:[
        {
          "id": 59,
          "pid": 0,
          "name": "",
          "name_1": "",
        },
        {
          "id": 56,
          "pid": 59,
          "name": "",
          "name_2": "",
        },
        {
          "id": 55,
          "pid": 56,
          "name": "",
          "name_3": "",
        },
        ...
      ]

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值