Virtualized Table 虚拟化表格 beta

在前端开发领域,表格一直都是一个高频出现的组件,尤其是在中后台和数据分析场景。 但是,对于 Table V1来说,当一屏里超过 1000 条数据记录时,就会出现卡顿等性能问题,体验不是很好。

通过虚拟化表格组件,超大数据渲染将不再是一个头疼的问题。

TIP

该组件仍在测试中,生产环境使用可能有风险。 若您发现了 bug 或问题,请于 GitHub Virtualized Table 虚拟化表格 2.2.0 - 图1 报告给我们以便修复。 同时,有一些 API 并未在此文档中提及,因为部分还没有开发完全,因此我们不在此提及。

即使虚拟化的表格是高效的,但是当数据负载过大时,网络内存容量也会成为您应用程序的瓶颈。 因此请牢记,虚拟化表格永远不是最完美的解决方案,请考虑数据分页、过滤器等优化方案。

基础用法

让我们简单渲染一个拥有 1000 行 10 列的虚拟化表格组件,来展示其极佳的性能。

Virtualized Table 虚拟化表格 2.2.0 - 图2

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :width="700"
  6. :height="400"
  7. fixed
  8. />
  9. </template>
  10. <script lang="ts" setup>
  11. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  12. Array.from({ length }).map((_, columnIndex) => ({
  13. ...props,
  14. key: `${prefix}${columnIndex}`,
  15. dataKey: `${prefix}${columnIndex}`,
  16. title: `Column ${columnIndex}`,
  17. width: 150,
  18. }))
  19. const generateData = (
  20. columns: ReturnType<typeof generateColumns>,
  21. length = 200,
  22. prefix = 'row-'
  23. ) =>
  24. Array.from({ length }).map((_, rowIndex) => {
  25. return columns.reduce(
  26. (rowData, column, columnIndex) => {
  27. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  28. return rowData
  29. },
  30. {
  31. id: `${prefix}${rowIndex}`,
  32. parentId: null,
  33. }
  34. )
  35. })
  36. const columns = generateColumns(10)
  37. const data = generateData(columns, 1000)
  38. </script>

自动调整大小

如果您不想手动设置表格的 widthheight ,可以使用 AutoResizer 组件包裹表格组件,这将会自动更新表格的宽度和高度。

尝试调整您的浏览器大小来看看它是如何工作的。

TIP

由于AutoResizer 组件的默认高度是 100%, 所以请确保 该组件的父元素被设置了一个固定的高度 也可以通过 设置style 属性为 AutoResizer指定高度。

Virtualized Table 虚拟化表格 2.2.0 - 图3

  1. <template>
  2. <div style="height: 400px">
  3. <el-auto-resizer>
  4. <template #default="{ height, width }">
  5. <el-table-v2
  6. :columns="columns"
  7. :data="data"
  8. :width="width"
  9. :height="height"
  10. fixed
  11. />
  12. </template>
  13. </el-auto-resizer>
  14. </div>
  15. </template>
  16. <script lang="ts" setup>
  17. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  18. Array.from({ length }).map((_, columnIndex) => ({
  19. ...props,
  20. key: `${prefix}${columnIndex}`,
  21. dataKey: `${prefix}${columnIndex}`,
  22. title: `Column ${columnIndex}`,
  23. width: 150,
  24. }))
  25. const generateData = (
  26. columns: ReturnType<typeof generateColumns>,
  27. length = 200,
  28. prefix = 'row-'
  29. ) =>
  30. Array.from({ length }).map((_, rowIndex) => {
  31. return columns.reduce(
  32. (rowData, column, columnIndex) => {
  33. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  34. return rowData
  35. },
  36. {
  37. id: `${prefix}${rowIndex}`,
  38. parentId: null,
  39. }
  40. )
  41. })
  42. const columns = generateColumns(10)
  43. const data = generateData(columns, 200)
  44. </script>

自定义单元格渲染

您可以自由定制表格单元格的渲染内容,下面是一个简单例子。

Virtualized Table 虚拟化表格 2.2.0 - 图4

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :width="700"
  6. :height="400"
  7. fixed
  8. />
  9. </template>
  10. <script lang="tsx" setup>
  11. import { ref } from 'vue'
  12. import dayjs from 'dayjs'
  13. import {
  14. ElButton,
  15. ElIcon,
  16. ElTag,
  17. ElTooltip,
  18. TableV2FixedDir,
  19. } from 'element-plus'
  20. import { Timer } from '@element-plus/icons-vue'
  21. import type { Column } from 'element-plus'
  22. let id = 0
  23. const dataGenerator = () => ({
  24. id: `random-id-${++id}`,
  25. name: 'Tom',
  26. date: '2020-10-1',
  27. })
  28. const columns: Column<any>[] = [
  29. {
  30. key: 'date',
  31. title: 'Date',
  32. dataKey: 'date',
  33. width: 150,
  34. fixed: TableV2FixedDir.LEFT,
  35. cellRenderer: ({ cellData: date }) => (
  36. <ElTooltip content={dayjs(date).format('YYYY/MM/DD')}>
  37. {
  38. <span class="flex items-center">
  39. <ElIcon class="mr-3">
  40. <Timer />
  41. </ElIcon>
  42. {dayjs(date).format('YYYY/MM/DD')}
  43. </span>
  44. }
  45. </ElTooltip>
  46. ),
  47. },
  48. {
  49. key: 'name',
  50. title: 'Name',
  51. dataKey: 'name',
  52. width: 150,
  53. align: 'center',
  54. cellRenderer: ({ cellData: name }) => <ElTag>{name}</ElTag>,
  55. },
  56. {
  57. key: 'operations',
  58. title: 'Operations',
  59. cellRenderer: () => (
  60. <>
  61. <ElButton size="small">Edit</ElButton>
  62. <ElButton size="small" type="danger">
  63. Delete
  64. </ElButton>
  65. </>
  66. ),
  67. width: 150,
  68. align: 'center',
  69. },
  70. ]
  71. const data = ref(Array.from({ length: 200 }).map(dataGenerator))
  72. </script>

带有筛选框的表格

使用自定义的单元格渲染来给表格组件添加筛选能力。

Virtualized Table 虚拟化表格 2.2.0 - 图5

  1. <template>
  2. <div style="height: 400px">
  3. <el-auto-resizer>
  4. <template #default="{ height, width }">
  5. <el-table-v2
  6. :columns="columns"
  7. :data="data"
  8. :width="width"
  9. :height="height"
  10. fixed
  11. />
  12. </template>
  13. </el-auto-resizer>
  14. </div>
  15. </template>
  16. <script lang="tsx" setup>
  17. import { ref, resolveDynamicComponent, unref } from 'vue'
  18. import type { FunctionalComponent } from 'vue'
  19. import type { Column, ElCheckbox } from 'element-plus'
  20. const Checkbox = resolveDynamicComponent('ElCheckbox') as typeof ElCheckbox
  21. type SelectionCellProps = {
  22. value: boolean
  23. intermediate?: boolean
  24. onChange: (value: boolean) => void
  25. }
  26. const SelectionCell: FunctionalComponent<SelectionCellProps> = ({
  27. value,
  28. intermediate = false,
  29. onChange,
  30. }) => {
  31. return (
  32. <Checkbox
  33. onChange={onChange}
  34. modelValue={value}
  35. indeterminate={intermediate}
  36. />
  37. )
  38. }
  39. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  40. Array.from({ length }).map((_, columnIndex) => ({
  41. ...props,
  42. key: `${prefix}${columnIndex}`,
  43. dataKey: `${prefix}${columnIndex}`,
  44. title: `Column ${columnIndex}`,
  45. width: 150,
  46. }))
  47. const generateData = (
  48. columns: ReturnType<typeof generateColumns>,
  49. length = 200,
  50. prefix = 'row-'
  51. ) =>
  52. Array.from({ length }).map((_, rowIndex) => {
  53. return columns.reduce(
  54. (rowData, column, columnIndex) => {
  55. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  56. return rowData
  57. },
  58. {
  59. id: `${prefix}${rowIndex}`,
  60. checked: false,
  61. parentId: null,
  62. }
  63. )
  64. })
  65. const columns: Column<any>[] = generateColumns(10)
  66. columns.unshift({
  67. key: 'selection',
  68. width: 50,
  69. cellRenderer: ({ rowData }) => {
  70. const onChange = (value: boolean) => (rowData.checked = value)
  71. return <SelectionCell value={rowData.checked} onChange={onChange} />
  72. },
  73. headerCellRenderer: () => {
  74. const _data = unref(data)
  75. const onChange = (value: boolean) =>
  76. (data.value = _data.map((row) => {
  77. row.checked = value
  78. return row
  79. }))
  80. const allSelected = _data.every((row) => row.checked)
  81. const containsChecked = _data.some((row) => row.checked)
  82. return (
  83. <SelectionCell
  84. value={allSelected}
  85. intermediate={containsChecked && !allSelected}
  86. onChange={onChange}
  87. />
  88. )
  89. },
  90. })
  91. const data = ref(generateData(columns, 200))
  92. </script>

