import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';
export interface ExportPdfOptions {
filename?: string;
pageSize?: 'A4' | 'A5' | 'Letter';
pageMargin?: { bottom: number; left: number; right: number; top: number };
quality?: number;
}
// 页面尺寸(单位:mm)
const PAGE_SIZES_MM = {
A4: { width: 210, height: 297 },
A5: { width: 148, height: 210 },
Letter: { width: 216, height: 279 },
};
// 页面尺寸(单位:px,96dpi)
const PAGE_SIZES_PX = {
A4: { width: 794, height: 1123 },
A5: { width: 559, height: 794 },
Letter: { width: 816, height: 1056 },
};
/**
* 将多个页面元素导出为 PDF
* @param pages 页面元素数组
* @param options 导出选项
*/
export async function exportPagesToPdf(
pages: HTMLElement[],
options: ExportPdfOptions = {},
): Promise {
const {
filename = `contract-${Date.now()}.pdf`,
pageSize = 'A4',
quality = 4, // 提高默认分辨率,3 = 3倍缩放
} = options;
const pageConfig = PAGE_SIZES_MM[pageSize];
// 创建 PDF
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: [pageConfig.width, pageConfig.height],
});
// 逐页渲染
for (const [i, page] of pages.entries()) {
// 导出前隐藏不需要的元素(如已签署徽章)
const hiddenElements: HTMLElement[] = [];
page.querySelectorAll('.signed-badge').forEach((el) => {
const htmlEl = el as HTMLElement;
hiddenElements.push(htmlEl);
htmlEl.style.display = 'none';
});
// 导出前移除签名区域的边框和背景样式
const signedElements: { el: HTMLElement; originalStyle: string }[] = [];
page
.querySelectorAll('.signature-display.signed, .seal-display.signed')
.forEach((el) => {
const htmlEl = el as HTMLElement;
signedElements.push({
el: htmlEl,
originalStyle: htmlEl.getAttribute('style') || '',
});
htmlEl.style.border = 'none';
htmlEl.style.background = 'transparent';
htmlEl.style.padding = '0';
});
// 使用 html2canvas 将元素转换为 canvas
const canvas = await html2canvas(page, {
scale: quality,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
logging: false,
});
// 导出后恢复隐藏的元素
hiddenElements.forEach((el) => {
el.style.display = '';
});
// 导出后恢复签名区域的样式
signedElements.forEach(({ el, originalStyle }) => {
el.setAttribute('style', originalStyle);
});
// 计算图片尺寸(填满整页)
const imgData = canvas.toDataURL('image/jpeg', 0.95);
if (i > 0) {
pdf.addPage();
}
// 添加图片,填满整页
pdf.addImage(imgData, 'JPEG', 0, 0, pageConfig.width, pageConfig.height);
}
// 下载 PDF
pdf.save(filename);
}
/**
* 将 HTML 元素导出为 PDF
* @param element 要导出的 HTML 元素
* @param options 导出选项
*/
export async function exportToPdf(
element: HTMLElement,
options: ExportPdfOptions = {},
): Promise {
const {
filename = `contract-${Date.now()}.pdf`,
pageSize = 'A4',
pageMargin = { top: 10, right: 10, bottom: 10, left: 10 },
quality = 2,
} = options;
const pageConfig = PAGE_SIZES_MM[pageSize];
// 导出前隐藏不需要的元素(如已签署徽章)
const hiddenElements: HTMLElement[] = [];
element.querySelectorAll('.signed-badge').forEach((el) => {
const htmlEl = el as HTMLElement;
hiddenElements.push(htmlEl);
htmlEl.style.display = 'none';
});
// 导出前移除签名区域的边框和背景样式
const signedElements: { el: HTMLElement; originalStyle: string }[] = [];
element
.querySelectorAll('.signature-display.signed, .seal-display.signed')
.forEach((el) => {
const htmlEl = el as HTMLElement;
signedElements.push({
el: htmlEl,
originalStyle: htmlEl.getAttribute('style') || '',
});
htmlEl.style.border = 'none';
htmlEl.style.background = 'transparent';
htmlEl.style.padding = '0';
});
// 使用 html2canvas 将元素转换为 canvas
const canvas = await html2canvas(element, {
scale: quality,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
logging: false,
});
// 导出后恢复隐藏的元素
hiddenElements.forEach((el) => {
el.style.display = '';
});
// 导出后恢复签名区域的样式
signedElements.forEach(({ el, originalStyle }) => {
el.setAttribute('style', originalStyle);
});
// 创建 PDF
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: [pageConfig.width, pageConfig.height],
});
// 计算可用区域(减去边距)
const contentWidth = pageConfig.width - pageMargin.left - pageMargin.right;
const contentHeight = pageConfig.height - pageMargin.top - pageMargin.bottom;
// 计算图片尺寸(按宽度缩放)
const imgWidth = contentWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// 将 canvas 转换为图片数据
const imgData = canvas.toDataURL('image/jpeg', 0.95);
// 如果内容高度小于等于一页可用高度,直接添加
if (imgHeight <= contentHeight) {
pdf.addImage(
imgData,
'JPEG',
pageMargin.left,
pageMargin.top,
imgWidth,
imgHeight,
);
} else {
// 需要分页
let remainingHeight = imgHeight;
let sourceY = 0;
let pageIndex = 0;
while (remainingHeight > 0) {
if (pageIndex > 0) {
pdf.addPage();
}
// 计算当前页显示的内容高度
const currentPageContentHeight = Math.min(remainingHeight, contentHeight);
// 计算源图片中对应的像素高度
const sourceHeight =
(currentPageContentHeight / imgHeight) * canvas.height;
// 创建临时 canvas 来裁剪当前页的内容
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = sourceHeight;
const tempCtx = tempCanvas.getContext('2d');
if (tempCtx) {
tempCtx.drawImage(
canvas,
0,
sourceY,
canvas.width,
sourceHeight, // 源区域
0,
0,
canvas.width,
sourceHeight, // 目标区域
);
const pageImgData = tempCanvas.toDataURL('image/jpeg', 0.95);
pdf.addImage(
pageImgData,
'JPEG',
pageMargin.left,
pageMargin.top,
imgWidth,
currentPageContentHeight,
);
}
sourceY += sourceHeight;
remainingHeight -= contentHeight;
pageIndex++;
}
}
// 下载 PDF
pdf.save(filename);
}
/**
* 将合同渲染器导出为 PDF
* @param containerSelector 容器选择器
* @param options 导出选项
*/
export async function exportContractToPdf(
containerSelector: string,
options: ExportPdfOptions = {},
): Promise {
const container = document.querySelector(containerSelector) as HTMLElement;
if (!container) {
console.error('Container not found:', containerSelector);
return false;
}
// 查找合同纸张元素
const paper = container.querySelector('.contract-paper') as HTMLElement;
if (!paper) {
console.error('Contract paper not found');
return false;
}
try {
await exportToPdf(paper, options);
return true;
} catch (error) {
console.error('Export PDF failed:', error);
return false;
}
}