feat: ai's first take...

This commit is contained in:
2026-02-26 00:17:23 +08:00
parent f005514f56
commit 3c9cf552e5
21 changed files with 4001 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
import { Component, createSignal, onMount } from 'solid-js';
import { useLocation } from '@solidjs/router';
import { parseMarkdown } from './markdown';
const App: Component = () => {
const location = useLocation();
const [content, setContent] = createSignal('');
onMount(async () => {
// 根据路由加载对应的 markdown 文件
const path = location.pathname.slice(1) || 'index';
try {
const response = await fetch(`/content/${path}.md`);
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- 使用 `:dice[2d6+d8]` 插入骰子组件\n- 使用 `:table[./data.csv]` 插入表格组件');
}
});
return (
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow">
<div class="max-w-4xl mx-auto px-4 py-4">
<h1 class="text-2xl font-bold text-gray-900">TTRPG Tools</h1>
</div>
</header>
<main class="max-w-4xl mx-auto px-4 py-8">
<article class="prose prose-lg" innerHTML={parseMarkdown(content())} />
</main>
</div>
);
};
export default App;
+14
View File
@@ -0,0 +1,14 @@
import type { CommandHandler } from '../types.js';
export const compileCommand: CommandHandler = async (dir, options) => {
console.log(`开始编译...`);
console.log(`目录:${dir}`);
console.log(`输出目录:${options.output}`);
// TODO: 实现编译逻辑
// 1. 扫描目录下的所有 .md 文件
// 2. 解析 markdown 并生成路由
// 3. 打包为带 hash 路由的单个 HTML 入口
console.log('编译完成!');
};
+14
View File
@@ -0,0 +1,14 @@
import type { CommandHandler } from '../types.js';
export const serveCommand: CommandHandler = async (dir, options) => {
console.log(`启动开发服务器...`);
console.log(`目录:${dir}`);
console.log(`端口:${options.port}`);
// TODO: 实现开发服务器逻辑
// 1. 扫描目录下的所有 .md 文件
// 2. 启动 rsbuild 开发服务器
// 3. 监听文件变化
console.log('开发服务器已启动:http://localhost:' + options.port);
};
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { serveCommand } from './commands/serve.js';
import { compileCommand } from './commands/compile.js';
const program = new Command();
program
.name('ttrpg')
.description('TTRPG 工具箱 - 用于编译和预览 TTRPG 文档')
.version('0.0.1');
program
.command('serve')
.description('运行一个 web 服务器预览目录中的内容,并实时监听更新')
.argument('[dir]', '要预览的目录', '.')
.option('-p, --port <port>', '端口号', '3000')
.action(serveCommand);
program
.command('compile')
.description('将目录中的内容输出为带 hash 路由、单个 html 入口的 web 应用')
.argument('[dir]', '要编译的目录', '.')
.option('-o, --output <dir>', '输出目录', './dist/output')
.action(compileCommand);
program.parse();
+15
View File
@@ -0,0 +1,15 @@
export interface ServeOptions {
port: string;
}
export interface CompileOptions {
output: string;
}
export type CommandHandler = (dir: string, options: ServeOptions | CompileOptions) => Promise<void>;
export interface MarkdownFile {
path: string;
route: string;
content: string;
}
+66
View File
@@ -0,0 +1,66 @@
import { Component, createSignal, Show } from 'solid-js';
export interface DiceProps {
formula: string;
key?: string;
}
function rollDice(formula: string): number {
// 解析骰子公式,例如:2d6+d8
const parts = formula.split('+').map(p => p.trim());
let total = 0;
for (const part of parts) {
const match = part.match(/^(\d+)?d(\d+)$/i);
if (match) {
const count = parseInt(match[1] || '1');
const sides = parseInt(match[2]);
for (let i = 0; i < count; i++) {
total += Math.floor(Math.random() * sides) + 1;
}
} else {
// 处理固定数字
const num = parseInt(part);
if (!isNaN(num)) {
total += num;
}
}
}
return total;
}
export const Dice: Component<DiceProps> = (props) => {
const [result, setResult] = createSignal<number | null>(null);
const [isRolled, setIsRolled] = createSignal(false);
const handleClick = () => {
if (isRolled()) {
// 重置为公式
setResult(null);
setIsRolled(false);
} else {
// 掷骰子
const rollResult = rollDice(props.formula);
setResult(rollResult);
setIsRolled(true);
}
};
const displayText = () => (isRolled() ? `${result()}` : props.formula);
const queryParams = () => (props.key && isRolled() ? `?dice-${props.key}=${result()}` : '');
return (
<a
href={queryParams()}
onClick={(e) => {
e.preventDefault();
handleClick();
}}
class="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 cursor-pointer"
>
<span>🎲</span>
<span>{displayText()}</span>
</a>
);
};
+5
View File
@@ -0,0 +1,5 @@
export { Dice } from './dice';
export type { DiceProps } from './dice';
export { Table } from './table';
export type { TableProps } from './table';
+91
View File
@@ -0,0 +1,91 @@
import { Component, createSignal, For, Show } from 'solid-js';
import { parse } from 'csv-parse/sync';
export interface TableProps {
src: string;
roll?: boolean;
remix?: boolean;
}
interface TableRow {
label: string;
body: string;
[key: string]: string;
}
export const Table: Component<TableProps> = (props) => {
const [rows, setRows] = createSignal<TableRow[]>([]);
const [activeTab, setActiveTab] = createSignal(0);
// 解析 CSV 内容
const parseCSV = (content: string) => {
const records = parse(content, {
columns: true,
skip_empty_lines: true,
});
setRows(records as TableRow[]);
};
// 加载 CSV 文件
const loadCSV = async () => {
try {
const response = await fetch(props.src);
const content = await response.text();
parseCSV(content);
} catch (error) {
console.error('Failed to load CSV:', error);
}
};
// 初始化加载
loadCSV();
// 随机切换 tab
const handleRoll = () => {
const randomIndex = Math.floor(Math.random() * rows().length);
setActiveTab(randomIndex);
};
// 处理 body 内容中的 {{prop}} 语法
const processBody = (body: string, currentRow: TableRow): string => {
if (!props.remix) {
// 不启用 remix 时,只替换当前行的引用
return body.replace(/\{\{(\w+)\}\}/g, (_, key) => currentRow[key] || '');
} else {
// 启用 remix 时,每次引用使用随机行的内容
return body.replace(/\{\{(\w+)\}\}/g, (_, key) => {
const randomRow = rows()[Math.floor(Math.random() * rows().length)];
return randomRow?.[key] || '';
});
}
};
return (
<div class="ttrpg-table">
<div class="flex gap-2 border-b border-gray-200">
<For each={rows()}>
{(row, index) => (
<button
onClick={() => setActiveTab(index())}
class={`px-4 py-2 font-medium transition-colors ${
activeTab() === index()
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<Show when={props.roll}>
<span class="mr-1">🎲</span>
</Show>
{row.label}
</button>
)}
</For>
</div>
<div class="p-4 prose max-w-none">
<Show when={rows().length > 0}>
<div innerHTML={processBody(rows()[activeTab()].body, rows()[activeTab()])} />
</Show>
</div>
</div>
);
};
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TTRPG Tools</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
import { render } from 'solid-js/web';
import { Router, Route } from '@solidjs/router';
import App from './App';
import './styles.css';
const root = document.getElementById('root');
if (root) {
render(() => (
<Router>
<Route path="/" component={App} />
<Route path="/:path*" component={App} />
</Router>
), root);
}
+11
View File
@@ -0,0 +1,11 @@
import { Marked } from 'marked';
import { createDirectives } from 'marked-directive';
// 使用 marked-directive 来支持通过 @solidjs/element 添加的 UI 组件
const marked = new Marked().use(createDirectives());
export function parseMarkdown(content: string): string {
return marked.parse(content) as string;
}
export { marked };
+2
View File
@@ -0,0 +1,2 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";