feat: ai's attempt

This commit is contained in:
hypercross 2026-02-26 09:24:26 +08:00
parent f596e66983
commit d2a383c5d8
5 changed files with 209 additions and 17 deletions

View File

@ -1,16 +1,15 @@
import { Component, createSignal, onMount } from 'solid-js'; import { Component, createSignal, onMount } from 'solid-js';
import { useLocation } from '@solidjs/router'; import { useLocation } from '@solidjs/router';
import { parseMarkdown } from './markdown';
// 导入组件以注册自定义元素 // 导入组件以注册自定义元素
import './components'; import './components';
import { Article } from './components';
const App: Component = () => { const App: Component = () => {
const location = useLocation(); const location = useLocation();
const [content, setContent] = createSignal('');
const [currentPath, setCurrentPath] = createSignal(''); const [currentPath, setCurrentPath] = createSignal('');
onMount(async () => { onMount(() => {
// 根据路由加载对应的 markdown 文件 // 根据路由加载对应的 markdown 文件
let path = decodeURIComponent(location.pathname.slice(1)); let path = decodeURIComponent(location.pathname.slice(1));
if (!path) { if (!path) {
@ -23,17 +22,6 @@ const App: Component = () => {
path = `${path}.md`; path = `${path}.md`;
} }
setCurrentPath(path); setCurrentPath(path);
try {
const response = await fetch(path);
if (response.ok) {
const text = await response.text();
setContent(text);
} else {
setContent('# 欢迎使用 TTRPG Tools\n\n没有找到对应的内容文件。');
}
} catch (error) {
setContent('# 欢迎使用 TTRPG Tools\n\n这是一个 TTRPG 文档工具箱。\n\n## 功能\n\n- 使用 `:md-dice[2d6+d8]` 插入骰子组件\n- 使用 `:md-table[./data.csv]` 插入表格组件');
}
}); });
return ( return (
@ -44,9 +32,7 @@ const App: Component = () => {
</div> </div>
</header> </header>
<main class="max-w-4xl mx-auto px-4 py-8"> <main class="max-w-4xl mx-auto px-4 py-8">
<article class="prose prose-lg" data-src={currentPath()}> <Article src={currentPath()} />
<div innerHTML={parseMarkdown(content())} />
</article>
</main> </main>
</div> </div>
); );

View File

@ -0,0 +1,57 @@
import { Component, createSignal, onMount, onCleanup, Show } from 'solid-js';
import { parseMarkdown } from '../markdown';
import { fetchData } from '../data-loader';
export interface ArticleProps {
src: string;
onLoaded?: () => void;
onError?: (error: Error) => void;
}
/**
* Article
* src md markdown
*/
export const Article: Component<ArticleProps> = (props) => {
const [content, setContent] = createSignal('');
const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal<Error | null>(null);
let articleRef: HTMLArticleElement | undefined;
onMount(async () => {
setLoading(true);
try {
const text = await fetchData(props.src);
setContent(text);
setLoading(false);
props.onLoaded?.();
} catch (err) {
const errorObj = err instanceof Error ? err : new Error(String(err));
setError(errorObj);
setLoading(false);
props.onError?.(errorObj);
}
});
onCleanup(() => {
// 清理时清空内容,触发内部组件的销毁
setContent('');
});
return (
<article ref={articleRef} class="prose prose-lg" data-src={props.src}>
<Show when={loading()}>
<div class="text-gray-500">...</div>
</Show>
<Show when={error()}>
<div class="text-red-500">{error()?.message}</div>
</Show>
<Show when={!loading() && !error()}>
<div innerHTML={parseMarkdown(content())} />
</Show>
</article>
);
};
export default Article;

View File

@ -1,6 +1,11 @@
// 导入以注册自定义元素 // 导入以注册自定义元素
import './dice'; import './dice';
import './table'; import './table';
import './md-link';
// 导出组件
export { Article } from './Article';
export type { ArticleProps } from './Article';
// 导出类型 // 导出类型
export type { DiceProps } from './dice'; export type { DiceProps } from './dice';

View File

@ -0,0 +1,72 @@
import { customElement, noShadowDOM } from "solid-element";
import { createSignal, onCleanup } from "solid-js";
import { render } from "solid-js/web";
import { Article } from "./Article";
customElement("md-link", {}, (props, { element }) => {
noShadowDOM();
const [showArticle, setShowArticle] = createSignal(false);
let articleContainer: HTMLDivElement | undefined;
let disposeArticle: (() => void) | null = null;
// 从 element 的 textContent 获取链接目标
const linkSrc = element?.textContent?.trim() || "";
// 查找包含此元素的 p 标签
const parentP = element?.closest("p");
const toggleArticle = () => {
if (!parentP) return;
if (showArticle()) {
// 隐藏文章
if (articleContainer) {
if (disposeArticle) {
disposeArticle();
disposeArticle = null;
}
articleContainer.remove();
articleContainer = undefined;
}
setShowArticle(false);
} else {
// 显示文章
articleContainer = document.createElement("div");
articleContainer.classList.add("md-link-article");
articleContainer.classList.add("mt-4", "ml-4", "border-l-2", "border-gray-200", "pl-4");
parentP.after(articleContainer);
// 渲染 Article 组件
disposeArticle = render(() => (
<Article
src={linkSrc}
onLoaded={() => console.log("Article loaded:", linkSrc)}
onError={(err) => console.error("Article error:", err)}
/>
), articleContainer);
setShowArticle(true);
}
};
onCleanup(() => {
if (disposeArticle) {
disposeArticle();
disposeArticle = null;
}
});
return (
<a
href="#"
onClick={(e) => {
e.preventDefault();
toggleArticle();
}}
class="text-blue-600 hover:text-blue-800 hover:underline"
>
{linkSrc}
</a>
);
});

72
src/data-loader/index.ts Normal file
View File

@ -0,0 +1,72 @@
/**
*
*/
export interface DataIndex {
[key: string]: string;
}
/**
*
* dev 使 import.meta.glob
* cli
*/
export function getDataIndex(): DataIndex {
// @ts-ignore - import.meta.glob 在构建时会被处理
if (typeof import.meta !== 'undefined' && import.meta.glob) {
// Dev 环境:使用 import.meta.glob 动态导入
// @ts-ignore
const modules = import.meta.glob('../../content/**/*', { as: 'raw', eager: false });
const index: DataIndex = {};
for (const [path, importer] of Object.entries(modules)) {
const normalizedPath = path.replace('/src/', '/');
index[normalizedPath] = normalizedPath;
}
return index;
}
// CLI 环境:返回空索引,由 CLI 注入
return {};
}
/**
* CLI 使
*/
export function setDataIndex(index: DataIndex) {
(window as any).__TTRPG_DATA_INDEX__ = index;
}
/**
*
*/
function getIndexedData(path: string): string | null {
const index = (window as any).__TTRPG_DATA_INDEX__ as DataIndex | undefined;
if (index && index[path]) {
return index[path];
}
return null;
}
/**
*
* @param path
* @returns
*/
export async function fetchData(path: string): Promise<string> {
// 首先尝试从索引获取
const indexedData = getIndexedData(path);
if (indexedData) {
return indexedData;
}
// 索引不存在时,使用 fetch 加载
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`Failed to fetch: ${path}`);
}
return await response.text();
} catch (error) {
console.error('fetchData error:', error);
throw error;
}
}