ant design vue table 使用虚拟滚动

文章目录


前言

前言: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)思路

  1. 移除了标志位:不再需要isSelectAllOperation标志位来区分操作类型
  2. 使用info参数:利用onChange事件传递的info参数获取操作类型和当前行信息
  3. 增量更新策略:
    全选/取消全选:直接设置完整的选择列表
    单选:在完整列表中添加或移除单个ID
    多选:先移除可视区域所有ID,再添加当前选中的ID
  4. 保持数据完整性:始终基于完整的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)思路

  1. 移除 onSelectChange 中的全选判断逻辑
  2. onSelectChange 只处理单行选择,不判断全选
  3. 全选操作完全由 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就可以了,没有那么复杂

相关推荐
木子雨廷1 小时前
Flutter 内存管理实战:从 GC 原理到 DevTools 泄漏排查
前端·flutter
Rkgua1 小时前
TS中`Function`、`CallableFunction` 和 `NewableFunction`的函数区别
前端
Asize1 小时前
重生之我在 Vibe Coding 时代当程序员:第十一课,JS底层 :变量提升真相
前端·javascript
HYCS1 小时前
用pixi.js实现fabric.js(五):事件系统
前端·javascript·canvas
Momo__1 小时前
Node.js 26 来了:Temporal API 默认启用,Date 终于可以退休了
前端·node.js
小宇AI1 小时前
用纯 Node.js 写了一个 JS 解释器 — kernel-js-lite
javascript
雨季mo浅忆1 小时前
记录前端内网开发之新入职篇
前端·内网开发
杨运交2 小时前
[025][Web模块]基于 Spring Boot 的请求日志过滤器设计与实现
前端·spring boot·后端
IT_陈寒2 小时前
React的useEffect里设状态?我又踩雷了
前端·人工智能·后端