Table

一个用于在行和列中显示数据的响应式表格元素。

用法

表格组件是基于TanStack Table构建的,并由useVueTable可组合项提供灵活且完全类型安全的 API。 TanStack Table 的某些功能尚不支持,我们将随着时间推移添加更多功能。

#日期状态
金额
#4600Mar 11, 15:30已支付
€594.00
#4599Mar 11, 10:10失败
€276.00
#4598Mar 11, 08:50已退款
€315.00
#4597Mar 10, 19:45已支付
€529.00
#4596Mar 10, 15:55已支付
€639.00
#4595Mar 10, 13:40已退款
€428.00
#4594Mar 10, 09:15已支付
€683.00
#4593Mar 9, 20:25失败
€947.00
#4592Mar 9, 18:45已支付
€851.00
#4591Mar 9, 16:05已支付
€762.00
#4590Mar 9, 14:20已支付
€573.00
#4589Mar 9, 11:35失败
€389.00
#4588Mar 8, 22:50已退款
€701.00
#4587Mar 8, 20:15已支付
€856.00
#4586Mar 8, 17:40已支付
€492.00
#4585Mar 8, 14:55失败
€637.00
#4584Mar 8, 12:30已支付
€784.00
#4583Mar 8, 09:45已退款
€345.00
#4582Mar 7, 23:10已支付
€918.00
#4581Mar 7, 20:25已支付
€567.00
已选择 0 行(共 0 行)。
此示例演示了 Table 组件最常见的用例。在 GitHub 上查看源代码。

数据

data 属性用作对象数组,列将根据对象的键生成。

Id日期状态邮箱金额
46002024-03-11T15:30:00已支付[email protected]594
45992024-03-11T10:10:00失败[email protected]276
45982024-03-11T08:50:00已退款[email protected]315
45972024-03-10T19:45:00已支付[email protected]529
45962024-03-10T15:55:00已支付[email protected]639
<script setup lang="ts">
const data = ref([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])
</script>

<template>
  <UTable :data="data" class="flex-1" />
</template>

columns 属性用作数组ColumnDef对象,其属性包括

  • accessorKey: 提取列值时使用的行对象的键。
  • header: 要为列显示的标题。如果传入字符串,它可以用作列 ID 的默认值。如果传入函数,它将接收一个用于标题的 props 对象,并应返回渲染的标题值(具体类型取决于所使用的适配器)。
  • footer: 要为列显示的页脚。功能与标题完全相同,但显示在表格下方。
  • cell: 要为列显示的每一行单元格。如果传入函数,它将接收一个用于单元格的 props 对象,并应返回渲染的单元格值(具体类型取决于所使用的适配器)。
  • meta: 列的额外属性。
    • class:
      • td: 要应用于 td 元素的类。
      • th: 要应用于 th 元素的类。
    • style:
      • td: 要应用于 td 元素的样式。
      • th: 要应用于 th 元素的样式。

为了渲染组件或其他 HTML 元素,您需要使用 Vueh 函数headercell 属性内部。这与其他使用插槽的组件不同,但提供了更大的灵活性。

您也可以使用插槽来自定义表格的标题和数据单元格。
#日期状态邮箱
金额
#4600Mar 11, 15:30已支付[email protected]
€594.00
#4599Mar 11, 10:10失败[email protected]
€276.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1" />
</template>
使用 h 渲染组件时,您可以使用 resolveComponent 函数或从 #components 导入。

Meta

meta 属性用作对象(TableMeta)来传递属性,例如

  • class:
    • tr: 要应用于 tr 元素的类。
  • style:
    • tr: 要应用于 tr 元素的样式。

加载中

使用 loading 属性显示加载状态,使用 loading-color 属性更改其颜色,使用 loading-animation 属性更改其动画。

Id日期状态邮箱金额
46002024-03-11T15:30:00已支付[email protected]594
45992024-03-11T10:10:00失败[email protected]276
45982024-03-11T08:50:00已退款[email protected]315
45972024-03-10T19:45:00已支付[email protected]529
45962024-03-10T15:55:00已支付[email protected]639
<script setup lang="ts">
const data = ref([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])
</script>

<template>
  <UTable loading loading-color="primary" loading-animation="carousel" :data="data" class="flex-1" />
</template>

固定

使用 sticky 属性使标题或页脚固定。

Id日期状态邮箱金额
46002024-03-11T15:30:00已支付[email protected]594
45992024-03-11T10:10:00失败[email protected]276
45982024-03-11T08:50:00已退款[email protected]315
45972024-03-10T19:45:00已支付[email protected]529
45962024-03-10T15:55:00已支付[email protected]639
45952024-03-10T15:55:00已支付[email protected]639
45942024-03-10T15:55:00已支付[email protected]639
<script setup lang="ts">
const data = ref([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  },
  {
    id: '4595',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  },
  {
    id: '4594',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])
</script>

<template>
  <UTable sticky :data="data" class="flex-1 max-h-[312px]" />
</template>

示例

带行操作

您可以添加一个新列,该列在 cell 内部渲染一个 DropdownMenu 组件,以渲染行操作。

#日期状态邮箱
金额
#4600Mar 11, 15:30已支付[email protected]
€594.00
#4599Mar 11, 10:10失败[email protected]
€276.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table'
import { useClipboard } from '@vueuse/core'

const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')

const toast = useToast()
const { copy } = useClipboard()

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  },
  {
    id: 'actions',
    cell: ({ row }) => {
      return h(
        'div',
        { class: 'text-right' },
        h(
          UDropdownMenu,
          {
            content: {
              align: 'end'
            },
            items: getRowItems(row),
            'aria-label': 'Actions dropdown'
          },
          () =>
            h(UButton, {
              icon: 'i-lucide-ellipsis-vertical',
              color: 'neutral',
              variant: 'ghost',
              class: 'ml-auto',
              'aria-label': 'Actions dropdown'
            })
        )
      )
    }
  }
]

function getRowItems(row: Row<Payment>) {
  return [
    {
      type: 'label',
      label: 'Actions'
    },
    {
      label: 'Copy payment ID',
      onSelect() {
        copy(row.original.id)

        toast.add({
          title: 'Payment ID copied to clipboard!',
          color: 'success',
          icon: 'i-lucide-circle-check'
        })
      }
    },
    {
      type: 'separator'
    },
    {
      label: 'View customer'
    },
    {
      label: 'View payment details'
    }
  ]
}
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1" />
</template>

带可展开行

您可以添加一个新列,该列在 cell 内部渲染一个 Button 组件,以使用 TanStack Table 切换行的可展开状态展开 API.

