feat: reactive sidebar
This commit is contained in:
parent
b6718927c7
commit
2ab6ed9687
12
src/App.tsx
12
src/App.tsx
|
|
@ -3,7 +3,7 @@ import { useLocation } from "@solidjs/router";
|
|||
|
||||
// 导入组件以注册自定义元素
|
||||
import "./components";
|
||||
import { Article, Sidebar } from "./components";
|
||||
import { Article, MobileSidebar, DesktopSidebar } from "./components";
|
||||
|
||||
const App: Component = () => {
|
||||
const location = useLocation();
|
||||
|
|
@ -22,15 +22,19 @@ const App: Component = () => {
|
|||
|
||||
return (
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<Sidebar
|
||||
{/* 桌面端固定侧边栏 */}
|
||||
<DesktopSidebar />
|
||||
{/* 移动端抽屉式侧边栏 */}
|
||||
<MobileSidebar
|
||||
isOpen={isSidebarOpen()}
|
||||
onClose={() => setIsSidebarOpen(false)}
|
||||
/>
|
||||
<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">
|
||||
{/* 仅在移动端显示菜单按钮 */}
|
||||
<button
|
||||
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="目录"
|
||||
>
|
||||
☰
|
||||
|
|
@ -38,7 +42,7 @@ const App: Component = () => {
|
|||
<h1 class="text-2xl font-bold text-gray-900">TTRPG Tools</h1>
|
||||
</div>
|
||||
</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()} />
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,49 +6,31 @@ import { FileTreeNode, HeadingNode } from "./FileTree";
|
|||
interface SidebarProps {
|
||||
isOpen: boolean;
|
||||
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 [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 pathname = location.pathname;
|
||||
return pathHeadings()[pathname] || pathHeadings()[`${pathname}.md`] || [];
|
||||
return props.pathHeadings[pathname] || props.pathHeadings[`${pathname}.md`] || [];
|
||||
});
|
||||
|
||||
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="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-bold text-gray-900">目录</h2>
|
||||
<Show when={!props.isDesktop}>
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
class="text-gray-500 hover:text-gray-700"
|
||||
|
|
@ -56,6 +38,7 @@ export const Sidebar: Component<SidebarProps> = (props) => {
|
|||
>
|
||||
✕
|
||||
</button>
|
||||
</Show>
|
||||
</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>
|
||||
{fileTree().map((node) => (
|
||||
{props.fileTree.map((node) => (
|
||||
<FileTreeNode
|
||||
node={node}
|
||||
currentPath={location.pathname}
|
||||
pathHeadings={pathHeadings()}
|
||||
currentPath={props.currentPath}
|
||||
pathHeadings={props.pathHeadings}
|
||||
depth={0}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
|
|
@ -90,7 +73,78 @@ export const Sidebar: Component<SidebarProps> = (props) => {
|
|||
</div>
|
||||
</Show>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 桌面端固定侧边栏
|
||||
*/
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import './md-link';
|
|||
// 导出组件
|
||||
export { Article } from './Article';
|
||||
export type { ArticleProps } from './Article';
|
||||
export { Sidebar } from './Sidebar';
|
||||
export { MobileSidebar, DesktopSidebar } from './Sidebar';
|
||||
export type { SidebarProps } from './Sidebar';
|
||||
export { FileTreeNode, HeadingNode } from './FileTree';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue