2026-06-30 19:20:08 +08:00
|
|
|
import { Component, createEffect, createMemo, createSignal } from "solid-js";
|
2026-02-26 14:07:58 +08:00
|
|
|
import { useLocation } from "@solidjs/router";
|
2026-07-07 18:10:43 +08:00
|
|
|
import { useJournalStream } from "./components/stores/journalStream";
|
|
|
|
|
import { addRevealedClasses } from "./components/stores/reveal";
|
2026-07-07 21:38:43 +08:00
|
|
|
import { useJournalCompletions } from "./components/journal/completions";
|
2026-02-26 00:17:23 +08:00
|
|
|
|
2026-02-26 00:47:26 +08:00
|
|
|
// 导入组件以注册自定义元素
|
2026-02-26 14:07:58 +08:00
|
|
|
import "./components";
|
2026-06-25 21:04:53 +08:00
|
|
|
import {
|
|
|
|
|
Article,
|
|
|
|
|
MobileSidebar,
|
|
|
|
|
DesktopSidebar,
|
|
|
|
|
DocDialog,
|
2026-06-30 19:20:08 +08:00
|
|
|
DataSourceDialog,
|
2026-06-25 21:04:53 +08:00
|
|
|
} from "./components";
|
2026-06-30 19:20:08 +08:00
|
|
|
import { generateToc, type FileNode, type TocNode } from "./data-loader";
|
2026-07-06 15:06:19 +08:00
|
|
|
import { JournalPanel } from "./components/journal";
|
2026-02-26 00:47:26 +08:00
|
|
|
|
2026-02-26 00:17:23 +08:00
|
|
|
const App: Component = () => {
|
|
|
|
|
const location = useLocation();
|
2026-07-06 15:38:28 +08:00
|
|
|
const stream = useJournalStream();
|
2026-07-07 21:38:43 +08:00
|
|
|
const comp = useJournalCompletions();
|
2026-02-26 14:24:48 +08:00
|
|
|
const [isSidebarOpen, setIsSidebarOpen] = createSignal(false);
|
2026-06-25 21:04:53 +08:00
|
|
|
const [isDocOpen, setIsDocOpen] = createSignal(false);
|
2026-06-30 19:20:08 +08:00
|
|
|
const [isDataSourceOpen, setIsDataSourceOpen] = createSignal(false);
|
2026-07-06 15:06:19 +08:00
|
|
|
const [isJournalOpen, setIsJournalOpen] = createSignal(false);
|
2026-06-30 19:20:08 +08:00
|
|
|
|
|
|
|
|
// Sidebar and TOC data — reload when source changes
|
|
|
|
|
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
|
|
|
|
|
const [pathHeadings, setPathHeadings] = createSignal<
|
|
|
|
|
Record<string, TocNode[]>
|
|
|
|
|
>({});
|
|
|
|
|
const [tocKey, setTocKey] = createSignal(0);
|
|
|
|
|
|
|
|
|
|
const loadToc = async () => {
|
|
|
|
|
const toc = await generateToc();
|
|
|
|
|
setFileTree(toc.fileTree);
|
|
|
|
|
setPathHeadings(toc.pathHeadings);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Load TOC on mount and when source changes
|
|
|
|
|
createEffect(() => {
|
|
|
|
|
void tocKey();
|
|
|
|
|
void loadToc();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleSourceChanged = () => {
|
|
|
|
|
setTocKey((k) => k + 1);
|
|
|
|
|
};
|
2026-02-26 00:17:23 +08:00
|
|
|
|
2026-02-26 14:51:26 +08:00
|
|
|
const currentPath = createMemo(() => {
|
2026-02-26 00:17:23 +08:00
|
|
|
// 根据路由加载对应的 markdown 文件
|
2026-02-26 14:07:58 +08:00
|
|
|
let path = decodeURIComponent(location.pathname);
|
|
|
|
|
|
|
|
|
|
if (!path) path = "/content/";
|
|
|
|
|
if (path.endsWith("/")) path += "index";
|
|
|
|
|
if (!path.endsWith(".md")) path += ".md";
|
|
|
|
|
|
2026-02-26 14:51:26 +08:00
|
|
|
return path;
|
2026-02-26 00:17:23 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div class="min-h-screen bg-gray-50">
|
2026-02-26 15:22:40 +08:00
|
|
|
{/* 桌面端固定侧边栏 */}
|
2026-07-06 15:30:21 +08:00
|
|
|
<DesktopSidebar
|
|
|
|
|
fileTree={fileTree()}
|
|
|
|
|
pathHeadings={pathHeadings()}
|
|
|
|
|
onDataSourceOpen={() => setIsDataSourceOpen(true)}
|
|
|
|
|
/>
|
2026-02-26 15:22:40 +08:00
|
|
|
{/* 移动端抽屉式侧边栏 */}
|
|
|
|
|
<MobileSidebar
|
2026-02-26 14:24:48 +08:00
|
|
|
isOpen={isSidebarOpen()}
|
|
|
|
|
onClose={() => setIsSidebarOpen(false)}
|
2026-06-30 19:20:08 +08:00
|
|
|
fileTree={fileTree()}
|
|
|
|
|
pathHeadings={pathHeadings()}
|
2026-07-06 15:30:21 +08:00
|
|
|
onDataSourceOpen={() => setIsDataSourceOpen(true)}
|
2026-02-26 14:24:48 +08:00
|
|
|
/>
|
|
|
|
|
<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">
|
2026-02-26 15:22:40 +08:00
|
|
|
{/* 仅在移动端显示菜单按钮 */}
|
2026-02-26 14:24:48 +08:00
|
|
|
<button
|
|
|
|
|
onClick={() => setIsSidebarOpen(true)}
|
2026-02-26 15:22:40 +08:00
|
|
|
class="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
|
2026-02-26 14:24:48 +08:00
|
|
|
title="目录"
|
|
|
|
|
>
|
|
|
|
|
☰
|
|
|
|
|
</button>
|
2026-02-26 00:17:23 +08:00
|
|
|
<h1 class="text-2xl font-bold text-gray-900">TTRPG Tools</h1>
|
2026-06-25 21:04:53 +08:00
|
|
|
<div class="flex-1" />
|
2026-07-06 15:06:19 +08:00
|
|
|
<button
|
|
|
|
|
onClick={() => setIsJournalOpen((v) => !v)}
|
2026-07-06 15:38:28 +08:00
|
|
|
class={`relative w-8 h-8 rounded flex items-center justify-center text-sm ${
|
2026-07-06 15:30:21 +08:00
|
|
|
isJournalOpen()
|
|
|
|
|
? "bg-blue-100 text-blue-600"
|
|
|
|
|
: "text-gray-500 hover:bg-gray-100"
|
2026-07-06 15:06:19 +08:00
|
|
|
}`}
|
2026-07-06 15:38:28 +08:00
|
|
|
title={`Journal (${stream.connectionStatus})`}
|
2026-06-30 19:20:08 +08:00
|
|
|
>
|
2026-07-06 15:38:28 +08:00
|
|
|
<span
|
|
|
|
|
class="absolute top-0.5 right-0.5 w-2 h-2 rounded-full border border-white"
|
|
|
|
|
classList={{
|
|
|
|
|
"bg-gray-400": stream.connectionStatus === "disconnected",
|
|
|
|
|
"bg-yellow-400 animate-pulse":
|
|
|
|
|
stream.connectionStatus === "connecting",
|
|
|
|
|
"bg-green-500": stream.connectionStatus === "connected",
|
|
|
|
|
"bg-red-500": stream.connectionStatus === "error",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-07-06 15:30:21 +08:00
|
|
|
📋
|
2026-06-30 19:20:08 +08:00
|
|
|
</button>
|
2026-06-25 21:04:53 +08:00
|
|
|
<button
|
|
|
|
|
onClick={() => setIsDocOpen(true)}
|
|
|
|
|
class="w-8 h-8 rounded-full border border-gray-300 text-gray-500 hover:text-gray-700 hover:border-gray-400 flex items-center justify-center text-sm font-medium"
|
|
|
|
|
title="指令文档"
|
|
|
|
|
>
|
|
|
|
|
?
|
|
|
|
|
</button>
|
2026-02-26 00:17:23 +08:00
|
|
|
</div>
|
|
|
|
|
</header>
|
2026-07-06 16:12:34 +08:00
|
|
|
{/* fill the rest of the space */}
|
|
|
|
|
<div
|
|
|
|
|
class="fixed top-16 left-0 right-0 bottom-0 overflow-auto transition-all duration-300"
|
|
|
|
|
classList={{ "md:pr-[420px]": isJournalOpen() }}
|
|
|
|
|
>
|
2026-06-25 21:04:53 +08:00
|
|
|
<main class="max-w-4xl mx-auto px-4 py-8 pt-4 md:ml-64 2xl:ml-auto flex justify-center items-center">
|
2026-07-03 22:11:50 +08:00
|
|
|
<Article
|
|
|
|
|
class="prose text-black prose-sm max-w-full flex-1"
|
|
|
|
|
src={currentPath()}
|
2026-07-07 21:38:43 +08:00
|
|
|
onDom={(dom) =>
|
|
|
|
|
addRevealedClasses(dom, location.pathname, comp.data)
|
|
|
|
|
}
|
2026-07-03 22:11:50 +08:00
|
|
|
/>
|
2026-06-25 21:04:53 +08:00
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
<DocDialog isOpen={isDocOpen()} onClose={() => setIsDocOpen(false)} />
|
2026-06-30 19:20:08 +08:00
|
|
|
<DataSourceDialog
|
|
|
|
|
isOpen={isDataSourceOpen()}
|
|
|
|
|
onClose={() => setIsDataSourceOpen(false)}
|
|
|
|
|
onSourceChanged={handleSourceChanged}
|
|
|
|
|
/>
|
2026-07-06 15:06:19 +08:00
|
|
|
<JournalPanel
|
|
|
|
|
open={isJournalOpen()}
|
|
|
|
|
onClose={() => setIsJournalOpen(false)}
|
|
|
|
|
/>
|
2026-02-26 00:17:23 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default App;
|