feat: reactive sidebar

This commit is contained in:
hypercross 2026-02-26 15:22:40 +08:00
parent b6718927c7
commit 2ab6ed9687
3 changed files with 117 additions and 59 deletions

View File

@ -3,7 +3,7 @@ import { useLocation } from "@solidjs/router";
// 导入组件以注册自定义元素 // 导入组件以注册自定义元素
import "./components"; import "./components";
import { Article, Sidebar } from "./components"; import { Article, MobileSidebar, DesktopSidebar } from "./components";
const App: Component = () => { const App: Component = () => {
const location = useLocation(); const location = useLocation();
@ -22,15 +22,19 @@ const App: Component = () => {
return ( return (
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50">
<Sidebar {/* 桌面端固定侧边栏 */}
<DesktopSidebar />
{/* 移动端抽屉式侧边栏 */}
<MobileSidebar
isOpen={isSidebarOpen()} isOpen={isSidebarOpen()}
onClose={() => setIsSidebarOpen(false)} onClose={() => setIsSidebarOpen(false)}
/> />
<header class="fixed top-0 left-0 right-0 bg-white shadow z-30"> <header class="fixed top-0 left-0 right-0 bg-white shadow z-30">
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center gap-4"> <div class="max-w-4xl mx-auto px-4 py-4 flex items-center gap-4">
{/* 仅在移动端显示菜单按钮 */}
<button <button
onClick={() => setIsSidebarOpen(true)} onClick={() => setIsSidebarOpen(true)}
class="text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100" class="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
title="目录" title="目录"
> >
@ -38,7 +42,7 @@ const App: Component = () => {
<h1 class="text-2xl font-bold text-gray-900">TTRPG Tools</h1> <h1 class="text-2xl font-bold text-gray-900">TTRPG Tools</h1>
</div> </div>
</header> </header>
<main class="max-w-4xl mx-auto px-4 py-8 pt-20"> <main class="max-w-4xl mx-auto px-4 py-8 pt-20 md:ml-64">
<Article src={currentPath()} /> <Article src={currentPath()} />
</main> </main>
</div> </div>

View File

@ -6,49 +6,31 @@ import { FileTreeNode, HeadingNode } from "./FileTree";
interface SidebarProps { interface SidebarProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
isDesktop?: boolean;
} }
/** /**
* *
*/ */
export const Sidebar: Component<SidebarProps> = (props) => { const SidebarContent: Component<{
fileTree: FileNode[];
pathHeadings: Record<string, TocNode[]>;
currentPath: string;
onClose: () => void;
}> = (props) => {
const location = useLocation(); const location = useLocation();
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
const [pathHeadings, setPathHeadings] = createSignal<
Record<string, TocNode[]>
>({});
// 加载目录数据
onMount(async () => {
const toc = await generateToc();
setFileTree(toc.fileTree);
setPathHeadings(toc.pathHeadings);
});
// 响应式获取当前文件的标题列表 // 响应式获取当前文件的标题列表
const currentFileHeadings = createMemo(() => { const currentFileHeadings = createMemo(() => {
const pathname = location.pathname; const pathname = location.pathname;
return pathHeadings()[pathname] || pathHeadings()[`${pathname}.md`] || []; return props.pathHeadings[pathname] || props.pathHeadings[`${pathname}.md`] || [];
}); });
return ( return (
<>
{/* 遮罩层 */}
<div
class={`fixed inset-0 bg-black/50 z-40 transition-opacity duration-300 ease-in-out ${
props.isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
}`}
onClick={props.onClose}
/>
{/* 侧边栏 */}
<aside
class={`fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-50 overflow-y-auto transform transition-transform duration-300 ease-in-out ${
props.isOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<div class="p-4"> <div class="p-4">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-bold text-gray-900"></h2> <h2 class="text-lg font-bold text-gray-900"></h2>
<Show when={!props.isDesktop}>
<button <button
onClick={props.onClose} onClick={props.onClose}
class="text-gray-500 hover:text-gray-700" class="text-gray-500 hover:text-gray-700"
@ -56,6 +38,7 @@ export const Sidebar: Component<SidebarProps> = (props) => {
> >
</button> </button>
</Show>
</div> </div>
{/* 文件树 */} {/* 文件树 */}
@ -63,11 +46,11 @@ export const Sidebar: Component<SidebarProps> = (props) => {
<h3 class="text-xs font-semibold text-gray-500 uppercase mb-2 px-2"> <h3 class="text-xs font-semibold text-gray-500 uppercase mb-2 px-2">
</h3> </h3>
{fileTree().map((node) => ( {props.fileTree.map((node) => (
<FileTreeNode <FileTreeNode
node={node} node={node}
currentPath={location.pathname} currentPath={props.currentPath}
pathHeadings={pathHeadings()} pathHeadings={props.pathHeadings}
depth={0} depth={0}
onClose={props.onClose} onClose={props.onClose}
/> />
@ -90,7 +73,78 @@ export const Sidebar: Component<SidebarProps> = (props) => {
</div> </div>
</Show> </Show>
</div> </div>
);
};
/**
*
*/
export const MobileSidebar: Component<SidebarProps> = (props) => {
const location = useLocation();
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
const [pathHeadings, setPathHeadings] = createSignal<
Record<string, TocNode[]>
>({});
// 加载目录数据
onMount(async () => {
const toc = await generateToc();
setFileTree(toc.fileTree);
setPathHeadings(toc.pathHeadings);
});
return (
<>
{/* 遮罩层 */}
<div
class={`fixed inset-0 bg-black/50 z-40 transition-opacity duration-300 ease-in-out md:hidden ${
props.isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
}`}
onClick={props.onClose}
/>
{/* 侧边栏 */}
<aside
class={`fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-50 overflow-y-auto transform transition-transform duration-300 ease-in-out md:hidden ${
props.isOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<SidebarContent
fileTree={fileTree()}
pathHeadings={pathHeadings()}
currentPath={location.pathname}
onClose={props.onClose}
/>
</aside> </aside>
</> </>
); );
}; };
/**
*
*/
export const DesktopSidebar: Component = () => {
const location = useLocation();
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
const [pathHeadings, setPathHeadings] = createSignal<
Record<string, TocNode[]>
>({});
// 加载目录数据
onMount(async () => {
const toc = await generateToc();
setFileTree(toc.fileTree);
setPathHeadings(toc.pathHeadings);
});
return (
<aside class="hidden md:block fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-30 overflow-y-auto pt-16">
<SidebarContent
fileTree={fileTree()}
pathHeadings={pathHeadings()}
currentPath={location.pathname}
onClose={() => {}}
isDesktop={true}
/>
</aside>
);
};

View File

@ -6,7 +6,7 @@ import './md-link';
// 导出组件 // 导出组件
export { Article } from './Article'; export { Article } from './Article';
export type { ArticleProps } from './Article'; export type { ArticleProps } from './Article';
export { Sidebar } from './Sidebar'; export { MobileSidebar, DesktopSidebar } from './Sidebar';
export type { SidebarProps } from './Sidebar'; export type { SidebarProps } from './Sidebar';
export { FileTreeNode, HeadingNode } from './FileTree'; export { FileTreeNode, HeadingNode } from './FileTree';