const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs-extra');
const dotenv = require('dotenv');
// 加载环境变量
dotenv.config();
const logFile = path.join(__dirname, '../logs/debug.log');
fs.ensureDirSync(path.dirname(logFile));
function logToFile(message) {
const timestamp = new Date().toISOString();
fs.appendFileSync(logFile, `${timestamp}: ${message}\n`);
}
class PDFGenerator {
constructor(options = {}) {
// 从环境变量获取PDF输出目录,支持绝对路径和相对路径
let outputDirFromEnv;
if (process.env.PDF_OUTPUT_DIR) {
// 检查是否是绝对路径
if (path.isAbsolute(process.env.PDF_OUTPUT_DIR)) {
outputDirFromEnv = process.env.PDF_OUTPUT_DIR;
logToFile(`使用绝对路径: ${outputDirFromEnv}`);
} else {
// 相对路径,相对于项目根目录
outputDirFromEnv = path.join(__dirname, '..', process.env.PDF_OUTPUT_DIR);
logToFile(`使用相对路径: ${outputDirFromEnv}`);
}
} else {
// 默认路径
outputDirFromEnv = path.join(__dirname, '../public/pdfs');
logToFile(`使用默认路径: ${outputDirFromEnv}`);
}
this.options = {
templatePath: path.join(__dirname, '../public/templates/report-template.html'),
outputDir: outputDirFromEnv,
...options
};
logToFile(`PDF输出目录设置为: ${this.options.outputDir}`);
}
async generatePDF(reportData) {
// 确保我们使用的是 data 字段中的数据
const data = reportData.data;
let browser = null;
try {
logToFile('开始生成PDF...');
logToFile('启动浏览器...');
browser = await puppeteer.launch({
headless: 'new',
defaultViewport: { width: 1200, height: 800 },
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
args: [
'--allow-file-access-from-files',
'--disable-web-security',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage'
],
timeout: 60000 // 增加浏览器启动超时时间
});
logToFile('浏览器启动成功');
const page = await browser.newPage();
logToFile('创建新页面成功');
// 设置页面超时时间
page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(60000);
logToFile('读取模板文件...');
let htmlContent = await fs.readFile(this.options.templatePath, 'utf-8');
logToFile('模板文件读取成功');
// 更新模板内容
logToFile('开始更新模板内容...');
htmlContent = await this.updateTemplateContent(htmlContent, data);
logToFile('模板内容更新完成');
logToFile('设置页面内容...');
try {
await page.setContent(htmlContent, {
waitUntil: 'domcontentloaded', // 改为domcontentloaded,不等待所有资源加载完成
timeout: 60000 // 增加超时时间
});
logToFile('页面内容设置完成');
} catch (error) {
logToFile(`设置页面内容失败: ${error.message}`);
// 尝试使用更宽松的加载策略
await page.setContent(htmlContent, {
waitUntil: 'load', // 只等待页面加载完成
timeout: 60000
});
logToFile('使用备用策略设置页面内容完成');
}
// 等待一段时间确保页面稳定
logToFile('等待页面稳定...');
await page.evaluate(() => {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
});
logToFile('页面稳定等待完成');
// 隐藏所有 print-only pdf-container 容器
logToFile('开始处理PDF容器...');
await page.evaluate(() => {
const containers = document.querySelectorAll('.print-only.pdf-container');
containers.forEach(container => {
container.style.display = 'none';
});
});
logToFile('PDF容器处理完成');
// 等待图片加载
logToFile('开始等待图片加载...');
try {
await page.evaluate(async () => {
console.log('开始检查图片加载状态...');
try {
const imageLoadResults = await Promise.all(
Array.from(document.images)
.map((img, index) => {
console.log(`图片 ${index}: ${img.src}`);
if (img.complete) {
console.log(`图片 ${index} 已完成加载`);
return Promise.resolve(img.src);
}
return new Promise((resolve, reject) => {
img.onload = () => {
console.log(`图片 ${index} 加载成功`);
resolve(img.src);
};
img.onerror = (e) => {
console.error(`图片 ${index} 加载失败: ${img.src}`);
resolve('error');
};
});
})
);
console.log('所有图片加载结果:', imageLoadResults);
} catch (error) {
console.error('图片加载过程中出错:', error);
}
});
logToFile('图片加载完成');
} catch (error) {
logToFile(`图片加载过程中出错: ${error.message}`);
// 继续执行,不中断流程
}
// 调整主检医生签名样式
await page.evaluate(() => {
const doctorImages = document.querySelectorAll('#summary-doctor-image, #summary-doctor-image2');
doctorImages.forEach(img => {
img.style.width = '30px';
img.style.height = '30px';
});
});
// 等待所有 PDF 渲染完成
console.log('等待 PDF 渲染完成...');
await page.evaluate(async () => {
return new Promise((resolve) => {
// 获取所有需要转换的图片容器
const containers = document.querySelectorAll('.ultrasound-image, .blood-image, .biochemistry-image, .urine-image, .dr-image, .hba1c-image');
if (containers.length === 0) {
console.log('没有找到需要渲染的图片容器');
resolve();
return;
}
let completedCount = 0;
const totalCount = containers.length;
// 为每个容器执行渲染
containers.forEach(async (container) => {
const pdfUrl = container.getAttribute('data-pdf-url');
if (pdfUrl) {
try {
await renderPDFAsImage(pdfUrl, container);
completedCount++;
console.log(`PDF渲染进度: ${completedCount}/${totalCount}`);
if (completedCount === totalCount) {
console.log('所有 PDF 渲染完成');
resolve();
}
} catch (error) {
console.error('PDF 渲染失败:', error);
completedCount++;
if (completedCount === totalCount) {
resolve();
}
}
} else {
completedCount++;
if (completedCount === totalCount) {
resolve();
}
}
});
// 设置超时保护
setTimeout(() => {
if (completedCount < totalCount) {
console.log('PDF 渲染超时,继续执行');
resolve();
}
}, 60000);
});
});
// 额外等待一段时间确保渲染完全完成
await new Promise(resolve => setTimeout(resolve, 2000));
// 添加额外的检查,确保所有加载提示都被隐藏
await page.evaluate(() => {
document.querySelectorAll('.pdf-container').forEach(container => {
const loadingElem = container.querySelector('.pdf-loading');
const imageContainer = container.querySelector('.pdf-image-container');
// 只要图片容器存在,就隐藏加载提示
if (imageContainer && loadingElem) {
loadingElem.style.display = 'none';
imageContainer.style.display = 'block'; // 确保图片容器显示
}
});
});
// 再次等待一段时间,确保所有更改都已应用
await new Promise(resolve => setTimeout(resolve, 1000));
// 处理汇总内容分页
if (data.summaryResult) {
await new Promise(resolve => {
page.evaluate((summaryResult) => {
if (typeof handleSummaryPagination === 'function') {
handleSummaryPagination(summaryResult);
const summaryPage2 = document.getElementById('summary-page-2');
if (summaryPage2) {
const content = document.getElementById('summary-content-2');
if (content && content.textContent.trim() !== '') {
summaryPage2.style.display = 'block';
}
}
}
}, data.summaryResult);
setTimeout(resolve, 2000);
});
}
// 处理糖化血红蛋白分页
await page.evaluate(() => {
const hba1cPage = document.getElementById('hba1c-report-page');
if (hba1cPage && hba1cPage.style.display !== 'none') {
if (typeof handleHba1cPagination === 'function') {
const hba1cText = document.getElementById('hba1c-summary-content')?.textContent;
handleHba1cPagination(hba1cText);
}
}
if (typeof updatePageNumbers === 'function') {
updatePageNumbers();
}
});
await new Promise(resolve => setTimeout(resolve, 1000));
// 确保输出目录存在
await fs.ensureDir(this.options.outputDir);
// 生成PDF文件名 - 修改为"体检编号-姓名"格式
const fileName = `${data.medicalSn || 'unknown'}-${data.pName || 'noname'}.pdf`;
const pdfPath = path.join(this.options.outputDir, fileName);
console.log('生成PDF...');
const pdf = await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true,
margin: {
top: '0',
right: '0',
bottom: '0',
left: '0'
},
preferCSSPageSize: true
});
// 在PDF生成完成后保存HTML用于调试
try {
const debugHtmlPath = path.join(__dirname, '../logs/last-report.html');
fs.writeFileSync(debugHtmlPath, htmlContent);
} catch (error) {
logToFile(`保存调试HTML出错: ${error.message}`);
}
return {
success: true,
fileName,
path: pdfPath
};
} catch (error) {
console.error('PDF生成失败:', error);
throw error;
} finally {
if (browser) {
await browser.close();
}
}
}
async updateTemplateContent(html, data) {
// 添加数据结构调试
console.log('Received data:', JSON.stringify(data, null, 2));
// 替换图片相对路径为base64编码
try {
const templateDir = path.dirname(this.options.templatePath);
const imagePath = path.join(templateDir, '首页标签.png');
console.log('图片路径:', imagePath);
// 读取图片文件并转换为Base64
const imageBuffer = await fs.readFile(imagePath);
const base64Image = `data:image/png;base64,${imageBuffer.toString('base64')}`;
console.log('图片已转换为base64格式');
// 替换所有图片引用
html = html.replace(/\s*
${summaryContent}
]*class="summary-content"[^>]*>).*?(<\/p>)/s,
`$1${data.summaryResult}$2`
);
// 更新主检医生签名
if (data.sign) {
// 更新第一页签名
const summaryDoctorImageRegex = /(]*id="summary-doctor-image"[^>]*>)/;
if (html.match(summaryDoctorImageRegex)) {
html = html.replace(summaryDoctorImageRegex, `
`);
}
// 更新第二页签名
const summaryDoctorImage2Regex = /(
]*id="summary-doctor-image2"[^>]*>)/;
if (html.match(summaryDoctorImage2Regex)) {
html = html.replace(summaryDoctorImage2Regex, `
`);
}
}
// 添加分页处理脚本到 HTML 中
const paginationScript = `
`;
html = html.replace('