From 4953d33f0fa75a63944b77fb393b0c07898fd47f Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 5 May 2026 16:30:49 +0800 Subject: [PATCH] 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. --- src/components/md-deck/PrintPreview.tsx | 340 +++++++++++------- src/components/md-deck/PrintPreviewHeader.tsx | 157 +++++++- src/components/md-deck/hooks/deck-registry.ts | 99 +++++ src/components/md-deck/hooks/usePageLayout.ts | 163 ++++++--- src/components/md-deck/index.tsx | 256 ++++++------- 5 files changed, 699 insertions(+), 316 deletions(-) create mode 100644 src/components/md-deck/hooks/deck-registry.ts diff --git a/src/components/md-deck/PrintPreview.tsx b/src/components/md-deck/PrintPreview.tsx index 713afa2..d93d45f 100644 --- a/src/components/md-deck/PrintPreview.tsx +++ b/src/components/md-deck/PrintPreview.tsx @@ -1,18 +1,21 @@ -import { createSignal, For, Show } from 'solid-js'; -import { Portal } from 'solid-js/web'; -import type { DeckStore } from './hooks/deckStore'; -import { usePageLayout } from './hooks/usePageLayout'; -import { usePDFExport, type ExportOptions } from './hooks/usePDFExport'; -import { usePlotterExport } from './hooks/usePlotterExport'; -import { getShapeSvgClipPath } from './hooks/shape-styles'; -import { PrintPreviewHeader } from './PrintPreviewHeader'; -import { PrintPreviewFooter } from './PrintPreviewFooter'; -import { CardLayer } from './CardLayer'; -import { PltPreview } from './PltPreview'; +import { createEffect, createMemo, createSignal, For, Show } from "solid-js"; +import { Portal } from "solid-js/web"; +import type { DeckStore } from "./hooks/deckStore"; +import { usePageLayout } from "./hooks/usePageLayout"; +import { usePDFExport, type ExportOptions } from "./hooks/usePDFExport"; +import { usePlotterExport } from "./hooks/usePlotterExport"; +import { getShapeSvgClipPath } from "./hooks/shape-styles"; +import { PrintPreviewHeader } from "./PrintPreviewHeader"; +import { PrintPreviewFooter } from "./PrintPreviewFooter"; +import { CardLayer } from "./CardLayer"; +import { PltPreview } from "./PltPreview"; +import { getDeckById } from "./hooks/deck-registry"; +import type { CardData } from "./types"; export interface PrintPreviewProps { - articlePath?: string, + articlePath?: string; store: DeckStore; + deckId: string; onClose: () => void; onExport: () => void; } @@ -22,15 +25,41 @@ export interface PrintPreviewProps { */ export function PrintPreview(props: PrintPreviewProps) { const { store } = props; - const { getA4Size, pages, cropMarks } = usePageLayout(store); + + // 合并的卡牌列表:主牌组 + 选中的其他牌组 + const [selectedDeckIds, setSelectedDeckIds] = createSignal([]); + + // 当某个已选中的牌组被卸载时,自动从选中列表中移除 + createEffect(() => { + const ids = selectedDeckIds(); + const stillValid = ids.filter((id) => getDeckById(id)); + if (stillValid.length !== ids.length) { + setSelectedDeckIds(stillValid); + } + }); + + const mergedCards = createMemo(() => { + const base = store.state.cards as CardData[]; + const extras = selectedDeckIds() + .map((id) => getDeckById(id)) + .filter(Boolean) + .flatMap((entry) => entry!.store.state.cards as CardData[]); + return [...base, ...extras]; + }); + + const mergedCardCount = () => mergedCards().length; + + const { getA4Size, pages, cropMarks } = usePageLayout(store, mergedCards); const { exportToPDF } = usePDFExport(store, props.onClose); const { generatePltData } = usePlotterExport(store); const [showPltPreview, setShowPltPreview] = createSignal(false); - const [pltCode, setPltCode] = createSignal(''); + const [pltCode, setPltCode] = createSignal(""); - const frontVisibleLayers = () => store.state.frontLayerConfigs.filter((l) => l.visible); - const backVisibleLayers = () => store.state.backLayerConfigs.filter((l) => l.visible); + const frontVisibleLayers = () => + store.state.frontLayerConfigs.filter((l) => l.visible); + const backVisibleLayers = () => + store.state.backLayerConfigs.filter((l) => l.visible); const handleExport = async () => { const options: ExportOptions = { @@ -42,7 +71,7 @@ export function PrintPreview(props: PrintPreviewProps) { gridAreaWidth: store.state.dimensions?.gridAreaWidth || 56, gridAreaHeight: store.state.dimensions?.gridAreaHeight || 88, visibleLayers: frontVisibleLayers(), - dimensions: store.state.dimensions! + dimensions: store.state.dimensions!, }; await exportToPDF(pages(), cropMarks(), options); }; @@ -53,7 +82,7 @@ export function PrintPreview(props: PrintPreviewProps) { setPltCode(data.pltCode); setShowPltPreview(true); } else { - alert('没有可预览的卡片'); + alert("没有可预览的卡片"); } }; @@ -63,143 +92,178 @@ export function PrintPreview(props: PrintPreviewProps) { return ( - }> + + } + >
{ + setSelectedDeckIds((prev) => + prev.includes(id) + ? prev.filter((x) => x !== id) + : [...prev, id], + ); + }} onExport={handleExport} onOpenPltPreview={handleOpenPltPreview} onClose={props.onClose} /> - + -
- - {(page) => { - // 根据页面类型(正面/背面)决定使用哪个图层配置 - const isFrontPage = page.cards[0]?.side !== 'back'; - const visibleLayersForPage = isFrontPage ? frontVisibleLayers() : backVisibleLayers(); - - return ( - - +
+ + {(page) => { + // 根据页面类型(正面/背面)决定使用哪个图层配置 + const isFrontPage = page.cards[0]?.side !== "back"; + const visibleLayersForPage = isFrontPage + ? frontVisibleLayers() + : backVisibleLayers(); - - {(line) => ( - <> - - - - )} - + return ( + + - - {(line) => ( - <> - - - - )} - + + {(line) => ( + <> + + + + )} + - - {(card) => { - const cardWidth = store.state.dimensions?.cardWidth || 56; - const cardHeight = store.state.dimensions?.cardHeight || 88; - const clipPathId = `clip-${page.pageIndex}-${card.data.id || card.x}-${card.y}`; - const shapeClipPath = getShapeSvgClipPath(clipPathId, cardWidth, cardHeight, store.state.shape, store.state.cornerRadius); + + {(line) => ( + <> + + + + )} + - return ( - - - {shapeClipPath} - - -
-
+ {(card) => { + const cardWidth = + store.state.dimensions?.cardWidth || 56; + const cardHeight = + store.state.dimensions?.cardHeight || 88; + const clipPathId = `clip-${page.pageIndex}-${card.data.id || card.x}-${card.y}`; + const shapeClipPath = getShapeSvgClipPath( + clipPathId, + cardWidth, + cardHeight, + store.state.shape, + store.state.cornerRadius, + ); + + return ( + + + {shapeClipPath} + + - -
-
-
-
- ); - }} -
-
- ); - }} -
+
+
+ +
+
+ + + ); + }} + + + ); + }} + +
+
-
- -
+
); } diff --git a/src/components/md-deck/PrintPreviewHeader.tsx b/src/components/md-deck/PrintPreviewHeader.tsx index 196230c..f06a4dd 100644 --- a/src/components/md-deck/PrintPreviewHeader.tsx +++ b/src/components/md-deck/PrintPreviewHeader.tsx @@ -1,8 +1,14 @@ -import type { DeckStore } from './hooks/deckStore'; +import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; +import type { DeckStore } from "./hooks/deckStore"; +import { getCompatibleDecks } from "./hooks/deck-registry"; export interface PrintPreviewHeaderProps { store: DeckStore; + deckId: string; pageCount: number; + cardCount: number; + selectedDeckIds: string[]; + onToggleDeck: (id: string) => void; onExport: () => void; onOpenPltPreview: () => void; onClose: () => void; @@ -15,51 +21,164 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) { const frontOddPageOffsetY = () => store.state.printFrontOddPageOffsetY; const doubleSided = () => store.state.printDoubleSided; + const [showAddDeck, setShowAddDeck] = createSignal(false); + let addDeckRef: HTMLDivElement | undefined; + + // 点击外部关闭下拉菜单 + const handleClickOutside = (e: MouseEvent) => { + if (addDeckRef && !addDeckRef.contains(e.target as Node)) { + setShowAddDeck(false); + } + }; + + onMount(() => { + document.addEventListener("click", handleClickOutside); + }); + + onCleanup(() => { + document.removeEventListener("click", handleClickOutside); + }); + + const compatibleDecks = () => + getCompatibleDecks( + store.state.sizeW, + store.state.sizeH, + store.state.bleed, + props.deckId, + ); + + const mergedDeckCount = () => props.selectedDeckIds.length + 1; + return (

打印预览

-

共 {props.pageCount} 页,{store.state.cards.length} 张卡牌

+

+ 共 {props.pageCount} 页,{props.cardCount} 张卡牌 + 1}> + (来自 {mergedDeckCount()} 个牌组) + +

