Compare commits
No commits in common. "e57e09ae0541d24f2adbf655e1f88ec458aeff0f" and "588ae49f5f4f3a47fe831cf9dd62d11ffe8f33e2" have entirely different histories.
e57e09ae05
...
588ae49f5f
|
|
@ -1,6 +1,6 @@
|
||||||
label,body,adj,noun
|
label,body,adj,noun,group
|
||||||
1,**{{adj}}** 的{{noun}},高大,战士
|
🔥,**{{adj}}** 的{{noun}},高大,战士,基本
|
||||||
2,{{adj}}的{{noun}},矮小,法师
|
💧,{{adj}}的{{noun}},矮小,法师,基本
|
||||||
3,{{adj}}的{{noun}},帅气,弓手
|
🌪️,{{adj}}的{{noun}},帅气,弓手,高级
|
||||||
4,{{adj}}的{{noun}},丑陋,盗贼
|
🌱,{{adj}}的{{noun}},丑陋,盗贼,高级
|
||||||
5,{{adj}}的{{noun}},平庸,牧师
|
⚡,{{adj}}的{{noun}},平庸,牧师,高级
|
||||||
|
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"description": "A TTRPG toolbox based on solid.js and rsbuild",
|
"description": "A TTRPG toolbox based on solid.js and rsbuild",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ttrpg": "./dist/cli/index.js"
|
"ttrpg": "./dist/cli.js"
|
||||||
},
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
|
|
||||||
12
src/App.tsx
12
src/App.tsx
|
|
@ -3,7 +3,7 @@ import { useLocation } from "@solidjs/router";
|
||||||
|
|
||||||
// 导入组件以注册自定义元素
|
// 导入组件以注册自定义元素
|
||||||
import "./components";
|
import "./components";
|
||||||
import { Article, MobileSidebar, DesktopSidebar } from "./components";
|
import { Article, Sidebar } from "./components";
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -22,19 +22,15 @@ 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="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
|
class="text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
|
||||||
title="目录"
|
title="目录"
|
||||||
>
|
>
|
||||||
☰
|
☰
|
||||||
|
|
@ -42,7 +38,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 md:ml-64">
|
<main class="max-w-4xl mx-auto px-4 py-8 pt-20">
|
||||||
<Article src={currentPath()} />
|
<Article src={currentPath()} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,13 @@ import type { ServeCommandHandler } from "../types.js";
|
||||||
import { createServer, Server, IncomingMessage, ServerResponse } from "http";
|
import { createServer, Server, IncomingMessage, ServerResponse } from "http";
|
||||||
import { readdirSync, statSync, readFileSync, existsSync } from "fs";
|
import { readdirSync, statSync, readFileSync, existsSync } from "fs";
|
||||||
import { createReadStream } from "fs";
|
import { createReadStream } from "fs";
|
||||||
import { join, resolve, extname, sep, relative, dirname } from "path";
|
import { join, resolve, extname, sep, relative } from "path";
|
||||||
import { watch } from "chokidar";
|
import { watch } from "chokidar";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
interface ContentIndex {
|
interface ContentIndex {
|
||||||
[path: string]: string;
|
[path: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 CLI 脚本文件所在目录路径(用于定位 dist 文件夹)
|
|
||||||
*/
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
const distDir = resolve(__dirname, "..", "..", "..", "dist", "web");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIME 类型映射
|
* MIME 类型映射
|
||||||
*/
|
*/
|
||||||
|
|
@ -180,6 +172,7 @@ function createRequestHandler(
|
||||||
*/
|
*/
|
||||||
export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
const contentDir = resolve(dir);
|
const contentDir = resolve(dir);
|
||||||
|
const distDir = resolve(process.cwd(), "dist/web");
|
||||||
let contentIndex: ContentIndex = {};
|
let contentIndex: ContentIndex = {};
|
||||||
|
|
||||||
// 扫描内容目录生成索引
|
// 扫描内容目录生成索引
|
||||||
|
|
@ -243,7 +236,6 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`\n开发服务器已启动:http://localhost:${port}`);
|
console.log(`\n开发服务器已启动:http://localhost:${port}`);
|
||||||
console.log(`内容目录:${contentDir}`);
|
console.log(`内容目录:${contentDir}`);
|
||||||
console.log(`静态资源目录:${distDir}`);
|
|
||||||
console.log(`索引文件:http://localhost:${port}/__CONTENT_INDEX.json\n`);
|
console.log(`索引文件:http://localhost:${port}/__CONTENT_INDEX.json\n`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import { Component, createMemo, createSignal, Show } from "solid-js";
|
|
||||||
import { type FileNode, type TocNode } from "../data-loader";
|
|
||||||
import { useNavigate } from "@solidjs/router";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查当前文件路径是否在文件夹内
|
|
||||||
*/
|
|
||||||
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<string, TocNode[]>;
|
|
||||||
depth: number;
|
|
||||||
onClose: () => void;
|
|
||||||
}> = (props) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
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 handleClick = () => {
|
|
||||||
if (isDir) {
|
|
||||||
setIsExpanded(!isExpanded());
|
|
||||||
} else {
|
|
||||||
navigate(props.node.path);
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const indent = props.depth * 12;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class={`flex items-center py-1 px-2 cursor-pointer hover:bg-gray-100 rounded ${
|
|
||||||
isActive() ? "bg-blue-50 text-blue-700" : "text-gray-700"
|
|
||||||
}`}
|
|
||||||
style={{ "padding-left": `${indent + 8}px` }}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Show when={isDir}>
|
|
||||||
<span class="mr-1 text-gray-400">{isExpanded() ? "📂" : "📁"}</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={!isDir}>
|
|
||||||
<span class="mr-1 text-gray-400">📄</span>
|
|
||||||
</Show>
|
|
||||||
<span class="text-sm truncate">{props.node.name}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={isDir && isExpanded() && props.node.children}>
|
|
||||||
<div>
|
|
||||||
{props.node.children!.map((child) => (
|
|
||||||
<FileTreeNode
|
|
||||||
node={child}
|
|
||||||
currentPath={props.currentPath}
|
|
||||||
pathHeadings={props.pathHeadings}
|
|
||||||
depth={props.depth + 1}
|
|
||||||
onClose={props.onClose}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标题节点组件
|
|
||||||
*/
|
|
||||||
export const HeadingNode: Component<{
|
|
||||||
node: TocNode;
|
|
||||||
basePath: string;
|
|
||||||
depth: number;
|
|
||||||
}> = (props) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const anchor = props.node.title.toLowerCase().replace(/\s+/g, "-");
|
|
||||||
const href = `${props.basePath}#${anchor}`;
|
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
navigate(href);
|
|
||||||
};
|
|
||||||
|
|
||||||
const indent = props.depth * 12;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
class="block py-0.5 px-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded truncate"
|
|
||||||
style={{ "padding-left": `${indent + 8}px` }}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
{props.node.title}
|
|
||||||
</a>
|
|
||||||
<Show when={props.node.children}>
|
|
||||||
<div>
|
|
||||||
{props.node.children!.map((child) => (
|
|
||||||
<HeadingNode
|
|
||||||
node={child}
|
|
||||||
basePath={props.basePath}
|
|
||||||
depth={props.depth + 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,36 +1,158 @@
|
||||||
import { Component, createMemo, createSignal, onMount, Show } from "solid-js";
|
import { Component, createSignal, onMount, Show } from "solid-js";
|
||||||
import { generateToc, type FileNode, type TocNode } from "../data-loader";
|
import { generateToc, type FileNode, type TocNode } from "../data-loader";
|
||||||
import { useLocation } from "@solidjs/router";
|
import { useLocation, useNavigate } from "@solidjs/router";
|
||||||
import { FileTreeNode, HeadingNode } from "./FileTree";
|
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isDesktop?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 侧边栏内容组件
|
* 文件树节点组件
|
||||||
*/
|
*/
|
||||||
const SidebarContent: Component<{
|
const FileTreeNode: Component<{
|
||||||
fileTree: FileNode[];
|
node: FileNode;
|
||||||
pathHeadings: Record<string, TocNode[]>;
|
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
onClose: () => void;
|
pathHeadings: Record<string, TocNode[]>;
|
||||||
|
depth: number;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const location = useLocation();
|
const navigate = useNavigate();
|
||||||
|
const [isExpanded, setIsExpanded] = createSignal(true);
|
||||||
|
const isDir = !!props.node.children;
|
||||||
|
const isActive = props.currentPath === props.node.path;
|
||||||
|
|
||||||
// 响应式获取当前文件的标题列表
|
const handleClick = () => {
|
||||||
const currentFileHeadings = createMemo(() => {
|
if (isDir) {
|
||||||
|
setIsExpanded(!isExpanded());
|
||||||
|
} else {
|
||||||
|
navigate(props.node.path);
|
||||||
|
props.onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const indent = props.depth * 12;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class={`flex items-center py-1 px-2 cursor-pointer hover:bg-gray-100 rounded ${
|
||||||
|
isActive ? "bg-blue-50 text-blue-700" : "text-gray-700"
|
||||||
|
}`}
|
||||||
|
style={{ "padding-left": `${indent + 8}px` }}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<Show when={isDir}>
|
||||||
|
<span class="mr-1 text-gray-400">
|
||||||
|
{isExpanded() ? "📂" : "📁"}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={!isDir}>
|
||||||
|
<span class="mr-1 text-gray-400">📄</span>
|
||||||
|
</Show>
|
||||||
|
<span class="text-sm truncate">{props.node.name}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={isDir && isExpanded() && props.node.children}>
|
||||||
|
<div>
|
||||||
|
{props.node.children!.map((child) => (
|
||||||
|
<FileTreeNode
|
||||||
|
node={child}
|
||||||
|
currentPath={props.currentPath}
|
||||||
|
pathHeadings={props.pathHeadings}
|
||||||
|
depth={props.depth + 1}
|
||||||
|
onClose={props.onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题节点组件
|
||||||
|
*/
|
||||||
|
const HeadingNode: Component<{
|
||||||
|
node: TocNode;
|
||||||
|
basePath: string;
|
||||||
|
depth: number;
|
||||||
|
}> = (props) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const anchor = props.node.title.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
const href = `${props.basePath}#${anchor}`;
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(href);
|
||||||
|
};
|
||||||
|
|
||||||
|
const indent = props.depth * 12;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
class="block py-0.5 px-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded truncate"
|
||||||
|
style={{ "padding-left": `${indent + 8}px` }}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{props.node.title}
|
||||||
|
</a>
|
||||||
|
<Show when={props.node.children}>
|
||||||
|
<div>
|
||||||
|
{props.node.children!.map((child) => (
|
||||||
|
<HeadingNode
|
||||||
|
node={child}
|
||||||
|
basePath={props.basePath}
|
||||||
|
depth={props.depth + 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧边栏组件
|
||||||
|
*/
|
||||||
|
export const Sidebar: Component<SidebarProps> = (props) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
|
||||||
|
const [pathHeadings, setPathHeadings] = createSignal<
|
||||||
|
Record<string, TocNode[]>
|
||||||
|
>({});
|
||||||
|
const [currentFileHeadings, setCurrentFileHeadings] = createSignal<TocNode[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const toc = await generateToc();
|
||||||
|
setFileTree(toc.fileTree);
|
||||||
|
setPathHeadings(toc.pathHeadings);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据当前路径更新当前文件的标题列表
|
||||||
|
onMount(() => {
|
||||||
|
const updateHeadings = () => {
|
||||||
const pathname = location.pathname;
|
const pathname = location.pathname;
|
||||||
return props.pathHeadings[pathname] || props.pathHeadings[`${pathname}.md`] || [];
|
const headings = pathHeadings()[pathname] || pathHeadings()[`${pathname}.md`];
|
||||||
|
setCurrentFileHeadings(headings || []);
|
||||||
|
};
|
||||||
|
updateHeadings();
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Show when={props.isOpen}>
|
||||||
|
{/* 遮罩层 */}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||||
|
onClick={props.onClose}
|
||||||
|
/>
|
||||||
|
{/* 侧边栏 */}
|
||||||
|
<aside class="fixed top-0 left-0 h-full w-64 bg-white shadow-lg z-50 overflow-y-auto">
|
||||||
<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"
|
||||||
|
|
@ -38,7 +160,6 @@ const SidebarContent: Component<{
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 文件树 */}
|
{/* 文件树 */}
|
||||||
|
|
@ -46,11 +167,11 @@ const SidebarContent: Component<{
|
||||||
<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>
|
||||||
{props.fileTree.map((node) => (
|
{fileTree().map((node) => (
|
||||||
<FileTreeNode
|
<FileTreeNode
|
||||||
node={node}
|
node={node}
|
||||||
currentPath={props.currentPath}
|
currentPath={location.pathname}
|
||||||
pathHeadings={props.pathHeadings}
|
pathHeadings={pathHeadings()}
|
||||||
depth={0}
|
depth={0}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
/>
|
/>
|
||||||
|
|
@ -73,78 +194,7 @@ const SidebarContent: Component<{
|
||||||
</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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 桌面端固定侧边栏
|
|
||||||
*/
|
|
||||||
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>
|
</aside>
|
||||||
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ import './md-link';
|
||||||
// 导出组件
|
// 导出组件
|
||||||
export { Article } from './Article';
|
export { Article } from './Article';
|
||||||
export type { ArticleProps } from './Article';
|
export type { ArticleProps } from './Article';
|
||||||
export { MobileSidebar, DesktopSidebar } from './Sidebar';
|
export { Sidebar } from './Sidebar';
|
||||||
export type { SidebarProps } from './Sidebar';
|
export type { SidebarProps } from './Sidebar';
|
||||||
export { FileTreeNode, HeadingNode } from './FileTree';
|
|
||||||
|
|
||||||
// 导出数据类型
|
// 导出数据类型
|
||||||
export type { DiceProps } from './dice';
|
export type { DiceProps } from './dice';
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { customElement, noShadowDOM } from "solid-element";
|
||||||
import { createSignal, onCleanup } from "solid-js";
|
import { createSignal, onCleanup } from "solid-js";
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
import { Article } from "./Article";
|
import { Article } from "./Article";
|
||||||
import { resolvePath } from "../utils/path";
|
|
||||||
|
|
||||||
customElement("md-link", {}, (props, { element }) => {
|
customElement("md-link", {}, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
@ -28,7 +27,15 @@ customElement("md-link", {}, (props, { element }) => {
|
||||||
const articleEl = element?.closest('article[data-src]');
|
const articleEl = element?.closest('article[data-src]');
|
||||||
const articlePath = articleEl?.getAttribute('data-src') || '';
|
const articlePath = articleEl?.getAttribute('data-src') || '';
|
||||||
|
|
||||||
// 解析相对路径
|
// 解析相对路径(相对于 markdown 文件所在目录)
|
||||||
|
const resolvePath = (base: string, relative: string): string => {
|
||||||
|
if (relative.startsWith('/')) {
|
||||||
|
return relative;
|
||||||
|
}
|
||||||
|
const baseDir = base.substring(0, base.lastIndexOf('/') + 1);
|
||||||
|
return baseDir + relative;
|
||||||
|
};
|
||||||
|
|
||||||
const linkSrc = resolvePath(articlePath, path);
|
const linkSrc = resolvePath(articlePath, path);
|
||||||
|
|
||||||
// 查找包含此元素的 p 标签
|
// 查找包含此元素的 p 标签
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { customElement, noShadowDOM } from 'solid-element';
|
||||||
import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
|
import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
|
||||||
import { parse } from 'csv-parse/browser/esm/sync';
|
import { parse } from 'csv-parse/browser/esm/sync';
|
||||||
import { marked } from '../markdown';
|
import { marked } from '../markdown';
|
||||||
import { resolvePath } from '../utils/path';
|
|
||||||
|
|
||||||
interface TableRow {
|
interface TableRow {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -33,7 +32,15 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
||||||
const articleEl = element?.closest('article[data-src]');
|
const articleEl = element?.closest('article[data-src]');
|
||||||
const articlePath = articleEl?.getAttribute('data-src') || '';
|
const articlePath = articleEl?.getAttribute('data-src') || '';
|
||||||
|
|
||||||
// 解析相对路径
|
// 解析相对路径(相对于 markdown 文件所在目录)
|
||||||
|
const resolvePath = (base: string, relative: string): string => {
|
||||||
|
if (relative.startsWith('/')) {
|
||||||
|
return relative;
|
||||||
|
}
|
||||||
|
const baseDir = base.substring(0, base.lastIndexOf('/') + 1);
|
||||||
|
return baseDir + relative;
|
||||||
|
};
|
||||||
|
|
||||||
const resolvedSrc = resolvePath(articlePath, src);
|
const resolvedSrc = resolvePath(articlePath, src);
|
||||||
|
|
||||||
// 加载 CSV 文件的函数
|
// 加载 CSV 文件的函数
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { createDirectives } from 'marked-directive';
|
||||||
const marked = new Marked().use(createDirectives());
|
const marked = new Marked().use(createDirectives());
|
||||||
|
|
||||||
export function parseMarkdown(content: string): string {
|
export function parseMarkdown(content: string): string {
|
||||||
return marked.parse(content.trimStart()) as string;
|
return marked.parse(content) as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { marked };
|
export { marked };
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { resolvePath } from './path';
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
/**
|
|
||||||
* 解析相对路径为绝对路径
|
|
||||||
* 支持 ./ 和 ../ 语法
|
|
||||||
* @param base - 基准路径(通常是当前 markdown 文件的路径)
|
|
||||||
* @param relative - 相对路径或绝对路径
|
|
||||||
* @returns 解析后的路径
|
|
||||||
*/
|
|
||||||
export function resolvePath(base: string, relative: string): string {
|
|
||||||
if (relative.startsWith('/')) {
|
|
||||||
return relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseParts = base.split('/').filter(Boolean);
|
|
||||||
// 移除文件名,保留目录路径
|
|
||||||
if (baseParts.length > 0 && !base.endsWith('/')) {
|
|
||||||
baseParts.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const relativeParts = relative.split('/');
|
|
||||||
|
|
||||||
for (const part of relativeParts) {
|
|
||||||
if (part === '.' || part === '') {
|
|
||||||
// 跳过 . 和空字符串
|
|
||||||
continue;
|
|
||||||
} else if (part === '..') {
|
|
||||||
// 向上一级
|
|
||||||
baseParts.pop();
|
|
||||||
} else {
|
|
||||||
// 普通路径段
|
|
||||||
baseParts.push(part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '/' + baseParts.join('/');
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue