feat: ai's attempt
This commit is contained in:
parent
f596e66983
commit
d2a383c5d8
20
src/App.tsx
20
src/App.tsx
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue