最近接到一个新需求,一个表单组件, 要求横向和纵向两种模式,要求固定表头和首列。这里做个记录。 首先呢, 当然是踩在巨人的肩膀上, 看看其他的开源库怎么实现的固定表头和首列,
一、结构分析
效果
拆分
代码
分析
其实, 整个表单被分成了三个部分, 每一种颜色是一个单独的表格块,拼接起来的, 每一个部分都是用一个容器包裹着一个table表单, 用于限制宽高和处理滚动
<div class="wrapper">
<table></table>
</div>
- 黄色部分, 只渲染了表头部分,外部容器
overflo:hidden, - 绿色部分就是核心, 渲染了tbody部分, 外部容器
overflow: atuo, 监听他的滚动条事件, 然后同步到固定表头和固定列的滚动条 - 蓝色部分其实包含了了一个thead和tbody, 然后设置宽度等于首列的宽,
overflow: hidden然后绝对定位覆盖在右边的表单上面 - 在body部分横向滚动的时候, thead同步滚动, 在body垂直滚动的时候, 左边的tbody也同步滚动
这里我用了两个子组件tableHead和tableBody来分别渲染thead和tbody部分,
这样就比较清晰了。剩下的就是核心代码编写了。
二、实现
接下来遇到的一个问题就是多级表头了。
这里总结一下规律
- 每一个单元格的colsan应该是他全部子节点的总数,如果他不含子节点,则为1
- 每一个单元格的rowspan分两种情况, 如果他不含子节点, 就是最大层数-当前曾是 + 1, 如果包含子节点,则为1
丢代码
// 提取全部的列
const getAllColumns = (columns) => {
const result = [];
columns.forEach((column) => {
if (column.childs && column.childs.length) {
result.push(column);
result.push.apply(result, getAllColumns(column.childs));
// result.push(...getAllColumns(column.childs))
} else {
result.push(column);
}
});
return result;
}
这个函数的作用是将多个树形结构组成的数组摊平 A、B分别是这个表头的顶级单元格
// 从树形整理成二维数组
const convertToRows = (originColumns) => {
let maxLevel = 1;
// 递归的算出每一个子节点的层数, 和colspan
const traverse = (column, parent) => {
if (parent) {
column.level = parent.level + 1;
if (maxLevel < column.level) {
maxLevel = column.level;
}
}
if (column.childs && column.childs.length) {
let colSpan = 0;
column.childs.forEach((subColumn) => {
traverse(subColumn, column);
colSpan += subColumn.colSpan;
});
// 节点的colspan是子节点的和
column.colSpan = colSpan;
} else {
// 不存在则为一
column.colSpan = 1;
}
};
originColumns.forEach((column) => {
column.level = 1;
traverse(column);
});
const rows = [];
for (let i = 0; i < maxLevel; i++) {
rows.push([]);
}
// 转化成一维数组的数据,
// 注意, 在调用getAllColumns之前, 已经将每一个节点设置好了level
const allColumns = getAllColumns(originColumns);
// 转化成二维数组, 计算rowspan
allColumns.forEach((column) => {
if (!column.childs.length) {
column.rowSpan = maxLevel - column.level + 1;
} else {
column.rowSpan = 1;
}
rows[column.level - 1].push(column);
});
return rows;
}
这一步将根据level将这个数组变成一个二维数组、每一层的节点在一个数组内
其他细节, 样式的调整
- colgroup和col可以快速的格式化单元格,而col的个数就是这个table列的个数, 应该是每一个树的最终节点的和
- 出现纵向滚动条之后会影响到整体宽度, 导致横向混动thead和tbody不同步。产生错位, 如下图
解决办法是判断当纵向滚动条存在的时候, 在thead添加一个单元格, 宽度为滚动条的宽度
// 获取滚动条宽度
export function getScrollbarWidth() {
if (Vue.prototype.$isServer) return 0;
if (scrollBarWidth !== undefined) return scrollBarWidth;
const outer = document.createElement('div');
outer.className = 'el-scrollbar__wrap';
outer.style.visibility = 'hidden';
outer.style.width = '100px';
outer.style.position = 'absolute';
outer.style.top = '-9999px';
document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';
const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
scrollBarWidth = widthNoScroll - widthWithScroll;
return scrollBarWidth;
};
-
在出现横向滚动条的时候, 左边绝对定位会覆盖一部分横向滚动条, 如下
所以也要特殊处理, 限制左边的tbody容器的高度减去一个滚动条的高度 -
如果窗口宽度大于表格的理论宽度, 表格就会自动适应, 底部的tbody边框了。而左边绝对定位的宽度没变, 会导致错位, 所以要给table设置一个宽度, 这个宽度是可以通过列数*每列的宽计算出来的
-
如果需要操作栏的话, 可以通过vue的作用域插槽来实现, 将当列的数据抛出去就好了