可编辑单元格

类似上面添加筛选框的方法,我们可以用同样的方法实现可编辑单元格

Virtualized Table 虚拟化表格 2.2.0 - 图6

  1. <template>
  2. <div style="height: 400px">
  3. <el-auto-resizer>
  4. <template #default="{ height, width }">
  5. <el-table-v2
  6. :columns="columns"
  7. :data="data"
  8. :width="width"
  9. :height="height"
  10. fixed
  11. />
  12. </template>
  13. </el-auto-resizer>
  14. </div>
  15. </template>
  16. <script lang="tsx" setup>
  17. import { nextTick, ref, resolveDynamicComponent } from 'vue'
  18. import type { FunctionalComponent } from 'vue'
  19. import type { Column, ElInput } from 'element-plus'
  20. const Input = resolveDynamicComponent('ElInput') as typeof ElInput
  21. type SelectionCellProps = {
  22. value: string
  23. intermediate?: boolean
  24. onChange: (value: string) => void
  25. forwardRef: (el: InstanceType<typeof ElInput>) => void
  26. }
  27. const InputCell: FunctionalComponent<SelectionCellProps> = ({
  28. value,
  29. onChange,
  30. forwardRef,
  31. }) => {
  32. return <Input ref={forwardRef as any} onInput={onChange} modelValue={value} />
  33. }
  34. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  35. Array.from({ length }).map((_, columnIndex) => ({
  36. ...props,
  37. key: `${prefix}${columnIndex}`,
  38. dataKey: `${prefix}${columnIndex}`,
  39. title: `Column ${columnIndex}`,
  40. width: 150,
  41. }))
  42. const generateData = (
  43. columns: ReturnType<typeof generateColumns>,
  44. length = 200,
  45. prefix = 'row-'
  46. ) =>
  47. Array.from({ length }).map((_, rowIndex) => {
  48. return columns.reduce(
  49. (rowData, column, columnIndex) => {
  50. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  51. return rowData
  52. },
  53. {
  54. id: `${prefix}${rowIndex}`,
  55. editing: false,
  56. parentId: null,
  57. }
  58. )
  59. })
  60. const columns: Column<any>[] = generateColumns(10)
  61. columns[0] = {
  62. ...columns[0],
  63. title: 'Editable Column',
  64. cellRenderer: ({ rowData, column }) => {
  65. const onChange = (value: string) => {
  66. rowData[column.dataKey!] = value
  67. }
  68. const onEnterEditMode = () => {
  69. rowData.editing = true
  70. }
  71. const onExitEditMode = () => (rowData.editing = false)
  72. const input = ref()
  73. const setRef = (el) => {
  74. input.value = el
  75. if (el) {
  76. el.focus?.()
  77. }
  78. }
  79. return rowData.editing ? (
  80. <InputCell
  81. forwardRef={setRef}
  82. value={rowData[column.dataKey!]}
  83. onChange={onChange}
  84. onBlur={onExitEditMode}
  85. onKeydownEnter={onExitEditMode}
  86. />
  87. ) : (
  88. <div class="table-v2-inline-editing-trigger" onClick={onEnterEditMode}>
  89. {rowData[column.dataKey!]}
  90. </div>
  91. )
  92. },
  93. }
  94. const data = ref(generateData(columns, 200))
  95. </script>
  96. <style>
  97. .table-v2-inline-editing-trigger {
  98. border: 1px transparent dotted;
  99. padding: 4px;
  100. }
  101. .table-v2-inline-editing-trigger:hover {
  102. border-color: var(--el-color-primary);
  103. }
  104. </style>

带状态的表格

可将表格内容 highlight 显示,方便区分「成功、信息、警告、危险」等内容。

可以通过指定 Table 组件的 row-class-name 属性来为 Table 中的某一行添加 class, 表明该行处于某种状态。 每10行会自动添加 bg-blue-200 类名,每5行会添加 bg-red-100 类名。

Virtualized Table 虚拟化表格 2.2.0 - 图7

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :row-class="rowClass"
  6. :width="700"
  7. :height="400"
  8. />
  9. </template>
  10. <script lang="tsx" setup>
  11. import { ref } from 'vue'
  12. import dayjs from 'dayjs'
  13. import {
  14. ElButton,
  15. ElIcon,
  16. ElTag,
  17. ElTooltip,
  18. TableV2FixedDir,
  19. } from 'element-plus'
  20. import { Timer } from '@element-plus/icons-vue'
  21. import type { Column, RowClassNameGetter } from 'element-plus'
  22. let id = 0
  23. const dataGenerator = () => ({
  24. id: `random-id-${++id}`,
  25. name: 'Tom',
  26. date: '2020-10-1',
  27. })
  28. const columns: Column<any>[] = [
  29. {
  30. key: 'date',
  31. title: 'Date',
  32. dataKey: 'date',
  33. width: 150,
  34. fixed: TableV2FixedDir.LEFT,
  35. cellRenderer: ({ cellData: date }) => (
  36. <ElTooltip content={dayjs(date).format('YYYY/MM/DD')}>
  37. {
  38. <span class="flex items-center">
  39. <ElIcon class="mr-3">
  40. <Timer />
  41. </ElIcon>
  42. {dayjs(date).format('YYYY/MM/DD')}
  43. </span>
  44. }
  45. </ElTooltip>
  46. ),
  47. },
  48. {
  49. key: 'name',
  50. title: 'Name',
  51. dataKey: 'name',
  52. width: 150,
  53. align: 'center',
  54. cellRenderer: ({ cellData: name }) => <ElTag>{name}</ElTag>,
  55. },
  56. {
  57. key: 'operations',
  58. title: 'Operations',
  59. cellRenderer: () => (
  60. <>
  61. <ElButton size="small">Edit</ElButton>
  62. <ElButton size="small" type="danger">
  63. Delete
  64. </ElButton>
  65. </>
  66. ),
  67. width: 150,
  68. align: 'center',
  69. flexGrow: 1,
  70. },
  71. ]
  72. const data = ref(Array.from({ length: 200 }).map(dataGenerator))
  73. const rowClass = ({ rowIndex }: Parameters<RowClassNameGetter<any>>[0]) => {
  74. if (rowIndex % 10 === 5) {
  75. return 'bg-red-100'
  76. } else if (rowIndex % 10 === 0) {
  77. return 'bg-blue-200'
  78. }
  79. return ''
  80. }
  81. </script>

表格行的粘性布局

您可以简单地使用 fixed-data 属性来实现将某些行固定到表格的头部。

您可以使用滚动事件动态地设置粘性行,见此示例。

Virtualized Table 虚拟化表格 2.2.0 - 图8

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="tableData"
  5. :fixed-data="fixedData"
  6. :width="700"
  7. :height="400"
  8. :row-class="rowClass"
  9. fixed
  10. @scroll="onScroll"
  11. />
  12. </template>
  13. <script lang="ts" setup>
  14. import { computed, ref } from 'vue'
  15. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  16. Array.from({ length }).map((_, columnIndex) => ({
  17. ...props,
  18. key: `${prefix}${columnIndex}`,
  19. dataKey: `${prefix}${columnIndex}`,
  20. title: `Column ${columnIndex}`,
  21. width: 150,
  22. }))
  23. const generateData = (
  24. columns: ReturnType<typeof generateColumns>,
  25. length = 200,
  26. prefix = 'row-'
  27. ) =>
  28. Array.from({ length }).map((_, rowIndex) => {
  29. return columns.reduce(
  30. (rowData, column, columnIndex) => {
  31. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  32. return rowData
  33. },
  34. {
  35. id: `${prefix}${rowIndex}`,
  36. parentId: null,
  37. }
  38. )
  39. })
  40. const columns = generateColumns(10)
  41. const data = generateData(columns, 200)
  42. const rowClass = ({ rowIndex }) => {
  43. if (rowIndex < 0 || (rowIndex + 1) % 5 === 0) return 'sticky-row'
  44. }
  45. const stickyIndex = ref(0)
  46. const fixedData = computed(() =>
  47. data.slice(stickyIndex.value, stickyIndex.value + 1)
  48. )
  49. const tableData = computed(() => {
  50. return data.slice(1)
  51. })
  52. const onScroll = ({ scrollTop }) => {
  53. stickyIndex.value = Math.floor(scrollTop / 250) * 5
  54. }
  55. </script>
  56. <style>
  57. .el-el-table-v2__fixed-header-row {
  58. background-color: var(--el-color-primary-light-5);
  59. font-weight: bold;
  60. }
  61. </style>

