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

用法

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

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

数据

使用 data prop 作为对象数组,列将根据对象的键自动生成。

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

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

使用 columns prop 作为ColumnDef对象数组,包含以下属性:

  • accessorKey: 提取列值时使用的行对象键。
  • header: 列的头部显示内容。如果传入字符串,可用作列 ID 的默认值。如果传入函数,它将接收一个头部 props 对象,并应返回渲染后的头部值(具体类型取决于所使用的适配器)。
  • cell: 列中每行的单元格显示内容。如果传入函数,它将接收一个单元格 props 对象,并应返回渲染后的单元格值(具体类型取决于所使用的适配器)。
  • meta: 列的额外属性。
    • :
      • td: 应用于 td 元素的类。
      • th: 应用于 th 元素的类。

为了渲染组件或其他 HTML 元素,你需要使用 Vue 的h 函数headercell props 中。这与其他使用 slot 的组件不同,但提供了更大的灵活性。

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

const UBadge = resolveComponent('UBadge')

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

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

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

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

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

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

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

Meta

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

  • :
    • tr: 应用于 tr 元素的类。

加载中

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

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

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

粘性

使用 sticky prop 使头部具有粘性。

Id日期状态邮箱金额
46002024-03-11T15:30:00paid[email protected]594
45992024-03-11T10:10:00failed[email protected]276
45982024-03-11T08:50:00refunded[email protected]315
45972024-03-10T19:45:00paid[email protected]529
45962024-03-10T15:55:00paid[email protected]639
45952024-03-10T15:55:00paid[email protected]639
45942024-03-10T15:55:00paid[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:30paid[email protected]
€594.00
#4599Mar 11, 10:10failed[email protected]
€276.00
#4598Mar 11, 08:50refunded[email protected]
€315.00
#4597Mar 10, 19:45paid[email protected]
€529.00
#4596Mar 10, 15:55paid[email protected]
€639.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table'

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

const toast = useToast()

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

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

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

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

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

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

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

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

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

带可展开行

你可以添加一个新列,在 cell 中渲染一个 Button 组件,使用 TanStack Table 的展开 API.

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

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

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

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

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

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

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

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

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

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

带行选择

你可以添加一个新列,在 headercell 中渲染一个 Checkbox 组件,使用 TanStack Table 的行选择 API.

日期状态邮箱
金额
Mar 11, 15:30paid[email protected]
€594.00
Mar 11, 10:10failed[email protected]
€276.00
Mar 11, 08:50refunded[email protected]
€315.00
Mar 10, 19:45paid[email protected]
€529.00
Mar 10, 15:55paid[email protected]
€639.00
选中 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 prop 来控制行的选择状态(可以通过 v-model 进行绑定)。

@select 事件

你可以添加 @select 监听器使行可点击。处理函数接收 TableRow 实例作为第一个参数,可选的 Event 作为第二个参数。

你可以使用它来导航到页面、打开模态框,甚至手动选择行。
日期状态邮箱
金额
Mar 11, 15:30paid[email protected]
€594.00
Mar 11, 10:10failed[email protected]
€276.00
Mar 11, 08:50refunded[email protected]
€315.00
Mar 10, 19:45paid[email protected]
€529.00
Mar 10, 15:55paid[email protected]
€639.00
选中 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>

带列排序

你可以更新列的 header,在 header 中渲染一个 Button 组件,使用 TanStack Table 的排序 API.

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

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

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

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

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

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

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

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

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

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

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

你也可以创建一个可重用组件,使任何列头部都可以排序。

#4596Mar 10, 15:55paid[email protected]
€639.00
#4597Mar 10, 19:45paid[email protected]
€529.00
#4598Mar 11, 08:50refunded[email protected]
€315.00
#4599Mar 11, 10:10failed[email protected]
€276.00
#4600Mar 11, 15:30paid[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:00paid[email protected]
€594,000.00
#459900000000000000000000000000000000000002024-03-11T10:10:00failed[email protected]
€276,000.00
#459800000000000000000000000000000000000002024-03-11T08:50:00refunded[email protected]
€315,000.00
#459700000000000000000000000000000000000002024-03-10T19:45:00paid[email protected]
€5,290,000.00
#459600000000000000000000000000000000000002024-03-10T15:55:00paid[email protected]
€639,000.00
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column } from '@tanstack/vue-table'

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

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

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

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

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

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

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

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

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

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

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

带列可见性

你可以使用一个 DropdownMenu 组件,使用 TanStack Table 的列可见性 API.

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

const UBadge = resolveComponent('UBadge')

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

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

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

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

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

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

const table = useTemplateRef('table')

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

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

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

带列过滤

你可以使用一个 Input 组件,使用 TanStack Table 的列过滤 API.

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

const UBadge = resolveComponent('UBadge')

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

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

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

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

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

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

const table = useTemplateRef('table')

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

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

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

带全局过滤

你可以使用一个 Input 组件,使用 TanStack Table 的全局过滤 API.

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

const UBadge = resolveComponent('UBadge')

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

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

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

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

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

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

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

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

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

带分页

你可以使用一个 Pagination 组件,使用分页 API.

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

#日期邮箱
金额
#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 prop 来控制分页状态(可以通过 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在 Table 上启用拖放功能。这个集成封装了Sortable.js以提供流畅的拖放体验。

由于表格 ref 没有暴露 tbody 元素,请通过 :ui prop 为其添加一个唯一的类,以便用 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

Front-end Developer

[email protected]Member
2
Courtney Henry avatar

Courtney Henry

Designer

[email protected]Admin
3
Tom Cook avatar

Tom Cook

Director of Product

