feat: ai's first take...
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export { Dice } from './dice';
|
||||
export type { DiceProps } from './dice';
|
||||
|
||||
export { Table } from './table';
|
||||
export type { TableProps } from './table';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user