关于 table-builder
table-builder 是用于构建数据表格的设计技能,涵盖排序、筛选、分页和响应式布局,并指导开发者配置列、行操作及加载或空数据状态。
主要功能
- 01类型化列配置
- 02客户端或服务端渲染
- 03排序、筛选、搜索与分页
- 04行编辑、删除和查看操作
- 05空数据与加载状态
- 06响应式布局和无障碍支持
适用场景
- 01构建可搜索的用户数据表
- 02为大型数据集采用服务端分页
- 03为表格行添加编辑、删除或查看操作
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill table-builder安装后在 Claude Code 中输入/table-builder确认已加载。
详细介绍
表格构建器
生成可投入生产环境的数据表格,支持排序、筛选和分页。
核心工作流
- 定义列:配置列及其类型
- 选择模式:服务端或客户端渲染
- 添加功能:排序、筛选、分页、搜索
- 行操作:编辑、删除、查看操作
- 空状态:无数据和错误视图
- 加载状态:骨架屏和 suspense
- 移动端适配:堆叠列或水平滚动
列配置
import { ColumnDef } from "@tanstack/react-table";
export const columns: ColumnDef<User>[] = [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => (
<span className="font-mono text-sm">{row.original.id}</span>
),
},
{
accessorKey: "name",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="font-medium">{row.getValue("name")}</div>
),
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string;
return (
<Badge variant={status === "active" ? "success" : "secondary"}>
{status}
</Badge>
);
},
},
{
id: "actions",
cell: ({ row }) => <RowActions row={row} />,
},
];
React 表格实现
"use client";
import {
useReactTable,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
flexRender,
} from "@tanstack/react-table";
export function DataTable<TData, TValue>({
columns,
data,
}: {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
state: { sorting, columnFilters, pagination },
});
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Input
placeholder="Search..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(e) =>
table.getColumn("name")?.setFilterValue(e.target.value)
}
className="max-w-sm"
/>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
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>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<DataTablePagination table={table} />
</div>
);
}
服务端模式
// app/users/page.tsx
import { DataTable } from "@/components/ui/data-table";
async function getUsers(params: {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: "asc" | "desc";
search?: string;
}) {
const response = await fetch(`/api/users?${new URLSearchParams(params)}`);
return response.json();
}
export default async function UsersPage({
searchParams,
}: {
searchParams: { page?: string; search?: string };
}) {
const page = Number(searchParams.page) || 1;
const search = searchParams.search || "";
const { data, totalPages } = await getUsers({ page, pageSize: 10, search });
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<DataTable columns={columns} data={data} totalPages={totalPages} />
</div>
);
}
分页组件
export function DataTablePagination({ table }: { table: Table<any> }) {
return (
<div className="flex items-center justify-between px-2">
<div className="text-sm text-gray-700">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected
</div>
<div className="flex items-center space-x-6">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => table.setPageSize(Number(value))}
>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
</Select>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}
行操作
function RowActions({ row }: { row: Row<User> }) {
const user = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(user.id)}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push(`/users/${user.id}`)}>
View Details
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(user)}>
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => handleDelete(user)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
空状态
export function EmptyState() {
return (
<div className="flex flex-col items-center justify-center py-12">
<div className="rounded-full bg-gray-100 p-6">
<InboxIcon className="h-12 w-12 text-gray-400" />
</div>
<h3 className="mt-4 text-lg font-semibold">No data found</h3>
<p className="mt-2 text-sm text-gray-600">
Get started by creating a new record.
</p>
<Button className="mt-4">Create New</Button>
</div>
);
}
加载骨架屏
export function TableSkeleton() {
return (
<div className="space-y-4">
<Skeleton className="h-10 w-64" />
<div className="rounded-md border">
{Array.from({ length: 10 }).map((_, i) => (
<div key={i} className="flex gap-4 border-b p-4">
<Skeleton className="h-6 w-full" />
</div>
))}
</div>
</div>
);
}
最佳实践
- 复杂功能使用 TanStack Table
- 大型数据集使用服务端分页
- 对搜索输入进行防抖处理
- 在 URL 参数中持久化排序和筛选状态
- 移动端:水平滚动或卡片视图
- 无障碍访问:键盘导航、ARIA
输出检查清单
- 包含类型的列定义
- 排序功能
- 筛选/搜索
- 分页控件
- 行操作菜单
- 空状态组件
- 加载骨架屏
- 适配移动端
- URL 状态持久化
- 无障碍属性
评论
支持 Markdown