报告打印PDF

This commit is contained in:
Euni4U 2025-04-15 11:38:14 +08:00
commit c023644ade
17 changed files with 9180 additions and 0 deletions

8
.env Normal file
View File

@ -0,0 +1,8 @@
# API基础地址
API_BASE_URL=https://pacs.gw12320.com/adminInspect/admin-api
# 服务端口
PORT=3000
# PDF输出目录可以是相对路径或绝对路径
# 相对路径示例public/pdfs
# 绝对路径示例D:/reports/pdfs
PDF_OUTPUT_DIR=public/pdfs

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

BIN
chrome/chrome.exe Normal file

Binary file not shown.

26
logs/debug.log Normal file
View File

@ -0,0 +1,26 @@
2025-04-07T07:41:54.892Z: PDF输出目录设置为: D:\桌面\个人\开发环境\report-service\public\pdfs
2025-04-07T07:42:17.998Z: 找到小结内容,来源项目: "身高", 内容: "未见异常..."
2025-04-07T07:42:18.000Z: 第一个数据项的完整结构: {"itemCode":"TW001","itemName":"体温","examDescription":null,"itemResult":"36","itemValue":36,"itemStatus":"1","analyse":"未见异常","inspectdoctor":"丁国凯","inspecttime":1743996963000,"pacsData":null,"pacsDataType":null,"data":null,"type":"体温"}
2025-04-07T07:42:18.005Z: 替换完成,内容长度: 4
2025-04-07T07:42:18.078Z - 替换后的内容: <div class="report-summary" id="ultrasound-summary">
<p>
<strong...
2025-04-07T07:42:33.577Z: 调试HTML已保存至: D:\桌面\个人\开发环境\report-service\logs\last-report.html
2025-04-07T07:45:44.525Z: 使用绝对路径: D:/桌面/1
2025-04-07T07:45:44.527Z: PDF输出目录设置为: D:/桌面/1
2025-04-07T07:45:50.538Z: 找到小结内容,来源项目: "身高", 内容: "未见异常..."
2025-04-07T07:45:50.540Z: 第一个数据项的完整结构: {"itemCode":"TW001","itemName":"体温","examDescription":null,"itemResult":"36","itemValue":36,"itemStatus":"1","analyse":"未见异常","inspectdoctor":"丁国凯","inspecttime":1743996963000,"pacsData":null,"pacsDataType":null,"data":null,"type":"体温"}
2025-04-07T07:45:50.544Z: 替换完成,内容长度: 4
2025-04-07T07:45:50.624Z - 替换后的内容: <div class="report-summary" id="ultrasound-summary">
<p>
<strong...
2025-04-07T07:46:03.524Z: 调试HTML已保存至: D:\桌面\个人\开发环境\report-service\logs\last-report.html
2025-04-07T07:48:10.657Z: 找到小结内容,来源项目: "身高", 内容: "未见异常..."
2025-04-07T07:48:10.657Z: 第一个数据项的完整结构: {"itemCode":"TW001","itemName":"体温","examDescription":null,"itemResult":"36","itemValue":36,"itemStatus":"1","analyse":"未见异常","inspectdoctor":"丁国凯","inspecttime":1743996963000,"pacsData":null,"pacsDataType":null,"data":null,"type":"体温"}
2025-04-07T07:48:10.659Z: 替换完成,内容长度: 4
2025-04-07T07:48:10.702Z - 替换后的内容: <div class="report-summary" id="ultrasound-summary">
<p>
<strong...
2025-04-07T07:48:21.063Z: 调试HTML已保存至: D:\桌面\个人\开发环境\report-service\logs\last-report.html
2025-04-07T09:30:53.039Z: 使用绝对路径: D:/桌面/1
2025-04-07T09:30:53.040Z: PDF输出目录设置为: D:/桌面/1

2247
logs/last-report.html Normal file

File diff suppressed because one or more lines are too long

2089
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "report-service",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.8.4",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"path": "^0.12.7",
"puppeteer": "^24.4.0"
}
}

1521
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/test.html Normal file

Binary file not shown.

97
routes/report.js Normal file
View File

