likes
comments
collection
share

虚拟表格能支持自适应行高啦~

作者站长头像
站长
· 阅读数 3

hello 大家好,🙎🏻‍♀️🙋🏻‍♀️🙆🏻‍♀️

我是一个热爱知识传递,正在学习写作的作者,ClyingDeng 凳凳!

之前我们有对基于element-plus组件库的表格进行二次封装dyingtable,实现了表格的属性化配置。但是只有这些还是远远不够的呀!

去年,有个宝子就给我提了这样一个需求:我想要一个能够支持大数据的虚拟表格,跟那个element虚拟表格差不多,但是我们又想要一个跟它有点不一样的,我们的每个单元格数据有的多有的少,想实现一个自适应行高的,太规整了数据挤不下啊!🧐🧐🧐

那必须要得,安排啊🥳🥳🥳~

哐哐哐,一顿操作,花了几个月搞出来了。带大家来看看👀

首先,对于技术选型,我选择了我熟悉的vue+vite组合,这样写起来也顺手一些。

初始化

先初始化一个vite项目,分析我们接到的需求,该怎么去实现?🔜🔜🔜

为了使我们这个对组件库不产生依赖,可以独立使用,我是直接基于tbale原生标签去实现了表格的基础功能和它的虚拟功能。

虚拟表格能支持自适应行高啦~

表格内部子组件

在设计表格的时候,考虑到头部需要置顶的功能,我将表格拆成了两个table,一个用来表示table-header,一个用来做table-body。在上下滚动的时候将头部固定,身体滚动;左右滚动时将表头与表体进行联动。按这样的思路应该是可以去实现的。

table-column

根据表头与表体,提取公用组件,表格的单元格——table-column

作为基石的单元格,我考虑了几个主要的情况:

  • 数据展示。接收data,格式可能是直接单元格渲染的数据,也可以是一个对象,如果是一个对象,可以指定对象传入的keyProp。后期还可以拓展一些插槽。
  • 接受一些index序号、高度等作为后期拓展使用🤪🤪🤪

table-header

表头目前仅支持一级表头,感兴趣的可以帮忙完善多级表头(将内部的tr.children进行循环)💐。

表头是基于表格的配置项二次封装的。通过传入表头的columns来对表头的宽度、变量、对齐方式等进行配置。

因为表头做的是一级表头,所以我只考虑了虚拟表格的左右滚动功能。

左右滚动功能

表头我是给它指定了一个高度,宽度给了表格宽度的默认值600px

initLR

先考虑占满一屏需要多少数据,就会出现列过多、过少的情况。

  • 列数据少。如果只有三列,没有指定列表width,就执行平分了;如果部分指定将剩余未指定的列平分;如果指定部分超过默认列宽,未指定部分按照默认宽度80px渲染。
  • 列数据多。指定宽度之和未超出默认表格宽度,剩余未指定的列平分剩余宽度;如果超出,未指定宽度的列使用默认宽度。

获取到屏幕宽度一屏数据后,考虑大数据列情况:🤔🤔🤔

表格有n列,使用分页初始化渲染三页数据--如果只有两页数据,可能会出现第二页数据列宽完全撑不满一屏宽度,为了解决白屏,我这边使用三页数据渲染,确保一屏有两屏幕以上的数据。

此外,需要考虑数据不足三页的情况,如果只有1~2页数据就直接平铺滚动。

这样成功初始化三页列数据。

scrollEvent

滚动事件监听,对数据进行操作。

表头左右滚动判断当前元素的scrollLeft滚动位置,将上一次的滚动位置与当前的进行对比,如果当前位置大于之前滚动位置,表明当前操作是向右滚动,所以进行后置数据插入;反之,向左滚动,进行前置数据插入。

const scrollEvent = (e: any) => {
  let scrollLeft = e.target.scrollLeft // 当前滚动的位置
  emits('scrollLeft', scrollLeft)
  //  0-pageSizeLR*pageNumLR
  // 开始/结束位置
  if (scrollLeft > oldscrollLeft.value) {
    // 向右滚动
    onLeftScroll(scrollLeft)
  }
  if (scrollLeft < oldscrollLeft.value) {
    // 向左滚动
    onRightScroll(scrollLeft)
  }
}

