文章目录
- 前言
- 一、为什么选中虚拟滚动
- [二、在ant design vue table中手撕虚拟滚动](#二、在ant design vue table中手撕虚拟滚动)
- 三.关于虚拟滚动需要全选的逻辑
-
- 1:问题说明
- 2:思路(主要修改点)
-
- [(1)添加全选处理函数 onSelectAll](#(1)添加全选处理函数 onSelectAll)
- [(2)添加复选框属性获取函数 getCheckboxProps](#(2)添加复选框属性获取函数 getCheckboxProps)
- (3)保持原有数据完整
- 3:关键代码(使用页面list.vue)
- [4: 全选后,但实际上还是只有12条效果](#4: 全选后,但实际上还是只有12条效果)
- [5: 分页100条,取消选中1条,数据变回11条](#5: 分页100条,取消选中1条,数据变回11条)
- 6:全选后,点击一行取消没有效果
- [7. 依旧有问题](#7. 依旧有问题)
- 四.关于虚拟滚动加上全选的最终方法(自定义复选框)
-
- [1. list.vue](#1. list.vue)
-
- [(1) html逻辑](#(1) html逻辑)
- (2)ts逻辑
- 2.useVirtualList.ts(参考总结第1点)
- 3.丝滑效果
- 总结(代码展示)
前言
前言:ant design vue 本身根本不支持虚拟滚动,之前的倾向于使用vxe-table来解决这种dom过多的应用场景,但是由于这个项目对于表格进行了二次封装,绑定的内容太多了,ts的逻辑部分已经有六百行了,要换vxe-table的话好像更复杂了,所以只能硬着头皮试试手撕一下虚拟滚动了。
项目对应的版本如下
bash
"vue": "^3.3.9",
"ant-design-vue": "^4.2.5",
项目框架: vite + vue + ts
一、为什么选中虚拟滚动
1.原因
dom太多,一次性渲染导致页面卡顿
2.解决方案
(1)启用虚拟滚动(推荐)
(2)优化表格行的内容渲染,特别是包含多个子元素的字段
就是将过长的dom限制长度(比如名称),减少每行内部的DOM节点数量,降低渲染复杂度
(3)分页调整:直接减少单页数据量,是最简单有效的优化方式
之前需求有要200/500页的分页了,100条已经算最低了,所以不能再降低了。
3.虚拟滚动的思路
举个栗子:以100条分页的数据为例,可视区域显示8条,上下加个边界值,各两条,一共12条数据。每次滚动,只显示12个dom节点,有点像轮播图。
核心思路:通过监听表格容器的滚动事件,计算可视区域内的行索引范围,只渲染可视区域内的行数据,从而减少DOM节点数量。
关键点:
- 使用visibleData计算属性只返回可视区域内的数据
- 添加上下占位行来模拟完整的滚动高度
- 监听滚动事件实时更新可视区域范围
- 支持overscanCount预渲染参数,提升滚动流畅度
二、在ant design vue table中手撕虚拟滚动
话不多说,直接上代码,需要所有代码的直接看总结
1.useVirtualList.ts
路径:src/utils/useVirtualList.ts 将虚拟滚动封装成一个ts
由于内容太多总结那里有,就不重复写了
2.list.vue (你使用table表格的页面)
javascript
import { useVirtualList } from '@/utils/useVirtualList'
import { reactive, ref, onMounted, nextTick, computed, watch } from 'vue'
// 虚拟滚动配置
const tableHeight = ref(400) // 表格容器高度
const virtualList = ref<any>(null) // 使用虚拟滚动
const virtualListConfig = {
start: 0,
end: 0,
tableHeight: tableHeight,
itemHeight: 36, // 与TableList的row-height保持一致
className: 'topic-table', // 唯一类名,用于虚拟滚动定位
}
const tableConfig: any = reactive({
dataList: [],
loading: false, // 页面loading
columns: [], // 表头分组
rowSelection: {
selectedRowKeys: [],
onChange: onSelectChange,
onSelectAll: onSelectAll, // 添加全选处理
getCheckboxProps: getCheckboxProps, //添加复选框属性获取方法
},
})
// 计算可见区域数据
const visibleData = computed(() => {
if (!virtualList.value) return tableConfig.dataList
return tableConfig.dataList.slice(virtualList.value.start, virtualList.value.end)
})
// 监听数据变化,重新初始化虚拟滚动
watch(
() => tableConfig.dataList.length,
(newLength) => {
nextTick(() => {
// 重新初始化虚拟滚动
virtualList.value = useVirtualList(newLength, virtualListConfig)
})
},
{ immediate: true }
)
// 确保表格容器高度正确
onMounted(() => {
// 动态计算表格容器高度
const updateTableHeight = () => {
const windowHeight = document.body.getBoundingClientRect().height
// 392 = 导航栏 + 两行搜索框 + 分页 + 内间距(根据实际情况调整)
tableHeight.value = Math.max(windowHeight - 392, 400)
}
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
})
三.关于虚拟滚动需要全选的逻辑
1:问题说明
在虚拟滚动的思路中我们有说过,本质上是只显示可视区域的dom,所以全选的时候,直接点击全选,我们是只能看到选中了12条数据的
2:思路(主要修改点)
(1)添加全选处理函数 onSelectAll
当全选时,获取所有数据的ID(tableConfig.dataList.map(item => item.id))
当取消全选时,清空所有选中的ID
这样可以确保全选/取消全选操作作用于所有数据,而不仅仅是可视区域
(2)添加复选框属性获取函数 getCheckboxProps
根据 selectedRowKeys 数组判断每行是否应该被选中
这样即使行不在可视区域,也能正确显示选中状态
(3)保持原有数据完整
虚拟滚动只影响渲染的数据(visibleData)
但 tableConfig.dataList 仍然包含所有数据
全选操作基于 tableConfig.dataList,确保能获取到所有100条数据的ID
3:关键代码(使用页面list.vue)
javascript
// 表格选中后的操作
const tableConfig: any = reactive({
dataList: [],
loading: false, // 页面loading
columns: [], // 表头分组
rowSelection: {
selectedRowKeys: [],
onChange: onSelectChange,
onSelectAll: onSelectAll, // 添加全选处理
getCheckboxProps: getCheckboxProps, //添加复选框属性获取方法
},
})
function onSelectChange(selectedRowKeys: string[]) {
tableConfig.rowSelection.selectedRowKeys = selectedRowKeys
}
// 全选操作处理
function onSelectAll(selected: boolean, selectedRows: any[], changeRows: any[]) {
if (selected) {
// 全选时,获取所有数据的ID,而不仅仅是可视区域的
const allKeys = tableConfig.dataList.map((item: any) => item.id)
tableConfig.rowSelection.selectedRowKeys = allKeys
} else {
// 取消全选时,清空所有选中的ID
tableConfig.rowSelection.selectedRowKeys = []
}
}
// 获取每行的复选框属性
function getCheckboxProps(record: any) {
// 即使行不在可视区域,也要根据selectedRowKeys来判断是否选中
return {
props: {
checked: tableConfig.rowSelection.selectedRowKeys.includes(record.id),
disabled: false,
}
}
}
4: 全选后,但实际上还是只有12条效果
(1)问题
就是全选后,进行批量导出/删除等功能,请求等id还是只有12个
(2)原因
当点击全选按钮时,虽然onSelectAll方法会将所有数据的ID赋值给selectedRowKeys,但紧接着Ant Design Vue的Table组件会触发onChange事件,并且只传递可视区域中被选中的行的keys,这就覆盖了我们在onSelectAll方法中设置的完整选择列表。
(3)思路
- 添加一个isSelectAllOperation标志位来跟踪是否正在进行全选操作
- 在onSelectAll方法中设置这个标志位为true
- 在onSelectChange方法中检查这个标志位,如果存在,则不更新selectedRowKeys,并清除标志位
这样,当用户点击全选按钮时,onSelectAll方法会设置selectedRowKeys为所有数据的ID,然后onChange方法会被调用,但由于isSelectAllOperation标志存在,它不会覆盖我们设置的完整选择列表。
(4)解决代码
javascript
const isSelectAllOperation = ref(false) // 在tableConfig定义之前添加标志位
// 表格选中后的操作
function onSelectChange(selectedRowKeys: string[]) {
// 虚拟滚动全选所 onSelectAll-->table change (只传递可视区域dom12条)-->onSelectChange覆盖全选id导致的
if (isSelectAllOperation.value) {
isSelectAllOperation.value = false
return
}
tableConfig.rowSelection.selectedRowKeys = selectedRowKeys
}
// 全选操作处理
function onSelectAll(selected: boolean) {
if (selected) {
isSelectAllOperation.value = true
// 全选时,获取所有数据的ID,而不仅仅是可视区域的
const allKeys = tableConfig.dataList.map((item: any) => item.id)
tableConfig.rowSelection.selectedRowKeys = allKeys
} else {
isSelectAllOperation.value = true
// 取消全选时,清空所有选中的ID
tableConfig.rowSelection.selectedRowKeys = []
}
}
5: 分页100条,取消选中1条,数据变回11条

(1)原因
- 全选时我们正确设置了selectedRowKeys为100条数据的ID
- 但当取消单个选择时,Ant Design Vue Table的onChange事件只传递当前可视区域12条数据的选择状态
- 由于isSelectAllOperation标志位已重置为false,onSelectChange方法直接用这12条数据的选择状态覆盖了完整的selectedRowKeys数组
- 因此其他88条不在可视区域的选中数据ID被丢失,只剩下可视区域中仍被选中的11条
(2)思路
- 移除了标志位:不再需要isSelectAllOperation标志位来区分操作类型
- 使用info参数:利用onChange事件传递的info参数获取操作类型和当前行信息
- 增量更新策略:
全选/取消全选:直接设置完整的选择列表
单选:在完整列表中添加或移除单个ID
多选:先移除可视区域所有ID,再添加当前选中的ID - 保持数据完整性:始终基于完整的selectedRowKeys数组进行修改,确保虚拟滚动下所有数据的选择状态都能被正确管理
(3)代码
javascript
// 表格选中后的操作 - 增量更新选择状态
function onSelectChange(selectedRowKeys: string[], info: any) {
// 获取当前操作的行记录
const { currentRowKeys, type } = info
// 获取当前的完整选择列表
let newSelectedRowKeys = [...tableConfig.rowSelection.selectedRowKeys]
if (type === 'all') {
// 全选/取消全选操作
if (selectedRowKeys.length === 0) {
// 取消全选 - 清空所有
newSelectedRowKeys = []
} else {
// 全选 - 获取所有数据的ID
newSelectedRowKeys = tableConfig.dataList.map((item: any) => item.id)
}
} else if (type === 'single') {
// 单选操作
const rowKey = currentRowKeys[0]
const index = newSelectedRowKeys.indexOf(rowKey)
if (index > -1) {
// 取消选择
newSelectedRowKeys.splice(index, 1)
} else {
// 选中
newSelectedRowKeys.push(rowKey)
}
} else if (type === 'multiple') {
// 多选操作 - 直接使用可视区域的选择结果更新完整列表
// 先移除所有在可视区域的数据ID
const visibleRowKeys = info.selectedRows.map((row: any) => row.id)
newSelectedRowKeys = newSelectedRowKeys.filter(id => !visibleRowKeys.includes(id))
// 再添加当前可视区域中选中的数据ID
newSelectedRowKeys.push(...selectedRowKeys)
}
// 更新选择状态
tableConfig.rowSelection.selectedRowKeys = newSelectedRowKeys
}
6:全选后,点击一行取消没有效果

(1)原因
当可见区域所有行都被选中时被当作全选。修复:移除 onSelectChange 中的全选判断,全选仅由 onSelectAll 处理。
(2)思路
- 移除 onSelectChange 中的全选判断逻辑
- onSelectChange 只处理单行选择,不判断全选
- 全选操作完全由 onSelectAll 处理(用户点击表头全选复选框时)
(3)代码
javascript
const visibleData = computed(() => {
if (!virtualList.value) return tableConfig.dataList
return tableConfig.dataList.slice(virtualList.value.start, virtualList.value.end)
/ 方法1:如果传入了 visibleDataRef,直接使用
if (visibleDataRef?.value) {
return visibleDataRef.value.map((item: any) => item.id)
}
return []
}
// 表格选中后的操作 - 处理虚拟滚动场景下的选择
// 注意:onSelectChange 只处理单行选择,全选操作由 onSelectAll 处理
function onSelectChange(selectedRowKeys: string[]) {
// 获取当前的完整选择列表(使用Set提高查找效率)
const currentSelected = new Set(tableConfig.rowSelection.selectedRowKeys)
// 获取当前页所有数据的ID(完整数据源,100行)
const allPageRowKeys = tableConfig.dataList.map((item: any) => item.id)
const allPageRowKeysSet = new Set(allPageRowKeys)
// 获取当前可见区域的行ID(12行)
const visibleRowKeys = getVisibleRowKeys()
// selectedRowKeys 参数只包含当前可见区域被选中的行ID
const newSelectedSet = new Set(selectedRowKeys)
// 单行选择操作:只更新可见区域中变化行的状态
// 关键:不要在这里判断全选,全选应该只由 onSelectAll 处理
if (visibleRowKeys.length > 0) {
// 有可见区域信息:精确更新可见区域中每一行的状态
visibleRowKeys.forEach((rowId: any) => {
const isNowSelected = newSelectedSet.has(rowId)
const wasSelected = currentSelected.has(rowId)
if (isNowSelected && !wasSelected) {
// 新选中的行:添加到选择列表
currentSelected.add(rowId)
} else if (!isNowSelected && wasSelected) {
// 取消选中的行:从选择列表移除
currentSelected.delete(rowId)
}
})
} else {
// 通过比较 selectedRowKeys 和 currentSelected 来找出变化
// 添加新选中的行(在 selectedRowKeys 中但不在 currentSelected 中)
selectedRowKeys.forEach((rowId: any) => {
if (allPageRowKeysSet.has(rowId) && !currentSelected.has(rowId)) {
currentSelected.add(rowId)
}
})
}
tableConfig.rowSelection.selectedRowKeys = Array.from(currentSelected)
}
})
7. 依旧有问题
如下图:全选后,取消一条数据,继续滚动,全选按钮的状态变为了全选,这是由于虚拟滚动,计算当前可视区域的全选都为true,所以将全选复选框也设置为了全选导致的
然后就进入了死循环,改一个问题,出现另外一个,最终不得不换了一个方法

四.关于虚拟滚动加上全选的最终方法(自定义复选框)
我们所有的问题,都来自于当前数据是依赖于ant design vue table自动的复选款,使用代码逻辑重新覆盖的问题较多。但需要理解的是:为什么这么久了ant design vue没有加虚拟滚动,而是用了一个新框架,所以我的理解是,自定义一下复选款,然后自定义表头的全选复选框,通过一个数组,来存储虚拟滚动绑定的所有数据
1. list.vue
(1) html逻辑
html
<TableList
:tableConfig="{ ...tableConfig, dataList: visibleData }"
:pagination="pagination"
:handlePagination="handlePagination"
:row-height="36"
class="grow overflow-hidden"
>
<!-- 自定义表头:全选复选框 -->
<template v-slot:type="{ column }">
<template v-if="column.key === 'selection'">
<Checkbox
:checked="isAllSelected"
:indeterminate="hasSomeSelected && !isAllSelected"
@change="(e: any) => handleSelectAll(e.target.checked)"
/>
</template>
</template>
</TableList>
(2)ts逻辑
javascript
import { reactive, createVNode, ref, onMounted, watch, nextTick, computed } from 'vue'
import { useVirtualList } from '@/utils/useVirtualList'
import { Checkbox } from 'ant-design-vue'
// 选中的行ID数组(基于完整数据源,100行)
const selectedRowKeys = ref<any[]>([])
// 判断是否全选(所有100行都被选中)
const isAllSelected = computed(() => {
const allPageRowKeys = tableConfig.dataList.map((item: any) => item.id)
return allPageRowKeys.length > 0 &&
allPageRowKeys.every((id: any) => selectedRowKeys.value.includes(id))
})
// 判断是否有部分行被选中(用于indeterminate状态)
const hasSomeSelected = computed(() => {
const allPageRowKeys = tableConfig.dataList.map((item: any) => item.id)
return allPageRowKeys.some((id: any) => selectedRowKeys.value.includes(id))
})
// 单行选择处理
function handleRowSelect(record: any, checked: boolean) {
if (checked) {
// 选中:添加到选择列表
if (!selectedRowKeys.value.includes(record.id)) {
selectedRowKeys.value.push(record.id)
}
} else {
// 取消选中:从选择列表移除
const index = selectedRowKeys.value.indexOf(record.id)
if (index > -1) {
selectedRowKeys.value.splice(index, 1)
}
}
}
// 全选/取消全选处理
function handleSelectAll(checked: boolean) {
if (checked) {
// 全选:选择所有100行
selectedRowKeys.value = tableConfig.dataList.map((item: any) => item.id)
} else {
// 取消全选:清空所有选择
selectedRowKeys.value = []
}
}
const tableConfig: any = reactive({
// 列表数据
dataList: [],
loading: false, // 页面loading
// 表头分组
columns: [
{
title: '', // 表头通过插槽自定义
key: 'selection',
width: 60,
fixed: 'left',
customRender: ({ record }: any) => {
// 自定义单元格:单行复选框
return createVNode(Checkbox, {
checked: selectedRowKeys.value.includes(record.id),
onChange: (e: any) => handleRowSelect(record, e.target.checked),
})
},
}
]
})
const tableHeight = ref(400) // 表格容器高度
// 虚拟滚动配置
const virtualList = ref<any>(null) // 使用虚拟滚动
const virtualListConfig = {
start: 0,
end: 0,
tableHeight: tableHeight,
itemHeight: 36, // 与TableList的row-height保持一致
className: 'topic-table', // 唯一类名,用于虚拟滚动定位
}
// 计算可见区域数据
const visibleData = computed(() => {
if (!virtualList.value) return tableConfig.dataList
return tableConfig.dataList.slice(virtualList.value.start, virtualList.value.end)
})
// 移除所有旧的 rowSelection 相关代码,使用自定义列实现
const pagination = reactive({
total: 0,
current: 1,
pageSize: 20,
totalPage: 10,
pageSizeOptions: ['10', '20', '50', '100'],
showSizeChanger: true,
// hideOnSinglePage: true, // 没有数据或数据只有一页隐藏分页
onShowSizeChange: showSizeChange, // pagesize修改的触发方法
// 显示数据总量和当前数据顺序的回调方法
showTotal: () => `已选${tableConfig.rowSelection.selectedRowKeys.length}个`,
})
// 分页方法
function showSizeChange(_: number, size: number) {
pagination.current = 1
pagination.pageSize = size
}
// 将批量的逻辑修改(比如批量导出)
async function batchExportHandle() {
// const values: number[] = Object.values(
// tableConfig.rowSelection.selectedRowKeys
// ) // 获取对象的所有值
const values: number[] = selectedRowKeys.value // 直接使用数组
}
// 监听数据变化,重新初始化虚拟滚动
watch(
() => tableConfig.dataList.length,
(newLength) => {
nextTick(() => {
// 重新初始化虚拟滚动
virtualList.value = useVirtualList(newLength, virtualListConfig)
})
},
{ immediate: true }
)
onMounted(() => {
// 动态计算表格容器高度
const updateTableHeight = () => {
const windowHeight = document.body.getBoundingClientRect().height
// 392 = 导航栏 + 两行搜索框 + 分页 + 内间距(根据实际情况调整)
tableHeight.value = Math.max(windowHeight - 392, 400)
}
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
})
2.useVirtualList.ts(参考总结第1点)
路径:src/utils/useVirtualList.ts
3.丝滑效果

总结(代码展示)
1.useVirtualList.ts
路径:src/utils/useVirtualList.ts 将虚拟滚动封装成一个ts
javascript
import { reactive, nextTick, isRef, Ref } from "vue";
// 虚拟滚动配置接口
export interface VirtualListConfig {
start?: number;
end?: number;
itemHeight: number;
className?: string;
tableHeight: number | Ref<number>;
}
// 虚拟滚动返回值接口
export interface VirtualListInstance {
start: number;
end: number;
itemHeight: number;
tableHeight: number | Ref<number>;
className: string;
currentOffset: number;
wrapperDom: HTMLElement | null;
contentDom: HTMLElement | null;
count: number;
}
export const useVirtualList = (dataLen: number, config: VirtualListConfig): VirtualListInstance => {
const {
start = 0,
end,
itemHeight,
className = "",
tableHeight
} = config;
const virtualList = reactive<VirtualListInstance>({
start,
end: 0, // 初始值,后续会计算
itemHeight,
tableHeight,
className,
currentOffset: 0,
wrapperDom: null,
contentDom: null,
count: 0 // 初始值,后续会计算
});
const isDynamicHeight = isRef(tableHeight); // 是否传入的表格高度是动态的
const heightValue = isDynamicHeight ? tableHeight.value : tableHeight;
// 确保高度值有效
if (typeof heightValue !== 'number' || isNaN(heightValue)) {
throw new Error('tableHeight must be a valid number or Ref<number>');
}
// 确保itemHeight有效
if (typeof itemHeight !== 'number' || isNaN(itemHeight) || itemHeight <= 0) {
throw new Error('itemHeight must be a positive number');
}
// 计算可视区域显示的行数
const visibleCount = Math.ceil(heightValue / itemHeight);
// 确保至少显示一行
const displayCount = Math.max(1, visibleCount);
virtualList.end = end || Math.min(dataLen, start + displayCount);
virtualList.count = displayCount;
virtualList.className = className;
// 确保高度值有效
if (typeof heightValue !== 'number' || isNaN(heightValue)) {
throw new Error('tableHeight must be a valid number or Ref<number>');
}
// 确保itemHeight有效
if (typeof itemHeight !== 'number' || isNaN(itemHeight) || itemHeight <= 0) {
throw new Error('itemHeight must be a positive number');
}
virtualList.end = end || Math.max(1, Math.ceil(heightValue / itemHeight));
virtualList.count = virtualList.end - start;
virtualList.className = className;
const selectorClassName = `${className ? "." + className + " " : ""}`;
const init = () => {
// 获取container和content元素
virtualList.wrapperDom = document.querySelector(`${selectorClassName}.ant-table-body`);
virtualList.contentDom = document.querySelector(`${selectorClassName}.ant-table-body table`);
if (!virtualList.wrapperDom || !virtualList.contentDom) return;
// 样式调整
virtualList.wrapperDom.style.position = "relative";
virtualList.wrapperDom.style.top = virtualList.wrapperDom.style.left = "0";
virtualList.contentDom.style.position = "absolute";
virtualList.wrapperDom.addEventListener("scroll", handleScroll);
// 创建占位元素,撑起高度
let isExist = document.querySelector(`${selectorClassName}.palceholder-dom`);
if (isExist) virtualList.wrapperDom?.removeChild(isExist);
const placeHolderDom = document.createElement("div");
placeHolderDom.className = "palceholder-dom";
virtualList.wrapperDom.appendChild(placeHolderDom);
placeHolderDom.style.height = dataLen * virtualList.itemHeight + "px";
};
const handleScroll = () => {
if (!virtualList.wrapperDom || !virtualList.contentDom) return;
// 获取偏移量
const scrollTop = virtualList.wrapperDom.scrollTop;
// 计算最大可能的start值,确保end不会超过dataLen
const maxStart = Math.max(0, dataLen - virtualList.count);
// 重新计算start,确保不会超过最大值
const start = Math.min(maxStart, Math.max(0, Math.floor(scrollTop / virtualList.itemHeight)));
// 计算end值,确保不超过dataLen
const end = Math.min(
dataLen,
start + virtualList.count
);
// 仅在需要时更新状态,减少响应式更新次数
if (virtualList.start !== start || virtualList.end !== end) {
virtualList.start = start;
virtualList.end = end;
// contentDom元素进行偏移,保证视觉可见
virtualList.currentOffset = start * virtualList.itemHeight;
virtualList.contentDom.style.transform = `translate3d(0, ${virtualList.currentOffset}px, 0)`;
}
};
nextTick(() => {
init();
});
return virtualList;
};
2.list.vue (你使用table表格的页面)
javascript
import { useVirtualList } from '@/utils/useVirtualList'
import { reactive, ref, onMounted, nextTick, computed, watch } from 'vue'
// 虚拟滚动配置
const tableHeight = ref(400) // 表格容器高度
const virtualList = ref<any>(null) // 使用虚拟滚动
const virtualListConfig = {
start: 0,
end: 0,
tableHeight: tableHeight,
itemHeight: 36, // 与TableList的row-height保持一致
className: 'topic-table', // 唯一类名,用于虚拟滚动定位
}
// 计算可见区域数据
const visibleData = computed(() => {
if (!virtualList.value) return tableConfig.dataList
return tableConfig.dataList.slice(virtualList.value.start, virtualList.value.end)
})
const tableConfig: any = reactive({
dataList: [],
loading: false, // 页面loading
columns: [], // 表头分组
rowSelection: {
selectedRowKeys: [],
onChange: onSelectChange,
onSelectAll: onSelectAll, // 添加全选处理
getCheckboxProps: getCheckboxProps, //添加复选框属性获取方法
},
})
/ 方法1:如果传入了 visibleDataRef,直接使用
if (visibleDataRef?.value) {
return visibleDataRef.value.map((item: any) => item.id)
}
return []
}
// 表格选中后的操作 - 处理虚拟滚动场景下的选择
// 注意:onSelectChange 只处理单行选择,全选操作由 onSelectAll 处理
function onSelectChange(selectedRowKeys: string[]) {
// 获取当前的完整选择列表(使用Set提高查找效率)
const currentSelected = new Set(tableConfig.rowSelection.selectedRowKeys)
// 获取当前页所有数据的ID(完整数据源,100行)
const allPageRowKeys = tableConfig.dataList.map((item: any) => item.id)
const allPageRowKeysSet = new Set(allPageRowKeys)
// 获取当前可见区域的行ID(12行)
const visibleRowKeys = getVisibleRowKeys()
// selectedRowKeys 参数只包含当前可见区域被选中的行ID
const newSelectedSet = new Set(selectedRowKeys)
// 单行选择操作:只更新可见区域中变化行的状态
// 关键:不要在这里判断全选,全选应该只由 onSelectAll 处理
if (visibleRowKeys.length > 0) {
// 有可见区域信息:精确更新可见区域中每一行的状态
visibleRowKeys.forEach((rowId: any) => {
const isNowSelected = newSelectedSet.has(rowId)
const wasSelected = currentSelected.has(rowId)
if (isNowSelected && !wasSelected) {
// 新选中的行:添加到选择列表
currentSelected.add(rowId)
} else if (!isNowSelected && wasSelected) {
// 取消选中的行:从选择列表移除
currentSelected.delete(rowId)
}
})
} else {
// 通过比较 selectedRowKeys 和 currentSelected 来找出变化
// 添加新选中的行(在 selectedRowKeys 中但不在 currentSelected 中)
selectedRowKeys.forEach((rowId: any) => {
if (allPageRowKeysSet.has(rowId) && !currentSelected.has(rowId)) {
currentSelected.add(rowId)
}
})
}
tableConfig.rowSelection.selectedRowKeys = Array.from(currentSelected)
}
// 获取每行的复选框属性
function getCheckboxProps(record: any) {
// 即使行不在可视区域,也要根据selectedRowKeys来判断是否选中
return {
props: {
checked: tableConfig.rowSelection.selectedRowKeys.includes(record.id),
disabled: false,
}
}
}
const pagination = reactive({
total: 0,
current: 1,
pageSize: 20,
totalPage: 10,
pageSizeOptions: ['10', '20', '50', '100'],
showSizeChanger: true,
// hideOnSinglePage: true, // 没有数据或数据只有一页隐藏分页
onShowSizeChange: showSizeChange, // pagesize修改的触发方法
// 显示数据总量和当前数据顺序的回调方法
showTotal: () => `已选${tableConfig.rowSelection.selectedRowKeys.length}个`,
})
function showSizeChange(_: number, size: number) {
pagination.current = 1
pagination.pageSize = size
}
// 计算可见区域数据
const visibleData = computed(() => {
if (!virtualList.value) return tableConfig.dataList
return tableConfig.dataList.slice(virtualList.value.start, virtualList.value.end)
})
// 监听数据变化,重新初始化虚拟滚动
watch(
() => tableConfig.dataList.length,
(newLength) => {
nextTick(() => {
// 重新初始化虚拟滚动
virtualList.value = useVirtualList(newLength, virtualListConfig)
})
},
{ immediate: true }
)
// 确保表格容器高度正确
onMounted(() => {
// 动态计算表格容器高度
const updateTableHeight = () => {
const windowHeight = document.body.getBoundingClientRect().height
// 392 = 导航栏 + 两行搜索框 + 分页 + 内间距(根据实际情况调整)
tableHeight.value = Math.max(windowHeight - 392, 400)
}
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
})
3.html部分
html
<TableList :tableConfig="{ ...tableConfig, dataList: visibleData }"
:pagination="pagination"
:handlePagination="handlePagination"
:row-height="36"
class="grow overflow-hidden"
>
</TableList>
补充部分(TableList对于table的二次封装部分)
javascript
// table就是ant design vue的按需引入,为了节省打包的大小
import { Table } from 'ant-design-vue'
interface Props {
// 表格配置
tableConfig: {
dataList: object[] // 列表数据
columns: Array<any> // 列的标题
rowSelection?: object // 选择后进行操作的配置
spinning?: boolean // 是否加载中
scrollConfig?: {
y: number | undefined // 自定义滚动条的y
}
}
pagination?: object | false // 分页
handlePagination?: Function // 操作分页的点击事件
// 优化配置
rowHeight?: number // 行高,用于计算
}
<Table
:data-source="tableConfig.dataList"
:columns="tableConfig.columns"
:row-selection="tableConfig.rowSelection"
:pagination="pagination"
row-key="id"
size="small"
bordered
@change="props.handlePagination"
:scroll="scroll"
:row-class-name="tableConfig.rowClassName"
:components="{
body: {
wrapper: 'tbody',
row: 'tr',
cell: 'td'
}
}"
>
</Table>
其实就是:data-source="tableConfig.dataList"
:columns="tableConfig.columns"
:row-selection="tableConfig.rowSelection"结构成你的table就可以了,没有那么复杂