固定列表格

出于某些原因,您可能会让一些列固定在表格的左侧和右侧。您可以通过为表格添加特殊属性来实现。

您可以设置该行的 fixed 属性为 true (代表FixedDir.LEFT)、FixedDir.LEFTFixedDir.RIGHT

Virtualized Table 虚拟化表格 2.2.0 - 图9

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :sort-by="sortBy"
  6. :width="700"
  7. :height="400"
  8. fixed
  9. @column-sort="onSort"
  10. />
  11. </template>
  12. <script lang="ts" setup>
  13. import { ref } from 'vue'
  14. import { TableV2FixedDir, TableV2SortOrder } from 'element-plus'
  15. import type { SortBy } from 'element-plus'
  16. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  17. Array.from({ length }).map((_, columnIndex) => ({
  18. ...props,
  19. key: `${prefix}${columnIndex}`,
  20. dataKey: `${prefix}${columnIndex}`,
  21. title: `Column ${columnIndex}`,
  22. width: 150,
  23. }))
  24. const generateData = (
  25. columns: ReturnType<typeof generateColumns>,
  26. length = 200,
  27. prefix = 'row-'
  28. ) =>
  29. Array.from({ length }).map((_, rowIndex) => {
  30. return columns.reduce(
  31. (rowData, column, columnIndex) => {
  32. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  33. return rowData
  34. },
  35. {
  36. id: `${prefix}${rowIndex}`,
  37. parentId: null,
  38. }
  39. )
  40. })
  41. const columns = generateColumns(10)
  42. let data = generateData(columns, 200)
  43. columns[0].fixed = true
  44. columns[1].fixed = TableV2FixedDir.LEFT
  45. columns[9].fixed = TableV2FixedDir.RIGHT
  46. for (let i = 0; i < 3; i++) columns[i].sortable = true
  47. const sortBy = ref<SortBy>({
  48. key: 'column-0',
  49. order: TableV2SortOrder.ASC,
  50. })
  51. const onSort = (_sortBy: SortBy) => {
  52. data = data.reverse()
  53. sortBy.value = _sortBy
  54. }
  55. </script>

表头分组

正如这个示例,通过自定义表头渲染以将表头分组。

TIP

在这种形况下,我们使用JSX特性(该功能在 playground 中不被支持,您可以在您的本地环境或在线 IDE (如 codesandbox )中使用)

建议您使用 JSX 使用您的表格组件,因为它包含 VNode 操作。

Virtualized Table 虚拟化表格 2.2.0 - 图10

  1. <template>
  2. <el-table-v2
  3. fixed
  4. :columns="fixedColumns"
  5. :data="data"
  6. :header-height="[50, 40, 50]"
  7. :header-class="headerClass"
  8. :width="700"
  9. :height="400"
  10. >
  11. <template #header="props">
  12. <customized-header v-bind="props" />
  13. </template>
  14. </el-table-v2>
  15. </template>
  16. <script lang="tsx" setup>
  17. import { TableV2FixedDir, TableV2Placeholder } from 'element-plus'
  18. import type { FunctionalComponent } from 'vue'
  19. import type {
  20. HeaderClassNameGetter,
  21. TableV2CustomizedHeaderSlotParam,
  22. } from 'element-plus'
  23. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  24. Array.from({ length }).map((_, columnIndex) => ({
  25. ...props,
  26. key: `${prefix}${columnIndex}`,
  27. dataKey: `${prefix}${columnIndex}`,
  28. title: `Column ${columnIndex}`,
  29. width: 150,
  30. }))
  31. const generateData = (
  32. columns: ReturnType<typeof generateColumns>,
  33. length = 200,
  34. prefix = 'row-'
  35. ) =>
  36. Array.from({ length }).map((_, rowIndex) => {
  37. return columns.reduce(
  38. (rowData, column, columnIndex) => {
  39. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  40. return rowData
  41. },
  42. {
  43. id: `${prefix}${rowIndex}`,
  44. parentId: null,
  45. }
  46. )
  47. })
  48. const columns = generateColumns(15)
  49. const data = generateData(columns, 200)
  50. const fixedColumns = columns.map((column, columnIndex) => {
  51. let fixed: TableV2FixedDir | undefined = undefined
  52. if (columnIndex < 3) fixed = TableV2FixedDir.LEFT
  53. if (columnIndex > 12) fixed = TableV2FixedDir.RIGHT
  54. return { ...column, fixed, width: 100 }
  55. })
  56. const CustomizedHeader: FunctionalComponent<
  57. TableV2CustomizedHeaderSlotParam
  58. > = ({ cells, columns, headerIndex }) => {
  59. if (headerIndex === 2) return cells
  60. const groupCells = [] as typeof cells
  61. let width = 0
  62. let idx = 0
  63. columns.forEach((column, columnIndex) => {
  64. if (column.placeholderSign === TableV2Placeholder)
  65. groupCells.push(cells[columnIndex])
  66. else {
  67. width += cells[columnIndex].props!.column.width
  68. idx++
  69. const nextColumn = columns[columnIndex + 1]
  70. if (
  71. columnIndex === columns.length - 1 ||
  72. nextColumn.placeholderSign === TableV2Placeholder ||
  73. idx === (headerIndex === 0 ? 4 : 2)
  74. ) {
  75. groupCells.push(
  76. <div
  77. class="flex items-center justify-center custom-header-cell"
  78. style={{
  79. ...cells[columnIndex].props!.style,
  80. width: `${width}px`,
  81. }}
  82. >
  83. Group width {width}
  84. </div>
  85. )
  86. width = 0
  87. idx = 0
  88. }
  89. }
  90. })
  91. return groupCells
  92. }
  93. const headerClass = ({
  94. headerIndex,
  95. }: Parameters<HeaderClassNameGetter<any>>[0]) => {
  96. if (headerIndex === 1) return 'el-primary-color'
  97. return ''
  98. }
  99. </script>
  100. <style>
  101. .el-el-table-v2__header-row .custom-header-cell {
  102. border-right: 1px solid var(--el-border-color);
  103. }
  104. .el-el-table-v2__header-row .custom-header-cell:last-child {
  105. border-right: none;
  106. }
  107. .el-primary-color {
  108. background-color: var(--el-color-primary);
  109. color: var(--el-color-white);
  110. font-size: 14px;
  111. font-weight: bold;
  112. }
  113. .el-primary-color .custom-header-cell {
  114. padding: 0 4px;
  115. }
  116. </style>

过滤器

虚拟化表格可以提供自定义的表头渲染,因此我们可以使用这个来过滤渲染。

Virtualized Table 虚拟化表格 2.2.0 - 图11

  1. <template>
  2. <el-table-v2
  3. fixed
  4. :columns="fixedColumns"
  5. :data="data"
  6. :width="700"
  7. :height="400"
  8. />
  9. </template>
  10. <script lang="tsx" setup>
  11. import { ref } from 'vue'
  12. import {
  13. ElButton,
  14. ElCheckbox,
  15. ElIcon,
  16. ElPopover,
  17. TableV2FixedDir,
  18. } from 'element-plus'
  19. import { Filter } from '@element-plus/icons-vue'
  20. import type { HeaderCellSlotProps } from 'element-plus'
  21. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  22. Array.from({ length }).map((_, columnIndex) => ({
  23. ...props,
  24. key: `${prefix}${columnIndex}`,
  25. dataKey: `${prefix}${columnIndex}`,
  26. title: `Column ${columnIndex}`,
  27. width: 150,
  28. }))
  29. const generateData = (
  30. columns: ReturnType<typeof generateColumns>,
  31. length = 200,
  32. prefix = 'row-'
  33. ) =>
  34. Array.from({ length }).map((_, rowIndex) => {
  35. return columns.reduce(
  36. (rowData, column, columnIndex) => {
  37. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  38. return rowData
  39. },
  40. {
  41. id: `${prefix}${rowIndex}`,
  42. parentId: null,
  43. }
  44. )
  45. })
  46. const columns = generateColumns(10)
  47. const data = ref(generateData(columns, 200))
  48. const shouldFilter = ref(false)
  49. const popoverRef = ref()
  50. const onFilter = () => {
  51. popoverRef.value.hide()
  52. if (shouldFilter.value) {
  53. data.value = generateData(columns, 100, 'filtered-')
  54. } else {
  55. data.value = generateData(columns, 200)
  56. }
  57. }
  58. const onReset = () => {
  59. shouldFilter.value = false
  60. onFilter()
  61. }
  62. columns[0].headerCellRenderer = (props: HeaderCellSlotProps) => {
  63. return (
  64. <div class="flex items-center justify-center">
  65. <span class="mr-2 text-xs">{props.column.title}</span>
  66. <ElPopover ref={popoverRef} trigger="click" {...{ width: 200 }}>
  67. {{
  68. default: () => (
  69. <div class="filter-wrapper">
  70. <div class="filter-group">
  71. <ElCheckbox v-model={shouldFilter.value}>
  72. Filter Text
  73. </ElCheckbox>
  74. </div>
  75. <div class="el-table-v2__demo-filter">
  76. <ElButton text onClick={onFilter}>
  77. Confirm
  78. </ElButton>
  79. <ElButton text onClick={onReset}>
  80. Reset
  81. </ElButton>
  82. </div>
  83. </div>
  84. ),
  85. reference: () => (
  86. <ElIcon class="cursor-pointer">
  87. <Filter />
  88. </ElIcon>
  89. ),
  90. }}
  91. </ElPopover>
  92. </div>
  93. )
  94. }
  95. const fixedColumns = columns.map((column, columnIndex) => {
  96. let fixed: TableV2FixedDir | undefined = undefined
  97. if (columnIndex < 2) fixed = TableV2FixedDir.LEFT
  98. if (columnIndex > 9) fixed = TableV2FixedDir.RIGHT
  99. return { ...column, fixed, width: 100 }
  100. })
  101. </script>
  102. <style>
  103. .el-table-v2__demo-filter {
  104. border-top: var(--el-border);
  105. margin: 12px -12px -12px;
  106. padding: 0 12px;
  107. display: flex;
  108. justify-content: space-between;
  109. }
  110. </style>

