vue组件之table表格

table表格组件预览地址 (展现官网会略卡,一开始容易加载不出来)css

基于vue写一个table组件

目前暂时打算完成的功能vue

  • 固定表头
  • 固定列
  • 固定排序,接受排序函数,请求后端排序
  • 请求时期的动画
  • 多选框
  • 展开行

结构和api借鉴AntDesign的,git

<x-table      
 :columns="columns1"
 :data="data">
 </x-table>
复制代码

api设计:github

  • data,显示的表格数据,是一个数组,数组里的每一个对象都须要一个惟一的key好用来确认他们的index
{key:1,name:'JavaScript',price:80,year:12},
复制代码
  • columns 表头,里面的属性名对应data里面的属性名。
columns1:[
                {text:'名字',field:'name'},
                {text:'价格',field:'price'},
                {text:'年份',field:'year'},
            ],
复制代码

固定表头

效果如图 web

实现原理和结构

遗憾的是table的头部没法只经过css去固定,这东西很是特殊。因而, 把这里分为两个部分,bodyheaderchrome

header是一个崭新的table,该table组件里面只有<thead></thead>,经过绝对定位覆盖body的头部,以达到固定头部的目的。后端

<template>  
   <div>
    <!-- 固定的头部,header部分-->
   <table>
   <thead>
   </thead>
   </table>
   
    <!-- body部分-->
   <table>
   <tbody>
   </tbody>
   </table>
   </div>
</template>
复制代码

Api设计与宽度的控制

首先固定头部确定须要一个最大高度,也就是maxHeightapi

<x-table :columns="columns1"
                 :data="data"
                 //.....
                 maxHeight="300"
        >
        </x-table>
复制代码

这里的header部分的table由于是没有body的,两个table对应的每一个格子的宽度不相同,就致使了不对齐的问题,因而就须要固定宽度。数组

columns传入的每条数据里面加入width,意味着每列对应格子的宽度。浏览器

columns1:[
                    {text:'名字',field:'name',width:200},
                    ],
复制代码

如今是如何控制格子的宽度,开始踩坑的时候我用js遍历去给格子.style.width赋值,但这种作法是彻底不可行的。

在网页上检查了elementUI table组件的HTML结构后,发现是用 colgroup作的。

<colgroup>
                 <col v-for="(column,index) in columns" :key="index" :style="{width:`${column.width}px`}">
               </colgroup>
复制代码

这里还要考虑checkBox的影响,这在后面会说到。

而后就能够用绝对定位覆盖到上面。

position: absolute;
            left: 0;
            top: 0;
复制代码

固定最大高度,超出部分能够滚动

