用法
Table组件构建在TanStack Table v8之上,并由useVueTable组合提供支持,以提供灵活且完全类型安全的API。
它将您的数据呈现为行和列,并支持排序,过滤,分页,行选择,扩展,分组,固定和虚拟化,因此您可以构建从简单的数据表到功能齐全的数据网格的一切。
| # | Date | Status | Amount | |||
|---|---|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | ||
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | ||
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | ||
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | ||
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €639.00 | ||
| #4595 | Mar 10, 13:40 | refunded | ava.thomas@example.com | €428.00 | ||
| #4594 | Mar 10, 09:15 | paid | michael.wilson@example.com | €683.00 | ||
| #4593 | Mar 9, 20:25 | failed | olivia.taylor@example.com | €947.00 | ||
| #4592 | Mar 9, 18:45 | paid | benjamin.jackson@example.com | €851.00 | ||
| #4591 | Mar 9, 16:05 | paid | sophia.miller@example.com | €762.00 | ||
| #4590 | Mar 9, 14:20 | paid | noah.clark@example.com | €573.00 | ||
| #4589 | Mar 9, 11:35 | failed | isabella.lee@example.com | €389.00 | ||
| #4588 | Mar 8, 22:50 | refunded | liam.walker@example.com | €701.00 | ||
| #4587 | Mar 8, 20:15 | paid | charlotte.hall@example.com | €856.00 | ||
| #4586 | Mar 8, 17:40 | paid | mason.young@example.com | €492.00 | ||
| #4585 | Mar 8, 14:55 | failed | amelia.king@example.com | €637.00 | ||
| #4584 | Mar 8, 12:30 | paid | elijah.wright@example.com | €784.00 | ||
| #4583 | Mar 8, 09:45 | refunded | harper.scott@example.com | €345.00 | ||
| #4582 | Mar 7, 23:10 | paid | evelyn.green@example.com | €918.00 | ||
| #4581 | Mar 7, 20:25 | paid | logan.baker@example.com | €567.00 |
Data
使用data prop作为对象数组,列将根据对象的键生成。
| Id | Date | Status | Amount | |
|---|---|---|---|---|
| 4600 | 2024-03-11T15:30:00 | paid | james.anderson@example.com | 594 |
| 4599 | 2024-03-11T10:10:00 | failed | mia.white@example.com | 276 |
| 4598 | 2024-03-11T08:50:00 | refunded | william.brown@example.com | 315 |
| 4597 | 2024-03-10T19:45:00 | paid | emma.davis@example.com | 529 |
| 4596 | 2024-03-10T15:55:00 | paid | ethan.harris@example.com | 639 |
<script setup lang="ts">
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
</script>
<template>
<UTable :data="data" class="flex-1" />
</template><script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
</script>
<template>
<UTable :data="data" class="flex-1" />
</template>列
使用columns prop作为ColumnDef对象的数组,其属性如下:
accessorKey:提取列的值时要使用的行对象的键。header:要为列显示的标题。如果传递字符串,则可以将其用作列ID的默认值。如果传递函数,则会传递标题的props对象,并应返回呈现的标题值(确切类型取决于所使用的适配器)。footer:要为列显示的页脚。与页眉完全相同,但显示在表下。cell:显示列的每一行的单元格。如果传递一个函数,它将被传递一个单元格的props对象,并且应该返回呈现的单元格值(确切的类型取决于所使用的适配器)。meta:列的额外属性。
要渲染组件或其他HTML元素,需要在header和cell props中使用Vue h函数。这与其他使用插槽的组件不同,但允许更大的灵活性。
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template>h渲染组件时,可以使用resolveComponent函数或从#components导入。Meta
使用meta prop作为对象(TableMeta)来传递属性,如:
class:tr:应用于tr元素的类。 {class="text-muted"}
style:tr:应用于tr元素的样式。 {class="text-muted"}
| ID | Date | Status | Amount | |
|---|---|---|---|---|
| 4600 | Mar 11, 15:30 | paid | james.anderson@example.com | $594.00 |
| 4599 | Mar 11, 10:10 | failed | mia.white@example.com | $276.00 |
| 4598 | Mar 11, 08:50 | refunded | william.brown@example.com | $315.00 |
| 4597 | Mar 10, 19:45 | paid | emma.davis@example.com | $529.00 |
| 4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | $639.00 |
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import type { TableMeta, Row } from '@tanstack/vue-table'
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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: 'ID',
meta: {
class: {
th: 'text-center font-semibold',
td: 'text-center font-mono'
}
}
},
{
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',
meta: {
class: {
th: 'text-center',
td: 'text-center'
}
},
cell: ({ row }) => {
const status = row.getValue('status') as string
const colorMap = {
paid: 'text-success',
failed: 'text-error',
refunded: 'text-warning'
}
return h(
'span',
{ class: `font-semibold capitalize ${colorMap[status as keyof typeof colorMap]}` },
status
)
}
},
{
accessorKey: 'email',
header: 'Email',
meta: {
class: {
th: 'text-left',
td: 'text-left'
}
}
},
{
accessorKey: 'amount',
header: 'Amount',
meta: {
class: {
th: 'text-right font-bold text-primary',
td: 'text-right font-mono'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
return h('span', { class: 'font-semibold text-success' }, formatted)
}
}
]
const meta: TableMeta<Payment> = {
class: {
tr: (row: Row<Payment>) => {
if (row.original.status === 'failed') {
return 'bg-error/10'
}
if (row.original.status === 'refunded') {
return 'bg-warning/10'
}
return ''
}
}
}
</script>
<template>
<UTable :data="data" :columns="columns" :meta="meta" class="flex-1" />
</template><script setup lang="ts">
import { ref, h } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { TableMeta, Row } from '@tanstack/vue-table'
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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: 'ID',
meta: {
class: {
th: 'text-center font-semibold',
td: 'text-center font-mono'
}
}
},
{
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',
meta: {
class: {
th: 'text-center',
td: 'text-center'
}
},
cell: ({ row }) => {
const status = row.getValue('status') as string
const colorMap = {
paid: 'text-success',
failed: 'text-error',
refunded: 'text-warning'
}
return h(
'span',
{ class: `font-semibold capitalize ${colorMap[status as keyof typeof colorMap]}` },
status
)
}
},
{
accessorKey: 'email',
header: 'Email',
meta: {
class: {
th: 'text-left',
td: 'text-left'
}
}
},
{
accessorKey: 'amount',
header: 'Amount',
meta: {
class: {
th: 'text-right font-bold text-primary',
td: 'text-right font-mono'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
return h('span', { class: 'font-semibold text-success' }, formatted)
}
}
]
const meta: TableMeta<Payment> = {
class: {
tr: (row: Row<Payment>) => {
if (row.original.status === 'failed') {
return 'bg-error/10'
}
if (row.original.status === 'refunded') {
return 'bg-warning/10'
}
return ''
}
}
}
</script>
<template>
<UTable :data="data" :columns="columns" :meta="meta" class="flex-1" />
</template>加载中
使用loading prop显示加载状态,使用loading-color prop更改其颜色,使用loading-animation prop更改其动画。
| Id | Date | Status | Amount | |
|---|---|---|---|---|
| 4600 | 2024-03-11T15:30:00 | paid | james.anderson@example.com | 594 |
| 4599 | 2024-03-11T10:10:00 | failed | mia.white@example.com | 276 |
| 4598 | 2024-03-11T08:50:00 | refunded | william.brown@example.com | 315 |
| 4597 | 2024-03-10T19:45:00 | paid | emma.davis@example.com | 529 |
| 4596 | 2024-03-10T15:55:00 | paid | ethan.harris@example.com | 639 |
<script setup lang="ts">
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
</script>
<template>
<UTable
loading
loading-color="primary"
loading-animation="carousel"
:data="data"
class="flex-1"
/>
</template><script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
</script>
<template>
<UTable
loading
loading-color="primary"
loading-animation="carousel"
:data="data"
class="flex-1"
/>
</template>Sticky
使用sticky属性使页眉或页脚具有粘性。
| Id | Date | Status | Amount | |
|---|---|---|---|---|
| 4600 | 2024-03-11T15:30:00 | paid | james.anderson@example.com | 594 |
| 4599 | 2024-03-11T10:10:00 | failed | mia.white@example.com | 276 |
| 4598 | 2024-03-11T08:50:00 | refunded | william.brown@example.com | 315 |
| 4597 | 2024-03-10T19:45:00 | paid | emma.davis@example.com | 529 |
| 4596 | 2024-03-10T15:55:00 | paid | ethan.harris@example.com | 639 |
| 4595 | 2024-03-10T15:55:00 | paid | ethan.harris@example.com | 639 |
| 4594 | 2024-03-10T15:55:00 | paid | ethan.harris@example.com | 639 |
<script setup lang="ts">
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4594',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
</script>
<template>
<UTable sticky :data="data" class="flex-1 max-h-[312px]" />
</template><script setup lang="ts">
import { ref } from 'vue'
const data = ref([
{
id: '4600',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4594',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
</script>
<template>
<UTable sticky :data="data" class="flex-1 max-h-[312px]" />
</template>示例
使用行操作
您可以添加一个新列,在cell中呈现DropdownMenu组件,以呈现行操作。
| # | Date | Status | Amount | ||
|---|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
},
{
id: 'actions',
meta: {
class: {
td: 'text-right'
}
},
cell: ({ row }) => {
return h(
UDropdownMenu,
{
content: {
align: 'end'
},
items: getRowItems(row),
'aria-label': 'Actions dropdown'
},
() =>
h(UButton, {
icon: 'i-lucide-ellipsis-vertical',
color: 'neutral',
variant: 'ghost',
'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><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
},
{
id: 'actions',
meta: {
class: {
td: 'text-right'
}
},
cell: ({ row }) => {
return h(
UDropdownMenu,
{
content: {
align: 'end'
},
items: getRowItems(row),
'aria-label': 'Actions dropdown'
},
() =>
h(UButton, {
icon: 'i-lucide-ellipsis-vertical',
color: 'neutral',
variant: 'ghost',
'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 Expanding APIs切换行的可扩展状态。
#expanded插槽来呈现扩展的内容,它将接收行作为参数。| # | Date | Status | Amount | ||
|---|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
{
"id": "4599",
"date": "2024-03-11T10:10:00",
"status": "failed",
"email": "mia.white@example.com",
"amount": 276
} | |||||
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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 Is通过添加到单元格的一些按钮显示/隐藏子行。
重要部件
- 添加
groupingprop,其中包含要分组的列ID数组。 - 添加
grouping-optionsprop。它必须包含getGroupedRowModel,您可以从@tanstack/vue-table导入或实现自己的prop。 - 通过
row.toggleExpanded()方法在行的任何单元格上扩展行。记住,它也切换#expanded插槽。 - 在列定义上使用
aggregateFn来定义如何聚合行。 列定义上的*agregatedCell渲染器仅在没有cell渲染器的情况下有效。
| Item | # | Date | Amount | |
|---|---|---|---|---|
Account 1 | 3 records | Mar 11, 15:30 | 3 customers | €1,548.00 |
Account 2 | 2 records | Mar 11, 10:10 | 2 customers | €805.00 |
<script setup lang="ts">
import { 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: 'james.anderson@example.com',
amount: 594,
account: {
id: '1',
name: 'Account 1'
}
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276,
account: {
id: '2',
name: 'Account 2'
}
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315,
account: {
id: '1',
name: 'Account 1'
}
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529,
account: {
id: '2',
name: 'Account 2'
}
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
},
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><script setup lang="ts">
import { resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594,
account: {
id: '1',
name: 'Account 1'
}
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276,
account: {
id: '2',
name: 'Account 2'
}
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315,
account: {
id: '1',
name: 'Account 1'
}
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529,
account: {
id: '2',
name: 'Account 2'
}
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
},
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>行固定:badge{label="4.6+" class="align-text-top"}
您可以在cell内添加一个呈现Button组件的列,以使用TanStack表行固定API 切换行的固定状态。无论排序或筛选如何,固定的行都将位于表的顶部或底部。
| Date | Status | Amount | ||
|---|---|---|---|---|
| Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| Mar 10, 15:55 | paid | ethan.harris@example.com | €639.00 | |
| Mar 10, 13:40 | refunded | ava.thomas@example.com | €428.00 | |
| Mar 10, 09:15 | paid | michael.wilson@example.com | €683.00 | |
| Mar 9, 20:25 | failed | olivia.taylor@example.com | €947.00 | |
| Mar 9, 18:45 | paid | benjamin.jackson@example.com | €851.00 | |
| Mar 9, 16:05 | paid | sophia.miller@example.com | €762.00 |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { RowPinningState } from '@tanstack/table-core'
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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T13:40:00',
status: 'refunded',
email: 'ava.thomas@example.com',
amount: 428
},
{
id: '4594',
date: '2024-03-10T09:15:00',
status: 'paid',
email: 'michael.wilson@example.com',
amount: 683
},
{
id: '4593',
date: '2024-03-09T20:25:00',
status: 'failed',
email: 'olivia.taylor@example.com',
amount: 947
},
{
id: '4592',
date: '2024-03-09T18:45:00',
status: 'paid',
email: 'benjamin.jackson@example.com',
amount: 851
},
{
id: '4591',
date: '2024-03-09T16:05:00',
status: 'paid',
email: 'sophia.miller@example.com',
amount: 762
}
])
const columns: TableColumn<Payment>[] = [
{
id: 'pin',
cell: ({ row }) =>
h(UButton, {
icon: 'i-lucide-star',
color: row.getIsPinned() ? 'primary' : 'neutral',
variant: 'ghost',
'aria-label': row.getIsPinned() ? 'Unpin row' : 'Pin row to top',
onClick: () => {
if (row.getIsPinned()) {
row.pin(false)
} else {
row.pin('top')
}
}
})
},
{
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,
timeZone: 'UTC'
})
}
},
{
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const rowPinning = ref<RowPinningState>({ top: ['4599', '4597'], bottom: [] })
</script>
<template>
<UTable
v-model:row-pinning="rowPinning"
:data="data"
:columns="columns"
:get-row-id="(row: Payment) => row.id"
class="flex-1 h-96"
/>
</template><script setup lang="ts">
import { h, resolveComponent, ref } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { RowPinningState } from '@tanstack/table-core'
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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T13:40:00',
status: 'refunded',
email: 'ava.thomas@example.com',
amount: 428
},
{
id: '4594',
date: '2024-03-10T09:15:00',
status: 'paid',
email: 'michael.wilson@example.com',
amount: 683
},
{
id: '4593',
date: '2024-03-09T20:25:00',
status: 'failed',
email: 'olivia.taylor@example.com',
amount: 947
},
{
id: '4592',
date: '2024-03-09T18:45:00',
status: 'paid',
email: 'benjamin.jackson@example.com',
amount: 851
},
{
id: '4591',
date: '2024-03-09T16:05:00',
status: 'paid',
email: 'sophia.miller@example.com',
amount: 762
}
])
const columns: TableColumn<Payment>[] = [
{
id: 'pin',
cell: ({ row }) =>
h(UButton, {
icon: 'i-lucide-star',
color: row.getIsPinned() ? 'primary' : 'neutral',
variant: 'ghost',
'aria-label': row.getIsPinned() ? 'Unpin row' : 'Pin row to top',
onClick: () => {
if (row.getIsPinned()) {
row.pin(false)
} else {
row.pin('top')
}
}
})
},
{
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,
timeZone: 'UTC'
})
}
},
{
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const rowPinning = ref<RowPinningState>({ top: ['4599', '4597'], bottom: [] })
</script>
<template>
<UTable
v-model:row-pinning="rowPinning"
:data="data"
:columns="columns"
:get-row-id="(row: Payment) => row.id"
class="flex-1 h-96"
/>
</template>row-pinning属性来控制行的固定状态(可以使用v-model绑定)。带行选择
您可以添加一个新列,在header和cell中呈现Checkbox组件,以使用TanStack Table Row Selection API 选择行。
| Date | Status | Amount | ||
|---|---|---|---|---|
| Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { h, resolveComponent, ref, useTemplateRef } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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侦听器,使行可单击,无论是否有复选框列。
Event和TableRow实例作为第一个和第二个参数。| Date | Status | Amount | ||
|---|---|---|---|---|
| Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const table = useTemplateRef('table')
const rowSelection = ref<Record<string, boolean>>({})
function onSelect(e: Event, row: TableRow<Payment>) {
/* If you decide to also select the column you can do this */
row.toggleSelected(!row.getIsSelected())
}
</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><script setup lang="ts">
import { h, resolveComponent, ref, useTemplateRef } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const table = useTemplateRef('table')
const rowSelection = ref<Record<string, boolean>>({})
function onSelect(e: Event, row: TableRow<Payment>) {
/* If you decide to also select the column you can do this */
row.toggleSelected(!row.getIsSelected())
}
</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侦听器以使行可右键单击,并将Table包装在ContextMenu组件中以显示行操作。
Event和TableRow实例作为第一个和第二个参数。| # | Date | Status | Amount | ||
|---|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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侦听器以使行可悬停,并使用Pover或Tooltip组件来显示行详细信息。
Event和TableRow实例作为第一个和第二个参数。| # | Date | Status | Amount | ||
|---|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { refDebounced } from '@vueuse/core'
import { h, resolveComponent, ref, computed } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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属性,以呈现列的脚注。
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €639.00 |
| Total: €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
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 `Total: ${formatted}`
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
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 `Total: ${formatted}`
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template>带列跨度
您可以使用meta列中的colspan和rowspan属性来合并单元格。这些属性接受静态值或接收单元格并返回跨度值的函数。
rowspan时,被前一行的跨度“吸收”的单元格需要在视觉上隐藏。使用class Meta一个函数,为这些单元格返回'hidden'。| Category | Name | Price | Stock |
|---|---|---|---|
| Electronics | Laptop | $999.00 | 45 |
| Phone | $699.00 | 120 | |
| Tablet | $499.00 | 78 | |
| Clothing | T-Shirt | $29.00 | 200 |
| Jeans | $59.00 | 150 |
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import type { Cell } from '@tanstack/vue-table'
type Product = {
category: string
name: string
price: number
stock: number
}
const data = ref<Product[]>([
{ category: 'Electronics', name: 'Laptop', price: 999, stock: 45 },
{ category: 'Electronics', name: 'Phone', price: 699, stock: 120 },
{ category: 'Electronics', name: 'Tablet', price: 499, stock: 78 },
{ category: 'Clothing', name: 'T-Shirt', price: 29, stock: 200 },
{ category: 'Clothing', name: 'Jeans', price: 59, stock: 150 }
])
function getCategoryRowSpan(cell: Cell<Product, unknown>) {
const category = cell.row.original.category
const rows = cell.getContext().table.getRowModel().rows
const rowIndex = rows.findIndex((r: (typeof rows)[number]) => r.id === cell.row.id)
if (rowIndex > 0 && rows[rowIndex - 1]!.original.category === category) {
return '1'
}
let span = 1
for (let i = rowIndex + 1; i < rows.length; i++) {
if (rows[i]!.original.category === category) span++
else break
}
return `${span}`
}
function getCategoryClass(cell: Cell<Product, unknown>) {
const category = cell.row.original.category
const rows = cell.getContext().table.getRowModel().rows
const rowIndex = rows.findIndex((r: (typeof rows)[number]) => r.id === cell.row.id)
if (rowIndex > 0 && rows[rowIndex - 1]!.original.category === category) {
return 'hidden'
}
return 'font-medium align-middle border-r border-default'
}
const columns: TableColumn<Product>[] = [
{
accessorKey: 'category',
header: 'Category',
meta: {
rowspan: {
td: getCategoryRowSpan
},
class: {
td: getCategoryClass
}
}
},
{
accessorKey: 'name',
header: 'Name'
},
{
accessorKey: 'price',
header: 'Price',
meta: {
class: {
th: 'text-right',
td: 'text-right'
}
},
cell: ({ row }) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(row.getValue('price'))
}
},
{
accessorKey: 'stock',
header: 'Stock',
meta: {
class: {
th: 'text-right',
td: 'text-right'
}
}
}
]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template><script setup lang="ts">
import { ref } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Cell } from '@tanstack/vue-table'
type Product = {
category: string
name: string
price: number
stock: number
}
const data = ref<Product[]>([
{ category: 'Electronics', name: 'Laptop', price: 999, stock: 45 },
{ category: 'Electronics', name: 'Phone', price: 699, stock: 120 },
{ category: 'Electronics', name: 'Tablet', price: 499, stock: 78 },
{ category: 'Clothing', name: 'T-Shirt', price: 29, stock: 200 },
{ category: 'Clothing', name: 'Jeans', price: 59, stock: 150 }
])
function getCategoryRowSpan(cell: Cell<Product, unknown>) {
const category = cell.row.original.category
const rows = cell.getContext().table.getRowModel().rows
const rowIndex = rows.findIndex((r: (typeof rows)[number]) => r.id === cell.row.id)
if (rowIndex > 0 && rows[rowIndex - 1]!.original.category === category) {
return '1'
}
let span = 1
for (let i = rowIndex + 1; i < rows.length; i++) {
if (rows[i]!.original.category === category) span++
else break
}
return `${span}`
}
function getCategoryClass(cell: Cell<Product, unknown>) {
const category = cell.row.original.category
const rows = cell.getContext().table.getRowModel().rows
const rowIndex = rows.findIndex((r: (typeof rows)[number]) => r.id === cell.row.id)
if (rowIndex > 0 && rows[rowIndex - 1]!.original.category === category) {
return 'hidden'
}
return 'font-medium align-middle border-r border-default'
}
const columns: TableColumn<Product>[] = [
{
accessorKey: 'category',
header: 'Category',
meta: {
rowspan: {
td: getCategoryRowSpan
},
class: {
td: getCategoryClass
}
}
},
{
accessorKey: 'name',
header: 'Name'
},
{
accessorKey: 'price',
header: 'Price',
meta: {
class: {
th: 'text-right',
td: 'text-right'
}
},
cell: ({ row }) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(row.getValue('price'))
}
},
{
accessorKey: 'stock',
header: 'Stock',
meta: {
class: {
th: 'text-right',
td: 'text-right'
}
}
}
]
</script>
<template>
<UTable :data="data" :columns="columns" class="flex-1" />
</template>带列排序
您可以更新列header,以在header中呈现Button组件,从而使用TanStack Table Sorting APIs切换排序状态。
在这些列上也设置enableSorting: true。这将aria-sort放在<th>上,以便屏幕阅读器可以读取列的当前排序状态:none,ascending或descending。Button保留更改它的控件。
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €639.00 |
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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',
enableSorting: true,
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const sorting = ref([
{
id: 'email',
desc: false
}
])
</script>
<template>
<UTable v-model:sorting="sorting" :data="data" :columns="columns" class="flex-1" />
</template><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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',
enableSorting: true,
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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 | paid | ethan.harris@example.com | €639.00 |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
enableSorting: true,
header: ({ column }) => getHeader(column, 'ID'),
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
enableSorting: true,
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',
enableSorting: true,
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',
enableSorting: true,
header: ({ column }) => getHeader(column, 'Email')
},
{
accessorKey: 'amount',
enableSorting: true,
header: ({ column }) => getHeader(column, 'Amount'),
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
enableSorting: true,
header: ({ column }) => getHeader(column, 'ID'),
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
enableSorting: true,
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',
enableSorting: true,
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',
enableSorting: true,
header: ({ column }) => getHeader(column, 'Email')
},
{
accessorKey: 'amount',
enableSorting: true,
header: ({ column }) => getHeader(column, 'Amount'),
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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表列固定APIs切换固定状态。
size值,以确保正确的列宽处理,特别是对于多个固定的列。| #4600000000000000000000000000000000000000 | 2024-03-11T15:30:00 | paid | james.anderson@example.com | €594,000.00 |
| #4599000000000000000000000000000000000000 | 2024-03-11T10:10:00 | failed | mia.white@example.com | €276,000.00 |
| #4598000000000000000000000000000000000000 | 2024-03-11T08:50:00 | refunded | william.brown@example.com | €315,000.00 |
| #4597000000000000000000000000000000000000 | 2024-03-10T19:45:00 | paid | emma.davis@example.com | €5,290,000.00 |
| #4596000000000000000000000000000000000000 | 2024-03-10T15:55:00 | paid | ethan.harris@example.com | €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: '4600000000000000000000000000000000000000',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594000
},
{
id: '4599000000000000000000000000000000000000',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276000
},
{
id: '4598000000000000000000000000000000000000',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315000
},
{
id: '4597000000000000000000000000000000000000',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 5290000
},
{
id: '4596000000000000000000000000000000000000',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639000
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: ({ column }) => getHeader(column, 'ID', 'left'),
cell: ({ row }) => `#${row.getValue('id')}`,
size: 381
},
{
accessorKey: 'date',
header: ({ column }) => getHeader(column, 'Date', 'left'),
size: 172
},
{
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')
)
},
size: 103
},
{
accessorKey: 'email',
header: ({ column }) => getHeader(column, 'Email', 'left'),
size: 232
},
{
accessorKey: 'amount',
header: ({ column }) => getHeader(column, 'Amount', 'right'),
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
},
size: 130
}
]
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: ['id'],
right: ['amount']
})
</script>
<template>
<UTable v-model:column-pinning="columnPinning" :data="data" :columns="columns" class="flex-1" />
</template><script setup lang="ts">
import { h, resolveComponent, ref } 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: '4600000000000000000000000000000000000000',
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594000
},
{
id: '4599000000000000000000000000000000000000',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276000
},
{
id: '4598000000000000000000000000000000000000',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315000
},
{
id: '4597000000000000000000000000000000000000',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 5290000
},
{
id: '4596000000000000000000000000000000000000',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
amount: 639000
}
])
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: ({ column }) => getHeader(column, 'ID', 'left'),
cell: ({ row }) => `#${row.getValue('id')}`,
size: 381
},
{
accessorKey: 'date',
header: ({ column }) => getHeader(column, 'Date', 'left'),
size: 172
},
{
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')
)
},
size: 103
},
{
accessorKey: 'email',
header: ({ column }) => getHeader(column, 'Email', 'left'),
size: 232
},
{
accessorKey: 'amount',
header: ({ column }) => getHeader(column, 'Amount', 'right'),
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
},
size: 130
}
]
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: ['id'],
right: ['amount']
})
</script>
<template>
<UTable v-model:column-pinning="columnPinning" :data="data" :columns="columns" class="flex-1" />
</template>column-pinning属性来控制列的固定状态(可以使用v-model绑定)。具有列可见性
您可以使用DropdownMenu组件,通过TanStack表列可见性APIs切换列的可见性。
| Date | Status | Amount | |
|---|---|---|---|
| Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 |
| Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { h, resolveComponent, ref, useTemplateRef } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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 列过滤APIs按列过滤行。
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { h, resolveComponent, ref, useTemplateRef } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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 全局过滤APIs过滤行。
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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><script setup lang="ts">
import { h, resolveComponent, ref } 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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
status: 'failed',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
status: 'refunded',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
status: 'paid',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
status: 'paid',
email: 'ethan.harris@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
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组件通过Pagination APIs控制分页状态。
有不同的分页方法,如分页指南中所解释的。在这个例子中,我们使用客户端分页,所以我们需要手动传递getPaginationRowModel()函数。
| # | Date | Amount | |
|---|---|---|---|
| #4600 | Mar 11, 15:30 | james.anderson@example.com | €594.00 |
| #4599 | Mar 11, 10:10 | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | william.brown@example.com | €315.00 |
| #4597 | Mar 10, 19:45 | emma.davis@example.com | €529.00 |
| #4596 | Mar 10, 15:55 | ethan.harris@example.com | €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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T13:20:00',
email: 'sophia.miller@example.com',
amount: 428
},
{
id: '4594',
date: '2024-03-10T11:05:00',
email: 'noah.wilson@example.com',
amount: 673
},
{
id: '4593',
date: '2024-03-09T22:15:00',
email: 'olivia.jones@example.com',
amount: 382
},
{
id: '4592',
date: '2024-03-09T20:30:00',
email: 'liam.taylor@example.com',
amount: 547
},
{
id: '4591',
date: '2024-03-09T18:45:00',
email: 'ava.thomas@example.com',
amount: 291
},
{
id: '4590',
date: '2024-03-09T16:20:00',
email: 'lucas.martin@example.com',
amount: 624
},
{
id: '4589',
date: '2024-03-09T14:10:00',
email: 'isabella.clark@example.com',
amount: 438
},
{
id: '4588',
date: '2024-03-09T12:05:00',
email: 'mason.rodriguez@example.com',
amount: 583
},
{
id: '4587',
date: '2024-03-09T10:30:00',
email: 'sophia.lee@example.com',
amount: 347
},
{
id: '4586',
date: '2024-03-09T08:15:00',
email: 'ethan.walker@example.com',
amount: 692
},
{
id: '4585',
date: '2024-03-08T23:40:00',
email: 'amelia.hall@example.com',
amount: 419
},
{
id: '4584',
date: '2024-03-08T21:25:00',
email: 'oliver.young@example.com',
amount: 563
},
{
id: '4583',
date: '2024-03-08T19:50:00',
email: 'aria.king@example.com',
amount: 328
},
{
id: '4582',
date: '2024-03-08T17:35:00',
email: 'henry.wright@example.com',
amount: 647
},
{
id: '4581',
date: '2024-03-08T15:20:00',
email: 'luna.lopez@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const pagination = ref({
pageIndex: 0,
pageSize: 5
})
const globalFilter = ref('')
</script>
<template>
<div class="w-full space-y-4 pb-4">
<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:pagination="pagination"
v-model:global-filter="globalFilter"
:data="data"
:columns="columns"
:pagination-options="{
getPaginationRowModel: getPaginationRowModel()
}"
class="flex-1"
/>
<div class="flex justify-end border-t border-default pt-4 px-4">
<UPagination
: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><script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
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: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: 'emma.davis@example.com',
amount: 529
},
{
id: '4596',
date: '2024-03-10T15:55:00',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T13:20:00',
email: 'sophia.miller@example.com',
amount: 428
},
{
id: '4594',
date: '2024-03-10T11:05:00',
email: 'noah.wilson@example.com',
amount: 673
},
{
id: '4593',
date: '2024-03-09T22:15:00',
email: 'olivia.jones@example.com',
amount: 382
},
{
id: '4592',
date: '2024-03-09T20:30:00',
email: 'liam.taylor@example.com',
amount: 547
},
{
id: '4591',
date: '2024-03-09T18:45:00',
email: 'ava.thomas@example.com',
amount: 291
},
{
id: '4590',
date: '2024-03-09T16:20:00',
email: 'lucas.martin@example.com',
amount: 624
},
{
id: '4589',
date: '2024-03-09T14:10:00',
email: 'isabella.clark@example.com',
amount: 438
},
{
id: '4588',
date: '2024-03-09T12:05:00',
email: 'mason.rodriguez@example.com',
amount: 583
},
{
id: '4587',
date: '2024-03-09T10:30:00',
email: 'sophia.lee@example.com',
amount: 347
},
{
id: '4586',
date: '2024-03-09T08:15:00',
email: 'ethan.walker@example.com',
amount: 692
},
{
id: '4585',
date: '2024-03-08T23:40:00',
email: 'amelia.hall@example.com',
amount: 419
},
{
id: '4584',
date: '2024-03-08T21:25:00',
email: 'oliver.young@example.com',
amount: 563
},
{
id: '4583',
date: '2024-03-08T19:50:00',
email: 'aria.king@example.com',
amount: 328
},
{
id: '4582',
date: '2024-03-08T17:35:00',
email: 'henry.wright@example.com',
amount: 647
},
{
id: '4581',
date: '2024-03-08T15:20:00',
email: 'luna.lopez@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const pagination = ref({
pageIndex: 0,
pageSize: 5
})
const globalFilter = ref('')
</script>
<template>
<div class="w-full space-y-4 pb-4">
<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:pagination="pagination"
v-model:global-filter="globalFilter"
:data="data"
:columns="columns"
:pagination-options="{
getPaginationRowModel: getPaginationRowModel()
}"
class="flex-1"
/>
<div class="flex justify-end border-t border-default pt-4 px-4">
<UPagination
: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 | Name | Company | |
|---|---|---|---|
| No data | |||
<script setup lang="ts">
import type { AvatarProps, TableColumn } from '@nuxt/ui'
const UAvatar = resolveComponent('UAvatar')
type User = {
id: number
name: string
username: string
email: string
avatar: AvatarProps
company: { name: string }
}
const { data, status } = useLazyFetch<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` }
})) || []
)
},
server: false
})
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,
loading: 'lazy',
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' || status === 'idle'"
class="flex-1 h-80"
/>
</template><script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { AvatarProps, TableColumn } from '@nuxt/ui'
const UAvatar = resolveComponent('UAvatar')
type User = {
id: number
name: string
username: string
email: string
avatar: AvatarProps
company: { name: string }
}
const { data, status } = useLazyFetch<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` }
})) || []
)
},
server: false
})
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,
loading: 'lazy',
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' || status === 'idle'"
class="flex-1 h-80"
/>
</template>useLazyFetch和server: false在客户端获取数据,而不阻塞初始渲染。加载状态检查pending和idle的状态,以在获取之前和期间显示加载指示器。无限滚动
如果使用服务器端分页,则可以使用useInfiniteScroll组合项在用户滚动时加载更多数据。
| ID | Avatar | First name | Username | |
|---|---|---|---|---|
| No data | ||||
<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 } = useLazyFetch(
'https://dummyjson.com/users?limit=10&select=firstName,username,email,image',
{
key: 'table-users-infinite-scroll',
params: { skip },
transform: (data?: UserResponse) => {
return data?.users
},
server: false
}
)
const columns: TableColumn<User>[] = [
{
accessorKey: 'id',
header: 'ID'
},
{
accessorKey: 'image',
header: 'Avatar',
cell: ({ row }) => h(UAvatar, { src: row.original.image, loading: 'lazy' })
},
{
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 || [])]
})
const table = useTemplateRef('table')
onMounted(() => {
useInfiniteScroll(
table.value?.$el,
() => {
skip.value += 10
},
{
distance: 200,
canLoadMore: () => {
return status.value !== 'pending'
}
}
)
})
</script>
<template>
<UTable
ref="table"
:data="users"
:columns="columns"
:loading="status === 'pending' || status === 'idle'"
sticky
class="flex-1 h-80"
/>
</template><script setup lang="ts">
import { ref, watch, onMounted, h, resolveComponent, useTemplateRef } from 'vue'
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 } = useLazyFetch(
'https://dummyjson.com/users?limit=10&select=firstName,username,email,image',
{
key: 'table-users-infinite-scroll',
params: { skip },
transform: (data?: UserResponse) => {
return data?.users
},
server: false
}
)
const columns: TableColumn<User>[] = [
{
accessorKey: 'id',
header: 'ID'
},
{
accessorKey: 'image',
header: 'Avatar',
cell: ({ row }) => h(UAvatar, { src: row.original.image, loading: 'lazy' })
},
{
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 || [])]
})
const table = useTemplateRef('table')
onMounted(() => {
useInfiniteScroll(
table.value?.$el,
() => {
skip.value += 10
},
{
distance: 200,
canLoadMore: () => {
return status.value !== 'pending'
}
}
)
})
</script>
<template>
<UTable
ref="table"
:data="users"
:columns="columns"
:loading="status === 'pending' || status === 'idle'"
sticky
class="flex-1 h-80"
/>
</template>useLazyFetch和server: false在客户端上获取数据,而不会阻塞初始呈现。加载状态检查pending和idle的状态,以在获取之前和期间显示加载指示符。当用户滚动时,会加载其他页面。使用拖放
您可以使用@vueuse/integrations中的useSortable组合来启用Table上的拖放功能。此集成包装了Sortable.js,以提供无缝拖放体验。
:ui prop向它添加一个唯一的类,以便使用useSortable(例如:ui="{ tbody: 'my-table-tbody' }")将其作为目标。| # | Date | Amount | |
|---|---|---|---|
| #4600 | Mar 11, 15:30 | james.anderson@example.com | €594.00 |
| #4599 | Mar 11, 10:10 | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | william.brown@example.com | €315.00 |
| #4597 | Mar 10, 19:45 | emma.davis@example.com | €529.00 |
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable'
type Payment = {
id: string
date: string
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: 'emma.davis@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
useSortable('.my-table-tbody', data, {
animation: 150
})
</script>
<template>
<UTable
ref="table"
:data="data"
:columns="columns"
:ui="{
tbody: 'my-table-tbody'
}"
class="flex-1"
/>
</template><script setup lang="ts">
import { ref } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable'
type Payment = {
id: string
date: string
email: string
amount: number
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
email: 'james.anderson@example.com',
amount: 594
},
{
id: '4599',
date: '2024-03-11T10:10:00',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: 'emma.davis@example.com',
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
useSortable('.my-table-tbody', data, {
animation: 150
})
</script>
<template>
<UTable
ref="table"
:data="data"
:columns="columns"
:ui="{
tbody: 'my-table-tbody'
}"
class="flex-1"
/>
</template>虚拟化:badge{label="4.1+" class="align-text-top"}
使用virtualize prop以布尔值或带有{ estimateSize: 65, overscan: 12 }等选项的对象的形式启用大型数据集的虚拟化。您还可以传递其他TanStack Virtual选项以自定义虚拟化行为。sticky prop与virtualize结合使用,以在滚动大型数据集时保持页眉或页脚可见。
| # | Date | Status | Amount | |
|---|---|---|---|---|
<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[]>(
Array(1000)
.fill(0)
.map((_, i) => ({
id: `4600-${i}`,
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
}))
)
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
</script>
<template>
<UTable sticky virtualize :data="data" :columns="columns" class="flex-1 h-80" />
</template><script setup lang="ts">
import { h, resolveComponent, ref } 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[]>(
Array(1000)
.fill(0)
.map((_, i) => ({
id: `4600-${i}`,
date: '2024-03-11T15:30:00',
status: 'paid',
email: 'james.anderson@example.com',
amount: 594
}))
)
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
</script>
<template>
<UTable sticky virtualize :data="data" :columns="columns" class="flex-1 h-80" />
</template>class="h-[400px]")。带有外部滚动元素:badge{label="4.10+" class="align-text-top"}
在virtualize prop中传递一个getScrollElement函数,以针对祖先滚动容器而不是表本身的根进行虚拟化。将scrollMargin设置为表从滚动元素开始的偏移量(例如,其上方内容的高度),因此表头和表体共享一个滚动条。
Payments
The title stays put while the wide table scrolls both axes under one scrollbar.
| # | Date | Customer | Method | Status | Amount | |
|---|---|---|---|---|---|---|
<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
customer: string
email: string
method: string
status: 'paid' | 'failed' | 'refunded'
amount: number
}
const names = [
'James Anderson',
'Mary Johnson',
'Robert Williams',
'Patricia Brown',
'Michael Davis'
]
const methods = ['Visa', 'Mastercard', 'PayPal', 'Amex']
const statuses = ['paid', 'failed', 'refunded'] as const
const data = ref<Payment[]>(
Array.from({ length: 1000 }, (_, i) => {
const customer = names[i % names.length]!
return {
id: `4600-${i}`,
date: '2024-03-11T15:30:00',
customer,
email: `${customer.toLowerCase().replace(' ', '.')}@example.com`,
method: methods[i % methods.length]!,
status: statuses[i % statuses.length]!,
amount: 100 + ((i * 37) % 900)
}
})
)
// The container owns the scroll; the table virtualizes against it so the header and rows share one scrollbar.
const container = useTemplateRef('container')
const title = useTemplateRef('title')
const getScrollElement = () => container.value
// `scrollMargin` is the table's offset within the scroll element (border-box height of the title above it).
const { height: titleHeight } = useElementSize(title, undefined, { box: 'border-box' })
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) =>
new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
},
{
accessorKey: 'customer',
header: 'Customer'
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'method',
header: 'Method'
},
{
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: 'amount',
header: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(Number.parseFloat(row.getValue('amount')))
}
]
</script>
<template>
<div ref="container" class="w-full h-96 overflow-auto">
<div
ref="title"
class="sticky left-0 z-10 flex items-end justify-between gap-4 p-6 bg-elevated/50"
>
<div>
<h2 class="text-2xl font-bold text-highlighted">Payments</h2>
<p class="text-muted">
The title stays put while the wide table scrolls both axes under one scrollbar.
</p>
</div>
<UBadge color="neutral" variant="subtle" :label="`${data.length} rows`" />
</div>
<UTable
sticky
:data="data"
:columns="columns"
:virtualize="{ scrollMargin: titleHeight, getScrollElement }"
:ui="{ base: 'min-w-[1200px]' }"
/>
</div>
</template><script setup lang="ts">
import { useElementSize } from '@vueuse/core'
import { h, resolveComponent, ref, useTemplateRef } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UBadge = resolveComponent('UBadge')
type Payment = {
id: string
date: string
customer: string
email: string
method: string
status: 'paid' | 'failed' | 'refunded'
amount: number
}
const names = [
'James Anderson',
'Mary Johnson',
'Robert Williams',
'Patricia Brown',
'Michael Davis'
]
const methods = ['Visa', 'Mastercard', 'PayPal', 'Amex']
const statuses = ['paid', 'failed', 'refunded'] as const
const data = ref<Payment[]>(
Array.from({ length: 1000 }, (_, i) => {
const customer = names[i % names.length]!
return {
id: `4600-${i}`,
date: '2024-03-11T15:30:00',
customer,
email: `${customer.toLowerCase().replace(' ', '.')}@example.com`,
method: methods[i % methods.length]!,
status: statuses[i % statuses.length]!,
amount: 100 + ((i * 37) % 900)
}
})
)
// The container owns the scroll; the table virtualizes against it so the header and rows share one scrollbar.
const container = useTemplateRef('container')
const title = useTemplateRef('title')
const getScrollElement = () => container.value
// `scrollMargin` is the table's offset within the scroll element (border-box height of the title above it).
const { height: titleHeight } = useElementSize(title, undefined, { box: 'border-box' })
const columns: TableColumn<Payment>[] = [
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) =>
new Date(row.getValue('date')).toLocaleString('en-US', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
},
{
accessorKey: 'customer',
header: 'Customer'
},
{
accessorKey: 'email',
header: 'Email'
},
{
accessorKey: 'method',
header: 'Method'
},
{
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: 'amount',
header: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(Number.parseFloat(row.getValue('amount')))
}
]
</script>
<template>
<div ref="container" class="w-full h-96 overflow-auto">
<div
ref="title"
class="sticky left-0 z-10 flex items-end justify-between gap-4 p-6 bg-elevated/50"
>
<div>
<h2 class="text-2xl font-bold text-highlighted">Payments</h2>
<p class="text-muted">
The title stays put while the wide table scrolls both axes under one scrollbar.
</p>
</div>
<UBadge color="neutral" variant="subtle" :label="`${data.length} rows`" />
</div>
<UTable
sticky
:data="data"
:columns="columns"
:virtualize="{ scrollMargin: titleHeight, getScrollElement }"
:ui="{ base: 'min-w-[1200px]' }"
/>
</div>
</template>overflow是visible,外部容器拥有两个轴上的滚动,所以给它overflow-auto(而不仅仅是overflow-y-auto)以保持宽表的水平滚动。然后sticky头部锚定到该容器。使用树数据
您可以使用get-sub-rows属性在表中显示分层(树)数据。
例如,如果您的数据对象具有children数组,则将:get-sub-rows="row => row.children"设置为启用可扩展行。
| # | Date | Amount | ||
|---|---|---|---|---|
4600 | Mar 11, 15:30 | james.anderson@example.com | €594.00 | |
4599 | Mar 11, 10:10 | mia.white@example.com | €276.00 | |
4598 | Mar 11, 08:50 | william.brown@example.com | €315.00 | |
4597 | Mar 10, 19:45 | emma.davis@example.com | €529.00 | |
4589 | Mar 9, 11:35 | isabella.lee@example.com | €389.00 | |
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UCheckbox = resolveComponent('UCheckbox')
const UButton = resolveComponent('UButton')
type Payment = {
id: string
date: string
email: string
amount: number
children?: Payment[]
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
email: 'james.anderson@example.com',
amount: 594,
children: [
{
id: '4599',
date: '2024-03-11T10:10:00',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: 'emma.davis@example.com',
amount: 529,
children: [
{
id: '4592',
date: '2024-03-09T18:45:00',
email: 'benjamin.jackson@example.com',
amount: 851
},
{
id: '4591',
date: '2024-03-09T16:05:00',
email: 'sophia.miller@example.com',
amount: 762
},
{
id: '4590',
date: '2024-03-09T14:20:00',
email: 'noah.clark@example.com',
amount: 573,
children: [
{
id: '4596',
date: '2024-03-10T15:55:00',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T13:40:00',
email: 'ava.thomas@example.com',
amount: 428
}
]
}
]
}
]
},
{
id: '4589',
date: '2024-03-09T11:35:00',
email: 'isabella.lee@example.com',
amount: 389
}
])
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() ? true : row.getIsSomeSelected() ? 'indeterminate' : false,
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
})
},
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => {
return h(
'div',
{
style: {
paddingInlineStart: `${row.depth}rem`
},
class: 'flex items-center gap-2'
},
[
h(UButton, {
color: 'neutral',
variant: 'outline',
size: 'xs',
icon: row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus',
class: !row.getCanExpand() && 'invisible',
ui: {
base: 'p-0 rounded-sm',
leadingIcon: 'size-4'
},
onClick: row.getToggleExpandedHandler()
}),
row.getValue('id') as string
]
)
}
},
{
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const expanded = ref({ 0: true })
</script>
<template>
<UTable
v-model:expanded="expanded"
:data="data"
:columns="columns"
:get-sub-rows="(row) => row.children"
class="flex-1"
:ui="{
base: 'border-separate border-spacing-0',
tbody: '[&>tr]:last:[&>td]:border-b-0',
tr: 'group',
td: 'empty:p-0 group-has-[td:not(:empty)]:border-b border-default'
}"
/>
</template><script setup lang="ts">
import { h, resolveComponent, ref } from 'vue'
import type { TableColumn } from '@nuxt/ui'
const UCheckbox = resolveComponent('UCheckbox')
const UButton = resolveComponent('UButton')
type Payment = {
id: string
date: string
email: string
amount: number
children?: Payment[]
}
const data = ref<Payment[]>([
{
id: '4600',
date: '2024-03-11T15:30:00',
email: 'james.anderson@example.com',
amount: 594,
children: [
{
id: '4599',
date: '2024-03-11T10:10:00',
email: 'mia.white@example.com',
amount: 276
},
{
id: '4598',
date: '2024-03-11T08:50:00',
email: 'william.brown@example.com',
amount: 315
},
{
id: '4597',
date: '2024-03-10T19:45:00',
email: 'emma.davis@example.com',
amount: 529,
children: [
{
id: '4592',
date: '2024-03-09T18:45:00',
email: 'benjamin.jackson@example.com',
amount: 851
},
{
id: '4591',
date: '2024-03-09T16:05:00',
email: 'sophia.miller@example.com',
amount: 762
},
{
id: '4590',
date: '2024-03-09T14:20:00',
email: 'noah.clark@example.com',
amount: 573,
children: [
{
id: '4596',
date: '2024-03-10T15:55:00',
email: 'ethan.harris@example.com',
amount: 639
},
{
id: '4595',
date: '2024-03-10T13:40:00',
email: 'ava.thomas@example.com',
amount: 428
}
]
}
]
}
]
},
{
id: '4589',
date: '2024-03-09T11:35:00',
email: 'isabella.lee@example.com',
amount: 389
}
])
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() ? true : row.getIsSomeSelected() ? 'indeterminate' : false,
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'aria-label': 'Select row'
})
},
{
accessorKey: 'id',
header: '#',
cell: ({ row }) => {
return h(
'div',
{
style: {
paddingInlineStart: `${row.depth}rem`
},
class: 'flex items-center gap-2'
},
[
h(UButton, {
color: 'neutral',
variant: 'outline',
size: 'xs',
icon: row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus',
class: !row.getCanExpand() && 'invisible',
ui: {
base: 'p-0 rounded-sm',
leadingIcon: 'size-4'
},
onClick: row.getToggleExpandedHandler()
}),
row.getValue('id') as string
]
)
}
},
{
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: 'Amount',
meta: {
class: {
th: 'text-right',
td: 'text-right font-medium'
}
},
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount)
}
}
]
const expanded = ref({ 0: true })
</script>
<template>
<UTable
v-model:expanded="expanded"
:data="data"
:columns="columns"
:get-sub-rows="(row) => row.children"
class="flex-1"
:ui="{
base: 'border-separate border-spacing-0',
tbody: '[&>tr]:last:[&>td]:border-b-0',
tr: 'group',
td: 'empty:p-0 group-has-[td:not(:empty)]:border-b border-default'
}"
/>
</template>带插槽
您可以使用插槽来自定义表格的标题和数据单元格。
使用#<column>-header插槽自定义列的标题。您将可以访问插槽范围中的column、header和table属性。
使用#<column>-cell插槽自定义列的单元格。您将可以访问插槽范围中的cell、column、getValue、renderValue、row和table属性。
| ID | Name | Role | ||
|---|---|---|---|---|
| 1 | Lindsay Walton Front-end Developer | lindsay.walton@example.com | Member | |
| 2 | Courtney Henry Designer | courtney.henry@example.com | Admin | |
| 3 | Tom Cook Director of Product | tom.cook@example.com | Member | |
| 4 | Whitney Francis Copywriter | whitney.francis@example.com | Admin | |
| 5 | Leonard Krasner Senior Designer | leonard.krasner@example.com | Owner | |
| 6 | Floyd Miles Principal Designer | floyd.miles@example.com | Member |
<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: 'lindsay.walton@example.com',
role: 'Member'
},
{
id: 2,
name: 'Courtney Henry',
position: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
},
{
id: 3,
name: 'Tom Cook',
position: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
},
{
id: 4,
name: 'Whitney Francis',
position: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
},
{
id: 5,
name: 'Leonard Krasner',
position: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
},
{
id: 6,
name: 'Floyd Miles',
position: 'Principal Designer',
email: 'floyd.miles@example.com',
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"
loading="lazy"
: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><script setup lang="ts">
import { ref } from 'vue'
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: 'lindsay.walton@example.com',
role: 'Member'
},
{
id: 2,
name: 'Courtney Henry',
position: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
},
{
id: 3,
name: 'Tom Cook',
position: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
},
{
id: 4,
name: 'Whitney Francis',
position: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
},
{
id: 5,
name: 'Leonard Krasner',
position: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
},
{
id: 6,
name: 'Floyd Miles',
position: 'Principal Designer',
email: 'floyd.miles@example.com',
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"
loading="lazy"
: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
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
data | T[] | |
columns | TableColumn<T, unknown>[] | |
caption | string | |
meta | TableMeta<T>You can pass any object to | |
virtualize | false | boolean | (Partial<Omit<VirtualizerOptions<Element, Element>, "count" | "estimateSize" | "overscan">> & { getScrollElement?: (() => Element | null) ; overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; }) | undefinedEnable virtualization for large datasets. Note: row pinning is not supported when virtualization is enabled. |
empty | t('table.noData') | stringThe text to display when the table is empty. |
sticky | false | boolean | "header" | "footer" Whether the table should have a sticky header or footer. True for both, 'header' for header only, 'footer' for footer only. |
loading | boolean Whether the table should be in loading state. | |
loadingColor | 'primary' | "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" |
loadingAnimation | 'carousel' | "carousel" | "carousel-inverse" | "swing" | "elastic" |
watchOptions | { deep: true } | WatchOptions<boolean>Use the |
globalFilterOptions | Omit<GlobalFilterOptions<T>, "onGlobalFilterChange"> | |
columnFiltersOptions | Omit<ColumnFiltersOptions<T>, "getFilteredRowModel" | "onColumnFiltersChange"> | |
columnPinningOptions | Omit<ColumnPinningOptions, "onColumnPinningChange"> | |
columnSizingOptions | Omit<ColumnSizingOptions, "onColumnSizingChange" | "onColumnSizingInfoChange"> | |
visibilityOptions | Omit<VisibilityOptions, "onColumnVisibilityChange"> | |
sortingOptions | Omit<SortingOptions<T>, "getSortedRowModel" | "onSortingChange"> | |
groupingOptions | Omit<GroupingOptions, "onGroupingChange"> | |
expandedOptions | Omit<ExpandedOptions<T>, "getExpandedRowModel" | "onExpandedChange"> | |
rowSelectionOptions | Omit<RowSelectionOptions<T>, "onRowSelectionChange"> | |
rowPinningOptions | Omit<RowPinningOptions<T>, "onRowPinningChange"> | |
paginationOptions | Omit<PaginationOptions, "onPaginationChange"> | |
facetedOptions | FacetedOptions<T> | |
onSelect | (e: Event, row: TableRow<T>): void | |
onHover | (e: Event, row: TableRow<T> | null): void | |
onContextmenu | (e: Event, row: TableRow<T>): void | ((e: Event, row: TableRow<T>) => void)[] | |
state | Partial<TableState> | |
onStateChange | (updater: Updater<TableState>): void | |
renderFallbackValue | any | |
_features | TableFeature<any>[]An array of extra features that you can add to the table instance. | |
autoResetAll | boolean Set this option to override any of the | |
debugAll | boolean Set this option to | |
debugCells | boolean Set this option to | |
debugColumns | boolean Set this option to | |
debugHeaders | boolean Set this option to | |
debugRows | boolean Set this option to | |
debugTable | boolean Set this option to | |
defaultColumn | Partial<ColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<ColumnDefBase<T, unknown> & IdIdentifier<T, unknown>> | Partial<GroupColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<GroupColumnDefBase<T, unknown> & IdIdentifier<T, unknown>> | Partial<AccessorKeyColumnDefBase<T, unknown> & Partial<StringHeaderIdentifier>> | Partial<AccessorKeyColumnDefBase<T, unknown> & Partial<IdIdentifier<T, unknown>>> | Partial<AccessorFnColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<AccessorFnColumnDefBase<T, unknown> & IdIdentifier<T, unknown>>Default column options to use for all column defs supplied to the table. | |
getRowId | (originalRow: T, index: number, parent?: Row<T> | undefined): stringThis optional function is used to derive a unique ID for any given row. If not provided the rows index is used (nested rows join together with | |
getSubRows | (originalRow: T, index: number): T[] This optional function is used to access the sub rows for any given row. If you are using nested rows, you will need to use this function to return the sub rows object (or undefined) from the row. | |
initialState | InitialTableStateUse this option to optionally pass initial state to the table. This state will be used when resetting various table states either automatically by the table (eg. Table state will not be reset when this object changes, which also means that the initial state object does not need to be stable. | |
mergeOptions | (defaultOptions: TableOptions<T>, options: Partial<TableOptions<T>>): TableOptions<T>This option is used to optionally implement the merging of table options. | |
cellpadding | string | number | |
cellspacing | string | number | |
summary | string | |
width | string | number | |
globalFilter | string | |
columnFilters | ColumnFiltersState | |
columnOrder | ColumnOrderState | |
columnVisibility | VisibilityState | |
columnPinning | ColumnPinningState | |
columnSizing | ColumnSizingState | |
columnSizingInfo | ColumnSizingInfoState | |
rowSelection | RowSelectionState | |
rowPinning | RowPinningState | |
sorting | SortingState | |
grouping | GroupingState | |
expanded | true | Record<string, boolean> | |
pagination | PaginationState | |
ui | { root?: SlotClass; base?: SlotClass; caption?: SlotClass; thead?: SlotClass; tbody?: SlotClass; tfoot?: SlotClass; tr?: SlotClass; th?: SlotClass; td?: SlotClass; separator?: SlotClass; empty?: SlotClass; loading?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
expanded | { row: Row<T>; } |
empty | {} |
loading | {} |
caption | {} |
body-top | {} |
body-bottom | {} |
Expose
您可以使用useTemplateRef访问类型化的组件实例。
<script setup lang="ts">
const table = useTemplateRef('table')
</script>
<template>
<UTable ref="table" />
</template>
这将使您能够访问以下内容:
| 名称 | 类型 |
|---|---|
tableRef | Ref<HTMLTableElement | null> |
tableApi | Table |
Theme
export default defineAppConfig({
ui: {
table: {
slots: {
root: 'relative overflow-auto outline-primary/25 focus-visible:outline-3',
base: 'min-w-full overflow-clip',
caption: 'sr-only',
thead: 'relative',
tbody: 'isolate [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:outline-primary/25 [&>tr]:data-[selectable=true]:focus-visible:outline-3 [&>tr]:data-[selectable=true]:-outline-offset-3 divide-y divide-default',
tfoot: 'relative',
tr: 'data-[selected=true]:bg-elevated/50',
th: 'px-4 py-3.5 text-sm text-highlighted text-start 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 start-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 z-1',
td: 'sticky bg-default/75 z-1'
}
},
sticky: {
true: {
thead: 'sticky top-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1',
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1'
},
header: {
thead: 'sticky top-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1'
},
footer: {
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1'
}
},
loading: {
true: {
thead: 'after:absolute after:z-1 after:h-px motion-reduce:after:inset-x-0 motion-reduce:after:animate-pulse'
}
},
externalScroll: {
true: {
root: 'overflow-visible'
}
},
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: 'motion-safe:after:animate-[carousel_2s_linear_infinite] motion-safe:rtl:after:animate-[carousel-rtl_2s_linear_infinite]'
}
},
{
loading: true,
loadingAnimation: 'carousel-inverse',
class: {
thead: 'motion-safe:after:animate-[carousel-inverse_2s_linear_infinite] motion-safe:rtl:after:animate-[carousel-inverse-rtl_2s_linear_infinite]'
}
},
{
loading: true,
loadingAnimation: 'swing',
class: {
thead: 'motion-safe:after:animate-[swing_2s_var(--ease-in-out)_infinite]'
}
},
{
loading: true,
loadingAnimation: 'elastic',
class: {
thead: 'motion-safe:after:animate-[elastic_2s_var(--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 outline-primary/25 focus-visible:outline-3',
base: 'min-w-full overflow-clip',
caption: 'sr-only',
thead: 'relative',
tbody: 'isolate [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:outline-primary/25 [&>tr]:data-[selectable=true]:focus-visible:outline-3 [&>tr]:data-[selectable=true]:-outline-offset-3 divide-y divide-default',
tfoot: 'relative',
tr: 'data-[selected=true]:bg-elevated/50',
th: 'px-4 py-3.5 text-sm text-highlighted text-start 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 start-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 z-1',
td: 'sticky bg-default/75 z-1'
}
},
sticky: {
true: {
thead: 'sticky top-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1',
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1'
},
header: {
thead: 'sticky top-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1'
},
footer: {
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 backdrop-blur-sm z-1'
}
},
loading: {
true: {
thead: 'after:absolute after:z-1 after:h-px motion-reduce:after:inset-x-0 motion-reduce:after:animate-pulse'
}
},
externalScroll: {
true: {
root: 'overflow-visible'
}
},
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: 'motion-safe:after:animate-[carousel_2s_linear_infinite] motion-safe:rtl:after:animate-[carousel-rtl_2s_linear_infinite]'
}
},
{
loading: true,
loadingAnimation: 'carousel-inverse',
class: {
thead: 'motion-safe:after:animate-[carousel-inverse_2s_linear_infinite] motion-safe:rtl:after:animate-[carousel-inverse-rtl_2s_linear_infinite]'
}
},
{
loading: true,
loadingAnimation: 'swing',
class: {
thead: 'motion-safe:after:animate-[swing_2s_var(--ease-in-out)_infinite]'
}
},
{
loading: true,
loadingAnimation: 'elastic',
class: {
thead: 'motion-safe:after:animate-[elastic_2s_var(--ease-in-out)_infinite]'
}
}
],
defaultVariants: {
loadingColor: 'primary',
loadingAnimation: 'carousel'
}
}
}
})
]
})