Table
真正的 table:36 高的弱色表头、至少 48 高的行,行与行之间是缝线;选中的行是粉色淡底,数字用圆体右对齐。
字幕组
本月发布最多的四个组
| 字幕组 | 语言 | 资源 | 本月 | 状态 |
|---|---|---|---|---|
喵萌奶茶屋@nekomoe | 简中·繁中 | 1,284 | 42 | 已同步 |
绿茶字幕组@greentea | 简中 | 612 | 18 | 同步中 |
星空字幕组@xksub | 简中·繁中 | 2,093 | 57 | 已同步 |
北宇治字幕组@kitauji | 简中·日 | 88 | 0 | 30 天未更新 |
table-demo.tsx
import { Badge, StatusBadge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { EntityAvatar } from "@/components/ui/entity-avatar"
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableIdentity,
TableRow,
} from "@/components/ui/table"
const GROUPS = [
{ name: "喵萌奶茶屋", slug: "nekomoe", logo: "/demo/logos/paw.svg", langs: "简中·繁中", releases: 1284, month: 42, state: "ok" as const },
{ name: "绿茶字幕组", slug: "greentea", logo: "/demo/logos/leaf.svg", langs: "简中", releases: 612, month: 18, state: "run" as const },
{ name: "星空字幕组", slug: "xksub", logo: "/demo/logos/star.svg", langs: "简中·繁中", releases: 2093, month: 57, state: "ok" as const, selected: true },
{ name: "北宇治字幕组", slug: "kitauji", logo: null, langs: "简中·日", releases: 88, month: 0, state: "warn" as const },
]
const STATE = {
ok: { tone: "ok" as const, word: "已同步" },
run: { tone: "run" as const, word: "同步中" },
warn: { tone: "warn" as const, word: "30 天未更新" },
}
export default function TableDemo() {
return (
<Card className="w-full max-w-2xl overflow-hidden pb-0">
<CardHeader stitched>
<CardTitle>字幕组</CardTitle>
<CardDescription>本月发布最多的四个组</CardDescription>
</CardHeader>
<CardContent className="px-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>字幕组</TableHead>
<TableHead>语言</TableHead>
<TableHead numeric>资源</TableHead>
<TableHead numeric>本月</TableHead>
<TableHead>状态</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{GROUPS.map((group) => (
<TableRow key={group.slug} data-state={group.selected ? "selected" : undefined}>
<TableCell>
<TableIdentity
media={<EntityAvatar name={group.name} src={group.logo} shape="rounded" />}
title={group.name}
meta={`@${group.slug}`}
/>
</TableCell>
<TableCell>
<Badge variant="lang" face="text">
{group.langs}
</Badge>
</TableCell>
<TableCell numeric>{group.releases.toLocaleString("en-US")}</TableCell>
<TableCell numeric>{group.month}</TableCell>
<TableCell>
<StatusBadge tone={STATE[group.state].tone}>{STATE[group.state].word}</StatusBadge>
</TableCell>
</TableRow>
))}
</TableBody>
<TableCaption className="mb-3">数字为 2026 年 10 月的统计</TableCaption>
</Table>
</CardContent>
</Card>
)
}安装
npx shadcn@latest add @anibt/table第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。
手动安装
- 1. 先装主题组件只读
@anibt/theme的 token 和配方,没有主题就没有样子。npx shadcn@latest add @anibt/theme - 2. 把下面的文件放进项目components/ui/table.tsx
import * as React from "react" import { cn } from "@/lib/utils" /** * Table — the AniBT 「贴纸手账」 data table, on a real `<table>`. * * It draws no box of its own: a table sits straight on the panel it is placed * in (put it in a `Card`, with the card's padding off). The head row is 36px * of 12/600 muted labels; body rows are at least 48px, parted by the dashed * stitch; the pointer presses a row to the sunk tone and a selected row * (`data-state="selected"`) takes the pink wash. Numbers set `numeric` on the * head and the cell: right-aligned, in the round face, tabular. * * `TableHeader sticky` pins the head to the top of a height-capped container * (`containerClassName="max-h-80"`); the pinned cells repeat the card's paper * so rows scrolling under them do not show through. * * `TableIdentity` is the first cell of an entity row: an optional picture, the * name on one line and a muted line under it, always 32px tall. * * The parts and props are shadcn's, plus `containerClassName`, `sticky`, * `numeric` and `TableIdentity`. */ function Table({ className, containerClassName, ...props }: React.ComponentProps<"table"> & { containerClassName?: string }) { return ( <div data-slot="table-container" className={cn("relative w-full overflow-x-auto", containerClassName)}> <table data-slot="table" className={cn("w-full caption-bottom border-separate border-spacing-0 text-[13px]", className)} {...props} /> </div> ) } function TableHeader({ className, sticky = false, ...props }: React.ComponentProps<"thead"> & { sticky?: boolean }) { return ( <thead data-slot="table-header" data-sticky={sticky ? "true" : undefined} className={cn( "[&_th]:bg-(image:--anibt-stitch) [&_th]:bg-size-[8px_1px] [&_th]:bg-bottom [&_th]:bg-repeat-x [&_tr]:h-auto [&_tr]:bg-none [&_tr]:hover:bg-transparent", sticky && "[&_th]:sticky [&_th]:top-0 [&_th]:z-10 [&_th]:bg-card", className )} {...props} /> ) } function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { return <tbody data-slot="table-body" className={cn("[&_tr:last-child]:bg-none", className)} {...props} /> } function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { return ( <tfoot data-slot="table-footer" className={cn( "font-semibold text-foreground [&_td]:bg-muted [&_td]:bg-(image:--anibt-stitch) [&_td]:bg-size-[8px_1px] [&_td]:bg-top [&_td]:bg-repeat-x [&_tr]:bg-none [&_tr]:hover:bg-transparent", className )} {...props} /> ) } function TableRow({ className, ...props }: React.ComponentProps<"tr">) { return ( <tr data-slot="table-row" className={cn( "h-12 bg-(image:--anibt-stitch) bg-size-[8px_1px] bg-bottom bg-repeat-x transition-colors duration-150 hover:bg-muted data-[state=selected]:bg-accent", className )} {...props} /> ) } const NUMERIC = "data-[numeric=true]:text-right data-[numeric=true]:anibt-num" function TableHead({ className, numeric, ...props }: React.ComponentProps<"th"> & { numeric?: boolean }) { return ( <th data-slot="table-head" data-numeric={numeric ? "true" : undefined} className={cn( "h-9 px-3 text-left align-middle text-[12px] leading-4 font-semibold whitespace-nowrap text-muted-foreground first:pl-4 last:pr-4 [&:has([role=checkbox])]:pr-0", NUMERIC, className )} {...props} /> ) } function TableCell({ className, numeric, ...props }: React.ComponentProps<"td"> & { numeric?: boolean }) { return ( <td data-slot="table-cell" data-numeric={numeric ? "true" : undefined} className={cn( "px-3 py-2 align-middle text-[13px] leading-5 text-anibt-ink2 first:pl-4 last:pr-4 [&:has([role=checkbox])]:pr-0", NUMERIC, className )} {...props} /> ) } function TableCaption({ className, ...props }: React.ComponentProps<"caption">) { return ( <caption data-slot="table-caption" className={cn("mt-3 text-[12px] text-muted-foreground", className)} {...props} /> ) } /** Picture, name and a muted line: the first cell of an entity row. */ function TableIdentity({ className, media, title, meta, ...props }: Omit<React.ComponentProps<"div">, "title"> & { media?: React.ReactNode title: React.ReactNode meta?: React.ReactNode }) { return ( <div data-slot="table-identity" className={cn("flex min-w-0 items-center gap-2.5", className)} {...props}> {media} <div className="flex h-8 min-w-0 flex-col justify-center"> <span className="anibt-ell text-[14px] leading-4 font-semibold text-foreground">{title}</span> {meta ? <span className="anibt-ell mt-0.5 text-[12px] leading-4 text-muted-foreground">{meta}</span> : null} </div> </div> ) } export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, TableIdentity }
用法
用法
import { Card, CardContent } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
<Card className="overflow-hidden py-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>字幕组</TableHead>
<TableHead numeric>资源</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>喵萌奶茶屋</TableCell>
<TableCell numeric>1,284</TableCell>
</TableRow>
</TableBody>
</Table>
</Card>表格自己不画框:它直接坐在所在的面板上,放进 Card 并去掉卡片的内边距即可。表头是 12/600 的弱色字,下面一条缝线;行至少 48 高,行间缝线,指针下压成下沉色,data-state="selected" 的行是粉色淡底。首列和末列多留 4px,和卡片边对齐。
数字列在表头和单元格上都写 numeric:右对齐、圆体、等宽数字,竖着扫一列不会跳。
TableIdentity 是实体行的第一格:可选的图片、一行名字、一行弱色说明,始终 32 高,没有说明的行也不会变矮。
吸顶表头
| 集 | 标题 | 大小 | 做种 |
|---|---|---|---|
| 01 | 出发 | 380 MB | 140 |
| 02 | 末班车之后 | 417 MB | 129 |
| 03 | 樱花的约定 | 454 MB | 118 |
| 04 | 看不见的站台 | 491 MB | 107 |
| 05 | 夜色的乘客 | 408 MB | 96 |
| 06 | 终点站 | 445 MB | 85 |
| 07 | 回程 | 482 MB | 74 |
| 08 | 雨中 | 399 MB | 63 |
| 09 | 第二次相遇 | 436 MB | 52 |
| 10 | 沉默的车厢 | 473 MB | 41 |
| 11 | 黎明 | 390 MB | 30 |
| 12 | 夜樱 | 427 MB | 19 |
| 合计 12 集 | 5.4 GB | — | |
table-sticky.tsx
import { Card } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table"
const EPISODES = Array.from({ length: 12 }, (_, index) => ({
episode: index + 1,
title: ["出发", "末班车之后", "樱花的约定", "看不见的站台", "夜色的乘客", "终点站", "回程", "雨中", "第二次相遇", "沉默的车厢", "黎明", "夜樱"][index],
size: 380 + ((index * 37) % 120),
seeders: Math.max(3, 140 - index * 11),
}))
export default function TableSticky() {
return (
<Card className="w-full max-w-xl overflow-hidden py-0">
<Table containerClassName="max-h-72">
<TableHeader sticky>
<TableRow>
<TableHead numeric className="w-14">
集
</TableHead>
<TableHead>标题</TableHead>
<TableHead numeric>大小</TableHead>
<TableHead numeric>做种</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{EPISODES.map((row) => (
<TableRow key={row.episode}>
<TableCell numeric>{String(row.episode).padStart(2, "0")}</TableCell>
<TableCell className="text-foreground">{row.title}</TableCell>
<TableCell numeric>{row.size} MB</TableCell>
<TableCell numeric className="text-anibt-ok">
{row.seeders}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>合计 12 集</TableCell>
<TableCell numeric>5.4 GB</TableCell>
<TableCell numeric>—</TableCell>
</TableRow>
</TableFooter>
</Table>
</Card>
)
}给表格容器一个最大高度(containerClassName="max-h-72"),TableHeader sticky 就会钉在顶上;钉住的表头用卡片的纸色,下面滚过的行不会透出来。TableFooter 是合计行:下沉色、上缘一条缝线。
属性
Table
containerClassNamestring默认 —滚动容器的 class:限高、圆角
TableHeader
stickyboolean默认 false钉在滚动容器的顶上
TableHead · TableCell
numericboolean默认 false右对齐、圆体、等宽数字
TableIdentity
titleReactNode默认 —必填:一行名字
metaReactNode默认 —名字下的弱色说明
mediaReactNode默认 —左边的图片,常用 EntityAvatar
其余部分与 shadcn 同名,属性透传给对应的表格元素。
