PD01▸📊 Data Display · 資料展示▸表格
Data Table
資料表
難度 ★★★★★標籤 table · sort · select4 min read
模式簡述
Data Table:列出大量資料。要支援排序、選取、空狀態、載入。後台必備。
| Status | Users | ||
|---|---|---|---|
| Frontend Lab | active | 128 | |
| Singa Web | active | 42 | |
| OpenClaw | paused | 9 | |
| Lab Site | active | 256 | |
| MJ Site | draft | 0 |
Claude Code Prompt
參考:PD01 table 組件:DataTable.tsx Data Table:列出大量資料。要支援排序、選取、空狀態、載入。後台必備。 限制: - 沒做空狀態 - 載入狀態瞬間切換 - 全選 checkbox 沒做 indeterminate
完整程式碼
"use client";
import { useEffect, useState } from "react";
import { ArrowUpDown, Inbox } from "lucide-react";
import { cn } from "@/lib/utils";
type State = "normal" | "sorted" | "selected" | "empty" | "loading";
const ROWS = [
{ name: "Frontend Lab", status: "active", users: 128 },
{ name: "Singa Web", status: "active", users: 42 },
{ name: "OpenClaw", status: "paused", users: 9 },
{ name: "Lab Site", status: "active", users: 256 },
{ name: "MJ Site", status: "draft", users: 0 },
];
export function DataTable({ state = "normal" }: { state?: State }) {
const [selected, setSelected] = useState<number[]>([]);
useEffect(() => {
if (state === "selected") setSelected([0, 2]);
else setSelected([]);
}, [state]);
const data = state === "sorted" ? [...ROWS].sort((a, b) => a.name.localeCompare(b.name)) : ROWS;
if (state === "empty") {
return (
<div className="grid w-full max-w-lg place-items-center rounded-lg border border-dashed border-border/60 py-12">
<Inbox className="mb-2 h-6 w-6 text-muted-foreground/60" />
<p className="font-mono text-xs text-muted-foreground">沒有資料</p>
</div>
);
}
return (
<div className="w-full max-w-lg overflow-hidden rounded-lg border border-border bg-background/30">
{selected.length > 0 && (
<div className="flex items-center justify-between border-b border-border bg-lab-accent/10 px-3 py-2 text-xs text-lab-accent">
<span>已選 {selected.length} 列</span>
<button className="font-mono hover:underline">批次刪除</button>
</div>
)}
<table className="w-full text-xs">
<thead className="border-b border-border bg-background/40">
<tr>
<th className="w-8 px-3 py-2"></th>
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
<button className="inline-flex items-center gap-1 hover:text-foreground">
Name <ArrowUpDown className="h-3 w-3" />
</button>
</th>
<th className="px-3 py-2 text-left font-medium text-muted-foreground">Status</th>
<th className="px-3 py-2 text-right font-medium text-muted-foreground">Users</th>
</tr>
</thead>
<tbody>
{state === "loading"
? [0, 1, 2, 3, 4].map((i) => (
<tr key={i} className="border-t border-border">
<td className="px-3 py-2"><div className="h-3 w-3 animate-pulse rounded bg-muted" /></td>
<td className="px-3 py-2"><div className="h-3 w-24 animate-pulse rounded bg-muted" /></td>
<td className="px-3 py-2"><div className="h-3 w-16 animate-pulse rounded bg-muted" /></td>
<td className="px-3 py-2 text-right"><div className="ml-auto h-3 w-10 animate-pulse rounded bg-muted" /></td>
</tr>
))
: data.map((row, i) => (
<tr
key={i}
className={cn(
"border-t border-border",
selected.includes(i) && "bg-lab-accent/5",
)}
>
<td className="px-3 py-2">
<input
type="checkbox"
checked={selected.includes(i)}
onChange={() =>
setSelected((p) =>
p.includes(i) ? p.filter((x) => x !== i) : [...p, i],
)
}
className="h-3 w-3 accent-lab-accent"
/>
</td>
<td className="px-3 py-2 text-foreground">{row.name}</td>
<td className="px-3 py-2">
<span
className={cn(
"rounded-full px-1.5 py-0.5 text-[10px]",
row.status === "active" && "bg-emerald-500/10 text-emerald-500",
row.status === "paused" && "bg-yellow-500/10 text-yellow-500",
row.status === "draft" && "bg-muted text-muted-foreground",
)}
>
{row.status}
</span>
</td>
<td className="px-3 py-2 text-right font-mono tabular-nums">{row.users}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}何時用
- ✅ 後台管理大量結構化資料
- ✅ 需要對列做批次操作(select multiple)
- ✅ 需要排序、篩選的資料展示
何時別用
- ❌ 手機優先(表格在小螢幕完全廢,要改 card list)
- ❌ 資料量極大(用虛擬捲動或 server pagination)
- ❌ 純呈現性內容(用 list 或 grid)
我踩過的坑
-
沒做空狀態:第一次打開沒資料是一片白。要明確說「還沒有資料」+ CTA。
-
載入狀態瞬間切換:fetch 完直接替換內容會閃。用 skeleton 過渡 200ms+。
-
全選 checkbox 沒做 indeterminate:部分選中時應該 - 而不是空白或勾。
相關模式
- PD06
- PA07