vue表格-横竖表头,固定行列的填坑

5,016 阅读9分钟

1、开始挖坑

最近产品经历跟我说需要一个表格来展示数据,我一想这个简单,不就一表格吗,三下五除二给弄了一个

<table>
    <thead>
        <tr>
            <th>表头1</th>
            <th>表头2</th>
            <th>表头3</th>
            <th>表头4</th>
            <th>表头5</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>内容1</td>
            <td>内容2</td>
            <td>内容3</td>
            <td>内容4</td>
            <td>内容5</td>
        </tr>
    </tbody>
</table>

结果,弄好后跟我说,我要表头固定,左侧第一行有一个全选单选框,第一列固定,而且还能根据后台配置来固定左侧固定几列,能左右上下滚动. 当时一听完,我表情是这样的:

身为打工仔,底层搬运工的我只好继续做,于是我又继续开始挖坑之旅

1.1、横向表格坑

1.1.1、第一版

其实横向表格的固定表头,左侧全选单选框,左侧固定列,宽度限制这些其实都不难,element UI组件内功能,但是郁闷的是,我们的数据完全不符合element UI的数据格式,这里可能有人说你不会自己封装下吗,那么请看下图(我只想说谁爱封装谁封装去,反正我不去,头疼):

接着又去看了下后台的配置,一共有几种格式的td类型: 单个,左斜角,右斜角,单个多行,想着我用一个div来控制,还能在里面根据计算算斜线,说干就干,于是有了下面的几个文件:

- theadType
    -single.vue   // 单个数据展示
    -leftBevel.vue 展示左斜角
    -rightBevel.vue 展示右斜角
    -multipleColumns.vue 展示一个td/th 里面两三层(看数据层级)
    -index.js 导出组件
-headNode // 展示theadType里面的组件根据type
-tableBody // 表格内容区域
-tableHead // 表格头部区域

思路:

1、表头和表格内容分开,利于表头固定 2、表格内容渲染在td的div内,至于斜线可以实时去计算 3、

实现:

基于以上思路,开始折腾,首先开始写headNode这个中间组件:

// 根据传过来的modeType 去动态展示component组件
<template>
  <component v-bind:is="modeType" :msg="msg" :renderType="renderType" :isScroll="isScroll" />
</template>

然后在 tableBody和tableHead 里面各自引入headNode,然后在加上一些常规的全选复选框操作,就构成了横向表格

至此,第一坑已经挖完, 现在打开来看看效果.结果内心是这样的(由于当时做的时候没截图,只能用下图表达我当时心情):

简单形容下效果:

  • 1、内容滚动的时候头部滚动不及时,并且出现不对齐的情况
  • 2、表头合并的单元格那部分表现极其差,大小不一致,宽度后台设置没用高度七上八下
  • 3、不滚动时在表格中间几列边框完全不对齐

当时弄了会儿,应该是single出了问题.在single中调用自身展示出以下这种效果的时候样式没调整好:

1
2 3

于是就想到了用flex来调整文字和div的对齐(由于本人对flex不熟悉,特意贴上flex的属性),

最终single.vue的代码:

<template v-if="renderType === 'head'">
  <span style=""  ref="spanName" :class="{'spanStyle': true}" v-if="msg.headBindingList.length !== 0">{{msg.headName}}</span>
  <template v-else>
    <p ref="titleName" :class="[msg.childs.length === 0 && msg.headBindingList.length === 0 ? 'pNoBottom' : 'pHasBottom']">{{msg.headName}}</p>
    <div style="display: flex;align-items: stretch;">
      <div :style="{width: item.width + 'px', flex: isScroll === true ? '1' : 'none' , }" :key="'singleP-' + index + item.headId" v-for="(item, index) in msg.childs">
        <single :msg="item" :renderType="renderType"></single>
      </div>
    </div>
  </template>
</template>


到这总算看的过去,而且产品那里对付过去了!!!!(反正不足之处后期优化吗,哈哈)

1.2、竖向表格坑

横向表头表格对付过去之后,产品大佬又给我升级了:

  • 我要表头在左侧,弄竖向表格
  • 我要左侧表格永久固定,头部第一行多选单选永久固定
  • 我要根据后台传值,固定头部几行

1.2.1、 第一版

思路

  • 1、按照横向表格的思路 表头和表内容分开
  • 2、弄一个左侧的表头固定
  • 3、弄一个头部的行固定

实现

按照横向表头的思路,把表头tableTreeList给传递给表头组件,把bodyList传递给内容组件,结果发现表头和内容的高度不能一直,但是搞了很多.各种动态计算传值,不仅麻烦还没什么多大效果,所以就放弃了第一版

1.2.2、第二版

既然分开表格和表格不行,那我就心想着试试看合在一起,把左边的表格也写在tbody里面用tr - td 去实现

思路

目的是把过来的headTreeList中的数据重组,打散然后弄成一个二维数组好循环展示内容

  • 1、重组headTreeList中的表头数据
  • 2、把bodyList中的数据放到重组的表头数据数组中
  • 3、固定表头
  • 3、固定行
内容表格的表头和内容重组
  • 1、 首先获取当前表头中最深的层级,也就是colspan(后面问了才知道,后台有返回层级这个值)
getMaxHeight () {
      let maxHeightArr = []
      let getHeight = (node) => {
        if (node.childs.length === 0) {
          maxHeightArr.push(node.headLine)
        } else {
          node.childs.forEach(child => {
            getHeight(child)
          })
        }
      }
      this.headLayout.headTreeList.forEach(ht => {
        getHeight(ht)
      })
      this.maxHeight = Math.max(...maxHeightArr)
    },
  • 2、设置每个单元格的colspan 和rowspan
/* 设置每个单元格宽度和高度 */
    setWidthHeight (node) {
      if (Array.isArray(node)) {
        node.forEach(n => {
          return this.setWidthHeight(n)
        })
        return
      }
      // 设置元素跨列数
      node.height = node.childs.length > 0 ? (node.childs[0].headLine - node.headLine) : (this.maxHeight - node.headLine + 1)
      // 设置元素跨行数
      node.Nwidth = this.getNodeWidth(node)
      node.childs.forEach(child => {
        this.setWidthHeight(child)
      })
    },
    /* 获取节点跨行数值 */
    getNodeWidth: function (node) {
      var getWidth = function (node) {
        let Nwidth = 0
        if (+node.childs.length === 0) {
          return 1
        }
        node.childs.forEach(ch => {
          Nwidth += parseInt(getWidth(ch))
        })
        return Nwidth
      }
      return getWidth(node)
    },
  • 3、 根据表头来生成不带表格内容的表头数据(二维数组)
* 根据宽度和高度生成对应表格数据 */
    setTable (node) {
      let allWidth = node.reduce((pre, now) => {
        return pre + now.Nwidth
      }, 0)
      let array = new Array(this.maxHeight)
      for (let i = 0; i < array.length; i++) {
        array[i] = new Array(allWidth)
      }
      //
      let height = 0
      var setSigTable = function (snode, height, Nwidth) {
        array[height][Nwidth] = snode
        snode.childs.forEach(ch => {
          setSigTable(ch, height + snode.height, Nwidth)
          Nwidth = Nwidth + ch.Nwidth
        })
      }
      if (Array.isArray(node)) {
        var Nwidth = 0
        node.forEach(n => {
          setSigTable(n, height, Nwidth)
          Nwidth = Nwidth + n.Nwidth
        })
      }

      this.allArray = new Array(this.maxHeight)
      for (var j = 0; j < this.maxHeight; j++) {
        this.allArray[j] = this.allArray[j] || []
        this.allArray[j] = this.allArray[j].concat(array[j])
      }
    }
  • 4、接下来初始化表格数据
