ttrpg-tools/src/components/table.tsx

102 lines
2.9 KiB
TypeScript
Raw Normal View History

2026-02-26 00:47:26 +08:00
import { customElement, noShadowDOM } from 'solid-element';
import { createSignal, For, Show } from 'solid-js';
2026-02-26 00:49:20 +08:00
import { parse } from 'csv-parse/browser/esm/sync';
2026-02-26 00:17:23 +08:00
interface TableRow {
label: string;
body: string;
[key: string]: string;
}
2026-02-26 00:51:32 +08:00
customElement('md-table', { src: '', roll: false, remix: false }, (props, { element }) => {
2026-02-26 00:47:26 +08:00
noShadowDOM();
2026-02-26 00:17:23 +08:00
const [rows, setRows] = createSignal<TableRow[]>([]);
const [activeTab, setActiveTab] = createSignal(0);
2026-02-26 00:47:26 +08:00
const [loaded, setLoaded] = createSignal(false);
2026-02-26 00:17:23 +08:00
// 解析 CSV 内容
const parseCSV = (content: string) => {
const records = parse(content, {
columns: true,
skip_empty_lines: true,
});
setRows(records as TableRow[]);
2026-02-26 00:47:26 +08:00
setLoaded(true);
2026-02-26 00:17:23 +08:00
};
// 加载 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);
2026-02-26 00:47:26 +08:00
setLoaded(true);
2026-02-26 00:17:23 +08:00
}
};
// 初始化加载
2026-02-26 00:47:26 +08:00
if (!loaded()) {
loadCSV();
}
2026-02-26 00:17:23 +08:00
// 随机切换 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>
2026-02-26 00:47:26 +08:00
<Show when={props.roll}>
<button
onClick={handleRoll}
class="px-2 py-2 text-gray-500 hover:text-gray-700"
title="随机切换"
>
🎲
</button>
</Show>
2026-02-26 00:17:23 +08:00
</div>
<div class="p-4 prose max-w-none">
2026-02-26 00:47:26 +08:00
<Show when={loaded() && rows().length > 0}>
2026-02-26 00:17:23 +08:00
<div innerHTML={processBody(rows()[activeTab()].body, rows()[activeTab()])} />
</Show>
</div>
</div>
);
2026-02-26 00:47:26 +08:00
});