可排序表格

您可以使用排序状态来对表格进行排序。

Virtualized Table 虚拟化表格 2.2.0 - 图12

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :sort-by="sortState"
  6. :width="700"
  7. :height="400"
  8. fixed
  9. @column-sort="onSort"
  10. />
  11. </template>
  12. <script lang="ts" setup>
  13. import { ref } from 'vue'
  14. import { TableV2SortOrder } from 'element-plus'
  15. import type { SortBy } from 'element-plus'
  16. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  17. Array.from({ length }).map((_, columnIndex) => ({
  18. ...props,
  19. key: `${prefix}${columnIndex}`,
  20. dataKey: `${prefix}${columnIndex}`,
  21. title: `Column ${columnIndex}`,
  22. width: 150,
  23. }))
  24. const generateData = (
  25. columns: ReturnType<typeof generateColumns>,
  26. length = 200,
  27. prefix = 'row-'
  28. ) =>
  29. Array.from({ length }).map((_, rowIndex) => {
  30. return columns.reduce(
  31. (rowData, column, columnIndex) => {
  32. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  33. return rowData
  34. },
  35. {
  36. id: `${prefix}${rowIndex}`,
  37. parentId: null,
  38. }
  39. )
  40. })
  41. const columns = generateColumns(10)
  42. let data = generateData(columns, 200)
  43. columns[0].sortable = true
  44. const sortState = ref<SortBy>({
  45. key: 'column-0',
  46. order: TableV2SortOrder.ASC,
  47. })
  48. const onSort = (sortBy: SortBy) => {
  49. console.log(sortBy)
  50. data = data.reverse()
  51. sortState.value = sortBy
  52. }
  53. </script>

受控的排序

您可以在需要时定义多个可排序的列。 请记住,当您在定义了多个可排序的列时, UI 可能会显得有些奇怪,因为用户不知道哪一列被排序。

Virtualized Table 虚拟化表格 2.2.0 - 图13

  1. <template>
  2. <el-table-v2
  3. v-model:sort-state="sortState"
  4. :columns="columns"
  5. :data="data"
  6. :width="700"
  7. :height="400"
  8. fixed
  9. @column-sort="onSort"
  10. />
  11. </template>
  12. <script lang="ts" setup>
  13. import { ref } from 'vue'
  14. import { TableV2SortOrder } from 'element-plus'
  15. import type { SortBy, SortState } from 'element-plus'
  16. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  17. Array.from({ length }).map((_, columnIndex) => ({
  18. ...props,
  19. key: `${prefix}${columnIndex}`,
  20. dataKey: `${prefix}${columnIndex}`,
  21. title: `Column ${columnIndex}`,
  22. width: 150,
  23. }))
  24. const generateData = (
  25. columns: ReturnType<typeof generateColumns>,
  26. length = 200,
  27. prefix = 'row-'
  28. ) =>
  29. Array.from({ length }).map((_, rowIndex) => {
  30. return columns.reduce(
  31. (rowData, column, columnIndex) => {
  32. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  33. return rowData
  34. },
  35. {
  36. id: `${prefix}${rowIndex}`,
  37. parentId: null,
  38. }
  39. )
  40. })
  41. const columns = generateColumns(10)
  42. const data = ref(generateData(columns, 200))
  43. columns[0].sortable = true
  44. columns[1].sortable = true
  45. const sortState = ref<SortState>({
  46. 'column-0': TableV2SortOrder.DESC,
  47. 'column-1': TableV2SortOrder.ASC,
  48. })
  49. const onSort = ({ key, order }: SortBy) => {
  50. sortState.value[key] = order
  51. data.value = data.value.reverse()
  52. }
  53. </script>

高亮显示鼠标悬停单元格

当数据列表很大时,有时候会忘记正在访问的行和列,使用这个属性可以给予你这个帮助。

Virtualized Table 虚拟化表格 2.2.0 - 图14

  1. <template>
  2. <div style="height: 400px">
  3. <el-auto-resizer>
  4. <template #default="{ height, width }">
  5. <el-table-v2
  6. :columns="columns"
  7. :cell-props="cellProps"
  8. :class="kls"
  9. :data="data"
  10. :width="width"
  11. :height="height"
  12. />
  13. </template>
  14. </el-auto-resizer>
  15. </div>
  16. </template>
  17. <script lang="ts" setup>
  18. import { ref } from 'vue'
  19. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  20. Array.from({ length }).map((_, columnIndex) => ({
  21. ...props,
  22. key: `${prefix}${columnIndex}`,
  23. dataKey: `${prefix}${columnIndex}`,
  24. title: `Column ${columnIndex}`,
  25. width: 150,
  26. }))
  27. const generateData = (
  28. columns: ReturnType<typeof generateColumns>,
  29. length = 200,
  30. prefix = 'row-'
  31. ) =>
  32. Array.from({ length }).map((_, rowIndex) => {
  33. return columns.reduce(
  34. (rowData, column, columnIndex) => {
  35. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  36. return rowData
  37. },
  38. {
  39. id: `${prefix}${rowIndex}`,
  40. parentId: null,
  41. }
  42. )
  43. })
  44. const columns = generateColumns(10)
  45. columns.unshift({
  46. key: 'column-n-1',
  47. width: 50,
  48. title: 'Row No.',
  49. cellRenderer: ({ rowIndex }) => `${rowIndex + 1}`,
  50. align: 'center',
  51. })
  52. const data = generateData(columns, 200)
  53. const cellProps = ({ columnIndex }) => {
  54. const key = `hovering-col-${columnIndex}`
  55. return {
  56. ['data-key']: key,
  57. onMouseenter: () => {
  58. kls.value = key
  59. },
  60. onMouseleave: () => {
  61. kls.value = ''
  62. },
  63. }
  64. }
  65. const kls = ref<string>('')
  66. </script>
  67. <style>
  68. .hovering-col-0 [data-key='hovering-col-0'],
  69. .hovering-col-1 [data-key='hovering-col-1'],
  70. .hovering-col-2 [data-key='hovering-col-2'],
  71. .hovering-col-3 [data-key='hovering-col-3'],
  72. .hovering-col-4 [data-key='hovering-col-4'],
  73. .hovering-col-5 [data-key='hovering-col-5'],
  74. .hovering-col-6 [data-key='hovering-col-6'],
  75. .hovering-col-7 [data-key='hovering-col-7'],
  76. .hovering-col-8 [data-key='hovering-col-8'],
  77. .hovering-col-9 [data-key='hovering-col-9'],
  78. .hovering-col-10 [data-key='hovering-col-10'] {
  79. background: var(--el-table-row-hover-bg-color);
  80. }
  81. [data-key='hovering-col-0'] {
  82. font-weight: bold;
  83. user-select: none;
  84. pointer-events: none;
  85. }
  86. </style>

横跨列

虚拟化表格没有使用内置的 table 元素,故 colspanrowspanTableV1 比较略有不同。 通过定制的行渲染器,我们仍然可以实现这个需求。 在如下例子,你会学习如何做到这一点。

Virtualized Table 虚拟化表格 2.2.0 - 图15

  1. <template>
  2. <el-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
  3. <template #row="props">
  4. <Row v-bind="props" />
  5. </template>
  6. </el-table-v2>
  7. </template>
  8. <script lang="ts" setup>
  9. import { cloneVNode } from 'vue'
  10. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  11. Array.from({ length }).map((_, columnIndex) => ({
  12. ...props,
  13. key: `${prefix}${columnIndex}`,
  14. dataKey: `${prefix}${columnIndex}`,
  15. title: `Column ${columnIndex}`,
  16. width: 150,
  17. }))
  18. const generateData = (
  19. columns: ReturnType<typeof generateColumns>,
  20. length = 200,
  21. prefix = 'row-'
  22. ) =>
  23. Array.from({ length }).map((_, rowIndex) => {
  24. return columns.reduce(
  25. (rowData, column, columnIndex) => {
  26. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  27. return rowData
  28. },
  29. {
  30. id: `${prefix}${rowIndex}`,
  31. parentId: null,
  32. }
  33. )
  34. })
  35. const columns = generateColumns(10)
  36. const data = generateData(columns, 200)
  37. const colSpanIndex = 1
  38. columns[colSpanIndex].colSpan = ({ rowIndex }) => (rowIndex % 4) + 1
  39. columns[colSpanIndex].align = 'center'
  40. const Row = ({ rowData, rowIndex, cells, columns }) => {
  41. const colSpan = columns[colSpanIndex].colSpan({ rowData, rowIndex })
  42. if (colSpan > 1) {
  43. let width = Number.parseInt(cells[colSpanIndex].props.style.width)
  44. for (let i = 1; i < colSpan; i++) {
  45. width += Number.parseInt(cells[colSpanIndex + i].props.style.width)
  46. cells[colSpanIndex + i] = null
  47. }
  48. const style = {
  49. ...cells[colSpanIndex].props.style,
  50. width: `${width}px`,
  51. backgroundColor: 'var(--el-color-primary-light-3)',
  52. }
  53. cells[colSpanIndex] = cloneVNode(cells[colSpanIndex], { style })
  54. }
  55. return cells
  56. }
  57. </script>

纵跨行

既然我们有 Colspan 当然我们也有纵跨的列。它与 colspan 略有不同,但是原理相似。

Virtualized Table 虚拟化表格 2.2.0 - 图16

  1. <template>
  2. <el-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
  3. <template #row="props">
  4. <Row v-bind="props" />
  5. </template>
  6. </el-table-v2>
  7. </template>
  8. <script lang="ts" setup>
  9. import { cloneVNode } from 'vue'
  10. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  11. Array.from({ length }).map((_, columnIndex) => ({
  12. ...props,
  13. key: `${prefix}${columnIndex}`,
  14. dataKey: `${prefix}${columnIndex}`,
  15. title: `Column ${columnIndex}`,
  16. width: 150,
  17. }))
  18. const generateData = (
  19. columns: ReturnType<typeof generateColumns>,
  20. length = 200,
  21. prefix = 'row-'
  22. ) =>
  23. Array.from({ length }).map((_, rowIndex) => {
  24. return columns.reduce(
  25. (rowData, column, columnIndex) => {
  26. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  27. return rowData
  28. },
  29. {
  30. id: `${prefix}${rowIndex}`,
  31. parentId: null,
  32. }
  33. )
  34. })
  35. const columns = generateColumns(10)
  36. const data = generateData(columns, 200)
  37. const rowSpanIndex = 0
  38. columns[rowSpanIndex].rowSpan = ({ rowIndex }) =>
  39. rowIndex % 2 === 0 && rowIndex <= data.length - 2 ? 2 : 1
  40. const Row = ({ rowData, rowIndex, cells, columns }) => {
  41. const rowSpan = columns[rowSpanIndex].rowSpan({ rowData, rowIndex })
  42. if (rowSpan > 1) {
  43. const cell = cells[rowSpanIndex]
  44. const style = {
  45. ...cell.props.style,
  46. backgroundColor: 'var(--el-color-primary-light-3)',
  47. height: `${rowSpan * 50 - 1}px`,
  48. alignSelf: 'flex-start',
  49. zIndex: 1,
  50. }
  51. cells[rowSpanIndex] = cloneVNode(cell, { style })
  52. }
  53. return cells
  54. }
  55. </script>

同时跨行和跨列

我们当然可以同时使用横跨列与纵跨行来满足您的业务需求!

Virtualized Table 虚拟化表格 2.2.0 - 图17

  1. <template>
  2. <el-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
  3. <template #row="props">
  4. <Row v-bind="props" />
  5. </template>
  6. </el-table-v2>
  7. </template>
  8. <script lang="tsx" setup>
  9. import { cloneVNode } from 'vue'
  10. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  11. Array.from({ length }).map((_, columnIndex) => ({
  12. ...props,
  13. key: `${prefix}${columnIndex}`,
  14. dataKey: `${prefix}${columnIndex}`,
  15. title: `Column ${columnIndex}`,
  16. width: 150,
  17. }))
  18. const generateData = (
  19. columns: ReturnType<typeof generateColumns>,
  20. length = 200,
  21. prefix = 'row-'
  22. ) =>
  23. Array.from({ length }).map((_, rowIndex) => {
  24. return columns.reduce(
  25. (rowData, column, columnIndex) => {
  26. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  27. return rowData
  28. },
  29. {
  30. id: `${prefix}${rowIndex}`,
  31. parentId: null,
  32. }
  33. )
  34. })
  35. const columns = generateColumns(10)
  36. const data = generateData(columns, 200)
  37. const colSpanIndex = 1
  38. columns[colSpanIndex].colSpan = ({ rowIndex }) => (rowIndex % 4) + 1
  39. columns[colSpanIndex].align = 'center'
  40. const rowSpanIndex = 0
  41. columns[rowSpanIndex].rowSpan = ({ rowIndex }) =>
  42. rowIndex % 2 === 0 && rowIndex <= data.length - 2 ? 2 : 1
  43. const Row = ({ rowData, rowIndex, cells, columns }) => {
  44. const colSpan = columns[colSpanIndex].colSpan({ rowData, rowIndex })
  45. if (colSpan > 1) {
  46. let width = Number.parseInt(cells[colSpanIndex].props.style.width)
  47. for (let i = 1; i < colSpan; i++) {
  48. width += Number.parseInt(cells[colSpanIndex + i].props.style.width)
  49. cells[colSpanIndex + i] = null
  50. }
  51. const style = {
  52. ...cells[colSpanIndex].props.style,
  53. width: `${width}px`,
  54. backgroundColor: 'var(--el-color-primary-light-3)',
  55. }
  56. cells[colSpanIndex] = cloneVNode(cells[colSpanIndex], { style })
  57. }
  58. const rowSpan = columns[rowSpanIndex].rowSpan({ rowData, rowIndex })
  59. if (rowSpan > 1) {
  60. const cell = cells[rowSpanIndex]
  61. const style = {
  62. ...cell.props.style,
  63. backgroundColor: 'var(--el-color-danger-light-3)',
  64. height: `${rowSpan * 50}px`,
  65. alignSelf: 'flex-start',
  66. zIndex: 1,
  67. }
  68. cells[rowSpanIndex] = cloneVNode(cell, { style })
  69. } else {
  70. const style = cells[rowSpanIndex].props.style
  71. // override the cell here for creating a pure node without pollute the style
  72. cells[rowSpanIndex] = (
  73. <div style={{ ...style, width: `${style.width}px` }} />
  74. )
  75. }
  76. return cells
  77. }
  78. </script>

树形数据

虚拟化表格当然可以渲染树形数据,您可以通过点击箭头图标来展开/折叠树节点。

