平时刷算法都是为了面试,除此之外,感觉刷算法一点用都没有。但是,直到我开发碰上这个需求,不禁感叹,刷算法还是很有用的。
业务需求
看到这个图,我想第一感觉就是直接原生table
撸就完事了。巴特!需求是表格的表头和表体都是动态配置的。所以,要根据配置的数据来渲染表格,涉及到跨行就难搞了,同时,还要支持自定义配置列宽度。
解决方案的前置知识
利用colgroup
中col
的width
属性,就可以对每列进行宽度控制,该属性虽已废弃,但是在浏览器中还有较好的支持(查看支持的浏览器版本)
table
的跨行规则,例如,表格有两行五列,第一列跨两行,则第一行的数据为5条,第二行的数据为4条,因为有一个跨行
解决方案
由于后台返回来的数据是一个树结构,而我们需要将树转成一条条符合table
渲染的数据结构,树的深度代表表格有多少列。所以,这里就很容易联想到了树的全路径遍历
算法来处理树结构。举个例子🌰
利用
树的全路径遍历算法
得到下图结果:
那么,根据table
的跨行规则,我们需要进行合并,需要得出以下数据结果:
结果生成的表格如下:
解释: 因为1是树的根节点,所以他是跨的最多行,也就是5行。那么生成的5条数据中就不能出现1,对算法进行稍微的改造,将遍历过的节点加上标识,这样在遍历路径的时候,有标识的节点就不添加当前行中。
至于4后边的跨5行,可以根据id去重,将id相同的合并,也就是4-1到4-5的子项都是同样的数据id,在遍历是记录id去重。
代码实现
表格框架此处选择将表头和表体分离的方式,方便以后扩展其他功能(demo用vue3+ts演示),mock数据见资源导航
<template>
<div class="table">
<table border="1" align="center" class="mainTable">
<colgroup>
<col v-for="(width, index) in widths" :key="index" :width="width" />
</colgroup>
<thead>
<tr>
<th ref="colRefs" v-for="(item, index) in headerData" :key="index">
{{ item.title }}
</th>
</tr>
</thead>
</table>
<table border="1" align="center" class="mainTable">
<colgroup>
<col v-for="(width, index) in widths" :key="index" :width="width" />
</colgroup>
<tbody align="center">
<template v-for="(item, index) in bodyData">
<tr v-for="(item1, index1) in numArr[index]" :key="item.id + item1">
<td v-for="(item2, index2) in rowData[index][index1]" :rowspan="item2.maxRow" :key="index2">
{{ item2.content }}
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
逻辑部分实现:
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import data from "./mock1.json";
const headerData = reactive<HeaderData[]>(data.headerData)
const bodyData = reactive(data.bodyData)
const numArr = reactive<number[][]>([])
const rowData = reactive<Array<TreeNode[][]>>([])
const widths = reactive<(string | number)[]>([])
let idSets = new Set()
const colRefs = ref([])
interface HeaderData {
id: string,
title: string,
type: number,
width?: string | number
}
interface TreeNode {
id: string,
content: string,
contentValue: null,
maxRow: number,
children: TreeNode[],
isTraverse?: boolean
}
const getRowData = (data: TreeNode[]) => {
//遍历树的所有路径
const getAllPath = (tree: TreeNode[]):TreeNode[][] => {
const paths = []; //记录路径
for (let i = 0; i < tree.length; i++) {
//遍历同层的所有节点
if (tree[i].children && tree[i].children.length) {
//如果有子节点便继续深入,直到到达叶子节点
const res = getAllPath(tree[i].children);
for (let j = 0; j < res.length; j++) {
if (!tree[i].isTraverse && !idSets.has(tree[i].id)) {
// 添加过的加标识 同时将id放到Set结构中去重 子节点返回后将其返回的路径与自身拼接
idSets.add(tree[i].id)
paths.push([tree[i], ...res[j]]);
tree[i].isTraverse = true;
} else {
paths.push([...res[j]]);
}
}
} else {
if (!tree[i].isTraverse) {
//没有子节点的话,直接将自身拼接到paths中
paths.push([tree[i]]);
tree[i].isTraverse = true;
}
}
}
return paths;
};
// 遍历每个大项下边的所有路径(第一列每条数据视为一个大项也就是一个树结构)
for (let item of data) {
let arr = [item];
rowData.push(getAllPath(arr));
}
}
const createTable = (data:TreeNode[]) => {
// 得到每条数据行数组- 该数组也可做计算行序号用
let k = 0;
while (k < data.length) {
let arr1 = [];
let i = 1;
if (data[k].maxRow) {
for (let j = 0; j < data[k].maxRow; j++) {
arr1.push(i);
i++;
}
} else {
arr1.push(1);
}
k++;
numArr.push(arr1);
}
getRowData(data);
}
createTable(bodyData)
onMounted(() => {
//得到表头每列的宽度-如果没有自定义宽度就取元素宽度
colRefs.value.forEach((item:HTMLElement, index) => {
const width = headerData[index].width
if (width) {
widths.push(width);
} else {
widths.push(item.offsetWidth);
}
});
})
</script>
说明:maxRow
代表当前树的层级,也就是最大跨行数。maxRow
可以通过树的广度优先遍历获取
,此处直接表示(交给后端,让后端处理)
总结
算法在我们平时并不是一个关注点,也不会专门去研究,但是,一旦我们遇到难一点的需求需要处理数据的时候,算法就尤为重要了,可以为我们提供很多思路,快速的得出解决方案。
由此,在平时只要稍微关注下算法,了解一些数据结构,刷刷算法,在关键时候可以省去很多的时间,剩下的时间摸鱼它不香嘛