"use client"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { useRouter, usePathname } from "next/navigation"; import { useSortable, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { ChevronRight, FileText, Folder, FolderOpen, MoreHorizontal, Trash2, FilePlus, FolderPlus, Download, Upload, } from "lucide-react"; import { Page, useEditorStore } from "@/lib/store"; import { cn } from "@/lib/utils"; import { exportPageAsMarkdown, exportFolderAsZip } from "@/lib/export"; import { useImport } from "@/components/import-context"; import { useConfirm } from "@/components/confirm-provider"; interface TreeViewProps { pages: Page[]; parentId: string | null; expanded: Record; toggleExpand: (id: string) => void; level?: number; folderFileCount: Record; } function buildFolderFileCount(pages: Page[]): Record { const byParent = new Map(); for (const page of pages) { const siblings = byParent.get(page.parentId) || []; siblings.push(page); byParent.set(page.parentId, siblings); } const counts: Record = {}; const dfs = (folderId: string): number => { const children = byParent.get(folderId) || []; let total = 0; for (const child of children) { if (child.type === "file") { total += 1; } else { total += dfs(child.id); } } counts[folderId] = total; return total; }; for (const page of pages) { if (page.type === "folder") { dfs(page.id); } } return counts; } function TreeNode({ node, pages, level, expanded, toggleExpand, folderFileCount, }: { node: Page; pages: Page[]; level: number; expanded: Record; toggleExpand: (id: string) => void; folderFileCount: Record; }) { const { activePageId, setActivePageId, addPage, deletePage } = useEditorStore(); const router = useRouter(); const pathname = usePathname(); const { triggerImport } = useImport(); const confirm = useConfirm(); const isFolder = node.type === "folder"; const hasChildren = pages.some((p) => p.parentId === node.id); const isExpanded = expanded[node.id]; const isActive = activePageId === node.id; const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: node.id, data: { type: node.type, title: node.title, parentId: node.parentId }, }); const style = { paddingLeft: `${level * 12 + 6}px`, transform: CSS.Translate.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
{ setActivePageId(node.id); if (pathname !== "/") router.push("/"); }} > {isActive && }
{node.icon ? ( {node.icon} ) : isFolder ? ( isExpanded ? ( ) : ( ) ) : ( )} {node.title || "无标题"} {isFolder && ( {folderFileCount[node.id] ?? 0} )}
{ e.stopPropagation(); const targetParentId = node.type === "folder" ? node.id : node.parentId; const order = node.type === "folder" ? undefined : (node.order || 0) + 1; addPage(targetParentId, "file", undefined, order); if (node.type === "folder") toggleExpand(node.id); }} > 新建文件 { e.stopPropagation(); const targetParentId = node.type === "folder" ? node.id : node.parentId; const order = node.type === "folder" ? undefined : (node.order || 0) + 1; addPage(targetParentId, "folder", undefined, order); if (node.type === "folder") toggleExpand(node.id); }} > 新建文件夹 { e.stopPropagation(); const targetParentId = node.type === "folder" ? node.id : node.parentId; triggerImport(targetParentId); if (node.type === "folder") toggleExpand(node.id); }} > 导入文档... { e.stopPropagation(); if (node.type === "folder") { exportFolderAsZip(node.id, pages, node.title); } else { exportPageAsMarkdown(node); } }} > 导出 { e.stopPropagation(); const ok = await confirm({ title: "确认删除", description: "此操作无法撤销。", confirmText: "删除", cancelText: "取消", tone: "danger", }); if (ok) { deletePage(node.id); } }} > 删除
{(isExpanded || hasChildren) && isExpanded && ( )}
); } export function TreeView({ pages, parentId, expanded, toggleExpand, level = 0, folderFileCount }: TreeViewProps) { const computedFolderFileCount = level === 0 ? buildFolderFileCount(pages) : folderFileCount; const nodes = pages .filter((p) => p.parentId === parentId) .sort((a, b) => (a.order || 0) - (b.order || 0)); if (nodes.length === 0) return null; return ( n.id)} strategy={verticalListSortingStrategy}>
{nodes.map((node) => ( ))}
); }