Showing
1 changed file
with
195 additions
and
17 deletions
| @@ -193,10 +193,22 @@ class HTMLRenderer: | @@ -193,10 +193,22 @@ class HTMLRenderer: | ||
| 193 | chapters = "".join(self._render_chapter(chapter) for chapter in self.chapters) | 193 | chapters = "".join(self._render_chapter(chapter) for chapter in self.chapters) |
| 194 | widget_scripts = "\n".join(self.widget_scripts) | 194 | widget_scripts = "\n".join(self.widget_scripts) |
| 195 | hydration = self._hydration_script() | 195 | hydration = self._hydration_script() |
| 196 | + overlay = """ | ||
| 197 | +<div id="export-overlay" class="export-overlay no-print" aria-hidden="true"> | ||
| 198 | + <div class="export-dialog" role="status" aria-live="assertive"> | ||
| 199 | + <div class="export-spinner" aria-hidden="true"></div> | ||
| 200 | + <p class="export-status">正在导出PDF,请稍候...</p> | ||
| 201 | + <div class="export-progress" role="progressbar" aria-valuetext="正在导出"> | ||
| 202 | + <div class="export-progress-bar"></div> | ||
| 203 | + </div> | ||
| 204 | + </div> | ||
| 205 | +</div> | ||
| 206 | +""".strip() | ||
| 196 | 207 | ||
| 197 | return f""" | 208 | return f""" |
| 198 | <body> | 209 | <body> |
| 199 | {header} | 210 | {header} |
| 211 | +{overlay} | ||
| 200 | <main> | 212 | <main> |
| 201 | {cover} | 213 | {cover} |
| 202 | {hero} | 214 | {hero} |
| @@ -1524,6 +1536,75 @@ body {{ | @@ -1524,6 +1536,75 @@ body {{ | ||
| 1524 | .action-btn:hover {{ | 1536 | .action-btn:hover {{ |
| 1525 | transform: translateY(-1px); | 1537 | transform: translateY(-1px); |
| 1526 | }} | 1538 | }} |
| 1539 | +body.exporting {{ | ||
| 1540 | + cursor: progress; | ||
| 1541 | +}} | ||
| 1542 | +.export-overlay {{ | ||
| 1543 | + position: fixed; | ||
| 1544 | + inset: 0; | ||
| 1545 | + background: rgba(3, 9, 26, 0.55); | ||
| 1546 | + backdrop-filter: blur(2px); | ||
| 1547 | + display: flex; | ||
| 1548 | + align-items: center; | ||
| 1549 | + justify-content: center; | ||
| 1550 | + opacity: 0; | ||
| 1551 | + pointer-events: none; | ||
| 1552 | + transition: opacity 0.3s ease; | ||
| 1553 | + z-index: 999; | ||
| 1554 | +}} | ||
| 1555 | +.export-overlay.active {{ | ||
| 1556 | + opacity: 1; | ||
| 1557 | + pointer-events: all; | ||
| 1558 | +}} | ||
| 1559 | +.export-dialog {{ | ||
| 1560 | + background: rgba(12, 19, 38, 0.92); | ||
| 1561 | + padding: 24px 32px; | ||
| 1562 | + border-radius: 18px; | ||
| 1563 | + color: #fff; | ||
| 1564 | + text-align: center; | ||
| 1565 | + min-width: 280px; | ||
| 1566 | + box-shadow: 0 16px 40px rgba(0,0,0,0.45); | ||
| 1567 | +}} | ||
| 1568 | +.export-spinner {{ | ||
| 1569 | + width: 48px; | ||
| 1570 | + height: 48px; | ||
| 1571 | + border-radius: 50%; | ||
| 1572 | + border: 3px solid rgba(255,255,255,0.2); | ||
| 1573 | + border-top-color: var(--secondary-color); | ||
| 1574 | + margin: 0 auto 16px; | ||
| 1575 | + animation: export-spin 1s linear infinite; | ||
| 1576 | +}} | ||
| 1577 | +.export-status {{ | ||
| 1578 | + margin: 0; | ||
| 1579 | + font-size: 1rem; | ||
| 1580 | +}} | ||
| 1581 | +.export-progress {{ | ||
| 1582 | + width: 220px; | ||
| 1583 | + height: 6px; | ||
| 1584 | + background: rgba(255,255,255,0.25); | ||
| 1585 | + border-radius: 999px; | ||
| 1586 | + overflow: hidden; | ||
| 1587 | + margin: 20px auto 0; | ||
| 1588 | + position: relative; | ||
| 1589 | +}} | ||
| 1590 | +.export-progress-bar {{ | ||
| 1591 | + position: absolute; | ||
| 1592 | + top: 0; | ||
| 1593 | + bottom: 0; | ||
| 1594 | + width: 45%; | ||
| 1595 | + border-radius: inherit; | ||
| 1596 | + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); | ||
| 1597 | + animation: export-progress 1.4s ease-in-out infinite; | ||
| 1598 | +}} | ||
| 1599 | +@keyframes export-spin {{ | ||
| 1600 | + from {{ transform: rotate(0deg); }} | ||
| 1601 | + to {{ transform: rotate(360deg); }} | ||
| 1602 | +}} | ||
| 1603 | +@keyframes export-progress {{ | ||
| 1604 | + 0% {{ left: -45%; }} | ||
| 1605 | + 50% {{ left: 20%; }} | ||
| 1606 | + 100% {{ left: 110%; }} | ||
| 1607 | +}} | ||
| 1527 | main {{ | 1608 | main {{ |
| 1528 | max-width: {container_width}; | 1609 | max-width: {container_width}; |
| 1529 | margin: 40px auto; | 1610 | margin: 40px auto; |
| @@ -1777,6 +1858,23 @@ pre.code-block {{ | @@ -1777,6 +1858,23 @@ pre.code-block {{ | ||
| 1777 | box-shadow: none; | 1858 | box-shadow: none; |
| 1778 | margin: 0; | 1859 | margin: 0; |
| 1779 | }} | 1860 | }} |
| 1861 | + .chapter > *, | ||
| 1862 | + .hero-section, | ||
| 1863 | + .callout, | ||
| 1864 | + .chart-card, | ||
| 1865 | + .kpi-grid, | ||
| 1866 | + .table-wrap, | ||
| 1867 | + figure, | ||
| 1868 | + blockquote {{ | ||
| 1869 | + break-inside: avoid; | ||
| 1870 | + page-break-inside: avoid; | ||
| 1871 | + }} | ||
| 1872 | + .chapter h2, | ||
| 1873 | + .chapter h3, | ||
| 1874 | + .chapter h4 {{ | ||
| 1875 | + break-after: avoid; | ||
| 1876 | + page-break-after: avoid; | ||
| 1877 | + }} | ||
| 1780 | }} | 1878 | }} |
| 1781 | """ | 1879 | """ |
| 1782 | 1880 | ||
| @@ -2103,32 +2201,112 @@ function hydrateCharts() { | @@ -2103,32 +2201,112 @@ function hydrateCharts() { | ||
| 2103 | }); | 2201 | }); |
| 2104 | } | 2202 | } |
| 2105 | 2203 | ||
| 2204 | +function getExportOverlayParts() { | ||
| 2205 | + const overlay = document.getElementById('export-overlay'); | ||
| 2206 | + if (!overlay) { | ||
| 2207 | + return null; | ||
| 2208 | + } | ||
| 2209 | + return { | ||
| 2210 | + overlay, | ||
| 2211 | + status: overlay.querySelector('.export-status') | ||
| 2212 | + }; | ||
| 2213 | +} | ||
| 2214 | + | ||
| 2215 | +function showExportOverlay(message) { | ||
| 2216 | + const parts = getExportOverlayParts(); | ||
| 2217 | + if (!parts) return; | ||
| 2218 | + if (message && parts.status) { | ||
| 2219 | + parts.status.textContent = message; | ||
| 2220 | + } | ||
| 2221 | + parts.overlay.classList.add('active'); | ||
| 2222 | + document.body.classList.add('exporting'); | ||
| 2223 | +} | ||
| 2224 | + | ||
| 2225 | +function updateExportOverlay(message) { | ||
| 2226 | + if (!message) return; | ||
| 2227 | + const parts = getExportOverlayParts(); | ||
| 2228 | + if (parts && parts.status) { | ||
| 2229 | + parts.status.textContent = message; | ||
| 2230 | + } | ||
| 2231 | +} | ||
| 2232 | + | ||
| 2233 | +function hideExportOverlay(delay) { | ||
| 2234 | + const parts = getExportOverlayParts(); | ||
| 2235 | + if (!parts) return; | ||
| 2236 | + const close = () => { | ||
| 2237 | + parts.overlay.classList.remove('active'); | ||
| 2238 | + document.body.classList.remove('exporting'); | ||
| 2239 | + }; | ||
| 2240 | + if (delay && delay > 0) { | ||
| 2241 | + setTimeout(close, delay); | ||
| 2242 | + } else { | ||
| 2243 | + close(); | ||
| 2244 | + } | ||
| 2245 | +} | ||
| 2246 | + | ||
| 2106 | function exportPdf() { | 2247 | function exportPdf() { |
| 2107 | const target = document.querySelector('main'); | 2248 | const target = document.querySelector('main'); |
| 2108 | - if (!target || typeof html2canvas === 'undefined' || typeof jspdf === 'undefined') { | 2249 | + if (!target || typeof jspdf === 'undefined' || typeof jspdf.jsPDF !== 'function') { |
| 2109 | alert('PDF导出依赖未就绪'); | 2250 | alert('PDF导出依赖未就绪'); |
| 2110 | return; | 2251 | return; |
| 2111 | } | 2252 | } |
| 2112 | - html2canvas(target, {scale: 2}).then(canvas => { | ||
| 2113 | - const imgData = canvas.toDataURL('image/png'); | 2253 | + const exportBtn = document.getElementById('export-btn'); |
| 2254 | + if (exportBtn) { | ||
| 2255 | + exportBtn.disabled = true; | ||
| 2256 | + } | ||
| 2257 | + showExportOverlay('正在导出PDF,请稍候...'); | ||
| 2114 | const pdf = new jspdf.jsPDF('p', 'mm', 'a4'); | 2258 | const pdf = new jspdf.jsPDF('p', 'mm', 'a4'); |
| 2115 | const pageWidth = pdf.internal.pageSize.getWidth(); | 2259 | const pageWidth = pdf.internal.pageSize.getWidth(); |
| 2116 | - const pageHeight = pdf.internal.pageSize.getHeight(); | ||
| 2117 | - const imgHeight = canvas.height * pageWidth / canvas.width; | ||
| 2118 | - let heightLeft = imgHeight; | ||
| 2119 | - let position = 0; | ||
| 2120 | - | ||
| 2121 | - pdf.addImage(imgData, 'PNG', 0, position, pageWidth, imgHeight); | ||
| 2122 | - heightLeft -= pageHeight; | ||
| 2123 | - | ||
| 2124 | - while (heightLeft > 0) { | ||
| 2125 | - position = heightLeft - imgHeight; | ||
| 2126 | - pdf.addPage(); | ||
| 2127 | - pdf.addImage(imgData, 'PNG', 0, position, pageWidth, imgHeight); | ||
| 2128 | - heightLeft -= pageHeight; | 2260 | + const pxWidth = Math.max(target.scrollWidth, document.documentElement.scrollWidth); |
| 2261 | + const restoreButton = () => { | ||
| 2262 | + if (exportBtn) { | ||
| 2263 | + exportBtn.disabled = false; | ||
| 2264 | + } | ||
| 2265 | + }; | ||
| 2266 | + let renderTask; | ||
| 2267 | + try { | ||
| 2268 | + renderTask = pdf.html(target, { | ||
| 2269 | + x: 8, | ||
| 2270 | + y: 12, | ||
| 2271 | + width: pageWidth - 16, | ||
| 2272 | + margin: [12, 12, 20, 12], | ||
| 2273 | + autoPaging: 'text', | ||
| 2274 | + windowWidth: pxWidth, | ||
| 2275 | + pagebreak: { | ||
| 2276 | + mode: ['css', 'legacy'], | ||
| 2277 | + avoid: ['.chapter > *', '.callout', '.chart-card', '.table-wrap', '.kpi-grid', '.hero-section'] | ||
| 2278 | + }, | ||
| 2279 | + html2canvas: { | ||
| 2280 | + scale: 0.72, | ||
| 2281 | + useCORS: true, | ||
| 2282 | + logging: false | ||
| 2283 | + }, | ||
| 2284 | + callback: (doc) => doc.save('report.pdf') | ||
| 2285 | + }); | ||
| 2286 | + } catch (err) { | ||
| 2287 | + console.error('PDF 导出失败', err); | ||
| 2288 | + updateExportOverlay('导出失败,请稍后重试'); | ||
| 2289 | + hideExportOverlay(1200); | ||
| 2290 | + restoreButton(); | ||
| 2291 | + alert('PDF导出失败,请稍后重试'); | ||
| 2292 | + return; | ||
| 2129 | } | 2293 | } |
| 2130 | - pdf.save('report.pdf'); | 2294 | + if (renderTask && typeof renderTask.then === 'function') { |
| 2295 | + renderTask.then(() => { | ||
| 2296 | + updateExportOverlay('导出完成,正在保存...'); | ||
| 2297 | + hideExportOverlay(800); | ||
| 2298 | + restoreButton(); | ||
| 2299 | + }).catch(err => { | ||
| 2300 | + console.error('PDF 导出失败', err); | ||
| 2301 | + updateExportOverlay('导出失败,请稍后重试'); | ||
| 2302 | + hideExportOverlay(1200); | ||
| 2303 | + restoreButton(); | ||
| 2304 | + alert('PDF导出失败,请稍后重试'); | ||
| 2131 | }); | 2305 | }); |
| 2306 | + } else { | ||
| 2307 | + hideExportOverlay(); | ||
| 2308 | + restoreButton(); | ||
| 2309 | + } | ||
| 2132 | } | 2310 | } |
| 2133 | 2311 | ||
| 2134 | document.addEventListener('DOMContentLoaded', () => { | 2312 | document.addEventListener('DOMContentLoaded', () => { |
-
Please register or login to post a comment