Virtualized Table 虚拟化表格 2.2.0 - 图18

  1. <template>
  2. <el-table-v2
  3. v-model:expanded-row-keys="expandedRowKeys"
  4. :columns="columns"
  5. :data="treeData"
  6. :width="700"
  7. :expand-column-key="expandColumnKey"
  8. :height="400"
  9. fixed
  10. @row-expand="onRowExpanded"
  11. @expanded-rows-change="onExpandedRowsChange"
  12. />
  13. </template>
  14. <script lang="ts" setup>
  15. import { computed, ref } from 'vue'
  16. import { TableV2FixedDir } from 'element-plus'
  17. import type { ExpandedRowsChangeHandler, RowExpandHandler } from 'element-plus'
  18. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  19. Array.from({ length }).map((_, columnIndex) => ({
  20. ...props,
  21. key: `${prefix}${columnIndex}`,
  22. dataKey: `${prefix}${columnIndex}`,
  23. title: `Column ${columnIndex}`,
  24. width: 150,
  25. }))
  26. const generateData = (
  27. columns: ReturnType<typeof generateColumns>,
  28. length = 200,
  29. prefix = 'row-'
  30. ) =>
  31. Array.from({ length }).map((_, rowIndex) => {
  32. return columns.reduce(
  33. (rowData, column, columnIndex) => {
  34. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  35. return rowData
  36. },
  37. {
  38. id: `${prefix}${rowIndex}`,
  39. parentId: null,
  40. }
  41. )
  42. })
  43. const columns = generateColumns(10).map((column, columnIndex) => {
  44. let fixed!: TableV2FixedDir
  45. if (columnIndex < 2) fixed = TableV2FixedDir.LEFT
  46. if (columnIndex > 8) fixed = TableV2FixedDir.RIGHT
  47. return { ...column, fixed }
  48. })
  49. const data = generateData(columns, 200)
  50. const expandColumnKey = 'column-0'
  51. // add some sub items
  52. for (let i = 0; i < 50; i++) {
  53. data.push(
  54. {
  55. ...data[0],
  56. id: `${data[0].id}-sub-${i}`,
  57. parentId: data[0].id,
  58. [expandColumnKey]: `Sub ${i}`,
  59. },
  60. {
  61. ...data[2],
  62. id: `${data[2].id}-sub-${i}`,
  63. parentId: data[2].id,
  64. [expandColumnKey]: `Sub ${i}`,
  65. },
  66. {
  67. ...data[2],
  68. id: `${data[2].id}-sub-sub-${i}`,
  69. parentId: `${data[2].id}-sub-${i}`,
  70. [expandColumnKey]: `Sub-Sub ${i}`,
  71. }
  72. )
  73. }
  74. function unflatten(
  75. data: ReturnType<typeof generateData>,
  76. rootId = null,
  77. dataKey = 'id',
  78. parentKey = 'parentId'
  79. ) {
  80. const tree: any[] = []
  81. const childrenMap = {}
  82. for (const datum of data) {
  83. const item = { ...datum }
  84. const id = item[dataKey]
  85. const parentId = item[parentKey]
  86. if (Array.isArray(item.children)) {
  87. childrenMap[id] = item.children.concat(childrenMap[id] || [])
  88. } else if (!childrenMap[id]) {
  89. childrenMap[id] = []
  90. }
  91. item.children = childrenMap[id]
  92. if (parentId !== undefined && parentId !== rootId) {
  93. if (!childrenMap[parentId]) childrenMap[parentId] = []
  94. childrenMap[parentId].push(item)
  95. } else {
  96. tree.push(item)
  97. }
  98. }
  99. return tree
  100. }
  101. const treeData = computed(() => unflatten(data))
  102. const expandedRowKeys = ref<string[]>([])
  103. const onRowExpanded = ({ expanded }: Parameters<RowExpandHandler<any>>[0]) => {
  104. console.log('Expanded:', expanded)
  105. }
  106. const onExpandedRowsChange = (
  107. expandedKeys: Parameters<ExpandedRowsChangeHandler>[0]
  108. ) => {
  109. console.log(expandedKeys)
  110. }
  111. </script>

动态高度行

虚拟表格也支持渲染动态高度的单元格。当不知道一条数据具体的展示高度时,不妨使用动态渲染高度来解决。 通过设置预估行高度 estimated-row-height 来启用此 功能,估计值越接近,渲染效果将会越平滑。

TIP

每行的实际渲染高度是在渲染时动态测量的, 如果您尝试渲染大量数据,渲染界面 可能 会出现抖动。

Virtualized Table 虚拟化表格 2.2.0 - 图19

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :sort-by="sort"
  6. :estimated-row-height="40"
  7. :width="700"
  8. :height="400"
  9. fixed
  10. @column-sort="onColumnSort"
  11. />
  12. </template>
  13. <script lang="tsx" setup>
  14. import { ref } from 'vue'
  15. import {
  16. ElButton,
  17. ElTag,
  18. TableV2FixedDir,
  19. TableV2SortOrder,
  20. } from 'element-plus'
  21. import type { Column, SortBy } from '@element-plus/components/table-v2'
  22. const longText =
  23. 'Quaerat ipsam necessitatibus eum quibusdam est id voluptatem cumque mollitia.'
  24. const midText = 'Corrupti doloremque a quos vero delectus consequatur.'
  25. const shortText = 'Eius optio fugiat.'
  26. const textList = [shortText, midText, longText]
  27. // generate random number in range 0 to 2
  28. let id = 0
  29. const dataGenerator = () => ({
  30. id: `random:${++id}`,
  31. name: 'Tom',
  32. date: '2016-05-03',
  33. description: textList[Math.floor(Math.random() * 3)],
  34. })
  35. const columns: Column<any>[] = [
  36. {
  37. key: 'id',
  38. title: 'Id',
  39. dataKey: 'id',
  40. width: 150,
  41. sortable: true,
  42. fixed: TableV2FixedDir.LEFT,
  43. },
  44. {
  45. key: 'name',
  46. title: 'Name',
  47. dataKey: 'name',
  48. width: 150,
  49. align: 'center',
  50. cellRenderer: ({ cellData: name }) => <ElTag>{name}</ElTag>,
  51. },
  52. {
  53. key: 'description',
  54. title: 'Description',
  55. dataKey: 'description',
  56. width: 150,
  57. cellRenderer: ({ cellData: description }) => (
  58. <div style="padding: 10px 0;">{description}</div>
  59. ),
  60. },
  61. {
  62. key: 'operations',
  63. title: 'Operations',
  64. cellRenderer: () => (
  65. <>
  66. <ElButton size="small">Edit</ElButton>
  67. <ElButton size="small" type="danger">
  68. Delete
  69. </ElButton>
  70. </>
  71. ),
  72. width: 150,
  73. align: 'center',
  74. },
  75. ]
  76. const data = ref(
  77. Array.from({ length: 200 })
  78. .map(dataGenerator)
  79. .sort((a, b) => (a.name > b.name ? 1 : -1))
  80. )
  81. const sort = ref<SortBy>({ key: 'name', order: TableV2SortOrder.ASC })
  82. const onColumnSort = (sortBy: SortBy) => {
  83. const order = sortBy.order === 'asc' ? 1 : -1
  84. const dataClone = [...data.value]
  85. dataClone.sort((a, b) => (a[sortBy.key] > b[sortBy.key] ? order : -order))
  86. sort.value = sortBy
  87. data.value = dataClone
  88. }
  89. </script>

可展开的附加信息

通过动态高度渲染,我们可以在表格中显示可展开的更多附加信息。

Virtualized Table 虚拟化表格 2.2.0 - 图20

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :estimated-row-height="50"
  6. :expand-column-key="columns[0].key"
  7. :width="700"
  8. :height="400"
  9. >
  10. <template #row="props">
  11. <Row v-bind="props" />
  12. </template>
  13. </el-table-v2>
  14. </template>
  15. <script lang="tsx" setup>
  16. import { ref } from 'vue'
  17. const detailedText = `Velit sed aspernatur tempora. Natus consequatur officiis dicta vel assumenda.
  18. Itaque est temporibus minus quis. Ipsum commodiab porro vel voluptas illum.
  19. Qui quam nulla et dolore autem itaque est.
  20. Id consequatur ipsum ea fuga et odit eligendi impedit.
  21. Maiores officiis occaecati et magnam et sapiente est velit sunt.
  22. Non et tempore temporibus. Excepturi et quos. Minus distinctio aut.
  23. Voluptatem ea excepturi omnis vel. Non aperiam sit sed laboriosam eaque omnis deleniti.
  24. Est molestiae omnis non et nulla repudiandae fuga sit.`
  25. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  26. Array.from({ length }).map((_, columnIndex) => ({
  27. ...props,
  28. key: `${prefix}${columnIndex}`,
  29. dataKey: `${prefix}${columnIndex}`,
  30. title: `Column ${columnIndex}`,
  31. width: 150,
  32. }))
  33. const generateData = (
  34. columns: ReturnType<typeof generateColumns>,
  35. length = 200,
  36. prefix = 'row-'
  37. ) =>
  38. Array.from({ length }).map((_, rowIndex) => {
  39. return columns.reduce(
  40. (rowData, column, columnIndex) => {
  41. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  42. return rowData
  43. },
  44. {
  45. id: `${prefix}${rowIndex}`,
  46. parentId: null,
  47. }
  48. )
  49. })
  50. const columns = generateColumns(10)
  51. const data = ref(
  52. generateData(columns, 200).map((data) => {
  53. data.children = [
  54. {
  55. id: `${data.id}-detail-content`,
  56. detail: detailedText,
  57. },
  58. ]
  59. return data
  60. })
  61. )
  62. const Row = ({ cells, rowData }) => {
  63. if (rowData.detail) return <div class="p-6">{rowData.detail}</div>
  64. return cells
  65. }
  66. Row.inheritAttrs = false
  67. </script>
  68. <style>
  69. .el-table-v2__row-depth-0 {
  70. height: 50px;
  71. }
  72. .el-table-v2__cell-text {
  73. overflow: hidden;
  74. text-overflow: ellipsis;
  75. white-space: nowrap;
  76. }
  77. </style>

