2026-05-11 11:25:57 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Component,
|
|
|
|
|
|
createEffect,
|
|
|
|
|
|
onCleanup,
|
|
|
|
|
|
Show,
|
|
|
|
|
|
createResource,
|
|
|
|
|
|
createMemo,
|
|
|
|
|
|
} from "solid-js";
|
|
|
|
|
|
import { parseMarkdown } from "../markdown";
|
|
|
|
|
|
import { extractSection } from "../data-loader";
|
|
|
|
|
|
import mermaid from "mermaid";
|
|
|
|
|
|
import { getIndexedData } from "../data-loader/file-index";
|
|
|
|
|
|
import { resolvePath } from "./utils/path";
|
|
|
|
|
|
import { useLocation } from "@solidjs/router";
|
2026-02-26 09:24:26 +08:00
|
|
|
|
|
|
|
|
|
|
export interface ArticleProps {
|
|
|
|
|
|
src: string;
|
2026-05-11 11:25:57 +08:00
|
|
|
|
section?: string; // 指定要显示的标题(不含 #)
|
|
|
|
|
|
iconPath?: string; // 图标路径前缀,默认为 "./assets",空字符串表示禁用
|
2026-02-26 09:24:26 +08:00
|
|
|
|
onLoaded?: () => void;
|
|
|
|
|
|
onError?: (error: Error) => void;
|
2026-05-11 11:25:57 +08:00
|
|
|
|
class?: string; // 额外的 class 用于样式控制
|
2026-03-22 00:57:35 +08:00
|
|
|
|
scrollToHash?: boolean; // 是否自动滚动到 hash
|
2026-02-26 09:24:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 11:25:57 +08:00
|
|
|
|
async function fetchArticleContent(params: {
|
|
|
|
|
|
src: string;
|
|
|
|
|
|
section?: string;
|
|
|
|
|
|
}): Promise<string> {
|
2026-03-13 15:52:51 +08:00
|
|
|
|
const text = await getIndexedData(params.src);
|
2026-02-26 14:51:26 +08:00
|
|
|
|
// 如果指定了 section,提取对应内容
|
|
|
|
|
|
return params.section ? extractSection(text, params.section) : text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 00:57:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 滚动到指定的 hash 元素
|
|
|
|
|
|
*/
|
|
|
|
|
|
function scrollToHash(hash: string) {
|
|
|
|
|
|
if (!hash) return;
|
|
|
|
|
|
// 移除 # 前缀
|
2026-05-11 11:25:57 +08:00
|
|
|
|
const id = hash.startsWith("#") ? hash.slice(1) : hash;
|
2026-03-22 00:57:35 +08:00
|
|
|
|
if (!id) return;
|
2026-05-11 11:25:57 +08:00
|
|
|
|
|
2026-03-22 00:57:35 +08:00
|
|
|
|
// 使用 decodeURIComponent 解码 ID(处理中文等特殊字符)
|
|
|
|
|
|
const decodedId = decodeURIComponent(id);
|
2026-05-11 11:25:57 +08:00
|
|
|
|
|
2026-03-22 00:57:35 +08:00
|
|
|
|
// 尝试查找元素
|
|
|
|
|
|
const element = document.getElementById(decodedId);
|
|
|
|
|
|
if (element) {
|
|
|
|
|
|
// 使用 scrollIntoView 滚动到元素
|
2026-05-11 11:25:57 +08:00
|
|
|
|
element.scrollIntoView({ behavior: "instant", block: "start" });
|
2026-03-22 00:57:35 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 09:24:26 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Article 组件
|
|
|
|
|
|
* 用于将特定 src 位置的 md 文件显示为 markdown 文章
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const Article: Component<ArticleProps> = (props) => {
|
2026-03-22 00:57:35 +08:00
|
|
|
|
const location = useLocation();
|
2026-02-26 14:51:26 +08:00
|
|
|
|
const [content, { refetch }] = createResource(
|
|
|
|
|
|
() => ({ src: props.src, section: props.section }),
|
2026-05-11 11:25:57 +08:00
|
|
|
|
fetchArticleContent,
|
2026-02-26 14:51:26 +08:00
|
|
|
|
);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
|
2026-03-14 10:27:07 +08:00
|
|
|
|
// 解析 iconPath,默认为 "./assets",空字符串表示禁用
|
|
|
|
|
|
const iconPrefix = createMemo(() => {
|
2026-05-11 11:25:57 +08:00
|
|
|
|
if (props.iconPath === "") return undefined; // 空字符串禁用图标前缀
|
2026-03-14 10:27:07 +08:00
|
|
|
|
return resolvePath(props.src, props.iconPath ?? "./assets");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-26 14:51:26 +08:00
|
|
|
|
createEffect(() => {
|
|
|
|
|
|
const data = content();
|
|
|
|
|
|
if (data) {
|
2026-02-26 09:24:26 +08:00
|
|
|
|
props.onLoaded?.();
|
2026-03-02 11:03:51 +08:00
|
|
|
|
// 内容加载完成后,渲染 mermaid 图表
|
|
|
|
|
|
void mermaid.run();
|
2026-05-11 11:25:57 +08:00
|
|
|
|
|
2026-03-22 00:57:35 +08:00
|
|
|
|
// 内容渲染后检查 hash 并滚动
|
|
|
|
|
|
scrollToHash(location.hash);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onCleanup(() => {
|
|
|
|
|
|
// 清理时清空内容,触发内部组件的销毁
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-11 11:25:57 +08:00
|
|
|
|
<article class={`prose ${props.class || ""}`} data-src={props.src}>
|
2026-02-26 14:51:26 +08:00
|
|
|
|
<Show when={content.loading}>
|
2026-02-26 09:24:26 +08:00
|
|
|
|
<div class="text-gray-500">加载中...</div>
|
|
|
|
|
|
</Show>
|
2026-02-26 14:51:26 +08:00
|
|
|
|
<Show when={content.error}>
|
|
|
|
|
|
<div class="text-red-500">加载失败:{content.error?.message}</div>
|
2026-02-26 09:24:26 +08:00
|
|
|
|
</Show>
|
2026-02-26 14:51:26 +08:00
|
|
|
|
<Show when={!content.loading && !content.error && content()}>
|
2026-05-11 11:25:57 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="relative"
|
|
|
|
|
|
innerHTML={parseMarkdown(content()!, iconPrefix())}
|
|
|
|
|
|
/>
|
2026-02-26 09:24:26 +08:00
|
|
|
|
</Show>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default Article;
|