Lzh on GitHub
<template>
  <div>
    <p>
      对于表格,`translate` CSS 函数的偏移量是基于 <strong>行本身的初始位置</strong>。正因为如此,我们需要以不同的方式计算 `translateY` 像素计数,并将其基于 <strong>索引</strong>    </p>

    <div
      ref="parentRef"
      class="container"
    >
      <div :style="{ height: `${totalSize}px` }">
        <table>
          <thead>
            <tr
              v-for="headerGroup in table.getHeaderGroups()"
              :key="headerGroup.id"
            >
              <th
                v-for="header in headerGroup.headers"
                :key="header.id"
                :colspan="header.colSpan"
                :style="{ width: `${header.getSize()}px` }"
              >
                <div
                  v-if="!header.isPlaceholder"
                  :class="[
                    'text-left',
                    header.column.getCanSort()
                      ? 'cursor-pointer select-none'
                      : ''
                  ]"
                  @click="
                    getSortingHandler(
                      $event,
                      header.column.getToggleSortingHandler()
                    )
                  "
                >
                  <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>
            <tr
              v-for="(virtualRow, index) in virtualRows"
              :key="virtualRow.key"
              :style="{
                transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`
              }"
            >
              <td
                v-for="cell in rows[virtualRow.index].getVisibleCells()"
                :key="cell.id"
              >
                <FlexRender
                  :render="cell.column.columnDef.cell"
                  :props="cell.getContext()"
                />
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
import type {
  ColumnDef,
  SortingState } from '@tanstack/vue-table'
import {
  FlexRender,
  useVueTable,
  getCoreRowModel,
  getSortedRowModel
} from '@tanstack/vue-table'
import type { Person } from './makeData'
import { makeData } from './makeData'

const data = ref(makeData(100))

const sorting = ref<SortingState>([])

const getSortingHandler = (e: Event, fn: any) => {
  return fn(e)
}

const setSorting = (sortingUpdater: any) => {
  const newSortVal = sortingUpdater(sorting.value)

  sorting.value = newSortVal
}

const columns = computed<ColumnDef<Person>[]>(() => {
  return [
    {
      accessorKey: 'id',
      header: 'ID',
      size: 60
    },
    {
      accessorKey: 'firstName',
      cell: info => info.getValue()
    },
    {
      accessorFn: row => row.lastName,
      id: 'lastName',
      cell: info => info.getValue(),
      header: () => 'Last Name'
    },
    {
      accessorKey: 'age',
      header: () => 'Age',
      size: 50
    },
    {
      accessorKey: 'visits',
      header: () => 'Visits',
      size: 50
    },
    {
      accessorKey: 'status',
      header: 'Status'
    },
    {
      accessorKey: 'progress',
      header: 'Profile Progress',
      size: 80
    },
    {
      accessorKey: 'createdAt',
      header: 'Created At',
      cell: info => info.getValue<Date>().toLocaleString()
    }
  ]
})

const table = useVueTable({
  get data() {
    return data.value
  },
  columns: columns.value,
  state: {
    get sorting() {
      return sorting.value
    }
  },
  onSortingChange: setSorting,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  debugTable: true
})

const rows = computed(() => {
  return table.getRowModel().rows
})

const parentRef = ref<HTMLElement | null>(null)

const rowVirtualizerOptions = computed(() => {
  return {
    count: rows.value.length,
    getScrollElement: () => parentRef.value,
    estimateSize: () => 34,
    overscan: 5
  }
})

const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)

const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())

const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
</script>

<style scoped>
.container {
  height: 600px;
  overflow: auto;
}

.cursor-pointer {
  cursor: pointer;
}

.select-none {
  user-select: none;
}

.text-left {
  text-align: left;
}
</style>