Frontend Lab
← 回到所有模式
PD02📊 Data Display · 資料展示樹狀

Tree View

樹狀檢視

難度 ★★★★★標籤 tree · 巢狀 · expand4 min read

模式簡述

Tree View:階層資料的展開/收合。檔案管理、組織結構、技術分類常用。

全收合|只顯示頂層

Claude Code Prompt

💬 Claude Code Prompt
參考:PD02 tree
組件:TreeView.tsx

Tree View:階層資料的展開/收合。檔案管理、組織結構、技術分類常用。

限制:
- 展開全部按鈕沒做
- 沒記住展開狀態
- 鍵盤導航沒做

完整程式碼

TreeView.tsx
"use client";
import { useEffect, useState } from "react";
import { ChevronRight, ChevronDown, Folder, FileText } from "lucide-react";

type State = "collapsed" | "partial" | "all-open";

type Node = { name: string; children?: Node[] };
const TREE: Node[] = [
{
  name: "src",
  children: [
    {
      name: "components",
      children: [
        { name: "Modal.tsx" },
        { name: "Tabs.tsx" },
      ],
    },
    { name: "app", children: [{ name: "page.tsx" }, { name: "layout.tsx" }] },
  ],
},
{ name: "content", children: [{ name: "patterns" }, { name: "effects" }] },
{ name: "README.md" },
];

function TreeNode({
node,
level,
expanded,
toggle,
path,
}: {
node: Node;
level: number;
expanded: Set<string>;
toggle: (p: string) => void;
path: string;
}) {
const hasChildren = node.children && node.children.length > 0;
const isOpen = expanded.has(path);

return (
  <div>
    <button
      onClick={() => hasChildren && toggle(path)}
      className="flex w-full items-center gap-1.5 rounded px-1 py-0.5 text-left text-xs hover:bg-background/60"
      style={{ paddingLeft: 4 + level * 14 }}
    >
      {hasChildren ? (
        isOpen ? (
          <ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
        ) : (
          <ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
        )
      ) : (
        <span className="w-3" />
      )}
      {hasChildren ? (
        <Folder className="h-3.5 w-3.5 shrink-0 text-lab-accent" />
      ) : (
        <FileText className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
      )}
      <span>{node.name}</span>
    </button>
    {hasChildren && isOpen && (
      <div>
        {node.children!.map((c, i) => (
          <TreeNode
            key={i}
            node={c}
            level={level + 1}
            expanded={expanded}
            toggle={toggle}
            path={`${path}/${c.name}`}
          />
        ))}
      </div>
    )}
  </div>
);
}

function collectAllPaths(nodes: Node[], parent = ""): string[] {
return nodes.flatMap((n) => {
  const p = `${parent}/${n.name}`;
  return n.children ? [p, ...collectAllPaths(n.children, p)] : [];
});
}

export function TreeView({ state = "partial" }: { state?: State }) {
const [expanded, setExpanded] = useState<Set<string>>(new Set());
useEffect(() => {
  if (state === "collapsed") setExpanded(new Set());
  else if (state === "partial") setExpanded(new Set(["/src", "/content"]));
  else setExpanded(new Set(collectAllPaths(TREE)));
}, [state]);

return (
  <div className="w-full max-w-sm rounded-lg border border-border bg-background/30 p-2">
    {TREE.map((n, i) => (
      <TreeNode
        key={i}
        node={n}
        level={0}
        expanded={expanded}
        toggle={(p) =>
          setExpanded((prev) => {
            const next = new Set(prev);
            if (next.has(p)) next.delete(p);
            else next.add(p);
            return next;
          })
        }
        path={`/${n.name}`}
      />
    ))}
  </div>
);
}

何時用

  • ✅ 檔案系統、資料夾結構
  • ✅ 組織架構、權限階層
  • ✅ 巢狀分類(書籍章節、技術領域)

何時別用

  • ❌ 扁平資料(用 list)
  • ❌ 層級超過 5 層(使用者迷路,改用麵包屑或搜尋)
  • ❌ 手機優先(樹太擠,改用 drill-down nav)

我踩過的坑

  1. 展開全部按鈕沒做:節點多時手動點到底很累。應提供「全展開/全收合」。

  2. 沒記住展開狀態:使用者切頁回來全部重置。存到 localStorage 或 URL。

  3. 鍵盤導航沒做:ArrowRight 展開、Left 收合、Up/Down 切換 focus。

相關模式

  • PC03
  • PD08

搜尋

按 ⌘K 隨時開啟