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

最近工作中有一个需求,业务方不想表格局部滚动,又想让表格在随页面滚动时表头可以固定在页面顶部不消失。但是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中存在左右固定的布局的,样式可能会错乱。感兴趣的同学再深入研究下~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,529评论 6 524
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,683评论 3 406
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,905评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,318评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,325评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,754评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,081评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,088评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,653评论 1 327
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,651评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,766评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,359评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,083评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,491评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,654评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,367评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,836评论 2 367