Fix PDF generation and report upload: Fix pagination, footer font, backend upload logic, and UI handlers
This commit is contained in:
parent
bf359d82cd
commit
2772a2a9f2
@ -1443,6 +1443,55 @@ class AppServer:
|
||||
self.logger.error(f'保存会话信息失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
# ==================== 报告上传API ====================
|
||||
|
||||
@self.app.route('/api/reports/<session_id>/upload', methods=['POST'])
|
||||
def upload_report_pdf(session_id):
|
||||
"""接收前端生成的PDF并保存到文件系统,同时更新会话报告路径"""
|
||||
try:
|
||||
if not self.db_manager:
|
||||
return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500
|
||||
file = flask_request.files.get('file')
|
||||
if not file:
|
||||
return jsonify({'success': False, 'error': '缺少文件'}), 400
|
||||
# 获取会话信息以得到 patient_id
|
||||
session_data = self.db_manager.get_session_data(session_id)
|
||||
if not session_data:
|
||||
return jsonify({'success': False, 'error': '检测会话不存在'}), 404
|
||||
|
||||
patient_id = session_data.get('patient_id')
|
||||
if not patient_id:
|
||||
return jsonify({'success': False, 'error': '无法获取患者ID'}), 400
|
||||
|
||||
# 选择文件根目录:配置 FILEPATH.path
|
||||
base_dir_cfg = self.config_manager.get_config_value('FILEPATH', 'path', fallback=None) if self.config_manager else None
|
||||
if not base_dir_cfg:
|
||||
return jsonify({'success': False, 'error': '未配置文件存储路径'}), 500
|
||||
|
||||
base_dir = os.path.abspath(base_dir_cfg)
|
||||
timestamp = datetime.now().strftime('%H%M%S%f')[:-3] # 精确到毫秒
|
||||
|
||||
# 构建存储路径
|
||||
base_path = os.path.join(base_dir, str(patient_id), str(session_id))
|
||||
db_base_path = os.path.join(str(patient_id), str(session_id))
|
||||
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
|
||||
filename = f"report_{timestamp}.pdf"
|
||||
abs_path = os.path.join(base_path, filename)
|
||||
|
||||
file.save(abs_path)
|
||||
|
||||
# 生成相对路径存入数据库
|
||||
rel_path = os.path.join(db_base_path, filename).replace('\\', '/')
|
||||
self.db_manager.update_session_report_path(session_id, rel_path)
|
||||
return jsonify({'success': True, 'path': rel_path})
|
||||
except Exception as e:
|
||||
self.logger.error(f'上传报告失败: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
|
||||
@self.app.route('/api/detection/<session_id>/save-data', methods=['POST'])
|
||||
def save_detection_data(session_id):
|
||||
"""采集检测数据"""
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
@ -8,6 +8,100 @@ let mainWindow;
|
||||
let localServer;
|
||||
let backendProcess;
|
||||
|
||||
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) throw new Error('窗口未找到');
|
||||
|
||||
// 1. 准备打印环境:克隆节点到独立的打印容器,确保流式布局
|
||||
await win.webContents.executeJavaScript(`
|
||||
(function(){
|
||||
const selector = '${(payload && payload.selector) ? payload.selector : '#report-root'}';
|
||||
const root = document.querySelector(selector);
|
||||
if (!root) throw new Error('报告根节点缺失');
|
||||
|
||||
// 创建打印容器
|
||||
const printContainer = document.createElement('div');
|
||||
printContainer.id = 'electron-print-container';
|
||||
|
||||
// 样式设置:绝对定位覆盖全屏,背景白,z-index最高
|
||||
printContainer.style.position = 'absolute';
|
||||
printContainer.style.top = '0';
|
||||
printContainer.style.left = '0';
|
||||
printContainer.style.width = '100%';
|
||||
printContainer.style.minHeight = '100vh';
|
||||
printContainer.style.background = '#ffffff';
|
||||
printContainer.style.zIndex = '2147483647';
|
||||
printContainer.style.display = 'block';
|
||||
printContainer.style.overflow = 'visible'; // 关键:允许内容溢出以触发分页
|
||||
|
||||
// 克隆目标节点
|
||||
const cloned = root.cloneNode(true);
|
||||
|
||||
// 强制重置克隆节点的关键样式,确保它是流式布局
|
||||
cloned.style.position = 'static'; // 必须是 static 或 relative
|
||||
cloned.style.display = 'block';
|
||||
cloned.style.width = '100%';
|
||||
cloned.style.height = 'auto';
|
||||
cloned.style.overflow = 'visible';
|
||||
cloned.style.margin = '0';
|
||||
cloned.style.transform = 'none'; // 移除可能影响打印的变换
|
||||
|
||||
// 将克隆节点加入容器
|
||||
printContainer.appendChild(cloned);
|
||||
|
||||
// 将容器加入 body
|
||||
document.body.appendChild(printContainer);
|
||||
|
||||
// 关键修复:打印时只保留我们的打印容器可见,其他所有元素隐藏
|
||||
// 我们创建一个 style 标签来强制隐藏除了 printContainer 以外的所有 body 直接子元素
|
||||
const style = document.createElement('style');
|
||||
style.id = 'print-style-override';
|
||||
style.innerHTML = \`
|
||||
@media print {
|
||||
body > *:not(#electron-print-container) {
|
||||
display: none !important;
|
||||
}
|
||||
#electron-print-container {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
\`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.classList.add('print-mode');
|
||||
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
|
||||
try {
|
||||
const pdf = await win.webContents.printToPDF({
|
||||
pageSize: (payload && payload.pageSize) ? payload.pageSize : 'A4',
|
||||
landscape: !!(payload && payload.landscape),
|
||||
printBackground: !(payload && payload.printBackground === false),
|
||||
marginsType: 0,
|
||||
displayHeaderFooter: true, // 启用页眉页脚
|
||||
headerTemplate: '<div></div>', // 空页眉
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; text-align: center; font-size: 10px; font-family: 'Microsoft YaHei', sans-serif; padding-top: 5px; padding-bottom: 5px;">
|
||||
第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页
|
||||
</div>
|
||||
`
|
||||
});
|
||||
return pdf;
|
||||
} finally {
|
||||
// 3. 清理环境
|
||||
await win.webContents.executeJavaScript(`
|
||||
(function(){
|
||||
const container = document.getElementById('electron-print-container');
|
||||
if (container) container.remove();
|
||||
document.body.classList.remove('print-mode');
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
function startBackendService() {
|
||||
// 在打包后的应用中,使用process.resourcesPath获取resources目录
|
||||
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
|
||||
@ -98,6 +192,7 @@ function createWindow() {
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
icon: path.join(__dirname, '../public/logo.png'),
|
||||
|
||||
@ -1,2 +1,6 @@
|
||||
const { contextBridge } = require('electron');
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
generateReportPdf: (payload) => ipcRenderer.invoke('generate-report-pdf', payload),
|
||||
getBackendUrl: () => 'http://localhost:5000'
|
||||
});
|
||||
|
||||
@ -20,9 +20,10 @@
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.3.9",
|
||||
"html2canvas": "^1.4.1",
|
||||
"three": "^0.160.0",
|
||||
"jspdf": "^3.0.4",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.4"
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="generateReport-container-headertitle">生成报告</div>
|
||||
<div style="display: flex;">
|
||||
<div class="generateReport-container-cancelbutton" @click="closeCancel">取消</div>
|
||||
<div class="generateReport-container-confirmbutton" @click="confirmCancel">确定</div>
|
||||
<div class="generateReport-container-confirmbutton" @click="confirmGenerateReport">确定</div>
|
||||
</div>
|
||||
<img src="@/assets/archive/close.png" alt="" @click="closeCancel" style="cursor: pointer;">
|
||||
</div>
|
||||
@ -295,7 +295,7 @@ onMounted(() => {
|
||||
function closeCancel() {
|
||||
emit("closeGenerateReport",false)
|
||||
}
|
||||
function confirmCancel() {
|
||||
function confirmGenerateReport() {
|
||||
if(rawData.value.id == null){
|
||||
ElMessage.error('请选择原始数据')
|
||||
return
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="PopUpReport-container">
|
||||
<div class="PopUpReport-container" id="popup-report-root">
|
||||
<div class="PopUpReport-container-body" id="pdf-content">
|
||||
<div style="height: 100%; padding:0 90px; box-sizing: border-box;">
|
||||
<div class="PopUpReport-container-bodytitle">体态测量报告单</div>
|
||||
@ -195,8 +195,8 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getBackendUrl } from '@/services/api.js'
|
||||
import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const emit = defineEmits([ 'closePopUpReport' ]);
|
||||
const props = defineProps({
|
||||
selectedPatient: {
|
||||
@ -272,65 +272,65 @@ onUnmounted(() => {
|
||||
|
||||
})
|
||||
const generatePDF = async () => {
|
||||
const element = document.getElementById('pdf-content');
|
||||
|
||||
// PDF尺寸配置
|
||||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||
|
||||
// 计算缩放比例
|
||||
const scale = 2; // 提高清晰度
|
||||
const elementWidth = element.offsetWidth;
|
||||
const elementHeight = element.scrollHeight;
|
||||
const widthRatio = pageWidth / (elementWidth / scale);
|
||||
|
||||
// 智能分页算法
|
||||
const pageContentHeight = (pageHeight / widthRatio) * scale;
|
||||
let position = 0;
|
||||
let currentPage = 1;
|
||||
|
||||
while (position < elementHeight) {
|
||||
// 添加新页面(首页除外)
|
||||
if (currentPage > 1) {
|
||||
pdf.addPage();
|
||||
try {
|
||||
const root = document.getElementById('popup-report-root')
|
||||
if (!root) {
|
||||
ElMessage.error('报告容器未找到')
|
||||
return
|
||||
}
|
||||
|
||||
// 分块渲染
|
||||
const canvas = await html2canvas(element, {
|
||||
scale,
|
||||
useCORS: true,
|
||||
windowHeight: pageContentHeight,
|
||||
height: pageContentHeight,
|
||||
y: position,
|
||||
x: 0,
|
||||
scrollY: -window.scrollY // 锁定滚动位置
|
||||
});
|
||||
if (!window.electronAPI) {
|
||||
console.error('electronAPI 未定义')
|
||||
return
|
||||
}
|
||||
|
||||
// 计算当前块尺寸
|
||||
const imgData = canvas.toDataURL('image/jpeg', 1.0);
|
||||
const imgWidth = pageWidth;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
const pdfBuffer = await window.electronAPI.generateReportPdf({
|
||||
selector: '#popup-report-root',
|
||||
pageSize: 'A4',
|
||||
landscape: true, // 横向
|
||||
printBackground: true
|
||||
})
|
||||
|
||||
// 添加图片到PDF
|
||||
pdf.addImage(imgData, 'JPEG', 0, 0, imgWidth, imgHeight);
|
||||
const blob = new Blob([pdfBuffer], { type: 'application/pdf' })
|
||||
const form = new FormData()
|
||||
// 使用检测ID作为文件名
|
||||
const filename = `${props.detectionInfo.id || 'report'}.pdf`
|
||||
form.append('file', blob, filename)
|
||||
|
||||
// 更新位置
|
||||
position += pageContentHeight;
|
||||
currentPage++;
|
||||
// 如果有detectionInfo.id,则上传到后端
|
||||
if (props.detectionInfo.id) {
|
||||
const res = await fetch(`${BACKEND_URL}/api/reports/${props.detectionInfo.id}/upload`, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
||||
}
|
||||
|
||||
const json = await res.json()
|
||||
if (json.success) {
|
||||
ElMessage.success('报告生成并上传成功')
|
||||
emit('closePopUpReport', true)
|
||||
} else {
|
||||
throw new Error(json.error || '上传失败')
|
||||
}
|
||||
} else {
|
||||
// 仅供预览或下载
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
emit('closePopUpReport', true)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('报告生成异常:', e)
|
||||
ElMessage.error(`报告生成失败:${e.message}`)
|
||||
}
|
||||
|
||||
// 生成并下载PDF
|
||||
const pdfBlob = pdf.output('blob');
|
||||
const url = URL.createObjectURL(pdfBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'document.pdf';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
emit("closePopUpReport",false)
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -350,7 +350,7 @@ const generatePDF = async () => {
|
||||
width: 1600px;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
padding: 50px 0 20px;
|
||||
padding: 20px 0 30px;
|
||||
|
||||
|
||||
}
|
||||
@ -521,3 +521,157 @@ const generatePDF = async () => {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 10mm;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
/* 隐藏所有元素,但保留占位,避免 display:none 导致的父级隐藏问题 */
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* 显式显示目标容器及其子元素 */
|
||||
#popup-report-root,
|
||||
#popup-report-root *,
|
||||
#electron-print-container,
|
||||
#electron-print-container * {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* 确保根节点可见并重置布局 */
|
||||
#popup-report-root {
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background-color: white !important;
|
||||
z-index: 9999 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* 容器样式重置 */
|
||||
.PopUpReport-container-body {
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 10px 10px 30px 10px !important; /* 底部增加padding给页码留空间 */
|
||||
border: none !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
/* 移除内层div的固定高度 */
|
||||
.PopUpReport-container-body > div {
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 针对克隆节点的特定样式 */
|
||||
#electron-print-container .PopUpReport-container {
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
/* 左右分栏调整 */
|
||||
.PopUpReport-container-leftbox,
|
||||
.PopUpReport-container-rightbox {
|
||||
width: 49% !important;
|
||||
display: inline-block !important;
|
||||
vertical-align: top !important;
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 10px !important;
|
||||
border: none !important;
|
||||
/* 允许分栏内部内容自然分页 */
|
||||
break-inside: auto !important;
|
||||
}
|
||||
|
||||
.PopUpReport-container-leftbox {
|
||||
border-right: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
/* 图片容器自适应 */
|
||||
.PopUpReport-container-body div[style*="width: 600px"] {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
margin-bottom: 10px !important;
|
||||
break-inside: avoid !important;
|
||||
}
|
||||
|
||||
/* 图片自适应 */
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
object-fit: contain !important;
|
||||
max-height: 300px !important; /* 限制最大高度,防止一页占满 */
|
||||
}
|
||||
|
||||
/* 头部信息紧凑化 */
|
||||
.PopUpReport-container-bodytitle {
|
||||
font-size: 24px !important;
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.PopUpReport-container-display,
|
||||
.PopUpReport-container-userinfodisplay {
|
||||
padding: 10px 0 !important;
|
||||
}
|
||||
|
||||
.PopUpReport-container-userinfotext2 {
|
||||
font-size: 14px !important;
|
||||
width: auto !important;
|
||||
margin-right: 20px !important;
|
||||
padding-bottom: 5px !important;
|
||||
}
|
||||
|
||||
/* 标题字号调整 */
|
||||
.PopUpReport-content-title,
|
||||
.PopUpReport-container-testdatatitle,
|
||||
.PopUpReport-title2 {
|
||||
font-size: 16px !important;
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 5px !important;
|
||||
}
|
||||
|
||||
/* 分页控制 */
|
||||
.PopUpReport-content-title {
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
img {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
/* 隐藏不需要的UI元素 */
|
||||
.displayflexselect-icon,
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 页脚页码设置 */
|
||||
@bottom-right {
|
||||
content: "第 " counter(page) " 页 共 " counter(pages) " 页";
|
||||
font-size: 12px;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user