向右滚动—onLeftScroll

向左拉,加载右侧后面的列数据。👉👉👉

通过widthMap来记录每页的列宽度,在向右滚动时进行页宽度收集。

开始滚动,判断当前第一页列数据是否滚出可视区域:

  • 如果超出,就进行列数据的新增,在columnList后push加上一页数据,且将widthMap设置给列表的paddingLeft保持当前滚动位置,继续触发滚动数据变化。
  • 如果加载到最后不满一页 整个屏幕禁止滚动。
// 滚动
let scrollWidth = ref<any>(0)
const onLeftScroll = (scrollLeft: number) => {
  let midChild = scrollBody.value.getElementsByTagName('th')[pageSizeLR.value] //第一页数据的高度
  nextTick(() => {
    // 渲染出的真实节点的最后一个子节点滚动的位置 加上 本身高度 减去 滚动的偏移量 是否占满不了一屏
    if (midChild.offsetLeft < scrollLeft) {
      // 最后边界不满一页数据也需要加载
      if ((pageNumLR.value - 1) * pageSizeLR.value > props.columns.length) {
        return
      }
      addDataFnLR() // 加数据
      // console.log(midChild.offsetLeft, scrollLeft, widthMap.value, pageNumLR.value)
      let arr = cloneDeep(columnList.value)
      // 完全滚出页面的数据高度
      scrollWidth.value = widthMap.value[pageNumLR.value - 4]
      // 数据处理 超出的最前面一页的数据去除
      columnList.value = arr.slice(Number(pageSizeLR.value), columnList.value.length)
      // 去除数据后 使用padding占位
      scrollBody.value.style.paddingLeft = scrollWidth.value + 'px'
      nextTick(() => {
        let second = scrollBody.value.getElementsByTagName('th')[columnList.value.length - 1]
        widthMap.value[pageNumLR.value - 1] = second.offsetLeft + second.offsetWidth
        oldscrollLeft.value = scrollLeft
        // console.log('columnList', columnList.value, pageSizeLR.value)

        //加载到最后不满一页 整个屏幕禁止滚动
        if (columnList.value.length < pageSizeLR.value * 3) {
          scrollWidthContainer.value = widthMap.value[pageNumLR.value - 1]
          emits('maxScrollWidth', scrollWidthContainer.value)
          return
        }
        //滚动触发数据变化
        onLeftScroll(scrollLeft)
      })
    }
  })
}

向左滚动—onRightScroll

向左滚动与向右滚动类似,当页面向右拉动。👈🏻👈🏻👈🏻

当最后一页数据超出宽度,将数据去除并在前置方向加上上一页数据,即在总列数据进行向前一页分页截取。

// 向上/左添加数据
const unshiftDataFnLR = (allData = props.columns) => {
  let pageData = allData.slice(pageSizeLR.value * (pageNumLR.value - 5), pageSizeLR.value * (pageNumLR.value - 4))
  if (pageData.length) columnList.value = pageData.concat(columnList.value)
  pageNumLR.value--
}

table-body

表体依旧接收一些基础功能的参数,对于接收到的columns进行列渲染,再通过接收表格数据参数dataList,进行初始页面的渲染。

      <tbody ref="scrollBody" class="scroll-container">
        <tr v-for="(item, index) in dataList" :key="`tbody_${Math.random() * index}`" class="dy-vt-wrapper-tr">
          <td
            v-for="(column, i) in columnList"
            :key="`tcolumn_${column[i]}_${Math.random() * index}`"
            class="dy-table__cell"
            :class="[
              { 'dy-table__cell-border': border },
              `dy-table_cell-text-${alignDir.includes(column.align) ? column.align : 'center'}`
            ]"
            :style="{
              width: setColumnWidth(column).realWidth + 'px',
              // @ts-ignore
              height: heightItemMap[pageSize * (pageNum - 4) + index] + 'px'
            }"
          >
            <dy-table-column :data="item" :index="index" :column="column" :key-prop="column.prop"></dy-table-column>
          </td>
        </tr>
      </tbody>

依旧同表头组件类似,初始渲染三页数据,不同的是需要判断上下滚动、左右滚动两大方向。

向下滚动

表格向下滚动,不给单元格设置默认高度,单元格根据列宽自适应行高。

