• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
艾米栗写代码
帮助1

在 element-ui 和 antv 中都有表格合并,但如何确定哪几行要合并呢? 在随机给定数据的情况下,如何实现自动合并呢?本文将一个一个地解答这些问题。

并在延伸中,谈到了,如何将本文的方法应用到element-ui和antv中。

一、需求描述及样例展示

① 自动行合并

② 当两列为层级关系的时候,合并要有层级关系。

在举例之前,我们先规定一下展示数据。

展示的是不同 app 在不同手机型号下的下载量。

数据为:

  1.  
    // 行信息
  2.  
    const columns = [
  3.  
    {
  4.  
    key:'app',
  5.  
    label:'app',
  6.  
    },
  7.  
    {
  8.  
    key:'phone',
  9.  
    label:'手机类型',
  10.  
    },
  11.  
    {
  12.  
    key:'phoneType',
  13.  
    label:'手机型号',
  14.  
    },
  15.  
    {
  16.  
    key:'downloadCount',
  17.  
    label:'下载量',
  18.  
    },
  19.  
    ];
  20.  
     
  21.  
    //数据信息
  22.  
    const data = [
  23.  
    {
  24.  
    app:'微信',
  25.  
    phone:'iphone',
  26.  
    phoneType:'iphone11',
  27.  
    downloadCount:'138,999,999'
  28.  
    },
  29.  
    {
  30.  
    app:'微信',
  31.  
    phone:'iphone',
  32.  
    phoneType:'iphone12',
  33.  
    downloadCount:'138,999,999'
  34.  
    },
  35.  
    {
  36.  
    app:'微信',
  37.  
    phone:'huawei',
  38.  
    phoneType:'mate40',
  39.  
    downloadCount:'138,999,999'
  40.  
    },
  41.  
    {
  42.  
    app:'微信',
  43.  
    phone:'huawei',
  44.  
    phoneType:'mate40pro',
  45.  
    downloadCount:'138,999,999'
  46.  
    },
  47.  
    {
  48.  
    app:'抖音',
  49.  
    phone:'huawei',
  50.  
    phoneType:'mate40',
  51.  
    downloadCount:'138,999,999'
  52.  
    },
  53.  
    {
  54.  
    app:'抖音',
  55.  
    phone:'iphone',
  56.  
    phoneType:'iphone12',
  57.  
    downloadCount:'138,999,999'
  58.  
    },
  59.  
    {
  60.  
    app:'抖音',
  61.  
    phone:'iphone',
  62.  
    phoneType:'iphone11',
  63.  
    downloadCount:'138,999,999'
  64.  
    },
  65.  
    ]
学新通

常规表格展现结果为:

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 实现表格的行合并的方式,代码如下。

  1.  
    <table border="1">
  2.  
    <tr>
  3.  
    <th>First Name</th>
  4.  
    <th>Bill Gates</td>
  5.  
    </tr>
  6.  
    <tr>
  7.  
    <td rowspan="2">Telephone:</th>
  8.  
    <td>555 77 854</td>
  9.  
    </tr>
  10.  
    <tr>
  11.  
    <td style="display:none;">Telephone:</th>
  12.  
    <td>555 77 855</td>
  13.  
    </tr>
  14.  
    </table>

效果图如下:

学新通

 可以得知,原生html 是通过 在列上设置 rowspan 来实现行合并。rowspan 的 数值为几则合并几行。

2.2 vue 实现表格渲染

但在实际需求中,表格的渲染一定是批量完成的。

以上述手机下载量数据为例,在vue中,渲染这个表格的代码为:

  1.  
    <table>
  2.  
    <tr>
  3.  
    <td v-for="column in columns">
  4.  
    {{column.label}}
  5.  
    </td>
  6.  
    </tr>
  7.  
    <tr v-for="(item, index) in data">
  8.  
    <td v-for="column in columns">
  9.  
    {{item[column.key]}}
  10.  
    </td>
  11.  
    </tr>
  12.  
    </table>

2.3 vue 实现表格行合并渲染

由上述信息可知,为了实现行合并,我们需要知道,每一列数据中,

①开始合并的行,在开始合并的行添加 rowspan 和 数值

②结束合并的行,在开始合并行和结束合并行之间的所有行style 添加 display :none。

将上述三个需要计算的数值可以抽象为:

  1.  
    /** 合并行 */
  2.  
    interface MergeRow {
  3.  
    /** 开始合并的行 */
  4.  
    start: number;
  5.  
     
  6.  
    /** 开始合并的行 */
  7.  
    end: number;
  8.  
     
  9.  
    /** 一共合并的行数: end - start 1 */
  10.  
    count: number;
  11.  
    }

2.3.1 计算行合并

计算以上三个数值,可以抽象为: 在数组中,找到连续重复的数。

这是一个很简单的OJ问题,需要一个变量记录个数即可。 那么,把这个问题稍稍扩展一下,变成,找到数组中有几组连续重复的数,并记录开始重复,结束重复和重复数量

那么解法就变成了如下代码:

  1.  
    const {data, columns} = table;
  2.  
    const newColumns = columns.map((column)=>{
  3.  
    /** 当前列的key */
  4.  
    const valKey = column.key;
  5.  
     
  6.  
    /** 当前列中需要 行合并的信息 */
  7.  
    const merge:MergeRow[] = [];
  8.  
     
  9.  
    //第一行数据
  10.  
    let lastVal = data[0][valKey];
  11.  
    let valNum = 0;
  12.  
    let startIndex = 0;
  13.  
     
  14.  
    data.forEach((item,index)=>{
  15.  
    /** 当前行,当前列对应的数值 */
  16.  
    const curValue = item[valKey];
  17.  
     
  18.  
    // 当前值和上一行的值相等,则计数 1
  19.  
    if(curValue === lastVal) {
  20.  
    valNum ;
  21.  
    }
  22.  
     
  23.  
    // 如果当前值和上一行的值不相等,并且计数大于1的话,则上几行存在相邻相等的数值,需要记录
  24.  
    if(curValue !== lastVal) {
  25.  
    merge.push({
  26.  
    start:startIndex,
  27.  
    end: index - 1,
  28.  
    count: valNum,
  29.  
    });
  30.  
    // 如果当前值和上一行的值不相等,并且计数小于等于1. 则上一行没有数据需要记录,进行覆盖
  31.  
    lastVal = curValue;
  32.  
    valNum = 1;
  33.  
    startIndex = index;
  34.  
    }
  35.  
     
  36.  
    if(index === data.length -1) {
  37.  
    merge.push({
  38.  
    start:startIndex,
  39.  
    end: index - 1,
  40.  
    count: valNum,
  41.  
    });
  42.  
    }
  43.  
     
  44.  
     
  45.  
    });
  46.  
    column.merge = merge;
  47.  
    return column;
  48.  
    });
学新通

注意,在在上面的代码中,我把每一列中,存在的合并信息,存到了merge属性中。 

这是为了渲染的时候可以读取这些信息准备的。

2.3.2 vue 渲染行合并

渲染的代码如下:

  1.  
    <table>
  2.  
    <tr>
  3.  
    <td v-for="column in columns"> {{column.label}}</td>
  4.  
    </tr>
  5.  
    <tr v-for="(item,index) in data">
  6.  
    <td v-for="column in columns"
  7.  
    display="display:`${colum.merge ? colum.merge.find(m=>m.start == index) ? 'auto':'none':'auto'}`"
  8.  
    rowspan="`${colum.merge && colum.merge.find(m=>m.start == index) ? colum.merge.find(m=>m.start == index).count : 1}`"
  9.  
    >{{item[column.key]}}</td>
  10.  
    </tr>
  11.  
    </table>

主要是添加了display 和 rowspan的逻辑。

display这里使用了两次三元运算符,

第一次,判断当前列的数据中是否存在merge ,如果不存在merge,则当前行正常渲染;

第二次,当存在merge的时候,判断是否是开始合并行,如果是,则正常渲染,不是则隐藏当前面行。

rowspan  只使用了一次三元判断,判断是否是开始合并行,如果是则读取count, 不是则为1.

2.3.3 级联渲染

看似我们已经实现了自动行合并,但,实际还有一个问题,上述方法,每一列的行合并是独立的。

学新通

再看这个图,手机类型这一列,有三行都是huawei,但是,不隶属于同一个app,所以不能合并。

为了解决这个问题,我们需要在计算合并的时候,打一个补丁:

判断一下,当这两行相等的时候,上一列的这两行是否也相等。 

