/** * PDF 导出器 * 使用 jspdf + html2canvas 在客户端生成 PDF * 使用 iframe 方案确保HTML样式正确渲染 */ import type { ExportData, ExportOptions, Exporter } from './types'; import { htmlExporter } from './html'; /** * 在客户端生成 PDF(使用 iframe 方案) * 这个函数需要在浏览器环境中调用 */ export async function generatePdfInBrowser( data: ExportData, options: ExportOptions ): Promise { // 动态导入 jspdf 和 html2canvas(仅在客户端) const [{ default: jsPDF }, { default: html2canvas }] = await Promise.all([ import('jspdf'), import('html2canvas'), ]); // 首先生成 HTML 内容 const htmlContent = await htmlExporter.export(data, options) as string; // 创建隐藏的 iframe 来渲染完整的 HTML 文档 const iframe = document.createElement('iframe'); iframe.style.cssText = ` position: fixed; left: -9999px; top: 0; width: 900px; height: 100%; border: none; visibility: hidden; `; document.body.appendChild(iframe); try { // 等待 iframe 准备就绪并写入 HTML 内容 await new Promise((resolve, reject) => { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (!iframeDoc) { reject(new Error('无法访问 iframe 文档')); return; } // 写入完整的 HTML 文档 iframeDoc.open(); iframeDoc.write(htmlContent); iframeDoc.close(); // 等待 iframe 加载完成 iframe.onload = () => resolve(); // 设置超时,防止 onload 不触发 setTimeout(() => resolve(), 1000); }); // 获取 iframe 中的文档 const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (!iframeDoc) { throw new Error('无法访问 iframe 文档'); } // 获取要渲染的容器(.container 或 body) const container = iframeDoc.querySelector('.container') || iframeDoc.body; // 等待图片加载 const images = container.querySelectorAll('img'); await Promise.all( Array.from(images).map( (img) => new Promise((resolve) => { if ((img as HTMLImageElement).complete) { resolve(); } else { (img as HTMLImageElement).onload = () => resolve(); (img as HTMLImageElement).onerror = () => resolve(); } }) ) ); // 额外等待一下确保样式完全渲染 await new Promise(resolve => setTimeout(resolve, 300)); // 使用 html2canvas 渲染 iframe 中的内容 const canvas = await html2canvas(container as HTMLElement, { scale: 2, useCORS: true, allowTaint: true, backgroundColor: '#F8F9FA', // 与页面背景色一致 logging: false, width: 900, windowWidth: 900, }); // 创建 PDF(A4 尺寸) const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4', }); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); // 计算图片在 PDF 中的尺寸 // 保持宽度铺满,按比例计算高度 const imgWidth = canvas.width; const imgHeight = canvas.height; // 图片缩放到 PDF 宽度 const scaledWidth = pdfWidth; const scaledHeight = (imgHeight * pdfWidth) / imgWidth; // 计算需要多少页 const totalPages = Math.ceil(scaledHeight / pdfHeight); // 分页添加内容 for (let page = 0; page < totalPages; page++) { if (page > 0) { pdf.addPage(); } // 计算当前页在原始图片中的位置 const sourceY = (page * pdfHeight * imgWidth) / pdfWidth; const sourceHeight = Math.min( (pdfHeight * imgWidth) / pdfWidth, imgHeight - sourceY ); // 如果没有更多内容,跳过 if (sourceHeight <= 0) break; // 创建当前页的裁剪画布 const pageCanvas = document.createElement('canvas'); pageCanvas.width = imgWidth; pageCanvas.height = Math.ceil(sourceHeight); const ctx = pageCanvas.getContext('2d'); if (ctx) { // 从原始画布中裁剪当前页的内容 ctx.drawImage( canvas, 0, // 源 x Math.floor(sourceY), // 源 y imgWidth, // 源宽度 Math.ceil(sourceHeight), // 源高度 0, // 目标 x 0, // 目标 y imgWidth, // 目标宽度 Math.ceil(sourceHeight) // 目标高度 ); // 将裁剪后的画布添加到 PDF const pageImgData = pageCanvas.toDataURL('image/png'); const pageScaledHeight = (sourceHeight * pdfWidth) / imgWidth; pdf.addImage( pageImgData, 'PNG', 0, 0, pdfWidth, pageScaledHeight ); } } // 返回 Blob return pdf.output('blob'); } finally { // 清理 iframe document.body.removeChild(iframe); } } /** * PDF 导出器(服务端占位符) * 实际的 PDF 生成在客户端通过 generatePdfInBrowser 函数执行 */ export const pdfExporter: Exporter = { async export(data: ExportData, options: ExportOptions): Promise { // 检查是否在浏览器环境 if (typeof window !== 'undefined') { return generatePdfInBrowser(data, options); } // 在服务端,返回一个错误提示 throw new Error('PDF export must be performed on the client side'); }, getContentType(): string { return 'application/pdf'; }, getFileExtension(): string { return 'pdf'; }, };