写作之前
先来一张效果图
- 右侧是一个正常的table,有很多列,此处只是随便弄了2列
- 分类的层级不固定1-5级
- 左侧第一列是table,但是这个table遇到分类一样的时候需要自动合并
合并规则
- 第一列前4个都是A1所以需要合并,A2单独一列无需合并
- 第二列B1单独一列,B2合并3列
- 第三列虽然第1,2行都是C1但是不能合并,因为前面的分类不一样
如何实现这样的交互
- 首先我们看到这样的一个自动合并,首先想到的肯定是
table,再利用rowspan合并列 - 行合并之后下一行td的数量将会比上一行少
html结构如何设计
要显示一个这样的table结构,肯定有3层的for循环, 每一个分类一个 tbody
<table>
<tbody v-for="item in showTbodyList" :key="item.name">
<tr v-for="(tr, index) in item.children" :key="index">
<td v-for="it in tr" :key="it.key" :rowspan="it.rowspan">
{{ it.name }}
</td>
</tr>
</tbody>
</table>
后台数据结构
暂时后端只有5级,但是我们要考虑的肯定是 n 级
interface IData{
level1: string, // 第一级分类
level2: string, // 第二级分类
level3: string, // 第三级分类
level4: string, // 第四级分类
level5: string, // 第五级分类
key: string, // 数据唯一id
// ...其他属性忽略
}
原始数据
开始的图片的数据如下
如果要显示,那么最终的数据应该是这样的
组件设计
首先分析那些是变化的
- 展示的数据
- 用于表示的层级字段名
所以得到以下 props
props: {
// 层级的字段名
levelList: {
type: Array,
required: true,
},
//要展示的list
list: {
type: Array,
required: true,
},
},
获取最大的层级深度
因为层级是不固定的所以我们需要获取最大的层级深度maxColumns
此处我们使用计算属性实现,从后往前遍历,如果有一个数据有值,那么此时的i就是最大的maxColumns
maxColumns() {
const { list, levelList } = this;
for (let len = levelList.length, i = len - 1; i >= 0; i--) {
const level = levelList[i];
if (list.some(item => item[level])) {
return i + 1;
}
}
return 0;
}
如何得到没有合并的表格
- 首先找出第一级分类的名字,对数据筛选里面有第一级分类的,那么这就是tbody的数据
- tbody的数据行数就是tr的行数
- td的个数就是
maxColumns所以代码如下
getTbodyList() {
const { list, levelList, maxColumns } = this;
// 获取第一列有几种
const classification = Array.from(
new Set(list.map(item => item[levelList[0]])),
);
return classification.map(name => {
const tbodyList = list.filter(item => item[levelList[0]] === name);
const trLength = tbodyList.length;
const children = [];
// 行
for (let i = 0; i < trLength; i++) {
const tr = [];
// 列
for (let j = 0; j < maxColumns; j++) {
const tdName = tbodyList[i][levelList[j]] || '';
tr.push({
name: tdName,
rowspan: null,
key: Math.random(),
});
}
children.push(tr);
}
return {
name,
children,
};
});
}
此时我们可以获取到 合并规则 时的样子
合并行
上一步生成tableData之后
- 以列为单位
- 找出当前列的分类
- 遍历每一个分类
- 开始时下标
i为0 - 找出一列中分类一样的数据
trs,将trs第0条的i个的rowspan设置为trs.length - 设置
tableData为trs,下标i+1 - 开始递归
setTableRowSpan(tableData) {
const { maxColumns } = this;
function setRowspan(tbodyData, i = 0) {
if (i >= maxColumns) {
return;
}
const classification = Array.from(
new Set(tbodyData.map(item => item[i].name)),
);
for (let j = 0; j < classification.length; j++) {
const trs = tbodyData.filter(
item => item[i].name === classification[j],
);
trs[0][i].rowspan = trs.length;
setRowspan(trs, i + 1);
}
}
tableData.forEach(item => {
setRowspan(item.children);
});
}
清除rowspan为null的td
cleanEmptyTd(table) {
table.forEach(tbody => {
tbody.children.forEach(tr => {
// 必须从后往前
for (let i = tr.length - 1; i >= 0; i--) {
const td = tr[i];
if (!td.rowspan) {
tr.splice(i, 1);
}
}
});
});
}
github
本文所有代码都放到github
如果有更好的方案,或者文中有错误的地方评论区见,也可以直接github提issure