@ -0,0 +1,97 @@
const express = require('express');
const router = express.Router();
const PDFGenerator = require('../utils/pdfGenerator');
const path = require('path');
const fs = require('fs-extra');
const axios = require('axios');
const pdfGenerator = new PDFGenerator();
// 从配置文件读取基础URL
const BASE_URL = process.env.API_BASE_URL || 'http://192.168.1.100:8080'; // 请替换为实际的医疗系统地址
// 从医疗系统获取报告数据
async function getReportData(medicalSn) {
try {
console.log(`正在请求医疗系统API: ${BASE_URL}/inspect/patient/getReportAll`);
console.log('请求参数:', { medicalSn });
const response = await axios.get(`${BASE_URL}/inspect/patient/getReportAll`, {
params: {
medicalSn: medicalSn
}
});
return response.data;
} catch (error) {
console.error('获取报告数据失败:', {
message: error.message,
code: error.code,
response: error.response?.data,
status: error.response?.status
});
throw error;
}
}
// 生成PDF的路由
router.post('/generate', async (req, res) => {
try {
console.log('收到生成PDF请求:', req.body);
const { medicalSn } = req.body;
if (!medicalSn) {
throw new Error('体检编号不能为空');
}
const reportData = await getReportData(medicalSn);
// 检查是否为无效数据
const isInvalidData = reportData.code === 0 &&
reportData.data &&
Object.values(reportData.data).every(value => value === null);
if (!reportData || isInvalidData) {
throw new Error('体检编号不存在或未查询到相关报告数据');
}
console.log('开始生成PDF...');
const result = await pdfGenerator.generatePDF(reportData);
console.log('PDF生成成功:', result);
res.json({
success: true,
data: {
fileName: result.fileName,
path: `/pdfs/${result.fileName}`
}
});
} catch (error) {
console.error('生成PDF失败详细错误:', error);
res.status(500).json({
success: false,
message: error.message || '生成PDF失败'
});
}
});
// 下载PDF的路由
router.get('/download/:fileName', async (req, res) => {
try {
const { fileName } = req.params;
const filePath = path.join(__dirname, '../public/pdfs', fileName);
const exists = await fs.pathExists(filePath);
if (!exists) {
return res.status(404).json({
success: false,
message: 'PDF文件不存在'
});
}
res.download(filePath);
} catch (error) {
console.error('下载PDF失败:', error);
res.status(500).json({
success: false,
message: '下载PDF失败'
});
}
});
module.exports = router;

42
server.js Normal file
View File

@ -0,0 +1,42 @@
// 加载环境变量
const dotenv = require('dotenv');
const path = require('path');
// 加载环境变量
dotenv.config();
const express = require('express');
const cors = require('cors');
const reportRoutes = require('./routes/report');
const app = express();
// 中间件
app.use(cors());
app.use(express.json());
// 静态文件服务
app.use(express.static(path.join(__dirname, 'public')));
// 路由
app.use('/api/report', reportRoutes);
// 添加模板预览路由
app.get('/preview-template', (req, res) => {
res.sendFile(path.join(__dirname, 'public/templates/report-template.html'));
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
console.log(`API基础地址: ${process.env.API_BASE_URL}`);
});

31
service_log.txt Normal file
View File

@ -0,0 +1,31 @@
启动时间: 周一 2025/04/07 17:30:48.68
[信息] 检查 Node.js 安装状态...
[成功] Node.js 已安装
v22.11.0
[信息] 检查 PM2 安装状态...
[成功] PM2 已安装
当前工作目录: D:\桌面\个人\开发环境\report-service
[信息] 检查 package.json...
[成功] 找到 package.json
[信息] 正在安装项目依赖...
up to date in 1s
24 packages are looking for funding
run `npm fund` for details
[成功] 依赖安装完成
[信息] 服务已在运行,正在重启...
Use --update-env to update environment variables
[PM2] Applying action restartProcessId on app [report-service](ids: [ 0 ])
[PM2] [report-service](0) ✓
┌────┬───────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼───────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ report-service │ default │ 1.0.0 │ fork │ 30000 │ 0s │ 1 │ online │ 0% │ 55.1mb │ 桜 │ disabled │
└────┴───────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
┌────┬───────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼───────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ report-service │ default │ 1.0.0 │ fork │ 30000 │ 2s │ 1 │ online │ 0% │ 69.9mb │ 桜 │ disabled │
└────┴───────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
[成功] 服务已启动!

