From 79a015b77af9fae83fdae82307a5446a9ba7ed75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E4=B8=80=E4=B8=81?= <1769123563@qq.com> Date: Sat, 15 Nov 2025 02:37:01 +0800 Subject: [PATCH] Improve PDF Export --- ReportEngine/renderers/html_renderer.py | 218 +++++++++++++++++++++--- 1 file changed, 198 insertions(+), 20 deletions(-) diff --git a/ReportEngine/renderers/html_renderer.py b/ReportEngine/renderers/html_renderer.py index a3d3199..2b8b47e 100644 --- a/ReportEngine/renderers/html_renderer.py +++ b/ReportEngine/renderers/html_renderer.py @@ -193,10 +193,22 @@ class HTMLRenderer: chapters = "".join(self._render_chapter(chapter) for chapter in self.chapters) widget_scripts = "\n".join(self.widget_scripts) hydration = self._hydration_script() + overlay = """ + +""".strip() return f""" {header} +{overlay}
{cover} {hero} @@ -1524,6 +1536,75 @@ body {{ .action-btn:hover {{ transform: translateY(-1px); }} +body.exporting {{ + cursor: progress; +}} +.export-overlay {{ + position: fixed; + inset: 0; + background: rgba(3, 9, 26, 0.55); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 999; +}} +.export-overlay.active {{ + opacity: 1; + pointer-events: all; +}} +.export-dialog {{ + background: rgba(12, 19, 38, 0.92); + padding: 24px 32px; + border-radius: 18px; + color: #fff; + text-align: center; + min-width: 280px; + box-shadow: 0 16px 40px rgba(0,0,0,0.45); +}} +.export-spinner {{ + width: 48px; + height: 48px; + border-radius: 50%; + border: 3px solid rgba(255,255,255,0.2); + border-top-color: var(--secondary-color); + margin: 0 auto 16px; + animation: export-spin 1s linear infinite; +}} +.export-status {{ + margin: 0; + font-size: 1rem; +}} +.export-progress {{ + width: 220px; + height: 6px; + background: rgba(255,255,255,0.25); + border-radius: 999px; + overflow: hidden; + margin: 20px auto 0; + position: relative; +}} +.export-progress-bar {{ + position: absolute; + top: 0; + bottom: 0; + width: 45%; + border-radius: inherit; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + animation: export-progress 1.4s ease-in-out infinite; +}} +@keyframes export-spin {{ + from {{ transform: rotate(0deg); }} + to {{ transform: rotate(360deg); }} +}} +@keyframes export-progress {{ + 0% {{ left: -45%; }} + 50% {{ left: 20%; }} + 100% {{ left: 110%; }} +}} main {{ max-width: {container_width}; margin: 40px auto; @@ -1777,6 +1858,23 @@ pre.code-block {{ box-shadow: none; margin: 0; }} + .chapter > *, + .hero-section, + .callout, + .chart-card, + .kpi-grid, + .table-wrap, + figure, + blockquote {{ + break-inside: avoid; + page-break-inside: avoid; + }} + .chapter h2, + .chapter h3, + .chapter h4 {{ + break-after: avoid; + page-break-after: avoid; + }} }} """ @@ -2103,32 +2201,112 @@ function hydrateCharts() { }); } +function getExportOverlayParts() { + const overlay = document.getElementById('export-overlay'); + if (!overlay) { + return null; + } + return { + overlay, + status: overlay.querySelector('.export-status') + }; +} + +function showExportOverlay(message) { + const parts = getExportOverlayParts(); + if (!parts) return; + if (message && parts.status) { + parts.status.textContent = message; + } + parts.overlay.classList.add('active'); + document.body.classList.add('exporting'); +} + +function updateExportOverlay(message) { + if (!message) return; + const parts = getExportOverlayParts(); + if (parts && parts.status) { + parts.status.textContent = message; + } +} + +function hideExportOverlay(delay) { + const parts = getExportOverlayParts(); + if (!parts) return; + const close = () => { + parts.overlay.classList.remove('active'); + document.body.classList.remove('exporting'); + }; + if (delay && delay > 0) { + setTimeout(close, delay); + } else { + close(); + } +} + function exportPdf() { const target = document.querySelector('main'); - if (!target || typeof html2canvas === 'undefined' || typeof jspdf === 'undefined') { + if (!target || typeof jspdf === 'undefined' || typeof jspdf.jsPDF !== 'function') { alert('PDF导出依赖未就绪'); return; } - html2canvas(target, {scale: 2}).then(canvas => { - const imgData = canvas.toDataURL('image/png'); - const pdf = new jspdf.jsPDF('p', 'mm', 'a4'); - const pageWidth = pdf.internal.pageSize.getWidth(); - const pageHeight = pdf.internal.pageSize.getHeight(); - const imgHeight = canvas.height * pageWidth / canvas.width; - let heightLeft = imgHeight; - let position = 0; - - pdf.addImage(imgData, 'PNG', 0, position, pageWidth, imgHeight); - heightLeft -= pageHeight; - - while (heightLeft > 0) { - position = heightLeft - imgHeight; - pdf.addPage(); - pdf.addImage(imgData, 'PNG', 0, position, pageWidth, imgHeight); - heightLeft -= pageHeight; + const exportBtn = document.getElementById('export-btn'); + if (exportBtn) { + exportBtn.disabled = true; + } + showExportOverlay('正在导出PDF,请稍候...'); + const pdf = new jspdf.jsPDF('p', 'mm', 'a4'); + const pageWidth = pdf.internal.pageSize.getWidth(); + const pxWidth = Math.max(target.scrollWidth, document.documentElement.scrollWidth); + const restoreButton = () => { + if (exportBtn) { + exportBtn.disabled = false; } - pdf.save('report.pdf'); - }); + }; + let renderTask; + try { + renderTask = pdf.html(target, { + x: 8, + y: 12, + width: pageWidth - 16, + margin: [12, 12, 20, 12], + autoPaging: 'text', + windowWidth: pxWidth, + pagebreak: { + mode: ['css', 'legacy'], + avoid: ['.chapter > *', '.callout', '.chart-card', '.table-wrap', '.kpi-grid', '.hero-section'] + }, + html2canvas: { + scale: 0.72, + useCORS: true, + logging: false + }, + callback: (doc) => doc.save('report.pdf') + }); + } catch (err) { + console.error('PDF 导出失败', err); + updateExportOverlay('导出失败,请稍后重试'); + hideExportOverlay(1200); + restoreButton(); + alert('PDF导出失败,请稍后重试'); + return; + } + if (renderTask && typeof renderTask.then === 'function') { + renderTask.then(() => { + updateExportOverlay('导出完成,正在保存...'); + hideExportOverlay(800); + restoreButton(); + }).catch(err => { + console.error('PDF 导出失败', err); + updateExportOverlay('导出失败,请稍后重试'); + hideExportOverlay(1200); + restoreButton(); + alert('PDF导出失败,请稍后重试'); + }); + } else { + hideExportOverlay(); + restoreButton(); + } } document.addEventListener('DOMContentLoaded', () => {