合并冲突

This commit is contained in:
limengnan 2025-12-09 13:35:12 +08:00
commit 4173c3b862
9 changed files with 2316 additions and 274 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1443,6 +1443,55 @@ class AppServer:
self.logger.error(f'保存会话信息失败: {e}') self.logger.error(f'保存会话信息失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500 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']) @self.app.route('/api/detection/<session_id>/save-data', methods=['POST'])
def save_detection_data(session_id): def save_detection_data(session_id):
"""采集检测数据""" """采集检测数据"""

View File

@ -1,4 +1,4 @@
const { app, BrowserWindow } = require('electron'); const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path'); const path = require('path');
const http = require('http'); const http = require('http');
const fs = require('fs'); const fs = require('fs');
@ -8,6 +8,100 @@ let mainWindow;
let localServer; let localServer;
let backendProcess; 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() { function startBackendService() {
// 在打包后的应用中使用process.resourcesPath获取resources目录 // 在打包后的应用中使用process.resourcesPath获取resources目录
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..'); const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
@ -98,6 +192,7 @@ function createWindow() {
webPreferences: { webPreferences: {
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
sandbox: false, // 显式关闭沙盒,避免 preload 加载问题
preload: path.join(__dirname, 'preload.js') preload: path.join(__dirname, 'preload.js')
}, },
icon: path.join(__dirname, '../public/logo.png'), icon: path.join(__dirname, '../public/logo.png'),

View File

@ -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'
});

View File

@ -13,6 +13,7 @@
"echarts": "^5.4.3", "echarts": "^5.4.3",
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^3.0.4",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"three": "^0.160.0", "three": "^0.160.0",
@ -64,10 +65,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.2", "version": "7.28.4",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.2.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -1168,6 +1168,12 @@
"undici-types": "~5.26.4" "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": { "node_modules/@types/plist": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz", "resolved": "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz",
@ -1180,6 +1186,13 @@
"xmlbuilder": ">=11.0.1" "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": { "node_modules/@types/responselike": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz",
@ -1190,6 +1203,13 @@
"@types/node": "*" "@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": { "node_modules/@types/verror": {
"version": "1.10.11", "version": "1.10.11",
"resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz", "resolved": "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz",
@ -1732,7 +1752,7 @@
}, },
"node_modules/base64-arraybuffer": { "node_modules/base64-arraybuffer": {
"version": "1.0.2", "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==", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1983,6 +2003,26 @@
"node": ">= 0.4" "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": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
@ -2254,6 +2294,18 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
@ -2343,7 +2395,7 @@
}, },
"node_modules/css-line-break": { "node_modules/css-line-break": {
"version": "2.1.0", "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==", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2609,6 +2661,16 @@
"node": ">=8" "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": { "node_modules/dotenv": {
"version": "9.0.2", "version": "9.0.2",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz",
@ -3279,6 +3341,17 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fd-slicer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz",
@ -3289,6 +3362,12 @@
"pend": "~1.2.0" "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": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz",
@ -3894,7 +3973,7 @@
}, },
"node_modules/html2canvas": { "node_modules/html2canvas": {
"version": "1.4.1", "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==", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4026,6 +4105,12 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
@ -4235,6 +4320,23 @@
"graceful-fs": "^4.1.6" "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": { "node_modules/junk": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/junk/-/junk-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/junk/-/junk-3.1.0.tgz",
@ -4757,6 +4859,12 @@
"dev": true, "dev": true,
"license": "BlueOak-1.0.0" "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": { "node_modules/parse-author": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/parse-author/-/parse-author-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/parse-author/-/parse-author-2.0.0.tgz",
@ -4864,6 +4972,13 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@ -5017,6 +5132,16 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/rcedit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/rcedit/-/rcedit-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/rcedit/-/rcedit-3.1.0.tgz",
@ -5104,6 +5229,13 @@
"minimatch": "^5.1.0" "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": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
@ -5171,6 +5303,16 @@
"node": ">= 4" "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": { "node_modules/roarr": {
"version": "2.15.4", "version": "2.15.4",
"resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", "resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz",
@ -5543,6 +5685,16 @@
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true "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": { "node_modules/stat-mode": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz",
@ -5697,6 +5849,16 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz",
@ -5784,7 +5946,7 @@
}, },
"node_modules/text-segmentation": { "node_modules/text-segmentation": {
"version": "1.0.3", "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==", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5938,7 +6100,7 @@
}, },
"node_modules/utrie": { "node_modules/utrie": {
"version": "1.0.2", "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==", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -20,9 +20,10 @@
"echarts": "^5.4.3", "echarts": "^5.4.3",
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"three": "^0.160.0", "jspdf": "^3.0.4",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"three": "^0.160.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-echarts": "^6.6.1", "vue-echarts": "^6.6.1",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"

View File

@ -172,7 +172,7 @@
<div class="body-foot-box"> <div class="body-foot-box">
<div class="body-title-display"> <div class="body-title-display">
<div class="body-son-display"> <div class="body-son-display">
<img src="@/assets/new/title3.svg" alt="" style="margin-right: 8px;"> <img src="@/assets/detection/title1.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">足部压力</div> <div class="body-posture-text">足部压力</div>
</div> </div>
<div class="body-son-display"> <div class="body-son-display">
@ -255,7 +255,7 @@
<div class="body-userinfo-box"> <div class="body-userinfo-box">
<div class="body-title-display"> <div class="body-title-display">
<div class="body-son-display"> <div class="body-son-display">
<img src="@/assets/new/title4.svg" alt="" style="margin-right: 8px;"> <img src="@/assets/detection/title1.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">用户信息</div> <div class="body-posture-text">用户信息</div>
</div> </div>
<div class="body-son-display"></div> <div class="body-son-display"></div>
@ -317,7 +317,7 @@
<div class="body-video-box"> <div class="body-video-box">
<div class="body-title-display"> <div class="body-title-display">
<div class="body-son-display"> <div class="body-son-display">
<img src="@/assets/new/title5.svg" alt="" style="margin-right: 8px;"> <img src="@/assets/detection/title1.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">视频</div> <div class="body-posture-text">视频</div>
</div> </div>
<div class="body-son-display"> <div class="body-son-display">
@ -364,7 +364,7 @@
<div class="pop-up-tip-container"> <div class="pop-up-tip-container">
<div class="pop-up-tip-header"> <div class="pop-up-tip-header">
<div>提示</div> <div>提示</div>
<img src="@/assets/new/u264.svg" alt="" style="cursor: pointer;" @click="handleCancel"> <img src="@/assets/detection/close.png" alt="" style="cursor: pointer;" @click="handleCancel">
</div> </div>
<div class="pop-up-tip-text">本次检测未截图或录像操作不予存档记录</div> <div class="pop-up-tip-text">本次检测未截图或录像操作不予存档记录</div>
<div class="tipconfirmbutton-box"> <div class="tipconfirmbutton-box">
@ -387,7 +387,7 @@
<div class="pop-up-camera-container"> <div class="pop-up-camera-container">
<div class="pop-up-camera-header"> <div class="pop-up-camera-header">
<div>相机参数设置</div> <div>相机参数设置</div>
<img src="@/assets/new/u264.svg" alt="" style="cursor: pointer;" @click="handleCameraCancel"> <img src="@/assets/detection/close.png" alt="" style="cursor: pointer;" @click="handleCameraCancel">
</div> </div>
<div class="pop-up-camera-body"> <div class="pop-up-camera-body">
<div class="pop-up-camera-display"> <div class="pop-up-camera-display">

View File

@ -4,7 +4,7 @@
<div class="generateReport-container-headertitle">生成报告</div> <div class="generateReport-container-headertitle">生成报告</div>
<div style="display: flex;"> <div style="display: flex;">
<div class="generateReport-container-cancelbutton" @click="closeCancel">取消</div> <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> </div>
<img src="@/assets/archive/close.png" alt="" @click="closeCancel" style="cursor: pointer;"> <img src="@/assets/archive/close.png" alt="" @click="closeCancel" style="cursor: pointer;">
</div> </div>
@ -302,7 +302,7 @@ onMounted(() => {
function closeCancel() { function closeCancel() {
emit("closeGenerateReport",false) emit("closeGenerateReport",false)
} }
function confirmCancel() { function confirmGenerateReport() {
if(rawData.value.id == null){ if(rawData.value.id == null){
ElMessage.error('请选择原始数据') ElMessage.error('请选择原始数据')
return return

View File

@ -1,204 +1,202 @@
<template> <template>
<div class="PopUpReport-container"> <div class="PopUpReport-container" id="popup-report-root">
<div class="PopUpReport-container-body" id="pdf-content"> <div class="PopUpReport-container-body" id="pdf-content">
<div style="height: 100%; padding:0 90px; box-sizing: border-box;"> <div style="height: 100%; padding:0 90px; box-sizing: border-box;">
<div class="PopUpReport-container-bodytitle">体态测量报告单</div> <div class="PopUpReport-container-bodytitle">体态测量报告单</div>
<div class="PopUpReport-container-display"> <div class="PopUpReport-container-display">
<div class="PopUpReport-container-userinfotext">检测时间{{ detectionInfo.start_time }}</div> <div class="PopUpReport-container-userinfotext">检测时间{{ detectionInfo.start_time }}</div>
<div class="PopUpReport-container-userinfotext">ID{{ detectionInfo.id }}</div> <div class="PopUpReport-container-userinfotext">ID{{ detectionInfo.id }}</div>
</div>
<div class="PopUpReport-container-userinfodisplay">
<div class="PopUpReport-container-userinfotext2">
ID{{ selectedPatient.id }}
</div> </div>
<div class="PopUpReport-container-userinfodisplay"> <div class="PopUpReport-container-userinfotext2 width-210">
<div class="PopUpReport-container-userinfotext2"> 姓名{{ selectedPatient.name }}
ID{{ selectedPatient.id }}
</div>
<div class="PopUpReport-container-userinfotext2 width-210">
姓名{{ selectedPatient.name }}
</div>
<div class="PopUpReport-container-userinfotext2 width-195">
性别{{ selectedPatient.gender }}
</div>
<div class="PopUpReport-container-userinfotext2 width-195">
年龄{{ calculateAge(selectedPatient.birth_date) }}
</div>
<div class="PopUpReport-container-userinfotext2 width-235">
身高{{ selectedPatient.height }}cm
</div>
<div class="PopUpReport-container-userinfotext2 width-215">
体重{{ selectedPatient.weight }}kg
</div>
<div class="PopUpReport-container-userinfotext2 width-95">
鞋码{{ selectedPatient.shoe_size }}
</div>
<div class="PopUpReport-container-userinfotext2">
电话{{ selectedPatient.phone }}
</div>
<div class="PopUpReport-container-userinfotext2 width-405">
邮箱{{ selectedPatient.email }}
</div>
<div class="PopUpReport-container-userinfotext2 width-430">
居住地{{ selectedPatient.residence }}
</div>
<div class="PopUpReport-container-userinfotext2 width-310">
职业{{ selectedPatient.occupation }}
</div>
</div> </div>
<div class="PopUpReport-container-testdatatitle">检测数据</div> <div class="PopUpReport-container-userinfotext2 width-195">
<div class="PopUpReport-containerdisplay"> 性别{{ selectedPatient.gender }}
<div class="PopUpReport-container-leftbox">
<div class="displayflex">
<div class="displayflextext1">原始数据</div>
<div class="displayflextext1">
{{ rawData.order }}
</div>
</div>
<!-- 选中后显示内容 -->
<div v-if="rawData.order && rawData.order!=''">
<div class="PopUpReport-content-title">整体数据</div>
<div style="width: 600px;height: 387px;">
<img :src="BACKEND_URL+'/' + rawData.screen_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">身体姿态</div>
<div style="width: 100%;height: 454px; display: flex;justify-content: center;">
<img :src="BACKEND_URL+'/' + rawData.body_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">头部姿态</div>
<div style="width: 555px;padding:20px 0; display: flex;justify-content: space-between;">
<img src="@/assets/archive/roll.png">
<img src="@/assets/archive/yaw.png">
<img src="@/assets/archive/pitch.png">
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.rotationLeftMax}}°
</span>
</div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.tiltLeftMax}}°
</span></div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.pitchDownMax}}°
</span></div>
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.rotationRightMax}}°
</span></div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.tiltRightMax}}°
</span>
</div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.pitchUpMax}}°
</span>
</div>
</div>
<div class="PopUpReport-content-title">足底压力</div>
<div style="width: 600px;height: 370px; display: flex;margin-bottom: 100px;">
<img :src="BACKEND_URL+'/' + rawData.foot_data_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">视频1图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + rawData.foot1_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">视频2图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + rawData.foot2_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
</div>
</div>
<div class="PopUpReport-container-rightbox">
<div class="displayflex">
<div class="displayflextext1">矫正数据</div>
<div class="displayflextext1">
{{ calibrationData.order }}
</div>
</div>
<!-- 选中后显示内容 -->
<div v-if="calibrationData.order && calibrationData.order!=''">
<div class="PopUpReport-content-title">整体数据</div>
<div style="width: 600px;height: 387px;">
<img :src="BACKEND_URL+'/' + calibrationData.screen_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">身体姿态</div>
<div style="width: 100%;height: 454px; display: flex;justify-content: center;">
<img :src="BACKEND_URL+'/' + calibrationData.body_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">头部姿态</div>
<div style="width: 555px;padding:20px 0; display: flex;justify-content: space-between;">
<img src="@/assets/archive/roll.png">
<img src="@/assets/archive/yaw.png">
<img src="@/assets/archive/pitch.png">
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.rotationLeftMax}}°
</span>
</div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.tiltLeftMax}}°
</span></div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.pitchDownMax}}°
</span></div>
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.rotationRightMax}}°
</span></div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.tiltRightMax}}°
</span>
</div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.pitchUpMax}}°
</span>
</div>
</div>
<div class="PopUpReport-content-title">足底压力</div>
<div style="width: 600px;height: 370px; display: flex;margin-bottom: 100px;">
<img :src="BACKEND_URL+'/' + calibrationData.foot_data_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">视频1图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + calibrationData.foot1_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">视频2图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + calibrationData.foot2_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
</div>
</div>
</div> </div>
<div class="PopUpReport-container-testdatatitle">诊断结果</div> <div class="PopUpReport-container-userinfotext2 width-195">
<div class="PopUpReport-title2">记录</div> 年龄{{ calculateAge(selectedPatient.birth_date) }}
<div class="PopUpReport-border1">{{ detectionInfo.diagnosis_info }}</div> </div>
<div class="PopUpReport-title2">处理</div> <div class="PopUpReport-container-userinfotext2 width-235">
<div class="PopUpReport-border2">{{ detectionInfo.treatment_info }}</div> 身高{{ selectedPatient.height }}cm
<div class="PopUpReport-title2">备注</div> </div>
<div class="PopUpReport-border3">{{ detectionInfo.suggestion_info }}</div> <div class="PopUpReport-container-userinfotext2 width-215">
<div class="PopUpReport-footer"> 体重{{ selectedPatient.weight }}kg
<div style="margin-right: 80px;">检测时间{{ detectionInfo.created_at }}</div> </div>
<div style="margin-right: 80px;">报告时间{{ getFormattedTime() }}</div> <div class="PopUpReport-container-userinfotext2 width-95">
<div>检测医生{{ detectionInfo.creator_name }}</div> 鞋码{{ selectedPatient.shoe_size }}
</div>
<div class="PopUpReport-container-userinfotext2">
电话{{ selectedPatient.phone }}
</div>
<div class="PopUpReport-container-userinfotext2 width-405">
邮箱{{ selectedPatient.email }}
</div>
<div class="PopUpReport-container-userinfotext2 width-430">
居住地{{ selectedPatient.residence }}
</div>
<div class="PopUpReport-container-userinfotext2 width-310">
职业{{ selectedPatient.occupation }}
</div> </div>
</div> </div>
<div class="PopUpReport-container-testdatatitle">检测数据</div>
<div class="PopUpReport-containerdisplay">
<div class="PopUpReport-container-leftbox">
<div class="displayflex">
<div class="displayflextext1">原始数据</div>
<div class="displayflextext1">
{{ rawData.order }}
</div>
</div>
<!-- 选中后显示内容 -->
<div v-if="rawData.order && rawData.order!=''">
<div class="PopUpReport-content-title">整体数据</div>
<div style="width: 600px;height: 387px;">
<img :src="BACKEND_URL+'/' + rawData.screen_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">身体姿态</div>
<div style="width: 100%;height: 454px; display: flex;justify-content: center;">
<img :src="BACKEND_URL+'/' + rawData.body_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">头部姿态</div>
<div style="width: 555px;padding:20px 0; display: flex;justify-content: space-between;">
<img src="@/assets/archive/roll.png">
<img src="@/assets/archive/yaw.png">
<img src="@/assets/archive/pitch.png">
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.rotationLeftMax}}°
</span>
</div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.tiltLeftMax}}°
</span></div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.pitchDownMax}}°
</span></div>
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.rotationRightMax}}°
</span></div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.tiltRightMax}}°
</span>
</div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesLeft.pitchUpMax}}°
</span>
</div>
</div>
<div class="PopUpReport-content-title">足底压力</div>
<div style="width: 600px;height: 370px; display: flex;margin-bottom: 100px;">
<img :src="BACKEND_URL+'/' + rawData.foot_data_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">视频1图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + rawData.foot1_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">视频2图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + rawData.foot2_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
</div>
</div>
<div class="PopUpReport-container-rightbox">
<div class="displayflex">
<div class="displayflextext1">矫正数据</div>
<div class="displayflextext1">
{{ calibrationData.order }}
</div>
</div>
<!-- 选中后显示内容 -->
<div v-if="calibrationData.order && calibrationData.order!=''">
<div class="PopUpReport-content-title">整体数据</div>
<div style="width: 600px;height: 387px;">
<img :src="BACKEND_URL+'/' + calibrationData.screen_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">身体姿态</div>
<div style="width: 100%;height: 454px; display: flex;justify-content: center;">
<img :src="BACKEND_URL+'/' + calibrationData.body_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">头部姿态</div>
<div style="width: 555px;padding:20px 0; display: flex;justify-content: space-between;">
<img src="@/assets/archive/roll.png">
<img src="@/assets/archive/yaw.png">
<img src="@/assets/archive/pitch.png">
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.rotationLeftMax}}°
</span>
</div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.tiltLeftMax}}°
</span></div>
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.pitchDownMax}}°
</span></div>
</div>
<div style="width: 630px;padding:20px 0; display: flex;justify-content: space-between;">
<div class="rollyawpitchtext"><span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.rotationRightMax}}°
</span></div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.tiltRightMax}}°
</span>
</div>
<div class="rollyawpitchtext">
<span class="rollyawpitchtextcolor">
{{headPoseMaxValuesRight.pitchUpMax}}°
</span>
</div>
</div>
<div class="PopUpReport-content-title">足底压力</div>
<div style="width: 600px;height: 370px; display: flex;margin-bottom: 100px;">
<img :src="BACKEND_URL+'/' + calibrationData.foot_data_image" alt="" srcset="" style="width: 100%;height: 100%;
object-fit:contain; ">
</div>
<div class="PopUpReport-content-title">视频1图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + calibrationData.foot1_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
<div class="PopUpReport-content-title">视频2图片</div>
<div style="width: 600px;height: 338px; display: flex;">
<img :src="BACKEND_URL+'/' + calibrationData.foot2_image" alt="" srcset="" style="width: 100%;height: 100%;">
</div>
</div>
</div>
</div>
<div class="PopUpReport-container-testdatatitle">诊断结果</div>
<div class="PopUpReport-title2">记录</div>
<div class="PopUpReport-border1">{{ detectionInfo.diagnosis_info }}</div>
<div class="PopUpReport-title2">处理</div>
<div class="PopUpReport-border2">{{ detectionInfo.treatment_info }}</div>
<div class="PopUpReport-title2">备注</div>
<div class="PopUpReport-border3">{{ detectionInfo.suggestion_info }}</div>
</div> </div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { getBackendUrl } from '@/services/api.js' import { getBackendUrl } from '@/services/api.js'
import html2canvas from 'html2canvas'; import { ElMessage } from 'element-plus'
import jsPDF from 'jspdf';
const emit = defineEmits([ 'closePopUpReport' ]); const emit = defineEmits([ 'closePopUpReport' ]);
const props = defineProps({ const props = defineProps({
selectedPatient: { selectedPatient: {
@ -274,65 +272,65 @@ onUnmounted(() => {
}) })
const generatePDF = async () => { const generatePDF = async () => {
const element = document.getElementById('pdf-content'); try {
const root = document.getElementById('popup-report-root')
// PDF if (!root) {
const pdf = new jsPDF('p', 'mm', 'a4'); ElMessage.error('报告容器未找到')
const pageWidth = pdf.internal.pageSize.getWidth(); return
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();
} }
// if (!window.electronAPI) {
const canvas = await html2canvas(element, { console.error('electronAPI 未定义')
scale, return
useCORS: true, }
windowHeight: pageContentHeight,
height: pageContentHeight,
y: position,
x: 0,
scrollY: -window.scrollY //
});
// const pdfBuffer = await window.electronAPI.generateReportPdf({
const imgData = canvas.toDataURL('image/jpeg', 1.0); selector: '#popup-report-root',
const imgWidth = pageWidth; pageSize: 'A4',
const imgHeight = (canvas.height * imgWidth) / canvas.width; landscape: true, //
printBackground: true
})
// PDF const blob = new Blob([pdfBuffer], { type: 'application/pdf' })
pdf.addImage(imgData, 'JPEG', 0, 0, imgWidth, imgHeight); const form = new FormData()
// 使ID
const filename = `${props.detectionInfo.id || 'report'}.pdf`
form.append('file', blob, filename)
// // detectionInfo.id
position += pageContentHeight; if (props.detectionInfo.id) {
currentPage++; 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 = '体态测量报告单.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
emit("closePopUpReport",false)
}; };
function getFormattedTime() { function getFormattedTime() {
const now = new Date(); const now = new Date();
@ -364,7 +362,7 @@ function getFormattedTime() {
width: 1600px; width: 1600px;
background: #fff; background: #fff;
border-radius: 5px; border-radius: 5px;
padding: 50px 0 20px; padding: 20px 0 30px;
} }
@ -534,14 +532,150 @@ function getFormattedTime() {
color: #383838; color: #383838;
padding: 5px; padding: 5px;
} }
.PopUpOnlyReport-footer{ </style>
margin-top: 40px;
padding-top: 40px; <style>
border-top: 1px solid #333; @media print {
display: flex; @page {
font-weight: 700; size: A4 landscape;
font-style: normal; margin: 10mm;
color: rgb(40, 40, 40); }
font-size: 18px;
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 !important;
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;
}
} }
</style> </style>