+ {/* Add Deck button */} +
+ + + +
+
+

+ 选择要合并打印的牌组(相同尺寸 {store.state.sizeW}× + {store.state.sizeH}mm) +

+
+
+ + {(entry) => { + const isSelected = () => + props.selectedDeckIds.includes(entry.id); + const cardCount = () => entry.store.state.cards.length; + + return ( + + ); + }} + +
+ +
+ 没有找到相同尺寸的其他牌组 +
+
+
+ +
+
+
+
+
- +
- +
@@ -67,7 +186,11 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) { store.actions.setPrintFrontOddPageOffsetX(Number(e.target.value))} + onChange={(e) => + store.actions.setPrintFrontOddPageOffsetX( + Number(e.target.value), + ) + } class="w-16 px-2 py-1 border border-gray-300 rounded text-sm" step="0.1" /> @@ -78,7 +201,11 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) { store.actions.setPrintFrontOddPageOffsetY(Number(e.target.value))} + onChange={(e) => + store.actions.setPrintFrontOddPageOffsetY( + Number(e.target.value), + ) + } class="w-16 px-2 py-1 border border-gray-300 rounded text-sm" step="0.1" /> diff --git a/src/components/md-deck/hooks/deck-registry.ts b/src/components/md-deck/hooks/deck-registry.ts new file mode 100644 index 0000000..f155445 --- /dev/null +++ b/src/components/md-deck/hooks/deck-registry.ts @@ -0,0 +1,99 @@ +import { createSignal } from 'solid-js'; +import type { DeckStore } from './deckStore'; + +export interface DeckRegistryEntry { + id: string; + store: DeckStore; + path: string; + label?: string; +} + +/** + * 全局卡牌组注册表 + * 收集页面上所有已加载的 :md-deck 实例,用于跨牌组合并打印。 + */ +const [registry, setRegistry] = createSignal>(new Map()); + +function getRegistry(): Map { + return registry(); +} + +function updateRegistry(fn: (map: Map) => void) { + const newMap = new Map(registry()); + fn(newMap); + setRegistry(newMap); +} + +/** + * 注册一个卡牌组实例 + */ +export function registerDeck(id: string, store: DeckStore, path: string, label?: string): void { + updateRegistry((map) => { + map.set(id, { id, store, path, label }); + }); +} + +/** + * 注销一个卡牌组实例 + */ +export function unregisterDeck(id: string): void { + updateRegistry((map) => { + map.delete(id); + }); +} + +/** + * 获取与指定尺寸兼容的其他卡牌组 + * @param sizeW 卡牌宽度 (mm) + * @param sizeH 卡牌高度 (mm) + * @param bleed 出血 (mm) + * @param excludeId 排除的卡牌组 ID(通常是调用者自身) + * @returns 兼容的卡牌组列表 + */ +export function getCompatibleDecks( + sizeW: number, + sizeH: number, + bleed: number, + excludeId?: string +): DeckRegistryEntry[] { + const result: DeckRegistryEntry[] = []; + const map = getRegistry(); + + for (const [id, entry] of map) { + if (id === excludeId) continue; + + const s = entry.store.state; + // 必须尺寸和出血完全一致才算兼容 + if (s.sizeW === sizeW && s.sizeH === sizeH && s.bleed === bleed) { + result.push(entry); + } + } + + return result; +} + +/** + * 根据 ID 获取注册表项 + */ +export function getDeckById(id: string): DeckRegistryEntry | undefined { + return getRegistry().get(id); +} + +/** + * 获取所有已注册的卡牌组 + */ +export function getAllDecks(): DeckRegistryEntry[] { + return Array.from(getRegistry().values()); +} + +/** + * 响应式获取兼容卡牌组列表(用于在组件中使用) + */ +export function useCompatibleDecks( + sizeW: () => number, + sizeH: () => number, + bleed: () => number, + excludeId?: string +) { + return () => getCompatibleDecks(sizeW(), sizeH(), bleed(), excludeId); +} diff --git a/src/components/md-deck/hooks/usePageLayout.ts b/src/components/md-deck/hooks/usePageLayout.ts index eabcf41..a2e99ba 100644 --- a/src/components/md-deck/hooks/usePageLayout.ts +++ b/src/components/md-deck/hooks/usePageLayout.ts @@ -1,6 +1,7 @@ -import { createMemo } from 'solid-js'; -import type { DeckStore } from './deckStore'; -import type { PageData, CropMarkData } from './usePDFExport'; +import { createMemo } from "solid-js"; +import type { DeckStore } from "./deckStore"; +import type { PageData, CropMarkData } from "./usePDFExport"; +import type { CardData } from "../types"; export interface A4Size { width: number; @@ -21,22 +22,29 @@ const PRINT_MARGIN = 5; /** * 页面布局计算 hook + * @param store 主牌组的 store + * @param extraCards 可选的额外卡牌列表(用于合并打印) */ -export function usePageLayout(store: DeckStore): UsePageLayoutReturn { +export function usePageLayout( + store: DeckStore, + extraCards?: () => CardData[], +): UsePageLayoutReturn { const orientation = () => store.state.printOrientation; const doubleSided = () => store.state.printDoubleSided; const frontOddPageOffsetX = () => store.state.printFrontOddPageOffsetX; const frontOddPageOffsetY = () => store.state.printFrontOddPageOffsetY; const getA4Size = () => { - if (orientation() === 'landscape') { + if (orientation() === "landscape") { return { width: A4_WIDTH_LANDSCAPE, height: A4_HEIGHT_LANDSCAPE }; } return { width: A4_WIDTH_PORTRAIT, height: A4_HEIGHT_PORTRAIT }; }; const pages = createMemo(() => { - const cards = store.state.cards; + const baseCards = store.state.cards as CardData[]; + const extra = extraCards?.() ?? []; + const cards = [...baseCards, ...extra]; const cardWidth = store.state.dimensions?.cardWidth || 56; const cardHeight = store.state.dimensions?.cardHeight || 88; const { width: a4Width, height: a4Height } = getA4Size(); @@ -68,8 +76,18 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { result.push({ pageIndex: result.length, cards: [], - bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } + bounds: { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + }, + frameBounds: { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + }, }); } @@ -78,7 +96,10 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { // 计算当前正面页的卡牌范围 const startCardIndex = pageIndex * cardsPerPage; - const endCardIndex = Math.min(startCardIndex + cardsPerPage, totalCards); + const endCardIndex = Math.min( + startCardIndex + cardsPerPage, + totalCards, + ); for (let i = startCardIndex; i < endCardIndex; i++) { // 正面:正常顺序排列 @@ -92,30 +113,50 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { const frontX = baseOffsetX + col * cardWidth + pageOffsetX; const frontY = baseOffsetY + row * cardHeight + pageOffsetY; - frontPage.cards.push({ data: cards[i], x: frontX, y: frontY, side: 'front' as const }); + frontPage.cards.push({ + data: cards[i], + x: frontX, + y: frontY, + side: "front" as const, + }); frontPage.bounds.minX = Math.min(frontPage.bounds.minX, frontX); frontPage.bounds.minY = Math.min(frontPage.bounds.minY, frontY); - frontPage.bounds.maxX = Math.max(frontPage.bounds.maxX, frontX + cardWidth); - frontPage.bounds.maxY = Math.max(frontPage.bounds.maxY, frontY + cardHeight); + frontPage.bounds.maxX = Math.max( + frontPage.bounds.maxX, + frontX + cardWidth, + ); + frontPage.bounds.maxY = Math.max( + frontPage.bounds.maxY, + frontY + cardHeight, + ); // 背面:逆转顺序排列(长边方向) // 对于竖向打印,长边是垂直方向,所以逆转行 // 对于横向打印,长边是水平方向,所以逆转列 - const backRow = orientation() === 'portrait' - ? (rowsPerPage - 1 - row) - : row; - const backCol = orientation() === 'portrait' - ? col - : (cardsPerRow - 1 - col); + const backRow = + orientation() === "portrait" ? rowsPerPage - 1 - row : row; + const backCol = + orientation() === "portrait" ? col : cardsPerRow - 1 - col; const backX = baseOffsetX + backCol * cardWidth; const backY = baseOffsetY + backRow * cardHeight; - backPage.cards.push({ data: cards[i], x: backX, y: backY, side: 'back' as const }); + backPage.cards.push({ + data: cards[i], + x: backX, + y: backY, + side: "back" as const, + }); backPage.bounds.minX = Math.min(backPage.bounds.minX, backX); backPage.bounds.minY = Math.min(backPage.bounds.minY, backY); - backPage.bounds.maxX = Math.max(backPage.bounds.maxX, backX + cardWidth); - backPage.bounds.maxY = Math.max(backPage.bounds.maxY, backY + cardHeight); + backPage.bounds.maxX = Math.max( + backPage.bounds.maxX, + backX + cardWidth, + ); + backPage.bounds.maxY = Math.max( + backPage.bounds.maxY, + backY + cardHeight, + ); } } } else { @@ -123,8 +164,18 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { let currentPage: PageData = { pageIndex: 0, cards: [], - bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } + bounds: { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + }, + frameBounds: { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + }, }; for (let i = 0; i < cards.length; i++) { @@ -138,8 +189,18 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { currentPage = { pageIndex, cards: [], - bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } + bounds: { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + }, + frameBounds: { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + }, }; } @@ -150,11 +211,22 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { const cardX = baseOffsetX + col * cardWidth + pageOffsetX; const cardY = baseOffsetY + row * cardHeight + pageOffsetY; - currentPage.cards.push({ data: cards[i], x: cardX, y: cardY, side: 'front' as const }); + currentPage.cards.push({ + data: cards[i], + x: cardX, + y: cardY, + side: "front" as const, + }); currentPage.bounds.minX = Math.min(currentPage.bounds.minX, cardX); currentPage.bounds.minY = Math.min(currentPage.bounds.minY, cardY); - currentPage.bounds.maxX = Math.max(currentPage.bounds.maxX, cardX + cardWidth); - currentPage.bounds.maxY = Math.max(currentPage.bounds.maxY, cardY + cardHeight); + currentPage.bounds.maxX = Math.max( + currentPage.bounds.maxX, + cardX + cardWidth, + ); + currentPage.bounds.maxY = Math.max( + currentPage.bounds.maxY, + cardY + cardHeight, + ); } if (currentPage.cards.length > 0) { @@ -162,25 +234,27 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { } } - return result.map(page => { - const offsetX = doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetX() : 0; - const offsetY = doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetY() : 0; - + return result.map((page) => { + const offsetX = + doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetX() : 0; + const offsetY = + doubleSided() && page.pageIndex % 2 === 0 ? frontOddPageOffsetY() : 0; + return { ...page, frameBounds: { minX: baseOffsetX + offsetX, minY: baseOffsetY + offsetY, maxX: baseOffsetX + maxGridWidth + offsetX, - maxY: baseOffsetY + maxGridHeight + offsetY - } + maxY: baseOffsetY + maxGridHeight + offsetY, + }, }; }); }); const cropMarks = createMemo(() => { const pagesData = pages(); - return pagesData.map(page => { + return pagesData.map((page) => { const { frameBounds, cards } = page; const cardWidth = store.state.dimensions?.cardWidth || 56; const cardHeight = store.state.dimensions?.cardHeight || 88; @@ -188,7 +262,7 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { const xPositions = new Set(); const yPositions = new Set(); - cards.forEach(card => { + cards.forEach((card) => { xPositions.add(card.x); xPositions.add(card.x + cardWidth); yPositions.add(card.y); @@ -200,26 +274,31 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { const OVERLAP = 3; - const horizontalLines = sortedY.map(y => ({ + const horizontalLines = sortedY.map((y) => ({ y, xStart: frameBounds.minX - OVERLAP, - xEnd: frameBounds.maxX + OVERLAP + xEnd: frameBounds.maxX + OVERLAP, })); - const verticalLines = sortedX.map(x => ({ + const verticalLines = sortedX.map((x) => ({ x, yStart: frameBounds.minY - OVERLAP, - yEnd: frameBounds.maxY + OVERLAP + yEnd: frameBounds.maxY + OVERLAP, })); const frameBoundsWithMargin = { x: frameBounds.minX - 1, y: frameBounds.minY - 1, width: frameBounds.maxX - frameBounds.minX + 2, - height: frameBounds.maxY - frameBounds.minY + 2 + height: frameBounds.maxY - frameBounds.minY + 2, }; - return { horizontalLines, verticalLines, frameBounds, frameBoundsWithMargin }; + return { + horizontalLines, + verticalLines, + frameBounds, + frameBoundsWithMargin, + }; }); }); diff --git a/src/components/md-deck/index.tsx b/src/components/md-deck/index.tsx index 808bd83..bd86b9e 100644 --- a/src/components/md-deck/index.tsx +++ b/src/components/md-deck/index.tsx @@ -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('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( + "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 ( -
- {/* 导出 PDF 预览弹窗 */} - - store.actions.setExporting(false)} - onExport={() => {}} - /> - + // 清理函数 + onCleanup(() => { + unregisterDeck(deckId); + store.actions.clearError(); + }); - {/* Tab 选择器和编辑按钮 */} - 0 && !store.state.error}> - - - -
- - {/* 内容区域:错误/加载/卡牌预览/空状态 */} - {/* 左侧:CSV 数据编辑 */} - {/**/} - {/* */} - {/**/} - - -
- -
+ return ( +
+ {/* 导出 PDF 预览弹窗 */} + + store.actions.setExporting(false)} + onExport={() => {}} + /> - - - {/* 右侧:属性/图层编辑面板 */} - -
- -
+ {/* Tab 选择器和编辑按钮 */} + 0 && !store.state.error}> + + +
+ {/* 内容区域:错误/加载/卡牌预览/空状态 */} + {/* 左侧:CSV 数据编辑 */} + {/**/} + {/* */} + {/**/} + + +
+ +
+
+ + + + {/* 右侧:属性/图层编辑面板 */} + +
+ +
+
+
-
- ); -}); + ); + }, +);