自定义页脚

自定义表格 footer, 通常用来展示一些汇总数据和信息。

Virtualized Table 虚拟化表格 2.2.0 - 图21

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :row-height="40"
  6. :width="700"
  7. :height="400"
  8. :footer-height="50"
  9. fixed
  10. >
  11. <template #footer
  12. ><div
  13. class="flex items-center"
  14. style="
  15. justify-content: center;
  16. height: 100%;
  17. background-color: var(--el-color-primary-light-7);
  18. "
  19. >
  20. Display a message in the footer
  21. </div>
  22. </template>
  23. </el-table-v2>
  24. </template>
  25. <script lang="ts" setup>
  26. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  27. Array.from({ length }).map((_, columnIndex) => ({
  28. ...props,
  29. key: `${prefix}${columnIndex}`,
  30. dataKey: `${prefix}${columnIndex}`,
  31. title: `Column ${columnIndex}`,
  32. width: 150,
  33. }))
  34. const generateData = (
  35. columns: ReturnType<typeof generateColumns>,
  36. length = 200,
  37. prefix = 'row-'
  38. ) =>
  39. Array.from({ length }).map((_, rowIndex) => {
  40. return columns.reduce(
  41. (rowData, column, columnIndex) => {
  42. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  43. return rowData
  44. },
  45. {
  46. id: `${prefix}${rowIndex}`,
  47. parentId: null,
  48. }
  49. )
  50. })
  51. const columns = generateColumns(10)
  52. const data = generateData(columns, 200)
  53. </script>

自定义空元素渲染器

渲染自定义的空元素

Virtualized Table 虚拟化表格 2.2.0 - 图22

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="[]"
  5. :row-height="40"
  6. :width="700"
  7. :height="400"
  8. :footer-height="50"
  9. >
  10. <template #empty>
  11. <div class="flex items-center justify-center h-100%">
  12. <el-empty />
  13. </div>
  14. </template>
  15. </el-table-v2>
  16. </template>
  17. <script lang="tsx" setup>
  18. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  19. Array.from({ length }).map((_, columnIndex) => ({
  20. ...props,
  21. key: `${prefix}${columnIndex}`,
  22. dataKey: `${prefix}${columnIndex}`,
  23. title: `Column ${columnIndex}`,
  24. width: 150,
  25. }))
  26. const columns = generateColumns(10)
  27. </script>

浮动遮罩层

当您想要显示加载指示器之类的浮动元素,可以通过渲染一个浮动在表格之上的遮罩层来实现。

Virtualized Table 虚拟化表格 2.2.0 - 图23

  1. <template>
  2. <el-table-v2
  3. :columns="columns"
  4. :data="data"
  5. :row-height="40"
  6. :width="700"
  7. :height="400"
  8. >
  9. <template #overlay>
  10. <div
  11. class="el-loading-mask"
  12. style="display: flex; align-items: center; justify-content: center"
  13. >
  14. <el-icon class="is-loading" color="var(--el-color-primary)" :size="26">
  15. <loading-icon />
  16. </el-icon>
  17. </div>
  18. </template>
  19. </el-table-v2>
  20. </template>
  21. <script lang="ts" setup>
  22. import { Loading as LoadingIcon } from '@element-plus/icons-vue'
  23. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  24. Array.from({ length }).map((_, columnIndex) => ({
  25. ...props,
  26. key: `${prefix}${columnIndex}`,
  27. dataKey: `${prefix}${columnIndex}`,
  28. title: `Column ${columnIndex}`,
  29. width: 150,
  30. }))
  31. const generateData = (
  32. columns: ReturnType<typeof generateColumns>,
  33. length = 200,
  34. prefix = 'row-'
  35. ) =>
  36. Array.from({ length }).map((_, rowIndex) => {
  37. return columns.reduce(
  38. (rowData, column, columnIndex) => {
  39. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  40. return rowData
  41. },
  42. {
  43. id: `${prefix}${rowIndex}`,
  44. parentId: null,
  45. }
  46. )
  47. })
  48. const columns = generateColumns(10)
  49. const data = generateData(columns, 200)
  50. </script>
  51. <style>
  52. .example-showcase .el-table-v2__overlay {
  53. z-index: 9;
  54. }
  55. </style>

手动滚动

使用 Table V2 暴露的方法进行手动或编程式滚动指定偏移或行。

TIP

scrollToRow 的第二个参数代表滚动策略,计算了要滚动的位置,其默认值是 auto。 如果你想要表格滚动到某个特定位置,可以额外配置。 可用的选项是 "auto" | "center" | "end" | "start" | "smart"

smartauto 之间的区别是, autosmart 滚动策略的子集。

Virtualized Table 虚拟化表格 2.2.0 - 图24

  1. <template>
  2. <div class="mb-4 flex items-center">
  3. <el-form-item label="Scroll pixels" class="mr-4">
  4. <el-input v-model="scrollDelta" />
  5. </el-form-item>
  6. <el-form-item label="Scroll rows">
  7. <el-input v-model="scrollRows" />
  8. </el-form-item>
  9. </div>
  10. <div class="mb-4 flex items-center">
  11. <el-button @click="scrollByPixels"> Scroll by pixels </el-button>
  12. <el-button @click="scrollByRows"> Scroll by rows </el-button>
  13. </div>
  14. <div style="height: 400px">
  15. <el-auto-resizer>
  16. <template #default="{ height, width }">
  17. <el-table-v2
  18. ref="tableRef"
  19. :columns="columns"
  20. :data="data"
  21. :width="width"
  22. :height="height"
  23. fixed
  24. />
  25. </template>
  26. </el-auto-resizer>
  27. </div>
  28. </template>
  29. <script lang="ts" setup>
  30. import { ref } from 'vue'
  31. import type { TableV2Instance } from 'element-plus'
  32. const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
  33. Array.from({ length }).map((_, columnIndex) => ({
  34. ...props,
  35. key: `${prefix}${columnIndex}`,
  36. dataKey: `${prefix}${columnIndex}`,
  37. title: `Column ${columnIndex}`,
  38. width: 150,
  39. }))
  40. const generateData = (
  41. columns: ReturnType<typeof generateColumns>,
  42. length = 200,
  43. prefix = 'row-'
  44. ) =>
  45. Array.from({ length }).map((_, rowIndex) => {
  46. return columns.reduce(
  47. (rowData, column, columnIndex) => {
  48. rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
  49. return rowData
  50. },
  51. {
  52. id: `${prefix}${rowIndex}`,
  53. parentId: null,
  54. }
  55. )
  56. })
  57. const columns = generateColumns(10)
  58. const data = generateData(columns, 200)
  59. const tableRef = ref<TableV2Instance>()
  60. const scrollDelta = ref(200)
  61. const scrollRows = ref(10)
  62. function scrollByPixels() {
  63. tableRef.value?.scrollToTop(scrollDelta.value)
  64. }
  65. function scrollByRows() {
  66. tableRef.value?.scrollToRow(scrollRows.value)
  67. }
  68. </script>

TableV2 属性