您需要定义 #expanded 插槽来渲染展开的内容,该内容将接收行作为参数。
#日期状态邮箱
金额
#4600Mar 11, 15:30已支付[email protected]
€594.00
#4599Mar 11, 10:10失败[email protected]
€276.00
{
  "id": "4599",
  "date": "2024-03-11T10:10:00",
  "status": "failed",
  "email": "[email protected]",
  "amount": 276
}
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    id: 'expand',
    cell: ({ row }) =>
      h(UButton, {
        color: 'neutral',
        variant: 'ghost',
        icon: 'i-lucide-chevron-down',
        square: true,
        'aria-label': 'Expand',
        ui: {
          leadingIcon: [
            'transition-transform',
            row.getIsExpanded() ? 'duration-200 rotate-180' : ''
          ]
        },
        onClick: () => row.toggleExpanded()
      })
  },
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const expanded = ref({ 1: true })
</script>

<template>
  <UTable
    v-model:expanded="expanded"
    :data="data"
    :columns="columns"
    :ui="{ tr: 'data-[expanded=true]:bg-elevated/50' }"
    class="flex-1"
  >
    <template #expanded="{ row }">
      <pre>{{ row.original }}</pre>
    </template>
  </UTable>
</template>
您可以使用 expanded 属性控制行的可展开状态(可以使用 v-model 进行绑定)。
您还可以将此操作添加到 actions 列中的 DropdownMenu 组件。

带分组行

您可以根据给定的列值对行进行分组,并通过添加到单元格的按钮使用 TanStack Table 显示/隐藏子行分组 API.

重要部分

  • 添加 grouping 属性,其中包含要分组的列 ID 数组。
  • 添加 grouping-options 属性。它必须包含 getGroupedRowModel,您可以从 @tanstack/vue-table 导入它或自行实现。
  • 通过行上任何单元格的 row.toggleExpanded() 方法展开行。请记住,它也会切换 #expanded 插槽。
  • 在列定义上使用 aggregateFn 定义如何聚合行。
  • 列定义上的 agregatedCell 渲染器仅在没有 cell 渲染器时才起作用。
项目#日期邮箱
金额
Account 1
3 条记录Mar 11, 15:303 位客户
€1,548.00
Account 2
2 条记录Mar 11, 10:102 位客户
€805.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import { getGroupedRowModel } from '@tanstack/vue-table'
import type { GroupingOptions } from '@tanstack/vue-table'

const UBadge = resolveComponent('UBadge')

type Account = {
  id: string
  name: string
}

type PaymentStatus = 'paid' | 'failed' | 'refunded'

type Payment = {
  id: string
  date: string
  status: PaymentStatus
  email: string
  amount: number
  account: Account
}

const getColorByStatus = (status: PaymentStatus) => {
  return {
    paid: 'success',
    failed: 'error',
    refunded: 'neutral'
  }[status]
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594,
    account: {
      id: '1',
      name: 'Account 1'
    }
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276,
    account: {
      id: '2',
      name: 'Account 2'
    }
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315,
    account: {
      id: '1',
      name: 'Account 1'
    }
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529,
    account: {
      id: '2',
      name: 'Account 2'
    }
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639,
    account: {
      id: '1',
      name: 'Account 1'
    }
  }
])

const columns: TableColumn<Payment>[] = [
  {
    id: 'title',
    header: 'Item'
  },
  {
    id: 'account_id',
    accessorKey: 'account.id'
  },
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) =>
      row.getIsGrouped() ? `${row.getValue('id')} records` : `#${row.getValue('id')}`,
    aggregationFn: 'count'
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    },
    aggregationFn: 'max'
  },
  {
    accessorKey: 'status',
    header: 'Status'
  },
  {
    accessorKey: 'email',
    header: 'Email',
    meta: {
      class: {
        td: 'w-full'
      }
    },
    cell: ({ row }) =>
      row.getIsGrouped() ? `${row.getValue('email')} customers` : row.getValue('email'),
    aggregationFn: 'uniqueCount'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    },
    aggregationFn: 'sum'
  }
]

const grouping_options = ref<GroupingOptions>({
  groupedColumnMode: 'remove',
  getGroupedRowModel: getGroupedRowModel()
})
</script>

<template>
  <UTable
    :data="data"
    :columns="columns"
    :grouping="['account_id', 'status']"
    :grouping-options="grouping_options"
    :ui="{
      root: 'min-w-full',
      td: 'empty:p-0' // helps with the colspaned row added for expand slot
    }"
  >
    <template #title-cell="{ row }">
      <div v-if="row.getIsGrouped()" class="flex items-center">
        <span class="inline-block" :style="{ width: `calc(${row.depth} * 1rem)` }" />

        <UButton
          variant="outline"
          color="neutral"
          class="mr-2"
          size="xs"
          :icon="row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus'"
          @click="row.toggleExpanded()"
        />
        <strong v-if="row.groupingColumnId === 'account_id'">{{
          row.original.account.name
        }}</strong>
        <UBadge
          v-else-if="row.groupingColumnId === 'status'"
          :color="getColorByStatus(row.original.status)"
          class="capitalize"
          variant="subtle"
        >
          {{ row.original.status }}
        </UBadge>
      </div>
    </template>
  </UTable>
</template>

带行选择

您可以添加一个新列,该列在 headercell 内部渲染一个 Checkbox 组件,以使用 TanStack Table 选择行行选择 API.

日期状态邮箱
金额
Mar 11, 15:30已支付[email protected]
€594.00
Mar 11, 10:10失败[email protected]
€276.00
Mar 11, 08:50已退款[email protected]
€315.00
Mar 10, 19:45已支付[email protected]
€529.00
Mar 10, 15:55已支付[email protected]
€639.00
已选择 0 行(共 0 行)。
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UCheckbox = resolveComponent('UCheckbox')
const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    id: 'select',
    header: ({ table }) =>
      h(UCheckbox, {
        modelValue: table.getIsSomePageRowsSelected()
          ? 'indeterminate'
          : table.getIsAllPageRowsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
          table.toggleAllPageRowsSelected(!!value),
        'aria-label': 'Select all'
      }),
    cell: ({ row }) =>
      h(UCheckbox, {
        modelValue: row.getIsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
        'aria-label': 'Select row'
      })
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const table = useTemplateRef('table')

const rowSelection = ref({ 1: true })
</script>

