feat: groupin

This commit is contained in:
hypercross 2026-02-26 10:10:05 +08:00
parent cf61c996cc
commit 08f8e0da52
3 changed files with 104 additions and 34 deletions

View File

@ -18,9 +18,9 @@
:md-table[./sparks.csv] :md-table[./sparks.csv]
:md-table[./sparks.csv]{roll=true} :md-table[./sparks.csv]{roll}
:md-table[./sparks.csv]{roll=true remix=true} :md-table[./sparks.csv]{roll remix}
### 链接 ### 链接

View File

@ -1,6 +1,6 @@
label,body,adj,noun label,body,adj,noun,group
🔥,**{{adj}}** 的{{noun}},高大,战士 🔥,**{{adj}}** 的{{noun}},高大,战士,基本
💧,{{adj}}的{{noun}},矮小,法师 💧,{{adj}}的{{noun}},矮小,法师,基本
🌪️,{{adj}}的{{noun}},帅气,弓手 🌪️,{{adj}}的{{noun}},帅气,弓手,高级
🌱,{{adj}}的{{noun}},丑陋,盗贼 🌱,{{adj}}的{{noun}},丑陋,盗贼,高级
⚡,{{adj}}的{{noun}},平庸,牧师 ⚡,{{adj}}的{{noun}},平庸,牧师,高级

1 label body adj noun group
2 🔥 **{{adj}}** 的{{noun}} 高大 战士 基本
3 💧 {{adj}}的{{noun}} 矮小 法师 基本
4 🌪️ {{adj}}的{{noun}} 帅气 弓手 高级
5 🌱 {{adj}}的{{noun}} 丑陋 盗贼 高级
6 {{adj}}的{{noun}} 平庸 牧师 高级

View File

