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
}
}