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:17:23 +08:00
|
|
|
import { parse } from 'csv-parse/sync';
|
|
|
|
|
|
|
|
|
|
interface TableRow {
|
|
|
|
|
label: string;
|
|
|
|
|
body: string;
|
|
|
|
|
[key: string]: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 00:47:26 +08:00
|
|
|
customElement('ttrpg-table', { src: '', roll: false, remix: false }, (props, { element }) => {
|
|
|
|
|
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
|
|
|
});
|