而我们操作的时候,通过js去获取元素每页渲染后的高度,滚动超出通过padding进行填补。

// 滚动
let scrollHeight = ref(0)
const onDownScroll = (scrollTop: number) => {
  [dataList.value.length - 1] //最后一个元素离顶部的距离
  let midChild = scrollBody.value.getElementsByTagName('tr')[pageSize.value] //第一页数据的高度
  oldScrollTop.value = scrollTop
  nextTick(() => {
    // 渲染出的真实节点的最后一个子节点滚动的位置 加上 本身高度 减去 滚动的偏移量 是否占满不了一屏
    if (midChild.offsetTop < scrollTop) {
      // 最后边界不满一页数据也需要加载
      if ((pageNum.value - 1) * pageSize.value > props.data.length) {
        return
      }
      addDataFn() // 加数据
      let arr = cloneDeep(dataList.value)
      // 完全滚出页面的数据高度
      scrollHeight.value = heightMap.value[pageNum.value - 4]
      // 数据处理 超出的最前面一页的数据去除
      dataList.value = arr.slice(Number(pageSize.value), dataList.value.length)
      nextTick(() => {
        // 去除数据后 使用padding占位
        scrollBody.value.style.paddingTop = scrollHeight.value + 'px'
        let second = scrollBody.value.getElementsByTagName('tr')[dataList.value.length - 1]
        heightMap.value[pageNum.value - 1] = second.offsetTop + second.offsetHeight
        oldScrollTop.value = scrollTop
        // 收集每页数据高度
        collectItemHeight(
          pageSize.value * (pageNum.value - 4) + dataList.value.length - pageSize.value * 3,
          pageSize.value * (pageNum.value - 4) + dataList.value.length
        )
        //加载到最后不满一页 整个屏幕禁止滚动
        if (dataList.value.length < pageSize.value * 3) {
          scrollHeightContainer.value = heightMap.value[pageNum.value - 1]
          return
        }
        //滚动触发数据变化
        debounce(() => onDownScroll(scrollHeight.value), 500)
      })
    }
  })
}

组件引用

当我们完成这个组件后,可以对外进行一个暴露。通过install方法进行export。

import DyVirtualTable from './virtual-table/index.vue'
import CanvasTable from './canvas-table/index.vue'
let components = [DyVirtualTable, CanvasTable]
const install = (Vue: any) => {
  components.forEach((_: any) => {
    Vue.component(_.name, _)
  })
}
if (typeof window !== 'undefined' && (window as any).Vue) {
  install((window as any).Vue) // 全局直接通过script 引用的方式会默认调用install
}
export default {
  install
}

基础用法

columnList表格配置项:

  • table的配置项中用prop属性来对应对象中的键名即可填入数据
  • label属性来定义表格的列名
  • 可以使用width属性来定义列宽
  • align 是每列的对齐方式,可以是left / center / right,设置列左对齐、居中对齐、右对齐。

虚拟表格能支持自适应行高啦~

也可以配置表格的条纹:

虚拟表格能支持自适应行高啦~

以上数据是可以支持大数据和正常表格数据的哦,不需要使用两个不同的表格组件哦~

canvas-table

在使用div来实现一个虚拟表格后,凳凳还额外用canva画了一个,哈哈😂。

宝子们,可以使用看看,不过支持的应用场景比较少🌹。

原理类似,将我们获取到的表格宽高进行等宽、等高划分,获取初始一屏幕数据,向下滚动的时候,进行数据更新。

  // 画外框
  drawBorder(ctx, canvasWidth, canvasHeight)
  // 表格头渲染
  renderTHeader(ctx, canvasWidth, canvasHeight, row.value, col.value, regularHeadHeight)
  // 画行 横线
  drawRows(ctx, canvasWidth, canvasHeight, row.value, col.value, 0, regularHeadHeight)
  // 竖线
  drawCols(ctx, canvasWidth, canvasHeight, row.value, col.value, 0, regularHeadHeight)
  // 表格数据渲染
  renderData(ctx, canvasWidth, canvasHeight, row.value, col.value, 0, regularHeadHeight)

源码地址

git地址:github.com/ClyingDeng/…

文档地址:clyingdeng.github.io/dy-virtual-…