diff --git a/src/components/md-deck/hooks/usePDFExport.ts b/src/components/md-deck/hooks/usePDFExport.ts index 7acf630..994535e 100644 --- a/src/components/md-deck/hooks/usePDFExport.ts +++ b/src/components/md-deck/hooks/usePDFExport.ts @@ -1,24 +1,5 @@ -import { marked } from '../../../markdown'; -import { getLayerStyle } from './dimensions'; import type { DeckStore } from './deckStore'; import type { CardData, LayerConfig, Dimensions } from '../types'; -import jsPDF from 'jspdf'; - -/** - * 处理 body 内容中的 {{prop}} 语法并解析 markdown - */ -function processBody(body: string, currentRow: CardData): string { - const processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => currentRow[key] || ''); - return marked.parse(processedBody) as string; -} - -/** - * 渲染 layer 内容 - */ -function renderLayerContent(layer: { prop: string }, cardData: CardData): string { - const content = cardData[layer.prop] || ''; - return processBody(content, cardData); -} export interface PageCard { data: CardData; @@ -53,84 +34,12 @@ export interface ExportOptions { dimensions: Dimensions; } -/** - * 渲染卡牌到 canvas - */ -async function renderCardToCanvas(card: PageCard, options: ExportOptions): Promise { - const { cardWidth, cardHeight, gridOriginX, gridOriginY, gridAreaWidth, gridAreaHeight, fontSize, visibleLayers, dimensions } = options; - - const container = document.createElement('div'); - container.style.position = 'absolute'; - container.style.left = '-9999px'; - container.style.top = '-9999px'; - container.style.width = `${cardWidth}mm`; - container.style.height = `${cardHeight}mm`; - container.style.background = 'white'; - - const gridContainer = document.createElement('div'); - gridContainer.style.position = 'absolute'; - gridContainer.style.left = `${gridOriginX}mm`; - gridContainer.style.top = `${gridOriginY}mm`; - gridContainer.style.width = `${gridAreaWidth}mm`; - gridContainer.style.height = `${gridAreaHeight}mm`; - - for (const layer of visibleLayers) { - const layerEl = document.createElement('article'); - layerEl.className = 'absolute flex items-center justify-center text-center prose prose-sm'; - Object.assign(layerEl.style, getLayerStyle(layer, dimensions)); - layerEl.style.fontSize = `${fontSize}mm`; - layerEl.innerHTML = renderLayerContent(layer, card.data); - gridContainer.appendChild(layerEl); - } - - container.appendChild(gridContainer); - document.body.appendChild(container); - - try { - const html2canvas = (await import('html2canvas')).default; - return await html2canvas(container, { - scale: 2, - backgroundColor: null, - logging: false, - useCORS: true, - }); - } finally { - document.body.removeChild(container); - } -} - -/** - * 绘制页面边框和裁切线 - */ -function drawPageMarks(pdf: jsPDF, cropData: CropMarkData, pageFrameBounds: { minX: number; maxX: number; minY: number; maxY: number }) { - const frameMargin = cropData.frameBoundsWithMargin; - - // 外围边框 - pdf.setDrawColor(0); - pdf.setLineWidth(0.2); - pdf.rect(frameMargin.x, frameMargin.y, frameMargin.width, frameMargin.height); - - // 水平裁切线 - pdf.setDrawColor(136); - pdf.setLineWidth(0.1); - for (const line of cropData.horizontalLines) { - pdf.line(line.xStart, line.y, pageFrameBounds.minX, line.y); - pdf.line(pageFrameBounds.maxX, line.y, line.xEnd, line.y); - } - - // 垂直裁切线 - for (const line of cropData.verticalLines) { - pdf.line(line.x, line.yStart, line.x, pageFrameBounds.minY); - pdf.line(line.x, pageFrameBounds.maxY, line.x, line.yEnd); - } -} - export interface UsePDFExportReturn { exportToPDF: (pages: PageData[], cropMarks: CropMarkData[], options: ExportOptions) => Promise; } /** - * PDF 导出 hook + * PDF 导出 hook - 打开新窗口并打印 HTML 页面 */ export function usePDFExport(store: DeckStore, onClose: () => void): UsePDFExportReturn { const exportToPDF = async (pages: PageData[], cropMarks: CropMarkData[], options: ExportOptions) => { @@ -140,37 +49,176 @@ export function usePDFExport(store: DeckStore, onClose: () => void): UsePDFExpor store.actions.setExportError(null); try { - const pdf = new jsPDF({ - orientation: options.orientation, - unit: 'mm', - format: 'a4' - }); + const a4Width = options.orientation === 'landscape' ? 297 : 210; + const a4Height = options.orientation === 'landscape' ? 210 : 297; - for (let i = 0; i < totalPages; i++) { - if (i > 0) { - pdf.addPage(); - } - - const page = pages[i]; - const cropData = cropMarks[i]; - - drawPageMarks(pdf, cropData, page.frameBounds); - - const totalCards = page.cards.length; - for (let j = 0; j < totalCards; j++) { - const card = page.cards[j]; - const canvas = await renderCardToCanvas(card, options); - const imgData = canvas.toDataURL('image/png'); - pdf.addImage(imgData, 'PNG', card.x, card.y, options.cardWidth, options.cardHeight); - - const currentCardIndex = i * totalCards + j + 1; - const totalCardCount = totalPages * totalCards; - const progress = Math.round((currentCardIndex / totalCardCount) * 100); - store.actions.setExportProgress(progress); - } + // 创建新窗口 + const printWindow = window.open('', '_blank'); + if (!printWindow) { + throw new Error('无法打开新窗口,请允许弹出窗口'); } - pdf.save('deck.pdf'); + // 收集当前文档的所有样式 + const styles = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]')) + .map(el => el.outerHTML) + .join('\n'); + + // 为每个页面生成 HTML 内容 + const pagesHtml = pages.map((page, pageIndex) => { + const cropData = cropMarks[pageIndex]; + const frameMargin = cropData.frameBoundsWithMargin; + + // 生成裁切线 HTML + const horizontalLinesHtml = cropData.horizontalLines.map(line => ` +
+ `).join(''); + + const verticalLinesHtml = cropData.verticalLines.map(line => ` +
+ `).join(''); + + // 生成卡牌 HTML + const cardsHtml = page.cards.map((card, cardIndex) => { + // 从 DOM 中获取卡牌内容 + const pageSvg = document.querySelector(`svg[data-page="${page.pageIndex + 1}"]`); + const cardGroups = pageSvg?.querySelectorAll('.card-group'); + const cardGroup = cardGroups?.[cardIndex] as SVGGElement; + + let cardContent = ''; + if (cardGroup) { + const foreignObject = cardGroup.querySelector('foreignObject'); + if (foreignObject) { + const innerDiv = foreignObject.querySelector('div'); + if (innerDiv) { + const cardLayer = innerDiv.querySelector('.absolute'); + if (cardLayer) { + cardContent = cardLayer.innerHTML; + } + } + } + } + + return ` +
+
+ ${cardContent} +
+
+ `; + }).join('\n'); + + return ` +
+
+ ${horizontalLinesHtml} + ${verticalLinesHtml} + ${cardsHtml} +
+ `; + }).join('\n'); + + // 写入 HTML 内容 + printWindow.document.write(` + + + + 打印预览 + ${styles} + + + + ${pagesHtml} + + + `); + + printWindow.document.close(); + + // 等待内容加载完成后自动打印 + printWindow.onload = () => { + store.actions.setExportProgress(100); + }; + + // 关闭预览 onClose(); } catch (err) { const errorMsg = err instanceof Error ? err.message : '导出失败,未知错误';