| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- 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<void> {
- 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
- }
- }
|