ttrpg-tools/src/components/table.tsx

92 lines
2.5 KiB
TypeScript
Raw Normal View History

2026-02-26 00:17:23 +08:00
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>
);
};