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

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滚动位置,将上一次的滚动位置与当前的进行对比,如果当前位置大于之前滚动位置,表明当前操作是向右滚动,所以进行后置数据插入;反之,向左滚动,进行前置数据插入。

js 复制代码
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保持当前滚动位置,继续触发滚动数据变化。
  • 如果加载到最后不满一页 整个屏幕禁止滚动。
js 复制代码
// 滚动
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

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

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

js 复制代码
// 向上/左添加数据
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,进行初始页面的渲染。

html 复制代码
      <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进行填补。

js 复制代码
// 滚动
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。

js 复制代码
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画了一个,哈哈😂。

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

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

js 复制代码
  // 画外框
  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-...

相关推荐
一只大侠的侠5 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
猫头虎8 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端