From 47c15e933c0bcfa7833417633d43dfa2fcb798ab Mon Sep 17 00:00:00 2001 From: hyper Date: Fri, 27 Feb 2026 21:02:33 +0800 Subject: [PATCH] feat: error handling --- src/components/md-deck/PrintPreview.tsx | 244 +++++++++++++--------- src/components/md-deck/hooks/deckStore.ts | 18 +- 2 files changed, 165 insertions(+), 97 deletions(-) diff --git a/src/components/md-deck/PrintPreview.tsx b/src/components/md-deck/PrintPreview.tsx index bbb7898..ea6ffd1 100644 --- a/src/components/md-deck/PrintPreview.tsx +++ b/src/components/md-deck/PrintPreview.tsx @@ -195,114 +195,134 @@ export function PrintPreview(props: PrintPreviewProps) { const handleExportPDF = async () => { const pagesData = pages(); const a4Size = getA4Size(); + const totalPages = pagesData.length; - // 创建 jsPDF 实例 - const pdf = new jsPDF({ - orientation: orientation() === 'landscape' ? 'landscape' : 'portrait', - unit: 'mm', - format: 'a4' - }); + // 重置状态 + store.actions.setExportProgress(0); + store.actions.setExportError(null); - 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; + try { + // 创建 jsPDF 实例 + const pdf = new jsPDF({ + orientation: orientation() === 'landscape' ? 'landscape' : 'portrait', + unit: 'mm', + format: 'a4' + }); - // 为每页生成内容 - for (let i = 0; i < pagesData.length; i++) { - if (i > 0) { - pdf.addPage(); - } + 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; - const page = pagesData[i]; - const cropData = cropMarks()[i]; - - // 绘制外围边框 - const frameMargin = cropData.frameBoundsWithMargin; - pdf.setDrawColor(0); - pdf.setLineWidth(0.2); - pdf.rect(frameMargin.x, frameMargin.y, frameMargin.width, frameMargin.height); - - // 绘制水平裁切线 - for (const line of cropData.horizontalLines) { - pdf.setDrawColor(136); - pdf.setLineWidth(0.1); - // 左侧裁切线 - pdf.line(line.xStart, line.y, page.frameBounds.minX, line.y); - // 右侧裁切线 - pdf.line(page.frameBounds.maxX, line.y, line.xEnd, line.y); - } - - // 绘制垂直裁切线 - for (const line of cropData.verticalLines) { - pdf.setDrawColor(136); - pdf.setLineWidth(0.1); - // 上方裁切线 - pdf.line(line.x, line.yStart, line.x, page.frameBounds.minY); - // 下方裁切线 - pdf.line(line.x, page.frameBounds.maxY, line.x, line.yEnd); - } - - // 渲染卡牌内容 - for (const card of page.cards) { - // 创建临时容器渲染卡牌内容 - 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`; - - // 渲染每个 layer - for (const layer of visibleLayers()) { - const layerEl = document.createElement('div'); - layerEl.className = 'absolute flex items-center justify-center text-center prose prose-sm'; - Object.assign(layerEl.style, getLayerStyle(layer, store.state.dimensions!)); - layerEl.style.fontSize = `${fontSize}mm`; - layerEl.innerHTML = renderLayerContent(layer, card.data); - gridContainer.appendChild(layerEl); + // 为每页生成内容 + for (let i = 0; i < totalPages; i++) { + if (i > 0) { + pdf.addPage(); } - container.appendChild(gridContainer); - document.body.appendChild(container); + const page = pagesData[i]; + const cropData = cropMarks()[i]; - // 使用 html2canvas 渲染 - try { - const html2canvas = (await import('html2canvas')).default; - const canvas = await html2canvas(container, { - scale: 2, - backgroundColor: null, - logging: false, - useCORS: true - }); + // 绘制外围边框 + const frameMargin = cropData.frameBoundsWithMargin; + pdf.setDrawColor(0); + pdf.setLineWidth(0.2); + pdf.rect(frameMargin.x, frameMargin.y, frameMargin.width, frameMargin.height); - const imgData = canvas.toDataURL('image/png'); - pdf.addImage(imgData, 'PNG', card.x, card.y, cardWidth, cardHeight); - } catch (e) { - console.error('渲染卡牌内容失败:', e); + // 绘制水平裁切线 + for (const line of cropData.horizontalLines) { + pdf.setDrawColor(136); + pdf.setLineWidth(0.1); + // 左侧裁切线 + pdf.line(line.xStart, line.y, page.frameBounds.minX, line.y); + // 右侧裁切线 + pdf.line(page.frameBounds.maxX, line.y, line.xEnd, line.y); } - document.body.removeChild(container); + // 绘制垂直裁切线 + for (const line of cropData.verticalLines) { + pdf.setDrawColor(136); + pdf.setLineWidth(0.1); + // 上方裁切线 + pdf.line(line.x, line.yStart, line.x, page.frameBounds.minY); + // 下方裁切线 + pdf.line(line.x, page.frameBounds.maxY, line.x, line.yEnd); + } + + // 渲染卡牌内容 + const totalCards = page.cards.length; + for (let j = 0; j < totalCards; j++) { + const card = page.cards[j]; + + // 创建临时容器渲染卡牌内容 + 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`; + + // 渲染每个 layer + for (const layer of visibleLayers()) { + const layerEl = document.createElement('div'); + layerEl.className = 'absolute flex items-center justify-center text-center prose prose-sm'; + Object.assign(layerEl.style, getLayerStyle(layer, store.state.dimensions!)); + layerEl.style.fontSize = `${fontSize}mm`; + layerEl.innerHTML = renderLayerContent(layer, card.data); + gridContainer.appendChild(layerEl); + } + + container.appendChild(gridContainer); + document.body.appendChild(container); + + // 使用 html2canvas 渲染 + try { + const html2canvas = (await import('html2canvas')).default; + const canvas = await html2canvas(container, { + scale: 2, + backgroundColor: null, + logging: false, + useCORS: true + }); + + const imgData = canvas.toDataURL('image/png'); + pdf.addImage(imgData, 'PNG', card.x, card.y, cardWidth, cardHeight); + } catch (e) { + console.error('渲染卡牌内容失败:', e); + } + + document.body.removeChild(container); + + // 更新进度(按卡牌数量计算) + const currentCardIndex = i * totalCards + j + 1; + const totalCardCount = totalPages * totalCards; + const progress = Math.round((currentCardIndex / totalCardCount) * 100); + store.actions.setExportProgress(progress); + } } + + // 保存 PDF 文件 + pdf.save('deck.pdf'); + + // 完成,关闭预览 + props.onClose(); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : '导出失败,未知错误'; + store.actions.setExportError(errorMsg); + console.error('PDF 导出失败:', err); } - - // 保存 PDF 文件 - pdf.save('deck.pdf'); - - // 关闭预览 - props.onClose(); }; // 渲染单个卡片的 SVG 内容(使用 foreignObject) @@ -435,6 +455,38 @@ export function PrintPreview(props: PrintPreviewProps) { + {/* 进度条和错误信息 */} + 0 || store.state.exportError}> +
+ +
+ 导出进度: +
+
+
+ {store.state.exportProgress}% +
+ + +
+
+ + {store.state.exportError} +
+ +
+
+
+
+ {/* A4 纸张预览:每页都是一个完整的 SVG */}
diff --git a/src/components/md-deck/hooks/deckStore.ts b/src/components/md-deck/hooks/deckStore.ts index 06858c7..753ce6f 100644 --- a/src/components/md-deck/hooks/deckStore.ts +++ b/src/components/md-deck/hooks/deckStore.ts @@ -56,6 +56,8 @@ export interface DeckState { // 导出状态 isExporting: boolean; + exportProgress: number; // 0-100 + exportError: string | null; // 打印设置 printOrientation: 'portrait' | 'landscape'; @@ -106,6 +108,9 @@ export interface DeckActions { // 导出操作 setExporting: (exporting: boolean) => void; exportDeck: () => void; + setExportProgress: (progress: number) => void; + setExportError: (error: string | null) => void; + clearExportError: () => void; // 打印设置 setPrintOrientation: (orientation: 'portrait' | 'landscape') => void; @@ -147,6 +152,8 @@ export function createDeckStore( isLoading: false, error: null, isExporting: false, + exportProgress: 0, + exportError: null, printOrientation: 'portrait', printOddPageOffsetX: 0, printOddPageOffsetY: 0 @@ -289,9 +296,15 @@ export function createDeckStore( const setExporting = (exporting: boolean) => setState({ isExporting: exporting }); const exportDeck = () => { - setState({ isExporting: true }); + setState({ isExporting: true, exportProgress: 0, exportError: null }); }; + const setExportProgress = (progress: number) => setState({ exportProgress: progress }); + + const setExportError = (error: string | null) => setState({ exportError: error }); + + const clearExportError = () => setState({ exportError: null }); + const setPrintOrientation = (orientation: 'portrait' | 'landscape') => { setState({ printOrientation: orientation }); }; @@ -332,6 +345,9 @@ export function createDeckStore( copyCode, setExporting, exportDeck, + setExportProgress, + setExportError, + clearExportError, setPrintOrientation, setPrintOddPageOffsetX, setPrintOddPageOffsetY