ttrpg-tools/src/components/md-deck/index.tsx

117 lines
3.3 KiB
TypeScript
Raw Normal View History

2026-02-27 15:21:21 +08:00
import { customElement, noShadowDOM } from 'solid-element';
import { Show, createEffect, onCleanup } from 'solid-js';
import { resolvePath } from '../utils/path';
import { createDeckStore } from './hooks/deckStore';
import { DeckHeader } from './DeckHeader';
import { DeckContent } from './DeckContent';
import { DataEditorPanel, PropertiesEditorPanel } from './editor-panel';
interface DeckProps {
size?: string;
2026-02-27 15:32:04 +08:00
sizeW?: number;
sizeH?: number;
2026-02-27 15:21:21 +08:00
grid?: string;
2026-02-27 15:32:04 +08:00
gridW?: number;
gridH?: number;
2026-02-27 15:21:21 +08:00
bleed?: string;
padding?: string;
2026-02-27 15:32:04 +08:00
fontSize?: number;
2026-02-27 15:21:21 +08:00
layers?: string;
fixed?: boolean | string;
}
customElement<DeckProps>('md-deck', {
2026-02-27 15:32:04 +08:00
size: '',
sizeW: 54,
sizeH: 86,
grid: '',
gridW: 5,
gridH: 8,
2026-02-27 15:21:21 +08:00
bleed: '1',
padding: '2',
2026-02-27 15:32:04 +08:00
fontSize: 3,
2026-02-27 15:21:21 +08:00
layers: '',
fixed: false
}, (props, { element }) => {
noShadowDOM();
// 从 element 的 textContent 获取 CSV 路径
const csvPath = element?.textContent?.trim() || '';
// 隐藏原始文本内容
if (element) {
element.textContent = '';
}
// 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
const articleEl = element?.closest('article[data-src]');
const articlePath = articleEl?.getAttribute('data-src') || '';
// 解析相对路径
const resolvedSrc = resolvePath(articlePath, csvPath);
// 创建 store 并加载数据
const store = createDeckStore(resolvedSrc, (props.layers as string) || '');
2026-02-27 15:32:04 +08:00
// 解析 size 属性(支持旧格式 "54x86" 和新格式)
if (props.size && props.size.includes('x')) {
const [w, h] = props.size.split('x').map(Number);
store.actions.setSizeW(w);
store.actions.setSizeH(h);
} else {
store.actions.setSizeW(props.sizeW ?? 54);
store.actions.setSizeH(props.sizeH ?? 86);
}
// 解析 grid 属性(支持旧格式 "5x8" 和新格式)
if (props.grid && props.grid.includes('x')) {
const [w, h] = props.grid.split('x').map(Number);
store.actions.setGridW(w);
store.actions.setGridH(h);
} else {
store.actions.setGridW(props.gridW ?? 5);
store.actions.setGridH(props.gridH ?? 8);
}
2026-02-27 15:21:21 +08:00
store.actions.setBleed(props.bleed || '1');
store.actions.setPadding(props.padding || '2');
2026-02-27 15:32:04 +08:00
store.actions.setFontSize(props.fontSize ?? 3);
2026-02-27 15:21:21 +08:00
// 加载 CSV 数据
store.actions.loadCardsFromPath(resolvedSrc, (props.layers as string) || '');
// 清理函数
onCleanup(() => {
store.actions.clearError();
});
return (
<div class="md-deck flex gap-4">
{/* 左侧CSV 数据编辑 */}
<Show when={store.state.isEditing && !store.state.fixed}>
<DataEditorPanel
activeTab={store.state.activeTab}
cards={store.state.cards}
updateCardData={store.actions.updateCardData}
/>
</Show>
{/* 中间:卡牌预览和控制 */}
<div class="flex-1">
{/* Tab 选择器和编辑按钮 */}
<Show when={store.state.cards.length > 0 && !store.state.error}>
<DeckHeader store={store} />
</Show>
{/* 内容区域:错误/加载/卡牌预览/空状态 */}
<DeckContent store={store} isLoading={store.state.isLoading} />
</div>
{/* 右侧:属性编辑表单 */}
<Show when={store.state.isEditing && !store.state.fixed}>
<PropertiesEditorPanel store={store} />
</Show>
</div>
);
});