虚拟化表格 beta
随着 Web 演进,表格一直是仪表盘、数据分析等场景中最常用的组件之一。对 Table V1 而言,即便只有约 1000 行数据,也可能因性能问题而难以使用。
虚拟化表格可以在极短时间内渲染大量数据。
TIP
本组件仍处于测试阶段,请谨慎在生产环境使用。若发现问题,欢迎在 GitHub 反馈。文档未列出的 API 中,部分尚未完成开发,因此暂未说明。
即便虚拟化表格效率很高,当单次数据量过大时,网络带宽与内存仍可能成为瓶颈。请结合分页、筛选等手段使用,勿将其视为万能方案。
基础用法
下面用 10 列、1000 行的示例展示虚拟化表格的性能。
<template>
<u-table-v2
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
/>
</template>
<script lang="ts" setup>
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 1000)
</script>
自适应尺寸
不想手动传入表格的 width 与 height 时,可用 AutoResizer 包裹表格组件,由它自动更新宽高。
调整浏览器窗口大小即可观察效果。
TIP
请确保 AutoResizer 的父节点具有固定高度,因为其默认高度为 100%;也可通过给 AutoResizer 设置 style 明确高度。
<template>
<div style="height: 400px">
<u-auto-resizer>
<template #default="{ height, width }">
<u-table-v2
:columns="columns"
:data="data"
:width="width"
:height="height"
fixed
/>
</template>
</u-auto-resizer>
</div>
</template>
<script lang="ts" setup>
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
</script>
自定义单元格渲染
可按业务需求自定义单元格内容,下面是一个简单示例。
<template>
<u-table-v2
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
/>
</template>
<script lang="tsx" setup>
import { ref } from 'vue'
import dayjs from 'dayjs'
import { TableV2FixedDir, UButton, UIcon, UTag, UTooltip } from 'uniboot-ui'
import { Timer } from '@uniboot/icons-vue'
import type { Column } from 'uniboot-ui'
let id = 0
const dataGenerator = () => ({
id: `random-id-${++id}`,
name: 'Tom',
date: '2020-10-1',
})
const columns: Column<any>[] = [
{
key: 'date',
title: 'Date',
dataKey: 'date',
width: 150,
fixed: TableV2FixedDir.LEFT,
cellRenderer: ({ cellData: date }) => (
<UTooltip content={dayjs(date).format('YYYY/MM/DD')}>
{
<span class="flex items-center">
<UIcon class="mr-3">
<Timer />
</UIcon>
{dayjs(date).format('YYYY/MM/DD')}
</span>
}
</UTooltip>
),
},
{
key: 'name',
title: 'Name',
dataKey: 'name',
width: 150,
align: 'center',
cellRenderer: ({ cellData: name }) => <UTag>{name}</UTag>,
},
{
key: 'operations',
title: 'Operations',
cellRenderer: () => (
<>
<UButton size="small">Edit</UButton>
<UButton size="small" type="danger">
Delete
</UButton>
</>
),
width: 150,
align: 'center',
},
]
const data = ref(Array.from({ length: 200 }).map(dataGenerator))
</script>
带选择
通过自定义单元格渲染实现行选择。
<template>
<div style="height: 400px">
<u-auto-resizer>
<template #default="{ height, width }">
<u-table-v2
:columns="columns"
:data="data"
:width="width"
:height="height"
fixed
/>
</template>
</u-auto-resizer>
</div>
</template>
<script lang="tsx" setup>
import { ref, unref } from 'vue'
import { UCheckbox, useLocale } from 'uniboot-ui'
import type { FunctionalComponent } from 'vue'
import type { CheckboxValueType, Column } from 'uniboot-ui'
type SelectionCellProps = {
value: boolean
intermediate?: boolean
ariaLabel?: string
onChange: (value: CheckboxValueType) => void
}
const { t } = useLocale()
const SelectionCell: FunctionalComponent<SelectionCellProps> = ({
value,
intermediate = false,
ariaLabel,
onChange,
}) => {
return (
<UCheckbox
onChange={onChange}
modelValue={value}
ariaLabel={ariaLabel}
indeterminate={intermediate}
/>
)
}
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
checked: false,
parentId: null,
}
)
})
const columns: Column<any>[] = generateColumns(10)
columns.unshift({
key: 'selection',
width: 50,
cellRenderer: ({ rowData }) => {
const onChange = (value: CheckboxValueType) => (rowData.checked = value)
return (
<SelectionCell
value={rowData.checked}
ariaLabel={t('uniboot.table.selectRowLabel')}
onChange={onChange}
/>
)
},
headerCellRenderer: () => {
const _data = unref(data)
const onChange = (value: CheckboxValueType) =>
(data.value = _data.map((row) => {
row.checked = value
return row
}))
const allSelected = _data.every((row) => row.checked)
const containsChecked = _data.some((row) => row.checked)
return (
<SelectionCell
value={allSelected}
intermediate={containsChecked && !allSelected}
ariaLabel={t('uniboot.table.selectAllLabel')}
onChange={onChange}
/>
)
},
})
const data = ref(generateData(columns, 200))
</script>
行内编辑
与选择示例类似,可用同样方式为表格开启行内编辑。
<template>
<div style="height: 400px">
<u-auto-resizer>
<template #default="{ height, width }">
<u-table-v2
:columns="columns"
:data="data"
:width="width"
:height="height"
fixed
/>
</template>
</u-auto-resizer>
</div>
</template>
<script lang="tsx" setup>
import { ref, withKeys } from 'vue'
import { UInput } from 'uniboot-ui'
import type { FunctionalComponent } from 'vue'
import type { Column, InputInstance } from 'uniboot-ui'
type SelectionCellProps = {
value: string
intermediate?: boolean
onChange: (value: string) => void
onBlur: () => void
onKeydownEnter: () => void
forwardRef: (el: InputInstance) => void
}
const InputCell: FunctionalComponent<SelectionCellProps> = ({
value,
onChange,
onBlur,
onKeydownEnter,
forwardRef,
}) => {
return (
<UInput
ref={forwardRef as any}
onInput={onChange}
onBlur={onBlur}
onKeydown={withKeys(onKeydownEnter, ['enter'])}
modelValue={value}
/>
)
}
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
editing: false,
parentId: null,
}
)
})
const columns: Column<any>[] = generateColumns(10)
columns[0] = {
...columns[0],
title: 'Editable Column',
cellRenderer: ({ rowData, column }) => {
const onChange = (value: string) => {
rowData[column.dataKey!] = value
}
const onEnterEditMode = () => {
rowData.editing = true
}
const onExitEditMode = () => (rowData.editing = false)
const input = ref()
const setRef = (el) => {
input.value = el
if (el) {
el.focus?.()
}
}
return rowData.editing ? (
<InputCell
forwardRef={setRef}
value={rowData[column.dataKey!]}
onChange={onChange}
onBlur={onExitEditMode}
onKeydownEnter={onExitEditMode}
/>
) : (
<div class="table-v2-inline-editing-trigger" onClick={onEnterEditMode}>
{rowData[column.dataKey!]}
</div>
)
},
}
const data = ref(generateData(columns, 200))
</script>
<style>
.table-v2-inline-editing-trigger {
border: 1px transparent dotted;
padding: 4px;
}
.table-v2-inline-editing-trigger:hover {
border-color: var(--u-color-primary);
}
</style>
带状态样式
可用不同样式区分成功、信息、警告、危险等状态。
通过 row-class-name 自定义行样式,例如每第 10 行使用 bg-blue-200,每第 5 行使用 bg-red-100。
<template>
<u-table-v2
:columns="columns"
:data="data"
:row-class="rowClass"
:width="700"
:height="400"
/>
</template>
<script lang="tsx" setup>
import { ref } from 'vue'
import dayjs from 'dayjs'
import { TableV2FixedDir, UButton, UIcon, UTag, UTooltip } from 'uniboot-ui'
import { Timer } from '@uniboot/icons-vue'
import type { Column, RowClassNameGetter } from 'uniboot-ui'
let id = 0
const dataGenerator = () => ({
id: `random-id-${++id}`,
name: 'Tom',
date: '2020-10-1',
})
const columns: Column<any>[] = [
{
key: 'date',
title: 'Date',
dataKey: 'date',
width: 150,
fixed: TableV2FixedDir.LEFT,
cellRenderer: ({ cellData: date }) => (
<UTooltip content={dayjs(date).format('YYYY/MM/DD')}>
{
<span class="flex items-center">
<UIcon class="mr-3">
<Timer />
</UIcon>
{dayjs(date).format('YYYY/MM/DD')}
</span>
}
</UTooltip>
),
},
{
key: 'name',
title: 'Name',
dataKey: 'name',
width: 150,
align: 'center',
cellRenderer: ({ cellData: name }) => <UTag>{name}</UTag>,
},
{
key: 'operations',
title: 'Operations',
cellRenderer: () => (
<>
<UButton size="small">Edit</UButton>
<UButton size="small" type="danger">
Delete
</UButton>
</>
),
width: 150,
align: 'center',
flexGrow: 1,
},
]
const data = ref(Array.from({ length: 200 }).map(dataGenerator))
const rowClass = ({ rowIndex }: Parameters<RowClassNameGetter<any>>[0]) => {
if (rowIndex % 10 === 5) {
return 'bg-red-100'
} else if (rowIndex % 10 === 0) {
return 'bg-blue-200'
}
return ''
}
</script>
粘性行
可将部分行吸附在表格顶部,使用 fixed-data 即可。
本例结合滚动事件动态更新粘性行数据。
<template>
<u-table-v2
:columns="columns"
:data="tableData"
:fixed-data="fixedData"
:width="700"
:height="400"
:row-class="rowClass"
fixed
@scroll="onScroll"
/>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const rowClass = ({ rowIndex }) => {
if (rowIndex < 0 || (rowIndex + 1) % 5 === 0) return 'sticky-row'
}
const stickyIndex = ref(0)
const fixedData = computed(() =>
data.slice(stickyIndex.value, stickyIndex.value + 1)
)
const tableData = computed(() => {
return data.slice(1)
})
const onScroll = ({ scrollTop }) => {
stickyIndex.value = Math.floor(scrollTop / 250) * 5
}
</script>
<style>
.u-table-v2__fixed-header-row {
background-color: var(--u-color-primary-light-5);
font-weight: bold;
}
</style>
固定列
若需将列固定在左侧或右侧,可为列设置相应属性。
将列的 fixed 设为 true(等价于 FixedDir.LEFT),或显式使用 FixedDir.LEFT / FixedDir.RIGHT。
<template>
<u-table-v2
:columns="columns"
:data="data"
:sort-by="sortBy"
:width="700"
:height="400"
fixed
@column-sort="onSort"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { TableV2FixedDir, TableV2SortOrder } from 'uniboot-ui'
import type { SortBy } from 'uniboot-ui'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
let data = generateData(columns, 200)
columns[0].fixed = true
columns[1].fixed = TableV2FixedDir.LEFT
columns[9].fixed = TableV2FixedDir.RIGHT
for (let i = 0; i < 3; i++) columns[i].sortable = true
const sortBy = ref<SortBy>({
key: 'column-0',
order: TableV2SortOrder.ASC,
})
const onSort = (_sortBy: SortBy) => {
data = data.reverse()
sortBy.value = _sortBy
}
</script>
多级表头
通过自定义表头渲染器实现分组表头,如下例。
TIP
本例使用了文档演练场不支持的 JSX,请在本地或 codesandbox 等在线环境尝试。
由于涉及 VNode 操作,建议用 JSX 编写表格相关代码。
<template>
<u-table-v2
fixed
:columns="fixedColumns"
:data="data"
:header-height="[50, 40, 50]"
:header-class="headerClass"
:width="700"
:height="400"
>
<template #header="props">
<customized-header v-bind="props" />
</template>
</u-table-v2>
</template>
<script lang="tsx" setup>
import { TableV2FixedDir, TableV2Placeholder } from 'uniboot-ui'
import type { FunctionalComponent } from 'vue'
import type {
HeaderClassNameGetter,
TableV2CustomizedHeaderSlotParam,
} from 'uniboot-ui'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(15)
const data = generateData(columns, 200)
const fixedColumns = columns.map((column, columnIndex) => {
let fixed: TableV2FixedDir | undefined = undefined
if (columnIndex < 3) fixed = TableV2FixedDir.LEFT
if (columnIndex > 12) fixed = TableV2FixedDir.RIGHT
return { ...column, fixed, width: 100 }
})
const CustomizedHeader: FunctionalComponent<
TableV2CustomizedHeaderSlotParam
> = ({ cells, columns, headerIndex }) => {
if (headerIndex === 2) return cells
const groupCells = [] as typeof cells
let width = 0
let idx = 0
columns.forEach((column, columnIndex) => {
if (column.placeholderSign === TableV2Placeholder)
groupCells.push(cells[columnIndex])
else {
width += cells[columnIndex].props!.column.width
idx++
const nextColumn = columns[columnIndex + 1]
if (
columnIndex === columns.length - 1 ||
nextColumn.placeholderSign === TableV2Placeholder ||
idx === (headerIndex === 0 ? 4 : 2)
) {
groupCells.push(
<div
class="flex items-center justify-center custom-header-cell"
role="columnheader"
style={{
...cells[columnIndex].props!.style,
width: `${width}px`,
}}
>
Group width {width}
</div>
)
width = 0
idx = 0
}
}
})
return groupCells
}
const headerClass = ({
headerIndex,
}: Parameters<HeaderClassNameGetter<any>>[0]) => {
if (headerIndex === 1) return 'u-primary-color'
return ''
}
</script>
<style>
.u-table-v2__header-row .custom-header-cell {
border-right: 1px solid var(--u-border-color);
}
.u-table-v2__header-row .custom-header-cell:last-child {
border-right: none;
}
.u-primary-color {
background-color: var(--u-color-primary);
color: var(--u-color-white);
font-size: 14px;
font-weight: bold;
}
.u-primary-color .custom-header-cell {
padding: 0 4px;
}
</style>
筛选
虚拟化表格提供自定义表头渲染,可据此实现列筛选。
<template>
<u-table-v2
fixed
:columns="fixedColumns"
:data="data"
:width="700"
:height="400"
/>
</template>
<script lang="tsx" setup>
import { computed, ref } from 'vue'
import {
TableV2FixedDir,
UButton,
UCheckbox,
UIcon,
UPopover,
useLocale,
} from 'uniboot-ui'
import { Filter } from '@uniboot/icons-vue'
import type { HeaderCellSlotProps } from 'uniboot-ui'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = ref(generateData(columns, 200))
const { t } = useLocale()
const shouldFilter = ref(false)
const popoverRef = ref()
const ariaLabel = computed(() => {
return t('uniboot.table.filterLabel', { column: columns[0].title })
})
const onFilter = () => {
popoverRef.value.hide()
if (shouldFilter.value) {
data.value = generateData(columns, 100, 'filtered-')
} else {
data.value = generateData(columns, 200)
}
}
const onReset = () => {
shouldFilter.value = false
onFilter()
}
const handleShowPopover = () => {
const button = document.querySelector('.u-table-v2__demo-filter button')
;(button as HTMLElement)?.focus()
}
columns[0].headerCellRenderer = (props: HeaderCellSlotProps) => {
return (
<div class="flex items-center justify-center">
<span class="mr-2 text-xs">{props.column.title}</span>
<UPopover
ref={popoverRef}
trigger="click"
width={200}
onAfter-enter={handleShowPopover}
>
{{
default: () => (
<div class="filter-wrapper">
<div class="filter-group">
<UCheckbox v-model={shouldFilter.value}>Filter Text</UCheckbox>
</div>
<div class="u-table-v2__demo-filter">
<UButton text onClick={onFilter}>
Confirm
</UButton>
<UButton text onClick={onReset}>
Reset
</UButton>
</div>
</div>
),
reference: () => (
<button
type="button"
aria-label={ariaLabel.value}
class="u-table-v2__demo-filter-btn"
>
<UIcon size={14}>
<Filter />
</UIcon>
</button>
),
}}
</UPopover>
</div>
)
}
const fixedColumns = columns.map((column, columnIndex) => {
let fixed: TableV2FixedDir | undefined = undefined
if (columnIndex < 2) fixed = TableV2FixedDir.LEFT
if (columnIndex > 9) fixed = TableV2FixedDir.RIGHT
return { ...column, fixed, width: 100 }
})
</script>
<style>
.u-table-v2__demo-filter {
border-top: var(--u-border);
margin: 12px -12px -12px;
padding: 0 12px;
display: flex;
justify-content: space-between;
}
.u-table-v2__demo-filter-btn {
display: flex;
cursor: pointer;
padding: 0;
margin: 0;
background-color: transparent;
appearance: none;
border: none;
}
</style>
可排序
可通过排序状态对表格排序。
<template>
<u-table-v2
:columns="columns"
:data="data"
:sort-by="sortState"
:width="700"
:height="400"
fixed
@column-sort="onSort"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { TableV2SortOrder } from 'uniboot-ui'
import type { SortBy } from 'uniboot-ui'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
let data = generateData(columns, 200)
columns[0].sortable = true
const sortState = ref<SortBy>({
key: 'column-0',
order: TableV2SortOrder.ASC,
})
const onSort = (sortBy: SortBy) => {
console.log(sortBy)
data = data.reverse()
sortState.value = sortBy
}
</script>
受控排序
可按需定义多列可排序。若多列同时可排序,界面可能让用户难以判断当前生效的排序列。
<template>
<u-table-v2
v-model:sort-state="sortState"
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
@column-sort="onSort"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { TableV2SortOrder } from 'uniboot-ui'
import type { SortBy, SortState } from 'uniboot-ui'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = ref(generateData(columns, 200))
columns[0].sortable = true
columns[1].sortable = true
const sortState = ref<SortState>({
'column-0': TableV2SortOrder.DESC,
'column-1': TableV2SortOrder.ASC,
})
const onSort = ({ key, order }: SortBy) => {
sortState.value[key] = order
data.value = data.value.reverse()
}
</script>
十字高亮
大数据量下容易迷失当前行列,可使用十字高亮辅助定位。
<template>
<div style="height: 400px">
<u-auto-resizer>
<template #default="{ height, width }">
<u-table-v2
:columns="columns"
:cell-props="cellProps"
:class="kls"
:data="data"
:width="width"
:height="height"
/>
</template>
</u-auto-resizer>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
columns.unshift({
key: 'column-n-1',
width: 50,
title: 'Row No.',
cellRenderer: ({ rowIndex }) => `${rowIndex + 1}`,
align: 'center',
})
const data = generateData(columns, 200)
const cellProps = ({ columnIndex }) => {
const key = `hovering-col-${columnIndex}`
return {
['data-key']: key,
onMouseenter: () => {
kls.value = key
},
onMouseleave: () => {
kls.value = ''
},
}
}
const kls = ref<string>('')
</script>
<style>
.hovering-col-0 [data-key='hovering-col-0'],
.hovering-col-1 [data-key='hovering-col-1'],
.hovering-col-2 [data-key='hovering-col-2'],
.hovering-col-3 [data-key='hovering-col-3'],
.hovering-col-4 [data-key='hovering-col-4'],
.hovering-col-5 [data-key='hovering-col-5'],
.hovering-col-6 [data-key='hovering-col-6'],
.hovering-col-7 [data-key='hovering-col-7'],
.hovering-col-8 [data-key='hovering-col-8'],
.hovering-col-9 [data-key='hovering-col-9'],
.hovering-col-10 [data-key='hovering-col-10'] {
background: var(--u-table-row-hover-bg-color);
}
[data-key='hovering-col-0'] {
font-weight: bold;
user-select: none;
pointer-events: none;
}
</style>
列合并
虚拟化表格不使用原生 <table>,colspan / rowspan 与 TableV1 略有不同,但通过自定义行渲染仍可实现。本节演示列合并思路。
<template>
<u-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
<template #row="props">
<Row v-bind="props" />
</template>
</u-table-v2>
</template>
<script lang="ts" setup>
import { cloneVNode } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const colSpanIndex = 1
columns[colSpanIndex].colSpan = ({ rowIndex }) => (rowIndex % 4) + 1
columns[colSpanIndex].align = 'center'
const Row = ({ rowData, rowIndex, cells, columns }) => {
const colSpan = columns[colSpanIndex].colSpan({ rowData, rowIndex })
if (colSpan > 1) {
let width = Number.parseInt(cells[colSpanIndex].props.style.width)
for (let i = 1; i < colSpan; i++) {
width += Number.parseInt(cells[colSpanIndex + i].props.style.width)
cells[colSpanIndex + i] = null
}
const style = {
...cells[colSpanIndex].props.style,
width: `${width}px`,
backgroundColor: 'var(--u-color-primary-light-3)',
}
cells[colSpanIndex] = cloneVNode(cells[colSpanIndex], { style })
}
return cells
}
</script>
行合并
在 列合并 基础上,行合并思路相近,实现细节略有差异。
<template>
<u-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
<template #row="props">
<Row v-bind="props" />
</template>
</u-table-v2>
</template>
<script lang="ts" setup>
import { cloneVNode } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const rowSpanIndex = 0
columns[rowSpanIndex].rowSpan = ({ rowIndex }) =>
rowIndex % 2 === 0 && rowIndex <= data.length - 2 ? 2 : 1
const Row = ({ rowData, rowIndex, cells, columns }) => {
const rowSpan = columns[rowSpanIndex].rowSpan({ rowData, rowIndex })
if (rowSpan > 1) {
const cell = cells[rowSpanIndex]
const style = {
...cell.props.style,
backgroundColor: 'var(--u-color-primary-light-3)',
height: `${rowSpan * 50 - 1}px`,
alignSelf: 'flex-start',
zIndex: 1,
}
cells[rowSpanIndex] = cloneVNode(cell, { style })
}
return cells
}
</script>
行列合并
可将行合并与列合并组合使用以满足业务布局。
<template>
<u-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
<template #row="props">
<Row v-bind="props" />
</template>
</u-table-v2>
</template>
<script lang="tsx" setup>
import { cloneVNode } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const colSpanIndex = 1
columns[colSpanIndex].colSpan = ({ rowIndex }) => (rowIndex % 4) + 1
columns[colSpanIndex].align = 'center'
const rowSpanIndex = 0
columns[rowSpanIndex].rowSpan = ({ rowIndex }) =>
rowIndex % 2 === 0 && rowIndex <= data.length - 2 ? 2 : 1
const Row = ({ rowData, rowIndex, cells, columns }) => {
const colSpan = columns[colSpanIndex].colSpan({ rowData, rowIndex })
if (colSpan > 1) {
let width = Number.parseInt(cells[colSpanIndex].props.style.width)
for (let i = 1; i < colSpan; i++) {
width += Number.parseInt(cells[colSpanIndex + i].props.style.width)
cells[colSpanIndex + i] = null
}
const style = {
...cells[colSpanIndex].props.style,
width: `${width}px`,
backgroundColor: 'var(--u-color-primary-light-3)',
}
cells[colSpanIndex] = cloneVNode(cells[colSpanIndex], { style })
}
const rowSpan = columns[rowSpanIndex].rowSpan({ rowData, rowIndex })
if (rowSpan > 1) {
const cell = cells[rowSpanIndex]
const style = {
...cell.props.style,
backgroundColor: 'var(--u-color-danger-light-3)',
height: `${rowSpan * 50}px`,
alignSelf: 'flex-start',
zIndex: 1,
}
cells[rowSpanIndex] = cloneVNode(cell, { style })
} else {
const style = cells[rowSpanIndex].props.style
// override the cell here for creating a pure node without pollute the style
cells[rowSpanIndex] = (
<div style={{ ...style, width: `${style.width}px` }} />
)
}
return cells
}
</script>
树形数据
虚拟表格支持树形结构,点击箭头可展开或折叠节点。
<template>
<u-table-v2
v-model:expanded-row-keys="expandedRowKeys"
:columns="columns"
:data="treeData"
:width="700"
:expand-column-key="expandColumnKey"
:height="400"
fixed
@row-expand="onRowExpanded"
@expanded-rows-change="onExpandedRowsChange"
/>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { TableV2FixedDir } from 'uniboot-ui'
import type { ExpandedRowsChangeHandler, RowExpandHandler } from 'uniboot-ui'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10).map((column, columnIndex) => {
let fixed!: TableV2FixedDir
if (columnIndex < 2) fixed = TableV2FixedDir.LEFT
if (columnIndex > 8) fixed = TableV2FixedDir.RIGHT
return { ...column, fixed }
})
const data = generateData(columns, 200)
const expandColumnKey = 'column-0'
// add some sub items
for (let i = 0; i < 50; i++) {
data.push(
{
...data[0],
id: `${data[0].id}-sub-${i}`,
parentId: data[0].id,
[expandColumnKey]: `Sub ${i}`,
},
{
...data[2],
id: `${data[2].id}-sub-${i}`,
parentId: data[2].id,
[expandColumnKey]: `Sub ${i}`,
},
{
...data[2],
id: `${data[2].id}-sub-sub-${i}`,
parentId: `${data[2].id}-sub-${i}`,
[expandColumnKey]: `Sub-Sub ${i}`,
}
)
}
function unflatten(
data: ReturnType<typeof generateData>,
rootId = null,
dataKey = 'id',
parentKey = 'parentId'
) {
const tree: any[] = []
const childrenMap = {}
for (const datum of data) {
const item = { ...datum }
const id = item[dataKey]
const parentId = item[parentKey]
if (Array.isArray(item.children)) {
childrenMap[id] = item.children.concat(childrenMap[id] || [])
} else if (!childrenMap[id]) {
childrenMap[id] = []
}
item.children = childrenMap[id]
if (parentId !== undefined && parentId !== rootId) {
if (!childrenMap[parentId]) childrenMap[parentId] = []
childrenMap[parentId].push(item)
} else {
tree.push(item)
}
}
return tree
}
const treeData = computed(() => unflatten(data))
const expandedRowKeys = ref<string[]>([])
const onRowExpanded = ({ expanded }: Parameters<RowExpandHandler>[0]) => {
console.log('Expanded:', expanded)
}
const onExpandedRowsChange = (
expandedKeys: Parameters<ExpandedRowsChangeHandler>[0]
) => {
console.log(expandedKeys)
}
</script>
动态行高
当行内容高度不确定时,可启用动态行高:传入 estimated-row-height,预估值越接近真实内容,滚动越顺滑。
TIP
行高会在渲染过程中动态测量;若一次性展示数据量很大,界面可能出现跳动。
<template>
<u-table-v2
:columns="columns"
:data="data"
:sort-by="sort"
:estimated-row-height="40"
:width="700"
:height="400"
fixed
@column-sort="onColumnSort"
/>
</template>
<script lang="tsx" setup>
import { ref } from 'vue'
import {
UButton,
UTag,
TableV2FixedDir,
TableV2SortOrder,
} from 'uniboot-ui'
import type { Column, SortBy } from 'uniboot-ui'
const longText =
'Quaerat ipsam necessitatibus eum quibusdam est id voluptatem cumque mollitia.'
const midText = 'Corrupti doloremque a quos vero delectus consequatur.'
const shortText = 'Eius optio fugiat.'
const textList = [shortText, midText, longText]
// generate random number in range 0 to 2
let id = 0
const dataGenerator = () => ({
id: `random-${++id}`,
name: 'Tom',
date: '2016-05-03',
description: textList[Math.floor(Math.random() * 3)],
})
const columns: Column<any>[] = [
{
key: 'id',
title: 'Id',
dataKey: 'id',
width: 150,
sortable: true,
fixed: TableV2FixedDir.LEFT,
},
{
key: 'name',
title: 'Name',
dataKey: 'name',
width: 150,
align: 'center',
cellRenderer: ({ cellData: name }) => <UTag>{name}</UTag>,
},
{
key: 'description',
title: 'Description',
dataKey: 'description',
width: 150,
cellRenderer: ({ cellData: description }) => (
<div style="padding: 10px 0;">{description}</div>
),
},
{
key: 'operations',
title: 'Operations',
cellRenderer: () => (
<>
<UButton size="small">Edit</UButton>
<UButton size="small" type="danger">
Delete
</UButton>
</>
),
width: 150,
align: 'center',
},
]
const data = ref(
Array.from({ length: 200 })
.map(dataGenerator)
.sort((a, b) => (a.name > b.name ? 1 : -1))
)
const sort = ref<SortBy>({ key: 'name', order: TableV2SortOrder.ASC })
const onColumnSort = (sortBy: SortBy) => {
const order = sortBy.order === 'asc' ? 1 : -1
const dataClone = [...data.value]
dataClone.sort((a, b) => (a[sortBy.key] > b[sortBy.key] ? order : -order))
sort.value = sortBy
data.value = dataClone
}
</script>
详情视图
在动态行高基础上,可在表格内嵌套详情区域。
<template>
<u-table-v2
:columns="columns"
:data="data"
:estimated-row-height="50"
:expand-column-key="columns[0].key"
:width="700"
:height="400"
>
<template #row="props">
<Row v-bind="props" />
</template>
</u-table-v2>
</template>
<script lang="tsx" setup>
import { ref } from 'vue'
const detailedText = `Velit sed aspernatur tempora. Natus consequatur officiis dicta vel assumenda.
Itaque est temporibus minus quis. Ipsum commodiab porro vel voluptas illum.
Qui quam nulla et dolore autem itaque est.
Id consequatur ipsum ea fuga et odit eligendi impedit.
Maiores officiis occaecati et magnam et sapiente est velit sunt.
Non et tempore temporibus. Excepturi et quos. Minus distinctio aut.
Voluptatem ea excepturi omnis vel. Non aperiam sit sed laboriosam eaque omnis deleniti.
Est molestiae omnis non et nulla repudiandae fuga sit.`
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = ref(
generateData(columns, 200).map((data) => {
data.children = [
{
id: `${data.id}-detail-content`,
detail: detailedText,
},
]
return data
})
)
const Row = ({ cells, rowData }) => {
if (rowData.detail) return <div class="p-6">{rowData.detail}</div>
return cells
}
Row.inheritAttrs = false
</script>
<style>
.u-table-v2__row-depth-0 {
height: 50px;
}
.u-table-v2__cell-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
自定义表尾
需要在表格底部展示总结信息时,可自定义表尾。
<template>
<u-table-v2
:columns="columns"
:data="data"
:row-height="40"
:width="700"
:height="400"
:footer-height="50"
fixed
>
<template #footer
><div
class="flex items-center"
style="
justify-content: center;
height: 100%;
background-color: var(--u-color-primary-light-7);
"
>
Display a message in the footer
</div>
</template>
</u-table-v2>
</template>
<script lang="ts" setup>
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
</script>
自定义空状态
自定义无数据时的展示内容。
<template>
<u-table-v2
:columns="columns"
:data="[]"
:row-height="40"
:width="700"
:height="400"
:footer-height="50"
>
<template #empty>
<div class="flex items-center justify-center h-100%">
<u-empty />
</div>
</template>
</u-table-v2>
</template>
<script lang="tsx" setup>
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const columns = generateColumns(10)
</script>
遮罩层
在表格上方覆盖加载中或其它提示内容。
<template>
<u-table-v2
:columns="columns"
:data="data"
:row-height="40"
:width="700"
:height="400"
>
<template #overlay>
<div
class="u-loading-mask"
style="display: flex; align-items: center; justify-content: center"
>
<u-icon class="is-loading" color="var(--u-color-primary)" :size="26">
<loading-icon />
</u-icon>
</div>
</template>
</u-table-v2>
</template>
<script lang="ts" setup>
import { Loading as LoadingIcon } from '@uniboot/icons-vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
</script>
<style>
.example-showcase .u-table-v2__overlay {
z-index: 9;
}
</style>
手动滚动
使用 Table V2 暴露的方法按偏移或行索引程序化滚动。
TIP
scrollToRow 的第二个参数为滚动策略,默认 auto 由组件自行计算位置;也可手动指定。
可选值:"auto" | "center" | "end" | "start" | "smart"。auto 是 smart 策略的子集。
<template>
<div class="mb-4 flex items-center">
<u-form-item label="Scroll pixels" class="mr-4">
<u-input v-model="scrollDelta" />
</u-form-item>
<u-form-item label="Scroll rows">
<u-input v-model="scrollRows" />
</u-form-item>
</div>
<div class="mb-4 flex items-center">
<u-button @click="scrollByPixels"> Scroll by pixels </u-button>
<u-button @click="scrollByRows"> Scroll by rows </u-button>
</div>
<div style="height: 400px">
<u-auto-resizer>
<template #default="{ height, width }">
<u-table-v2
ref="tableRef"
:columns="columns"
:data="data"
:width="width"
:height="height"
fixed
/>
</template>
</u-auto-resizer>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { TableV2Instance } from 'uniboot-ui'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const tableRef = ref<TableV2Instance>()
const scrollDelta = ref(200)
const scrollRows = ref(10)
function scrollByPixels() {
tableRef.value?.scrollToTop(scrollDelta.value)
}
function scrollByRows() {
tableRef.value?.scrollToRow(scrollRows.value)
}
</script>
虚拟化表格 API
虚拟化表格 属性
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| cache | 为提升性能,在可视区域外额外预渲染的行数 | number | 2 |
| estimated-row-height | 动态行高模式下的预估行高 | number | — |
| header-class | 传给表头包裹层的自定义 class,可为函数返回值 | string / Function<HeaderClassGetter> | — |
| header-props | 传给表头组件的自定义 props | object / Function<HeaderPropsGetter> | — |
| header-cell-props | 传给表头单元格的自定义 props | object / Function<HeaderCellPropsGetter> | — |
| header-height | 表头高度;与 height 配合使用。传入数组时表示多行表头,长度即行数 | number/ number[] | 50 |
| footer-height | 表尾高度,参与表格总高度计算 | number | 0 |
| row-class | 传给行包裹层的自定义 class | string / Function<RowClassGetter> | — |
| row-key | 每行唯一键;未提供时使用行索引 | string / Symbol / number | id |
| row-props | 传给行组件的自定义 props | object / Function<RowPropsGetter> | — |
| row-height | 行高,用于计算表格总高度 | number | 50 |
| row-event-handlers | 绑定到每行的事件集合 | object<RowEventHandlers> | — |
| cell-props | 传给数据单元格的额外 props(不含表头) | object / Function<CellPropsGetter> | — |
| columns | 列定义数组 | Column[] | — |
| data | 表格数据源数组 | Data[] | [] |
| data-getter | 自定义从数据源取值的函数 | Function<DataGetter<T>> | — |
| fixed-data | 渲染在表头下方、主内容上方的粘性行数据 | object<Data> | — |
| expand-column-key | 指定哪一列用于控制行展开 | string | — |
| expanded-row-keys | 已展开行的 key 数组,可与 v-model 配合 | KeyType[] | — |
| default-expanded-row-keys | 默认展开行的 key 数组,非响应式 | KeyType[] | — |
| class | 虚拟表格根 class,会应用到左、中、右三张表 | string / array / object | — |
| fixed | 列宽布局是否为固定模式 | boolean | false |
| width required | 表格宽度 | number | — |
| height required | 表格高度 | number | — |
| max-height | 表格最大高度 | number | — |
| indent-size | 树形表格水平缩进像素 | number | 12 |
| h-scrollbar-size | 横向滚动条占位尺寸,用于避免横纵滚动条挤压重叠 | number | 6 |
| v-scrollbar-size | 纵向滚动条占位尺寸,用于避免横纵滚动条挤压重叠 | number | 6 |
| scrollbar-always-on | 为 true 时始终显示滚动条,而非仅悬停时显示 | boolean | false |
| sort-by | 单列排序状态对象 | object<SortBy> | {} |
| sort-state | 多列排序状态映射 | object<SortState> | undefined |
虚拟化表格 插槽
| 名称 | 类型 |
|---|---|
| cell | object<CellSlotProps> |
| header | object<HeaderSlotProps> |
| header-cell | object<HeaderCellSlotProps> |
| row | object<RowSlotProps> |
| footer | — |
| empty | — |
| overlay | — |
虚拟化表格 事件
| 事件名 | 说明 | 类型 |
|---|---|---|
| column-sort | 列排序时触发 | object<ColumnSortParam> |
| expanded-rows-change | 展开行变化时触发 | KeyType[] |
| end-reached | 滚动到底部时触发,回调参数为剩余可滚动距离,通常接近滚动条高度 | Function |
| scroll | 滚动后触发 | object<ScrollParams> |
| rows-rendered | 行渲染完成时触发 | object<RowsRenderedParams> |
| row-expand | 点击树节点箭头展开/折叠时触发 | object<RowExpandParams> |
虚拟化表格 方法
| 名称 | 说明 | 类型 |
|---|---|---|
| scrollTo | 滚动到指定坐标 | Function |
| scrollToLeft | 水平滚动到指定位置 | Function |
| scrollToTop | 垂直滚动到指定位置 | Function |
| scrollToRow | 按策略滚动到指定行索引 | Function |
TIP
以下配置为 JavaScript 对象,属性名不能使用 kebab-case。
列属性 Column
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| align | 单元格内容对齐方式 | Alignment | left |
| class | 列 className | string | — |
| key | 列唯一标识 | KeyType | — |
| dataKey | 数据字段唯一标识 | KeyType | — |
| fixed | 列是否固定及方向 | boolean / FixedDir | false |
| flexGrow | 非固定表格时列的 flex-grow | number | 0 |
| flexShrink | 非固定表格时列的 flex-shrink | number | 1 |
| headerClass | 表头单元格 class | string | — |
| hidden | 是否隐藏列 | boolean | — |
| style | 列单元格自定义样式,会与网格样式合并 | object | — |
| sortable | 是否可排序 | boolean | — |
| title | 表头默认展示文案 | string | — |
| maxWidth | 列最大宽度 | number | — |
| minWidth | 列最小宽度 | number | — |
| width required | 列宽度 | number | — |
| cellRenderer | 自定义单元格渲染器 | VueComponent / (props: CellRenderProps) => VNode | — |
| headerCellRenderer | 自定义表头单元格渲染器 | VueComponent / (props: HeaderRenderProps) => VNode | — |
类型声明
展开类型声明
type HeaderClassGetter = (param: {
columns: Column<any>[]
headerIndex: number
}) => string
type HeaderPropsGetter = (param: {
columns: Column<any>[]
headerIndex: number
}) => Record<string, any>
type HeaderCellPropsGetter = (param: {
columns: Column<any>[]
column: Column<any>
columnIndex: number
headerIndex: number
style: CSSProperties
}) => Record<string, any>
type RowClassGetter = (param: {
columns: Column<any>[]
rowData: any
rowIndex: number
}) => string
type RowPropsGetter = (param: {
columns: Column<any>[]
rowData: any
rowIndex: number
}) => Record<string, any>
type CellPropsGetter = (param: {
column: Column<any>
columns: Column<any>[]
columnIndex: number
cellData: any
rowData: any
rowIndex: number
}) => void
type DataGetterParams<T> = {
columns: Column<T>[]
column: Column<T>
columnIndex: number
} & RowCommonParams
type DataGetter<T> = (params: DataGetterParams<T>) => T
type CellRenderProps<T> = {
cellData: T
column: Column<T>
columns: Column<T>[]
columnIndex: number
rowData: any
rowIndex: number
}
type HeaderRenderProps<T> = {
column: Column<T>
columns: Column<T>[]
columnIndex: number
headerIndex: number
}
type ScrollParams = {
xAxisScrollDir: 'forward' | 'backward'
scrollLeft: number
yAxisScrollDir: 'forward' | 'backward'
scrollTop: number
}
type CellSlotProps<T> = {
column: Column<T>
columns: Column<T>[]
columnIndex: number
depth: number
style: CSSProperties
rowData: any
rowIndex: number
isScrolling: boolean
expandIconProps?:
| {
rowData: any
rowIndex: number
onExpand: (expand: boolean) => void
}
| undefined
}
type HeaderSlotProps = {
cells: VNode[]
columns: Column<any>[]
headerIndex: number
}
type HeaderCellSlotProps = {
class: string
columns: Column<any>[]
column: Column<any>
columnIndex: number
headerIndex: number
style: CSSProperties
headerCellProps?: any
sortBy: SortBy
sortState?: SortState | undefined
onColumnSorted: (e: MouseEvent) => void
}
type RowCommonParams = {
rowData: any
rowIndex: number
}
type RowEventHandlerParams = {
rowKey: KeyType
event: Event
} & RowCommonParams
type RowEventHandler = (params: RowEventHandlerParams) => void
type RowEventHandlers = {
onClick?: RowEventHandler
onContextmenu?: RowEventHandler
onDblclick?: RowEventHandler
onMouseenter?: RowEventHandler
onMouseleave?: RowEventHandler
}
type RowsRenderedParams = {
rowCacheStart: number
rowCacheEnd: number
rowVisibleStart: number
rowVisibleEnd: number
}
type RowSlotProps = {
columns: Column<any>[]
rowData: any
columnIndex: number
rowIndex: number
data: any
key: number | string
isScrolling?: boolean
style: CSSProperties
}
type RowExpandParams = {
expanded: boolean
rowKey: KeyType
} & RowCommonParams
type Data = {
[key: KeyType]: any
children?: Array<any>
}
type FixedData = Data
type KeyType = string | number | symbol
type ColumnSortParam<T> = { column: Column<T>; key: KeyType; order: SortOrder }
enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}
enum Alignment {
LEFT = 'left',
CENTER = 'center',
RIGHT = 'right',
}
type SortBy = { key: KeyType; Order: SortOrder }
type SortState = Record<KeyType, SortOrder>常见问题
如何在首列渲染复选框列表?
可自行实现单元格渲染器,参考 自定义单元格渲染 示例渲染 checkbox,并由你维护选中状态。
为什么虚拟化表格比 TableV1 功能更少?
虚拟化表格有意保持精简,把更多能力留给业务自行扩展;功能过多会增加维护成本,而对多数场景基础能力已足够。部分关键能力仍在迭代中。欢迎加入 Discord 关注进展并反馈需求。