@ -1,11 +1,12 @@
import { customElement, noShadowDOM } from 'solid-element'; import { customElement, noShadowDOM } from 'solid-element';
import { createSignal, For, Show, createEffect } from 'solid-js'; import { createSignal, For, Show, createEffect, createMemo } from 'solid-js';
import { parse } from 'csv-parse/browser/esm/sync'; import { parse } from 'csv-parse/browser/esm/sync';
import { marked } from '../markdown'; import { marked } from '../markdown';
interface TableRow { interface TableRow {
label: string; label: string;
body: string; body: string;
group?: string;
[key: string]: string; [key: string]: string;
} }
@ -13,12 +14,13 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
noShadowDOM(); noShadowDOM();
const [rows, setRows] = createSignal<TableRow[]>([]); const [rows, setRows] = createSignal<TableRow[]>([]);
const [activeTab, setActiveTab] = createSignal(0); const [activeTab, setActiveTab] = createSignal(0);
const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
const [loaded, setLoaded] = createSignal(false); const [loaded, setLoaded] = createSignal(false);
const [bodyHtml, setBodyHtml] = createSignal(''); const [bodyHtml, setBodyHtml] = createSignal('');
// 从 element 的 textContent 获取 CSV 路径 // 从 element 的 textContent 获取 CSV 路径
const src = element?.textContent?.trim() || ''; const src = element?.textContent?.trim() || '';
// 隐藏原始文本内容 // 隐藏原始文本内容
if (element) { if (element) {
element.textContent = ''; element.textContent = '';
@ -68,10 +70,37 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
loadCSV(); loadCSV();
} }
// 检测是否有 group 列
const hasGroup = createMemo(() => {
const allRows = rows();
return allRows.length > 0 && 'group' in allRows[0];
});
// 获取所有分组
const groups = createMemo(() => {
if (!hasGroup()) return [];
const allRows = rows();
const groupSet = new Set<string>();
for (const row of allRows) {
if (row.group) {
groupSet.add(row.group);
}
}
return Array.from(groupSet).sort();
});
// 根据当前选中的分组过滤行
const filteredRows = createMemo(() => {
const allRows = rows();
const group = activeGroup();
if (!group) return allRows;
return allRows.filter(row => row.group === group);
});
// 处理 body 内容中的 {{prop}} 语法并解析 markdown // 处理 body 内容中的 {{prop}} 语法并解析 markdown
const processBody = (body: string, currentRow: TableRow): string => { const processBody = (body: string, currentRow: TableRow): string => {
let processedBody = body; let processedBody = body;
if (!props.remix) { if (!props.remix) {
// 不启用 remix 时,只替换当前行的引用 // 不启用 remix 时,只替换当前行的引用
processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => currentRow[key] || ''); processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => currentRow[key] || '');
@ -82,62 +111,103 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
return randomRow?.[key] || ''; return randomRow?.[key] || '';
}); });
} }
// 使用 marked 解析 markdown // 使用 marked 解析 markdown
return marked.parse(processedBody) as string; return marked.parse(processedBody) as string;
}; };
// 更新 body 内容 // 更新 body 内容
const updateBodyContent = () => { const updateBodyContent = () => {
if (loaded() && rows().length > 0) { const filtered = filteredRows();
const currentRow = rows()[activeTab()]; if (loaded() && filtered.length > 0) {
const index = Math.min(activeTab(), filtered.length - 1);
const currentRow = filtered[index];
setBodyHtml(processBody(currentRow.body, currentRow)); setBodyHtml(processBody(currentRow.body, currentRow));
} }
}; };
// 监听 activeTab 变化并更新内容 // 监听 activeTab 和 activeGroup 变化并更新内容
createEffect(() => { createEffect(() => {
activeTab(); activeTab();
activeGroup();
updateBodyContent(); updateBodyContent();
}); });
// 切换分组时重置 tab 索引
const handleGroupChange = (group: string | null) => {
setActiveGroup(group);
setActiveTab(0);
};
// 随机切换 tab // 随机切换 tab
const handleRoll = () => { const handleRoll = () => {
const randomIndex = Math.floor(Math.random() * rows().length); const randomIndex = Math.floor(Math.random() * filteredRows().length);
setActiveTab(randomIndex); setActiveTab(randomIndex);
}; };
return ( return (
<div class="ttrpg-table"> <div class="ttrpg-table">
<div class="flex items-center gap-2 border-b border-gray-200"> <div class="flex items-center gap-2 border-b border-gray-200">
<Show when={props.roll}> <div class="flex flex-col w-full">
<button {/* 分组 tabs */}
onClick={handleRoll} <Show when={hasGroup()}>
class="text-gray-500 hover:text-gray-700" <div class="flex gap-2 border-b border-gray-100 pb-2">
title="随机切换"
>
🎲
</button>
</Show>
<div class="flex gap-2">
<For each={rows()}>
{(row, index) => (
<button <button
onClick={() => setActiveTab(index())} onClick={() => handleGroupChange(null)}
class={`font-medium transition-colors ${ class={`font-medium transition-colors ${
activeTab() === index() activeGroup() === null
? 'text-blue-600 border-b-2 border-blue-600' ? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700' : 'text-gray-500 hover:text-gray-700'
}`} }`}
> >
{row.label}
</button> </button>
)} <For each={groups()}>
</For> {(group) => (
<button
onClick={() => handleGroupChange(group)}
class={`font-medium transition-colors ${
activeGroup() === group
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{group}
</button>
)}
</For>
</div>
</Show>
{/* 内容 tabs */}
<div class="flex gap-2">
<Show when={props.roll}>
<button
onClick={handleRoll}
class="text-gray-500 hover:text-gray-700"
title="随机切换"
>
🎲
</button>
</Show>
<For each={filteredRows()}>
{(row, index) => (
<button
onClick={() => setActiveTab(index())}
class={`font-medium transition-colors ${
activeTab() === index()
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{row.label}
</button>
)}
</For>
</div>
</div> </div>
</div> </div>
<div class="p-1 prose max-w-none"> <div class="p-1 prose max-w-none">
<Show when={loaded() && rows().length > 0}> <Show when={loaded() && filteredRows().length > 0}>
<div innerHTML={bodyHtml()} /> <div innerHTML={bodyHtml()} />
</Show> </Show>
</div> </div>