feat: implement multi-deck merging for print preview
Introduce a deck registry to track all active deck instances. This allows users to select and merge multiple decks with matching dimensions into a single print preview and export session.
This commit is contained in:
+135
-121
@@ -1,12 +1,17 @@
|
||||
import { customElement, noShadowDOM } from 'solid-element';
|
||||
import { Show, onCleanup } from 'solid-js';
|
||||
import { resolvePath } from '../utils/path';
|
||||
import { createDeckStore } from './hooks/deckStore';
|
||||
import type { CardShape } from './types';
|
||||
import { DeckHeader } from './DeckHeader';
|
||||
import { DeckContent } from './DeckContent';
|
||||
import { PrintPreview } from './PrintPreview';
|
||||
import {DataEditorPanel, LayerEditorPanel, PropertiesEditorPanel} from './editor-panel';
|
||||
import { customElement, noShadowDOM } from "solid-element";
|
||||
import { Show, onCleanup } from "solid-js";
|
||||
import { resolvePath } from "../utils/path";
|
||||
import { createDeckStore } from "./hooks/deckStore";
|
||||
import { registerDeck, unregisterDeck } from "./hooks/deck-registry";
|
||||
import type { CardShape } from "./types";
|
||||
import { DeckHeader } from "./DeckHeader";
|
||||
import { DeckContent } from "./DeckContent";
|
||||
import { PrintPreview } from "./PrintPreview";
|
||||
import {
|
||||
DataEditorPanel,
|
||||
LayerEditorPanel,
|
||||
PropertiesEditorPanel,
|
||||
} from "./editor-panel";
|
||||
|
||||
interface DeckProps {
|
||||
size?: string;
|
||||
@@ -23,133 +28,142 @@ interface DeckProps {
|
||||
fixed?: boolean | string;
|
||||
}
|
||||
|
||||
customElement<DeckProps>('md-deck', {
|
||||
size: '',
|
||||
sizeW: 54,
|
||||
sizeH: 86,
|
||||
grid: '',
|
||||
gridW: 5,
|
||||
gridH: 8,
|
||||
bleed: 1,
|
||||
padding: 2,
|
||||
shape: 'rectangle',
|
||||
layers: '',
|
||||
backLayers: '',
|
||||
fixed: false
|
||||
}, (props, { element }) => {
|
||||
noShadowDOM();
|
||||
customElement<DeckProps>(
|
||||
"md-deck",
|
||||
{
|
||||
size: "",
|
||||
sizeW: 54,
|
||||
sizeH: 86,
|
||||
grid: "",
|
||||
gridW: 5,
|
||||
gridH: 8,
|
||||
bleed: 1,
|
||||
padding: 2,
|
||||
shape: "rectangle",
|
||||
layers: "",
|
||||
backLayers: "",
|
||||
fixed: false,
|
||||
},
|
||||
(props, { element }) => {
|
||||
noShadowDOM();
|
||||
|
||||
// 从 element 的 textContent 获取 CSV 路径
|
||||
const csvPath = element?.textContent?.trim() || '';
|
||||
// 从 element 的 textContent 获取 CSV 路径
|
||||
const csvPath = element?.textContent?.trim() || "";
|
||||
|
||||
// 隐藏原始文本内容
|
||||
if (element) {
|
||||
element.textContent = '';
|
||||
}
|
||||
// 隐藏原始文本内容
|
||||
if (element) {
|
||||
element.textContent = "";
|
||||
}
|
||||
|
||||
// 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
|
||||
const articleEl = element?.closest('article[data-src]');
|
||||
const articlePath = articleEl?.getAttribute('data-src') || '';
|
||||
// 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
|
||||
const articleEl = element?.closest("article[data-src]");
|
||||
const articlePath = articleEl?.getAttribute("data-src") || "";
|
||||
|
||||
// 解析相对路径
|
||||
const resolvedSrc = resolvePath(articlePath, csvPath);
|
||||
// 解析相对路径
|
||||
const resolvedSrc = resolvePath(articlePath, csvPath);
|
||||
|
||||
// 创建 store 并加载数据
|
||||
const store = createDeckStore(resolvedSrc);
|
||||
// 创建 store 并加载数据
|
||||
const store = createDeckStore(resolvedSrc);
|
||||
|
||||
// 解析 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);
|
||||
}
|
||||
// 生成唯一 ID 并注册到全局注册表
|
||||
const deckId = `deck-${crypto.randomUUID()}`;
|
||||
registerDeck(deckId, store, resolvedSrc, csvPath);
|
||||
|
||||
// 解析 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);
|
||||
}
|
||||
// 解析 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);
|
||||
}
|
||||
|
||||
// 解析 bleed 和 padding(支持旧字符串格式和新数字格式)
|
||||
if (typeof props.bleed === 'string') {
|
||||
store.actions.setBleed(Number(props.bleed));
|
||||
} else {
|
||||
store.actions.setBleed(props.bleed ?? 1);
|
||||
}
|
||||
// 解析 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);
|
||||
}
|
||||
|
||||
if (typeof props.padding === 'string') {
|
||||
store.actions.setPadding(Number(props.padding));
|
||||
} else {
|
||||
store.actions.setPadding(props.padding ?? 2);
|
||||
}
|
||||
// 解析 bleed 和 padding(支持旧字符串格式和新数字格式)
|
||||
if (typeof props.bleed === "string") {
|
||||
store.actions.setBleed(Number(props.bleed));
|
||||
} else {
|
||||
store.actions.setBleed(props.bleed ?? 1);
|
||||
}
|
||||
|
||||
// 设置形状
|
||||
store.actions.setShape(props.shape ?? 'rectangle');
|
||||
if (typeof props.padding === "string") {
|
||||
store.actions.setPadding(Number(props.padding));
|
||||
} else {
|
||||
store.actions.setPadding(props.padding ?? 2);
|
||||
}
|
||||
|
||||
// 加载 CSV 数据
|
||||
store.actions.loadCardsFromPath(
|
||||
resolvedSrc,
|
||||
csvPath,
|
||||
(props.layers as string) || '',
|
||||
(props.backLayers as string) || ''
|
||||
);
|
||||
// 设置形状
|
||||
store.actions.setShape(props.shape ?? "rectangle");
|
||||
|
||||
// 清理函数
|
||||
onCleanup(() => {
|
||||
store.actions.clearError();
|
||||
});
|
||||
// 加载 CSV 数据
|
||||
store.actions.loadCardsFromPath(
|
||||
resolvedSrc,
|
||||
csvPath,
|
||||
(props.layers as string) || "",
|
||||
(props.backLayers as string) || "",
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="md-deck mb-4">
|
||||
{/* 导出 PDF 预览弹窗 */}
|
||||
<Show when={store.state.isExporting}>
|
||||
<PrintPreview
|
||||
articlePath={articlePath}
|
||||
store={store}
|
||||
onClose={() => store.actions.setExporting(false)}
|
||||
onExport={() => {}}
|
||||
/>
|
||||
</Show>
|
||||
// 清理函数
|
||||
onCleanup(() => {
|
||||
unregisterDeck(deckId);
|
||||
store.actions.clearError();
|
||||
});
|
||||
|
||||
{/* Tab 选择器和编辑按钮 */}
|
||||
<Show when={store.state.cards.length > 0 && !store.state.error}>
|
||||
<DeckHeader store={store} />
|
||||
</Show>
|
||||
|
||||
<div class="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>*/}
|
||||
|
||||
<Show when={store.state.isEditing && !store.state.fixed}>
|
||||
<div class="flex-1">
|
||||
<PropertiesEditorPanel store={store} />
|
||||
</div>
|
||||
return (
|
||||
<div class="md-deck mb-4">
|
||||
{/* 导出 PDF 预览弹窗 */}
|
||||
<Show when={store.state.isExporting}>
|
||||
<PrintPreview
|
||||
articlePath={articlePath}
|
||||
store={store}
|
||||
deckId={deckId}
|
||||
onClose={() => store.actions.setExporting(false)}
|
||||
onExport={() => {}}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<DeckContent store={store} isLoading={store.state.isLoading} />
|
||||
|
||||
{/* 右侧:属性/图层编辑面板 */}
|
||||
<Show when={store.state.isEditing && !store.state.fixed}>
|
||||
<div class="flex-1">
|
||||
<LayerEditorPanel store={store} />
|
||||
</div>
|
||||
{/* Tab 选择器和编辑按钮 */}
|
||||
<Show when={store.state.cards.length > 0 && !store.state.error}>
|
||||
<DeckHeader store={store} />
|
||||
</Show>
|
||||
|
||||
<div class="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>*/}
|
||||
|
||||
<Show when={store.state.isEditing && !store.state.fixed}>
|
||||
<div class="flex-1">
|
||||
<PropertiesEditorPanel store={store} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<DeckContent store={store} isLoading={store.state.isLoading} />
|
||||
|
||||
{/* 右侧:属性/图层编辑面板 */}
|
||||
<Show when={store.state.isEditing && !store.state.fixed}>
|
||||
<div class="flex-1">
|
||||
<LayerEditorPanel store={store} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user