diff --git a/document/前端生成PDF并上传后端技术方案.md b/document/前端生成PDF并上传后端技术方案.md new file mode 100644 index 00000000..6dde504f --- /dev/null +++ b/document/前端生成PDF并上传后端技术方案.md @@ -0,0 +1,113 @@ +# 前端生成 PDF 并上传后端技术方案 + +## 概述 +- 目标:诊断报告页面在前端生成高保真 PDF,上传到 Python 后端持久化,并把文件路径写入对应检测会话。 +- 推荐:使用 Electron 主进程 `webContents.printToPDF` 生成 PDF,忠实度高、分页与中文字体友好;作为短期备选,提供 `html2canvas` 截图转 PDF 的实现。 + +## 架构流程 +- 前端渲染进程:用户在报告页面点击“生成报告”→ 发送 IPC 请求至主进程生成 PDF → 主进程返回 PDF Buffer 给渲染进程 → 渲染进程上传后端。 +- Python 后端:接收 PDF 文件流,写入 `backend/static/reports//.pdf`,记录相对路径到 `detection_sessions` 的报告字段。 + +## 前端实现(Electron 渲染) +### 报告页面打印版式 +- 报告根容器,例如 `#report-root`。 +- 打印样式建议: + - `@page { size: A4; margin: 12mm }` + - 固定宽度设计为 A4 比例,非交互元素隐藏,保留背景。 + - 通过添加类名(例如 `.print-mode`)切换打印样式。 + +### 渲染进程触发生成 +```ts +// renderer: 诊断报告视图中 +import { ipcRenderer } from 'electron' + +async function generatePdf(sessionId: string) { + const pdfBuffer: ArrayBuffer = await ipcRenderer.invoke('generate-report-pdf', { + selector: '#report-root', + pageSize: 'A4', + printBackground: true + }) + const blob = new Blob([pdfBuffer], { type: 'application/pdf' }) + const form = new FormData() + form.append('file', blob, `${sessionId}.pdf`) + + const res = await fetch(`${getBackendUrl()}/api/reports/${sessionId}/upload`, { + method: 'POST', + body: form + }) + const json = await res.json() + if (!json.success) throw new Error(json.error || '上传失败') +} +``` + +### 主进程生成 PDF +```ts +// main: 注册 IPC 处理 +import { ipcMain } from 'electron' + +ipcMain.handle('generate-report-pdf', async (event, payload) => { + const win = event.sender.getOwnerBrowserWindow() + await win.webContents.executeJavaScript(` + (function(){ + const root = document.querySelector('${payload.selector}') + if (!root) throw new Error('报告根节点缺失') + document.body.classList.add('print-mode') + return true + })() + `) + const pdf = await win.webContents.printToPDF({ + pageSize: payload.pageSize || 'A4', + printBackground: payload.printBackground !== false, + marginsType: 0 + }) + await win.webContents.executeJavaScript(` + (function(){ document.body.classList.remove('print-mode'); return true })() + `) + return pdf +}) +``` + +### 更新数据库记录 +```py +# database.py 片段 +def update_session_report_path(self, session_id: str, report_path: str): + conn = self.get_connection() + cursor = conn.cursor() + cursor.execute('UPDATE detection_sessions SET detection_report = ? WHERE id = ?', (report_path, session_id)) + conn.commit() +``` + +## 接口设计 +- 上传 PDF:`POST /api/reports//upload` + - 请求体:`multipart/form-data`,字段 `file` + - 返回:`{ success, path }` + +## 权限与安全 +- 校验 `session_id` 存在且归属当前登录用户(医生)。 +- 限制最大文件大小(例如 20MB),拒绝超限上传。 +- 文件名使用 `session_id.pdf`,避免任意文件名写盘。 + +## 版式与质量建议 +- 中文字体:在渲染环境预装中文字体或通过 `@font-face` 引入。 +- 分页:使用 `@page` 与 `page-break-before/after` 控制分页。 +- 背景:Electron 打印需开启 `printBackground: true`;确保 CSS 背景不被打印忽略。 + +## 错误与排查 +- 空白 PDF:确保打印前切入 `.print-mode` 并等待资源加载;检查选择器是否找到根节点。 +- 字体方框:安装中文字体或嵌入字体文件。 +- 资源丢失:使用相对路径或 base64;`html2canvas` 时启用 `useCORS` 且服务器设置跨域头。 +- 过大或截断:分页控制,或拆页生成。 + +## 测试用例 +- 单页报告:生成并上传,后端返回路径;数据库记录更新;历史档案中可下载或预览。 +- 多页报告:存在分页断点,打印后为多页;每页标题和页码正常。 +- 异常:网络断开、文件超限、会话不存在;前端提示具体错误。 + +## 部署说明 +- Electron 打包:已使用 `electron-builder`;生成 exe 后功能一致。 +- 后端存储:Windows 下路径统一使用 `/` 展示;静态文件由后端提供下载或前端拼接 `BACKEND_URL + '/' + path` 访问。 + +## 后续扩展 +- 加页眉页脚(时间、患者信息、页码)。 +- 报告水印与签章。 +- 历史报告下载与比对视图。 diff --git a/frontend/src/renderer/dist-electron-install/win-unpacked/resources/backend/Log/OrbbecSDK.log.txt b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/backend/Log/OrbbecSDK.log.txt index c0e30c17..fc9daa39 100644 Binary files a/frontend/src/renderer/dist-electron-install/win-unpacked/resources/backend/Log/OrbbecSDK.log.txt and b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/backend/Log/OrbbecSDK.log.txt differ diff --git a/frontend/src/renderer/dist-electron-install/win-unpacked/resources/config.ini b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/config.ini index 244e7630..efa5f6ce 100644 --- a/frontend/src/renderer/dist-electron-install/win-unpacked/resources/config.ini +++ b/frontend/src/renderer/dist-electron-install/win-unpacked/resources/config.ini @@ -35,7 +35,7 @@ chart_dpi = 300 export_format = csv [SECURITY] -secret_key = 579012d21afe892d663698a0875c78112bb7e73e949a0d9f591515cd7fce183b +secret_key = 55827ba4bade0523f51434154820c1809de46588877d911e4395d2c235cf5de5 session_timeout = 3600 max_login_attempts = 5 diff --git a/frontend/src/renderer/package-lock.json b/frontend/src/renderer/package-lock.json index 173ed90c..23090457 100644 --- a/frontend/src/renderer/package-lock.json +++ b/frontend/src/renderer/package-lock.json @@ -13,6 +13,7 @@ "echarts": "^5.4.3", "element-plus": "^2.3.9", "html2canvas": "^1.4.1", + "jspdf": "^3.0.4", "pinia": "^2.1.6", "socket.io-client": "^4.7.2", "three": "^0.160.0", @@ -64,10 +65,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", - "dev": true, + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1168,6 +1168,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz", @@ -1180,6 +1186,13 @@ "xmlbuilder": ">=11.0.1" } }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz", @@ -1190,6 +1203,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz", @@ -1732,7 +1752,7 @@ }, "node_modules/base64-arraybuffer": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", "engines": { @@ -1983,6 +2003,26 @@ "node": ">= 0.4" } }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -2254,6 +2294,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", @@ -2343,7 +2395,7 @@ }, "node_modules/css-line-break": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", "dependencies": { @@ -2609,6 +2661,16 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "9.0.2", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz", @@ -3279,6 +3341,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -3289,6 +3362,12 @@ "pend": "~1.2.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", @@ -3894,7 +3973,7 @@ }, "node_modules/html2canvas": { "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "license": "MIT", "dependencies": { @@ -4026,6 +4105,12 @@ "dev": true, "license": "ISC" }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4235,6 +4320,23 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jspdf": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", + "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/junk": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/junk/-/junk-3.1.0.tgz", @@ -4757,6 +4859,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse-author": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/parse-author/-/parse-author-2.0.0.tgz", @@ -4864,6 +4972,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", @@ -5017,6 +5132,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/rcedit": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/rcedit/-/rcedit-3.1.0.tgz", @@ -5104,6 +5229,13 @@ "minimatch": "^5.1.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", @@ -5171,6 +5303,16 @@ "node": ">= 4" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", @@ -5543,6 +5685,16 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", @@ -5697,6 +5849,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", @@ -5784,7 +5946,7 @@ }, "node_modules/text-segmentation": { "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "license": "MIT", "dependencies": { @@ -5938,7 +6100,7 @@ }, "node_modules/utrie": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "license": "MIT", "dependencies": {