Vue3+ElementPlus实现table表格头部置顶

391 阅读4分钟

本文将介绍在使用el-table时,页面滚动时表头的位置自动吸附在顶部

需求

Element Plus的表格组件在使用时,如果表格的内容过多滑动表格,看不到表头的不知道对应的数据是什么,用户体验不太友好,需要将表头固定,但是ELement Plus不支持,因此经过查询觉得可以通过以下几种方式实现该效果

方案一

el-table添加height属性实现表头固定,但是目前该页面的搜索模块已经占了部分面积,如果是小屏幕的电脑表格的视图区会更小,因此考虑一下,滚动到一定位置在给表格设置高度

实现代码如下:

// 滚动的页面
 <div ref="orderPageRef" class="page order"></div>
const orderPageRef: any = ref(null)
const scrollHeight = 480
// 注册scroll事件
onMounted(() => {
  orderPageRef.value.addEventListener('scroll', handleScroll)
})
// 组件卸载之前移除scroll事件
onBeforeUnmount(() => {
  orderPageRef.value.removeEventListener('scroll', handleScroll)
})
const handleScroll = () => {
  const top = orderPageRef.value.scrollTop
  // 判断滚动的距离是否大于定义的高度,如果大于给表格设置高度,否则移除高度
  if (top > scrollHeight) {
    tableHeight.value = `calc(100vh - 200px)
    tabelRef.value.$el.style = `width:100%;height:calc(100vh - 200px);`
  }
  else {
    tableHeight.value = null
    tabelRef.value.$el.style = `width:100%;`
  }
}

缺点

  1. 页面中会存在两个滚动条,页面滚动条和表格滚动条,表格滚动条也可通过操作CSS设置隐藏
  2. 由于给表格设置了高度,页面滚动条会发生变化,可能会造成页面疯狂抖动

方案二

通过修改el-table的CSS样式,使用sticky粘性定位实现,目前项目中也是通过该方案来实现的 实现代码如下

// 当前页面的高度应该是视图的高度,滚动的页面是order-children,order-children是由内容撑开
<div class="page order">
  <div class="order-children">
  </div>
</div>
.order {
  overflow: auto;
  width: 100%;
  height: 100%;
  :deep(.el-table) {
    overflow: visible; // 将el-table的overflow:hidden改为visible
  }
  :deep(.el-table__header-wrapper) {
    position: sticky; // 设置粘性定位
    top: 42px; // 距离顶部的位置
    z-index: 5;
  }
}

方案三

使用自定义指令,通过操作DOM元素,给表头设置fixed固定定位

实现代码如下

// main.ts中注册sticky指令
import { createApp } from 'vue'
import App from './App.vue'
import { createSticky } from '~/utils/sticky'

const app = createApp(App)
createSticky(app)

app.mount('#app')
// top: 固定距离顶部的位置  parent: 滚动的元素 zIndex: 设置层级
<el-table 
  v-sticky="{ top: '64px', parent: '.order', zIndex: 9 }"
  :data="tableData"
>
</el-table>
/**
 * 思路:通过简单的 el-table的 thead和tbody父级别区域,进行设置对于的fixed
 * 1.创建滚动条监听事件,根据滚动条计算表格所在可视窗口位置。设置thead是否固定定位
 * 3.监听横向滚动条的属性变化监听。当监听变化时,说明在拖动横向滚动条,需要将设置对应表头位置,防止错位
 */
function getElParentBySelector(el: any, queryClassSelector: string) {
  if (!el) {
    return el
  }
  if ([...el.classList].includes(queryClassSelector)) {
    return el
  }
  return getElParentBySelector(el.parentNode, queryClassSelector)
}

// 获取表格的宽度
function getTableShowWidth(thead: string) {
  const tableBox = getElParentBySelector(thead, 'el-table')
  return tableBox.getBoundingClientRect().width
}

let scrollHandler: any = null
let resizeHandler: any = null

// 当页面滚动时,表头位置也需要滚动
function setScrollWindow(scrollParent?: any, thead?: any) {
  requestAnimationFrame(() => {
    // 当order滚动了表头位置已经改变了,此时再次触发了window滚动,同步表头的滚动位置,避免错乱
    const scrollXPosition = window.scrollX
    if (scrollParent.scrollLeft) {
      thead.style.left = `${-scrollXPosition - scrollParent.scrollLeft + 72}px`
    }
    else {
      thead.style.left = `${-scrollXPosition + 72}px`
    }
  })
}

// 页面resize时动态设置表头高度
function setResizeWindow(width?: any, thead?: any, tbody?: any) {
  setTimeout(() => {
    const theadWrapper: any = document.querySelector('.el-table__header-wrapper')
    thead.style.width = `${tbody.offsetWidth < width ? tbody.offsetWidth : width}px`
    theadWrapper.style.width = `${tbody.offsetWidth < width ? tbody.offsetWidth : width}px`
  })
}

// 设置表头固定位置
function createTableSticky(el: any, binding: any) {
  let stickyTop = binding.value.top || 0
  const zIndex = binding.value.zIndex || 0
  stickyTop = Number.parseFloat(stickyTop)
  // 获取表格(element)
  let thead = el.querySelector('.el-table__header')
  thead = getElParentBySelector(thead, 'el-table__header-wrapper')
  const tbody = el.querySelector('.el-scrollbar') || el.querySelector('.el-table__body')
  // 获取thead 的显示宽度
  let headerShowWidth = getTableShowWidth(thead)
  // 获取滚动元素
  const scrollParent = document.querySelector(binding.value.parent || 'body')
  if (!scrollParent || binding.value.disabled === true) {
    return
  }
  scrollParent.addEventListener('scroll', () => {
    requestAnimationFrame(() => {
      headerShowWidth = getTableShowWidth(thead)
      const theadHeight = thead.clientHeight
      // // 获取thead距离顶部的距离
      const theadTop = thead.getBoundingClientRect().top
      // 判断是否需要回归原来位置
      const originally = tbody.getBoundingClientRect().top
      // 判断底部距离是否超过表头
      const goBeyond = tbody.getBoundingClientRect().bottom

      if (theadTop <= stickyTop) {
        thead.style.width
        = `${tbody.offsetWidth < headerShowWidth ? tbody.offsetWidth : headerShowWidth}px`
        thead.style.position = 'fixed'
        thead.style.zIndex = zIndex || 1994
        thead.style.top = `${stickyTop}px`
        thead.style.backgroundColor = '#fff'
      }

      if (originally - theadHeight > stickyTop || goBeyond - theadHeight / 2 <= stickyTop) {
        thead.style.width
        = `${tbody.offsetWidth < headerShowWidth ? tbody.offsetWidth : headerShowWidth}px`
        thead.style.position = 'static'
      }
      // 当windows滚动了表头位置已经发生了改变,此时再次滚动order元素,同步表头的滚动位置,避免错乱
      const scrollXPosition = scrollParent.scrollLeft
      if (window.scrollX) {
        thead.style.left = `${-scrollXPosition - window.scrollX + 72}px`
      }
      else {
        thead.style.left = `${-scrollXPosition + 72}px`
      }
    })
  })

  // 监听页面window滚动:表头区域动态滚动
  scrollHandler = () => setScrollWindow(scrollParent, thead)
  window.addEventListener('scroll', scrollHandler, false)

  // 监听resize事件:动态设置表头宽度
  resizeHandler = () => setResizeWindow(getTableShowWidth(thead), thead, tbody)
  window.addEventListener('resize', resizeHandler, false)
}

export function createSticky(vue: any) {
  let clearTimeId: any = 0
  // el-table表头吸顶效果
  vue.directive('sticky', {
    // 当被绑定的元素插入到 DOM 中时……
    mounted(el: any, binding: any) {
      const random = Number.parseInt(`${Math.random() * 10}`)
      // TIP 延时设置,确保表格进行渲染成功!
      clearTimeId = setTimeout(() => {
        createTableSticky(el, binding)
        // clearTimeout(clearTimeId)
      }, 1000 + random)
    },
    update(el: any, binding: any) {
      const random = Number.parseInt(`${Math.random() * 10}`)
      // TIP 延时设置,确保表格进行渲染成功!
      clearTimeId = setTimeout(() => {
        createTableSticky(el, binding)
        // clearTimeout(clearTimeId)
      }, 1000 + random)
    },
    //  指令卸载时,移除scroll和resize事件
    unmounted() {
      window.removeEventListener('scroll', scrollHandler, false)
      window.removeEventListener('resize', resizeHandler, false)
      clearTimeId && clearTimeout(clearTimeId)
    },
  })
}

缺点:

  1. 需要操作DOM元素注册事件,代码有些许复杂
  2. 当表头固定在顶部后,切换浏览器控制台,往左侧滚动,右侧有空白区域,目前还没找到原因