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 "./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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue