# merge-table **Repository Path**: IcecreamPanda/merge-table ## Basic Information - **Project Name**: merge-table - **Description**: 专注于树结构与合并表格之间的转换,基于vue的合并表格组件 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2019-05-11 - **Last Updated**: 2023-09-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 树转合并单元格的实现 不难想到,树形结构和合并单元格之间是可以相互转换的。我们可能会遇到有业务场景需要以合并单元格的形式展示,但是数据结构是以树的形式维护的。 如图: ```text Tree / \ A B /\ C D ``` 我们以 vue 为实例,实现树与合并单元格的转换 ## 数据结构分析 树结构我们约定为以 children 为子节点的属性,结构如下: ```javascript { id: 'root', text: 'tree', children: [ { id: 'eb1d5347-0a91-4d9c-99d5-a65a98f84147', text: 'A', children: [ { id: '7b5a2809-70c8-4460-987b-b2df5649c672', text: 'C', }, { id: '02a66fd8-4f6d-4bea-aca8-00e5e33c0d63', text: 'D', } ] }, { id: '78de42cb-7b25-495d-ac7e-02a180d1f28f', text: 'B', } ] } ``` 预期我们需要转换成这样: ![](./merge-table-1.jpg) 那么需要转换成什么样的数据结构才能生成这样的表格呢? ```html
tree A C
D
B
``` 首先它 DOM 结构是这样子的,`v-for`根据每一行每一列来遍历,因此需要的数组应该是:`[[tree,A,C],[D],[B]]`这种嵌套数组的形式; 其次,每一个 item 上都有对应的属性 colspan,rowspan 和 text。 因此最终的每一个元素应该有这三个属性。 所以: 我们最终需要将树结构转换成 嵌套数组,每个数组元素带 colspan,rowspan 和 text 属性: ```javascript ;[ [ { id: 'root', text: 'tree', children: null, level: 1, leafCount: 3, isLeaf: false, rowSpan: 3, colSpan: 1 }, { id: 'eb1d5347-0a91-4d9c-99d5-a65a98f84147', text: 'A', children: null, level: 2, leafCount: 2, isLeaf: false, rowSpan: 2, colSpan: 1 }, { id: '7b5a2809-70c8-4460-987b-b2df5649c672', text: 'C', level: 3, leafCount: 1, isLeaf: true, children: null, rowSpan: 1, colSpan: 1 } ], [ { id: '02a66fd8-4f6d-4bea-aca8-00e5e33c0d63', text: 'D', level: 3, leafCount: 1, isLeaf: true, children: null, rowSpan: 1, colSpan: 1 } ], [ { id: '78de42cb-7b25-495d-ac7e-02a180d1f28f', text: 'B', level: 2, leafCount: 1, isLeaf: true, children: null, rowSpan: 1, colSpan: 2 } ] ] ``` ## 数据转换 数据结构已经有了, 下面就要考虑如何进行转换了,经过观察对比规律,得到下面的结论: ### 先序遍历构造数组 > 叶子节点: 当前节点没有后代 > 先序遍历:根结点 ---> 左子树 ---> 右子树 对树进行**先序遍历**,遍历到了叶子节点,当前数组结束(即以该叶子节点为分界线,当前行结束)。 例子经过遍历后,得到嵌套数组: ```javascript ;[[tree, A, C], [D], [B]] ``` 数组有了,但是直接遍历渲染出来是不符合预期的,因为还差每个元素自己的行列合并数值 ### colSpan 计算 通过观察研究,可发现: > 这是最核心的规则之一 **每个 td 的列合并数值等于 树的最终高度 - 当前节点的高度 + 1** ### rowSpan 计算 通过观察研究,可发现: > 这是最核心的规则之二 **每个 td 的列合并数值等于 当前节点子代的所有叶子节点个数(如果当前节点是叶子节点,也需要包含在内,即等于 1)** ### 最终逻辑 经过上述分析,我们知道,树需要做遍历,并且需要知道树的高度、每一个节点的高度、每一个节点的叶子个数。 所以,在组件实现时,直接往树结构中注入下面属性 | 字段 | 解释 | | --------- | ------------------------------ | | level | 当前节点的层级,从根节点=1 开始 | | leafCount | 当前节点拥有的叶子数量 | | isLeaf | 是否叶子节点 | 属性注入逻辑: ```javascript ... /** * 递归建立以下内容: * 树的每个节点的深度(根节点深度为1); * 每个节点下属所有的叶子(包括孙子和所有后代)个数; * 节点是否是leaf * * @param {*} arr * @param {*} parentLevel * 返回,有效内容为当前节点的所有后代叶子个数,所有后代节点中,最深的节点的深度 */ calcLevelLeaf(arr, parentLevel = 0) { let count = 0 let deep = parentLevel + 1 arr.forEach(item => { item.level = parentLevel + 1 item.leafCount = 0 if (this._hasChildren(item)) { const {count: c, deep: d} = this.calcLevelLeaf( item.children, item.level ) item.leafCount += c item.isLeaf = false count += c deep = Math.max(deep, d) } else { item.isLeaf = true item.leafCount = 1 count += 1 } }) return { count, deep } } ... ``` 然后再执行先序遍历构造数组,此时就可以把 colspan 和 rowspan 都计算出来了,得到最终数组,然后给`v-for`遍历即可。 ## 最终成品 尽管对上面的三个操作的算法写得不好,但是起码是实现了这套逻辑。 也把这套逻辑封装成了组件 [merge-table](https://www.gdutzuo.top/merge-table) NPM: [merge-table](https://www.npmjs.com/package/merge-table) gitee: [merge-table](https://gitee.com/IcecreamPanda/merge-table) ## 后记 ### 算法优化 逻辑虽然实现,但是内部的算法还是比较糟糕的,希望后续能够优化减少遍历次数,欢迎 issue。 ### 属性抽离 对于注入属性,参考 iview-tree 的实现也是做了属性注入,个人觉得没有问题,只是后续应该可以换一个不容易冲突的属性名。 另外,tree 遍历过程用到的比如 id,children 的 key 应该抽离,因为组件是从以前的业务里搬出来的,所以暂时没考虑这一块,后续可以考虑进去。