数据表格
使用 TanStack Table 构建的强大表格和数据网格。
| Status | Amount | |||
|---|---|---|---|---|
success | ken99@yahoo.com | $316.00 | ||
success | Abe45@gmail.com | $242.00 | ||
processing | Monserrat44@gmail.com | $837.00 | ||
success | Silas22@gmail.com | $874.00 | ||
failed | carmella@hotmail.com | $721.00 |
简介
我创建的每个数据表格或数据网格都是独一无二的。它们的行为各不相同,有特定的排序和过滤需求,并且与不同的数据源协同工作。
将这些变化全部合并到单一组件中并不合理。如果这样做,我们将失去无头 UI 所提供的灵活性。
因此,与其提供一个数据表格组件,我认为提供一个关于如何构建您自己的表格的指南会更有帮助。
我们将从基本的 <Table /> 组件开始,从头开始构建一个复杂的数据表格。
提示: 如果您发现自己在应用程序的多个地方使用相同的表格,您始终可以将其提取到一个可重用的组件中。
目录
本指南将向您展示如何使用 TanStack Table 和 <Table /> 组件来构建您自己的自定义数据表格。我们将涵盖以下主题:
安装
- 将
<Table />组件添加到您的项目:
pnpm dlx shadcn-vue@latest add table
- 添加
tanstack/vue-table依赖:
pnpm add @tanstack/vue-table
示例
列固定
| Status | Amount | |||
|---|---|---|---|---|
success | ken99@yahoo.com | $316.00 | ||
success | Abe45@gmail.com | $242.00 | ||
processing | Monserrat44@gmail.com | $837.00 | ||
success | Silas22@gmail.com | $874.00 | ||
failed | carmella@hotmail.com | $721.00 |
响应式表格
响应式表格在 TanStack Table 的 v8.20.0 版本中添加。您可以查看文档以获取更多信息。我们添加了一个随机化 status 列的示例。一个要点是您需要改变完整的数据,因为它是一个 shallowRef 对象。
⚠️ 出于性能原因,底层使用了
shallowRef,这意味着数据不是深度响应式的,只有.value是。要更新数据,您必须直接改变数据。
相关 PR: Tanstack/table #5687
如果您想改变 props.data,您应该使用 defineModel。
对您的数据对象使用 ref 或 shallowRef 没有区别;它将被 TanStack Table 自动变为 shallowRef。
| Status | Amount | |||
|---|---|---|---|---|
success | ken99@yahoo.com | $316.00 | ||
success | Abe45@gmail.com | $242.00 | ||
processing | Monserrat44@gmail.com | $837.00 | ||
success | Silas22@gmail.com | $874.00 | ||
failed | carmella@hotmail.com | $721.00 |
前提条件
我们将构建一个表格来显示最近的付款。我们的数据如下所示:
interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
export const payments: Payment[] = [
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: 'm@example.com',
},
{
id: '489e1d42',
amount: 125,
status: 'processing',
email: 'example@gmail.com',
},
// ...
]项目结构
首先创建以下文件结构:
components
└── payments
├── columns.ts
├── data-table.vue
├── data-table-dropdown.vue
└── app.vue我在这里使用 Nuxt 示例,但这适用于任何其他 Vue 框架。
columns.ts它将包含我们的列定义。data-table.vue它将包含我们的<DataTable />组件。data-table-dropdown.vue它将包含我们的<DropdownAction />组件。app.vue这是我们将获取数据并渲染表格的地方。
基础表格
让我们从构建一个基础表格开始。
列定义
首先,我们将在 columns.ts 文件中定义我们的列。
import { h } from 'vue'
export const columns: ColumnDef<Payment>[] = [
{
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: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
}
]注意: 列是您定义表格核心外观的地方。它们定义了将显示的数据、数据的格式、排序和过滤方式。
<DataTable /> 组件
接下来,我们将创建一个 <DataTable /> 组件来渲染我们的表格。
<script setup lang="ts" generic="TData, TValue">
import type { ColumnDef } from "@tanstack/vue-table";
import { FlexRender, getCoreRowModel, useVueTable } from "@tanstack/vue-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const props = defineProps<{
columns: ColumnDef<TData, TValue>[];
data: TData[];
}>();
const table = useVueTable({
get data() {
return props.data;
},
get columns() {
return props.columns;
},
getCoreRowModel: getCoreRowModel(),
});
</script>
<template>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
>
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<TableRow
v-for="row in table.getRowModel().rows"
:key="row.id"
:data-state="row.getIsSelected() ? 'selected' : undefined"
>
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</TableCell>
</TableRow>
</template>
<template v-else>
<TableRow>
<TableCell :colspan="columns.length" class="h-24 text-center">
No results.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</template>提示:如果您发现自己在多个地方使用 <DataTable />,这是您可以提取到 components/ui/data-table.vue 以使其可重用的组件。
<DataTable :columns="columns" :data="data" />
渲染表格
最后,我们将在我们的索引组件中渲染我们的表格。
<script setup lang="ts">
import type { Payment } from "./components/columns";
import { onMounted, ref } from "vue";
import { columns } from "./components/columns";
import DataTable from "./components/DataTable.vue";
const data = ref<Payment[]>([]);
async function getData(): Promise<Payment[]> {
// 在此处从您的 API 获取数据。
return [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "m@example.com",
},
// ...
];
}
onMounted(async () => {
data.value = await getData();
});
</script>
<template>
<div class="container py-10 mx-auto">
<DataTable :columns="columns" :data="data" />
</div>
</template>单元格格式化
让我们格式化金额单元格以显示美元金额。我们还将单元格右对齐。
更新列定义
更新金额的 header 和 cell 定义如下:
import { h } from "vue";
export const columns: ColumnDef<Payment>[] = [
{
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: "USD",
}).format(amount);
return h("div", { class: "text-right font-medium" }, formatted);
},
},
];您可以使用相同的方法来格式化其他单元格和标题。
行操作
让我们向表格添加行操作。我们将为此使用一个 <Dropdown /> 组件。
将以下内容添加到您的 DataTableDropDown.vue 组件中
<script setup lang="ts">
import { MoreHorizontal } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
defineProps<{
payment: {
id: string;
};
}>();
function copy(id: string) {
navigator.clipboard.writeText(id);
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="w-8 h-8 p-0">
<span class="sr-only">Open menu</span>
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem @click="copy(payment.id)">
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>更新列定义
更新我们的列定义以添加一个新的 actions 列。actions 单元格返回一个 <Dropdown /> 组件。
import { ColumnDef } from "@tanstack/vue-table";
import DropdownAction from "@/components/DataTableDropDown.vue";
export const columns: ColumnDef<Payment>[] = [
// ...
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const payment = row.original;
return h(
"div",
{ class: "relative" },
h(DropdownAction, {
payment,
}),
);
},
},
];您可以使用 row.original 在 cell 函数中访问行数据。使用它来处理您的行操作,例如使用 id 向您的 API 发出 DELETE 调用。
分页
接下来,我们将向表格添加分页。
更新 <DataTable>
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
useVueTable,
} from "@tanstack/vue-table"
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})这将自动将您的行分页为每页 10 行。有关自定义页面大小和实现手动分页的更多信息,请参阅分页文档。
添加分页控件
我们可以使用 <Button /> 组件和 table.previousPage()、table.nextPage() API 方法向表格添加分页控件。
<script lang="ts" generic="TData, TValue">
import { Button } from "@/components/ui/button";
const table = useVueTable({
get data() {
return props.data;
},
get columns() {
return props.columns;
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
</script>
<template>
<div>
<div class="border rounded-md">
<Table> { // .... } </Table>
</div>
<div class="flex items-center justify-end py-4 space-x-2">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</Button>
<Button
variant="outline"
size="sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</Button>
</div>
</div>
</template>有关更高级的分页组件,请参阅可重用组件部分。
排序
让我们使电子邮件列可排序。
将以下内容添加到您的 utils 文件中
import type { Updater } from "@tanstack/vue-table";
import type { ClassValue } from "clsx";
import type { Ref } from "vue";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function valueUpdater<T extends Updater<any>>(
updaterOrValue: T,
ref: Ref,
) {
ref.value =
typeof updaterOrValue === "function"
? updaterOrValue(ref.value)
: updaterOrValue;
}valueUpdater 函数更新 Vue ref 对象的值。它处理直接赋值和使用函数的转换。如果 updaterOrValue 是一个函数,则使用当前的 ref 值调用它,并将结果分配给 ref.value。如果不是函数,则直接分配给 ref.value。此实用程序增强了更新 ref 值的灵活性。虽然 Vue ref 可以直接管理响应式状态,但 valueUpdater 简化了值更新,当新状态可以是直接值或基于当前状态生成它的函数时,提高了代码的可读性和可维护性。
更新 <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
SortingState,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { valueUpdater } from '@/lib/utils'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
state: {
get sorting() { return sorting.value },
},
})
</script>
<template>
<div>
<div class="border rounded-md">
<Table>{ ... }</Table>
</div>
</div>
</template>使标题单元格可排序
我们现在可以更新 email 标题单元格以添加排序控件。
// components/payments/columns.ts
import type {
ColumnDef,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: 'email',
header: ({ column }) => {
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},
]当用户在标题单元格上切换时,这将自动对表格进行排序(升序和降序)。
过滤
让我们添加一个搜索输入框来过滤表格中的电子邮件。
更新 <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
} from '@tanstack/vue-table'
import { valueUpdater } from '@/lib/utils'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
getFilteredRowModel: getFilteredRowModel(),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
},
})
</script>
<template>
<div>
<div class="flex items-center py-4">
<Input class="max-w-sm" placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)" />
</div>
<div class="border rounded-md">
<Table>{ ... }</Table>
</div>
</div>
</template>现在为 email 列启用了过滤。您也可以向其他列添加过滤器。有关自定义过滤器的更多信息,请参阅过滤文档。
可见性
使用 @tanstack/vue-table 可见性 API 添加列可见性相当简单。
更新 <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { valueUpdater } from '@/lib/utils'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
},
})
</script>
<template>
<div>
<div class="flex items-center py-4">
<Input class="max-w-sm" placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns
<ChevronDown class="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id"
class="capitalize" :modelValue="column.getIsVisible()" @update:modelValue="(value) => {
column.toggleVisibility(!!value)
}">
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<TableRow v-for="row in table.getRowModel().rows" :key="row.id"
:data-state="row.getIsSelected() ? 'selected' : undefined">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
</template>
<template v-else>
<TableRow>
<TableCell :colSpan="columns.length" class="h-24 text-center">
No results.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
</template>这添加了一个下拉菜单,您可以使用它来切换列的可见性。
行选择
接下来,我们将向表格添加行选择功能。
更新列定义
import type { ColumnDef } from '@tanstack/vue-table'
import { Checkbox } from '@/components/ui/checkbox'
export const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'modelValue': table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean) => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean) => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
]更新 <DataTable>
<script setup lang="ts" generic="TData, TValue">
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
},
})
</script>
<template>
<div>
<div class="border rounded-md">
<Table />
</div>
</div>
</template>这为每一行添加了一个复选框,并在标题中添加了一个复选框以选择所有行。
显示选中的行
您可以使用 table.getFilteredSelectedRowModel() API 显示选中行的数量。
<template>
<div>
<div class="border rounded-md">
<Table />
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="space-x-2">
<PaginationButtons />
</div>
</div>
</div>
</template>展开
让行可以展开。
更新 <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
ExpandedState,
} from '@tanstack/vue-table'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { valueUpdater } from '@/lib/utils'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
getExpandedRowModel,
useVueTable,
} from "@tanstack/vue-table"
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
</script>
<template>
<div>
<div class="flex items-center py-4">
<Input class="max-w-sm" placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns
<ChevronDown class="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id"
class="capitalize" :modelValue="column.getIsVisible()" @update:modelValue="(value) => {
column.toggleVisibility(!!value)
}">
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() ? 'selected' : undefined">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>
<template v-else>
<TableRow>
<TableCell :colSpan="columns.length" class="h-24 text-center">
No results.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
</template>将展开操作添加到 DataTableDropDown.vue 组件
<script setup lang="ts">
import { MoreHorizontal } from 'lucide-vue-next'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
defineProps<{
payment: {
id: string
}
}>()
defineEmits<{
(e: 'expand'): void
}>()
function copy(id: string) {
navigator.clipboard.writeText(id)
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="w-8 h-8 p-0">
<span class="sr-only">Open menu</span>
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem @click="copy(payment.id)">
Copy payment ID
</DropdownMenuItem>
<DropdownMenuItem @click="$emit('expand')">
Expand
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>使行可展开
现在我们可以更新操作单元格以添加展开控件。
<script setup lang="ts">
export const columns: ColumnDef<Payment>[] = [
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
onExpand: row.toggleExpanded,
}))
},
},
]
</script>可重用组件
这里有一些组件可以用来构建您的数据表格。这些来自 Tasks 演示。
列标题
使任何列标题可排序和可隐藏。
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import ArrowDownIcon from '~icons/radix-icons/arrow-down'
import ArrowUpIcon from '~icons/radix-icons/arrow-up'
import CaretSortIcon from '~icons/radix-icons/caret-sort'
import EyeNoneIcon from '~icons/radix-icons/eye-none'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface DataTableColumnHeaderProps {
column: Column<Task, any>
title: string
}
defineProps<DataTableColumnHeaderProps>()
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<div v-if="column.getCanSort()" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{{ title }}</span>
<ArrowDownIcon v-if="column.getIsSorted() === 'desc'" class="w-4 h-4 ml-2" />
<ArrowUpIcon v-else-if=" column.getIsSorted() === 'asc'" class="w-4 h-4 ml-2" />
<CaretSortIcon v-else class="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem @click="column.toggleSorting(false)">
<ArrowUpIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem @click="column.toggleSorting(true)">
<ArrowDownIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="column.toggleVisibility(false)">
<EyeNoneIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div v-else :class="$attrs.class">
{{ title }}
</div>
</template>export const columns = [
{
accessorKey: "email",
header: ({ column }) => (
h(DataTableColumnHeader, {
column: column,
title: 'Email'
})
),
},
]分页
向表格添加分页控件,包括页面大小和选择计数。
<script setup lang="ts">
import { type Table } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import ChevronLeftIcon from '~icons/radix-icons/chevron-left'
import ChevronRightIcon from '~icons/radix-icons/chevron-right'
import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left'
import DoubleArrowRightIcon from '~icons/radix-icons/double-arrow-right'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface DataTablePaginationProps {
table: Table<Task>
}
defineProps<DataTablePaginationProps>()
</script>
<template>
<div class="flex items-center justify-between px-2">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">
Rows per page
</p>
<Select
:model-value="`${table.getState().pagination.pageSize}`"
@update:model-value="table.setPageSize"
>
<SelectTrigger class="h-8 w-[70px]">
<SelectValue :placeholder="`${table.getState().pagination.pageSize}`" />
</SelectTrigger>
<SelectContent side="top">
<SelectItem v-for="pageSize in [10, 20, 30, 40, 50]" :key="pageSize" :value="`${pageSize}`">
{{ pageSize }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
Page {{ table.getState().pagination.pageIndex + 1 }} of
{{ table.getPageCount() }}
</div>
<div class="flex items-center space-x-2">
<Button
variant="outline"
class="hidden w-8 h-8 p-0 lg:flex"
:disabled="!table.getCanPreviousPage()"
@click="table.setPageIndex(0)"
>
<span class="sr-only">Go to first page</span>
<DoubleArrowLeftIcon class="w-4 h-4" />
</Button>
<Button
variant="outline"
class="w-8 h-8 p-0"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
<span class="sr-only">Go to previous page</span>
<ChevronLeftIcon class="w-4 h-4" />
</Button>
<Button
variant="outline"
class="w-8 h-8 p-0"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
<span class="sr-only">Go to next page</span>
<ChevronRightIcon class="w-4 h-4" />
</Button>
<Button
variant="outline"
class="hidden w-8 h-8 p-0 lg:flex"
:disabled="!table.getCanNextPage()"
@click="table.setPageIndex(table.getPageCount() - 1)"
>
<span class="sr-only">Go to last page</span>
<DoubleArrowRightIcon class="w-4 h-4" />
</Button>
</div>
</div>
</div>
</template><DataTablePagination :table="table" />列切换
一个切换列可见性的组件。
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import { type Task } from '../data/schema'
import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface DataTableViewOptionsProps {
table: Table<Task>
}
const props = defineProps<DataTableViewOptionsProps>()
const columns = computed(() => props.table.getAllColumns()
.filter(
column =>
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
))
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="outline"
size="sm"
class="hidden h-8 ml-auto lg:flex"
>
<MixerHorizontalIcon class="w-4 h-4 mr-2" />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
v-for="column in columns"
:key="column.id"
class="capitalize"
:modelValue="column.getIsVisible()"
@update:modelValue="(value) => column.toggleVisibility(!!value)"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</template><DataTableViewOptions :table="table" />