Frontend Lab
← 回到所有模式
PD01📊 Data Display · 資料展示表格

Data Table

資料表

難度 ★★★★★標籤 table · sort · select4 min read

模式簡述

Data Table:列出大量資料。要支援排序、選取、空狀態、載入。後台必備。

StatusUsers
Frontend Labactive128
Singa Webactive42
OpenClawpaused9
Lab Siteactive256
MJ Sitedraft0
正常|5 列資料

Claude Code Prompt

💬 Claude Code Prompt
參考:PD01 table
組件:DataTable.tsx

Data Table:列出大量資料。要支援排序、選取、空狀態、載入。後台必備。

限制:
- 沒做空狀態
- 載入狀態瞬間切換
- 全選 checkbox 沒做 indeterminate

完整程式碼

DataTable.tsx
"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)

我踩過的坑

  1. 沒做空狀態:第一次打開沒資料是一片白。要明確說「還沒有資料」+ CTA。

  2. 載入狀態瞬間切換:fetch 完直接替換內容會閃。用 skeleton 過渡 200ms+。

  3. 全選 checkbox 沒做 indeterminate:部分選中時應該 - 而不是空白或勾。

相關模式

  • PD06
  • PA07

搜尋

按 ⌘K 隨時開啟