美文网首页
基于ElementUI-Table的表头吸顶(黏性布局)效果实现

基于ElementUI-Table的表头吸顶(黏性布局)效果实现

作者: 灯下草虫鸣314 | 来源:发表于2020-07-03 14:11 被阅读0次

    最近工作中有一个需求,业务方不想表格局部滚动,又想让表格在随页面滚动时表头可以固定在页面顶部不消失。但是ElementUI中的table组件并没有实现此种效果。没办法只能直接撸代码,简单实现以下这种效果。
    为了方便后期方便引用。而且加深一下对Vue自定义指令的了解。我使用自定义指令来实现这项功能。
    考虑到,滚动父元素不同,代码兼容了#document滚动和在div中滚动两种滚动方式。

    代码已经上传到gitHub仓库 地址

    使用自定义指令

    1. 我们给每一想要固定头部的table都加一个自定义指令v-sticky,而且,我们需要传入自定义指令两个参数,top:指定距离顶部的高度,parent:指定滚动容器,如果滚动容器是#document,则不传入parent
     v-sticky="{
      top:0,
      parent:'#table_box' 
    }"
    
    1. 开始编写自定义指令
    • 代码逻辑写在注释中
    import Vue from 'vue'
    // 给固定头设置样式
    function doFix(dom, top) {
      dom.style.position = 'fixed'
      dom.style.zIndex = '2001'
      dom.style.top = top + 'px'
      dom.parentNode.style.paddingTop = top + 'px'
    }
    // 给固定头取消样式
    function removeFix(dom) {
      dom.parentNode.style.paddingTop = 0
      dom.style.position = 'static'
      dom.style.top = '0'
      dom.style.zIndex = '0'
    }
    // 给固定头添加class
    function addClass(dom, fixtop) {
      const old = dom.className
      if (!old.includes('fixed')) {
        dom.setAttribute('class', old + ' fixed')
        doFix(dom, fixtop)
      }
    }
    // 给固定头移除class
    function removeClass(dom) {
      const old = dom.className
      const idx = old.indexOf('fixed')
      if (idx !== -1) {
        const newClass = old.substr(0, idx - 1)
        dom.setAttribute('class', newClass)
        removeFix(dom)
      }
    }
    // 具体判断是否固定头的主函数
    function fixHead(parent, el, top) {
      /**
       * myTop 当前元素距离滚动父容器的高度,
       * fixtop 当前元素需要设置的绝对定位的高度
       * parentHeight 滚动父容器的高度
       */
      let myTop, fixtop, parentHeight
      // 表头DOM节点
      const dom = el.children[1]
    
      if (parent.tagName) {
        // 如果是DOM内局部滚动
        // 当前元素距离滚动父容器的高度= 当前元素距离父元素的高度-父容器的滚动距离-表头的高度
        myTop = el.offsetTop - parent.scrollTop - dom.offsetHeight
        // 父元素高度
        const height = getComputedStyle(parent).height
        parentHeight = Number(height.slice(0, height.length - 2))
        // 绝对定位高度 = 滚动父容器相对于视口的高度 + 传入的吸顶高度
        fixtop = top + parent.getBoundingClientRect().top
        // 如果自己距离顶部距离大于父元素的高度,也就是自己还没在父元素滚动出来,直接return
        if (myTop > parentHeight) {
          return
        }
      } else {
        // document节点滚动
        // 当前元素距离滚动父容器的高度 = 当前元素距离视口顶端的距离
        myTop = el.getBoundingClientRect().top
        // 父元素高度 = 视口的高度
        parentHeight = window.innerHeight
        //  绝对定位高度 = 传入的吸顶高度
        fixtop = top
        // 如果自己距离顶部距离大于父元素的高度,也就是自己还没在父元素滚动出来,直接return
        if (myTop > document.documentElement.scrollTop + parentHeight) {
          return
        }
      }
      // 如果 已经滚动的上去不在父容器显示了。直接return 
      if (Math.abs(myTop) > el.offsetHeight + 100) {
        return
      }
      if (myTop < 0 && Math.abs(myTop) > el.offsetHeight) {
        // 如果当前表格已经完全滚动到父元素上面,也就是不在父元素显示了。则需要去除fixed定位
        removeClass(dom)
      } else if (myTop <= 0) {
        // 如果表头滚动到 父容器顶部了。fixed定位
        addClass(dom, fixtop)
      } else if (myTop > 0) {
        // 如果表格向上滚动 又滚动到父容器里。取消fixed定位
        removeClass(dom)
      } else if (Math.abs(myTop) < el.offsetHeight) {
        // 如果滚动的距离的绝对值小于自身的高度,也就是说表格向上滚动,刚刚显示出表格的尾部是需要将表头fixed定位
        addClass(dom, fixtop)
      }
    }
    // 设置头部固定时表头外容器的宽度写死为表格body的宽度
    function setHeadWidth(el) {
      // 获取到当前表格个表格body的宽度
      const width = getComputedStyle(
        el.getElementsByClassName('el-table__body-wrapper')[0]
      ).width
      // 给表格设置宽度。这里默认一个页面中的多个表格宽度是一样的。所以直接遍历赋值,也可以根据自己需求,单独设置
      const tableParent = el.getElementsByClassName('el-table__header-wrapper')
      for (let i = 0; i < tableParent.length; i++) {
        tableParent[i].style.width = width
      }
    }
    /**
     * 这里有三个全局对象。用于存放监听事件。方便组件销毁后移除监听事件
     */
    const fixFunObj = {}      // 用于存放滚动容器的监听scroll事件
    const setWidthFunObj = {}   // 用于存放页面resize后重新计算head宽度事件
    const autoMoveFunObj ={}    // 用户存放如果是DOM元素内局部滚动时,document滚动时,fix布局的表头也需要跟着document一起向上滚动
    
    // 全局注册 自定义事件
    Vue.directive('sticky', {
      // 当被绑定的元素插入到 DOM 中时……
      inserted(el, binding, vnode) {
        // 首先设置表头宽度
        setHeadWidth(el)
        // 获取当前vueComponent的ID。作为存放各种监听事件的key
        const uid = vnode.componentInstance._uid
        // 当window resize时 重新计算设置表头宽度,并将监听函数存入 监听函数对象中,方便移除监听事件
        window.addEventListener(
          'resize',
          (setWidthFunObj[uid] = () => {
            setHeadWidth(el)
          })
        )
        // 获取当前滚动的容器是什么。如果是document滚动。则可默认不传入parent参数
        const scrollParent =
          document.querySelector(binding.value.parent) || document
        // 给滚动容器加scroll监听事件。并将监听函数存入 监听函数对象中,方便移除监听事件
        scrollParent.addEventListener(
          'scroll',
          (fixFunObj[uid] = () => {
            fixHead(scrollParent, el, binding.value.top)
          })
        )
        // 如果是局部DOM元素内滚动。则需要监听document滚动,document滚动是同步让表头一起滚动。并将监听函数存入 监听函数对象中,方便移除监听事件
        if (binding.value.parent) {
          document.addEventListener('scroll', autoMoveFunObj[uid] = ()=> {
            // 获取到表头DOM节点
            const dom = el.children[1]
            // 如果当前表头是fixed定位。则跟着document滚动一起滚
            if(getComputedStyle(dom).position=== 'fixed'){
              // 滚动的距离是: 滚动父容器距离视口顶端高度 + 传入的吸顶固定距离 
              const fixtop =
              binding.value.top + scrollParent.getBoundingClientRect().top
              doFix(dom, fixtop, 'fixed')
            }
          })
        }
      },
      // component 更新后。重新计算表头宽度
      componentUpdated(el) {
        setHeadWidth(el)
      },
      // 节点取消绑定时 移除各项监听事件。
      unbind(el, binding, vnode) {
        const uid = vnode.componentInstance._uid
        window.removeEventListener('resize', setWidthFunObj[uid])
        const scrollParent =
          document.querySelector(binding.value.parent) || document
        scrollParent.removeEventListener('scroll', fixFunObj[uid])
        if (binding.value.parent) {
          document.removeEventListener('scroll', autoMoveFunObj[uid])
        }
      }
    })
    

    添加测试代码

    • 首先是html代码
    <div class="table">
        <div id="table_box" class="table_box">
          <el-table
            v-for="item in [1, 2]"
            :key="item"
            ref="stickyTable"
            v-sticky="{
              top: 0,
              parent: '#table_box'
            }"
            :data="tableData"
            style="width: 100%"
            border
          >
            <el-table-column prop="date" :label="`日期${item}`" width="180">
            </el-table-column>
            <el-table-column prop="name" :label="`姓名${item}`" width="180">
            </el-table-column>
            <el-table-column prop="address" :label="`地址${item}`">
            </el-table-column>
          </el-table>
        </div>
        <el-table
          v-for="item in [3, 4]"
          :key="item"
          ref="stickyTable"
          v-sticky="{
            top: 0
          }"
          :data="tableData"
          style="width: 100%"
          border
        >
          <el-table-column prop="date" :label="`日期${item}`" width="180">
          </el-table-column>
          <el-table-column prop="name" :label="`姓名${item}`" width="180">
          </el-table-column>
          <el-table-column prop="address" :label="`地址${item}`"> </el-table-column>
        </el-table>
      </div>
    

    页面中定义了四个表格,前两个在一个父容器div#table_box中滚动,后两个则随document滚动

    • 再给表格加一下样式,方便区分每个表格和表头
    .table {
      width: 100%;
      border: 1px solid #ddd;
      padding: 10px 20px;
      .table_box {
        border: 1px solid red;
        margin-bottom: 20px;
        height: 200px;
        overflow-x: hidden;
        overflow-y: auto;
      }
      .el-table {
        margin-bottom: 50px;
        border: 1px solid transparent;
      }
      /deep/ .el-table__header-wrapper {
        th {
          background: rgba(244, 244, 244, 1);
        }
      }
    }
    
    • 加一些js,给表格添加数据。使用setTimeOut模拟异步请求数据
    export default {
      data() {
        return {
          tableData: []
        }
      },
      mounted() {
        this.setTableData()
      },
      methods: {
        setTableData() {
          const result = []
          for (let i = 0; i < 20; i++) {
            result.push({
              date: '2016-05-03',
              name: '王小虎' + i,
              address: '上海市普陀区金沙江路 1516 弄' + i
            })
          }
          setTimeout(() => {
            this.tableData = result
          }, 500)
        }
      }
    }
    

    该demo仅支持ElementUI中简单的table。如table中存在左右固定的布局的,样式可能会错乱。感兴趣的同学再深入研究下~

    相关文章

      网友评论

          本文标题:基于ElementUI-Table的表头吸顶(黏性布局)效果实现

          本文链接:https://www.haomeiwen.com/subject/oyzjqktx.html