8.1 关键概念
- Table(静态表格)
- shadcn 的 Table 只是一套带样式的语义标签(
Table/TableHeader/TableBody/TableRow/TableCell),无逻辑,适合展示固定数据。 - TanStack Table (v8)
- Headless 表格逻辑库,管理列定义、排序、筛选、分页、行选择等"状态与算法",不渲染 UI——UI 由你用 shadcn Table 画。
- ColumnDef(列定义)
- 描述每一列:取哪个字段(
accessorKey)、表头怎么渲染(header)、单元格怎么渲染(cell)。 - useReactTable
- 把数据 + 列定义 + 各种"row model"(排序/筛选/分页)组装成
table实例,供 UI 遍历渲染。 - flexRender
- TanStack 的渲染助手,能渲染 header/cell 里既可能是字符串也可能是 JSX 的定义。
- Badge / Avatar
- Badge 是状态标签(成功/警告等,含 variant);Avatar 是头像(图片 + 加载失败的文字回退 AvatarFallback)。
- Skeleton
- 加载占位骨架屏,数据未到时显示灰块动画,避免布局跳动与空白焦虑。
- Toast(sonner)
- 轻量通知条。shadcn 现推荐
sonner:放一个<Toaster />,任意处调用toast("已保存")即弹出。
8.2 安装
$ npx shadcn@latest add table badge avatar skeleton sonner
$ npm i @tanstack/react-table
8.3 静态 Table
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
<Table>
<TableHeader>
<TableRow>
<TableHead>发票</TableHead><TableHead>状态</TableHead>
<TableHead className="text-right">金额</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell><Badge>已付</Badge></TableCell>
<TableCell className="text-right">¥250.00</TableCell>
</TableRow>
</TableBody>
</Table>
8.4 定义列(ColumnDef)
// columns.tsx
"use client";
import { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
export type Payment = {
id: string; amount: number;
status: "pending" | "success" | "failed"; email: string;
};
export const columns: ColumnDef<Payment>[] = [
{ accessorKey: "status", header: "状态",
cell: ({ row }) => <Badge>{row.getValue("status")}</Badge> },
{ accessorKey: "email", header: "邮箱" },
{ accessorKey: "amount", header: () => <div className="text-right">金额</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));
return <div className="text-right">¥{amount.toFixed(2)}</div>;
} },
];
8.5 DataTable:接入 TanStack
// data-table.tsx
"use client";
import {
useReactTable, getCoreRowModel, getSortedRowModel,
getFilteredRowModel, getPaginationRowModel,
flexRender, type SortingState, type ColumnFiltersState,
} from "@tanstack/react-table";
import { useState } from "react";
export function DataTable<TData, TValue>({ columns, data }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [filters, setFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({
data, columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setFilters,
state: { sorting, columnFilters: filters },
});
return (
<Table>
<TableHeader>
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}>
{hg.headers.map((h) => (
<TableHead key={h.id}>
{flexRender(h.column.columnDef.header, h.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
);
}
8.6 可排序表头
// 在 ColumnDef 里让 header 变成可点击排序按钮
{
accessorKey: "amount",
header: ({ column }) => (
<Button variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
金额 <ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
}
8.7 筛选与分页控件
// 按 email 筛选
<Input placeholder="筛选邮箱…"
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn("email")?.setFilterValue(e.target.value)}
className="max-w-sm" />
// 分页按钮
<div className="flex justify-end gap-2 py-4">
<Button variant="outline" size="sm"
onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>上一页</Button>
<Button variant="outline" size="sm"
onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>下一页</Button>
</div>
DataTable 是"代码配方"
shadcn 官网的 DataTable 文档不是一个组件,而是一段可复制的组合示例。你把它抄进项目后自由拆分、加行选择/列显隐/服务端分页——因为代码归你所有。
8.8 Badge / Avatar / Skeleton
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Skeleton } from "@/components/ui/skeleton";
<Badge>默认</Badge>
<Badge variant="secondary">次要</Badge>
<Badge variant="destructive">错误</Badge>
<Badge variant="outline">描边</Badge>
<Avatar>
<AvatarImage src="/u.png" alt="@user" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
// 加载态骨架
<div className="flex items-center gap-4">
<Skeleton className="h-10 w-10 rounded-full" />
<Skeleton className="h-4 w-[200px]" />
</div>
8.9 Toast(sonner)
// app/layout.tsx —— 放一次 Toaster
import { Toaster } from "@/components/ui/sonner";
<body>{children}<Toaster /></body>
// 任意组件里调用
import { toast } from "sonner";
<Button onClick={() =>
toast("发票已创建", {
description: "2026-07-07 周一",
action: { label: "撤销", onClick: () => {} },
})
}>创建</Button>
// 语义变体
toast.success("保存成功");
toast.error("提交失败");
8.10 小结与下一步
- Table 只管样式;TanStack Table 管排序/筛选/分页/选择的 headless 逻辑,二者用
flexRender缝合成 DataTable。 - ColumnDef 用
accessorKey/header/cell描述列,cell里可渲染 Badge、按钮等任意 JSX。 - Skeleton 撑加载态,Toast 用 sonner——放一个 Toaster,处处
toast()。 - 下一章进入进阶定制:Command 命令面板、Combobox、DatePicker、Chart 与自建 Registry。