124
start-service.bat Normal file
View File

@ -0,0 +1,124 @@
@echo off
chcp 65001 >nul
title 报告服务启动程序
:: 创建日志文件
echo 启动时间: %date% %time% > service_log.txt
:: 设置控制台颜色
color 0A
echo ======================================
echo 报告服务启动程序
echo ======================================
echo.
:: 检查 Node.js 是否安装
echo [信息] 检查 Node.js 安装状态... >> service_log.txt
where node >nul 2>nul
if %errorlevel% neq 0 (
echo [错误] 未安装 Node.js请先安装 Node.js
echo [错误] 未安装 Node.js请先安装 Node.js >> service_log.txt
echo 请访问 https://nodejs.org 下载安装
echo.
echo 按任意键退出...
pause > nul
exit /b 1
)
echo [成功] Node.js 已安装 >> service_log.txt
:: 显示 Node.js 版本
echo [信息] Node.js 版本:
node -v
node -v >> service_log.txt
:: 检查是否已安装 PM2
echo [信息] 检查 PM2 安装状态... >> service_log.txt
where pm2 >nul 2>nul
if %errorlevel% neq 0 (
echo [信息] 正在安装 PM2...
echo [信息] 正在安装 PM2... >> service_log.txt
call npm install -g pm2
if %errorlevel% neq 0 (
echo [错误] PM2 安装失败,请以管理员权限运行此脚本
echo [错误] PM2 安装失败 >> service_log.txt
echo 请右键选择"以管理员身份运行"此脚本
echo.
echo 按任意键退出...
pause > nul
exit /b 1
)
)
echo [成功] PM2 已安装 >> service_log.txt
:: 切换到脚本所在目录
echo [信息] 切换工作目录... >> service_log.txt
cd /d "%~dp0"
echo 当前工作目录: %cd%
echo 当前工作目录: %cd% >> service_log.txt
:: 检查是否存在 package.json
echo [信息] 检查 package.json... >> service_log.txt
dir package.json 2>nul | find "package.json" >nul
if %errorlevel% neq 0 (
echo [错误] 未找到 package.json 文件
echo [错误] 未找到 package.json 文件 >> service_log.txt
echo 请确保在正确的项目目录中运行此脚本
echo 当前目录: %cd%
echo.
echo 目录内容:
dir
echo.
echo 按任意键退出...
pause > nul
exit /b 1
)
echo [成功] 找到 package.json >> service_log.txt
:: 安装依赖
echo [信息] 正在安装项目依赖...
echo [信息] 正在安装项目依赖... >> service_log.txt
call npm install >> service_log.txt 2>&1
if %errorlevel% neq 0 (
echo [错误] 依赖安装失败
echo [错误] 依赖安装失败 >> service_log.txt
echo 详细错误信息请查看 service_log.txt
echo.
echo 按任意键退出...
pause > nul
exit /b 1
)
echo [成功] 依赖安装完成 >> service_log.txt
:: 检查服务是否已在运行
echo [信息] 检查服务状态... >> service_log.txt
call pm2 list | findstr "report-service" >nul
if %errorlevel% equ 0 (
echo [信息] 服务已在运行,正在重启...
echo [信息] 服务已在运行,正在重启... >> service_log.txt
call pm2 restart report-service >> service_log.txt 2>&1
) else (
echo [信息] 正在启动服务...
echo [信息] 正在启动服务... >> service_log.txt
call pm2 start server.js --name "report-service" >> service_log.txt 2>&1
)
:: 显示服务状态
echo.
echo [信息] 服务状态:
call pm2 list
call pm2 list >> service_log.txt
echo.
echo [成功] 服务已启动!
echo [成功] 服务已启动! >> service_log.txt
echo 访问地址: http://localhost:3000
echo.
echo 提示:请不要关闭此窗口,服务将在后台继续运行
echo 如需停止服务,请运行 stop-service.bat
echo 如遇问题,请查看 service_log.txt 文件了解详细信息
echo ======================================
echo.
echo 按任意键继续...
pause > nul

37
stop-service.bat Normal file
View File

