37 lines
3.1 KiB
TypeScript
37 lines
3.1 KiB
TypeScript
/**
|
|
* 侧栏画布树显示组件,使用业务目录库派生结构/搜索路径,只保存哪些目录项被用户折叠。
|
|
* 不重写board父子归属;活动路径和搜索可临时展开祖先,路由Link负责进入真实画布页面。
|
|
*/
|
|
"use client";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { ChevronRight, Network } from "lucide-react";
|
|
import { activeBoardAncestors, boardDirectory, filterBoardDirectory, type BoardBranch } from "@/lib/board-directory";
|
|
import { boardHref, matchesRef, type Board } from "@/lib/workspace";
|
|
import styles from "./workspace.module.css";
|
|
|
|
/** branches只随boards/query重算,closed是当前挂载视图局部状态,宿主切工作区可用key重建。 */
|
|
export function BoardDirectory({ boards, workspace, active, query, demo }: { boards: Board[]; workspace: string; active?: string; query: string; demo: boolean }) {
|
|
const [closed, setClosed] = useState(new Set<string>());
|
|
// memo 按 boards 引用重算;宿主更新条目须提供新数组,不能只原地改标题/父链再期待重新派生。
|
|
const branches = useMemo(() => filterBoardDirectory(boardDirectory(boards), query), [boards, query]);
|
|
/** 活动画布或目录刷新时展开其祖先路径,防当前页藏在折叠树下。 */
|
|
useEffect(() => {
|
|
const path = activeBoardAncestors(boards, active);
|
|
setClosed((previous) => { const next = new Set(previous); for (const id of path) next.delete(id); return next; });
|
|
}, [active, boards]);
|
|
/** 递归渲染树,视觉缩进最多8层但不裁掉更深子项;搜索时强制展开匹配路径而不清空用户closed记录。 */
|
|
function render(items: BoardBranch[], depth = 0) {
|
|
return <ul className={styles.directoryList}>{items.map(({ board, children }) => {
|
|
// 搜索中的切换只修改 closed 记录,当前仍强制展开;清掉搜索后才看到用户折叠记录的效果。
|
|
const expanded = !!query.trim() || !closed.has(board.id);
|
|
return <li key={board.id}><div className={styles.directoryRow} style={{ paddingLeft: Math.min(depth, 8) * 16 }}>
|
|
{children.length ? <button type="button" className={styles.directoryToggle} aria-label={`${expanded ? "收起" : "展开"}${board.title}的子画布`} aria-expanded={expanded} onClick={() => setClosed((previous) => { const next = new Set(previous); if (expanded) next.add(board.id); else next.delete(board.id); return next; })}><ChevronRight size={13} style={{ transform: expanded ? "rotate(90deg)" : undefined }} /></button> : <span className={styles.directorySpacer} />}
|
|
<Link className={styles.pageLink} aria-current={matchesRef(board, active) ? "page" : undefined} href={boardHref(workspace, board.slug || board.id, demo)}><Network size={15} strokeWidth={1.6} aria-hidden="true" /><span>{board.title}</span></Link>
|
|
</div>{expanded && children.length > 0 && render(children, depth + 1)}</li>;
|
|
})}</ul>;
|
|
}
|
|
// 有结果显示树;仅搜索无命中给提示,原始目录空时不伪造一条默认页面。
|
|
return branches.length ? render(branches) : query ? <p className={styles.hint}>没有找到相关页面</p> : null;
|
|
}
|