elementUI Table组件实现表头吸顶效果

2,614 阅读3分钟

号外号外📢

表头吸顶功能进行了进一步重构,欢迎移步至新文章《纯css实现element UI Table组件表头吸顶效果》


需求描述

当 table 内容过多的时候,页面上滑滚动,表头的信息也会随着被遮挡,无法将表头信息和表格内容对应起来,这样用户对着没有特征的表格数据就会迷茫,然后上翻它对应的列信息是什么。为了解决用户在使用过程中的这种“信息不全”的问题,提出两个方案:

  1. 限制表格的最大高度,让表格内部出现滚动条
  2. 当页面滚动时表头不在页面可视范围内的进行表格吸顶操作

针对第一种方案,首先是受限于不同的用户屏幕大小和浏览器 resize 的时候窗口大小改变导致这个表格最大高度也要随之变化,其次就是页面里面除了表格以外的动态内容渲染也会对表格最大高度有影响。考虑到上述的问题之后,选择了第二个方案。

如何实现吸顶?🤔️

说到吸顶很容易会联想到两个跟布局相关的样式 fixed 和 sticky,我一开始选择了 fixed 布局,写了很多 js 代码,主要是在滚动事件通过计算表头和顶部的距离判断是否需要吸顶。虽然有加防抖的限制但是滚动起来的大量计算还是让呈现出来的效果不尽如人意,还有 fixed 之后的表格内部的横向滚动无法和表头做联动。经过很多现实的原因之后我还是选择了用 sticky 去实现表格的吸顶。

开始编码💪

环境:vue2.6+、element UI
step1:el-table__header-wrapper加上样式

position:sticky;
top:0px; // 距离取决于你要固定的位置

难题:加了 sticky 定位之后问题还没有解决,因为在 table 有列 fixed 的时候效果(如下图所示 fixed 列表头被遮挡),从观察 table 的 dom 结构我们可以知道 table 的 header 和 fixed 列的 header 是兄弟节点的关系,那是否给el-table__fixed加上sticky定位去解决这个问题?答案是不行的!因为 fixed 本身是 absolute 定位,style:height 是实时计算 table 高度的值,如果改成 sticky 定位,高度计算就会出问题(会一直增大)。

image.png

image.png

step2: 为了解决 table 有 fixed 列时候的效果问题,我将el-table__fixed节点浅克隆了一份命名为cloneHeaderContainerEl,并且将原来el-table__fixed的子节点.el-table__fixed-header-wrapper直接append到新克隆的容器cloneHeaderContainerEl里面,再将整个节点加入到el-table__header-wrapper dom 节点里面。然后el-table__header-wrapper节点需要加入flex布局样式,好适配多个表头的存在。具体代码是:

const fixHeaderRightWrap = tableEl?.querySelector('.el-table__fixed-right')
const fixHeaderLeftWrap = tableEl?.querySelector('.el-table__fixed')
this.hasFixedCol = [fixHeaderLeftWrap, fixHeaderRightWrap].some(el => !!el);

[fixHeaderLeftWrap, fixHeaderRightWrap].forEach((fixHeaderWrap, index) => {
  if (!fixHeaderWrap) return
  const cloneHeaderContainerEl = fixHeaderWrap.cloneNode(false)
  cloneHeaderContainerEl.style.height = 'auto'
  const fixedheaderWrapperEl = fixHeaderWrap.querySelector('.el-table__fixed-header-wrapper')
  cloneHeaderContainerEl.appendChild(fixedheaderWrapperEl)

  if (index === 0) {
    headerWrapperEl.insertBefore(cloneHeaderContainerEl, headerWrapperEl.firstChild)
    headerEl.style['margin-left'] = `-${cloneHeaderContainerEl.style.width}`
    this.fixedLeftEl = cloneHeaderContainerEl
  } else {
    headerWrapperEl.appendChild(cloneHeaderContainerEl)
    this.fixedRightEl = cloneHeaderContainerEl
  }
})

if (this.hasFixedCol) {
  headerWrapperEl.style.display = 'flex'
  window.addEventListener('resize', this.handleResize)
}

step3: 由于 fixed 的列会根据窗口大小变化调整el-table__fixed节点 style:width 宽度,但是克隆的节点并没有享有对应的事件,需要给克隆的el-table__fixed节点加上对应的 resize 事件,如下:

const fixHeaderRightWrap = tableEl?.querySelector('.el-table__fixed-right')
const fixHeaderLeftWrap = tableEl?.querySelector('.el-table__fixed')
this.hasFixedCol = [fixHeaderLeftWrap, fixHeaderRightWrap].some(el => !!el);

[fixHeaderLeftWrap, fixHeaderRightWrap].forEach((fixHeaderWrap, index) => {
  if (!fixHeaderWrap) return
  const cloneHeaderContainerEl = fixHeaderWrap.cloneNode(false)
  cloneHeaderContainerEl.style.height = 'auto'
  const fixedheaderWrapperEl = fixHeaderWrap.querySelector('.el-table__fixed-header-wrapper')
  cloneHeaderContainerEl.appendChild(fixedheaderWrapperEl)

  if (index === 0) {
    headerWrapperEl.insertBefore(cloneHeaderContainerEl, headerWrapperEl.firstChild)
    headerEl.style['margin-left'] = `-${cloneHeaderContainerEl.style.width}`
    this.fixedLeftEl = cloneHeaderContainerEl
  } else {
    headerWrapperEl.appendChild(cloneHeaderContainerEl)
    this.fixedRightEl = cloneHeaderContainerEl
  }
})

if (this.hasFixedCol) {
  headerWrapperEl.style.display = 'flex'
  window.addEventListener('resize', this.handleResize)
}

step4: 样式微调

.el-table {
  border-top: none;
  overflow: initial;
}

.el-table--border {
  ::v-deep {
    .el-table__header-wrapper {
      box-sizing: border-box;
      border-right: 1px solid #e9ecef;

      .el-table__fixed-right {
        right: -1px;
        left: auto;
      }
    }
  }
}

::v-deep {
  .el-table__fixed {
    box-shadow: 3px 0 5px rgb(0 0 0 / 12%);
  }
  .el-table__fixed-right {
    box-shadow: -3px 0 5px rgb(0 0 0 / 12%);
  }

  .el-table__header-wrapper {
    position: static;
    border-top: 1px solid #e9ecef;
    z-index: 5;

    .el-table__fixed, .el-table__fixed-right {
      position: sticky;
      height: auto;
      flex-shrink: 0;
      z-index: 9;
    }
  }

  .el-table__header-wrapper:has(+.is-scrolling-right, +.is-scrolling-none) {
    .el-table__fixed-right {
      box-shadow: none;
    }
  }

  .el-table__header-wrapper:has(+.is-scrolling-left, +.is-scrolling-none) {
    .el-table__fixed{
      box-shadow: none;
    }
  }
}

在线源码

效果呈现😁

因为吸顶现在通过样式去实现,没有经过太多的 js 控制计算,呈现出来的效果比较丝滑。
demo地址:在线demo

号外号外📢

表头吸顶功能进行了进一步重构,欢迎移步至新文章《纯css实现element UI Table组件表头吸顶效果》