首先table的外层须要包裹一层div,用来控制最大高度。设置css:overflow:auto;。`

固定列

实现原理和固定头部相似,可是复杂的多。 固定列分为左边固定和右边固定,这须要用户去设置 也就是说

columns1:[
            {.......,fixed:'left'},
            {........,fixed:'right'},
        ],
复制代码

全部fixed:'left'的都放在左边,fixed:'right'放在右边。 如今总体就能够分为三部分了。左列固定,中间滚动区域,右列固定。

三个部分的的头部数组收集

收集三个部分的数组,而且在格子的table里面遍历他们。

'收集函数'(){
           let [left,right,main] = [[],[],[]]
                this.columns.forEach(item=>{
                  [item.fixed].push(item)
                })
                    this.fixedLeft = left.concat(main,right)
                    this.fixedRight = right.concat(main,left)
                    this.scrollArea = left.concat(main,right)
     }
                
复制代码

concat对作一个拼接,这样子在外层div包裹的时候能够直接用maxWidthoverflow:hidden截取显示的部分。

//左边固定
                   <table class='左边'>
                        <colgroup>
                            <col style="width: 60px">
                            <col v-for="(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
                        </colgroup>
                        <thead>
                        <tr>
                         //.....
                            <th v-for="column in fixedLeft" :key="column.field">
                                    {{column.text}}
                            </th>
                        </tr>
                        </thead>
                    </table>
                    //中间滚动
                      <table class='滚动区域'>
                        <colgroup>
                            <col style="width: 60px">
                            <col v-for="(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
                        </colgroup>
                        <thead>
                        <tr>
                         //.....
                            <th v-for="column in fixedLeft" :key="column.field">
                                    {{column.text}}
                            </th>
                        </tr>
                        </thead>
                    </table>
                      //右边固定
                      <table class='右边'>
                        <colgroup>
                            <col style="width: 60px">
                            <col v-for="(column,index) in fixedLeft" :key="index" :style="{width:`${column.width}px`}">
                        </colgroup>
                        <thead>
                        <tr>
                         //.....
                            <th v-for="column in fixedLeft" :key="column.field">
                                    {{column.text}}
                            </th>
                        </tr>
                        </thead>
                    </table>
复制代码

横向滚动

须要注意的是table的宽度会受外面div包裹层的宽度影响。因此须要在开始就固定好table的宽度。而后给父级一个maxWidth

setMainWidth(){
            let [width,$refs] = [getComputedStyle(this.$refs.table).width,this.$refs]
            $refs.table.style.width = width
            $refs.wrapper.style.width = this.maxWidth +'px'
              //......
    },
复制代码

结合固定头部

由于考虑到固定列的同时还能固定头部,左右的结构和以前的大致相同

<div class="main">
     <!-- 中间的头部部分-->
     <table></table>
      <!-- 中间的body部分-->
     <table></table>
</div>
<div class="left">
    <!-- 左边的头部部分-->
     <table></table>
      <!-- 左边的body部分-->
     <table></table>
</div>
<div class="right">
    <!-- 右边的头部部分-->
     <table></table>
      <!-- 右边的body部分-->
     <table></table>
</div>
复制代码

这里固定左列固定右列除了css样式外,还有些不一样的地方。

  • checkbox,一旦存在左列固定,CheckBox必定算在左边。
  • colgroup 左边就须要考虑到CheckBox的占位和宽度。
  • 固定右列须要考虑滚动条的宽度。(由于这里滚动条尚未自制,可能会有样式误差)

hover同步变色

hover其中一部分其余的一块儿改变背景颜色

hoverChangeBg(index,e){
              let typeName = {
                  mouseenter:'#FCF9F9',
                  mouseleave:''
              }
              this.$refs.trMain[index].style.backgroundColor = typeName[e.type]
              if(this.fixedLeft.length>0){
                  this.$refs.trLeft[index].style.backgroundColor = typeName[e.type]
              }
              if(this.fixedRight.length>0){
                  this.$refs.trRight[index].style.backgroundColor = typeName[e.type]
              }
          },
复制代码

图画表示

滚动条厚度的计算,消失与覆盖

  • 首先让左边固定的部分右边的滚动条消失
'不须要展现滚动条的部分'{
                  &::-webkit-scrollbar{
                  display: none;
              }
              -ms-overflow-style: none;
              scrollbar-width: none;
              -ms-overflow-style: none;
              overflow: -moz-scrollbars-none;
            }
            //兼容chrome,firefox和IE(?)和其余大部分浏览器。
复制代码
  • 接着右边部分定位时设置
position:absolute;
right:0;
top:0;
复制代码

覆盖中间滚动区域的竖直滚动条

  • 获取滚动条的厚度,两边固定部分高度减去那个厚度(如图所示),若是没有则为0。
'我是获取滚动条厚度的函数'(){
                 const scrollBar = document.createElement('div')
                     let style = {
                         height:'50px',
                         overflow:'scroll',
                         position:'absolute',
                         top:'-9999px',
                         width:'50px'
                     }
                      Object.keys(style).forEach(item=>{
                         scrollBar.style[item]=style[item]
                      })
                      document.body.appendChild(scrollBar)
                      this.scrollBarWidth= scrollBar.offsetWidth - scrollBar.clientWidth
                      document.body.removeChild(scrollBar)
   }
复制代码

至于这个函数的兼容性问题,暂时没有考虑。

同步滚动

最麻烦的一部分,至今尚未彻底解决,事实上在elementUI上也略微有点瑕疵。先说下个人解决过程。

最开始的尝试(已放弃): 一开始使用mouserwheel监听,但兼容性存在问题。

判断浏览器是否为火狐

const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
复制代码

来监听mouserwheel或火狐特立独行的DOMMouseScroll。 固然,原生的监听须要在beforeDestroy钩子里删除一下。

const mousewheel = function(element, callback,name) {
    if (element && element.addEventListener) {
        element.addEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', function(event) {
            callback && callback.apply(this, [event,name]);
        });
    }
};

export default {
    bind(el, binding,name) {
        mousewheel(el, binding.value,name);
    }
};

复制代码

大致想法是用deltaY控制其余两部分的scrollTop,然而火狐没有这东西(至关的蛋疼),须要一些库的支持。总之大致的写法就是相似下面的

'须要同步滚动的部分'.scrollTop += e.deltaY
复制代码

只这么写这样子有个问题,就是滚滑轮的时候目标元素不必定在滚动,多是父级或者window,可是依然会触发mousewheel事件。这样子就会出现大量移位的状况。 以后试了n种方法,

'须要同步滚动的部分'.scrollTop += e.deltaY
'wheel的那部分'.scrollTop  += e.deltaY
复制代码

第二种方法: 使用原生的scroll事件,经过scrollTop来作同步

'须要同步滚动的部分'.scrollTop = scrollTop
复制代码

须要监听三个部分的scroll事件,可是一旦其中一个触发了scroll事件,就会修改其余两个的scrollTop,以后又会触发其余两部分的scroll事件。也就是说假设监听调用的都是同一个函数,那么滚一次就会调用三次这个函数。(在非Chrome浏览器上实际额外触发的次数更多,这也是滚动缓慢的缘由) 这种状况在除了chrome外的浏览器滚动十分缓慢。 目前尝试的两个方法:

  • 使用原生的addEventListenerremoveEventListener。控制scrollTop以前移除监听,以后再监听。
  • 添加hover监听,只有hover的区域能够触发scroll
scrollGradient(part){
              if(part!=='正在hover的区域')return
                let position = {
                    left:[`tableLeftWrapper`,`tableMainWrapper`,`tableRightWrapper`],
                    main:[`tableMainWrapper`,`tableLeftWrapper`,`tableRightWrapper`],
                    right:[`tableRightWrapper`,`tableMainWrapper`,`tableLeftWrapper`],
                }
                let scrollTop = this.$refs[position[part][0]].scrollTop
                //........
                }
复制代码
  • 事实上在pc上滚动滚动条的时候鼠标也是悬浮在改滚动条的元素区域内的,因此这方法看似是没有问题的。 然而在正在看其余程序的时候,hover网页是不触发的,这时候就会直接return了。

并且有时候mousewheel的区域并不必定会滚动,多是其余的元素(例如window)

目前的解决方法:每次计算scrollTop的时候作一个记录,每次触发scrollGradient的时候作一个判断,当前元素的scrollTop是否等于记录的scrollTop,是就return。这样子就能确保每次某个部分滚动并修改其余部分的scrollTop的时候,不会有额外的操做。

滚动错位

在同步滚动的过程当中,不免会由于修改元素的scrollTop从而再次触发scroll的监听函数或者是滚动a元素的同时,快速切换滚动b元素,触发回调再次修改a元素的scrollTop。 这可能会引起

  • 局部没有渲染或者没有回流等,致使的大量显示错位甚至空白。
  • 极高的开销,由卡顿掉帧带来的很差的体验。

目前尝试过的方法

  • pointer-events:none(毛用没有)
  • vue的passive(没有解决)
  • div层覆盖(反而有bug)
  • 原生的防抖和节流(效果不太满意,由于须要平缓的滚动效果),可是实测滚动限速颇有效。
  • 在Firefox下,滚动一次会触发屡次,有着良好的滚动效果,而且几乎没有出现错位bug。
  • (借鉴了一下element源码后)阻止两边的mousewheel,修改中间滚动部分的scrollTop,而后由中间滚动部分来同步两边的scrollTop。(bug依然会出现,可是出现次数少了不少)

重绘和回流

封装函数

最后就是在Firefox采用监听三个部分的scroll,用其中一个的scrollTop来同步其余部分scrollTop的老方法。

其余浏览器用监听两边固定部分的mouserwheel事件,禁止两边的wheel。控制中间滚动区域的scrollTop,而后再给两边的scrollTop赋值从而达到三部分同步。好处在于在几个部分滚动切换的过程当中下降了在滚动当前元素的同时再次去修改自身的scrollTop的次数。而这种作法的确下降了错位出现的频率。

//同步滚动
const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const data={
    currentScrollLeft:0,
    currentScrollTop:0,
}
const wheel =  function fixedWheel(e,target,scrollArea){
    //Chrome
    //....
    event.preventDefault()
    '中间滚动区域'.scrollTop += e.deltaY
    //....
}

const scroll = function (event,el,partArr) {
//Firefox
   let {scrollTop} = el
    if(data.currentScrollTop===scrollTop)return
    //....
       this.$refs.tableMain.classList.remove('transformClass')
            window.requestAnimationFrame(()=>{
            //'重绘以前调用这个回调'
              '若是存在的话'&&'其余部分的'.scrollTop = scrollTop//多是中间的滚动区域,也多是两边的固定区域
                   window.requestAnimationFrame(()=>{
                        this.$refs.'中间滚动区域'.classList.add('transformClass')
                    })
               })
}
const xScroll = function('一些参数') {
    if (el && el.addEventListener) {
         el.addEventListener(!isFirefox?'mousewheel':'scroll', function(event) {
             !isFirefox && wheel.apply(this, ['一些参数'])
             isFirefox && scroll.apply(this, ['一些参数'])
        })
    }
}

export default {
    bind(el, binding,name) {
        xScroll('一些参数');
    },
    data
};

复制代码

要使用的时候只需

<template>
<div v-xScroll>
    <table></table>
</div>
</template>
import xScroll from './同步滚动'
    export default {
        directives:{
            xScroll
        },
复制代码

最后的一个效果

目前的table组件就是练练手,有问题的地方但愿指出。最后,厚颜无耻的求个赞,若是你以为还能够的话,哈哈。

相关文章
相关标签/搜索