在 element-ui 和 antv 中都有表格合并,但如何确定哪几行要合并呢? 在随机给定数据的情况下,如何实现自动合并呢?本文将一一解答这些问题。
并在延伸中,谈到了,如何将本文的方法应用到element-ui和antv中。
一、需求描述及样例展示
① 自动行合并
② 当两列为层级关系的时候,合并要有层级关系。
在举例之前,我们先规定一下展示数据。
展示的是不同 app 在不同手机型号下的下载量。
数据为:
// 行信息
const columns = [
{
key:'app',
label:'app',
},
{
key:'phone',
label:'手机类型',
},
{
key:'phoneType',
label:'手机型号',
},
{
key:'downloadCount',
label:'下载量',
},
];
//数据信息
const data = [
{
app:'微信',
phone:'iphone',
phoneType:'iphone11',
downloadCount:'138,999,999'
},
{
app:'微信',
phone:'iphone',
phoneType:'iphone12',
downloadCount:'138,999,999'
},
{
app:'微信',
phone:'huawei',
phoneType:'mate40',
downloadCount:'138,999,999'
},
{
app:'微信',
phone:'huawei',
phoneType:'mate40pro',
downloadCount:'138,999,999'
},
{
app:'抖音',
phone:'huawei',
phoneType:'mate40',
downloadCount:'138,999,999'
},
{
app:'抖音',
phone:'iphone',
phoneType:'iphone12',
downloadCount:'138,999,999'
},
{
app:'抖音',
phone:'iphone',
phoneType:'iphone11',
downloadCount:'138,999,999'
},
]
常规表格展现结果为:
APP | 手机类型 | 手机型号 | 下载量 |
---|---|---|---|
微信 | iphone | iphone11 | 138,999,999 |
微信 | iphone | iphone12 | 138,999,999 |
微信 | huawei | mate40 | 138,999,999 |
微信 | huawei | mate40pro | 138,999,999 |
抖音 | huawei | mate40 | 138,999,999 |
抖音 | iphone | iphone12 | 138,999,999 |
抖音 | iphone | iphone11 | 138,999,999 |
产品最终要的结果是:
这里要注意的是:
虽然手机类型都是华为,但如果是不同 app,也不能合并。
虽然手机型号都是 mate40 pro,也要依据 app 是否相同才能进行合并。
二、需求剖析
2.1 合并功能原生实现
这里我们以原生html实现这种合并功能。
先来看看,html 实现表格的行合并的方式,代码如下。
<table border="1">
<tr>
<th>First Name</th>
<th>Bill Gates</td>
</tr>
<tr>
<td rowspan="2">Telephone:</th>
<td>555 77 854</td>
</tr>
<tr>
<td style="display:none;">Telephone:</th>
<td>555 77 855</td>
</tr>
</table>
效果图如下:
可以得知,原生html 是通过 在列上设置 rowspan 来实现行合并。rowspan 的 数值为几则合并几行。
2.2 vue 实现表格渲染
但在实际需求中,表格的渲染一定是批量完成的。
以上述手机下载量数据为例,在vue中,渲染这个表格的代码为:
<table>
<tr>
<td v-for="column in columns">
{{column.label}}
</td>
</tr>
<tr v-for="(item, index) in data">
<td v-for="column in columns">
{{item[column.key]}}
</td>
</tr>
</table>
2.3 vue 实现表格行合并渲染
由上述信息可知,为了实现行合并,我们需要知道,每一列数据中,
①开始合并的行,在开始合并的行添加 rowspan 和 数值
②结束合并的行,在开始合并行和结束合并行之间的所有行style 添加 display :none。
将上述三个需要计算的数值可以抽象为:
/** 合并行 */
interface MergeRow {
/** 开始合并的行 */
start: number;
/** 开始合并的行 */
end: number;
/** 一共合并的行数: end - start + 1 */
count: number;
}
2.3.1 计算行合并
计算以上三个数值,可以抽象为: 在数组中,找到连续重复的数。
这是一个很简单的OJ问题,需要一个变量记录个数即可。 那么,把这个问题稍稍扩展一下,变成,找到数组中有几组连续重复的数,并记录开始重复,结束重复和重复数量。
那么解法就变成了如下代码:
const {data, columns} = table;
const newColumns = columns.map((column)=>{
/** 当前列的key */
const valKey = column.key;
/** 当前列中需要 行合并的信息 */
const merge:MergeRow[] = [];
//第一行数据
let lastVal = data[0][valKey];
let valNum = 0;
let startIndex = 0;
data.forEach((item,index)=>{
/** 当前行,当前列对应的数值 */
const curValue = item[valKey];
// 当前值和上一行的值相等,则计数+1
if(curValue === lastVal) {
valNum ++;
}
// 如果当前值和上一行的值不相等,并且计数大于1的话,则上几行存在相邻相等的数值,需要记录
if(curValue !== lastVal) {
merge.push({
start:startIndex,
end: index - 1,
count: valNum,
});
// 如果当前值和上一行的值不相等,并且计数小于等于1. 则上一行没有数据需要记录,进行覆盖
lastVal = curValue;
valNum = 1;
startIndex = index;
}
if(index === data.length -1) {
merge.push({
start:startIndex,
end: index - 1,
count: valNum,
});
}
});
column.merge = merge;
return column;
});
注意,在在上面的代码中,我把每一列中,存在的合并信息,存到了merge属性中。
这是为了渲染的时候可以读取这些信息准备的。
2.3.2 vue 渲染行合并
渲染的代码如下:
<table>
<tr>
<td v-for="column in columns"> {{column.label}}</td>
</tr>
<tr v-for="(item,index) in data">
<td v-for="column in columns"
display="display:`${colum.merge ? colum.merge.find(m=>m.start == index) ? 'auto':'none':'auto'}`"
rowspan="`${colum.merge && colum.merge.find(m=>m.start == index) ? colum.merge.find(m=>m.start == index).count : 1}`"
>{{item[column.key]}}</td>
</tr>
</table>
主要是添加了display 和 rowspan的逻辑。
display这里使用了两次三元运算符,
第一次,判断当前列的数据中是否存在merge ,如果不存在merge,则当前行正常渲染;
第二次,当存在merge的时候,判断是否是开始合并行,如果是,则正常渲染,不是则隐藏当前面行。
rowspan 只使用了一次三元判断,判断是否是开始合并行,如果是则读取count, 不是则为1.
2.3.3 级联渲染
看似我们已经实现了自动行合并,但,实际还有一个问题,上述方法,每一列的行合并是独立的。
再看这个图,手机类型这一列,有三行都是huawei,但是,不隶属于同一个app,所以不能合并。
为了解决这个问题,我们需要在计算合并的时候,打一个补丁:
判断一下,当这两行相等的时候,上一列的这两行是否也相等。
友情提示: 可看补丁部分。
const newColumns = columns.map((column,colIndex)=>{
/** 当前列的key */
const valKey = column.key;
// 补丁:上一列的 key
const prevColKey = colIndex -1 >= 0 ? columns[colIndex - 1].key : valKey;
/** 当前列中需要 行合并的信息 */
const merge:MergeRow[] = [];
let lastVal = data[0][valKey];
// 补丁:上一列当前行的值
let lastPrevColVal = data[0][prevColKey];
let valNum = 0;
let startIndex = 0;
data.forEach((item,index)=>{
/** 当前行,当前列对应的数值 */
const curValue = item[valKey];
const curPrevColValue = item[prevColKey];
// 当前值和上一行的值相等,则计数+1
if(curValue === lastVal) {
// 补丁: 判断上一列是否相等
if(lastPrevColVal === curPrevColValue) {
valNum ++;
} else {
merge.push({
start:startIndex,
end: index - 1,
count: valNum,
});
// 如果当前值和上一行的值不相等,并且计数小于等于1. 则上一行没有数据需要记录,进行覆盖
lastVal = curValue;
// 这里也要重新赋值
lastPrevColVal = curPrevColValue;
valNum = 1;
startIndex = index;
}
}
// console.log('this is index', curValue, lastVal, valNum);
// 如果当前值和上一行的值不相等,并且计数大于1的话,则上几行存在相邻相等的数值,需要记录
if(curValue !== lastVal) {
merge.push({
start:startIndex,
end: index - 1,
count: valNum,
});
// 如果当前值和上一行的值不相等,并且计数小于等于1. 则上一行没有数据需要记录,进行覆盖
lastVal = curValue;
// 这里也要重新赋值
lastPrevColVal = curPrevColValue;
valNum = 1;
startIndex = index;
}
if(index === data.length -1) {
merge.push({
start:startIndex,
end: index - 1,
count: valNum,
});
}
});
merge.length && (column.merge = merge);
return column;
});
三、总结
到此为止,也就实现了表格的自动级联行合并。
其实预计这是一篇很短就可以说清楚的问题。但没想到写了整整半天。
在写的过程中,遇到的第一个问题是:如何把问题界定清楚?
也就是文章的第一部分。 为什么这么难?因为在跟产品讨论时,不需要上下文,天然我们明白彼此的问题。但读者并不清楚需求上下文的,所以我需要站到一个产品的角度去描述这个需求的边界。
第二个问题是,如何告诉阐述思考的过程?
这也是一定要写这篇文章的原因,在工作的这一年里,我第一次把学生时代的内容进行了实战,发现了很多落地的实践。 但在现在的技术博客和当今的大学计算机教学中,都极少见到类似文章。所以,就想通过本文展示如何将具象问题抽象成一个教科书问题。
即是希望给看到这篇文章的学生们一点工程视野,也想听听各位大佬的看法,看我思考问题是否有所偏差,这个问题有没有更好的解决方法。
那么,针对这个问题,我最终确定了如下思路:
① 用demo 界定问题。
② 放置前置知识(table 行合并 和vue 渲染表格)
③ 拆解行合并问题,并进一步抽象细化成数组重复问题。
④ 解决数组重复问题
⑤ 向上包装,解决行合并。
⑥ 向上包装,解决级联行合并。
⑦ 实现 vue 渲染。
这个过程可以说是一个经典的洋葱式思考,层层抽丝剥茧,找到问题的根源。 然后,再一层层往上包装。
私以为,这种解决问题的方式和算法中的分治法异曲同工,其精髓都是问题细化,逐步击破。
四、延伸
这一部分稍稍谈一下,如何把计算得到的合并参数应用到element-ui中。
通过给
table
传入span-method
方法可以实现合并行或列,方法的参数是一个对象,里面包含当前行row
、当前列column
、当前行号rowIndex
、当前列号columnIndex
四个属性。该函数可以返回一个包含两个元素的数组,第一个元素代表rowspan
,第二个元素代表colspan
。 也可以返回一个键名为rowspan
和colspan
的对象。引用自element-ui官网:Element - The world's most popular Vue UI framework
实际上,2.3.3 的代码中,item 就是当前行,column就是当前列。
剩下的,相信你一定可以!
那,为什么,我没有直接从element-ui的这个方法开始讲述呢?
因为我的实际是服务端渲染,用的模板语言,只能用原生htm了,呜呜呜。
五、参考资料
剑指Offer面试题03-找出数组中重复的数字(5种方法)_JohnArchie的博客-CSDN博客_剑指offer 寻找重复数