保姆级教程 | 表格自动行合并实现

在 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 iphone11138,999,999
微信iphone iphone12138,999,999
微信huaweimate40138,999,999
微信huaweimate40pro138,999,999
抖音huaweimate40138,999,999
抖音iphoneiphone12138,999,999
抖音iphoneiphone11

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。 也可以返回一个键名为rowspancolspan的对象。

引用自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 寻找重复数

分治法_百度百科

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值