From a7f48305beb6e92f15c89815b1b71616b807ed1b Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Sun, 7 Dec 2025 20:07:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=92=8C=E6=A1=A3=E6=A1=88=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config.ini | 6 +- backend/devices/utils/license_manager.py | 89 ++++++------ backend/utils.py | 11 +- frontend/src/renderer/src/services/api.js | 18 +-- frontend/src/renderer/src/views/Dashboard.vue | 19 ++- frontend/src/renderer/src/views/Detection.vue | 48 +++++-- .../renderer/src/views/DiagnosticMessage.vue | 20 ++- .../src/renderer/src/views/PatientCreate.vue | 130 ++++++++++++++---- .../src/renderer/src/views/PatientProfile.vue | 75 +++++++--- .../renderer/src/views/PatientProfile2.vue | 14 +- frontend/src/renderer/src/views/ViewUser.vue | 25 ++-- 11 files changed, 301 insertions(+), 154 deletions(-) diff --git a/backend/config.ini b/backend/config.ini index 70efd622..08a01276 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -29,7 +29,7 @@ backend = directshow [CAMERA2] enabled = True -device_index = 2 +device_index = 3 width = 1280 height = 720 fps = 30 @@ -50,12 +50,12 @@ synchronized_images_only = False [DEVICES] imu_enabled = True -imu_device_type = mock +imu_device_type = ble imu_port = COM9 imu_mac_address = ef:3c:1a:0a:fe:02 imu_baudrate = 9600 pressure_enabled = True -pressure_device_type = mock +pressure_device_type = real pressure_use_mock = False pressure_port = COM5 pressure_baudrate = 115200 diff --git a/backend/devices/utils/license_manager.py b/backend/devices/utils/license_manager.py index 2052b502..99dd8c2e 100644 --- a/backend/devices/utils/license_manager.py +++ b/backend/devices/utils/license_manager.py @@ -58,85 +58,82 @@ class LicenseManager: """生成机器硬件指纹""" if self._machine_id: return self._machine_id - try: - # 收集硬件信息 - hardware_info = [] - - # CPU信息 + core_info = [] + aux_info = [] try: if platform.system() == "Windows": - result = subprocess.run(['wmic', 'cpu', 'get', 'ProcessorId', '/value'], - capture_output=True, text=True, timeout=10) + result = subprocess.run(['wmic', 'cpu', 'get', 'ProcessorId', '/value'], capture_output=True, text=True, timeout=10) for line in result.stdout.split('\n'): if 'ProcessorId=' in line: cpu_id = line.split('=')[1].strip() if cpu_id: - hardware_info.append(f"CPU:{cpu_id}") + core_info.append(f"CPU:{cpu_id}") break - else: - # Linux/Mac 可以使用其他方法获取CPU信息 - pass except Exception as e: logger.warning(f"获取CPU信息失败: {e}") - - # 主板信息 try: if platform.system() == "Windows": - result = subprocess.run(['wmic', 'baseboard', 'get', 'SerialNumber', '/value'], - capture_output=True, text=True, timeout=10) + result = subprocess.run(['wmic', 'baseboard', 'get', 'SerialNumber', '/value'], capture_output=True, text=True, timeout=10) for line in result.stdout.split('\n'): if 'SerialNumber=' in line: board_serial = line.split('=')[1].strip() if board_serial and board_serial != "To be filled by O.E.M.": - hardware_info.append(f"BOARD:{board_serial}") + core_info.append(f"BOARD:{board_serial}") break except Exception as e: logger.warning(f"获取主板信息失败: {e}") - - # 磁盘信息 try: if platform.system() == "Windows": - result = subprocess.run(['wmic', 'diskdrive', 'get', 'SerialNumber', '/value'], - capture_output=True, text=True, timeout=10) + # 获取磁盘信息并过滤掉USB/可移动介质,收集所有内部磁盘序列号 + result = subprocess.run( + ['wmic', 'path', 'Win32_DiskDrive', 'get', 'SerialNumber,InterfaceType,PNPDeviceID,MediaType', '/value'], + capture_output=True, text=True, timeout=10 + ) + block = {} for line in result.stdout.split('\n'): - if 'SerialNumber=' in line: - disk_serial = line.split('=')[1].strip() - if disk_serial: - hardware_info.append(f"DISK:{disk_serial}") - break + line = line.strip() + if not line: + # 结束一个块 + serial = (block.get('SerialNumber') or '').strip() + iface = (block.get('InterfaceType') or '').strip().upper() + pnp = (block.get('PNPDeviceID') or '').strip().upper() + media = (block.get('MediaType') or '').strip().upper() + if serial and iface != 'USB' and not pnp.startswith('USBSTOR') and 'REMOVABLE' not in media: + core_info.append(f"DISK:{serial}") + block = {} + continue + if '=' in line: + k, v = line.split('=', 1) + block[k] = v + # 处理最后一个块 + if block: + serial = (block.get('SerialNumber') or '').strip() + iface = (block.get('InterfaceType') or '').strip().upper() + pnp = (block.get('PNPDeviceID') or '').strip().upper() + media = (block.get('MediaType') or '').strip().upper() + if serial and iface != 'USB' and not pnp.startswith('USBSTOR') and 'REMOVABLE' not in media: + core_info.append(f"DISK:{serial}") except Exception as e: logger.warning(f"获取磁盘信息失败: {e}") - - # MAC地址 try: import uuid - mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) - for elements in range(0,2*6,2)][::-1]) - hardware_info.append(f"MAC:{mac}") + mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) for elements in range(0, 2 * 6, 2)][::-1]) + aux_info.append(f"MAC:{mac}") except Exception as e: logger.warning(f"获取MAC地址失败: {e}") - - # 系统信息作为补充 - hardware_info.append(f"OS:{platform.system()}") - hardware_info.append(f"MACHINE:{platform.machine()}") - - # 如果没有获取到足够的硬件信息,使用系统信息作为fallback - if len(hardware_info) < 2: - hardware_info.append(f"NODE:{platform.node()}") - hardware_info.append(f"PROCESSOR:{platform.processor()}") - - # 生成指纹哈希 - combined_info = "|".join(sorted(hardware_info)) + aux_info.append(f"OS:{platform.system()}") + aux_info.append(f"MACHINE:{platform.machine()}") + if len(core_info) < 1: + core_info.append(f"NODE:{platform.node()}") + core_info.append(f"PROCESSOR:{platform.processor()}") + combined_info = "|".join(sorted(core_info)) machine_id = hashlib.sha256(combined_info.encode('utf-8')).hexdigest()[:16].upper() - self._machine_id = f"W10-{machine_id}" logger.info(f"生成机器指纹: {self._machine_id}") return self._machine_id - except Exception as e: logger.error(f"生成机器指纹失败: {e}") - # 使用fallback方案 fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}" fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper() self._machine_id = f"FB-{fallback_id}" @@ -429,4 +426,4 @@ class LicenseManager: except Exception as e: logger.error(f"生成激活请求失败: {e}") - raise \ No newline at end of file + raise diff --git a/backend/utils.py b/backend/utils.py index 9f4fe47f..c81b7ce0 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -218,17 +218,8 @@ class DataValidator: name = data['name'].strip() if len(name) < 2 or len(name) > 50: errors.append('姓名长度应在2-50个字符之间') - data['name'] = name + data['name'] = name - # 性别验证 - if data.get('gender'): - # 支持中文和英文性别值 - gender_map = {'男': 'male', '女': 'female', 'male': 'male', 'female': 'female'} - gender_value = data['gender'].strip() - if gender_value in gender_map: - data['gender'] = gender_map[gender_value] - else: - errors.append('性别值无效,应为:男、女、male、female') # 出生日期验证 if data.get('birth_date'): diff --git a/frontend/src/renderer/src/services/api.js b/frontend/src/renderer/src/services/api.js index f29d76b3..3e9f35d8 100644 --- a/frontend/src/renderer/src/services/api.js +++ b/frontend/src/renderer/src/services/api.js @@ -13,7 +13,7 @@ api.interceptors.request.use( if (window.electronAPI) { config.baseURL = window.electronAPI.getBackendUrl() } else { - config.baseURL = 'http://192.168.1.62:5000' + config.baseURL = 'http://localhost:5000' } // 为需要发送数据的请求设置Content-Type(避免覆盖FormData) @@ -187,22 +187,12 @@ export const patientAPI = { // 创建患者 createPatient(data) { return api.post('/api/patients', data) - }, - - // 创建患者(别名方法) - create(data) { - return this.createPatient(data) - }, + }, // 更新患者 updatePatient(id, data) { return api.put(`/api/patients/${id}`, data) - }, - - // 更新患者(别名方法) - update(id, data) { - return this.updatePatient(id, data) - }, + }, // 删除患者 deletePatient(id) { @@ -675,7 +665,7 @@ export const getBackendUrl = () => { if (window.electronAPI) { return window.electronAPI.getBackendUrl() } else { - return 'http://192.168.1.62:5000' + return 'http://localhost:5000' } } diff --git a/frontend/src/renderer/src/views/Dashboard.vue b/frontend/src/renderer/src/views/Dashboard.vue index 8004c370..70cf66c9 100644 --- a/frontend/src/renderer/src/views/Dashboard.vue +++ b/frontend/src/renderer/src/views/Dashboard.vue @@ -10,7 +10,7 @@
-
证件号
+
身份证号
{{ selectedPatient.idcode }} @@ -195,13 +195,13 @@
- 查看档案 + 查看患者档案
- 开始检测 + 开始体态检测
@@ -382,7 +382,11 @@ const getStatusText = (lastDetection) => { } const formatDate = (date) => { - return new Date(date).toLocaleDateString('zh-CN') + const d = new Date(date) + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` } // 添加计算年龄的方法 @@ -476,8 +480,11 @@ function closecreatbox(e,e2){ }else if(e == '新建') { loadPatients() }else if(e == '编辑') { - // patients.value[rowIndex.value] = e2 + if (rowIndex.value >= 0 && patients.value[rowIndex.value]) { + patients.value[rowIndex.value] = { ...patients.value[rowIndex.value], ...e2 } + } selectedPatient.value = e2 + loadPatients() } diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index b799339d..241e37f9 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -366,7 +366,7 @@
提示
-
本次操作未保存有效数据,不予记录。
+
本次检测未截图或录像操作,不予存档记录!
确定
@@ -521,8 +521,23 @@ function handleCancel(){ function handleCameraCancel(){ cameraDialogVisible.value = false } -function closeTipClick(){ - emit('endChange',false) +async function closeTipClick(){ + try { + if (timerId.value) { + clearInterval(timerId.value) + timerId.value = null + } + if (isRecording.value === true) { + stopRecord() + } + await stopDetection({}) + disconnectWebSocket() + window.removeEventListener('beforeunload', handleBeforeUnload) + emit('endChange',false) + routeTo('/') + } catch (error) { + emit('endChange',false) + } } const isDiagnosticMessage = ref(false) @@ -1711,19 +1726,15 @@ async function startDetection() { } // 停止检测 -async function stopDetection() { +async function stopDetection(summary = {}) { try { - // 计算检测持续时间 - let duration = 0 - // 调用后端API停止检测 + const payload = summary || {} const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - duration: duration - }) + body: JSON.stringify(payload) }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) @@ -2129,9 +2140,9 @@ function refreshClick(type) { } } -function closeDiagnosticMessage(e){ +async function closeDiagnosticMessage(e){ isDiagnosticMessage.value = false - if(e== true){ + if (e === true || (typeof e === 'object' && e)){ try { // 清理定时器 if (timerId.value) { @@ -2146,13 +2157,22 @@ function closeDiagnosticMessage(e){ console.log('✅ 录制已停止') } - // 停止检测 - stopDetection() + const summary = typeof e === 'object' && e !== null ? { + diagnosis_info: e.diagnosis_info ?? '', + treatment_info: e.treatment_info ?? '', + remark_info: (e.remark_info ?? e.suggestion_info ?? '') + } : { + diagnosis_info: diagnosticForm.value.diagnosis_info, + treatment_info: diagnosticForm.value.treatment_info, + remark_info: diagnosticForm.value.suggestion_info + } + await stopDetection(summary) // 断开WebSocket连接 disconnectWebSocket() // 移除页面关闭事件监听器 window.removeEventListener('beforeunload', handleBeforeUnload) emit('endChange',true) + routeTo('/') } catch (error) { console.error('❌ Detection组件卸载时出错:', error) emit('endChange',true) diff --git a/frontend/src/renderer/src/views/DiagnosticMessage.vue b/frontend/src/renderer/src/views/DiagnosticMessage.vue index 5589232a..03e7f37b 100644 --- a/frontend/src/renderer/src/views/DiagnosticMessage.vue +++ b/frontend/src/renderer/src/views/DiagnosticMessage.vue @@ -53,7 +53,11 @@ const props = defineProps({ }, }) -const diagnosticForm =ref({}) +const diagnosticForm = ref({ + diagnosis_info: '', + treatment_info: '', + suggestion_info: '' +}) // 生命周期 onMounted(() => { @@ -76,9 +80,9 @@ async function handleDiagnosticInfo(status) { 'Content-Type': 'application/json' }, body: JSON.stringify({ - diagnosis_info: diagnosticForm.diagnosis_info, - treatment_info: diagnosticForm.treatment_info, - suggestion_info: diagnosticForm.suggestion_info, + diagnosis_info: diagnosticForm.value.diagnosis_info, + treatment_info: diagnosticForm.value.treatment_info, + suggestion_info: diagnosticForm.value.suggestion_info, status: status, session_id: props.selectedPatient.sessionId, }) @@ -94,13 +98,17 @@ async function handleDiagnosticInfo(status) { message: status + '诊断信息成功', duration: 5000 }) - emit('closeDiagnosticMessage',true) + emit('closeDiagnosticMessage', { + diagnosis_info: diagnosticForm.value.diagnosis_info, + treatment_info: diagnosticForm.value.treatment_info, + remark_info: diagnosticForm.value.suggestion_info + }) } else { throw new Error(result.message || '诊断信息失败') } } catch (error) { ElMessage.error({ - message:'诊断信息失败', + message: error?.message || '诊断信息失败', duration: 5000 }) diff --git a/frontend/src/renderer/src/views/PatientCreate.vue b/frontend/src/renderer/src/views/PatientCreate.vue index fff35282..361e48db 100644 --- a/frontend/src/renderer/src/views/PatientCreate.vue +++ b/frontend/src/renderer/src/views/PatientCreate.vue @@ -30,7 +30,7 @@ + format="YYYY-MM-DD" value-format="YYYY-MM-DD" @change="calculateAgeres" /> @@ -89,7 +89,7 @@ - + @@ -148,12 +148,12 @@ const patientForm = reactive({ // 加载状态 const saveLoading = ref(false) const saveAndDetectLoading = ref(false) -const patienttitle = ref("新建档案") +const patienttitle = ref("新建患者档案") // 生命周期 onMounted(() => { // 从认证状态管理中加载用户信息 if (props.patienttype == 'edit') { - patienttitle.value = '编辑个人信息' + patienttitle.value = '编辑患者档案' let tempInfo = props.selectedPatient tempInfo.age = calculateAgeres(tempInfo.birth_date ) Object.assign(patientForm, tempInfo) @@ -180,28 +180,88 @@ const nationalityOptions = ref(["汉族", "满族", "蒙古族", "回族", "藏 "俄罗斯族", "鄂温克族", "德昂族", "保安族", "裕固族", "京族", "塔塔尔族", "独龙族", "鄂伦春族", "赫哲族", "门巴族", "珞巴族", "基诺族"]) // 表单验证规则 +const validateHeight = (rule, value, callback) => { + if (value === '' || value === null || value === undefined) return callback(new Error('请输入身高')) + const n = parseFloat(value) + if (isNaN(n)) return callback(new Error('身高格式无效')) + if (n < 50 || n > 250) return callback(new Error('身高应在50-250cm之间')) + callback() +} +const validateWeight = (rule, value, callback) => { + if (value === '' || value === null || value === undefined) return callback(new Error('请输入体重')) + const n = parseFloat(value) + if (isNaN(n)) return callback(new Error('体重格式无效')) + if (n < 10 || n > 300) return callback(new Error('体重应在10-300kg之间')) + callback() +} +const validateShoeSize = (rule, value, callback) => { + if (value === '' || value === null || value === undefined) return callback() + const n = parseFloat(value) + if (isNaN(n)) return callback(new Error('鞋码格式无效')) + if (n < 10 || n > 80) return callback(new Error('鞋码应在10-80之间')) + callback() +} +const validatePhone = (rule, value, callback) => { + const v = (value || '').trim() + if (!v) return callback(new Error('请输入联系电话')) + const digits = v.replace(/[-\s]/g, '') + if (!/^\d+$/.test(digits)) return callback(new Error('电话号码格式无效')) + callback() +} +const validateEmail = (rule, value, callback) => { + const v = (value || '').trim() + if (!v) return callback() + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!re.test(v)) return callback(new Error('电子邮箱格式无效')) + callback() +} +const validateIdcode = (rule, value, callback) => { + const v = (value || '').trim() + if (!v) return callback() + const re = /^\d{17}[\dXx]$/ + if (!re.test(v)) return callback(new Error('身份证号格式无效')) + const weights = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2] + const checkMap = ['1','0','X','9','8','7','6','5','4','3','2'] + let sum = 0 + for (let i = 0; i < 17; i++) { + sum += parseInt(v[i], 10) * weights[i] + } + const mod = sum % 11 + const code = checkMap[mod] + const last = v[17].toUpperCase() + if (last !== code) return callback(new Error('身份证号校验失败')) + callback() +} const formRules = { name: [ { required: true, message: '请输入患者姓名', trigger: 'blur' }, - { min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' } + { min: 2, max: 50, message: '姓名长度应在2-50个字符之间', trigger: 'blur' } ], gender: [ { required: true, message: '请选择性别', trigger: 'change' } ], - birthDate: [ + birth_date: [ { required: true, message: '请选择出生日期', trigger: 'change' } ], height: [ - { required: true, message: '请输入身高', trigger: 'blur' }, - { pattern: /^\d+(\.\d+)?$/, message: '请输入有效的身高', trigger: 'blur' } + { validator: validateHeight, trigger: 'blur' } ], weight: [ - { required: true, message: '请输入体重', trigger: 'blur' }, - { pattern: /^\d+(\.\d+)?$/, message: '请输入有效的体重', trigger: 'blur' } + { validator: validateWeight, trigger: 'blur' } + ], + shoe_size: [ + { validator: validateShoeSize, trigger: 'blur' } ], phone: [ - { required: true, message: '请输入联系电话', trigger: 'blur' }, - { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' } + { validator: validatePhone, trigger: 'blur' } + ] + , + email: [ + { validator: validateEmail, trigger: 'blur' } + ] + , + idcode: [ + { validator: validateIdcode, trigger: 'blur' } ] } @@ -225,6 +285,28 @@ const handleCancel = async () => { const validateForm = async () => { try { await patientFormRef.value.validate() + const errors = [] + const n = patientForm.name + if (!n || n.trim().length < 2 || n.trim().length > 50) errors.push('姓名长度应在2-50个字符之间') + const g = patientForm.gender + if (!g || (g !== '男' && g !== '女')) errors.push('性别值无效,应为:男、女') + const bd = patientForm.birth_date + if (!bd) errors.push('请选择出生日期') + if (bd) { + try { + const d = new Date(bd) + const today = new Date() + const lower = new Date('1900-01-01') + if (d > today) errors.push('出生日期不能是未来时间') + if (d < lower) errors.push('出生日期过早') + } catch { + errors.push('出生日期格式无效') + } + } + if (errors.length) { + ElMessage.error(errors.join('; ')) + return false + } return true } catch (error) { ElMessage.error('请完善必填信息') @@ -251,17 +333,17 @@ const savePatient = async () => { } try { - const response = await patientAPI.create(patientData) + const response = await patientAPI.createPatient(patientData) if (response.success) { emit('closecreatbox','新建',response.data) return response.data - } else { - throw new Error(response.message || '保存失败') + const msg = response.error || response.message || '保存失败' + throw new Error(msg) } } catch (error) { - console.error('保存患者信息失败:', error) - throw error + const msg = (error && error.response && error.response.data && (error.response.data.error || error.response.data.message)) || error.message || '保存失败' + throw new Error(msg) } } const updatePatient = async () => { @@ -287,10 +369,12 @@ const updatePatient = async () => { emit('closecreatbox','编辑',patientData) return response.data } else { - throw new Error(response.message || '修改失败') + const msg = response.error || response.message || '修改失败' + throw new Error(msg) } } catch (error) { - throw error + const msg = (error && error.response && error.response.data && (error.response.data.error || error.response.data.message)) || error.message || '修改失败' + throw new Error(msg) } } const handleSave = async () => { @@ -298,18 +382,17 @@ const handleSave = async () => { saveLoading.value = true try { - if(patientForm.id == null){ + if(patientForm.id == null||patientForm.id == ''){ await savePatient() }else{ await updatePatient() - } - + } ElMessage.success('患者档案保存成功') } catch (error) { - ElMessage.error('保存失败:' + error.message) + ElMessage.error('保存失败:' + (error && error.message ? error.message : '请检查填写信息')) } finally { saveLoading.value = false } @@ -444,6 +527,7 @@ const handleSave = async () => { font-weight: 400 !important; font-style: normal !important; color: #FFFFFF !important; + line-height: 40px !important; } :deep(.el-form-item__content) { diff --git a/frontend/src/renderer/src/views/PatientProfile.vue b/frontend/src/renderer/src/views/PatientProfile.vue index e885a74c..602574d6 100644 --- a/frontend/src/renderer/src/views/PatientProfile.vue +++ b/frontend/src/renderer/src/views/PatientProfile.vue @@ -53,8 +53,8 @@
职业
{{ selectedPatient.occupation }}
-
证件号
-
{{ selectedPatient.workplace }}
+
身份证号
+
{{ selectedPatient.idcode }}
@@ -95,9 +95,9 @@ @cell-click="selectRecord" @selection-change="handleSelectionChange" highlight-current-row> - - - + + +