属性描述说明类型默认值
cache为了更好的渲染效果预先多加载的行数Number2
estimated-row-height渲染动态的单元格的预估高度Number-
header-classheader 部分的自定义 class 名String/Function<HeaderClassGetter>-
header-propsheader 部分的自定义 props 名Object/Function<HeaderPropsGetter>-
header-cell-propsheader cell 部分的自定义 props 名Object/Function<HeaderCellPropsGetter>-
header-heightheader 部分的高度。当传入数组时, 将会渲染和数组长度一样多的 header 行Number/Array<Number>50
footer-heightfooter 部分的高度,当传入值时,这部分将被计算入 table 的高度里Number0
row-classrow wrapper 部分的自定义 class 名String/Function<RowClassGetter>-
row-key每行的 key 值,如果不提供,将使用索引 index 代替String/Symbol/Numberid
row-propsrow component 部分的自定义 class 名Object/Function<RowPropsGetter>-
row-height每行的高度, 用于计算表的总高度Number50
cell-props每个单元格 cell 的自定义 props (除了 header cell 以外)Object/Function<CellPropsGetter>-
columns列 column 的配置数组Array<Column>-
data要在表中渲染的数据数组Array<Data>[]
data-getter自定义数据源转换成表格消费的数据格式的处理函数Function-
fixed-data渲染行在表格主内容上方和 header 下方区域的数据Array<Data>-
expand-column-key列的 key 来标记哪个行可以被展开String-
expanded-row-keys存放行展开状态的 key 的数组,可以和 v-model 搭配使用Array<KeyType>-
default-expanded-row-keys默认展开的行的 key 的数组, 这个数据不是响应式的Array<KeyType>-
class表格的类名称,将应用于表格的全部的三个部分 (左、右、主)String/Array/Object-
fixed单元格宽度是自适应还是固定Booleanfalse
width 表的宽度,必填Number-
height 表的高度,必填Number-
max-height表格的最大高度Number-
h-scrollbar-size配置表格的水平滚动条大小,防止水平和垂直滚动条重叠。Number6
v-scrollbar-size配置表格的水平滚动条大小,防止水平和垂直滚动条重叠。Number6
scrollbar-always-on如果开启,滚动条将一直显示,反之只会在鼠标经过时显示。Booleanfalse
sort-by排序方式Object<SortBy>{}
sort-state多个排序Object<SortState>undefined

TableV2 插槽

插槽名参数
cellCellSlotProps
headerHeaderSlotProps
header-cellHeaderCellSlotProps
rowRowSlotProps
footer-
empty-
overlay-

Table 事件

事件名描述参数
column-sort列排序时调用Object<ColumnSortParam>
expanded-rows-change行展开状态改变时触发Array<KeyType>
end-reached到达表格末尾时触发-
scroll表格被用户滚动后触发Object<ScrollParams>
rows-rendered当行被渲染后触发Object<RowsRenderedParams>
row-event-handlers当行被添加了 EventHandler 时触发Object<RowEventHandlers>

Table 方法

事件名描述参数
scrollToScroll to a given position{ scrollLeft?: number, scrollTop?: number}
scrollToLeftScroll to a given horizontal positionscrollLeft: number
scrollToTopScroll to a given vertical positionscrollTop: number
scrollToRowscroll to a given row with specified scroll strategyrow: number, strategy?: “auto” |”center” | “end” | “start” | “smart”

TIP

请注意:这些是 JavaScript 对象,所以您 不能使用 短横线命名法(kebab-case)来处理这些属性

Column 属性

属性描述类型默认值
align表格单元格内容对齐方式Alignment Virtualized Table 虚拟化表格 2.2.0 - 图25left
class列的类名String-
fixed固定列位置Boolean/ FixedDir Virtualized Table 虚拟化表格 2.2.0 - 图26false
flexGrowCSS 属性 flex grow, 仅当不是固定表时才生效Number0
flexShrinkCSS 属性 flex shrink, 仅当不是固定表时才生效Number1
headerClass自定义 header 头部类名String-
hidden此列是否不可见Boolean-
style自定义列单元格的类名,将会与 gird 单元格合并CSSProperties-
sortable设置列是否可排序Boolean-
titleHeader 头部单元格中的默认文本String-
maxWidth列的最大宽度String-
minWidth列的最小宽度String-
width *列的宽度 必填Number-
cellRenderer自定义单元格渲染器VueComponent/(props: CellRenderProps) => VNode-
headerRenderer自定义头部渲染器VueComponent/(props: HeaderRenderProps) => VNode-

类型

显示类型声明

  1. type HeaderClassGetter = (param: {
  2. columns: Column<any>[]
  3. headerIndex: number
  4. }) => string
  5. type HeaderPropsGetter = (param: {
  6. columns: Column<any>[]
  7. headerIndex: number
  8. }) => Record<string, any>
  9. type HeaderCellPropsGetter = (param: {
  10. columns: Column<any>[]
  11. column: Column<any>
  12. columnIndex: number
  13. headerIndex: number
  14. style: CSSProperties
  15. }) => Record<string, any>
  16. type RowClassGetter = (param: {
  17. columns: Column<any>[]
  18. rowData: any
  19. rowIndex: number
  20. }) => string
  21. type RowPropsGetter = (param: {
  22. columns: Column<any>[]
  23. rowData: any
  24. rowIndex: number
  25. }) => Record<string, any>
  26. type CellPropsGetter = (param: {
  27. column: Column<any>
  28. columns: Column<any>[]
  29. columnIndex: number
  30. cellData: any
  31. rowData: any
  32. rowIndex: number
  33. }) => void
  34. type CellRenderProps<T> = {
  35. cellData: T
  36. column: Column<T>
  37. columns: Column<T>[]
  38. columnIndex: number
  39. rowData: any
  40. rowIndex: number
  41. }
  42. type HeaderRenderProps<T> = {
  43. column: Column<T>
  44. columns: Column<T>[]
  45. columnIndex: number
  46. headerIndex: number
  47. }
  48. type ScrollParams = {
  49. xAxisScrollDir: 'forward' | 'backward'
  50. scrollLeft: number
  51. yAxisScrollDir: 'forward' | 'backward'
  52. scrollTop: number
  53. }
  54. type CellSlotProps<T> = {
  55. column: Column<T>
  56. columns: Column<T>[]
  57. columnIndex: number
  58. depth: number
  59. style: CSSProperties
  60. rowData: any
  61. rowIndex: number
  62. isScrolling: boolean
  63. expandIconProps?:
  64. | {
  65. rowData: any
  66. rowIndex: number
  67. onExpand: (expand: boolean) => void
  68. }
  69. | undefined
  70. }
  71. type HeaderSlotProps = {
  72. cells: VNode[]
  73. columns: Column<any>[]
  74. headerIndex: number
  75. }
  76. type HeaderCellSlotProps = {
  77. class: string
  78. columns: Column<any>[]
  79. column: Column<any>
  80. columnIndex: number
  81. headerIndex: number
  82. style: CSSProperties
  83. headerCellProps?: any
  84. sortBy: SortBy
  85. sortState?: SortState | undefined
  86. onColumnSorted: (e: MouseEvent) => void
  87. }
  88. type RowSlotProps = {
  89. columnIndex: number
  90. rowIndex: number
  91. data: any
  92. key: number | string
  93. isScrolling?: boolean | undefined
  94. style: CSSProperties
  95. }
  96. type Data = {
  97. [key: KeyType]: any
  98. children?: Array<any>
  99. }
  100. type FixedData = Data
  101. type KeyType = string | number | symbol
  102. type ColumnSortParam<T> = { column: Column<T>; key: KeyType; order: SortOrder }
  103. enum SortOrder {
  104. ASC = 'asc',
  105. DESC = 'desc',
  106. }
  107. type SortBy = { key: KeyType; Order: SortOrder }
  108. type SortState = Record<KeyType, SortOrder>

常见问题

如何在第一列中渲染带复选框的列表?

由于可以自己定义单元格渲染器,您可以根据示例 自定义单元格渲染器 代码来渲染 checkbox,并自行管理其状态。

为什么虚拟化表提供的功能较 TableV1 少?

对于虚拟化表,我们打算减少一些功能,让用户根据需求自行实现。 整合过多的功能会让组件的代码变得难以维护,且对于大多数用户来说,基础功能就已足够。 一些主要的功能尚未开发。 我们很希望听从您的意见。 加入 Discord 频道 Virtualized Table 虚拟化表格 2.2.0 - 图27 来跟上我们最新的进展!

源代码

组件 Virtualized Table 虚拟化表格 2.2.0 - 图28 文档 Virtualized Table 虚拟化表格 2.2.0 - 图29

贡献者

Virtualized Table 虚拟化表格 2.2.0 - 图30 JeremyWuuuuu

Virtualized Table 虚拟化表格 2.2.0 - 图31 btea

Virtualized Table 虚拟化表格 2.2.0 - 图32 云游君

Virtualized Table 虚拟化表格 2.2.0 - 图33 Xc

Virtualized Table 虚拟化表格 2.2.0 - 图34 zz

Virtualized Table 虚拟化表格 2.2.0 - 图35 三咲智子

Virtualized Table 虚拟化表格 2.2.0 - 图36 webfansplz

Virtualized Table 虚拟化表格 2.2.0 - 图37 qiang

Virtualized Table 虚拟化表格 2.2.0 - 图38 Kim Yang

Virtualized Table 虚拟化表格 2.2.0 - 图39 SignDawn

Virtualized Table 虚拟化表格 2.2.0 - 图40 kooriookami