/**
* 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';
},
};