前言
这是一篇隔了好久才更新的文章,趁着这个谷雨节气克服下自己懒惰的毛病,写一下自己对于单元格合并组件的一些理解,一方面为了自己更好的总结归纳,另一方面分享给大家。如有写的不正确的地方,欢迎指出谢谢!
使用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;
}
我们发现有些相同数据的单元格出现,如果能将他们合并的话,那么数据看起来更加直观
所以需要用到单元格合并中的属性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>
最后效果如下
上面有个问题是我们是设置了规定的数据,然后进行统一的行列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'
}]
- 定义
rowSpanInst
和rowPos
对象,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,通过动画简单说明下思路
最后遍历每一列的代码
// 比较每一列的前后两项
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;
}
}
}
}
- 定义
colSpanInst
和colPos
对象,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
}
]
同样和遍历列一样道理,设置当前行,通过动画简单说明下思路
最终代码逻辑如下
// 比较每一行中的列
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>
);
}
总结
上面简单封装了合并单元格组件,还是基于原始的元素属性通过一定的方法去实现,如有更好的方法欢迎告知和指教。当然还有更多复杂的功能等待去完善和探索
- 通过data传入colspan或rowspan,不用直接进行数据比对,直接合并
- 单元格通过拖拽来实现自动合并