import { For, createMemo } from 'solid-js'; import { marked } from '../../markdown'; import { getLayerStyle } from './hooks/dimensions'; import type { DeckStore } from './hooks/deckStore'; export interface PrintPreviewProps { store: DeckStore; onClose: () => void; onPrint: () => void; } /** * 渲染 layer 内容 */ function renderLayerContent(layer: { prop: string }, cardData: { [key: string]: string }): string { const content = cardData[layer.prop] || ''; return marked.parse(content) as string; } /** * 打印预览组件:在 A4 纸张上排列所有卡牌 */ export function PrintPreview(props: PrintPreviewProps) { const { store } = props; // A4 纸张尺寸(mm):210 x 297 const A4_WIDTH_PORTRAIT = 210; const A4_HEIGHT_PORTRAIT = 297; const A4_WIDTH_LANDSCAPE = 297; const A4_HEIGHT_LANDSCAPE = 210; const PRINT_MARGIN = 5; // 打印边距 // 获取打印设置 const orientation = () => store.state.printOrientation; const oddPageOffsetX = () => store.state.printOddPageOffsetX; const oddPageOffsetY = () => store.state.printOddPageOffsetY; // 根据方向获取 A4 尺寸 const getA4Size = () => { if (orientation() === 'landscape') { return { width: A4_WIDTH_LANDSCAPE, height: A4_HEIGHT_LANDSCAPE }; } return { width: A4_WIDTH_PORTRAIT, height: A4_HEIGHT_PORTRAIT }; }; // 计算每张卡牌在 A4 纸上的位置(居中布局) const pages = createMemo(() => { const cards = store.state.cards; const cardWidth = store.state.dimensions?.cardWidth || 56; const cardHeight = store.state.dimensions?.cardHeight || 88; const { width: a4Width, height: a4Height } = getA4Size(); // 每行可容纳的卡牌数量 const usableWidth = a4Width - PRINT_MARGIN * 2; const cardsPerRow = Math.floor(usableWidth / cardWidth); // 每页可容纳的行数 const usableHeight = a4Height - PRINT_MARGIN * 2; const rowsPerPage = Math.floor(usableHeight / cardHeight); // 每页的卡牌数量 const cardsPerPage = cardsPerRow * rowsPerPage; // 计算最大卡牌区域的尺寸(用于居中和外围框) const maxGridWidth = cardsPerRow * cardWidth; const maxGridHeight = rowsPerPage * cardHeight; // 居中偏移量(使卡牌区域在 A4 纸上居中) const baseOffsetX = (a4Width - maxGridWidth) / 2; const baseOffsetY = (a4Height - maxGridHeight) / 2; // 分页 const result: { pageIndex: number; cards: Array<{ data: typeof cards[0]; x: number; y: number }>; bounds: { minX: number; minY: number; maxX: number; maxY: number }; }[] = []; let currentPage: typeof result[0] = { pageIndex: 0, cards: [], bounds: { 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; if (pageIndex !== currentPage.pageIndex) { result.push(currentPage); currentPage = { pageIndex, cards: [], bounds: { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } }; } // 奇数页应用偏移(pageIndex 从 0 开始,所以偶数索引是奇数页) 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 }); // 更新边界(含 1mm 边距) 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) } })); }); // 计算裁切线和外围框位置 const cropMarks = createMemo(() => { const pagesData = pages(); return pagesData.map(page => { const { frameBounds, cards } = page; const cardWidth = store.state.dimensions?.cardWidth || 56; const cardHeight = store.state.dimensions?.cardHeight || 88; // 收集所有唯一的裁切线位置 const xPositions = new Set(); const yPositions = new Set(); cards.forEach(card => { xPositions.add(card.x); xPositions.add(card.x + cardWidth); yPositions.add(card.y); yPositions.add(card.y + cardHeight); }); const sortedX = Array.from(xPositions).sort((a, b) => a - b); const sortedY = Array.from(yPositions).sort((a, b) => a - b); // 裁切线超出外围框的距离 const OVERLAP = 3; // 3mm // 生成水平裁切线(沿 Y 轴) const horizontalLines = sortedY.map(y => ({ y, xStart: frameBounds.minX - OVERLAP, xEnd: frameBounds.maxX + OVERLAP })); // 生成垂直裁切线(沿 X 轴) const verticalLines = sortedX.map(x => ({ x, yStart: frameBounds.minY - OVERLAP, yEnd: frameBounds.maxY + OVERLAP })); // 外围框边界(离卡牌区域边缘 1mm) const frameBoundsWithMargin = { x: frameBounds.minX - 1, y: frameBounds.minY - 1, width: frameBounds.maxX - frameBounds.minX + 2, height: frameBounds.maxY - frameBounds.minY + 2 }; return { horizontalLines, verticalLines, frameBounds, frameBoundsWithMargin }; }); }); const visibleLayers = createMemo(() => store.state.layerConfigs.filter((l) => l.visible)); // 渲染单个卡片的 SVG 内容(使用 foreignObject) const renderCardInSvg = (card: { data: typeof store.state.cards[0]; x: number; y: number }, pageIndex: number) => { const cardWidth = store.state.dimensions?.cardWidth || 56; const cardHeight = store.state.dimensions?.cardHeight || 88; const gridOriginX = store.state.dimensions?.gridOriginX || 0; const gridOriginY = store.state.dimensions?.gridOriginY || 0; const gridAreaWidth = store.state.dimensions?.gridAreaWidth || cardWidth; const gridAreaHeight = store.state.dimensions?.gridAreaHeight || cardHeight; const fontSize = store.state.dimensions?.fontSize || 3; return (
{/* 网格区域容器 */}
{/* 渲染每个 layer */} {(layer) => (
)}
); }; return (
{/* 打印样式:根据方向设置 @page 规则 */}
{/* 打印预览控制栏 */}

打印预览

共 {pages().length} 页,{store.state.cards.length} 张卡牌

{/* 方向选择 */}
{/* 奇数页偏移 */}
X: store.actions.setPrintOddPageOffsetX(Number(e.target.value))} class="w-16 px-2 py-1 border border-gray-300 rounded text-sm" step="0.1" /> mm
Y: store.actions.setPrintOddPageOffsetY(Number(e.target.value))} class="w-16 px-2 py-1 border border-gray-300 rounded text-sm" step="0.1" /> mm
{/* A4 纸张预览:每页都是一个完整的 SVG */}
); }