InitialVerticalTableData (headTreeList) {
  this.getMaxHeight()

  headTreeList.forEach((o, i) => {
    this.setWidthHeight(o)
  })
  this.setTable(headTreeList)
  let tableData = []
  this.allArray.forEach(td => {
    tableData.push(td)
  })
  // // 先获取最大的高度
  let height = tableData.length
  let width = tableData.length > 0 && tableData[0].length
  let newTableData = new Array(width)
  for (var i = 0; i < newTableData.length; i++) {
    newTableData[i] = new Array(height)
  }
  for (let i = 0, len = tableData.length; i < len; i++) {
    for (let j = 0, lenJ = tableData[i].length; j < lenJ; j++) {
      if (tableData[i][j] !== undefined) {
        newTableData[j][i] = tableData[i][j]
      }
    }
  }
  return newTableData
}
changeToVertial () {
  let headTreeList = this.headLayout.headTreeList
  let newTableData = this.InitialVerticalTableData(headTreeList)
  let arr = []
  newTableData.map((v, i) => {
    this.bodyList.map((val, idx) => {
      if (i === idx) {
        v = [...v, ...val]
        arr[i] = []
        arr[i].push(...v)
      }
    })
  })
  this.headList = arr.length === 0 ? newTableData : arr
  // 获取headList中的长度最大的数组, 设置td的宽度,取headList内第一个数组为准
  let copyList = $.extend(true, [], this.headList)
  let filterList = []
  copyList.forEach((el, idx) => {
    let arr = el.filter(o => {
      return o
    })
    filterList.push(arr)
  })
  filterList.sort((a, b) => b.length - a.length) // 按照子数组的长度排序
  let list = filterList[0]
  let widthArr = list.map(v => {
    let num = 0
    if (v && v.width) {
      num = v.width
    }
    return num
  })
  this.widthArr = widthArr
  this.$nextTick(() => {
    // 设置table的width是auto 还是100%
    let fatherDivWidth = this.$refs.tableVertical.clientWidth
    let tableWidth = widthArr.reduce((pre, cur) => {
      return pre + cur
    })
    // 判断table的display
    if (tableWidth < fatherDivWidth) {
      this.tableWidthStyle = true
    } else {
      this.tableWidthStyle = false
    }
    // 判断固定表头是否显示垫底的div
    bus.$emit('isVerticalShowBottom', this.tableWidthStyle)
    // 画对角线
    setTimeout(() => {
      this.shapeRange1()
      this.shapeRange2()
      this.countLeftHeadTdHeight()
    }, 0)
  })
},

至此,我们把表头数据和内容数据都塞入一个二维数据里面,而且数组里面的每个字数组里面又包含着表头数据和内容数据.正好是一个tr 里面的内容,也分别设置了colspan和rowspan

固定表头

在表格已经渲染好的情况下,因为要左右滚动表格,所以表头要固定,这就要复制一份表头再固定到原先表格的位置,当表格头部

  • 获取表头宽度
// 获取表头宽度传递给固定表头 - 解决没数据情况下固定表头出错的情况
      let fixedHeadWidth = 0
      trArr.sort((a, b) => b.getElementsByTagName('td').length - a.getElementsByTagName('td').length)
      Array.from(trArr[0].getElementsByTagName('td')).forEach((d, i) => {
        if (i < this.maxHeight) {
          fixedHeadWidth += d.clientWidth + 1 // 1 为border的宽度
        }
      })
      bus.$emit('getLeftHeadTableWidth', fixedHeadWidth)
  • 获取表头宽度
var height = document.documentElement.clientHeight -
    ($('.am-condition')[0].clientHeight + $('.am-tb-fixed-hd').height() + 10) - 45
  // 给内容固定div传高度
  this.bodyFixedHeight = height

重合部分表头

