实现单元格合并Table组件

2,451 阅读3分钟

前言

这是一篇隔了好久才更新的文章,趁着这个谷雨节气克服下自己懒惰的毛病,写一下自己对于单元格合并组件的一些理解,一方面为了自己更好的总结归纳,另一方面分享给大家。如有写的不正确的地方,欢迎指出谢谢!


使用table标签完成单元格合并

表格并在业务中很是常见,原生的table标签可以很快帮助我们实现一个表格

<table class="raw-table">
  <tr>
    <th>Month</th>
    <th>front-end</th>
    <th>back-end</th>
    <th>Savings</th>
  </tr>
  <tr>
    <td>January</td>
    <td>Express</td>
    <td>Express</td>
    <td>$100</td>
  </tr>
  <tr>
    <td>February</td>
    <td>Vue</td>
    <td>Java</td>
    <td>$200</td>
  </tr>
   <tr>
    <td>March</td>
    <td>React</td>
    <td>Koa</td>
    <td>$200</td>
  </tr>
  <tr>
    <td>April</td>
    <td>React</td>
    <td>Egg</td>
    <td>$200</td>
  </tr>
</table>

当然这样出来的table有点丑陋,需要进行一定的改造,可以对table添加class

.raw-table {
    width: 100%;
    margin-bottom: 10px;
    table-layout: fixed;
    border: 1px solid #dee2e6;
    border-bottom: none;
    border-spacing: 0;
}

td, th {
    color: #333;
    font-size: 14px;
    word-wrap: break-word;
    padding: 12px;
    vertical-align: middle;
    border-bottom: 1px solid #ccc;
    border-right: 1px solid #dee2e6;
    text-align: center;
}

image.png

我们发现有些相同数据的单元格出现,如果能将他们合并的话,那么数据看起来更加直观 所以需要用到单元格合并中的属性colspan, rowspan分别代表列合并和行合并

<table class="raw-table">
  <tr>
    <th>Month</th>
    <th>front-end</th>
    <th>back-end</th>
    <th>Savings</th>
  </tr>
  <tr>
    <td>January</td>
+   <td colspan="2">Express</td>
-
    <td>$100</td>
  </tr>
  <tr>
    <td>February</td>
    <td>Vue</td>
    <td>Java</td>
+   <td rowspan="3">$200</td>
  </tr>
   <tr>
    <td>March</td>
    <td>React</td>
    <td>Koa</td>
-
  </tr>
  <tr>
    <td>April</td>
    <td>React</td>
    <td>Egg</td>
-
  </tr>
</table>

最后效果如下

image.png

上面有个问题是我们是设置了规定的数据,然后进行统一的行列span设置。我们在业务开发中,肯定不止一次会使用到这个单元格合并组件,每次都去进行修改显得很愚蠢和麻烦,所以有了动态单元格合并的想法。接下来基于上面思路,开发一个动态单元格合并组件。

动态单元格合并

动态单元格合并,顾名思义就是数据是动态的,所以大概的思路是

  • 首先总体想法是通过遍历,两两比较数据的行和列
  • 定义data数据,用来比对每个数据的关系,是否进行必要的合并
const tableData = [
    { month: 'January', frontEnd: 'Express', backEnd: 'Express', savings: '$100'},
    { month: 'February', frontEnd: 'Vue', backEnd: 'Java', savings: '$100'},
    { month: 'March', frontEnd: 'React', backEnd: 'Koa', savings: '$100'},
    { month: 'April', frontEnd: 'React', backEnd: 'Egg', savings: '$100'}
]
  • 定义columns数据,用来表示头部,prop为属性key,通过这个prop来对tableData进行映射,label为属性名字
const columns = [{
  prop: 'month',
  label: 'Month',
  align: 'center'
},
{
  prop: 'frontEnd',
  label: 'Front-end',
  align: 'center'
},
{
  prop: 'backEnd',
  label: 'Back-end',
  align: 'center'
},
{
  prop: 'savings',
  label: 'Savings',
  align: 'center'
}]
  • 定义rowSpanInstrowPos对象,rowSpanInst用来存放每一列的数据情况,rowPos为当前操作行的一个记录锚点最后的数据结构,通过比较每一列中相邻两行的数据状况,如果相同获取colPos所在的行数据进行增加,并设置当前行数据为0;否则设置当前行数据为1, 更新rowPos到当前行,最后的数据形式
rowPos = {
    month: 3,
    frontEnd: 3,
    backEnd: 3,
    savings: 0
}
rowSpanInst = {
    month: [1,1,1,1],
    frontEnd: [1,1,1,1],
    backEnd: [1,1,1,1],
    savings: [3,0,0,0]
}

假设当前列在month,通过动画简单说明下思路

2.gif

最后遍历每一列的代码

// 比较每一列的前后两项
getRowSpanArr(columnName) {
  const Len = this.tableData.length;
  // 初始化当前列存储数据
  if (this.rowSpanInst[columnName] == null) {
    this.rowSpanInst[columnName] = [];
  }
  // 初始化当前列锚点为0
  if (this.rowPos[columnName] == null) {
    this.rowPos[columnName] = 0;
  }
  for (let i = 0; i < Len; i++) {
    if (i === 0) {
      this.rowSpanInst[columnName].push(1);
    } else {
      const array1 = this.tableData[i - 1];
      const array2 = this.tableData[i];
      // 判断当前元素与上一个元素是否相同
      if (
        array1[columnName] &&
        array2[columnName] &&
        array1[columnName] === array2[columnName]
      ) {
        this.rowSpanInst[columnName][this.rowPos[columnName]] += 1;
        this.rowSpanInst[columnName].push(0);
      } else {
        this.rowSpanInst[columnName].push(1);
        this.rowPos[columnName] = i;
      }
    }
  }
}
  • 定义colSpanInstcolPos对象,colSpanInst用来存放每一行的数据情况,colPos为当前操作行的一个记录锚点最后的数据结构,通过比较每一行中相邻两列的数据状况,如果相同获取colPos所在的列数据进行增加,并设置当前行数据为0;否则设置当前行数据为1, 更新colPos到当前列,最后的数据形式
colPos = [
    'savings',
    'savings',
    'savings',
    'savings'
]
colSpanInst = [
    {
        month: 1,
        frontEnd: 2,
        backEnd: 0,
        savings: 1
    },
    {
        month: 1,
        frontEnd: 1,
        backEnd: 1,
        savings: 1
    },
    {
        month: 1,
        frontEnd: 1,
        backEnd: 1,
        savings: 1
    },
    {
        month: 1,
        frontEnd: 1,
        backEnd: 1,
        savings: 1
    }
]

同样和遍历列一样道理,设置当前行,通过动画简单说明下思路

3.gif

最终代码逻辑如下

// 比较每一行中的列
getColSpanArr() {
  const Len = this.tableData.length;
  const columnsArray = this.columns.map(item => item.prop);
  for (let i = 0; i < Len; i++) {
    // 初始化当前行存储数据
    if (this.colSpanInst[i] == null) {
      this.colSpanInst[i] = {};
    }
    // 初始化当前列锚点为当前列名
    if (this.colPos[i] == null) {
      this.colPos[i] = columnsArray[i];
    }
    // 当前行数据
    const currentRow = this.tableData[i];
    // 从第二列开始逐个和前一列进行比较
    for (let j = 0; j < columnsArray.length; j++) {
      if (j === 0) {
        this.colSpanInst[i][columnsArray[j]] = 1;
      } else {
        const array1 = currentRow[columnsArray[j - 1]];
        const array2 = currentRow[columnsArray[j]];
        // 判断当前元素与上一个元素是否相同
        if (array1 && array2 && array1 === array2) {
          this.colSpanInst[i][this.colPos[i]] += 1;
          this.colSpanInst[i][columnsArray[j]] = 0;
        } else {
          this.colSpanInst[i][columnsArray[j]] = 1;
          this.colPos[i] = columnsArray[j];
        }
      }
    }
  }
}

最后加上render函数的逻辑,将对应的合并逻辑放在td上基本的形式已经出来了,值得注意的是

  • 存在一种情况,行和列同时合并同一个单元格,由于是行合并先进行判断,则当列合并的时候需要获取当前单元格行合并的情况,通过this.rowSpanInst[columnsArray[j - 1]][i] === 1并且this.rowSpanInst[columnsArray[j]][i] === 1来进行判断,如果不为1,则说明行已经进行合并或者被合并,列不进行合并仍然为独立的单元格

  • 由于是动态render,所以对于rowspan=0或者colspan=0还是会显示出来,所以需要进行display: none处理

  • 可能外部想要对数据进行额外的封装,所以可以column中设置render方法,通过内部调用将当前行数据回调给外层使用

render(h) {
    return (
      <table class="raw-table">
        <thead>
          <tr>
            {this.columns.map(column => (
              <th>
                {column.label}
              </th>
            ))}
          </tr>
        </thead>
        {this.tableData.map((data, index) => {
          return (
            <tr key={data.prop}>
              {this.columns.map(column => {
                return (
                  <td
                    style={{
                      'text-align': column.align || 'center',
                      'vertical-align': 'middle'
                    }}
                    rowspan={this.rowSpanInst[column.prop][index]}
                    colspan={this.colSpanInst[index][column.prop]}
+                   class={{
+                     hide:
+                       this.rowSpanInst[column.prop][index] === 0 ||
+                       this.colSpanInst[index][column.prop] === 0
                    }}
                  >
+                   {column.render
+                     ? column.render(h, { row: data })
+                     : data[column.prop]}
                  </td>
                );
              })}
            </tr>
          );
        })}
      </table>
    );
  }

4.gif

总结

上面简单封装了合并单元格组件,还是基于原始的元素属性通过一定的方法去实现,如有更好的方法欢迎告知和指教。当然还有更多复杂的功能等待去完善和探索

  • 通过data传入colspan或rowspan,不用直接进行数据比对,直接合并
  • 单元格通过拖拽来实现自动合并

源代码

merge-table