From b3dc7687866eeeb117a3dc96a9ae0e1a9dc3704b Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 13 Mar 2026 17:26:00 +0800 Subject: [PATCH] feat: card back support? --- src/components/md-deck/CardLayer.tsx | 12 +- src/components/md-deck/CardPreview.tsx | 1 + src/components/md-deck/PrintPreview.tsx | 200 +++++++++--------- src/components/md-deck/PrintPreviewHeader.tsx | 28 ++- .../md-deck/editor-panel/LayerEditorPanel.tsx | 52 +++-- .../editor-panel/PropertiesEditorPanel.tsx | 28 ++- src/components/md-deck/hooks/deckStore.ts | 143 ++++++++----- src/components/md-deck/hooks/layer-parser.ts | 18 +- src/components/md-deck/hooks/usePDFExport.ts | 3 +- src/components/md-deck/hooks/usePageLayout.ts | 156 ++++++++++---- src/components/md-deck/index.tsx | 9 +- src/components/md-deck/types.ts | 2 + 12 files changed, 430 insertions(+), 222 deletions(-) diff --git a/src/components/md-deck/CardLayer.tsx b/src/components/md-deck/CardLayer.tsx index 27b49b7..918216c 100644 --- a/src/components/md-deck/CardLayer.tsx +++ b/src/components/md-deck/CardLayer.tsx @@ -1,7 +1,7 @@ import {createMemo, For} from 'solid-js'; import {parseMarkdown} from '../../markdown'; import { getLayerStyle } from './hooks/dimensions'; -import type { CardData } from './types'; +import type { CardData, CardSide } from './types'; import {DeckStore} from "./hooks/deckStore"; import {processVariables} from "../utils/csv-loader"; import {resolvePath} from "../utils/path"; @@ -9,13 +9,19 @@ import {resolvePath} from "../utils/path"; export interface CardLayerProps { cardData: CardData; store: DeckStore; + side?: CardSide; } export function CardLayer(props: CardLayerProps) { - const layers = createMemo(() => props.store.state.layerConfigs.filter((l) => l.visible)); + const side = () => props.side || 'front'; + const layers = createMemo(() => + side() === 'front' + ? props.store.state.frontLayerConfigs.filter((l) => l.visible) + : props.store.state.backLayerConfigs.filter((l) => l.visible) + ); const dimensions = () => props.store.state.dimensions!; const showBounds = () => props.store.state.isEditing; - + function renderLayerContent(content: string) { const iconPath = resolvePath(props.store.state.cards.sourcePath, props.cardData.iconPath); return parseMarkdown(processVariables(content, props.cardData, props.store.state.cards), iconPath) as string; diff --git a/src/components/md-deck/CardPreview.tsx b/src/components/md-deck/CardPreview.tsx index 568dbce..52aea35 100644 --- a/src/components/md-deck/CardPreview.tsx +++ b/src/components/md-deck/CardPreview.tsx @@ -79,6 +79,7 @@ export function CardPreview(props: CardPreviewProps) { diff --git a/src/components/md-deck/PrintPreview.tsx b/src/components/md-deck/PrintPreview.tsx index 1ad6a9a..d575179 100644 --- a/src/components/md-deck/PrintPreview.tsx +++ b/src/components/md-deck/PrintPreview.tsx @@ -20,7 +20,8 @@ export function PrintPreview(props: PrintPreviewProps) { const { getA4Size, pages, cropMarks } = usePageLayout(store); const { exportToPDF } = usePDFExport(store, props.onClose); - const visibleLayers = () => store.state.layerConfigs.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 = { @@ -31,7 +32,7 @@ export function PrintPreview(props: PrintPreviewProps) { gridOriginY: store.state.dimensions?.gridOriginY || 0, gridAreaWidth: store.state.dimensions?.gridAreaWidth || 56, gridAreaHeight: store.state.dimensions?.gridAreaHeight || 88, - visibleLayers: visibleLayers(), + visibleLayers: frontVisibleLayers(), dimensions: store.state.dimensions! }; await exportToPDF(pages(), cropMarks(), options); @@ -51,103 +52,112 @@ export function PrintPreview(props: PrintPreviewProps) {
- {(page) => ( - - + {(page) => { + // 根据页面类型(正面/背面)决定使用哪个图层配置 + const isFrontPage = page.cards[0]?.side !== 'back'; + const visibleLayersForPage = isFrontPage ? frontVisibleLayers() : backVisibleLayers(); + + return ( + + - - {(line) => ( - <> - - - - )} - + + {(line) => ( + <> + + + + )} + - - {(line) => ( - <> - - - - )} - + + {(line) => ( + <> + + + + )} + - - {(card) => ( - - -
-
- + + {(card) => ( + + +
+
+ +
-
- - - )} - - - )} + + + )} + + + ); + }}
diff --git a/src/components/md-deck/PrintPreviewHeader.tsx b/src/components/md-deck/PrintPreviewHeader.tsx index 7aa5f40..16294c1 100644 --- a/src/components/md-deck/PrintPreviewHeader.tsx +++ b/src/components/md-deck/PrintPreviewHeader.tsx @@ -10,8 +10,9 @@ export interface PrintPreviewHeaderProps { export function PrintPreviewHeader(props: PrintPreviewHeaderProps) { const { store } = props; const orientation = () => store.state.printOrientation; - const oddPageOffsetX = () => store.state.printOddPageOffsetX; - const oddPageOffsetY = () => store.state.printOddPageOffsetY; + const frontOddPageOffsetX = () => store.state.printFrontOddPageOffsetX; + const frontOddPageOffsetY = () => store.state.printFrontOddPageOffsetY; + const doubleSided = () => store.state.printDoubleSided; return (
@@ -45,14 +46,27 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) {
+
- + +
+ +
+
X: store.actions.setPrintOddPageOffsetX(Number(e.target.value))} + value={frontOddPageOffsetX()} + 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" /> @@ -62,8 +76,8 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) { Y: store.actions.setPrintOddPageOffsetY(Number(e.target.value))} + value={frontOddPageOffsetY()} + 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/editor-panel/LayerEditorPanel.tsx b/src/components/md-deck/editor-panel/LayerEditorPanel.tsx index 08f932c..036e45f 100644 --- a/src/components/md-deck/editor-panel/LayerEditorPanel.tsx +++ b/src/components/md-deck/editor-panel/LayerEditorPanel.tsx @@ -18,33 +18,54 @@ const ORIENTATION_OPTIONS = [ export function LayerEditorPanel(props: LayerEditorPanelProps) { const { store } = props; + // 根据当前激活的面获取图层配置 + const currentLayerConfigs = () => + store.state.activeSide === 'front' + ? store.state.frontLayerConfigs + : store.state.backLayerConfigs; + const updateLayerOrientation = (layerProp: string, orientation: 'n' | 's' | 'e' | 'w') => { - const layer = store.state.layerConfigs.find(l => l.prop === layerProp); - if (layer) { - store.actions.updateLayerConfig(layerProp, { ...layer, orientation }); - } + const updateFn = store.state.activeSide === 'front' + ? store.actions.updateFrontLayerConfig + : store.actions.updateBackLayerConfig; + updateFn(layerProp, { orientation }); }; const updateLayerFontSize = (layerProp: string, fontSize?: number) => { - const layer = store.state.layerConfigs.find(l => l.prop === layerProp); - if (layer) { - store.actions.updateLayerConfig(layerProp, { ...layer, fontSize }); - } + const updateFn = store.state.activeSide === 'front' + ? store.actions.updateFrontLayerConfig + : store.actions.updateBackLayerConfig; + updateFn(layerProp, { fontSize }); + }; + + const toggleLayerVisible = (layerProp: string) => { + const toggleFn = store.state.activeSide === 'front' + ? store.actions.toggleFrontLayerVisible + : store.actions.toggleBackLayerVisible; + toggleFn(layerProp); + }; + + const setEditingLayer = (layerProp: string) => { + store.actions.setEditingLayer( + store.state.editingLayer === layerProp ? null : layerProp + ); }; return (
-

图层

+

+ 图层 ({store.state.activeSide === 'front' ? '正面' : '背面'}) +

- + {(layer) => (
store.actions.toggleLayerVisible(layer.prop)} + onChange={() => toggleLayerVisible(layer.prop)} class="cursor-pointer" /> {layer.prop} @@ -52,7 +73,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) { {layer.visible && ( <>
); diff --git a/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx b/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx index 8cd7f5e..4298b26 100644 --- a/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx +++ b/src/components/md-deck/editor-panel/PropertiesEditorPanel.tsx @@ -5,7 +5,7 @@ export interface PropertiesEditorPanelProps { } /** - * 卡牌属性编辑面板:尺寸、网格、出血、内边距 + * 卡牌属性编辑面板:尺寸、网格、出血、内边距、正背面切换 */ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) { const { store } = props; @@ -14,6 +14,32 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {

卡牌属性

+ {/* 正面/背面切换标签页 */} +
+
+ + +
+
+
diff --git a/src/components/md-deck/hooks/deckStore.ts b/src/components/md-deck/hooks/deckStore.ts index 7879aed..db8b0a8 100644 --- a/src/components/md-deck/hooks/deckStore.ts +++ b/src/components/md-deck/hooks/deckStore.ts @@ -1,8 +1,8 @@ import { createStore } from 'solid-js/store'; import { calculateDimensions } from './dimensions'; import { loadCSV, CSV } from '../../utils/csv-loader'; -import { initLayerConfigs, formatLayers } from './layer-parser'; -import type { CardData, LayerConfig, Dimensions } from '../types'; +import { initLayerConfigs, formatLayers, initLayerConfigsForSide } from './layer-parser'; +import type { CardData, LayerConfig, Dimensions, CardSide } from '../types'; /** * 默认配置常量 @@ -36,11 +36,13 @@ export interface DeckState { activeTab: number; // 图层配置 - layerConfigs: LayerConfig[]; + frontLayerConfigs: LayerConfig[]; + backLayerConfigs: LayerConfig[]; // 编辑状态 isEditing: boolean; editingLayer: string | null; + activeSide: CardSide; // 框选状态 isSelecting: boolean; @@ -60,8 +62,9 @@ export interface DeckState { // 打印设置 printOrientation: 'portrait' | 'landscape'; - printOddPageOffsetX: number; - printOddPageOffsetY: number; + printFrontOddPageOffsetX: number; + printFrontOddPageOffsetY: number; + printDoubleSided: boolean; } export interface DeckActions { @@ -78,15 +81,21 @@ export interface DeckActions { setActiveTab: (index: number) => void; updateCardData: (index: number, key: string, value: string) => void; - // 图层操作 - setLayerConfigs: (configs: LayerConfig[]) => void; - updateLayerConfig: (prop: string, updates: Partial) => void; - toggleLayerVisible: (prop: string) => void; + // 图层操作 - 正面 + setFrontLayerConfigs: (configs: LayerConfig[]) => void; + updateFrontLayerConfig: (prop: string, updates: Partial) => void; + toggleFrontLayerVisible: (prop: string) => void; + + // 图层操作 - 背面 + setBackLayerConfigs: (configs: LayerConfig[]) => void; + updateBackLayerConfig: (prop: string, updates: Partial) => void; + toggleBackLayerVisible: (prop: string) => void; // 编辑状态 setIsEditing: (editing: boolean) => void; setEditingLayer: (layer: string | null) => void; updateLayerPosition: (x1: number, y1: number, x2: number, y2: number) => void; + setActiveSide: (side: CardSide) => void; // 框选操作 setIsSelecting: (selecting: boolean) => void; @@ -95,13 +104,13 @@ export interface DeckActions { cancelSelection: () => void; // 数据加载 - loadCardsFromPath: (path: string, rawSrc: string, layersStr?: string) => Promise; + loadCardsFromPath: (path: string, rawSrc: string, layersStr?: string, backLayersStr?: string) => Promise; setError: (error: string | null) => void; clearError: () => void; // 生成代码 - generateCode: () => string; - copyCode: () => Promise; + generateCode: (backLayersStr?: string) => string; + copyCode: (backLayersStr?: string) => Promise; // 导出操作 setExporting: (exporting: boolean) => void; @@ -112,8 +121,9 @@ export interface DeckActions { // 打印设置 setPrintOrientation: (orientation: 'portrait' | 'landscape') => void; - setPrintOddPageOffsetX: (offset: number) => void; - setPrintOddPageOffsetY: (offset: number) => void; + setPrintFrontOddPageOffsetX: (offset: number) => void; + setPrintFrontOddPageOffsetY: (offset: number) => void; + setPrintDoubleSided: (doubleSided: boolean) => void; } export interface DeckStore { @@ -141,9 +151,11 @@ export function createDeckStore( dimensions: null, cards: [] as any, activeTab: 0, - layerConfigs: [], + frontLayerConfigs: [], + backLayerConfigs: [], isEditing: false, editingLayer: null, + activeSide: 'front', isSelecting: false, selectStart: null, selectEnd: null, @@ -153,8 +165,9 @@ export function createDeckStore( exportProgress: 0, exportError: null, printOrientation: 'portrait', - printOddPageOffsetX: 0, - printOddPageOffsetY: 0 + printFrontOddPageOffsetX: 0, + printFrontOddPageOffsetY: 0, + printDoubleSided: false }); // 更新尺寸并重新计算 dimensions @@ -201,22 +214,39 @@ export function createDeckStore( setState('cards', index, key, value); }; - const setLayerConfigs = (configs: LayerConfig[]) => setState({ layerConfigs: configs }); - const updateLayerConfig = (prop: string, updates: Partial) => { - setState('layerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config)); + // 正面图层操作 + const setFrontLayerConfigs = (configs: LayerConfig[]) => setState({ frontLayerConfigs: configs }); + const updateFrontLayerConfig = (prop: string, updates: Partial) => { + setState('frontLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config)); }; - const toggleLayerVisible = (prop: string) => { - setState('layerConfigs', (prev) => prev.map((config) => + const toggleFrontLayerVisible = (prop: string) => { + setState('frontLayerConfigs', (prev) => prev.map((config) => + config.prop === prop ? { ...config, visible: !config.visible } : config + )); + }; + + // 背面图层操作 + const setBackLayerConfigs = (configs: LayerConfig[]) => setState({ backLayerConfigs: configs }); + const updateBackLayerConfig = (prop: string, updates: Partial) => { + setState('backLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config)); + }; + const toggleBackLayerVisible = (prop: string) => { + setState('backLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, visible: !config.visible } : config )); }; const setIsEditing = (editing: boolean) => setState({ isEditing: editing }); const setEditingLayer = (layer: string | null) => setState({ editingLayer: layer }); + const setActiveSide = (side: CardSide) => setState({ activeSide: side }); + const updateLayerPosition = (x1: number, y1: number, x2: number, y2: number) => { const layer = state.editingLayer; if (!layer) return; - setState('layerConfigs', (prev) => prev.map((config) => + const currentSide = state.activeSide; + const configs = currentSide === 'front' ? state.frontLayerConfigs : state.backLayerConfigs; + const setter = currentSide === 'front' ? setFrontLayerConfigs : setBackLayerConfigs; + setter(configs.map((config) => config.prop === layer ? { ...config, x1, y1, x2, y2 } : config )); setState({ editingLayer: null }); @@ -230,7 +260,7 @@ export function createDeckStore( }; // 加载卡牌数据(核心逻辑) - const loadCardsFromPath = async (path: string, rawSrc: string, layersStr: string = '') => { + const loadCardsFromPath = async (path: string, rawSrc: string, layersStr: string = '', backLayersStr: string = '') => { if (!path) { setState({ error: '未指定 CSV 文件路径' }); return; @@ -240,26 +270,27 @@ export function createDeckStore( try { const data = await loadCSV(path); - + if (data.length === 0) { - setState({ + setState({ error: 'CSV 文件为空或格式不正确', - isLoading: false + isLoading: false }); return; } - setState({ - cards: data, + setState({ + cards: data, activeTab: 0, - layerConfigs: initLayerConfigs(data, layersStr), - isLoading: false + frontLayerConfigs: initLayerConfigsForSide(data, layersStr), + backLayerConfigs: initLayerConfigsForSide(data, backLayersStr), + isLoading: false }); updateDimensions(); } catch (err) { - setState({ + setState({ error: `加载 CSV 失败:${err instanceof Error ? err.message : '未知错误'}`, - isLoading: false + isLoading: false }); } }; @@ -267,14 +298,15 @@ export function createDeckStore( const setError = (error: string | null) => setState({ error }); const clearError = () => setState({ error: null }); - const generateCode = () => { - const layersStr = formatLayers(state.layerConfigs); + const generateCode = (backLayersStr?: string) => { + const frontLayersStr = formatLayers(state.frontLayerConfigs); + const backLayersString = backLayersStr || formatLayers(state.backLayerConfigs); const parts = [ `:md-deck[${state.rawSrc || state.src}]`, `{size="${state.sizeW}x${state.sizeH} "`, `grid="${state.gridW}x${state.gridH} "` ]; - + // 仅在非默认值时添加 bleed 和 padding if (state.bleed !== DECK_DEFAULTS.BLEED) { parts.push(`bleed="${state.bleed} "`); @@ -282,13 +314,17 @@ export function createDeckStore( if (state.padding !== DECK_DEFAULTS.PADDING) { parts.push(`padding="${state.padding} "`); } - - parts.push(`layers="${layersStr}"}`); + + parts.push(`layers="${frontLayersStr}"`); + if (backLayersString) { + parts.push(` backLayers="${backLayersString}"`); + } + parts.push('}'); return parts.join(''); }; - const copyCode = async () => { - const code = generateCode(); + const copyCode = async (backLayersStr?: string) => { + const code = generateCode(backLayersStr); try { await navigator.clipboard.writeText(code); alert('已复制到剪贴板!'); @@ -314,12 +350,16 @@ export function createDeckStore( setState({ printOrientation: orientation }); }; - const setPrintOddPageOffsetX = (offset: number) => { - setState({ printOddPageOffsetX: offset }); + const setPrintFrontOddPageOffsetX = (offset: number) => { + setState({ printFrontOddPageOffsetX: offset }); }; - const setPrintOddPageOffsetY = (offset: number) => { - setState({ printOddPageOffsetY: offset }); + const setPrintFrontOddPageOffsetY = (offset: number) => { + setState({ printFrontOddPageOffsetY: offset }); + }; + + const setPrintDoubleSided = (doubleSided: boolean) => { + setState({ printDoubleSided: doubleSided }); }; const actions: DeckActions = { @@ -332,12 +372,16 @@ export function createDeckStore( setCards, setActiveTab, updateCardData, - setLayerConfigs, - updateLayerConfig, - toggleLayerVisible, + setFrontLayerConfigs, + updateFrontLayerConfig, + toggleFrontLayerVisible, + setBackLayerConfigs, + updateBackLayerConfig, + toggleBackLayerVisible, setIsEditing, setEditingLayer, updateLayerPosition, + setActiveSide, setIsSelecting, setSelectStart, setSelectEnd, @@ -353,8 +397,9 @@ export function createDeckStore( setExportError, clearExportError, setPrintOrientation, - setPrintOddPageOffsetX, - setPrintOddPageOffsetY + setPrintFrontOddPageOffsetX, + setPrintFrontOddPageOffsetY, + setPrintDoubleSided }; return { state, actions }; diff --git a/src/components/md-deck/hooks/layer-parser.ts b/src/components/md-deck/hooks/layer-parser.ts index d0ff1a9..96dfe87 100644 --- a/src/components/md-deck/hooks/layer-parser.ts +++ b/src/components/md-deck/hooks/layer-parser.ts @@ -49,13 +49,13 @@ export function formatLayers(layers: LayerConfig[]): string { } /** - * 初始化图层配置 + * 初始化图层配置(用于特定面) */ -export function initLayerConfigs( +export function initLayerConfigsForSide( data: CSV, - existingLayersStr: string + layersStr: string ): LayerConfig[] { - const parsed = parseLayers(existingLayersStr); + const parsed = parseLayers(layersStr); const allProps = Object.keys(data[0] || {}).filter(k => k !== 'label'); return allProps.map(prop => { @@ -72,3 +72,13 @@ export function initLayerConfigs( }; }); } + +/** + * 初始化图层配置(向后兼容) + */ +export function initLayerConfigs( + data: CSV, + existingLayersStr: string +): LayerConfig[] { + return initLayerConfigsForSide(data, existingLayersStr); +} diff --git a/src/components/md-deck/hooks/usePDFExport.ts b/src/components/md-deck/hooks/usePDFExport.ts index 0c477e4..2353b0b 100644 --- a/src/components/md-deck/hooks/usePDFExport.ts +++ b/src/components/md-deck/hooks/usePDFExport.ts @@ -1,10 +1,11 @@ import type { DeckStore } from './deckStore'; -import type { CardData, LayerConfig, Dimensions } from '../types'; +import type { CardData, LayerConfig, Dimensions, CardSide } from '../types'; export interface PageCard { data: CardData; x: number; y: number; + side?: CardSide; } export interface PageData { diff --git a/src/components/md-deck/hooks/usePageLayout.ts b/src/components/md-deck/hooks/usePageLayout.ts index 27ffd37..940ffd4 100644 --- a/src/components/md-deck/hooks/usePageLayout.ts +++ b/src/components/md-deck/hooks/usePageLayout.ts @@ -24,8 +24,9 @@ const PRINT_MARGIN = 5; */ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { const orientation = () => store.state.printOrientation; - const oddPageOffsetX = () => store.state.printOddPageOffsetX; - const oddPageOffsetY = () => store.state.printOddPageOffsetY; + const doubleSided = () => store.state.printDoubleSided; + const frontOddPageOffsetX = () => store.state.printFrontOddPageOffsetX; + const frontOddPageOffsetY = () => store.state.printFrontOddPageOffsetY; const getA4Size = () => { if (orientation() === 'landscape') { @@ -52,56 +53,119 @@ export function usePageLayout(store: DeckStore): UsePageLayoutReturn { const baseOffsetY = (a4Height - maxGridHeight) / 2; const result: PageData[] = []; - let currentPage: PageData = { - pageIndex: 0, - cards: [], - bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } - }; + + if (doubleSided()) { + // 双面打印模式:每张卡牌需要 2 页(正面 + 背面) + // 背面卡牌顺序在长边方向上逆转 + const totalCards = cards.length; + + for (let i = 0; i < totalCards; i++) { + const frontPageIndex = i * 2; + const backPageIndex = i * 2 + 1; + + // 确保页面数组有足够长度 + while (result.length <= backPageIndex) { + result.push({ + pageIndex: result.length, + cards: [], + bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, + frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } + }); + } + + const frontPage = result[frontPageIndex]; + const backPage = result[backPageIndex]; + + // 正面:正常顺序排列 + const frontRow = Math.floor(i / cardsPerRow); + const frontCol = i % cardsPerRow; + const frontX = baseOffsetX + frontCol * cardWidth + frontOddPageOffsetX(); + const frontY = baseOffsetY + frontRow * cardHeight + frontOddPageOffsetY(); + + 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); + + // 背面:逆转顺序排列(长边方向) + // 对于竖向打印,长边是垂直方向,所以逆转行 + // 对于横向打印,长边是水平方向,所以逆转列 + const backIndex = totalCards - 1 - i; + const backRow = orientation() === 'portrait' + ? Math.floor(backIndex / cardsPerRow) + : Math.floor(i / cardsPerRow); + const backCol = orientation() === 'portrait' + ? backIndex % cardsPerRow + : (cardsPerRow - 1 - (i % cardsPerRow)); + + 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.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); + } + } else { + // 单面打印模式:原有逻辑 + let currentPage: PageData = { + pageIndex: 0, + cards: [], + 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++) { - const pageIndex = Math.floor(i / cardsPerPage); - const indexInPage = i % cardsPerPage; - const row = Math.floor(indexInPage / cardsPerRow); - const col = indexInPage % cardsPerRow; + for (let i = 0; i < cards.length; i++) { + const pageIndex = Math.floor(i / cardsPerPage); + const indexInPage = i % cardsPerPage; + const row = Math.floor(indexInPage / cardsPerRow); + const col = indexInPage % cardsPerRow; - if (pageIndex !== currentPage.pageIndex) { + if (pageIndex !== currentPage.pageIndex) { + result.push(currentPage); + currentPage = { + pageIndex, + cards: [], + bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, + frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } + }; + } + + const isOddPage = pageIndex % 2 === 0; + const pageOffsetX = isOddPage ? frontOddPageOffsetX() : 0; + const pageOffsetY = isOddPage ? frontOddPageOffsetY() : 0; + + 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.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); + } + + if (currentPage.cards.length > 0) { result.push(currentPage); - currentPage = { - pageIndex, - cards: [], - bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - frameBounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } - }; } - - const isOddPage = pageIndex % 2 === 0; - const pageOffsetX = isOddPage ? oddPageOffsetX() : 0; - const pageOffsetY = isOddPage ? oddPageOffsetY() : 0; - - const cardX = baseOffsetX + col * cardWidth + pageOffsetX; - const cardY = baseOffsetY + row * cardHeight + pageOffsetY; - - currentPage.cards.push({ data: cards[i], x: cardX, y: cardY }); - 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); } - if (currentPage.cards.length > 0) { - result.push(currentPage); - } - - return result.map(page => ({ - ...page, - frameBounds: { - minX: baseOffsetX + (page.pageIndex % 2 === 0 ? oddPageOffsetX() : 0), - minY: baseOffsetY + (page.pageIndex % 2 === 0 ? oddPageOffsetY() : 0), - maxX: baseOffsetX + maxGridWidth + (page.pageIndex % 2 === 0 ? oddPageOffsetX() : 0), - maxY: baseOffsetY + maxGridHeight + (page.pageIndex % 2 === 0 ? oddPageOffsetY() : 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 + } + }; + }); }); const cropMarks = createMemo(() => { diff --git a/src/components/md-deck/index.tsx b/src/components/md-deck/index.tsx index 7a196e2..53a6eaa 100644 --- a/src/components/md-deck/index.tsx +++ b/src/components/md-deck/index.tsx @@ -17,6 +17,7 @@ interface DeckProps { bleed?: number | string; padding?: number | string; layers?: string; + backLayers?: string; fixed?: boolean | string; } @@ -30,6 +31,7 @@ customElement('md-deck', { bleed: 1, padding: 2, layers: '', + backLayers: '', fixed: false }, (props, { element }) => { noShadowDOM(); @@ -86,7 +88,12 @@ customElement('md-deck', { } // 加载 CSV 数据 - store.actions.loadCardsFromPath(resolvedSrc, csvPath, (props.layers as string) || ''); + store.actions.loadCardsFromPath( + resolvedSrc, + csvPath, + (props.layers as string) || '', + (props.backLayers as string) || '' + ); // 清理函数 onCleanup(() => { diff --git a/src/components/md-deck/types.ts b/src/components/md-deck/types.ts index 5dab4a4..660286a 100644 --- a/src/components/md-deck/types.ts +++ b/src/components/md-deck/types.ts @@ -2,6 +2,8 @@ export interface CardData { [key: string]: string; } +export type CardSide = 'front' | 'back'; + export interface Layer { prop: string; x1: number;