仿制一个element tabel组建

1,551 阅读4分钟

最近接到一个新需求,一个表单组件, 要求横向和纵向两种模式,要求固定表头和首列。这里做个记录。 首先呢, 当然是踩在巨人的肩膀上, 看看其他的开源库怎么实现的固定表头和首列,

一、结构分析

效果

拆分

代码

分析

其实, 整个表单被分成了三个部分, 每一种颜色是一个单独的表格块,拼接起来的, 每一个部分都是用一个容器包裹着一个table表单, 用于限制宽高和处理滚动

<div class="wrapper"> 
    <table></table>
</div>
  • 黄色部分, 只渲染了表头部分,外部容器overflo:hidden
  • 绿色部分就是核心, 渲染了tbody部分, 外部容器overflow: atuo, 监听他的滚动条事件, 然后同步到固定表头和固定列的滚动条
  • 蓝色部分其实包含了了一个thead和tbody, 然后设置宽度等于首列的宽, overflow: hidden 然后绝对定位覆盖在右边的表单上面
  • 在body部分横向滚动的时候, thead同步滚动, 在body垂直滚动的时候, 左边的tbody也同步滚动

这里我用了两个子组件tableHead和tableBody来分别渲染thead和tbody部分,

这样就比较清晰了。剩下的就是核心代码编写了。

二、实现

接下来遇到的一个问题就是多级表头了。

仔细分析这个dom结构。其实他是分成三行来渲染的, 然后通过rowspan和colspan来实现的合并单元格 所以我们需要通过算法, 将树形的表头数据摊开成一个二维数组, 然后每个数据节点都需要算出rowspan和colspan。
这里总结一下规律

  • 每一个单元格的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的作用域插槽来实现, 将当列的数据抛出去就好了

这样, 一个横向的固定表头首列就完成了, 实际上就是element table组建的劣化版。有时间再写一下纵向表单的处理, 相对来说要复杂一下。