[email protected]Member
4
Whitney Francis avatar

Whitney Francis

Copywriter

[email protected]Admin
5
Leonard Krasner avatar

Leonard Krasner

Senior Designer

[email protected]Owner
6
Floyd Miles avatar

Floyd Miles

Principal Designer

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

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

const toast = useToast()

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

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

function getDropdownActions(user: User): DropdownMenuItem[][] {
  return [
    [
      {
        label: 'Copy user Id',
        icon: 'i-lucide-copy',
        onSelect: () => {
          navigator.clipboard.writeText(user.id.toString())
          toast.add({
            title: 'User ID copied to clipboard!',
            color: 'success',
            icon: 'i-lucide-circle-check'
          })
        }
      }
    ],
    [
      {
        label: 'Edit',
        icon: 'i-lucide-edit'
      },
      {
        label: 'Delete',
        icon: 'i-lucide-trash',
        color: 'error'
      }
    ]
  ]
}
</script>

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

API

属性

属性默认值类型
as

'div'

any

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

data

unknown[]

columns

TableColumn<unknown, unknown>[]

caption

string

meta

TableMeta<unknown>

你可以将任何对象传递给 options.meta,并通过 table.options.metatable 可用的任何地方访问它。

空状态

t('table.noData')

string

表格为空时显示的文本。

sticky

false

boolean

表格是否应具有粘性表头。

loading

boolean

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

loadingColor

'primary'

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

loadingAnimation

'carousel'

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

watchOptions

{ deep: true }

WatchOptions<boolean>

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

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>

state

Partial<TableState>

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,例如用户 ID、任务 ID、数据库 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; ... 4 more ...; loading?: ClassNameValue; }

插槽

Slot类型
expanded

{ row: Row<unknown>; }

空状态

{}

loading

{}

caption

{}

暴露

您可以使用以下方式访问带类型组件实例: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 [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)',
        tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
        tr: 'data-[selected=true]:bg-elevated/50',
        th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
        td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
        empty: 'py-6 text-center text-sm text-muted',
        loading: 'py-6 text-center'
      },
      variants: {
        pinned: {
          true: {
            th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
            td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
          }
        },
        sticky: {
          true: {
            thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
          }
        },
        loading: {
          true: {
            thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px'
          }
        },
        loadingAnimation: {
          carousel: '',
          'carousel-inverse': '',
          swing: '',
          elastic: ''
        },
        loadingColor: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        }
      },
      compoundVariants: [
        {
          loading: true,
          loadingColor: 'primary',
          class: {
            thead: 'after:bg-primary'
          }
        },
        {
          loading: true,
          loadingColor: 'neutral',
          class: {
            thead: 'after:bg-inverted'
          }
        },
        {
          loading: true,
          loadingAnimation: 'carousel',
          class: {
            thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'carousel-inverse',
          class: {
            thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'swing',
          class: {
            thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
          }
        },
        {
          loading: true,
          loadingAnimation: 'elastic',
          class: {
            thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
          }
        }
      ],
      defaultVariants: {
        loadingColor: 'primary',
        loadingAnimation: 'carousel'
      }
    }
  }
})
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 [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)',
            tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
            tr: 'data-[selected=true]:bg-elevated/50',
            th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
            td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
            empty: 'py-6 text-center text-sm text-muted',
            loading: 'py-6 text-center'
          },
          variants: {
            pinned: {
              true: {
                th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
                td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
              }
            },
            sticky: {
              true: {
                thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              }
            },
            loading: {
              true: {
                thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px'
              }
            },
            loadingAnimation: {
              carousel: '',
              'carousel-inverse': '',
              swing: '',
              elastic: ''
            },
            loadingColor: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            }
          },
          compoundVariants: [
            {
              loading: true,
              loadingColor: 'primary',
              class: {
                thead: 'after:bg-primary'
              }
            },
            {
              loading: true,
              loadingColor: 'neutral',
              class: {
                thead: 'after:bg-inverted'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel',
              class: {
                thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel-inverse',
              class: {
                thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'swing',
              class: {
                thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'elastic',
              class: {
                thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
              }
            }
          ],
          defaultVariants: {
            loadingColor: 'primary',
            loadingAnimation: 'carousel'
          }
        }
      }
    })
  ]
})
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 [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)',
            tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
            tr: 'data-[selected=true]:bg-elevated/50',
            th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
            td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
            empty: 'py-6 text-center text-sm text-muted',
            loading: 'py-6 text-center'
          },
          variants: {
            pinned: {
              true: {
                th: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0',
                td: 'sticky bg-default/75 data-[pinned=left]:left-0 data-[pinned=right]:right-0'
              }
            },
            sticky: {
              true: {
                thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
              }
            },
            loading: {
              true: {
                thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px'
              }
            },
            loadingAnimation: {
              carousel: '',
              'carousel-inverse': '',
              swing: '',
              elastic: ''
            },
            loadingColor: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            }
          },
          compoundVariants: [
            {
              loading: true,
              loadingColor: 'primary',
              class: {
                thead: 'after:bg-primary'
              }
            },
            {
              loading: true,
              loadingColor: 'neutral',
              class: {
                thead: 'after:bg-inverted'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel',
              class: {
                thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'carousel-inverse',
              class: {
                thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'swing',
              class: {
                thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
              }
            },
            {
              loading: true,
              loadingAnimation: 'elastic',
              class: {
                thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
              }
            }
          ],
          defaultVariants: {
            loadingColor: 'primary',
            loadingAnimation: 'carousel'
          }
        }
      }
    })
  ]
})
compoundVariants 中的某些颜色为方便阅读已省略。请查看 GitHub 上的源代码。