284 lines
7.4 KiB
TypeScript
284 lines
7.4 KiB
TypeScript
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;
|
||
}
|
||
}
|