虚拟行
<script setup lang="ts">
import './index.css'
import { computed, ref, h } from 'vue'
import {
FlexRender,
useVueTable,
getCoreRowModel,
getSortedRowModel
} from '@tanstack/vue-table'
import type { ColumnDef } from '@tanstack/vue-table'
import { useVirtualizer } from '@tanstack/vue-virtual'
import { makeData } from './virtualizedRowsMakeData'
import type { Person } from './virtualizedRowsMakeData'
const search = ref('')
const data = ref<Person[]>(makeData(100))
/**
* 计算属性:根据搜索关键词过滤后的数据
* @returns 过滤后的 Person 数组
*/
const filteredData = computed<Person[]>(() => {
// 获取搜索关键词并转换为小写以便进行不区分大小写的匹配
const searchValue = search.value.toLowerCase()
// 如果没有搜索关键词,则返回所有数据
if (!searchValue) return data.value
// 根据搜索关键词过滤数据
return data.value.filter((row) => {
// 检查行中的每个字段值是否包含搜索关键词
return Object.values(row).some((value) => {
// 如果值是日期类型,将其转换为本地字符串进行比较
if (value instanceof Date) {
return value.toLocaleString().toLowerCase().includes(searchValue)
}
// 将值转换为字符串并检查是否包含搜索关键词
return `${value}`.toLowerCase().includes(searchValue)
})
})
})
// 声明一个变量用于存储防抖定时器的引用
let searchTimeout: NodeJS.Timeout
/**
* 处理搜索输入的防抖函数
* 通过延迟更新搜索值来减少频繁的过滤操作
* @param ev - 输入事件对象
*/
function handleDebounceSearch(ev: Event) {
// 如果已存在定时器,则清除之前的定时器(重置防抖)
if (searchTimeout) {
clearTimeout(searchTimeout)
}
// 设置新的定时器,在300ms后更新搜索值
searchTimeout = setTimeout(() => {
// 从事件目标中获取输入框的值并更新搜索状态
search.value = (ev?.target as HTMLInputElement)?.value ?? ''
}, 300) // 300ms的防抖延迟
}
const columns = computed<ColumnDef<Person>[]>(() => [
{
accessorKey: 'id',
header: 'ID'
},
{
accessorKey: 'firstName',
cell: info => info.getValue()
},
{
accessorFn: row => row.lastName,
id: 'lastName',
cell: info => info.getValue(),
header: () => h('span', 'Last Name')
},
{
accessorKey: 'age',
header: () => 'Age'
},
{
accessorKey: 'visits',
header: () => h('span', 'Visits')
},
{
accessorKey: 'status',
header: 'Status'
},
{
accessorKey: 'progress',
header: 'Profile Progress'
},
{
accessorKey: 'createdAt',
header: 'Created At',
cell: info => info.getValue<Date>().toLocaleString()
}
])
const table = useVueTable({
get data() {
return filteredData.value
},
columns: columns.value,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: false
})
// 计算属性:获取表格的行模型数据
const rows = computed(() => table.getRowModel().rows)
// The virtualizer needs to know the scrollable container element
// 创建对表格容器元素的引用,虚拟化器需要知道滚动容器元素
const tableContainerRef = ref<HTMLDivElement | null>(null)
// 计算属性:配置虚拟化器的选项
const rowVirtualizerOptions = computed(() => {
return {
// 数据总条数,用于计算滚动条大小
count: rows.value.length,
// 估算每行的高度(像素),用于准确的滚动条拖拽
estimateSize: () => 33, // estimate row height for accurate scrollbar dragging
// 获取滚动容器元素的回调函数
getScrollElement: () => tableContainerRef.value,
// 预渲染的行数,避免滚动时出现空白区域
overscan: 5
}
})
// 创建行虚拟化器实例
const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)
// 计算属性:获取虚拟化的行项目列表
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())
// 计算属性:获取虚拟化内容的总高度
const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
/**
* 测量元素实际高度的函数
* @param el - 要测量的DOM元素
*/
function measureElement(el?: Element) {
// 如果元素不存在,则直接返回
if (!el) {
return
}
// 调用虚拟化器的measureElement方法来测量元素高度
rowVirtualizer.value.measureElement(el)
return undefined
}
</script>
<template>
<div>
<p class="text-center">
对于表格,translate CSS 函数的偏移量是基于行本身的初始位置。正因为如此,我们需要计算 translateY 的像素计数差值,并将其基于索引。
</p>
<h1 class="text-3xl font-bold text-center">
Virtualized Rows
</h1>
<div style="margin: 0 auto; width: min-content">
<input
:modelValue="search"
placeholder="Search"
class="p-2"
@input="handleDebounceSearch"
>
{{ rows.length.toLocaleString() }} results
</div>
</div>
<div
ref="tableContainerRef"
class="container"
:style="{
overflow: 'auto', //our scrollable table container
position: 'relative', //needed for sticky header
height: '800px' //should be a fixed height
}"
>
<div :style="{ height: `${totalSize}px` }">
<!-- Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights -->
<table :style="{ display: 'grid' }">
<thead
:style="{
display: 'grid',
position: 'sticky',
top: 0,
zIndex: 1
}"
>
<tr
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
:style="{ display: 'flex', width: '100%' }"
>
<th
v-for="header in headerGroup.headers"
:key="header.id"
:colspan="header.colSpan"
:style="{ width: `${header.getSize()}px` }"
>
<div
v-if="!header.isPlaceholder"
:class="{
'cursor-pointer select-none': header.column.getCanSort()
}"
@click="e => header.column.getToggleSortingHandler()?.(e)"
>
<FlexRender
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
<span v-if="header.column.getIsSorted() === 'asc'"> 🔼</span>
<span v-if="header.column.getIsSorted() === 'desc'"> 🔽</span>
</div>
</th>
</tr>
</thead>
<tbody
:style="{
display: 'grid',
height: `${totalSize}px`, //tells scrollbar how big the table is
position: 'relative' //needed for absolute positioning of rows
}"
>
<tr
v-for="vRow in virtualRows"
:ref="measureElement /*measure dynamic row height*/"
:key="rows[vRow.index].id"
:data-index="
vRow.index /* needed for dynamic row height measurement*/
"
:style="{
display: 'flex',
position: 'absolute',
transform: `translateY(${vRow.start}px)`, //this should always be a `style` as it changes on scroll
width: '100%'
}"
>
<td
v-for="cell in rows[vRow.index].getVisibleCells()"
:key="cell.id"
:style="{
display: 'flex',
width: `${cell.column.getSize()}px`
}"
>
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>