接下来弄表头和固定行的重合部分,我们也弄一个表格对出来,因为重合部分有几行未知,由后台控制,所以我们就要把内容表格的td拿来计算高度重合表头的高度

  • 1、 获取每个td的高度
// 计算左侧头部的td的高度(内容表格部分代码)
    countLeftHeadTdHeight () {
      // 获取左侧头部的高
      let leftHeadHeight = []
      let tableList = document.getElementById('tableVerticalCon').getElementsByTagName('table')
      let trArr = Array.from(tableList[0].children)
      // 把高度集合传递给表头固定组件
      let setTdHeight = (nodeArr) => {
        // 删除第一个标签colgroup, 不计算高度
        nodeArr.splice(0, 1)
        nodeArr.forEach((el, idx) => {
          let td = el.getElementsByTagName('td')
          if (td[0]) {
            if (td.length === 1) {
              leftHeadHeight.push(td[0].clientHeight + 1)
            } else {
              let a = td.length - 1
              leftHeadHeight.push(td[a].clientHeight + 1)
            }
          }
        })
      }
      setTdHeight(trArr)
      bus.$emit('getLeftHeadTdHeight', leftHeadHeight)
      // 获取表头宽度传递给固定表头 - 解决没数据情况下固定表头出错的情况
      let fixedHeadWidth = 0
      trArr.sort((a, b) => b.getElementsByTagName('td').length - a.getElementsByTagName('td').length)
      Array.from(trArr[0].getElementsByTagName('td')).forEach((d, i) => {
        if (i < this.maxHeight) {
          fixedHeadWidth += d.clientWidth + 1 // 1 为border的宽度
        }
      })
      bus.$emit('getLeftHeadTableWidth', fixedHeadWidth)
    },
    // 获取实际重合部分的高度(重合部分表格代码)
    let fixedRowHeightArr = $.extend(true, [], fixedHeadWidth)
    let coincideHeightArr = fixedRowHeightArr.splice(0, this.realFixedRow)
    let coincideHeight = coincideHeightArr.reduce((pre, next) => {
      return pre + next
    })
    this.coincideHeight = coincideHeight + 60 // 60为首行全选单选默认高度
  • 2、渲染的时候只渲染重合部分
<template  v-for="(item,indexs) in headList">
  <tr v-if="indexs < realFixedRow" :key="indexs">
    <template v-for="(tds,index) in item">
      <td :key="index"
          v-if="tds&&tds.headLine&&index < maxHeight"
          :rowspan="filterForType(tds, 'Nwidth')"
          :colspan="filterForType(tds, 'height')"
          :class="{isHeader: index < maxHeight}"
      >
        <...省略部分内容代码...>
  </tr>

固定行

这个因为固定行简单,所以就不在多说,大概思路就是把重组的内容表格数组拿过来,循环的时候把index 小于后台设置的固定行显示就ok了

至此竖列表格固定行固定头部渲染结束

难点

感觉竖列表格渲染难点在以下几点:

  • 1、要重组数据把表头和内容都挨个放到一个数组数组里面,组成一个二维数组
  • 2、渲染固定列和行的时候要那内容表格的第一行或者第一列的高度和宽度来按个计算和设置
  • 3、滚动的时候要估计到上下左右滚动,要去挨个监听
  • 4、重合部分要单独拿出,并单独计算高度和宽度
  • 5、后面考虑到colgroup来控制宽度,稍微方便很多

大概就以上几点

总结

表格显示其实没什么难得,关键在于各种计算,比如固定列的和行因为跟数据没什么关系,所以要从内容表格那里实时获取宽和高,然后再来展示,至于固定行和内容表格的宽度要根据后台设置,可以用table-layout:fixed 来实现,但是在超过一屏和不超过一屏的时候表格的宽度要计算下切换下width是100%,还是auto.至于滚动和全选什么的就不说了.功能都简单,接下来就是根据竖向表格的方法来重新优化横向表单的写法.最终的结构如下: