From 2772a2a9f293fe4981a3b8bd7c17003ec6200389 Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Tue, 9 Dec 2025 12:59:04 +0800 Subject: [PATCH 1/3] Fix PDF generation and report upload: Fix pagination, footer font, backend upload logic, and UI handlers --- backend/main.py | 49 ++++ frontend/src/renderer/main/main.js | 97 ++++++- frontend/src/renderer/main/preload.js | 6 +- frontend/src/renderer/package.json | 5 +- .../src/renderer/src/views/GenerateReport.vue | 4 +- .../src/renderer/src/views/PopUpReport.vue | 274 ++++++++++++++---- 6 files changed, 369 insertions(+), 66 deletions(-) diff --git a/backend/main.py b/backend/main.py index d6a850c3..20d3ae73 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1441,7 +1441,56 @@ class AppServer: except Exception as e: self.logger.error(f'保存会话信息失败: {e}') + return jsonify({'success': False, 'error': str(e)}), 500 + + # ==================== 报告上传API ==================== + + @self.app.route('/api/reports//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//save-data', methods=['POST']) def save_detection_data(session_id): diff --git a/frontend/src/renderer/main/main.js b/frontend/src/renderer/main/main.js index 39be081e..6fa0f75d 100644 --- a/frontend/src/renderer/main/main.js +++ b/frontend/src/renderer/main/main.js @@ -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: '
', // 空页眉 + footerTemplate: ` +
+ 第 页 / 共 页 +
+ ` + }); + 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'), diff --git a/frontend/src/renderer/main/preload.js b/frontend/src/renderer/main/preload.js index b78c184f..8990560c 100644 --- a/frontend/src/renderer/main/preload.js +++ b/frontend/src/renderer/main/preload.js @@ -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' +}); diff --git a/frontend/src/renderer/package.json b/frontend/src/renderer/package.json index b4ea5d44..c0713704 100644 --- a/frontend/src/renderer/package.json +++ b/frontend/src/renderer/package.json @@ -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" @@ -35,5 +36,5 @@ "electron-packager": "^17.1.2", "vite": "^4.4.9", "wait-on": "^7.0.1" - } + } } diff --git a/frontend/src/renderer/src/views/GenerateReport.vue b/frontend/src/renderer/src/views/GenerateReport.vue index 856322e0..e0477330 100644 --- a/frontend/src/renderer/src/views/GenerateReport.vue +++ b/frontend/src/renderer/src/views/GenerateReport.vue @@ -4,7 +4,7 @@
生成报告
取消
-
确定
+
确定
@@ -295,7 +295,7 @@ onMounted(() => { function closeCancel() { emit("closeGenerateReport",false) } -function confirmCancel() { +function confirmGenerateReport() { if(rawData.value.id == null){ ElMessage.error('请选择原始数据') return diff --git a/frontend/src/renderer/src/views/PopUpReport.vue b/frontend/src/renderer/src/views/PopUpReport.vue index e552c16c..f8034886 100644 --- a/frontend/src/renderer/src/views/PopUpReport.vue +++ b/frontend/src/renderer/src/views/PopUpReport.vue @@ -1,5 +1,5 @@