Table
用法
表格组件是基于TanStack Table构建的,并由useVueTable可组合项提供灵活且完全类型安全的 API。 TanStack Table 的某些功能尚不支持,我们将随着时间推移添加更多功能。
# | 日期 | 状态 | 金额 | |||
---|---|---|---|---|---|---|
#4600 | Mar 11, 15:30 | 已支付 | €594.00 | |||
#4599 | Mar 11, 10:10 | 失败 | €276.00 | |||
#4598 | Mar 11, 08:50 | 已退款 | €315.00 | |||
#4597 | Mar 10, 19:45 | 已支付 | €529.00 | |||
#4596 | Mar 10, 15:55 | 已支付 | €639.00 | |||
#4595 | Mar 10, 13:40 | 已退款 | €428.00 | |||
#4594 | Mar 10, 09:15 | 已支付 | €683.00 | |||
#4593 | Mar 9, 20:25 | 失败 | €947.00 | |||
#4592 | Mar 9, 18:45 | 已支付 | €851.00 | |||
#4591 | Mar 9, 16:05 | 已支付 | €762.00 | |||
#4590 | Mar 9, 14:20 | 已支付 | €573.00 | |||
#4589 | Mar 9, 11:35 | 失败 | €389.00 | |||
#4588 | Mar 8, 22:50 | 已退款 | €701.00 | |||
#4587 | Mar 8, 20:15 | 已支付 | €856.00 | |||
#4586 | Mar 8, 17:40 | 已支付 | €492.00 | |||
#4585 | Mar 8, 14:55 | 失败 | €637.00 | |||
#4584 | Mar 8, 12:30 | 已支付 | €784.00 | |||
#4583 | Mar 8, 09:45 | 已退款 | €345.00 | |||
#4582 | Mar 7, 23:10 | 已支付 | €918.00 | |||
#4581 | Mar 7, 20:25 | 已支付 | €567.00 |
数据
将 data
属性用作对象数组,列将根据对象的键生成。
Id | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
4600 | 2024-03-11T15:30:00 | 已支付 | [email protected] | 594 |
4599 | 2024-03-11T10:10:00 | 失败 | [email protected] | 276 |
4598 | 2024-03-11T08:50:00 | 已退款 | [email protected] | 315 |
4597 | 2024-03-10T19:45:00 | 已支付 | [email protected] | 529 |
4596 | 2024-03-10T15:55:00 | 已支付 | [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
属性用作数组ColumnDef对象,其属性包括
accessorKey
: 提取列值时使用的行对象的键。header
: 要为列显示的标题。如果传入字符串,它可以用作列 ID 的默认值。如果传入函数,它将接收一个用于标题的 props 对象,并应返回渲染的标题值(具体类型取决于所使用的适配器)。footer
: 要为列显示的页脚。功能与标题完全相同,但显示在表格下方。cell
: 要为列显示的每一行单元格。如果传入函数,它将接收一个用于单元格的 props 对象,并应返回渲染的单元格值(具体类型取决于所使用的适配器)。meta
: 列的额外属性。class
:td
: 要应用于td
元素的类。th
: 要应用于th
元素的类。
style
:td
: 要应用于td
元素的样式。th
: 要应用于th
元素的样式。
为了渲染组件或其他 HTML 元素,您需要使用 Vueh
函数在 header
和 cell
属性内部。这与其他使用插槽的组件不同,但提供了更大的灵活性。
# | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
#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 { 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
属性用作对象(TableMeta)来传递属性,例如
class
:tr
: 要应用于tr
元素的类。
style
:tr
: 要应用于tr
元素的样式。
加载中
使用 loading
属性显示加载状态,使用 loading-color
属性更改其颜色,使用 loading-animation
属性更改其动画。
Id | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
4600 | 2024-03-11T15:30:00 | 已支付 | [email protected] | 594 |
4599 | 2024-03-11T10:10:00 | 失败 | [email protected] | 276 |
4598 | 2024-03-11T08:50:00 | 已退款 | [email protected] | 315 |
4597 | 2024-03-10T19:45:00 | 已支付 | [email protected] | 529 |
4596 | 2024-03-10T15:55:00 | 已支付 | [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
属性使标题或页脚固定。
Id | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
4600 | 2024-03-11T15:30:00 | 已支付 | [email protected] | 594 |
4599 | 2024-03-11T10:10:00 | 失败 | [email protected] | 276 |
4598 | 2024-03-11T08:50:00 | 已退款 | [email protected] | 315 |
4597 | 2024-03-10T19:45:00 | 已支付 | [email protected] | 529 |
4596 | 2024-03-10T15:55:00 | 已支付 | [email protected] | 639 |
4595 | 2024-03-10T15:55:00 | 已支付 | [email protected] | 639 |
4594 | 2024-03-10T15:55:00 | 已支付 | [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 | 已支付 | [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 { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table'
import { useClipboard } from '@vueuse/core'
const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')
const toast = useToast()
const { copy } = useClipboard()
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() {
copy(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 | 已支付 | [email protected] | €594.00 | |
#4599 | Mar 11, 10:10 | 失败 | [email protected] | €276.00 | |
{ "id": "4599", "date": "2024-03-11T10:10:00", "status": "failed", "email": "[email protected]", "amount": 276 } | |||||
#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 { 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
属性控制行的可展开状态(可以使用 v-model
进行绑定)。actions
列中的 DropdownMenu
组件。带分组行
您可以根据给定的列值对行进行分组,并通过添加到单元格的按钮使用 TanStack Table 显示/隐藏子行分组 API.
重要部分
- 添加
grouping
属性,其中包含要分组的列 ID 数组。 - 添加
grouping-options
属性。它必须包含getGroupedRowModel
,您可以从@tanstack/vue-table
导入它或自行实现。 - 通过行上任何单元格的
row.toggleExpanded()
方法展开行。请记住,它也会切换#expanded
插槽。 - 在列定义上使用
aggregateFn
定义如何聚合行。 - 列定义上的
agregatedCell
渲染器仅在没有cell
渲染器时才起作用。
项目 | # | 日期 | 邮箱 | 金额 |
---|---|---|---|---|
Account 1 | 3 条记录 | Mar 11, 15:30 | 3 位客户 | €1,548.00 |
Account 2 | 2 条记录 | Mar 11, 10:10 | 2 位客户 | €805.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import { getGroupedRowModel } from '@tanstack/vue-table'
import type { GroupingOptions } from '@tanstack/vue-table'
const UBadge = resolveComponent('UBadge')
type Account = {
id: string
name: string
}
type PaymentStatus = 'paid' | 'failed' | 'refunded'
type Payment = {
id: string
date: string
status: PaymentStatus
email: string
amount: number
account: Account
}
const getColorByStatus = (status: PaymentStatus) => {
return {
paid: 'success',
failed: 'error',
refunded: 'neutral'
}[status]
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: '[email protected]',
amount: 594,
account: {
id: '1',
name: 'Account 1'
}
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: '[email protected]',
amount: 276,
account: {
id: '2',
name: 'Account 2'
}
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: '[email protected]',
amount: 315,
account: {
id: '1',
name: 'Account 1'
}
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: '[email protected]',
amount: 529,
account: {
id: '2',
name: 'Account 2'
}
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: '[email protected]',
amount: 639,
account: {
id: '1',
name: 'Account 1'
}
}
])
const columns: TableColumn<Payment>[] = [
{
id: 'title',
header: 'Item'
},
{
id: 'account_id',
accessorKey: 'account.id'
},
{
accessorKey: 'id',
header: '#',
cell: ({ row }) =>
row.getIsGrouped() ? `${row.getValue('id')} records` : `#${row.getValue('id')}`,
aggregationFn: 'count'
},
{
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
})
},
aggregationFn: 'max'
},
{
accessorKey: 'status',
header: 'Status'
},
{
accessorKey: 'email',
header: 'Email',
meta: {
class: {
td: 'w-full'
}
},
cell: ({ row }) =>
row.getIsGrouped() ? `${row.getValue('email')} customers` : row.getValue('email'),
aggregationFn: 'uniqueCount'
},
{
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)
},
aggregationFn: 'sum'
}
]
const grouping_options = ref<GroupingOptions>({
groupedColumnMode: 'remove',
getGroupedRowModel: getGroupedRowModel()
})
</script>
<template>
<UTable
:data="data"
:columns="columns"
:grouping="['account_id', 'status']"
:grouping-options="grouping_options"
:ui="{
root: 'min-w-full',
td: 'empty:p-0' // helps with the colspaned row added for expand slot
}"
>
<template #title-cell="{ row }">
<div v-if="row.getIsGrouped()" class="flex items-center">
<span class="inline-block" :style="{ width: `calc(${row.depth} * 1rem)` }" />
<UButton
variant="outline"
color="neutral"
class="mr-2"
size="xs"
:icon="row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus'"
@click="row.toggleExpanded()"
/>
<strong v-if="row.groupingColumnId === 'account_id'">{{
row.original.account.name
}}</strong>
<UBadge
v-else-if="row.groupingColumnId === 'status'"
:color="getColorByStatus(row.original.status)"
class="capitalize"
variant="subtle"
>
{{ row.original.status }}
</UBadge>
</div>
</template>
</UTable>
</template>
带行选择
您可以添加一个新列,该列在 header
和 cell
内部渲染一个 Checkbox 组件,以使用 TanStack Table 选择行行选择 API.
日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|
Mar 11, 15:30 | 已支付 | [email protected] | €594.00 | |
Mar 11, 10:10 | 失败 | [email protected] | €276.00 | |
Mar 11, 08:50 | 已退款 | [email protected] | €315.00 | |
Mar 10, 19:45 | 已支付 | [email protected] | €529.00 | |
Mar 10, 15:55 | 已支付 | [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
属性控制行的选择状态(可以使用 v-model
进行绑定)。带行选择事件
您可以添加 @select
监听器,使行可点击,无论是否有复选框列。
TableRow
实例作为第一个参数,以及可选的 Event
作为第二个参数。日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|
Mar 11, 15:30 | 已支付 | [email protected] | €594.00 | |
Mar 11, 10:10 | 失败 | [email protected] | €276.00 | |
Mar 11, 08:50 | 已退款 | [email protected] | €315.00 | |
Mar 10, 19:45 | 已支付 | [email protected] | €529.00 | |
Mar 10, 15:55 | 已支付 | [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>
新增 带行上下文菜单事件
您可以添加 @contextmenu
监听器,使行可右键点击,并将表格包装在 ContextMenu 组件中,例如显示行操作。
Event
和 TableRow
实例作为第一个和第二个参数。# | 日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|---|
#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 { h, resolveComponent } from 'vue'
import type { ContextMenuItem, TableColumn, TableRow } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'
const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')
const toast = useToast()
const { copy } = useClipboard()
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: '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 items = ref<ContextMenuItem[]>([])
function getRowItems(row: TableRow<Payment>) {
return [
{
type: 'label' as const,
label: 'Actions'
},
{
label: 'Copy payment ID',
onSelect() {
copy(row.original.id)
toast.add({
title: 'Payment ID copied to clipboard!',
color: 'success',
icon: 'i-lucide-circle-check'
})
}
},
{
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
onSelect() {
row.toggleExpanded()
}
},
{
type: 'separator' as const
},
{
label: 'View customer'
},
{
label: 'View payment details'
}
]
}
function onContextmenu(_e: Event, row: TableRow<Payment>) {
items.value = getRowItems(row)
}
</script>
<template>
<UContextMenu :items="items">
<UTable :data="data" :columns="columns" class="flex-1" @contextmenu="onContextmenu">
<template #expanded="{ row }">
<pre>{{ row.original }}</pre>
</template>
</UTable>
</UContextMenu>
</template>
新增 带行悬停事件
您可以添加 @hover
监听器,使行可悬停,并使用 Popover 或 Tooltip 组件显示行详细信息,例如。
Event
和 TableRow
实例作为第一个和第二个参数。# | 日期 | 状态 | 邮箱 | 金额 | |
---|---|---|---|---|---|
#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 { 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: '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 anchor = ref({ x: 0, y: 0 })
const reference = computed(() => ({
getBoundingClientRect: () =>
({
width: 0,
height: 0,
left: anchor.value.x,
right: anchor.value.x,
top: anchor.value.y,
bottom: anchor.value.y,
...anchor.value
}) as DOMRect
}))
const open = ref(false)
const openDebounced = refDebounced(open, 10)
const selectedRow = ref<TableRow<Payment> | null>(null)
function onHover(_e: Event, row: TableRow<Payment> | null) {
selectedRow.value = row
open.value = !!row
}
</script>
<template>
<div class="flex w-full flex-1 gap-1">
<UTable
:data="data"
:columns="columns"
class="flex-1"
@pointermove="
(ev: PointerEvent) => {
anchor.x = ev.clientX
anchor.y = ev.clientY
}
"
@hover="onHover"
/>
<UPopover
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
:open="openDebounced"
:reference="reference"
>
<template #content>
<div class="p-4">
{{ selectedRow?.original?.id }}
</div>
</template>
</UPopover>
</div>
</template>
refDebounced
以防止当光标从一行移动到另一行时,Popover 过快地打开和关闭。新增 带列页脚
您可以为列定义添加一个 footer
属性,以渲染列的页脚。
# | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
#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 |
总计: €2,353.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } 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'),
footer: ({ column }) => {
const total = column
.getFacetedRowModel()
.rows.reduce(
(acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')),
0
)
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(total)
return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`)
},
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>
带列排序
您可以更新列 header
,以在 header
内部渲染一个 Button 组件,从而使用 TanStack Table 切换排序状态排序 API.
# | 日期 | 状态 | 金额 | |
---|---|---|---|---|
#4597 | Mar 10, 19:45 | 已支付 | [email protected] | €529.00 |
#4596 | Mar 10, 15:55 | 已支付 | [email protected] | €639.00 |
#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 |
<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
属性控制列的排序状态(可以使用 v-model
进行绑定)。您还可以创建一个可复用组件,使任何列标题都可排序。
#4596 | Mar 10, 15:55 | 已支付 | [email protected] | €639.00 |
#4597 | Mar 10, 19:45 | 已支付 | [email protected] | €529.00 |
#4598 | Mar 11, 08:50 | 已退款 | [email protected] | €315.00 |
#4599 | Mar 11, 10:10 | 失败 | [email protected] | €276.00 |
#4600 | Mar 11, 15:30 | 已支付 | [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 | 已支付 | [email protected] | €594,000.00 |
#45990000000000000000000000000000000000000 | 2024-03-11T10:10:00 | 失败 | [email protected] | €276,000.00 |
#45980000000000000000000000000000000000000 | 2024-03-11T08:50:00 | 已退款 | [email protected] | €315,000.00 |
#45970000000000000000000000000000000000000 | 2024-03-10T19:45:00 | 已支付 | [email protected] | €5,290,000.00 |
#45960000000000000000000000000000000000000 | 2024-03-10T15:55:00 | 已支付 | [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
属性控制列的固定状态(可以使用 v-model
进行绑定)。带列可见性
您可以使用 DropdownMenu 组件,通过 TanStack Table 切换列的可见性列可见性 API.
日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|
Mar 11, 15:30 | 已支付 | [email protected] | €594.00 |
Mar 11, 10:10 | 失败 | [email protected] | €276.00 |
Mar 11, 08:50 | 已退款 | [email protected] | €315.00 |
Mar 10, 19:45 | 已支付 | [email protected] | €529.00 |
Mar 10, 15:55 | 已支付 | [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
属性控制列的可见性状态(可以使用 v-model
进行绑定)。带列过滤器
您可以使用 Input 组件,通过 TanStack Table 按列过滤行列过滤 API.
# | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
#4600 | Mar 11, 15:30 | 已支付 | [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
属性控制列的过滤状态(可以使用 v-model
进行绑定)。带全局过滤器
您可以使用 Input 组件,通过 TanStack Table 过滤行全局过滤 API.
# | 日期 | 状态 | 邮箱 | 金额 |
---|---|---|---|---|
#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 { 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
属性控制全局过滤状态(可以使用 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
属性控制分页状态(可以使用 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
启用表格的拖放功能。此集成封装了Sortable.js以提供无缝的拖放体验。
:ui
属性为其添加一个唯一的类,以便使用 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 前端开发人员 | [email protected] | 成员 | |
2 | Courtney Henry 设计师 | [email protected] | 管理员 | |
3 | Tom Cook 产品总监 | [email protected] | 成员 | |
4 | Whitney Francis 文案 | [email protected] | 管理员 | |
5 | Leonard Krasner 高级设计师 | [email protected] | 所有者 | |
6 | Floyd Miles 首席设计师 | [email protected] | 成员 |
<script setup lang="ts">
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'
interface User {
id: number
name: string
position: string
email: string
role: string
}
const toast = useToast()
const { copy } = useClipboard()
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: () => {
copy(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 |
| |
标题 |
| |
meta |
您可以将任何对象传递给 | |
空 |
|
表格为空时显示的文本。 |
sticky |
|
表格是否应有固定标题或页脚。`true` 表示两者都固定,`'header'` 表示仅标题固定,`'footer'` 表示仅页脚固定。 |
loading |
表格是否应处于加载状态。 | |
loadingColor |
|
|
loadingAnimation |
|
|
watchOptions |
|
使用 |
globalFilterOptions |
| |
columnFiltersOptions |
| |
columnPinningOptions |
| |
columnSizingOptions |
| |
visibilityOptions |
| |
sortingOptions |
| |
groupingOptions |
| |
expandedOptions |
| |
rowSelectionOptions |
| |
rowPinningOptions |
| |
paginationOptions |
| |
facetedOptions |
| |
onSelect |
| |
onHover |
| |
onContextmenu |
| |
state |
| |
onStateChange |
| |
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 |
|
插槽
插槽 | 类型 |
---|---|
expanded |
|
空 |
|
loading |
|
标题 |
|
body-top |
|
body-bottom |
|
可访问属性
您可以使用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',
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
tfoot: 'relative',
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',
separator: 'absolute z-[1] left-0 w-full h-px bg-(--ui-border-accented)',
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',
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
},
header: {
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
},
footer: {
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
}
},
loading: {
true: {
thead: 'after:absolute after:z-[1] 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',
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
tfoot: 'relative',
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',
separator: 'absolute z-[1] left-0 w-full h-px bg-(--ui-border-accented)',
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',
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
},
header: {
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
},
footer: {
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
}
},
loading: {
true: {
thead: 'after:absolute after:z-[1] 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',
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
tfoot: 'relative',
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',
separator: 'absolute z-[1] left-0 w-full h-px bg-(--ui-border-accented)',
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',
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
},
header: {
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
},
footer: {
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
}
},
loading: {
true: {
thead: 'after:absolute after:z-[1] 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'
}
}
}
})
]
})