Files
ai-agent-admin/web/apps/web-ele/src/components/contract-design/utils/exportPdf.ts
T
2026-06-08 18:14:59 +08:00

284 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
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<void> {
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<boolean> {
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;
}
}