Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user