声明:概念代码,不完善,思路参考,请勿直接拿去用
最终效果:el-table一直只渲染一小段数据
一、直接在页面组件写
<div class="eltable-container">
<el-table :data="virtualTableData" :style="virtualTableStyleObj" ref="mainTableRef" stripe border>
<el-table-column type="index" label="序号" width="80">
<!-- 在虚拟列表场景下,由于只渲染可视区域内的数据行,使用type="index"会导致序号从0开始重新计数。解决方案是自定义序号列,根据实际数据位置计算序号。 -->
<template #default="{ $index }">
<span>{{ startIdx + $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column v-for="item in purchaseMainTableColumns" :prop="item.path" :label="item.label"
:width="item.dynamicWidth" :fixed="item.fixed" :sortable="item.sortable">
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, reactive, onMounted,computed,nextTick } from 'vue'
import PurchaseOrderAPI from '@/api/order/purchase/index.js' //数据源接口
import {
purchaseMainTableColumns,
} from './config/tableConfig.js' //列配置
let virtualTableStyleObj = {
height: '800px',
width: '100%'
} //虚拟表格样式
const dataTotalCount = 1000 //数据源总数
let tableListData = reactive([]) //过渡数据源
let virtualTableData = ref([]) //数据源
let mainTableRef = ref(null) //表格实例
// 添加这些变量
const rowHeight = 50; // 实际行高
const bufferCount = 5; // 缓冲区行数
const visibleCount = ref(0); // 渲染行数
const startIdx = ref(0); // 起始索引
const placeholderHeight = computed(() => tableListData.length * rowHeight); // 占位高度(自定义滚动依赖于此高度)
const viewportHeight = 800;
onMounted(async() => {
let apiData = await PurchaseOrderAPI.getPurchasePageList({page:{
pageNumber: 1,
pageSize: 1000,
totalPage: -1,
totalRow: -1
}})
tableListData.push(...apiData.data.records)
// console.log(tableListData);
// console.log(mainTableRef.value?.$refs);
const scrollBarRef = mainTableRef.value?.$refs?.scrollBarRef;
const wrapRef = scrollBarRef.wrapRef; // 获取真实滚动容器
// console.log('wrapRef',wrapRef); //el-scrollbar__wrap el-scrollbar__wrap--hidden-default
// 获取表格内容区域高度(视口高度)
// const tableBodyWrapper = document.querySelector('.el-table__body-wrapper');
// 直接获取表格的 body wrapper
// 这里是 获取包裹table 的容器,目的是获取到一屏可用高度,计算实际渲染行数, .eltable-container 和 .el-table__body-wrapper 的height一样?
// const tableBodyWrapper = mainTableRef.value?.$el?.querySelector('.el-table__body-wrapper');
// if (!tableBodyWrapper) {
// console.error('无法找到表格滚动容器');
// return;
// }
// // console.log(tableBodyWrapper);
// // debugger
// const viewportHeight = tableBodyWrapper.clientHeight;
// console.log(viewportHeight); // 800
// 计算实际渲染行数
visibleCount.value = Math.ceil(viewportHeight / rowHeight) + bufferCount;// 25 // Math.ceil 表示向上取整 比如 800/42 = 19.047619 向上取整 20
// 创建占位元素(利用他滚动)
const placeholder = document.createElement('div');
placeholder.id = 'vheight';
placeholder.style.height = `${tableListData.length * rowHeight}px`;
wrapRef.append(placeholder);
// 初始化响应式数据
updateVirtualData();
// 监听滚动事件 (为什么要 给wrapRef?)
wrapRef.addEventListener('scroll', handleScroll);
})
let isScrolling = false;
let lastScrollTop = 0;
const handleScroll = (event) => {
// requestAnimationFrame(updateVirtualData)
//本次滚动方向 (也可以不用)
let scrollDirection = 'down';
const scrollTop = event.target.scrollTop; // Y方向滚动的数值
// console.log(scrollTop);
if(scrollTop - lastScrollTop > 0){
// console.log('向下滚动');
scrollDirection = 'down';
if(scrollTop >= (dataTotalCount * 50 - 15 * 50))return //(dataTotalCount * 50 - 15 * 50) //保险起见 放到向下滚动这里 209744这个数值代表 滚动到底部边界 是为了解决一下子滚动到最底部不停抽搐的bug
//这里确实解决了 抽搐问题,但是底部有一段空白 有时空白比较大 ,反正也不是大问题 暂时搁置
}
else if(scrollTop - lastScrollTop < 0){
// console.log('向上滚动');
scrollDirection = 'up';
}
// 如果滚动位置未变化,跳过处理
if (scrollTop === lastScrollTop) return;
lastScrollTop = scrollTop;
// 如果已经有动画帧请求,跳过
if (isScrolling) return;
isScrolling = true;
// 使用 requestAnimationFrame 确保在浏览器重绘前执行
requestAnimationFrame(() => {
// 计算新的起始索引
const newStartIdx = Math.floor(scrollTop / rowHeight);
// console.log('newStartIdx', newStartIdx);
// 只有当索引变化超过阈值时才更新
if (Math.abs(newStartIdx - startIdx.value) > 1) {
startIdx.value = newStartIdx;
updateVirtualData();
}
// 定位表格体
const tableBody = event.target.querySelector('.el-table__body');
if (tableBody) {
tableBody.style.transform = `translateY(${startIdx.value * rowHeight}px)`; //这个相当于手动 滚动表体到 指定位置
}
isScrolling = false;
});
};
// 从总数据源截取要渲染的数据
const updateVirtualData = () => {
virtualTableData.value = tableListData.slice(
startIdx.value,
startIdx.value + visibleCount.value
);
};
</script>
<style scoped lang='scss'>
</style>
二、组合式API + 组件封装
useVirtualTable.js
import { ref, computed } from 'vue'
export function useVirtualList(options) {
const startIdx = ref(0)
const visibleCount = computed(() => {
return Math.ceil(options.containerHeight / options.rowHeight) + 5
})
const virtualData = ref([])
const updateVirtualData = (newStartIdx = startIdx.value) => {
if (!options.sourceData.value) return
startIdx.value = newStartIdx
const endIdx = Math.min(
startIdx.value + visibleCount.value,
options.sourceData.value.length
)
virtualData.value = options.sourceData.value.slice(
startIdx.value,
endIdx
)
}
return {
virtualData,
startIdx,
visibleCount,
updateVirtualData
}
}
VirtualTable.vue
<div class="virtual-table-container">
<el-table ref="mainTableRef" :data="virtualData" :style="{ height: containerHeight + 'px' }">
<el-table-column type="index" label="序号" width="80">
<!-- 在虚拟列表场景下,由于只渲染可视区域内的数据行,使用type="index"会导致序号从0开始重新计数。解决方案是自定义序号列,根据实际数据位置计算序号。 -->
<template #default="{ $index }">
<span>{{ startIdx + $index + 1 }}</span>
</template>
</el-table-column>
<!-- 动态列渲染 -->
<el-table-column v-for="col in dynamicColumns" :key="col.path" :prop="col.path" :label="col.label"
:width="col.dynamicWidth" />
</el-table>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { useVirtualList } from '@/utils/useVirtualTable.js'
const props = defineProps({
sourceData: Array, // 原始数据源
dynamicColumns: Array, // 动态列配置
containerHeight: { // 容器高度
type: Number,
default: 800
},
rowHeight: { // 行高配置
type: Number,
default: 50
}
})
const mainTableRef = ref(null)
const scrollContainer = computed(() => {
return mainTableRef.value?.$el.querySelector('.el-table__body-wrapper')
})
// 创建虚拟列表实例
const { virtualData, startIdx, visibleCount, updateVirtualData } = useVirtualList({
sourceData: computed(() => props.sourceData),
containerHeight: props.containerHeight,
rowHeight: props.rowHeight,
containerRef: scrollContainer
})
console.log(virtualData, startIdx);
// 监听数据源变化
watch(props.sourceData, (newVal) => {
// debugger
if (newVal && newVal.length) {
updateVirtualData(0)
const scrollBarRef = mainTableRef.value?.$refs?.scrollBarRef;
const wrapRef = scrollBarRef.wrapRef; //
// const viewportHeight = 800;
// // 计算实际渲染行数
// visibleCount.value = Math.ceil(viewportHeight / rowHeight) + bufferCount;// 25 // Math.ceil 表示向上取整 比如 800/42 = 19.047619 向上取整 20
// 创建占位元素
const placeholder = document.createElement('div');
placeholder.id = 'vheight';
placeholder.style.height = `${1000 * rowHeight}px`;
wrapRef.append(placeholder);
// 监听滚动事件 (为什么要 给wrapRef?)
wrapRef.addEventListener('scroll', handleScroll);
// console.log(virtualData,props.dynamicColumns);
}
}, { immediate: true })
let isScrolling = false;
let lastScrollTop = 0;
const dataTotalCount = 1000
const rowHeight = 50; // 实际行高
const bufferCount = 5; // 缓冲区行数
// 处理滚动事件
const handleScroll = (event) => {
// debugger
// if (!scrollContainer.value) return
// const scrollTop = scrollContainer.value.scrollTop
// const newStartIdx = Math.floor(scrollTop / props.rowHeight)
// // 避免频繁更新
// if (Math.abs(newStartIdx - startIdx.value) > 5) {
// startIdx.value = newStartIdx
// updateVirtualData()
// }
// // 同步表格位置
// const tableBody = scrollContainer.value.querySelector('.el-table__body')
// if (tableBody) {
// tableBody.style.transform = `translateY(${startIdx.value * props.rowHeight}px)`
// }
//本次滚动方向 (也可以不用)
let scrollDirection = 'down';
const scrollTop = event.target.scrollTop; // Y滚动的数值
console.log(scrollTop);
if (scrollTop - lastScrollTop > 0) {
console.log('向下滚动');
scrollDirection = 'down';
if (scrollTop >= (dataTotalCount * 50 - 15 * 50)) return //(dataTotalCount * 50 - 15 * 50) //保险起见 放到向下滚动这里 209744这个数值代表 滚动到底部边界 是为了解决一下子滚动到最底部不停抽搐的bug
//这里确实解决了 抽搐问题,但是底部有一段空白 有时空白比较大 ,反正也不是大问题 暂时搁置
}
else if (scrollTop - lastScrollTop < 0) {
console.log('向上滚动');
scrollDirection = 'up';
}
// 如果滚动位置未变化,跳过处理
if (scrollTop === lastScrollTop) return;
lastScrollTop = scrollTop;
// 如果已经有动画帧请求,跳过
if (isScrolling) return;
isScrolling = true;
// 使用 requestAnimationFrame 确保在浏览器重绘前执行
requestAnimationFrame(() => {
// 计算新的起始索引
const newStartIdx = Math.floor(scrollTop / rowHeight);
// console.log('newStartIdx', newStartIdx);
// 只有当索引变化超过阈值时才更新
if (Math.abs(newStartIdx - startIdx.value) > 1) {
startIdx.value = newStartIdx;
updateVirtualData();
}
// 定位表格体
const tableBody = event.target.querySelector('.el-table__body');
if (tableBody) {
tableBody.style.transform = `translateY(${startIdx.value * rowHeight}px)`; //这个相当于手动 滚动表体到 指定位置
}
isScrolling = false;
});
}
onMounted(async () => {
await nextTick();
})
// 清理事件监听
onBeforeUnmount(() => {
if (mainTableRef.value) {
const scrollBarRef = mainTableRef.value?.$refs?.scrollBarRef;
const wrapRef = scrollBarRef.wrapRef; // 获取真实滚动容器
wrapRef.removeEventListener('scroll', handleScroll)
}
})
</script>
testVirtualTable.vue
<template>
<VirtualTable
:source-data="purchaseOrderData"
:dynamic-columns="purchaseMainTableColumns"
:container-height="800"
:row-height="50"
/>
</template>
<script setup>
import { ref, reactive, onMounted,computed,nextTick } from 'vue'
import PurchaseOrderAPI from '@/api/order/purchase/index.js' //接口
import VirtualTable from '@/components/base-ui/virtual-table/VirtualTable.vue' // 虚拟表格封装
import {
purchaseMainTableColumns,
} from './config/tableConfig.js' //列配置
let purchaseOrderData = reactive([]) // 虚拟表格数据源
onMounted(async() => {
//请求列表数据
let apiData = await PurchaseOrderAPI.getPurchasePageList({page:{
pageNumber: 1,
pageSize: 1000,
totalPage: -1,
totalRow: -1
}})
// 数据源赋值
purchaseOrderData.push(...apiData.data.records)
})
</script>
<style scoped lang='scss'>
</style>