@ -0,0 +1,37 @@
@echo off
chcp 65001 >nul
title 报告服务停止程序
:: 设置控制台颜色
color 0C
echo ======================================
echo 报告服务停止程序
echo ======================================
echo.
:: 检查 PM2 是否运行
where pm2 >nul 2>nul
if %errorlevel% neq 0 (
echo [错误] PM2 未安装,服务可能未在运行
pause
exit /b
)
:: 检查服务是否在运行
call pm2 list | findstr "report-service" >nul
if %errorlevel% neq 0 (
echo [信息] 服务当前未运行
pause
exit /b
)
:: 停止服务
echo [信息] 正在停止服务...
call pm2 stop report-service
call pm2 delete report-service
echo.
echo [成功] 服务已停止!
echo ======================================
pause

856
utils/pdfGenerator.js Normal file
View File

@ -0,0 +1,856 @@
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');
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 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(/<img\s+src="首页标签.png"/g, `<img src="${base64Image}"`);
} catch (error) {
console.error('处理图片时出错:', error);
}
// 计算年龄
let age = '--';
if (data.birthday) {
const birthDate = new Date(data.birthday);
const today = new Date();
let calculatedAge = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
calculatedAge--;
}
age = calculatedAge.toString();
}
// 更新所有报告标题中的填充值
const reportTitleRegex = /<div class="report-title">\s*<div[^>]*>\s*<span[^>]*>.*?<\/span>\s*<span[^>]*>填充值<\/span>\s*<\/div>\s*<\/div>/g;
const replacement = `<div class="report-title">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span></span>
<span>${data.medicalSn || '--'} ${data.pName || '--'} ${data.gender || '--'} ${age}</span>
</div>
</div>`;
html = html.replace(reportTitleRegex, replacement);
// 更新体检编号
const reportTitleRegex2 = /体检编号:.*?</;
html = html.replace(reportTitleRegex2, `体检编号:${data.medicalSn || '--'}<`);
// 更新头像
if (data.headPicUrl) {
const avatarSrc = data.headPicUrl.startsWith('data:image') ? data.headPicUrl : `data:image/jpeg;base64,${data.headPicUrl}`;
html = html.replace(/<img[^>]*class="avatar-image"[^>]*>/g, `<img src="${avatarSrc}" alt="头像" class="avatar-image" id="avatar-image">`);
}
// 格式化体检日期
let examDate = '--';
if (data.medicalDateTime) {
const date = new Date(data.medicalDateTime);
examDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
// 更新个人信息
const personalInfoMap = {
'姓名': data.pName || '--',
'性别': data.gender || '--',
'年龄': age,
'身份证号': data.cardId || '--',
'联系电话': data.phoneNum || '--',
'体检日期': examDate
};
// 遍历并更新每个字段
Object.entries(personalInfoMap).forEach(([title, value]) => {
// 根据字段名称确定是否需要 letter-spacing 样式
const needsLetterSpacing = ['身份证号', '联系电话', '体检日期'].includes(title);
// 构建正则表达式
let regex;
if (needsLetterSpacing) {
// 对于需要 letter-spacing 的字段,匹配带有样式的标签
regex = new RegExp(`<span class="person_title" style="letter-spacing: 4px;">${title}</span><span[^>]*class="person_content"[^>]*>[^<]*</span>`);
} else {
// 对于普通字段,匹配不带样式的标签
regex = new RegExp(`<span class="person_title">${title}</span><span[^>]*class="person_content"[^>]*>[^<]*</span>`);
}
const matches = html.match(regex);
if (matches) {
// 构建替换内容,保持原有样式
const replacement = needsLetterSpacing
? `<span class="person_title" style="letter-spacing: 4px;">${title}</span><span class="person_content">${value}</span>`
: `<span class="person_title">${title}</span><span class="person_content">${value}</span>`;
html = html.replace(regex, replacement);
}
});
// 更新前言中的姓名
const prefaceNameRegex = /<span class="underline">.*?<\/span>/;
if (data.pName) {
html = html.replace(prefaceNameRegex, `<span class="underline">${data.pName}</span>`);
}
// 更新一般检查数据
if (data.data && Array.isArray(data.data)) {
// 使用更精确的正则表达式来匹配表格单元格
const examData = {
'体温': `${data.data.find(item => item.itemName === '体温')?.itemResult || '--'}`,
'脉率': `${data.data.find(item => item.itemName === '脉率')?.itemResult || '--'}`,
'呼吸频率': `${data.data.find(item => item.itemName === '呼吸频率')?.itemResult || '--'}`,
'血压': `${data.data.find(item => item.itemName === '血压')?.itemResult || '--'}`,
'身高': `${data.data.find(item => item.itemName === '身高')?.itemResult || '--'}`,
'体重': `${data.data.find(item => item.itemName === '体重')?.itemResult || '--'}`,
'腰围': `${data.data.find(item => item.itemName === '腰围')?.itemResult || '--'}`,
'体质指数': `${data.data.find(item => item.itemName === '体质指数(BMI)')?.itemResult || '--'}`
};
// 添加单位
const units = {
'体温': '℃',
'脉率': '次/分',
'呼吸频率': '次/分',
'血压': 'mmHg',
'身高': 'cm',
'体重': 'kg',
'腰围': 'cm',
'体质指数': 'kg/m²'
};
// 遍历并更新每个检查项目
Object.entries(examData).forEach(([key, value]) => {
if (value !== '--') {
const unit = units[key] || '';
// 对于 BMI使用特殊的正则表达式
const regex = key === '体质指数'
? new RegExp(`<td>体质指数\\(BMI\\)</td>\\s*<td[^>]*>.*?</td>`, 'g')
: new RegExp(`<td>${key}</td>\\s*<td[^>]*>.*?</td>`, 'g');
const replacement = key === '体质指数'
? `<td>体质指数(BMI)</td><td>${value}${unit}</td>`
: `<td>${key}</td><td>${value}${unit}</td>`;
html = html.replace(regex, replacement);
}
});
// 查找身高项目数据
const heightData = data.data.find(item => item.itemName === '身高');
// 尝试多种可能的项目名和字段
let summaryItem = null;
let summaryContent = '--';
// 1. 先记录所有带有analyse字段的项目
const itemsWithAnalyse = data.data.filter(item => item.analyse && item.analyse.trim() !== '--');
// 2. 优先使用身高项目
summaryItem = data.data.find(item =>
item.itemName === '身高' && item.analyse && item.analyse.trim() !== '--'
);
// 3. 如果没找到,尝试一般检查
if (!summaryItem) {
summaryItem = data.data.find(item =>
(item.itemName === '一般检查' || item.itemName === '体格检查') &&
item.analyse && item.analyse.trim() !== '--'
);
}
// 4. 如果还没找到尝试使用第一个有analyse的项目
if (!summaryItem && itemsWithAnalyse.length > 0) {
summaryItem = itemsWithAnalyse[0];
}
// 5. 记录找到的结果
if (summaryItem) {
summaryContent = summaryItem.analyse;
} else {
logToFile(`未找到任何带有analyse字段的项目使用默认值"--"`);
}
// 使用找到的内容替换
try {
html = html.replace(
/<div class="report-summary" id="general-exam-summary">[\s\S]*?<\/div>/s,
`<div class="report-summary" id="general-exam-summary">
<h4>一般检查小结</h4>
<p id="general-summary-content">${summaryContent}</p>
</div>`
);
} catch (error) {
logToFile(`替换出错: ${error.message}`);
}
}
// 更新体质辨识结果
html = html.replace(
/(<td width="50%" class="constitution-value" style="text-align: center;">).*?(<\/td>)/s,
`$1${data.zybs || '--'}$2`
);
// 更新汇总结果
if (data.summaryResult) {
// 先更新第一页内容
html = html.replace(
/(<p id="summary-content-1"[^>]*class="summary-content"[^>]*>).*?(<\/p>)/s,
`$1${data.summaryResult}$2`
);
// 更新主检医生签名
if (data.sign) {
// 更新第一页签名
const summaryDoctorImageRegex = /(<img[^>]*id="summary-doctor-image"[^>]*>)/;
if (html.match(summaryDoctorImageRegex)) {
html = html.replace(summaryDoctorImageRegex, `<img id="summary-doctor-image" src="${data.sign}" alt="主检医生签名">`);
}
// 更新第二页签名
const summaryDoctorImage2Regex = /(<img[^>]*id="summary-doctor-image2"[^>]*>)/;
if (html.match(summaryDoctorImage2Regex)) {
html = html.replace(summaryDoctorImage2Regex, `<img id="summary-doctor-image2" src="${data.sign}" alt="主检医生签名">`);
}
}
// 添加分页处理脚本到 HTML 中
const paginationScript = `
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof handleSummaryPagination === 'function') {
handleSummaryPagination('${data.summaryResult.replace(/'/g, "\\'")}');
}
});
</script>
`;
html = html.replace('</body>', `${paginationScript}</body>`);
}
// 更新体质结果详情
if (data.features) {
html = html.replace(
/(<div id="constitution-detail-container".*?>).*?(<\/div>)/s,
`$1${data.features.replace(/\\r\\n/g, '<br>')}$2`
);
}
// 更新图片和 PDF 链接
if (data.data && Array.isArray(data.data)) {
data.data.forEach((item) => {
if (item.data) {
let contentId = '';
let summaryId = '';
let pdfContainerId = '';
switch (item.pacsDataType) {
case 'cbc':
contentId = 'blood-exam-content';
summaryId = 'blood-summary-div';
pdfContainerId = 'blood-exam-pdf-container';
break;
case 'ecg':
contentId = 'ecg-exam-content';
summaryId = 'ecg-summary';
break;
case 'bt':
contentId = 'biochemistry-exam-content';
summaryId = 'biochemistry-summary-div';
pdfContainerId = 'biochemistry-exam-pdf-container';
break;
case 'US':
contentId = 'ultrasound-exam-content';
summaryId = 'ultrasound-summary';
break;
case 'rt':
contentId = 'urine-exam-content';
summaryId = 'urine-summary-div';
pdfContainerId = 'urine-exam-pdf-container';
break;
default:
break;
}
if (contentId) {
console.log(`处理报告类型: ${item.pacsDataType}, 内容ID: ${contentId}`);
// 构建正则表达式来匹配特定ID的内容区域
const contentRegex = new RegExp(
`(<div class="report-content" id="${contentId}">)(.*?)(</div>)`, 's'
);
// 为PDF URL添加参数
const pdfUrl = item.data + '#toolbar=0&navpanes=0&view=Fit';
console.log(`PDF URL: ${pdfUrl}`);
// 准备替换内容
let replacement;
if (item.pacsDataType === 'ecg') {
// 心电图使用img标签
replacement = `$1<img src="${item.data}" alt="心电图检查">$3`;
// 更新心电图所见所得
if (item.analyse) {
console.log(`处理心电图报告ID: ${summaryId}`);
console.log('原始analyse:', item.analyse);
// 对于心电图,使用 analyse 按行分割
const analyseLines = item.analyse.split('\n');
const findings = analyseLines[0]?.replace('检查所见:', '') || '--';
const conclusion = analyseLines[1]?.replace('检查结果:', '') || '--';
console.log('处理后的所见:', findings);
console.log('处理后的所得:', conclusion);
// 更新所见所得
const findingsRegex = new RegExp(
`(<div[^>]*id="ecg-summary"[^>]*>).*?(</div>)`, 's'
);
const findingsMatch = html.match(findingsRegex);
console.log('所见正则匹配结果:', findingsMatch ? '成功' : '失败');
html = html.replace(findingsRegex, `$1<p><strong>【所见】</strong><br>${findings}</p><p><strong>【所得】</strong><br>${conclusion}</p>$2`);
}
} else if (item.pacsDataType === 'US') {
// 超声检查报告处理 - 设置 PDF URL
replacement = `$1<img src="" alt="超声检查报告" class="ultrasound-image" data-pdf-url="${pdfUrl}">$3`;
// 更新超声所见所得
if (item.analyse) {
// 使用与心电图报告相同的处理逻辑
console.log(`处理超声报告ID: ${summaryId}`);
// 记录分析内容
const logMessage = `超声报告分析内容: ${item.analyse}`;
console.log(logMessage);
// 同时写入文件日志
// fs.appendFileSync(path.join(__dirname, '../logs/debug.log'),
//`${new Date().toISOString()} - ${logMessage}\n`);
// 处理分析结果
const analyseLines = item.analyse.split('\n');
const findings = analyseLines[0]?.replace('检查所见:', '') || '--';
const conclusion = analyseLines[1]?.replace('检查结果:', '') || '--';
// 使用更精确的正则表达式匹配模板中的结构
const findingsRegex = new RegExp(
`(<div class="report-summary" id="ultrasound-summary">\\s*<p>\\s*<strong>【所见】</strong>\\s*<br>)[^<]*(</p>\\s*<p>\\s*<strong>【所得】</strong>\\s*<br>)[^<]*(</p>\\s*</div>)`, 's'
);
const findingsMatch = html.match(findingsRegex);
// 记录匹配结果
const matchResult = `超声所见所得匹配结果: ${findingsMatch ? '成功' : '失败'}`;
console.log(matchResult);
//fs.appendFileSync(path.join(__dirname, '../logs/debug.log'),
//`${new Date().toISOString()} - ${matchResult}\n`);
// 使用更精确的替换保持原有的HTML结构
html = html.replace(findingsRegex, `$1${findings}$2${conclusion}$3`);
// 检查替换后的内容
const afterReplacement = html.match(/<div class="report-summary" id="ultrasound-summary">[\s\S]*?<\/div>/);
const resultMessage = `替换后的内容: ${afterReplacement ? afterReplacement[0].substring(0, 100) + '...' : '未找到'}`;
console.log(resultMessage);
fs.appendFileSync(path.join(__dirname, '../logs/debug.log'),
`${new Date().toISOString()} - ${resultMessage}\n`);
}
} else if (item.pacsDataType === 'cbc') {
// 血常规PDF处理
console.log('处理血常规数据:', item);
console.log('血常规data字段:', item.data);
if (!item.data) {
console.error('血常规数据中缺少data字段');
return;
}
const pdfUrl = item.data + '#toolbar=0&navpanes=0&view=Fit';
console.log(`血常规PDF URL: ${pdfUrl}`);
// 检查是否为弃检项目
if (item.itemStatus === "2") {
console.log('血常规项目已弃检');
// 隐藏内容区域
const contentRegex = new RegExp(
`(<div[^>]*id="blood-exam-content"[^>]*>).*?(</div>)`, 's'
);
html = html.replace(contentRegex, `$1<div style="display:none;">$2</div>`);
// 更新小结区域为弃检提示
const summaryRegex = new RegExp(
`(<div[^>]*id="blood-summary-div"[^>]*>).*?(</div>)`, 's'
);
const abandonNotice = '<p class="abandon-notice" style="color:red;font-weight:bold;text-align:center;padding:20px;">该项目已弃检</p>';
html = html.replace(summaryRegex, `$1${abandonNotice}$2`);
} else {
// 构建替换内容
replacement = `$1<img src="" alt="血常规检查报告" class="blood-image" data-pdf-url="${pdfUrl}">$3`;
// 更新小结内容
if (item.analyse) {
console.log('更新血常规小结内容:', item.analyse);
const summaryRegex = new RegExp(
`(<div[^>]*id="blood-summary-div"[^>]*>\\s*<h4>血常规小结:</h4>\\s*<p[^>]*style="white-space: pre-line"[^>]*>).*?(</p>)`, 's'
);
html = html.replace(summaryRegex, `$1${item.analyse}$2`);
}
}
} else if (item.pacsDataType === 'bt') {
// 生化检查报告处理
if (!item.data) {
console.error('生化数据中缺少data字段');
return;
}
const pdfUrl = item.data + '#toolbar=0&navpanes=0&view=Fit';
// 检查是否为弃检项目
if (item.itemStatus === "2") {
// 隐藏内容区域
const contentRegex = new RegExp(
`(<div[^>]*id="biochemistry-exam-content"[^>]*>).*?(</div>)`, 's'
);
html = html.replace(contentRegex, `$1<div style="display:none;">$2</div>`);
// 更新小结区域为弃检提示
const summaryRegex = new RegExp(
`(<div[^>]*id="biochemistry-summary-div"[^>]*>).*?(</div>)`, 's'
);
const abandonNotice = '<p class="abandon-notice" style="color:red;font-weight:bold;text-align:center;padding:20px;">该项目已弃检</p>';
html = html.replace(summaryRegex, `$1${abandonNotice}$2`);
} else {
// 构建替换内容
replacement = `$1<img src="" alt="生化检查报告" class="biochemistry-image" data-pdf-url="${pdfUrl}">$3`;
// 更新小结内容
if (item.analyse) {
const summaryRegex = new RegExp(
`(<div[^>]*id="biochemistry-summary-div"[^>]*>\\s*<h4>生化小结:</h4>\\s*<p[^>]*style="white-space: pre-line"[^>]*>).*?(</p>)`, 's'
);
html = html.replace(summaryRegex, `$1${item.analyse}$2`);
}
}
} else if (item.pacsDataType === 'rt') {
// 尿常规检查报告处理
if (!item.data) {
console.error('尿常规数据中缺少data字段');
return;
}
const pdfUrl = item.data + '#toolbar=0&navpanes=0&view=Fit';
// 检查是否为弃检项目
if (item.itemStatus === "2") {
// 隐藏内容区域
const contentRegex = new RegExp(
`(<div[^>]*id="urine-exam-content"[^>]*>).*?(</div>)`, 's'
);
html = html.replace(contentRegex, `$1<div style="display:none;">$2</div>`);
// 更新小结区域为弃检提示
const summaryRegex = new RegExp(
`(<div[^>]*id="urine-summary-div"[^>]*>).*?(</div>)`, 's'
);
const abandonNotice = '<p class="abandon-notice" style="color:red;font-weight:bold;text-align:center;padding:20px;">该项目已弃检</p>';
html = html.replace(summaryRegex, `$1${abandonNotice}$2`);
} else {
// 构建替换内容
replacement = `$1<img src="" alt="尿常规检查报告" class="urine-image" data-pdf-url="${pdfUrl}">$3`;
// 更新小结内容
if (item.analyse) {
const summaryRegex = new RegExp(
`(<div[^>]*id="urine-summary-div"[^>]*>\\s*<h4>尿常规小结:</h4>\\s*<p[^>]*style="white-space: pre-line"[^>]*>).*?(</p>)`, 's'
);
html = html.replace(summaryRegex, `$1${item.analyse}$2`);
}
}
}
// 执行替换
const match = html.match(contentRegex);
if (match) {
console.log(`成功匹配内容区域: ${contentId}`);
html = html.replace(contentRegex, replacement);
} else {
console.log(`未找到内容区域: ${contentId}`);
}
}
}
});
}
console.log('一般检查数据:',
JSON.stringify(data.data.find(item => item.itemName === '一般检查'), null, 2)
);
console.log('替换后的小结内容:',
html.match(/<div class="report-summary" id="general-exam-summary">([\s\S]*?)<\/div>/)[0]
);
return html;
}
replaceExamContent(html, examType, content) {
const examIds = {
'超声': 'ultrasound',
'尿常规': 'urine',
'生化': 'biochemistry',
'血常规': 'blood',
'心电图': 'ecg'
};
const id = examIds[examType];
if (id) {
const regex = new RegExp(`(<div id="${id}-summary".*?>).*?(<\/div>)`, 's');
return html.replace(regex, `$1${content}$2`);
}
return html;
}
updatePdfUrl(html, examType, pdfUrl) {
const examIds = {
'超声': 'ultrasound',
'生化': 'biochemistry',
'血常规': 'blood'
};
const id = examIds[examType];
if (id) {
// 更新屏幕显示的iframe
const iframeRegex = new RegExp(`(<div class="${id}-exam".*?<iframe.*?src=").*?(")`);
html = html.replace(iframeRegex, `$1${pdfUrl}$2`);
// 更新打印用的PDF容器
const containerRegex = new RegExp(`(<div class="${id}-exam".*?data-pdf-url=").*?(")`);
html = html.replace(containerRegex, `$1${pdfUrl}$2`);
}
return html;
}
generateExamHtml(examType, examData) {
if (examType === '超声' || examType === '心电图') {
const [findings = '', conclusion = ''] = (examData.analyse || '').split('\n');
return `
<p><strong>所见</strong><br>${findings.replace('', '')}</p>
<p><strong>所得</strong><br>${conclusion.replace('', '')}</p>
`;
} else {
return `<p>${examData.analyse || ''}</p>`;
}
}
}
module.exports = PDFGenerator;