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; } }