import { toPng } from 'html-to-image' import jsPDF from 'jspdf' export interface ExportPDFOptions { elementId: string filename?: string title?: string subtitle?: string } /** * Create a temporary header element for PDF */ function createPDFHeader(title: string, subtitle: string): HTMLDivElement { const header = document.createElement('div') header.style.cssText = ` padding: 24px 32px; background: #ffffff; border-bottom: 1px solid #e5e7eb; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; ` const titleEl = document.createElement('h1') titleEl.textContent = title titleEl.style.cssText = ` margin: 0 0 8px 0; font-size: 24px; font-weight: 600; color: #1f2328; line-height: 1.3; ` const subtitleEl = document.createElement('p') subtitleEl.textContent = subtitle subtitleEl.style.cssText = ` margin: 0 0 8px 0; font-size: 14px; color: #6b7280; line-height: 1.5; ` const timestampEl = document.createElement('p') const timestamp = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) timestampEl.textContent = `η”Ÿζˆζ—Άι—΄: ${timestamp}` timestampEl.style.cssText = ` margin: 0; font-size: 12px; color: #9ca3af; line-height: 1.5; ` header.appendChild(titleEl) if (subtitle) header.appendChild(subtitleEl) header.appendChild(timestampEl) return header } /** * Export DOM element to PDF * @param options Export options */ export async function exportToPDF(options: ExportPDFOptions): Promise { const { elementId, filename = 'report.pdf', title = 'Performance Report', subtitle = '', } = options try { const element = document.getElementById(elementId) if (!element) { throw new Error(`Element with id "${elementId}" not found`) } // Create header element (separate from content) const header = createPDFHeader(title, subtitle) header.style.width = `${element.offsetWidth}px` header.style.position = 'fixed' header.style.left = '-9999px' header.style.top = '0' document.body.appendChild(header) // Wait a bit for header to render await new Promise(resolve => setTimeout(resolve, 100)) // Capture header const headerDataUrl = await toPng(header, { quality: 0.95, pixelRatio: 2, backgroundColor: '#ffffff', }) // Remove header document.body.removeChild(header) // Capture content directly (no cloning to preserve Canvas/SVG) const contentDataUrl = await toPng(element, { quality: 0.95, pixelRatio: 2, backgroundColor: '#ffffff', cacheBust: true, }) // Load header image const headerImg = new Image() headerImg.src = headerDataUrl await new Promise((resolve) => { headerImg.onload = resolve }) // Load content image const contentImg = new Image() contentImg.src = contentDataUrl await new Promise((resolve) => { contentImg.onload = resolve }) // A4 size: 210mm x 297mm const pdfWidth = 210 const pdfHeight = 297 const margin = 10 // Calculate scaling based on content width const ratio = (pdfWidth - margin * 2) / contentImg.width const headerHeight = headerImg.height * ratio const contentHeight = contentImg.height * ratio // Create PDF const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4', }) const pageWidth = pdf.internal.pageSize.getWidth() const pageHeight = pdf.internal.pageSize.getHeight() const availableHeight = pageHeight - margin * 2 let currentY = margin // Add header on first page pdf.addImage( headerDataUrl, 'PNG', margin, currentY, pdfWidth - margin * 2, headerHeight, ) currentY += headerHeight // Check if content needs pagination const remainingHeight = availableHeight - headerHeight if (contentHeight <= remainingHeight) { // Content fits on first page pdf.addImage( contentDataUrl, 'PNG', margin, currentY, pdfWidth - margin * 2, contentHeight, ) } else { // Content needs multiple pages const totalPages = Math.ceil((headerHeight + contentHeight) / availableHeight) // First page: remaining space after header const firstPageContentHeight = remainingHeight const sourceHeightRatio = firstPageContentHeight / contentHeight const sourcePixelHeight = contentImg.height * sourceHeightRatio let canvas = document.createElement('canvas') canvas.width = contentImg.width canvas.height = sourcePixelHeight let ctx = canvas.getContext('2d') if (ctx) { ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.drawImage( contentImg, 0, 0, contentImg.width, sourcePixelHeight, 0, 0, canvas.width, canvas.height, ) pdf.addImage(canvas.toDataURL('image/png', 0.95), 'PNG', margin, currentY, pdfWidth - margin * 2, firstPageContentHeight) } // Subsequent pages let remainingContentHeight = contentHeight - firstPageContentHeight let sourceY = sourcePixelHeight let pageNum = 2 while (remainingContentHeight > 0) { pdf.addPage() const pageContentHeight = Math.min(availableHeight, remainingContentHeight) const pageSourceHeightRatio = pageContentHeight / contentHeight const pageSourcePixelHeight = contentImg.height * pageSourceHeightRatio canvas = document.createElement('canvas') canvas.width = contentImg.width canvas.height = pageSourcePixelHeight ctx = canvas.getContext('2d') if (ctx) { ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.drawImage( contentImg, 0, sourceY, contentImg.width, pageSourcePixelHeight, 0, 0, canvas.width, canvas.height, ) pdf.addImage(canvas.toDataURL('image/png', 0.95), 'PNG', margin, margin, pdfWidth - margin * 2, pageContentHeight) } sourceY += pageSourcePixelHeight remainingContentHeight -= pageContentHeight pageNum++ } // Add page numbers const totalPagesCount = pdf.getNumberOfPages() for (let i = 1; i <= totalPagesCount; i++) { pdf.setPage(i) pdf.setFontSize(9) pdf.setTextColor(156, 163, 175) pdf.text( `${i} / ${totalPagesCount}`, pageWidth / 2, pageHeight - 5, { align: 'center' }, ) } } // Save PDF pdf.save(filename) } catch (error) { console.error('Failed to export PDF:', error) throw error } }