Table
用法
Table 组件基于TanStack Table并由useVueTable可组合函数提供灵活且完全类型安全的 API。TanStack Table 的某些功能尚不支持,我们将随着时间推移添加更多功能。
# | 日期 | 状态 | 金额 | |||
---|---|---|---|---|---|---|
#4600 | Mar 11, 15:30 | paid | €594.00 | |||
#4599 | Mar 11, 10:10 | failed | €276.00 | |||
#4598 | Mar 11, 08:50 | refunded | €315.00 | |||
#4597 | Mar 10, 19:45 | paid | €529.00 | |||
#4596 | Mar 10, 15:55 | paid | €639.00 | |||
#4595 | Mar 10, 13:40 | refunded | €428.00 | |||
#4594 | Mar 10, 09:15 | paid | €683.00 | |||
#4593 | Mar 9, 20:25 | failed | €947.00 | |||
#4592 | Mar 9, 18:45 | paid | €851.00 | |||
#4591 | Mar 9, 16:05 | paid | €762.00 | |||
#4590 | Mar 9, 14:20 | paid | €573.00 | |||
#4589 | Mar 9, 11:35 | failed | €389.00 | |||
#4588 | Mar 8, 22:50 | refunded | €701.00 | |||
#4587 | Mar 8, 20:15 | paid | €856.00 | |||
#4586 | Mar 8, 17:40 | paid | €492.00 | |||
#4585 | Mar 8, 14:55 | failed | €637.00 | |||
#4584 | Mar 8, 12:30 | paid | €784.00 | |||
#4583 | Mar 8, 09:45 | refunded | €345.00 | |||
#4582 | Mar 7, 23:10 | paid | €918.00 | |||
#4581 | Mar 7, 20:25 | paid | €567.00 |
数据
使用 data
prop 作为对象数组,列将根据对象的键自动生成。
Id | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
4600 | 2024-03-11T15:30:00 | paid | [email protected] | 594 |
4599 | 2024-03-11T10:10:00 | failed | [email protected] | 276 |
4598 | 2024-03-11T08:50:00 | refunded | [email protected] | 315 |
4597 | 2024-03-10T19:45:00 | paid | [email protected] | 529 |
4596 | 2024-03-10T15:55:00 | paid | [email protected] | 639 |
<script setup lang="ts">
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
</script>
<template>
<UTable :data="data" class="flex-1" />
</template>
列
使用 columns
prop 作为ColumnDef对象数组,包含以下属性:
accessorKey
: 提取列值时使用的行对象键。header
: 列的头部显示内容。如果传入字符串,可用作列 ID 的默认值。如果传入函数,它将接收一个头部 props 对象,并应返回渲染后的头部值(具体类型取决于所使用的适配器)。cell
: 列中每行的单元格显示内容。如果传入函数,它将接收一个单元格 props 对象,并应返回渲染后的单元格值(具体类型取决于所使用的适配器)。meta
: 列的额外属性。类
:td
: 应用于td
元素的类。th
: 应用于th
元素的类。
为了渲染组件或其他 HTML 元素,你需要使用 Vue 的h
函数在 header
和 cell
props 中。这与其他使用 slot 的组件不同,但提供了更大的灵活性。
# | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
#4600 | Mar 11, 15:30 | paid | [email protected] | €594.00 |
#4599 | Mar 11, 10:10 | failed | [email protected] | €276.00 |
#4598 | Mar 11, 08:50 | refunded | [email protected] | €315.00 |
#4597 | Mar 10, 19:45 | paid | [email protected] | €529.00 |
#4596 | Mar 10, 15:55 | paid | [email protected] | €639.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template>
h
渲染组件时,你可以使用 resolveComponent
函数或从 #components
导入。Meta
使用 meta
prop 作为对象 (TableMeta) 来传递属性,例如:
类
:tr
: 应用于tr
元素的类。
加载中
使用 loading
prop 显示加载状态,loading-color
prop 改变其颜色,loading-animation
prop 改变其动画。
Id | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
4600 | 2024-03-11T15:30:00 | paid | [email protected] | 594 |
4599 | 2024-03-11T10:10:00 | failed | [email protected] | 276 |
4598 | 2024-03-11T08:50:00 | refunded | [email protected] | 315 |
4597 | 2024-03-10T19:45:00 | paid | [email protected] | 529 |
4596 | 2024-03-10T15:55:00 | paid | [email protected] | 639 |
<script setup lang="ts">
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
</script>
<template>
<UTable loading loading-color="primary" loading-animation="carousel" :data="data" class="flex-1" />
</template>
粘性
使用 sticky
prop 使头部具有粘性。
Id | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
4600 | 2024-03-11T15:30:00 | paid | [email protected] | 594 |
4599 | 2024-03-11T10:10:00 | failed | [email protected] | 276 |
4598 | 2024-03-11T08:50:00 | refunded | [email protected] | 315 |
4597 | 2024-03-10T19:45:00 | paid | [email protected] | 529 |
4596 | 2024-03-10T15:55:00 | paid | [email protected] | 639 |
4595 | 2024-03-10T15:55:00 | paid | [email protected] | 639 |
4594 | 2024-03-10T15:55:00 | paid | [email protected] | 639 |
<script setup lang="ts">
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
},
{
id: '4595',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
},
{
id: '4594',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
</script>
<template>
<UTable sticky :data="data" class="flex-1 max-h-[312px]" />
</template>
示例
带行操作
你可以添加一个新列,在 cell
中渲染一个 DropdownMenu 组件来呈现行操作。
# | 日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|---|
#4600 | Mar 11, 15:30 | paid | [email protected] | €594.00 | |
#4599 | Mar 11, 10:10 | failed | [email protected] | €276.00 | |
#4598 | Mar 11, 08:50 | refunded | [email protected] | €315.00 | |
#4597 | Mar 10, 19:45 | paid | [email protected] | €529.00 | |
#4596 | Mar 10, 15:55 | paid | [email protected] | €639.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table'
const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')
const toast = useToast()
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
},
{
id: 'actions',
cell: ({ row }) => {
return h(
'div',
{ class: 'text-right' },
h(
UDropdownMenu,
{
content: {
align: 'end'
},
items: getRowItems(row),
'aria-label': 'Actions dropdown'
},
() =>
h(UButton, {
icon: 'i-lucide-ellipsis-vertical',
color: 'neutral',
variant: 'ghost',
class: 'ml-auto',
'aria-label': 'Actions dropdown'
})
)
)
}
}
]
function getRowItems(row: Row<Payment>) {
return [
{
type: 'label',
label: 'Actions'
},
{
label: 'Copy payment ID',
onSelect() {
navigator.clipboard.writeText(row.original.id)
toast.add({
title: 'Payment ID copied to clipboard!',
color: 'success',
icon: 'i-lucide-circle-check'
})
}
},
{
type: 'separator'
},
{
label: 'View customer'
},
{
label: 'View payment details'
}
]
}
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template>
带可展开行
你可以添加一个新列,在 cell
中渲染一个 Button 组件,使用 TanStack Table 的展开 API.
#expanded
插槽来渲染展开的内容,该插槽会接收行作为参数。# | 日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|---|
#4600 | Mar 11, 15:30 | paid | [email protected] | €594.00 | |
#4599 | Mar 11, 10:10 | failed | [email protected] | €276.00 | |
{ "id": "4599", "date": "2024-03-11T10:10:00", "status": "failed", "email": "[email protected]", "amount": 276 } | |||||
#4598 | Mar 11, 08:50 | refunded | [email protected] | €315.00 | |
#4597 | Mar 10, 19:45 | paid | [email protected] | €529.00 | |
#4596 | Mar 10, 15:55 | paid | [email protected] | €639.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
id: 'expand',
cell: ({ row }) =>
h(UButton, {
color: 'neutral',
variant: 'ghost',
icon: 'i-lucide-chevron-down',
square: true,
'aria-label': 'Expand',
ui: {
leadingIcon: [
'transition-transform',
row.getIsExpanded() ? 'duration-200 rotate-180' : ''
]
},
onClick: () => row.toggleExpanded()
})
},
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const expanded = ref({ 1: true })
</script>
<template>
<UTable
v-model:expanded="expanded"
:data="data"
:columns="columns"
:ui="{ tr: 'data-[expanded=true]:bg-elevated/50' }"
class="flex-1"
>
<template #expanded="{ row }">
<pre>{{ row.original }}</pre>
</template>
</UTable>
</template>
expanded
prop 来控制行的展开状态(可以通过 v-model
进行绑定)。actions
列中的 DropdownMenu
组件中。带行选择
你可以添加一个新列,在 header
和 cell
中渲染一个 Checkbox 组件,使用 TanStack Table 的行选择 API.
日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|
Mar 11, 15:30 | paid | [email protected] | €594.00 | |
Mar 11, 10:10 | failed | [email protected] | €276.00 | |
Mar 11, 08:50 | refunded | [email protected] | €315.00 | |
Mar 10, 19:45 | paid | [email protected] | €529.00 | |
Mar 10, 15:55 | paid | [email protected] | €639.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UCheckbox = resolveComponent('UCheckbox')
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
id: 'select',
header: ({ table }) =>
h(UCheckbox, {
modelValue: table.getIsSomePageRowsSelected()
? 'indeterminate'
: table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
table.toggleAllPageRowsSelected(!!value),
'aria-label': 'Select all'
}),
cell: ({ row }) =>
h(UCheckbox, {
modelValue: row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
})
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const table = useTemplateRef('table')
const rowSelection = ref({ 1: true })
</script>
<template>
<div class="flex-1 w-full">
<UTable ref="table" v-model:row-selection="rowSelection" :data="data" :columns="columns" />
<div class="px-4 py-3.5 border-t border-accented text-sm text-muted">
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
</div>
</div>
</template>
row-selection
prop 来控制行的选择状态(可以通过 v-model
进行绑定)。@select
事件 带
你可以添加 @select
监听器使行可点击。处理函数接收 TableRow
实例作为第一个参数,可选的 Event
作为第二个参数。
日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|
Mar 11, 15:30 | paid | [email protected] | €594.00 | |
Mar 11, 10:10 | failed | [email protected] | €276.00 | |
Mar 11, 08:50 | refunded | [email protected] | €315.00 | |
Mar 10, 19:45 | paid | [email protected] | €529.00 | |
Mar 10, 15:55 | paid | [email protected] | €639.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
id: 'select',
header: ({ table }) =>
h(UCheckbox, {
modelValue: table.getIsSomePageRowsSelected()
? 'indeterminate'
: table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
table.toggleAllPageRowsSelected(!!value),
'aria-label': 'Select all'
}),
cell: ({ row }) =>
h(UCheckbox, {
modelValue: row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
})
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const table = useTemplateRef('table')
const rowSelection = ref<Record<string, boolean>>({})
function onSelect(row: TableRow<Payment>, e?: Event) {
/* If you decide to also select the column you can do this */
row.toggleSelected(!row.getIsSelected())
console.log(e)
}
</script>
<template>
<div class=" flex w-full flex-1 gap-1">
<div class="flex-1">
<UTable
ref="table"
v-model:row-selection="rowSelection"
:data="data"
:columns="columns"
@select="onSelect"
/>
<div class="px-4 py-3.5 border-t border-accented text-sm text-muted">
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
</div>
</div>
</div>
</template>
带列排序
你可以更新列的 header
,在 header
中渲染一个 Button 组件,使用 TanStack Table 的排序 API.
# | 日期 | 状态 | 金额 | |
---|---|---|---|---|
#4597 | Mar 10, 19:45 | paid | [email protected] | €529.00 |
#4596 | Mar 10, 15:55 | paid | [email protected] | €639.00 |
#4600 | Mar 11, 15:30 | paid | [email protected] | €594.00 |
#4599 | Mar 11, 10:10 | failed | [email protected] | €276.00 |
#4598 | Mar 11, 08:50 | refunded | [email protected] | €315.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: ({ column }) => {
const isSorted = column.getIsSorted()
return h(UButton, {
color: 'neutral',
variant: 'ghost',
label: 'Email',
icon: isSorted
? isSorted === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
: 'i-lucide-arrow-up-down',
class: '-mx-2.5',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
})
}
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const sorting = ref([
{
id: 'email',
desc: false
}
])
</script>
<template>
<UTable v-model:sorting="sorting" :data="data" :columns="columns" class="flex-1" />
</template>
sorting
prop 来控制列的排序状态(可以通过 v-model
进行绑定)。你也可以创建一个可重用组件,使任何列头部都可以排序。
#4596 | Mar 10, 15:55 | paid | [email protected] | €639.00 |
#4597 | Mar 10, 19:45 | paid | [email protected] | €529.00 |
#4598 | Mar 11, 08:50 | refunded | [email protected] | €315.00 |
#4599 | Mar 11, 10:10 | failed | [email protected] | €276.00 |
#4600 | Mar 11, 15:30 | paid | [email protected] | €594.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column } from '@tanstack/vue-table'
const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')
const UDropdownMenu = resolveComponent('UDropdownMenu')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: ({ column }) => getHeader(column, 'ID'),
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: ({ column }) => getHeader(column, 'Date'),
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: ({ column }) => getHeader(column, 'Status'),
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: ({ column }) => getHeader(column, 'Email')
},
{
accessorKey: 'amount',
header: ({ column }) => h('div', { class: 'text-right' }, getHeader(column, 'Amount')),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
function getHeader(column: Column<Payment>, label: string) {
const isSorted = column.getIsSorted()
return h(
UDropdownMenu,
{
content: {
align: 'start'
},
'aria-label': 'Actions dropdown',
items: [
{
label: 'Asc',
type: 'checkbox',
icon: 'i-lucide-arrow-up-narrow-wide',
checked: isSorted === 'asc',
onSelect: () => {
if (isSorted === 'asc') {
column.clearSorting()
} else {
column.toggleSorting(false)
}
}
},
{
label: 'Desc',
icon: 'i-lucide-arrow-down-wide-narrow',
type: 'checkbox',
checked: isSorted === 'desc',
onSelect: () => {
if (isSorted === 'desc') {
column.clearSorting()
} else {
column.toggleSorting(true)
}
}
}
]
},
() =>
h(UButton, {
color: 'neutral',
variant: 'ghost',
label,
icon: isSorted
? isSorted === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
: 'i-lucide-arrow-up-down',
class: '-mx-2.5 data-[state=open]:bg-elevated',
'aria-label': `Sort by ${isSorted === 'asc' ? 'descending' : 'ascending'}`
})
)
}
const sorting = ref([
{
id: 'id',
desc: false
}
])
</script>
<template>
<UTable v-model:sorting="sorting" :data="data" :columns="columns" class="flex-1" />
</template>
带列固定
你可以更新列的 header
,在 header
中渲染一个 Button 组件,使用 TanStack Table 的固定 API.
#46000000000000000000000000000000000000000 | 2024-03-11T15:30:00 | paid | [email protected] | €594,000.00 |
#45990000000000000000000000000000000000000 | 2024-03-11T10:10:00 | failed | [email protected] | €276,000.00 |
#45980000000000000000000000000000000000000 | 2024-03-11T08:50:00 | refunded | [email protected] | €315,000.00 |
#45970000000000000000000000000000000000000 | 2024-03-10T19:45:00 | paid | [email protected] | €5,290,000.00 |
#45960000000000000000000000000000000000000 | 2024-03-10T15:55:00 | paid | [email protected] | €639,000.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column } from '@tanstack/vue-table'
const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '46000000000000000000000000000000000000000',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594000
},
{
id: '45990000000000000000000000000000000000000',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276000
},
{
id: '45980000000000000000000000000000000000000',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315000
},
{
id: '45970000000000000000000000000000000000000',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 5290000
},
{
id: '45960000000000000000000000000000000000000',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639000
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: ({ column }) => getHeader(column, 'ID', 'left'),
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: ({ column }) => getHeader(column, 'Date', 'left')
},
{
accessorKey: 'status',
header: ({ column }) => getHeader(column, 'Status', 'left'),
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: ({ column }) => getHeader(column, 'Email', 'left')
},
{
accessorKey: 'amount',
header: ({ column }) => h('div', { class: 'text-right' }, getHeader(column, 'Amount', 'right')),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
function getHeader(column: Column<Payment>, label: string, position: 'left' | 'right') {
const isPinned = column.getIsPinned()
return h(UButton, {
color: 'neutral',
variant: 'ghost',
label,
icon: isPinned ? 'i-lucide-pin-off' : 'i-lucide-pin',
class: '-mx-2.5',
onClick() {
column.pin(isPinned === position ? false : position)
}
})
}
const columnPinning = ref({
left: [],
right: ['amount']
})
</script>
<template>
<UTable v-model:column-pinning="columnPinning" :data="data" :columns="columns" class="flex-1" />
</template>
column-pinning
prop 来控制列的固定状态(可以通过 v-model
进行绑定)。带列可见性
你可以使用一个 DropdownMenu 组件,使用 TanStack Table 的列可见性 API.
日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|
Mar 11, 15:30 | paid | [email protected] | €594.00 |
Mar 11, 10:10 | failed | [email protected] | €276.00 |
Mar 11, 08:50 | refunded | [email protected] | €315.00 |
Mar 10, 19:45 | paid | [email protected] | €529.00 |
Mar 10, 15:55 | paid | [email protected] | €639.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import { upperFirst } from 'scule'
import type { TableColumn } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const table = useTemplateRef('table')
const columnVisibility = ref({
id: false
})
</script>
<template>
<div class="flex flex-col flex-1 w-full">
<div class="flex justify-end px-4 py-3.5 border-b border-accented">
<UDropdownMenu
:items="
table?.tableApi
?.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => ({
label: upperFirst(column.id),
type: 'checkbox' as const,
checked: column.getIsVisible(),
onUpdateChecked(checked: boolean) {
table?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked)
},
onSelect(e?: Event) {
e?.preventDefault()
}
}))
"
:content="{ align: 'end' }"
>
<UButton
label="Columns"
color="neutral"
variant="outline"
trailing-icon="i-lucide-chevron-down"
/>
</UDropdownMenu>
</div>
<UTable
ref="table"
v-model:column-visibility="columnVisibility"
:data="data"
:columns="columns"
/>
</div>
</template>
column-visibility
prop 来控制列的可见性状态(可以通过 v-model
进行绑定)。带列过滤
你可以使用一个 Input 组件,使用 TanStack Table 的列过滤 API.
# | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
#4600 | Mar 11, 15:30 | paid | [email protected] | €594.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const table = useTemplateRef('table')
const columnFilters = ref([
{
id: 'email',
value: 'james'
}
])
</script>
<template>
<div class="flex flex-col flex-1 w-full">
<div class="flex px-4 py-3.5 border-b border-accented">
<UInput
:model-value="table?.tableApi?.getColumn('email')?.getFilterValue() as string"
class="max-w-sm"
placeholder="Filter emails..."
@update:model-value="table?.tableApi?.getColumn('email')?.setFilterValue($event)"
/>
</div>
<UTable ref="table" v-model:column-filters="columnFilters" :data="data" :columns="columns" />
</div>
</template>
column-filters
prop 来控制列的过滤状态(可以通过 v-model
进行绑定)。带全局过滤
你可以使用一个 Input 组件,使用 TanStack Table 的全局过滤 API.
# | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
#4599 | Mar 11, 10:10 | failed | [email protected] | €276.00 |
#4598 | Mar 11, 08:50 | refunded | [email protected] | €315.00 |
#4597 | Mar 10, 19:45 | paid | [email protected] | €529.00 |
#4596 | Mar 10, 15:55 | paid | [email protected] | €639.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
status: 'paid' | 'failed' | 'refunded'
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const color = {
paid: 'success' as const,
failed: 'error' as const,
refunded: 'neutral' as const
}[row.getValue('status') as string]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.getValue('status')
)
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const globalFilter = ref('45')
</script>
<template>
<div class="flex flex-col flex-1 w-full">
<div class="flex px-4 py-3.5 border-b border-accented">
<UInput v-model="globalFilter" class="max-w-sm" placeholder="Filter..." />
</div>
<UTable ref="table" v-model:global-filter="globalFilter" :data="data" :columns="columns" />
</div>
</template>
global-filter
prop 来控制全局过滤状态(可以通过 v-model
进行绑定)。带分页
你可以使用一个 Pagination 组件,使用分页 API.
有不同的分页方法,如分页指南中所述。在此示例中,我们使用客户端分页,因此需要手动传递 getPaginationRowModel()
函数。
# | 日期 | 邮箱 | 金额 |
---|---|---|---|
#4600 | Mar 11, 15:30 | [email protected] | €594.00 |
#4599 | Mar 11, 10:10 | [email protected] | €276.00 |
#4598 | Mar 11, 08:50 | [email protected] | €315.00 |
#4597 | Mar 10, 19:45 | [email protected] | €529.00 |
#4596 | Mar 10, 15:55 | [email protected] | €639.00 |
<script setup lang="ts">
import { getPaginationRowModel } from '@tanstack/vue-table'
import type { TableColumn } from '@nuxt/ui'
const table = useTemplateRef('table')
type Payment = {
id: string
date: string
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: '[email protected]',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
email: '[email protected]',
amount: 639
},
{
id: '4595',
date: '2024-03-10T13:20:00',
email: '[email protected]',
amount: 428
},
{
id: '4594',
date: '2024-03-10T11:05:00',
email: '[email protected]',
amount: 673
},
{
id: '4593',
date: '2024-03-09T22:15:00',
email: '[email protected]',
amount: 382
},
{
id: '4592',
date: '2024-03-09T20:30:00',
email: '[email protected]',
amount: 547
},
{
id: '4591',
date: '2024-03-09T18:45:00',
email: '[email protected]',
amount: 291
},
{
id: '4590',
date: '2024-03-09T16:20:00',
email: '[email protected]',
amount: 624
},
{
id: '4589',
date: '2024-03-09T14:10:00',
email: '[email protected]',
amount: 438
},
{
id: '4588',
date: '2024-03-09T12:05:00',
email: '[email protected]',
amount: 583
},
{
id: '4587',
date: '2024-03-09T10:30:00',
email: '[email protected]',
amount: 347
},
{
id: '4586',
date: '2024-03-09T08:15:00',
email: '[email protected]',
amount: 692
},
{
id: '4585',
date: '2024-03-08T23:40:00',
email: '[email protected]',
amount: 419
},
{
id: '4584',
date: '2024-03-08T21:25:00',
email: '[email protected]',
amount: 563
},
{
id: '4583',
date: '2024-03-08T19:50:00',
email: '[email protected]',
amount: 328
},
{
id: '4582',
date: '2024-03-08T17:35:00',
email: '[email protected]',
amount: 647
},
{
id: '4581',
date: '2024-03-08T15:20:00',
email: '[email protected]',
amount: 482
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
const pagination = ref({
pageIndex: 0,
pageSize: 5
})
</script>
<template>
<div class="w-full space-y-4 pb-4">
<UTable
ref="table"
v-model:pagination="pagination"
:data="data"
:columns="columns"
:pagination-options="{
getPaginationRowModel: getPaginationRowModel()
}"
class="flex-1"
/>
<div class="flex justify-center border-t border-default pt-4">
<UPagination
:default-page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
:items-per-page="table?.tableApi?.getState().pagination.pageSize"
:total="table?.tableApi?.getFilteredRowModel().rows.length"
@update:page="(p) => table?.tableApi?.setPageIndex(p - 1)"
/>
</div>
</div>
</template>
pagination
prop 来控制分页状态(可以通过 v-model
进行绑定)。带获取的数据
你可以从 API 获取数据并在表格中使用。
ID | 姓名 | 邮箱 | 公司 |
---|---|---|---|
1 | Leanne Graham @Bret | [email protected] | Romaguera-Crona |
2 | Ervin Howell @Antonette | [email protected] | Deckow-Crist |
3 | Clementine Bauch @Samantha | [email protected] | Romaguera-Jacobson |
4 | Patricia Lebsack @Karianne | [email protected] | Robel-Corkery |
5 | Chelsey Dietrich @Kamren | [email protected] | Keebler LLC |
6 | Mrs. Dennis Schulist @Leopoldo_Corkery | [email protected] | Considine-Lockman |
7 | Kurtis Weissnat @Elwyn.Skiles | [email protected] | Johns Group |
8 | Nicholas Runolfsdottir V @Maxime_Nienow | [email protected] | Abernathy Group |
9 | Glenna Reichert @Delphine | [email protected] | Yost and Sons |
10 | Clementina DuBuque @Moriah.Stanton | [email protected] | Hoeger LLC |
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
const UAvatar = resolveComponent('UAvatar')
type User = {
id: number
name: string
username: string
email: string
avatar: { src: string }
company: { name: string }
}
const { data, status } = await useFetch<User[]>('https://jsonplaceholder.typicode.com/users', {
key: 'table-users',
transform: (data) => {
return (
data?.map((user) => ({
...user,
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, alt: `${user.name} avatar` }
})) || []
)
},
lazy: true
})
const columns: TableColumn<User>[] = [
{
accessorKey: 'id',
header: 'ID'
},
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => {
return h('div', { class: 'flex items-center gap-3' }, [
h(UAvatar, {
...row.original.avatar,
size: 'lg'
}),
h('div', undefined, [
h('p', { class: 'font-medium text-highlighted' }, row.original.name),
h('p', { class: '' }, `@${row.original.username}`)
])
])
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'company',
header: 'Company',
cell: ({ row }) => row.original.company.name
}
]
</script>
<template>
<UTable :data="data" :columns="columns" :loading="status === 'pending'" class="flex-1" />
</template>
带无限滚动
如果你使用服务器端分页,可以使用useInfiniteScroll
可组合函数在滚动时加载更多数据。
ID | Avatar | 名 | 邮箱 | 用户名 |
---|---|---|---|---|
无数据 |
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { useInfiniteScroll } from '@vueuse/core'
const UAvatar = resolveComponent('UAvatar')
type User = {
id: number
firstName: string
username: string
email: string
image: string
}
type UserResponse = {
users: User[]
total: number
skip: number
limit: number
}
const skip = ref(0)
const { data, status, execute } = await useFetch(
'https://dummyjson.com/users?limit=10&select=firstName,username,email,image',
{
key: 'table-users-infinite-scroll',
params: { skip },
transform: (data?: UserResponse) => {
return data?.users
},
lazy: true,
immediate: false
}
)
const columns: TableColumn<User>[] = [
{
accessorKey: 'id',
header: 'ID'
},
{
accessorKey: 'image',
header: 'Avatar',
cell: ({ row }) => h(UAvatar, { src: row.original.image })
},
{
accessorKey: 'firstName',
header: 'First name'
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'username',
header: 'Username'
}
]
const users = ref<User[]>([])
watch(data, () => {
users.value = [...users.value, ...(data.value || [])]
})
execute()
const table = useTemplateRef<ComponentPublicInstance>('table')
onMounted(() => {
useInfiniteScroll(
table.value?.$el,
() => {
skip.value += 10
},
{
distance: 200,
canLoadMore: () => {
return status.value !== 'pending'
}
}
)
})
</script>
<template>
<div class="w-full">
<UTable
ref="table"
:data="users"
:columns="columns"
:loading="status === 'pending'"
sticky
class="flex-1 h-80"
/>
</div>
</template>
带拖放功能
使用useSortable
可组合函数,来自@vueuse/integrations
在 Table 上启用拖放功能。这个集成封装了Sortable.js以提供流畅的拖放体验。
:ui
prop 为其添加一个唯一的类,以便用 useSortable
针对它(例如 :ui="{ tbody: 'my-table-tbody' }"
)。# | 日期 | 邮箱 | 金额 |
---|---|---|---|
#4600 | Mar 11, 15:30 | [email protected] | €594.00 |
#4599 | Mar 11, 10:10 | [email protected] | €276.00 |
#4598 | Mar 11, 08:50 | [email protected] | €315.00 |
#4597 | Mar 10, 19:45 | [email protected] | €529.00 |
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable.mjs'
type Payment = {
id: string
date: string
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
email: '[email protected]',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
email: '[email protected]',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: '[email protected]',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: '[email protected]',
amount: 529
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
return new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
}
}
]
useSortable('.my-table-tbody', data, {
animation: 150
})
</script>
<template>
<div class="w-full">
<UTable
ref="table"
:data="data"
:columns="columns"
:ui="{
tbody: 'my-table-tbody'
}"
/>
</div>
</template>
带插槽
你可以使用插槽来自定义表格的头部和数据单元格。
使用 #<column>-header
插槽来自定义列的头部。你可以在插槽作用域中访问 column
、header
和 table
属性。
使用 #<column>-cell
插槽来自定义列的单元格。你可以在插槽作用域中访问 cell
、column
、getValue
、renderValue
、row
和 table
属性。
ID | 姓名 | 邮箱 | 角色 | |
---|---|---|---|---|
1 | Lindsay Walton Front-end Developer | [email protected] | Member | |
2 | Courtney Henry Designer | [email protected] | Admin | |
3 | Tom Cook Director of Product | [email protected] | Member | |
4 | Whitney Francis Copywriter | [email protected] | Admin | |
5 | Leonard Krasner Senior Designer | [email protected] | Owner | |
6 | Floyd Miles Principal Designer | [email protected] | Member |
<script setup lang="ts">
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
interface User {
id: number
name: string
position: string
email: string
role: string
}
const toast = useToast()
const data = ref<User[]>([
{
id: 1,
name: 'Lindsay Walton',
position: 'Front-end Developer',
email: '[email protected]',
role: 'Member'
},
{
id: 2,
name: 'Courtney Henry',
position: 'Designer',
email: '[email protected]',
role: 'Admin'
},
{
id: 3,
name: 'Tom Cook',
position: 'Director of Product',
email: '[email protected]',
role: 'Member'
},
{
id: 4,
name: 'Whitney Francis',
position: 'Copywriter',
email: '[email protected]',
role: 'Admin'
},
{
id: 5,
name: 'Leonard Krasner',
position: 'Senior Designer',
email: '[email protected]',
role: 'Owner'
},
{
id: 6,
name: 'Floyd Miles',
position: 'Principal Designer',
email: '[email protected]',
role: 'Member'
}
])
const columns: TableColumn<User>[] = [
{
accessorKey: 'id',
header: 'ID'
},
{
accessorKey: 'name',
header: 'Name'
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'role',
header: 'Role'
},
{
id: 'action'
}
]
function getDropdownActions(user: User): DropdownMenuItem[][] {
return [
[
{
label: 'Copy user Id',
icon: 'i-lucide-copy',
onSelect: () => {
navigator.clipboard.writeText(user.id.toString())
toast.add({
title: 'User ID copied to clipboard!',
color: 'success',
icon: 'i-lucide-circle-check'
})
}
}
],
[
{
label: 'Edit',
icon: 'i-lucide-edit'
},
{
label: 'Delete',
icon: 'i-lucide-trash',
color: 'error'
}
]
]
}
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1">
<template #name-cell="{ row }">
<div class="flex items-center gap-3">
<UAvatar
:src="`https://i.pravatar.cc/120?img=${row.original.id}`"
size="lg"
:alt="`${row.original.name} avatar`"
/>
<div>
<p class="font-medium text-highlighted">
{{ row.original.name }}
</p>
<p>
{{ row.original.position }}
</p>
</div>
</div>
</template>
<template #action-cell="{ row }">
<UDropdownMenu :items="getDropdownActions(row.original)">
<UButton
icon="i-lucide-ellipsis-vertical"
color="neutral"
variant="ghost"
aria-label="Actions"
/>
</UDropdownMenu>
</template>
</UTable>
</template>
API
属性
属性 | 默认值 | 类型 |
---|---|---|
as |
|
此组件应渲染为的元素或组件。 |
data |
| |
columns |
| |
caption |
| |
meta |
你可以将任何对象传递给 | |
空状态 |
|
表格为空时显示的文本。 |
sticky |
|
表格是否应具有粘性表头。 |
loading |
表格是否应处于加载状态。 | |
loadingColor |
|
|
loadingAnimation |
|
|
watchOptions |
|
使用 |
globalFilterOptions |
| |
columnFiltersOptions |
| |
columnPinningOptions |
| |
columnSizingOptions |
| |
visibilityOptions |
| |
sortingOptions |
| |
groupingOptions |
| |
expandedOptions |
| |
rowSelectionOptions |
| |
rowPinningOptions |
| |
paginationOptions |
| |
facetedOptions |
| |
state |
| |
renderFallbackValue |
| |
_features |
你可以添加到表格实例的额外功能数组。 | |
autoResetAll |
设置此选项以覆盖任何 | |
debugAll |
将此选项设置为 | |
debugCells |
将此选项设置为 | |
debugColumns |
将此选项设置为 | |
debugHeaders |
将此选项设置为 | |
debugRows |
将此选项设置为 | |
debugTable |
将此选项设置为 | |
defaultColumn |
用于提供给表格的所有列定义使用的默认列选项。 | |
getRowId |
此可选函数用于为任何给定行派生唯一的 ID。如果未提供,则使用行的索引(嵌套行使用其祖父母的索引通过 | |
getSubRows |
此可选函数用于访问任何给定行的子行。如果您使用嵌套行,您需要使用此函数从行中返回子行对象(或 undefined)。 | |
initialState |
使用此选项可以可选地向表格传递初始状态。此状态将在表格自动重置各种表格状态时使用(例如 当此对象发生变化时,表格状态不会重置,这也意味着初始状态对象不需要是稳定的。
| |
mergeOptions |
此选项用于可选地实现表格选项的合并。 | |
globalFilter |
|
|
columnFilters |
|
|
columnOrder |
|
|
columnVisibility |
|
|
columnPinning |
|
|
columnSizing |
|
|
columnSizingInfo |
|
|
rowSelection |
|
|
rowPinning |
|
|
sorting |
|
|
grouping |
|
|
expanded |
|
|
pagination |
|
|
ui |
|
插槽
Slot | 类型 |
---|---|
expanded |
|
空状态 |
|
loading |
|
caption |
|
暴露
您可以使用以下方式访问带类型组件实例:useTemplateRef
.
<script setup lang="ts">
const table = useTemplateRef('table')
</script>
<template>
<UTable ref="table" />
</template>
这将让您访问以下内容:
姓名 | 类型 |
---|---|
tableRef | Ref<HTMLTableElement | null> |
tableApi | Ref<Table | null> |
主题
export default defineAppConfig({
ui: {
table: {
slots: {
root: 'relative overflow-auto',
base: 'min-w-full overflow-clip',
caption: 'sr-only',
thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)',
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
tr: 'data-[selected=true]:bg-elevated/50',
th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
empty: 'py-6 text-center text-sm text-muted',
loading: 'py-6 text-center'
},
variants: {
pinned: {
true: {
th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
}
},
sticky: {
true: {
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
}
},
loading: {
true: {
thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px'
}
},
loadingAnimation: {
carousel: '',
'carousel-inverse': '',
swing: '',
elastic: ''
},
loadingColor: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
}
},
compoundVariants: [
{
loading: true,
loadingColor: 'primary',
class: {
thead: 'after:bg-primary'
}
},
{
loading: true,
loadingColor: 'neutral',
class: {
thead: 'after:bg-inverted'
}
},
{
loading: true,
loadingAnimation: 'carousel',
class: {
thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'carousel-inverse',
class: {
thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'swing',
class: {
thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'elastic',
class: {
thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
}
}
],
defaultVariants: {
loadingColor: 'primary',
loadingAnimation: 'carousel'
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
table: {
slots: {
root: 'relative overflow-auto',
base: 'min-w-full overflow-clip',
caption: 'sr-only',
thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)',
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
tr: 'data-[selected=true]:bg-elevated/50',
th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
empty: 'py-6 text-center text-sm text-muted',
loading: 'py-6 text-center'
},
variants: {
pinned: {
true: {
th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
}
},
sticky: {
true: {
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
}
},
loading: {
true: {
thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px'
}
},
loadingAnimation: {
carousel: '',
'carousel-inverse': '',
swing: '',
elastic: ''
},
loadingColor: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
}
},
compoundVariants: [
{
loading: true,
loadingColor: 'primary',
class: {
thead: 'after:bg-primary'
}
},
{
loading: true,
loadingColor: 'neutral',
class: {
thead: 'after:bg-inverted'
}
},
{
loading: true,
loadingAnimation: 'carousel',
class: {
thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'carousel-inverse',
class: {
thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'swing',
class: {
thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'elastic',
class: {
thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
}
}
],
defaultVariants: {
loadingColor: 'primary',
loadingAnimation: 'carousel'
}
}
}
})
]
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import uiPro from '@nuxt/ui-pro/vite'
export default defineConfig({
plugins: [
vue(),
uiPro({
ui: {
table: {
slots: {
root: 'relative overflow-auto',
base: 'min-w-full overflow-clip',
caption: 'sr-only',
thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)',
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
tr: 'data-[selected=true]:bg-elevated/50',
th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
empty: 'py-6 text-center text-sm text-muted',
loading: 'py-6 text-center'
},
variants: {
pinned: {
true: {
th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
}
},
sticky: {
true: {
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
}
},
loading: {
true: {
thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px'
}
},
loadingAnimation: {
carousel: '',
'carousel-inverse': '',
swing: '',
elastic: ''
},
loadingColor: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
}
},
compoundVariants: [
{
loading: true,
loadingColor: 'primary',
class: {
thead: 'after:bg-primary'
}
},
{
loading: true,
loadingColor: 'neutral',
class: {
thead: 'after:bg-inverted'
}
},
{
loading: true,
loadingAnimation: 'carousel',
class: {
thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'carousel-inverse',
class: {
thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'swing',
class: {
thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'elastic',
class: {
thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
}
}
],
defaultVariants: {
loadingColor: 'primary',
loadingAnimation: 'carousel'
}
}
}
})
]
})