友情提示: 可看补丁部分。

  1.  
    const newColumns = columns.map((column,colIndex)=>{
  2.  
    /** 当前列的key */
  3.  
    const valKey = column.key;
  4.  
    // 补丁:上一列的 key
  5.  
    const prevColKey = colIndex -1 >= 0 ? columns[colIndex - 1].key : valKey;
  6.  
     
  7.  
    /** 当前列中需要 行合并的信息 */
  8.  
    const merge:MergeRow[] = [];
  9.  
     
  10.  
    let lastVal = data[0][valKey];
  11.  
    // 补丁:上一列当前行的值
  12.  
    let lastPrevColVal = data[0][prevColKey];
  13.  
    let valNum = 0;
  14.  
    let startIndex = 0;
  15.  
     
  16.  
    data.forEach((item,index)=>{
  17.  
    /** 当前行,当前列对应的数值 */
  18.  
    const curValue = item[valKey];
  19.  
    const curPrevColValue = item[prevColKey];
  20.  
     
  21.  
    // 当前值和上一行的值相等,则计数 1
  22.  
    if(curValue === lastVal) {
  23.  
    // 补丁: 判断上一列是否相等
  24.  
    if(lastPrevColVal === curPrevColValue) {
  25.  
    valNum ;
  26.  
    } else {
  27.  
    merge.push({
  28.  
    start:startIndex,
  29.  
    end: index - 1,
  30.  
    count: valNum,
  31.  
    });
  32.  
    // 如果当前值和上一行的值不相等,并且计数小于等于1. 则上一行没有数据需要记录,进行覆盖
  33.  
    lastVal = curValue;
  34.  
    // 这里也要重新赋值
  35.  
    lastPrevColVal = curPrevColValue;
  36.  
    valNum = 1;
  37.  
    startIndex = index;
  38.  
    }
  39.  
    }
  40.  
     
  41.  
    // console.log('this is index', curValue, lastVal, valNum);
  42.  
    // 如果当前值和上一行的值不相等,并且计数大于1的话,则上几行存在相邻相等的数值,需要记录
  43.  
    if(curValue !== lastVal) {
  44.  
    merge.push({
  45.  
    start:startIndex,
  46.  
    end: index - 1,
  47.  
    count: valNum,
  48.  
    });
  49.  
    // 如果当前值和上一行的值不相等,并且计数小于等于1. 则上一行没有数据需要记录,进行覆盖
  50.  
    lastVal = curValue;
  51.  
    // 这里也要重新赋值
  52.  
    lastPrevColVal = curPrevColValue;
  53.  
    valNum = 1;
  54.  
    startIndex = index;
  55.  
    }
  56.  
     
  57.  
    if(index === data.length -1) {
  58.  
    merge.push({
  59.  
    start:startIndex,
  60.  
    end: index - 1,
  61.  
    count: valNum,
  62.  
    });
  63.  
    }
  64.  
     
  65.  
     
  66.  
    });
  67.  
    merge.length && (column.merge = merge);
  68.  
    return column;
  69.  
    });
学新通

三、总结

到此为止,也就实现了表格的自动级联行合并。

其实预计这是一篇很短就可以说清楚的问题。但没想到写了整整半天。

在写的过程中,遇到的第一个问题是:如何把问题界定清楚?

也就是文章的第一部分。 为什么这么难?因为在跟产品讨论时,不需要上下文,天然我们明白彼此的问题。但读者并不清楚需求上下文的,所以我需要站到一个产品的角度去描述这个需求的边界。

第二个问题是,如何告诉阐述思考的过程?

这也是一定要写这篇文章的原因,在工作的这一年里,我第一次把学生时代的内容进行了实战,发现了很多落地的实践。 但在现在的技术博客和当今的大学计算机教学中,都极少见到类似文章。所以,就想通过本文展示如何将具象问题抽象成一个教科书问题。

即是希望给看到这篇文章的学生们一点工程视野,也想听听各位大佬的看法,看我思考问题是否有所偏差,这个问题有没有更好的解决方法。

那么,针对这个问题,我最终确定了如下思路:

① 用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了,呜呜呜。

五、参考资料

分治法_百度百科

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfjjfge
系列文章
更多 icon
同类精品
更多 icon
继续加载