<template>
  <div class="flex-1 w-full">
    <UTable ref="table" v-model:row-selection="rowSelection" :data="data" :columns="columns" />

    <div class="px-4 py-3.5 border-t border-accented text-sm text-muted">
      {{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
      {{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
    </div>
  </div>
</template>
您可以使用 row-selection 属性控制行的选择状态(可以使用 v-model 进行绑定)。

带行选择事件

您可以添加 @select 监听器,使行可点击,无论是否有复选框列。

处理函数接收 TableRow 实例作为第一个参数,以及可选的 Event 作为第二个参数。
日期状态邮箱
金额
Mar 11, 15:30已支付[email protected]
€594.00
Mar 11, 10:10失败[email protected]
€276.00
Mar 11, 08:50已退款[email protected]
€315.00
Mar 10, 19:45已支付[email protected]
€529.00
Mar 10, 15:55已支付[email protected]
€639.00
已选择 0 行(共 0 行)。
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    id: 'select',
    header: ({ table }) =>
      h(UCheckbox, {
        modelValue: table.getIsSomePageRowsSelected()
          ? 'indeterminate'
          : table.getIsAllPageRowsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
          table.toggleAllPageRowsSelected(!!value),
        'aria-label': 'Select all'
      }),
    cell: ({ row }) =>
      h(UCheckbox, {
        modelValue: row.getIsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
        'aria-label': 'Select row'
      })
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const table = useTemplateRef('table')

const rowSelection = ref<Record<string, boolean>>({})

function onSelect(row: TableRow<Payment>, e?: Event) {
  /* If you decide to also select the column you can do this  */
  row.toggleSelected(!row.getIsSelected())

  console.log(e)
}
</script>

<template>
  <div class="flex w-full flex-1 gap-1">
    <div class="flex-1">
      <UTable
        ref="table"
        v-model:row-selection="rowSelection"
        :data="data"
        :columns="columns"
        @select="onSelect"
      />

      <div class="px-4 py-3.5 border-t border-accented text-sm text-muted">
        {{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
        {{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
      </div>
    </div>
  </div>
</template>
您可以使用此功能导航到页面、打开模态框,甚至手动选择行。

带行上下文菜单事件 新增

您可以添加 @contextmenu 监听器,使行可右键点击,并将表格包装在 ContextMenu 组件中,例如显示行操作。

处理函数分别接收 EventTableRow 实例作为第一个和第二个参数。
#日期状态邮箱
金额
#4600Mar 11, 15:30已支付[email protected]
€594.00
#4599Mar 11, 10:10失败[email protected]
€276.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { ContextMenuItem, TableColumn, TableRow } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'

const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')

const toast = useToast()
const { copy } = useClipboard()

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    id: 'select',
    header: ({ table }) =>
      h(UCheckbox, {
        modelValue: table.getIsSomePageRowsSelected()
          ? 'indeterminate'
          : table.getIsAllPageRowsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
          table.toggleAllPageRowsSelected(!!value),
        'aria-label': 'Select all'
      }),
    cell: ({ row }) =>
      h(UCheckbox, {
        modelValue: row.getIsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
        'aria-label': 'Select row'
      })
  },
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const items = ref<ContextMenuItem[]>([])

function getRowItems(row: TableRow<Payment>) {
  return [
    {
      type: 'label' as const,
      label: 'Actions'
    },
    {
      label: 'Copy payment ID',
      onSelect() {
        copy(row.original.id)

        toast.add({
          title: 'Payment ID copied to clipboard!',
          color: 'success',
          icon: 'i-lucide-circle-check'
        })
      }
    },
    {
      label: row.getIsExpanded() ? 'Collapse' : 'Expand',
      onSelect() {
        row.toggleExpanded()
      }
    },
    {
      type: 'separator' as const
    },
    {
      label: 'View customer'
    },
    {
      label: 'View payment details'
    }
  ]
}

function onContextmenu(_e: Event, row: TableRow<Payment>) {
  items.value = getRowItems(row)
}
</script>

<template>
  <UContextMenu :items="items">
    <UTable :data="data" :columns="columns" class="flex-1" @contextmenu="onContextmenu">
      <template #expanded="{ row }">
        <pre>{{ row.original }}</pre>
      </template>
    </UTable>
  </UContextMenu>
</template>

带行悬停事件 新增

您可以添加 @hover 监听器,使行可悬停,并使用 PopoverTooltip 组件显示行详细信息,例如。

处理函数分别接收 EventTableRow 实例作为第一个和第二个参数。
#日期状态邮箱
金额
#4600Mar 11, 15:30已支付[email protected]
€594.00
#4599Mar 11, 10:10失败[email protected]
€276.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')
const UCheckbox = resolveComponent('UCheckbox')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    id: 'select',
    header: ({ table }) =>
      h(UCheckbox, {
        modelValue: table.getIsSomePageRowsSelected()
          ? 'indeterminate'
          : table.getIsAllPageRowsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
          table.toggleAllPageRowsSelected(!!value),
        'aria-label': 'Select all'
      }),
    cell: ({ row }) =>
      h(UCheckbox, {
        modelValue: row.getIsSelected(),
        'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
        'aria-label': 'Select row'
      })
  },
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const anchor = ref({ x: 0, y: 0 })

const reference = computed(() => ({
  getBoundingClientRect: () =>
    ({
      width: 0,
      height: 0,
      left: anchor.value.x,
      right: anchor.value.x,
      top: anchor.value.y,
      bottom: anchor.value.y,
      ...anchor.value
    }) as DOMRect
}))

const open = ref(false)
const openDebounced = refDebounced(open, 10)
const selectedRow = ref<TableRow<Payment> | null>(null)

function onHover(_e: Event, row: TableRow<Payment> | null) {
  selectedRow.value = row

  open.value = !!row
}
</script>

<template>
  <div class="flex w-full flex-1 gap-1">
    <UTable
      :data="data"
      :columns="columns"
      class="flex-1"
      @pointermove="
        (ev: PointerEvent) => {
          anchor.x = ev.clientX
          anchor.y = ev.clientY
        }
      "
      @hover="onHover"
    />

    <UPopover
      :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
      :open="openDebounced"
      :reference="reference"
    >
      <template #content>
        <div class="p-4">
          {{ selectedRow?.original?.id }}
        </div>
      </template>
    </UPopover>
  </div>
</template>
此示例与 Popover 的 随光标示例 类似,并使用一个refDebounced以防止当光标从一行移动到另一行时,Popover 过快地打开和关闭。

您可以为列定义添加一个 footer 属性,以渲染列的页脚。

#日期状态邮箱
金额
#4600Mar 11, 15:30已支付[email protected]
€594.00
#4599Mar 11, 10:10失败[email protected]
€276.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
总计: €2,353.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn, TableRow } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    footer: ({ column }) => {
      const total = column
        .getFacetedRowModel()
        .rows.reduce(
          (acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')),
          0
        )

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(total)

      return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`)
    },
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1" />
</template>

带列排序

您可以更新列 header,以在 header 内部渲染一个 Button 组件,从而使用 TanStack Table 切换排序状态排序 API.

#日期状态
金额
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
#4600Mar 11, 15:30已支付[email protected]
€594.00
#4599Mar 11, 10:10失败[email protected]
€276.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: ({ column }) => {
      const isSorted = column.getIsSorted()

      return h(UButton, {
        color: 'neutral',
        variant: 'ghost',
        label: 'Email',
        icon: isSorted
          ? isSorted === 'asc'
            ? 'i-lucide-arrow-up-narrow-wide'
            : 'i-lucide-arrow-down-wide-narrow'
          : 'i-lucide-arrow-up-down',
        class: '-mx-2.5',
        onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
      })
    }
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const sorting = ref([
  {
    id: 'email',
    desc: false
  }
])
</script>

<template>
  <UTable v-model:sorting="sorting" :data="data" :columns="columns" class="flex-1" />
</template>
您可以使用 sorting 属性控制列的排序状态(可以使用 v-model 进行绑定)。

您还可以创建一个可复用组件,使任何列标题都可排序。

#4596Mar 10, 15:55已支付[email protected]
€639.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4599Mar 11, 10:10失败[email protected]
€276.00
#4600Mar 11, 15:30已支付[email protected]
€594.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column } from '@tanstack/vue-table'

const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')
const UDropdownMenu = resolveComponent('UDropdownMenu')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: ({ column }) => getHeader(column, 'ID'),
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: ({ column }) => getHeader(column, 'Date'),
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: ({ column }) => getHeader(column, 'Status'),
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: ({ column }) => getHeader(column, 'Email')
  },
  {
    accessorKey: 'amount',
    header: ({ column }) => h('div', { class: 'text-right' }, getHeader(column, 'Amount')),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

function getHeader(column: Column<Payment>, label: string) {
  const isSorted = column.getIsSorted()

  return h(
    UDropdownMenu,
    {
      content: {
        align: 'start'
      },
      'aria-label': 'Actions dropdown',
      items: [
        {
          label: 'Asc',
          type: 'checkbox',
          icon: 'i-lucide-arrow-up-narrow-wide',
          checked: isSorted === 'asc',
          onSelect: () => {
            if (isSorted === 'asc') {
              column.clearSorting()
            } else {
              column.toggleSorting(false)
            }
          }
        },
        {
          label: 'Desc',
          icon: 'i-lucide-arrow-down-wide-narrow',
          type: 'checkbox',
          checked: isSorted === 'desc',
          onSelect: () => {
            if (isSorted === 'desc') {
              column.clearSorting()
            } else {
              column.toggleSorting(true)
            }
          }
        }
      ]
    },
    () =>
      h(UButton, {
        color: 'neutral',
        variant: 'ghost',
        label,
        icon: isSorted
          ? isSorted === 'asc'
            ? 'i-lucide-arrow-up-narrow-wide'
            : 'i-lucide-arrow-down-wide-narrow'
          : 'i-lucide-arrow-up-down',
        class: '-mx-2.5 data-[state=open]:bg-elevated',
        'aria-label': `Sort by ${isSorted === 'asc' ? 'descending' : 'ascending'}`
      })
  )
}

const sorting = ref([
  {
    id: 'id',
    desc: false
  }
])
</script>

<template>
  <UTable v-model:sorting="sorting" :data="data" :columns="columns" class="flex-1" />
</template>
在此示例中,我们使用函数定义列标题,但您也可以创建实际组件。

带列固定

您可以更新列 header,以在 header 内部渲染一个 Button 组件,从而使用 TanStack Table 切换固定状态固定 API.

固定的列将固定在表格的左侧或右侧。
#460000000000000000000000000000000000000002024-03-11T15:30:00已支付[email protected]
€594,000.00
#459900000000000000000000000000000000000002024-03-11T10:10:00失败[email protected]
€276,000.00
#459800000000000000000000000000000000000002024-03-11T08:50:00已退款[email protected]
€315,000.00
#459700000000000000000000000000000000000002024-03-10T19:45:00已支付[email protected]
€5,290,000.00
#459600000000000000000000000000000000000002024-03-10T15:55:00已支付[email protected]
€639,000.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column } from '@tanstack/vue-table'

const UBadge = resolveComponent('UBadge')
const UButton = resolveComponent('UButton')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '46000000000000000000000000000000000000000',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594000
  },
  {
    id: '45990000000000000000000000000000000000000',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276000
  },
  {
    id: '45980000000000000000000000000000000000000',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315000
  },
  {
    id: '45970000000000000000000000000000000000000',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 5290000
  },
  {
    id: '45960000000000000000000000000000000000000',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639000
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: ({ column }) => getHeader(column, 'ID', 'left'),
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: ({ column }) => getHeader(column, 'Date', 'left')
  },
  {
    accessorKey: 'status',
    header: ({ column }) => getHeader(column, 'Status', 'left'),
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: ({ column }) => getHeader(column, 'Email', 'left')
  },
  {
    accessorKey: 'amount',
    header: ({ column }) => h('div', { class: 'text-right' }, getHeader(column, 'Amount', 'right')),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

function getHeader(column: Column<Payment>, label: string, position: 'left' | 'right') {
  const isPinned = column.getIsPinned()

  return h(UButton, {
    color: 'neutral',
    variant: 'ghost',
    label,
    icon: isPinned ? 'i-lucide-pin-off' : 'i-lucide-pin',
    class: '-mx-2.5',
    onClick() {
      column.pin(isPinned === position ? false : position)
    }
  })
}

const columnPinning = ref({
  left: [],
  right: ['amount']
})
</script>

<template>
  <UTable v-model:column-pinning="columnPinning" :data="data" :columns="columns" class="flex-1" />
</template>
您可以使用 column-pinning 属性控制列的固定状态(可以使用 v-model 进行绑定)。

带列可见性

您可以使用 DropdownMenu 组件,通过 TanStack Table 切换列的可见性列可见性 API.

日期状态邮箱
金额
Mar 11, 15:30已支付[email protected]
€594.00
Mar 11, 10:10失败[email protected]
€276.00
Mar 11, 08:50已退款[email protected]
€315.00
Mar 10, 19:45已支付[email protected]
€529.00
Mar 10, 15:55已支付[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import { upperFirst } from 'scule'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const table = useTemplateRef('table')

const columnVisibility = ref({
  id: false
})
</script>

<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex justify-end px-4 py-3.5 border-b  border-accented">
      <UDropdownMenu
        :items="
          table?.tableApi
            ?.getAllColumns()
            .filter((column) => column.getCanHide())
            .map((column) => ({
              label: upperFirst(column.id),
              type: 'checkbox' as const,
              checked: column.getIsVisible(),
              onUpdateChecked(checked: boolean) {
                table?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked)
              },
              onSelect(e?: Event) {
                e?.preventDefault()
              }
            }))
        "
        :content="{ align: 'end' }"
      >
        <UButton
          label="Columns"
          color="neutral"
          variant="outline"
          trailing-icon="i-lucide-chevron-down"
        />
      </UDropdownMenu>
    </div>

    <UTable
      ref="table"
      v-model:column-visibility="columnVisibility"
      :data="data"
      :columns="columns"
    />
  </div>
</template>
您可以使用 column-visibility 属性控制列的可见性状态(可以使用 v-model 进行绑定)。

带列过滤器

您可以使用 Input 组件,通过 TanStack Table 按列过滤行列过滤 API.

#日期状态邮箱
金额
#4600Mar 11, 15:30已支付[email protected]
€594.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const table = useTemplateRef('table')

const columnFilters = ref([
  {
    id: 'email',
    value: 'james'
  }
])
</script>

<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex px-4 py-3.5 border-b border-accented">
      <UInput
        :model-value="table?.tableApi?.getColumn('email')?.getFilterValue() as string"
        class="max-w-sm"
        placeholder="Filter emails..."
        @update:model-value="table?.tableApi?.getColumn('email')?.setFilterValue($event)"
      />
    </div>

    <UTable ref="table" v-model:column-filters="columnFilters" :data="data" :columns="columns" />
  </div>
</template>
您可以使用 column-filters 属性控制列的过滤状态(可以使用 v-model 进行绑定)。

带全局过滤器

您可以使用 Input 组件,通过 TanStack Table 过滤行全局过滤 API.

#日期状态邮箱
金额
#4599Mar 11, 10:10失败[email protected]
€276.00
#4598Mar 11, 08:50已退款[email protected]
€315.00
#4597Mar 10, 19:45已支付[email protected]
€529.00
#4596Mar 10, 15:55已支付[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    status: 'paid',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    status: 'failed',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    status: 'refunded',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    status: 'paid',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    status: 'paid',
    email: '[email protected]',
    amount: 639
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => {
      const color = {
        paid: 'success' as const,
        failed: 'error' as const,
        refunded: 'neutral' as const
      }[row.getValue('status') as string]

      return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
        row.getValue('status')
      )
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))

      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)

      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const globalFilter = ref('45')
</script>

<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex px-4 py-3.5 border-b border-accented">
      <UInput v-model="globalFilter" class="max-w-sm" placeholder="Filter..." />
    </div>

    <UTable ref="table" v-model:global-filter="globalFilter" :data="data" :columns="columns" />
  </div>
</template>
您可以使用 global-filter 属性控制全局过滤状态(可以使用 v-model 进行绑定)。

带分页

您可以使用 Pagination 组件,通过分页 API.

分页指南中所述,有不同的分页方法。在此示例中,我们使用客户端分页,因此需要手动传递 getPaginationRowModel() 函数。

#日期邮箱
金额
#4600Mar 11, 15:30[email protected]
€594.00
#4599Mar 11, 10:10[email protected]
€276.00
#4598Mar 11, 08:50[email protected]
€315.00
#4597Mar 10, 19:45[email protected]
€529.00
#4596Mar 10, 15:55[email protected]
€639.00
<script setup lang="ts">
import { getPaginationRowModel } from '@tanstack/vue-table'
import type { TableColumn } from '@nuxt/ui'

const table = useTemplateRef('table')

type Payment = {
  id: string
  date: string
  email: string
  amount: number
}
const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    email: '[email protected]',
    amount: 529
  },
  {
    id: '4596',
    date: '2024-03-10T15:55:00',
    email: '[email protected]',
    amount: 639
  },
  {
    id: '4595',
    date: '2024-03-10T13:20:00',
    email: '[email protected]',
    amount: 428
  },
  {
    id: '4594',
    date: '2024-03-10T11:05:00',
    email: '[email protected]',
    amount: 673
  },
  {
    id: '4593',
    date: '2024-03-09T22:15:00',
    email: '[email protected]',
    amount: 382
  },
  {
    id: '4592',
    date: '2024-03-09T20:30:00',
    email: '[email protected]',
    amount: 547
  },
  {
    id: '4591',
    date: '2024-03-09T18:45:00',
    email: '[email protected]',
    amount: 291
  },
  {
    id: '4590',
    date: '2024-03-09T16:20:00',
    email: '[email protected]',
    amount: 624
  },
  {
    id: '4589',
    date: '2024-03-09T14:10:00',
    email: '[email protected]',
    amount: 438
  },
  {
    id: '4588',
    date: '2024-03-09T12:05:00',
    email: '[email protected]',
    amount: 583
  },
  {
    id: '4587',
    date: '2024-03-09T10:30:00',
    email: '[email protected]',
    amount: 347
  },
  {
    id: '4586',
    date: '2024-03-09T08:15:00',
    email: '[email protected]',
    amount: 692
  },
  {
    id: '4585',
    date: '2024-03-08T23:40:00',
    email: '[email protected]',
    amount: 419
  },
  {
    id: '4584',
    date: '2024-03-08T21:25:00',
    email: '[email protected]',
    amount: 563
  },
  {
    id: '4583',
    date: '2024-03-08T19:50:00',
    email: '[email protected]',
    amount: 328
  },
  {
    id: '4582',
    date: '2024-03-08T17:35:00',
    email: '[email protected]',
    amount: 647
  },
  {
    id: '4581',
    date: '2024-03-08T15:20:00',
    email: '[email protected]',
    amount: 482
  }
])
const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))
      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)
      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

const pagination = ref({
  pageIndex: 0,
  pageSize: 5
})
</script>

<template>
  <div class="w-full space-y-4 pb-4">
    <UTable
      ref="table"
      v-model:pagination="pagination"
      :data="data"
      :columns="columns"
      :pagination-options="{
        getPaginationRowModel: getPaginationRowModel()
      }"
      class="flex-1"
    />

    <div class="flex justify-center border-t border-default pt-4">
      <UPagination
        :default-page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
        :items-per-page="table?.tableApi?.getState().pagination.pageSize"
        :total="table?.tableApi?.getFilteredRowModel().rows.length"
        @update:page="(p) => table?.tableApi?.setPageIndex(p - 1)"
      />
    </div>
  </div>
</template>
您可以使用 pagination 属性控制分页状态(可以使用 v-model 进行绑定)。

带获取的数据

您可以从 API 获取数据并在表格中使用它们。

ID名称邮箱公司
1
Leanne Graham avatar

Leanne Graham

@Bret

[email protected]Romaguera-Crona
2
Ervin Howell avatar

Ervin Howell

@Antonette

[email protected]Deckow-Crist
3
Clementine Bauch avatar

Clementine Bauch

@Samantha

[email protected]Romaguera-Jacobson
4
Patricia Lebsack avatar

Patricia Lebsack

@Karianne

[email protected]Robel-Corkery
5
Chelsey Dietrich avatar

Chelsey Dietrich

@Kamren

[email protected]Keebler LLC
6
Mrs. Dennis Schulist avatar

Mrs. Dennis Schulist

@Leopoldo_Corkery

[email protected]Considine-Lockman
7
Kurtis Weissnat avatar

Kurtis Weissnat

@Elwyn.Skiles

[email protected]Johns Group
8
Nicholas Runolfsdottir V avatar

Nicholas Runolfsdottir V

@Maxime_Nienow

[email protected]Abernathy Group
9
Glenna Reichert avatar

Glenna Reichert

@Delphine

[email protected]Yost and Sons
10
Clementina DuBuque avatar

Clementina DuBuque

@Moriah.Stanton

[email protected]Hoeger LLC
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'

const UAvatar = resolveComponent('UAvatar')

type User = {
  id: number
  name: string
  username: string
  email: string
  avatar: { src: string }
  company: { name: string }
}

const { data, status } = await useFetch<User[]>('https://jsonplaceholder.typicode.com/users', {
  key: 'table-users',
  transform: (data) => {
    return (
      data?.map((user) => ({
        ...user,
        avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, alt: `${user.name} avatar` }
      })) || []
    )
  },
  lazy: true
})

const columns: TableColumn<User>[] = [
  {
    accessorKey: 'id',
    header: 'ID'
  },
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => {
      return h('div', { class: 'flex items-center gap-3' }, [
        h(UAvatar, {
          ...row.original.avatar,
          size: 'lg'
        }),
        h('div', undefined, [
          h('p', { class: 'font-medium text-highlighted' }, row.original.name),
          h('p', { class: '' }, `@${row.original.username}`)
        ])
      ])
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'company',
    header: 'Company',
    cell: ({ row }) => row.original.company.name
  }
]
</script>

<template>
  <UTable :data="data" :columns="columns" :loading="status === 'pending'" class="flex-1" />
</template>

带无限滚动

如果您使用服务器端分页,可以使用useInfiniteScroll可组合项在滚动时加载更多数据。

IDAvatar名字邮箱用户名
无数据
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { useInfiniteScroll } from '@vueuse/core'

const UAvatar = resolveComponent('UAvatar')

type User = {
  id: number
  firstName: string
  username: string
  email: string
  image: string
}

type UserResponse = {
  users: User[]
  total: number
  skip: number
  limit: number
}

const skip = ref(0)

const { data, status, execute } = await useFetch(
  'https://dummyjson.com/users?limit=10&select=firstName,username,email,image',
  {
    key: 'table-users-infinite-scroll',
    params: { skip },
    transform: (data?: UserResponse) => {
      return data?.users
    },
    lazy: true,
    immediate: false
  }
)

const columns: TableColumn<User>[] = [
  {
    accessorKey: 'id',
    header: 'ID'
  },
  {
    accessorKey: 'image',
    header: 'Avatar',
    cell: ({ row }) => h(UAvatar, { src: row.original.image })
  },
  {
    accessorKey: 'firstName',
    header: 'First name'
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'username',
    header: 'Username'
  }
]

const users = ref<User[]>([])

watch(data, () => {
  users.value = [...users.value, ...(data.value || [])]
})

execute()

const table = useTemplateRef<ComponentPublicInstance>('table')

onMounted(() => {
  useInfiniteScroll(
    table.value?.$el,
    () => {
      skip.value += 10
    },
    {
      distance: 200,
      canLoadMore: () => {
        return status.value !== 'pending'
      }
    }
  )
})
</script>

<template>
  <div class="w-full">
    <UTable
      ref="table"
      :data="users"
      :columns="columns"
      :loading="status === 'pending'"
      sticky
      class="flex-1 h-80"
    />
  </div>
</template>

带拖放

使用useSortable可组合项从@vueuse/integrations启用表格的拖放功能。此集成封装了Sortable.js以提供无缝的拖放体验。

由于表格 ref 未暴露 `tbody` 元素,请通过 :ui 属性为其添加一个唯一的类,以便使用 useSortable 定位它(例如 :ui="{ tbody: 'my-table-tbody' }")。
#日期邮箱
金额
#4600Mar 11, 15:30[email protected]
€594.00
#4599Mar 11, 10:10[email protected]
€276.00
#4598Mar 11, 08:50[email protected]
€315.00
#4597Mar 10, 19:45[email protected]
€529.00
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable.mjs'

type Payment = {
  id: string
  date: string
  email: string
  amount: number
}

const data = ref<Payment[]>([
  {
    id: '4600',
    date: '2024-03-11T15:30:00',
    email: '[email protected]',
    amount: 594
  },
  {
    id: '4599',
    date: '2024-03-11T10:10:00',
    email: '[email protected]',
    amount: 276
  },
  {
    id: '4598',
    date: '2024-03-11T08:50:00',
    email: '[email protected]',
    amount: 315
  },
  {
    id: '4597',
    date: '2024-03-10T19:45:00',
    email: '[email protected]',
    amount: 529
  }
])

const columns: TableColumn<Payment>[] = [
  {
    accessorKey: 'id',
    header: '#',
    cell: ({ row }) => `#${row.getValue('id')}`
  },
  {
    accessorKey: 'date',
    header: 'Date',
    cell: ({ row }) => {
      return new Date(row.getValue('date')).toLocaleString('en-US', {
        day: 'numeric',
        month: 'short',
        hour: '2-digit',
        minute: '2-digit',
        hour12: false
      })
    }
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'amount',
    header: () => h('div', { class: 'text-right' }, 'Amount'),
    cell: ({ row }) => {
      const amount = Number.parseFloat(row.getValue('amount'))
      const formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'EUR'
      }).format(amount)
      return h('div', { class: 'text-right font-medium' }, formatted)
    }
  }
]

useSortable('.my-table-tbody', data, {
  animation: 150
})
</script>

<template>
  <div class="w-full">
    <UTable
      ref="table"
      :data="data"
      :columns="columns"
      :ui="{
        tbody: 'my-table-tbody'
      }"
    />
  </div>
</template>

使用插槽

您可以使用插槽来自定义表格的标题和数据单元格。

使用 #<column>-header 插槽自定义列的标题。您将能够在插槽作用域中访问 columnheadertable 属性。

使用 #<column>-cell 插槽自定义列的单元格。您将能够在插槽作用域中访问 cellcolumngetValuerenderValuerowtable 属性。

ID名称邮箱角色
1
Lindsay Walton avatar

Lindsay Walton

前端开发人员

[email protected]成员
2
Courtney Henry avatar

Courtney Henry

设计师

[email protected]管理员
3
Tom Cook avatar

Tom Cook

产品总监

[email protected]成员
4
Whitney Francis avatar

Whitney Francis

文案

[email protected]管理员
5
Leonard Krasner avatar

Leonard Krasner

高级设计师

[email protected]所有者
6
Floyd Miles avatar

Floyd Miles

首席设计师

[email protected]成员
<script setup lang="ts">
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
import { useClipboard } from '@vueuse/core'

interface User {
  id: number
  name: string
  position: string
  email: string
  role: string
}

const toast = useToast()
const { copy } = useClipboard()

const data = ref<User[]>([
  {
    id: 1,
    name: 'Lindsay Walton',
    position: 'Front-end Developer',
    email: '[email protected]',
    role: 'Member'
  },
  {
    id: 2,
    name: 'Courtney Henry',
    position: 'Designer',
    email: '[email protected]',
    role: 'Admin'
  },
  {
    id: 3,
    name: 'Tom Cook',
    position: 'Director of Product',
    email: '[email protected]',
    role: 'Member'
  },
  {
    id: 4,
    name: 'Whitney Francis',
    position: 'Copywriter',
    email: '[email protected]',
    role: 'Admin'
  },
  {
    id: 5,
    name: 'Leonard Krasner',
    position: 'Senior Designer',
    email: '[email protected]',
    role: 'Owner'
  },
  {
    id: 6,
    name: 'Floyd Miles',
    position: 'Principal Designer',
    email: '[email protected]',
    role: 'Member'
  }
])

const columns: TableColumn<User>[] = [
  {
    accessorKey: 'id',
    header: 'ID'
  },
  {
    accessorKey: 'name',
    header: 'Name'
  },
  {
    accessorKey: 'email',
    header: 'Email'
  },
  {
    accessorKey: 'role',
    header: 'Role'
  },
  {
    id: 'action'
  }
]

function getDropdownActions(user: User): DropdownMenuItem[][] {
  return [
    [
      {
        label: 'Copy user Id',
        icon: 'i-lucide-copy',
        onSelect: () => {
          copy(user.id.toString())

          toast.add({
            title: 'User ID copied to clipboard!',
            color: 'success',
            icon: 'i-lucide-circle-check'
          })
        }
      }
    ],
    [
      {
        label: 'Edit',
        icon: 'i-lucide-edit'
      },
      {
        label: 'Delete',
        icon: 'i-lucide-trash',
        color: 'error'
      }
    ]
  ]
}
</script>

<template>
  <UTable :data="data" :columns="columns" class="flex-1">
    <template #name-cell="{ row }">
      <div class="flex items-center gap-3">
        <UAvatar
          :src="`https://i.pravatar.cc/120?img=${row.original.id}`"
          size="lg"
          :alt="`${row.original.name} avatar`"
        />
        <div>
          <p class="font-medium text-highlighted">
            {{ row.original.name }}
          </p>
          <p>
            {{ row.original.position }}
          </p>
        </div>
      </div>
    </template>
    <template #action-cell="{ row }">
      <UDropdownMenu :items="getDropdownActions(row.original)">
        <UButton
          icon="i-lucide-ellipsis-vertical"
          color="neutral"
          variant="ghost"
          aria-label="Actions"
        />
      </UDropdownMenu>
    </template>
  </UTable>
</template>

API

属性

属性默认值类型
as

'div'

any

此组件应渲染为的元素或组件。

data

unknown[]

columns

TableColumn<unknown, unknown>[]

标题

string

meta

TableMeta<unknown>

您可以将任何对象传递给 options.meta,并通过 table.options.meta 在任何可用的 table 中访问它。

t('table.noData')

string

表格为空时显示的文本。

sticky

false

boolean | "header" | "footer"

表格是否应有固定标题或页脚。`true` 表示两者都固定,`'header'` 表示仅标题固定,`'footer'` 表示仅页脚固定。

loading

boolean

表格是否应处于加载状态。

loadingColor

'primary'

"error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"

loadingAnimation

'carousel'

"carousel" | "carousel-inverse" | "swing" | "elastic"

watchOptions

{ deep: true }

WatchOptions<boolean>

使用 watchOptions 属性自定义响应性(例如:禁用对数据变化的深度监听或限制最大遍历深度)。这可以通过减少不必要的重新渲染来提高性能,但应谨慎使用,因为如果管理不当,可能会导致意外行为。

globalFilterOptions

Omit<GlobalFilterOptions<unknown>, "onGlobalFilterChange">

columnFiltersOptions

Omit<ColumnFiltersOptions<unknown>, "getFilteredRowModel" | "onColumnFiltersChange">

columnPinningOptions

Omit<ColumnPinningOptions, "onColumnPinningChange">

columnSizingOptions

Omit<ColumnSizingOptions, "onColumnSizingChange" | "onColumnSizingInfoChange">

visibilityOptions

Omit<VisibilityOptions, "onColumnVisibilityChange">

sortingOptions

Omit<SortingOptions<unknown>, "getSortedRowModel" | "onSortingChange">

groupingOptions

Omit<GroupingOptions, "onGroupingChange">

expandedOptions

Omit<ExpandedOptions<unknown>, "getExpandedRowModel" | "onExpandedChange">

rowSelectionOptions

Omit<RowSelectionOptions<unknown>, "onRowSelectionChange">

rowPinningOptions

Omit<RowPinningOptions<unknown>, "onRowPinningChange">

paginationOptions

Omit<PaginationOptions, "onPaginationChange">

facetedOptions

FacetedOptions<unknown>

onSelect

(row: TableRow<unknown>, e?: Event | undefined): void

onHover

(e: Event, row: TableRow<unknown> | null): void

onContextmenu

(e: Event, row: TableRow<unknown>): void | ((e: Event, row: TableRow<unknown>) => void)[]

state

Partial<TableState>

onStateChange

(updater: Updater<TableState>): void

renderFallbackValue

any

_features

TableFeature<any>[]

您可以添加到表格实例的额外功能数组。

autoResetAll

boolean

设置此选项以覆盖任何 autoReset... 功能选项。

debugAll

boolean

将此选项设置为 true 以将所有调试信息输出到控制台。

debugCells

boolean

将此选项设置为 true 以将单元格调试信息输出到控制台。

debugColumns

boolean

将此选项设置为 true 以将列调试信息输出到控制台。

debugHeaders

boolean

将此选项设置为 true 以将标题调试信息输出到控制台。

debugRows

boolean

将此选项设置为 true 以将行调试信息输出到控制台。

debugTable

boolean

将此选项设置为 true 以将表格调试信息输出到控制台。

defaultColumn

Partial<ColumnDefBase<unknown, unknown> & StringHeaderIdentifier> | Partial<ColumnDefBase<unknown, unknown> & IdIdentifier<unknown, unknown>> | Partial<GroupColumnDefBase<unknown, unknown> & StringHeaderIdentifier> | Partial<GroupColumnDefBase<unknown, unknown> & IdIdentifier<unknown, unknown>> | Partial<AccessorKeyColumnDefBase<unknown, unknown> & Partial<StringHeaderIdentifier>> | Partial<AccessorKeyColumnDefBase<unknown, unknown> & Partial<IdIdentifier<unknown, unknown>>> | Partial<AccessorFnColumnDefBase<unknown, unknown> & StringHeaderIdentifier> | Partial<AccessorFnColumnDefBase<unknown, unknown> & IdIdentifier<unknown, unknown>>

用于表中所有列定义的默认列选项。

getRowId

(originalRow: unknown, index: number, parent?: Row<unknown> | undefined): string

此可选函数用于为任何给定行派生唯一 ID。如果未提供,则使用行索引(嵌套行使用其祖父行的索引(例如 .)将其连接起来,例如 index.index.index)。如果您需要识别源自任何服务器端操作的单个行,建议您使用此函数返回一个无论网络 IO/歧义如何都具有意义的 ID,例如 userId、taskId、数据库 ID 字段等。

getSubRows

(originalRow: unknown, index: number): unknown[]

此可选函数用于访问任何给定行的子行。如果您正在使用嵌套行,则需要使用此函数从行中返回子行对象(或 undefined)。

initialState

InitialTableState

使用此选项可选择性地将初始状态传递给表格。当表格自动(例如 options.autoResetPageIndex)或通过函数(例如 table.resetRowSelection())重置各种表格状态时,将使用此状态。大多数重置函数允许您选择性地传递一个标志,以重置为空白/默认状态而不是初始状态。

当此对象更改时,表格状态将不会被重置,这也意味着初始状态对象不需要保持稳定。

mergeOptions

(defaultOptions: TableOptions<unknown>, options: Partial<TableOptions<unknown>>): TableOptions<unknown>

此选项用于可选地实现表格选项的合并。

globalFilter

undefined

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?: ClassNameValue; base?: ClassNameValue; caption?: ClassNameValue; thead?: ClassNameValue; tbody?: ClassNameValue; tfoot?: ClassNameValue; tr?: ClassNameValue; th?: ClassNameValue; td?: ClassNameValue; separator?: ClassNameValue; empty?: ClassNameValue; loading?: ClassNameValue; }

插槽

插槽类型
expanded

{ row: Row<unknown>; }

{}

loading

{}

标题

{}

body-top

{}

body-bottom

{}

可访问属性

您可以使用useTemplateRef.

<script setup lang="ts">
const table = useTemplateRef('table')
</script>

<template>
  <UTable ref="table" />
</template>

这将使您能够访问以下内容

名称类型
tableRefRef<HTMLTableElement | null>
tableApiRef<Table | null>

主题

app.config.ts
export default defineAppConfig({
  ui: {
    table: {
      slots: {
        root: 'relative overflow-auto',
        base: 'min-w-full overflow-clip',
        caption: 'sr-only',
        thead: 'relative',
        tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
        tfoot: 'relative',
        tr: 'data-[selected=true]:bg-elevated/50',
        th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
        td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
        separator: 'absolute z-[1] left-0 w-full h-px bg-(--ui-border-accented)',
        empty: 'py-6 text-center text-sm text-muted',
        loading: 'py-6 text-center'
      },
      variants: {
        pinned: {
          true: {
            th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
            td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
          }
        },
        sticky: {
          true: {
            thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur',
            tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
          },
          header: {
            thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
          },
          footer: {
            tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
          }
        },
        loading: {
          true: {
            thead: 'after:absolute after:z-[1] after:h-px'
          }
        },
        loadingAnimation: {
          carousel: '',
          'carousel-inverse': '',
          swing: '',
          elastic: ''
        },
        loadingColor: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        }
      },
      compoundVariants: [
        {
          loading: true,
          loadingColor: 'primary',
          class: {
            thead: 'after:bg-primary'
          }
        },
        {
          loading: true,
          loadingColor: 'neutral',
          class: {
            thead: 'after:bg-inverted'
          }
        },
        {
          loading: true,
          loadingAnimation: 'carousel',
          class: {
            thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'carousel-inverse',
          class: {
            thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'swing',
          class: {
            thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'elastic',
          class: {
            thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
          }
        }
      ],
      defaultVariants: {
        loadingColor: 'primary',
        loadingAnimation: 'carousel'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        table: {
          slots: {
            root: 'relative overflow-auto',
            base: 'min-w-full overflow-clip',
            caption: 'sr-only',
            thead: 'relative',
            tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
            tfoot: 'relative',
            tr: 'data-[selected=true]:bg-elevated/50',
            th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
            td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
            separator: 'absolute z-[1] left-0 w-full h-px bg-(--ui-border-accented)',
            empty: 'py-6 text-center text-sm text-muted',
            loading: 'py-6 text-center'
          },
          variants: {
            pinned: {
              true: {
                th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
                td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
              }
            },
            sticky: {
              true: {
                thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur',
                tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              },
              header: {
                thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              },
              footer: {
                tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              }
            },
            loading: {
              true: {
                thead: 'after:absolute after:z-[1] after:h-px'
              }
            },
            loadingAnimation: {
              carousel: '',
              'carousel-inverse': '',
              swing: '',
              elastic: ''
            },
            loadingColor: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            }
          },
          compoundVariants: [
            {
              loading: true,
              loadingColor: 'primary',
              class: {
                thead: 'after:bg-primary'
              }
            },
            {
              loading: true,
              loadingColor: 'neutral',
              class: {
                thead: 'after:bg-inverted'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel',
              class: {
                thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel-inverse',
              class: {
                thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'swing',
              class: {
                thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'elastic',
              class: {
                thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
              }
            }
          ],
          defaultVariants: {
            loadingColor: 'primary',
            loadingAnimation: 'carousel'
          }
        }
      }
    })
  ]
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import uiPro from '@nuxt/ui-pro/vite'

export default defineConfig({
  plugins: [
    vue(),
    uiPro({
      ui: {
        table: {
          slots: {
            root: 'relative overflow-auto',
            base: 'min-w-full overflow-clip',
            caption: 'sr-only',
            thead: 'relative',
            tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
            tfoot: 'relative',
            tr: 'data-[selected=true]:bg-elevated/50',
            th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
            td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
            separator: 'absolute z-[1] left-0 w-full h-px bg-(--ui-border-accented)',
            empty: 'py-6 text-center text-sm text-muted',
            loading: 'py-6 text-center'
          },
          variants: {
            pinned: {
              true: {
                th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
                td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
              }
            },
            sticky: {
              true: {
                thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur',
                tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              },
              header: {
                thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              },
              footer: {
                tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              }
            },
            loading: {
              true: {
                thead: 'after:absolute after:z-[1] after:h-px'
              }
            },
            loadingAnimation: {
              carousel: '',
              'carousel-inverse': '',
              swing: '',
              elastic: ''
            },
            loadingColor: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            }
          },
          compoundVariants: [
            {
              loading: true,
              loadingColor: 'primary',
              class: {
                thead: 'after:bg-primary'
              }
            },
            {
              loading: true,
              loadingColor: 'neutral',
              class: {
                thead: 'after:bg-inverted'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel',
              class: {
                thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel-inverse',
              class: {
                thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'swing',
              class: {
                thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'elastic',
              class: {
                thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
              }
            }
          ],
          defaultVariants: {
            loadingColor: 'primary',
            loadingAnimation: 'carousel'
          }
        }
      }
    })
  ]
})
为便于阅读,compoundVariants 中的某些颜色已省略。请在 GitHub 上查看源代码。