import { Component, createMemo, createSignal, Show } from "solid-js"; import { useLocation } from "@solidjs/router"; import { type FileNode, type TocNode } from "../data-loader"; import { useNavigateWithParams } from "./useNavigateWithParams"; import { useScrollContainer } from "../App"; /** * 检查当前文件路径是否在文件夹内 */ function isPathInDir(currentPath: string, dirPath: string): boolean { // 确保 dirPath 以 / 结尾,用于前缀匹配 const dirPathPrefix = dirPath.endsWith("/") ? dirPath : dirPath + "/"; return currentPath.startsWith(dirPathPrefix); } /** * 文件树节点组件 */ export const FileTreeNode: Component<{ node: FileNode; currentPath: string; pathHeadings: Record; depth: number; onClose: () => void; isHidden?: (node: FileNode) => boolean; }> = (props) => { const navigate = useNavigateWithParams(); const location = useLocation(); const isDir = !!props.node.children; const isActive = createMemo(() => props.currentPath === props.node.path); // 默认收起,除非当前文件在该文件夹内 const [isExpanded, setIsExpanded] = createSignal( isDir && isPathInDir(props.currentPath, props.node.path), ); const href = () => props.node.path + location.search; const handleClick = (e: MouseEvent) => { if (isDir) { e.preventDefault(); setIsExpanded(!isExpanded()); } else if (e.button === 0) { // Left-click: use SPA navigation e.preventDefault(); navigate(props.node.path); props.onClose(); } // Middle-click / right-click: let the browser handle the natively }; const indent = props.depth * 12; return (
{isExpanded() ? "📂" : "📁"} 📄 {props.node.name}
{props.node.children!.map((child) => ( ))}
); }; /** * 标题节点组件 */ export const HeadingNode: Component<{ node: TocNode; basePath: string; depth: number; isHidden?: (node: TocNode) => boolean; }> = (props) => { const navigate = useNavigateWithParams(); const location = useLocation(); const anchor = props.node.id || ""; const href = () => `${props.basePath}${location.search}#${anchor}`; const hasChildren = !!props.node.children; // 默认收起,除非当前锚点在该节点内 const [isExpanded, setIsExpanded] = createSignal(props.depth <= 0); const handleExpand = (e: MouseEvent) => { e.preventDefault(); if (hasChildren) { setIsExpanded(!isExpanded()); } }; const scrollContainer = useScrollContainer(); const handleClick = (e: MouseEvent) => { if (e.button === 0) { // Left-click: use SPA navigation e.preventDefault(); navigate(`${props.basePath}#${anchor}`); } // Middle-click / right-click: let the browser handle the natively // 滚动到目标元素,考虑导航栏高度偏移 requestAnimationFrame(() => { const element = document.getElementById(anchor); if (element) { const scroller = scrollContainer(); const navBarHeight = 80; const elementPosition = element.getBoundingClientRect().top; const containerTop = scroller?.getBoundingClientRect().top ?? 0; const relativePosition = elementPosition - containerTop; const currentScroll = scroller?.scrollTop ?? window.scrollY; const offsetPosition = currentScroll + relativePosition - navBarHeight; if (scroller) { scroller.scrollTo({ top: offsetPosition, behavior: "instant" }); } else { window.scrollTo({ top: offsetPosition, behavior: "instant" }); } } }); }; const indent = props.depth * 12; return (
{props.node.children!.map((child) => ( ))}
); };