This commit is contained in:
limengnan 2026-02-04 16:34:37 +08:00
commit c71f07f931
12 changed files with 1057 additions and 388 deletions

View File

@ -60,7 +60,7 @@ pressure_port = COM5
pressure_baudrate = 115200
[REMOTE]
enable = False
enable = True
port = COM6
baudrate = 115200
timeout = 0.1

View File

@ -830,7 +830,7 @@ class DatabaseManager:
cursor.execute('''
SELECT * FROM detection_data
WHERE session_id = ?
ORDER BY timestamp
ORDER BY timestamp desc
''', (session_id,))
data_rows = cursor.fetchall()
@ -849,7 +849,7 @@ class DatabaseManager:
cursor.execute('''
SELECT * FROM detection_video
WHERE session_id = ?
ORDER BY timestamp
ORDER BY timestamp desc
''', (session_id,))
video_rows = cursor.fetchall()
@ -1103,12 +1103,15 @@ class DatabaseManager:
cursor.execute('''
INSERT INTO detection_video (
id, session_id, screen_video, timestamp
) VALUES (?, ?, ?, ?)
id, session_id, screen_video, body_video, foot_video1, foot_video2, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
video_id,
session_id,
video.get('screen_video_path'),
video.get('body_video_path'),
video.get('foot_video1_path'),
video.get('foot_video2_path'),
china_time
))
@ -1120,6 +1123,50 @@ class DatabaseManager:
logger.error(f'保存检测视频失败: {e}')
return False
def update_detection_video_latest(self, session_id: str, video: Dict[str, Any]) -> bool:
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('''
SELECT id, screen_video, body_video, foot_video1, foot_video2
FROM detection_video
WHERE session_id = ?
ORDER BY timestamp DESC
LIMIT 1
''', (session_id,))
row = cursor.fetchone()
if not row:
return self.save_detection_video(session_id, video)
row_dict = dict(row)
video_id = row_dict.get('id')
screen_video = video.get('screen_video_path')
body_video = video.get('body_video_path')
foot_video1 = video.get('foot_video1_path')
foot_video2 = video.get('foot_video2_path')
if not screen_video:
screen_video = row_dict.get('screen_video')
if not body_video:
body_video = row_dict.get('body_video')
if not foot_video1:
foot_video1 = row_dict.get('foot_video1')
if not foot_video2:
foot_video2 = row_dict.get('foot_video2')
cursor.execute('''
UPDATE detection_video
SET screen_video = ?, body_video = ?, foot_video1 = ?, foot_video2 = ?
WHERE id = ?
''', (screen_video, body_video, foot_video1, foot_video2, video_id))
conn.commit()
return True
except Exception as e:
conn.rollback()
logger.error(f'更新检测视频失败: {e}')
return False
def delete_detection_video(self, video_ids: Union[str, List[str]]) -> bool:
"""删除检测视频记录支持单个或多个ID"""
conn = self.get_connection()

View File

@ -14,6 +14,7 @@ from typing import Optional, Dict, Any
import logging
import queue
import gc
import os
try:
from .base_device import BaseDevice
@ -110,7 +111,19 @@ class CameraManager(BaseDevice):
}
# 全局帧队列(用于录制)
self.frame_queue = queue.Queue(maxsize=10) # 最大长度10自动丢弃旧帧
self.frame_queue = queue.Queue(maxsize=10)
self._recording_enabled = False
self._recording_session_id = None
self._recording_frames_dir = None
self._recording_target_fps = None
self._recording_last_ts = 0.0
self._recording_index = 0
self._recording_written = 0
self._recording_drop = 0
self._recording_queue = queue.Queue(maxsize=300)
self._recording_thread = None
self._recording_stop_event = threading.Event()
# 属性缓存机制 - 避免重复设置相同属性值
self._property_cache = {}
@ -126,6 +139,130 @@ class CameraManager(BaseDevice):
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}")
def start_jpeg_recording(self, session_id: str, frames_dir: str, record_fps: Optional[int] = None) -> Dict[str, Any]:
try:
if not session_id:
return {'success': False, 'message': '缺少session_id'}
if not frames_dir:
return {'success': False, 'message': '缺少frames_dir'}
try:
os.makedirs(frames_dir, exist_ok=True)
except Exception as e:
return {'success': False, 'message': f'创建录制目录失败: {e}'}
if record_fps is None:
record_fps = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'fps', fallback='10'))
record_fps = max(1, int(record_fps))
self._recording_session_id = session_id
self._recording_frames_dir = frames_dir
self._recording_target_fps = record_fps
self._recording_last_ts = 0.0
self._recording_index = 0
self._recording_written = 0
self._recording_drop = 0
self._recording_stop_event.clear()
self._recording_enabled = True
if not self._recording_thread or not self._recording_thread.is_alive():
self._recording_thread = threading.Thread(
target=self._recording_writer_loop,
name=f"{self.device_id}-JpegWriter",
daemon=True
)
self._recording_thread.start()
return {'success': True, 'message': '相机JPEG录制已启动', 'device_id': self.device_id, 'fps': record_fps, 'frames_dir': frames_dir}
except Exception as e:
return {'success': False, 'message': str(e)}
def stop_jpeg_recording(self, session_id: Optional[str] = None) -> Dict[str, Any]:
try:
if session_id and self._recording_session_id and session_id != self._recording_session_id:
return {'success': False, 'message': 'session_id不匹配'}
self._recording_enabled = False
self._recording_session_id = None
frames_dir = self._recording_frames_dir
fps = self._recording_target_fps
written = int(self._recording_written)
dropped = int(self._recording_drop)
self._recording_frames_dir = None
self._recording_target_fps = None
self._recording_last_ts = 0.0
self._recording_stop_event.set()
if self._recording_thread and self._recording_thread.is_alive():
self._recording_thread.join(timeout=2.0)
self._recording_thread = None
while not self._recording_queue.empty():
try:
self._recording_queue.get_nowait()
except queue.Empty:
break
self._recording_stop_event.clear()
return {
'success': True,
'message': '相机JPEG录制已停止',
'device_id': self.device_id,
'frames_dir': frames_dir,
'fps': fps,
'frames_written': written,
'frames_dropped': dropped
}
except Exception as e:
return {'success': False, 'message': str(e)}
def _recording_writer_loop(self):
while not self._recording_stop_event.is_set():
try:
item = self._recording_queue.get(timeout=0.2)
except queue.Empty:
continue
try:
frames_dir, idx, jpeg_bytes = item
if not frames_dir or not jpeg_bytes:
continue
filename = f"frame_{idx:06d}.jpg"
fpath = os.path.join(frames_dir, filename)
with open(fpath, 'wb') as f:
f.write(jpeg_bytes)
self._recording_written += 1
except Exception:
self._recording_drop += 1
finally:
try:
self._recording_queue.task_done()
except Exception:
pass
def _maybe_enqueue_recording(self, timestamp: float, frame_bytes: bytes):
if not self._recording_enabled:
return
frames_dir = self._recording_frames_dir
target_fps = self._recording_target_fps
if not frames_dir or not target_fps or target_fps <= 0:
return
if self._recording_last_ts > 0:
min_interval = 1.0 / float(target_fps)
if (timestamp - self._recording_last_ts) < min_interval:
return
idx = self._recording_index
self._recording_index += 1
self._recording_last_ts = timestamp
try:
self._recording_queue.put_nowait((frames_dir, idx, frame_bytes))
except queue.Full:
self._recording_drop += 1
def _set_property_optimized(self, prop, value):
"""
优化的属性设置方法使用缓存避免重复设置
@ -649,23 +786,6 @@ class CameraManager(BaseDevice):
# 更新心跳时间,防止连接监控线程判定为超时
self.update_heartbeat()
# 保存原始帧到队列(用于录制)
try:
self.frame_queue.put_nowait({
'frame': frame.copy(),
'timestamp': time.time()
})
except queue.Full:
# 队列满时丢弃最旧的帧,添加新帧
try:
self.frame_queue.get_nowait() # 移除最旧的帧
self.frame_queue.put_nowait({
'frame': frame.copy(),
'timestamp': time.time()
})
except queue.Empty:
pass # 队列为空,忽略
# 处理帧(降采样以优化传输负载)
processed_frame = self._process_frame(frame)
@ -740,6 +860,7 @@ class CameraManager(BaseDevice):
# 转换为bytes再做base64减少中间numpy对象的长生命周期
frame_bytes = buffer.tobytes()
self._maybe_enqueue_recording(time.time(), frame_bytes)
frame_data = base64.b64encode(frame_bytes).decode('utf-8')
# 发送数据
@ -986,6 +1107,21 @@ class CameraManager(BaseDevice):
except queue.Empty:
break
self._recording_enabled = False
self._recording_session_id = None
self._recording_frames_dir = None
self._recording_target_fps = None
self._recording_last_ts = 0.0
self._recording_stop_event.set()
if self._recording_thread and self._recording_thread.is_alive():
self._recording_thread.join(timeout=2.0)
self._recording_thread = None
while not self._recording_queue.empty():
try:
self._recording_queue.get_nowait()
except queue.Empty:
break
self.last_frame = None
super().cleanup()

View File

@ -291,9 +291,11 @@ class DeviceCoordinator:
from .remote_control_manager import RemoteControlManager
remote = RemoteControlManager(self.socketio, self.config_manager)
self.devices['remote'] = remote
if remote.initialize():
return True
if not remote.initialize():
return False
if hasattr(remote, 'start_streaming'):
return bool(remote.start_streaming())
return True
except Exception as e:
self.logger.error(f"初始化遥控器失败: {e}")
return False
@ -743,6 +745,12 @@ class DeviceCoordinator:
except ImportError:
from femtobolt_manager import FemtoBoltManager
new_device = FemtoBoltManager(self.socketio, self.config_manager)
elif device_name == 'remote':
try:
from .remote_control_manager import RemoteControlManager
except ImportError:
from remote_control_manager import RemoteControlManager
new_device = RemoteControlManager(self.socketio, self.config_manager)
else:
raise ValueError(f"未知的设备类型: {device_name}")
@ -777,6 +785,17 @@ class DeviceCoordinator:
init_time = (time.time() - init_start) * 1000
self.logger.info(f"{device_name} 设备初始化成功 (耗时: {init_time:.1f}ms)")
if device_name == 'remote' and hasattr(new_device, 'start_streaming'):
self.logger.info(f"正在启动 {device_name} 设备推流...")
try:
if not new_device.start_streaming():
self.logger.error(f"启动 {device_name} 设备推流失败")
return False
was_streaming = True
except Exception as e:
self.logger.error(f"启动 {device_name} 推流异常: {e}")
return False
# 设备初始化成功后,确保状态广播正确
# 此时设备应该已经通过initialize()方法中的set_connected(True)触发了状态变化通知
# 但为了确保状态一致性,我们再次确认状态

View File

@ -66,7 +66,7 @@ class RemoteControlManager(BaseDevice):
def initialize(self) -> bool:
try:
self.logger.info(f"初始化遥控器串口: {self.port}, {self.baudrate}bps, 8N1")
self.set_connected(True)
self.set_connected(False)
self._device_info['initialized_at'] = time.time()
return True
except Exception as e:
@ -88,6 +88,9 @@ class RemoteControlManager(BaseDevice):
stopbits=self.stopbits,
timeout=self.timeout,
)
self.set_connected(True)
self.update_heartbeat()
self.is_streaming = True
self._running = True
self._thread = threading.Thread(target=self._worker_loop, daemon=True)
self._thread.start()
@ -110,6 +113,9 @@ class RemoteControlManager(BaseDevice):
self._thread.join(timeout=2.0)
if self._ser and self._ser.is_open:
self._ser.close()
self._ser = None
self.set_connected(False)
self.is_streaming = False
self.logger.info("遥控器串口监听已停止")
return True
except Exception as e:
@ -248,6 +254,7 @@ class RemoteControlManager(BaseDevice):
time.sleep(0.05)
continue
chunk = self._ser.read(64)
self.update_heartbeat()
if chunk:
try:
hexstr = ' '.join(f'{b:02X}' for b in chunk)
@ -262,5 +269,14 @@ class RemoteControlManager(BaseDevice):
self.logger.debug("遥控器串口暂无数据")
except Exception as e:
self.logger.error(f"遥控器串口读取异常: {e}")
try:
if self._ser and self._ser.is_open:
self._ser.close()
except Exception:
pass
self._ser = None
self.set_connected(False)
self.is_streaming = False
self._running = False
time.sleep(0.1)
self.logger.info("遥控器串口线程结束")

View File

@ -13,7 +13,8 @@ import signal
import base64
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, List, Optional
from typing import Dict, Any, List, Optional, Callable
import threading
try:
import pyautogui
@ -39,11 +40,38 @@ class RecordingManager:
# FFmpeg进程管理
self._ffmpeg_processes = {}
self._ffmpeg_meta = {}
self._transcode_threads = {}
# 默认参数
self.screen_fps = 25
self.screen_size = self._get_screen_size()
def _resolve_ffmpeg_path(self) -> Optional[str]:
ffmpeg_path = None
if self.config_manager:
ffmpeg_path = (
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or
self.config_manager.get_config_value('RECORDING', 'ffmpeg_path', fallback=None)
)
if ffmpeg_path and os.path.isfile(str(ffmpeg_path)):
return str(ffmpeg_path)
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
alt_path = os.path.join(base_dir, 'ffmpeg', 'bin', 'ffmpeg.exe')
if os.path.isfile(alt_path):
return alt_path
return None
def get_active_video_base(self, session_id: str) -> Optional[Dict[str, Any]]:
meta = self._ffmpeg_meta.get('screen') or {}
if meta.get('session_id') != session_id:
return None
return {
'base_path': meta.get('base_path'),
'file_dir': meta.get('file_dir'),
'patient_id': meta.get('patient_id'),
'session_id': meta.get('session_id')
}
def _get_screen_size(self):
try:
import pyautogui
@ -82,18 +110,8 @@ class RecordingManager:
os.makedirs(base_path, exist_ok=True)
screen_video_path = os.path.join(base_path, 'screen.mp4')
target_fps = fps or self.screen_fps
ffmpeg_path = None
if self.config_manager:
ffmpeg_path = (
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or
self.config_manager.get_config_value('RECORDING', 'ffmpeg_path', fallback=None)
)
if not ffmpeg_path or not os.path.isfile(str(ffmpeg_path)):
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
alt_path = os.path.join(base_dir, 'ffmpeg', 'bin', 'ffmpeg.exe')
if os.path.isfile(alt_path):
ffmpeg_path = alt_path
else:
ffmpeg_path = self._resolve_ffmpeg_path()
if not ffmpeg_path:
result['message'] = '未配置有效的ffmpeg_path请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
return result
@ -136,7 +154,7 @@ class RecordingManager:
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
)
self._ffmpeg_processes['screen'] = proc
self._ffmpeg_meta['screen'] = {'base_path': base_path, 'patient_id': patient_id, 'session_id': session_id, 'video_path': screen_video_path}
self._ffmpeg_meta['screen'] = {'base_path': base_path, 'file_dir': file_dir, 'patient_id': patient_id, 'session_id': session_id, 'video_path': screen_video_path}
result['success'] = True
result['message'] = 'ffmpeg录制已启动'
result['database_updates'] = {
@ -151,6 +169,86 @@ class RecordingManager:
result['message'] = f'ffmpeg启动失败: {e}'
return result
def transcode_jpeg_sequence_async(
self,
name: str,
frames_dir: str,
output_mp4_path: str,
fps: int,
on_done: Optional[Callable[[int], None]] = None
) -> Dict[str, Any]:
try:
if not frames_dir or not os.path.isdir(frames_dir):
return {'success': False, 'message': 'frames_dir不存在'}
try:
os.makedirs(os.path.dirname(output_mp4_path), exist_ok=True)
except Exception:
pass
ffmpeg_path = self._resolve_ffmpeg_path()
if not ffmpeg_path:
return {'success': False, 'message': 'ffmpeg_path无效'}
codec = (
self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_codec', fallback=None) or
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_codec', fallback=None) or
'libx264'
)
preset = (
self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_preset', fallback=None) or
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_preset', fallback=None) or
('p1' if codec == 'h264_nvenc' else 'ultrafast')
)
threads = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_threads', fallback='1') or '1')
bframes = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_bframes', fallback='0') or '0')
gop = int(self.config_manager.get_config_value('CAMERA_RECORDING', 'ffmpeg_gop', fallback=str(max(1, int(fps * 2)))) or str(max(1, int(fps * 2))))
input_pattern = os.path.join(frames_dir, 'frame_%06d.jpg')
cmd = [
str(ffmpeg_path),
'-y',
'-framerate', str(int(max(1, fps))),
'-start_number', '0',
'-i', input_pattern,
'-c:v', str(codec),
'-preset', str(preset),
'-bf', str(bframes),
'-g', str(gop),
'-pix_fmt', 'yuv420p',
'-threads', str(threads),
str(output_mp4_path)
]
def worker():
code = 1
try:
proc = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
)
self._ffmpeg_processes[name] = proc
code = proc.wait()
except Exception:
code = 1
finally:
self._ffmpeg_processes.pop(name, None)
self._transcode_threads.pop(name, None)
if on_done:
try:
on_done(int(code))
except Exception:
pass
t = threading.Thread(target=worker, name=f"FFmpegTranscode-{name}", daemon=True)
self._transcode_threads[name] = t
t.start()
return {'success': True, 'message': '转码任务已启动', 'name': name, 'output': output_mp4_path}
except Exception as e:
return {'success': False, 'message': str(e)}
def stop_recording_ffmpeg(self, session_id: str = None) -> Dict[str, Any]:
result = {'success': False, 'message': ''}
try:

View File

@ -434,6 +434,8 @@ class ConfigManager:
self.set_config_value('DEVICES', 'imu_use_mock', str(config_data['use_mock']))
if 'mac_address' in config_data:
self.set_config_value('DEVICES', 'imu_mac_address', config_data['mac_address'])
if 'ble_name' in config_data:
self.set_config_value('DEVICES', 'imu_ble_name', config_data['ble_name'])
results['imu'] = {
'success': True,

View File

@ -16,6 +16,7 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.exceptions import InvalidSignature
import base64
import shutil
logger = logging.getLogger(__name__)
@ -51,85 +52,345 @@ class LicenseManager:
def __init__(self, config_manager=None):
self.config_manager = config_manager
self._machine_id = None
self._machine_id_candidates = None
self._license_cache = None
self._cache_timestamp = None
def _get_backend_base_dir(self) -> str:
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
def _get_persistent_license_dir(self) -> str:
system = platform.system()
if system == "Windows":
base = os.environ.get("PROGRAMDATA") or os.environ.get("ALLUSERSPROFILE") or ""
if not base:
base = os.path.expanduser("~\\AppData\\Local")
else:
base = os.path.expanduser("~/.config")
return os.path.join(base, "BodyCheck", "license")
def _get_configured_license_paths(self) -> Tuple[str, str, int]:
license_path = "data/license.json"
public_key_path = "backend/license_pub.pem"
grace_days = 3
if self.config_manager:
license_path = self.config_manager.get_config_value("LICENSE", "path", license_path)
public_key_path = self.config_manager.get_config_value("LICENSE", "public_key", public_key_path)
grace_days = int(self.config_manager.get_config_value("LICENSE", "grace_days", str(grace_days)))
if not os.path.isabs(license_path):
license_path = os.path.join(self._get_backend_base_dir(), license_path)
if not os.path.isabs(public_key_path):
public_key_path = os.path.join(self._get_backend_base_dir(), public_key_path)
return license_path, public_key_path, grace_days
def _get_persistent_license_paths(self) -> Tuple[str, str]:
pdir = self._get_persistent_license_dir()
return os.path.join(pdir, "license.json"), os.path.join(pdir, "license_public_key.pem")
def _resolve_license_paths(self) -> Tuple[str, str, int]:
cfg_license_path, cfg_public_key_path, grace_days = self._get_configured_license_paths()
p_license_path, p_public_key_path = self._get_persistent_license_paths()
effective_license_path = cfg_license_path
if not os.path.exists(effective_license_path) and os.path.exists(p_license_path):
effective_license_path = p_license_path
effective_public_key_path = cfg_public_key_path
if not os.path.exists(effective_public_key_path) and os.path.exists(p_public_key_path):
effective_public_key_path = p_public_key_path
return effective_license_path, effective_public_key_path, grace_days
def _mirror_license_assets(self, license_path: Optional[str] = None, public_key_path: Optional[str] = None) -> None:
p_license_path, p_public_key_path = self._get_persistent_license_paths()
pdir = os.path.dirname(p_license_path)
try:
os.makedirs(pdir, exist_ok=True)
except Exception:
return
if license_path and os.path.exists(license_path):
try:
shutil.copyfile(license_path, p_license_path)
except Exception:
pass
if public_key_path and os.path.exists(public_key_path):
try:
shutil.copyfile(public_key_path, p_public_key_path)
except Exception:
pass
def install_license_file(self, source_license_path: str) -> Tuple[bool, str]:
try:
if not os.path.exists(source_license_path):
return False, "源授权文件不存在"
cfg_license_path, cfg_public_key_path, _ = self._get_configured_license_paths()
os.makedirs(os.path.dirname(cfg_license_path), exist_ok=True)
shutil.copyfile(source_license_path, cfg_license_path)
self._mirror_license_assets(license_path=cfg_license_path, public_key_path=cfg_public_key_path)
return True, cfg_license_path
except Exception as e:
return False, str(e)
def mirror_license_dir(self, source_dir: str) -> None:
if not source_dir or not os.path.isdir(source_dir):
return
license_candidate = os.path.join(source_dir, "license.json")
pub_candidates = [
os.path.join(source_dir, "license_public_key.pem"),
os.path.join(source_dir, "license_pub.pem"),
os.path.join(source_dir, "public_key.pem"),
]
pub_path = ""
for c in pub_candidates:
if os.path.exists(c):
pub_path = c
break
self._mirror_license_assets(license_path=license_candidate if os.path.exists(license_candidate) else None, public_key_path=pub_path or None)
def _run_powershell(self, command: str, timeout: int = 10) -> str:
try:
result = subprocess.run(
[
"powershell",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
command,
],
capture_output=True,
text=True,
timeout=timeout,
)
return (result.stdout or "").strip()
except Exception:
return ""
def _get_windows_cpu_id(self) -> str:
out = self._run_powershell(
"(Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 -ExpandProperty ProcessorId)"
)
return out.strip()
def _get_windows_baseboard_serial(self) -> str:
out = self._run_powershell(
"(Get-CimInstance -ClassName Win32_BaseBoard | Select-Object -First 1 -ExpandProperty SerialNumber)"
)
serial = out.strip()
if serial and serial != "To be filled by O.E.M.":
return serial
return ""
def _get_windows_disk_serials(self) -> list:
raw = self._run_powershell(
"$d=Get-CimInstance -ClassName Win32_DiskDrive | Select-Object SerialNumber,InterfaceType,PNPDeviceID,MediaType; $d | ConvertTo-Json -Compress"
)
if not raw:
return []
try:
data = json.loads(raw)
except Exception:
return []
disks = data if isinstance(data, list) else ([data] if isinstance(data, dict) else [])
serials = []
for d in disks:
serial = str((d.get("SerialNumber") or "")).strip()
iface = str((d.get("InterfaceType") or "")).strip().upper()
pnp = str((d.get("PNPDeviceID") or "")).strip().upper()
media = str((d.get("MediaType") or "")).strip().upper()
if serial and iface != "USB" and not pnp.startswith("USBSTOR") and "REMOVABLE" not in media:
serials.append(serial)
serials = sorted(set(serials))
return serials
def _get_windows_identifiers(self) -> Dict[str, Any]:
cpu_id = self._get_windows_cpu_id()
board_serial = self._get_windows_baseboard_serial()
disk_serials = self._get_windows_disk_serials()
if not cpu_id:
try:
result = subprocess.run(
["wmic", "cpu", "get", "ProcessorId", "/value"],
capture_output=True,
text=True,
timeout=10,
)
for line in (result.stdout or "").split("\n"):
if "ProcessorId=" in line:
cpu_id = line.split("=", 1)[1].strip()
if cpu_id:
break
except Exception:
pass
if not board_serial:
try:
result = subprocess.run(
["wmic", "baseboard", "get", "SerialNumber", "/value"],
capture_output=True,
text=True,
timeout=10,
)
for line in (result.stdout or "").split("\n"):
if "SerialNumber=" in line:
board_serial = line.split("=", 1)[1].strip()
if board_serial and board_serial != "To be filled by O.E.M.":
break
board_serial = ""
except Exception:
pass
if not disk_serials:
try:
result = subprocess.run(
[
"wmic",
"path",
"Win32_DiskDrive",
"get",
"SerialNumber,InterfaceType,PNPDeviceID,MediaType",
"/value",
],
capture_output=True,
text=True,
timeout=10,
)
block = {}
serials = []
for line in (result.stdout or "").split("\n"):
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:
serials.append(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:
serials.append(serial)
disk_serials = sorted(set(serials))
except Exception:
pass
mac = ""
try:
import uuid
mac = ":".join(
["{:02x}".format((uuid.getnode() >> elements) & 0xFF) for elements in range(0, 2 * 6, 2)][::-1]
)
except Exception:
mac = ""
return {
"cpu_id": cpu_id.strip() if cpu_id else "",
"board_serial": board_serial.strip() if board_serial else "",
"disk_serials": disk_serials or [],
"mac": mac,
"node": platform.node(),
"processor": platform.processor(),
}
def _hash_core_info(self, core_info: list) -> str:
combined_info = "|".join(sorted(core_info))
return hashlib.sha256(combined_info.encode("utf-8")).hexdigest()[:16].upper()
def _build_machine_id_candidates(self) -> list:
system = platform.system()
info: Dict[str, Any] = {}
if system == "Windows":
info = self._get_windows_identifiers()
else:
info = {
"cpu_id": "",
"board_serial": "",
"disk_serials": [],
"mac": "",
"node": platform.node(),
"processor": platform.processor(),
}
cpu_id = info.get("cpu_id") or ""
board_serial = info.get("board_serial") or ""
disk_serials = info.get("disk_serials") or []
mac = info.get("mac") or ""
variants = []
full_core = []
if cpu_id:
full_core.append(f"CPU:{cpu_id}")
if board_serial:
full_core.append(f"BOARD:{board_serial}")
for s in disk_serials:
full_core.append(f"DISK:{s}")
if full_core:
variants.append(full_core)
no_disk = []
if cpu_id:
no_disk.append(f"CPU:{cpu_id}")
if board_serial:
no_disk.append(f"BOARD:{board_serial}")
if no_disk:
variants.append(no_disk)
if cpu_id:
variants.append([f"CPU:{cpu_id}"])
if board_serial:
variants.append([f"BOARD:{board_serial}"])
if mac:
variants.append([f"MAC:{mac}"])
if cpu_id and mac:
variants.append([f"CPU:{cpu_id}", f"MAC:{mac}"])
fallback_core = []
node = (info.get("node") or "").strip()
proc = (info.get("processor") or "").strip()
if node:
fallback_core.append(f"NODE:{node}")
if proc:
fallback_core.append(f"PROCESSOR:{proc}")
if fallback_core:
variants.append(fallback_core)
prefix = "W10-" if system == "Windows" else "FB-"
candidates = []
for core in variants:
try:
mid = f"{prefix}{self._hash_core_info(core)}"
except Exception:
continue
if mid not in candidates:
candidates.append(mid)
return candidates
def get_machine_id(self) -> str:
"""生成机器硬件指纹"""
if self._machine_id:
return self._machine_id
try:
core_info = []
aux_info = []
try:
if platform.system() == "Windows":
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:
core_info.append(f"CPU:{cpu_id}")
break
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)
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.":
core_info.append(f"BOARD:{board_serial}")
break
except Exception as e:
logger.warning(f"获取主板信息失败: {e}")
try:
if platform.system() == "Windows":
# 获取磁盘信息并过滤掉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'):
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}")
try:
import uuid
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}")
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}"
candidates = self._build_machine_id_candidates()
if not candidates:
raise RuntimeError("无法生成机器指纹")
self._machine_id_candidates = candidates
self._machine_id = candidates[0]
logger.info(f"生成机器指纹: {self._machine_id}")
return self._machine_id
except Exception as e:
@ -137,8 +398,14 @@ class LicenseManager:
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}"
self._machine_id_candidates = [self._machine_id]
return self._machine_id
def get_machine_id_candidates(self) -> list:
if self._machine_id_candidates is None:
self.get_machine_id()
return list(self._machine_id_candidates or [])
def load_license(self, license_path: str) -> Optional[Dict[str, Any]]:
"""加载授权文件"""
try:
@ -231,7 +498,13 @@ class LicenseManager:
# 检查机器绑定
license_machine_id = license_data.get('machine_id', '')
if license_machine_id != machine_id:
candidates = []
if machine_id:
candidates.append(machine_id)
for mid in self.get_machine_id_candidates():
if mid not in candidates:
candidates.append(mid)
if license_machine_id not in candidates:
return False, f"授权文件与当前机器不匹配 (当前: {machine_id}, 授权: {license_machine_id})"
# 检查有效期
@ -269,18 +542,7 @@ class LicenseManager:
if not self.config_manager:
return LicenseStatus(valid=False, message="配置管理器未初始化")
license_path = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json')
public_key_path = self.config_manager.get_config_value('LICENSE', 'public_key', 'backend/license_pub.pem')
grace_days = int(self.config_manager.get_config_value('LICENSE', 'grace_days', '3'))
# 转换为绝对路径
if not os.path.isabs(license_path):
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # backend目录
license_path = os.path.join(base_dir, license_path)
if not os.path.isabs(public_key_path):
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # backend目录
public_key_path = os.path.join(base_dir, public_key_path)
license_path, public_key_path, grace_days = self._resolve_license_paths()
# 获取机器指纹
machine_id = self.get_machine_id()
@ -309,6 +571,8 @@ class LicenseManager:
self._cache_timestamp = datetime.now().timestamp()
return status
self._mirror_license_assets(license_path=license_path, public_key_path=public_key_path)
# 检查有效性
is_valid, message = self.check_validity(license_data, machine_id, grace_days)
@ -362,14 +626,7 @@ class LicenseManager:
if not self.config_manager:
return False, "配置管理器未初始化"
# 解析公钥路径与宽限期
public_key_path = self.config_manager.get_config_value('LICENSE', 'public_key', 'backend/license_pub.pem')
grace_days = int(self.config_manager.get_config_value('LICENSE', 'grace_days', '3'))
# 转换为绝对路径相对backend目录
if not os.path.isabs(public_key_path):
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
public_key_path = os.path.join(base_dir, public_key_path)
_, public_key_path, grace_days = self._resolve_license_paths()
if not os.path.exists(license_path):
return False, f"授权文件不存在: {license_path}"

View File

@ -10,6 +10,7 @@ import sys
import json
import time
import threading
import shutil
from datetime import datetime
from flask import Flask, jsonify
from flask import request as flask_request
@ -378,6 +379,10 @@ class AppServer:
'error': '授权管理器未初始化'
}), 500
if self.license_manager:
self.license_status = self.license_manager.get_license_status(force_reload=True)
self.app.license_status = self.license_status
return jsonify({
'success': True,
'data': {
@ -548,22 +553,14 @@ class AppServer:
if is_valid:
# 覆盖系统授权文件为上传的文件
try:
license_path_cfg = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json') if self.config_manager else 'data/license.json'
# 解析目标路径为绝对路径
if not os.path.isabs(license_path_cfg):
base_dir = os.path.dirname(os.path.abspath(__file__))
license_path_cfg = os.path.join(base_dir, license_path_cfg)
os.makedirs(os.path.dirname(license_path_cfg), exist_ok=True)
# 移动/覆盖授权文件
import shutil
shutil.copyfile(temp_path, license_path_cfg)
except Exception as e:
self.logger.error(f'保存授权文件失败: {e}')
return jsonify({'success': False, 'error': f'保存授权文件失败: {str(e)}'}), 500
ok, result = self.license_manager.install_license_file(temp_path)
if not ok:
self.logger.error(f'保存授权文件失败: {result}')
return jsonify({'success': False, 'error': f'保存授权文件失败: {result}'}), 500
# 更新授权状态(强制刷新)
self.license_status = self.license_manager.get_license_status(force_reload=True)
self.app.license_status = self.license_status
return jsonify({
'success': True,
@ -631,8 +628,11 @@ class AppServer:
return jsonify({'success': False, 'error': '压缩包包含非法路径'}), 400
zip_ref.extractall(target_dir)
self.license_manager.mirror_license_dir(target_dir)
# 刷新授权
self.license_status = self.license_manager.get_license_status(force_reload=True)
self.app.license_status = self.license_status
return jsonify({
'success': True,
@ -1449,6 +1449,30 @@ class AppServer:
# 使用新的ffmpeg录制方法
recording_response = self.recording_manager.start_recording_ffmpeg(session_id, patient_id, screen_location)
base_info = None
try:
base_info = self.recording_manager.get_active_video_base(session_id)
except Exception:
base_info = None
if base_info and base_info.get('base_path'):
base_path = base_info.get('base_path')
try:
cam1 = self.device_coordinator.devices.get('camera1') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
if cam1 and getattr(cam1, 'is_streaming', False):
cam1_frames_dir = os.path.join(base_path, 'camera1_frames')
cam1.start_jpeg_recording(session_id=session_id, frames_dir=cam1_frames_dir)
except Exception as e:
self.logger.error(f'启动camera1 JPEG录制失败: {e}')
try:
cam2 = self.device_coordinator.devices.get('camera2') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
if cam2 and getattr(cam2, 'is_streaming', False):
cam2_frames_dir = os.path.join(base_path, 'camera2_frames')
cam2.start_jpeg_recording(session_id=session_id, frames_dir=cam2_frames_dir)
except Exception as e:
self.logger.error(f'启动camera2 JPEG录制失败: {e}')
# 处理录制管理器返回的数据库更新信息
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
db_updates = recording_response['database_updates']
@ -1456,7 +1480,10 @@ class AppServer:
# 保存检测视频记录(映射到 detection_video 表字段)
video_paths = db_updates.get('video_paths', {})
video_record = {
'screen_video_path': video_paths.get('screen_video_path')
'screen_video_path': video_paths.get('screen_video_path'),
'body_video_path': None,
'foot_video1_path': None,
'foot_video2_path': None
}
try:
@ -1495,6 +1522,76 @@ class AppServer:
except Exception as rec_e:
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
raise
base_info = None
try:
base_info = self.recording_manager.get_active_video_base(session_id)
except Exception:
base_info = None
if base_info and base_info.get('base_path') and base_info.get('file_dir'):
base_path = base_info.get('base_path')
file_dir = base_info.get('file_dir')
cam1 = self.device_coordinator.devices.get('camera1') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
cam2 = self.device_coordinator.devices.get('camera2') if self.device_coordinator and hasattr(self.device_coordinator, 'devices') else None
if cam1:
try:
cam1_stop = cam1.stop_jpeg_recording(session_id=session_id)
cam1_frames_dir = cam1_stop.get('frames_dir')
cam1_fps = int(cam1_stop.get('fps') or 10)
if cam1_frames_dir and os.path.isdir(cam1_frames_dir):
out1 = os.path.join(base_path, 'foot1.mp4')
rel1 = os.path.relpath(out1, file_dir).replace('\\', '/')
def _done1(code: int):
if code == 0:
try:
self.db_manager.update_detection_video_latest(session_id, {'foot_video1_path': rel1})
except Exception as e:
self.logger.error(f'更新foot_video1失败: {e}')
try:
shutil.rmtree(cam1_frames_dir, ignore_errors=True)
except Exception as e:
self.logger.error(f'删除camera1_frames失败: {e}')
self.recording_manager.transcode_jpeg_sequence_async(
name=f'{session_id}-camera1',
frames_dir=cam1_frames_dir,
output_mp4_path=out1,
fps=cam1_fps,
on_done=_done1
)
except Exception as e:
self.logger.error(f'停止/转码camera1录制失败: {e}')
if cam2:
try:
cam2_stop = cam2.stop_jpeg_recording(session_id=session_id)
cam2_frames_dir = cam2_stop.get('frames_dir')
cam2_fps = int(cam2_stop.get('fps') or 10)
if cam2_frames_dir and os.path.isdir(cam2_frames_dir):
out2 = os.path.join(base_path, 'foot2.mp4')
rel2 = os.path.relpath(out2, file_dir).replace('\\', '/')
def _done2(code: int):
if code == 0:
try:
self.db_manager.update_detection_video_latest(session_id, {'foot_video2_path': rel2})
except Exception as e:
self.logger.error(f'更新foot_video2失败: {e}')
try:
shutil.rmtree(cam2_frames_dir, ignore_errors=True)
except Exception as e:
self.logger.error(f'删除camera2_frames失败: {e}')
self.recording_manager.transcode_jpeg_sequence_async(
name=f'{session_id}-camera2',
frames_dir=cam2_frames_dir,
output_mp4_path=out2,
fps=cam2_fps,
on_done=_done2
)
except Exception as e:
self.logger.error(f'停止/转码camera2录制失败: {e}')
return jsonify({'success': True,'msg': '停止录制成功'})
except Exception as e:
self.logger.error(f'停止检测失败: {e}', exc_info=True)

View File

@ -0,0 +1,64 @@
import os
from datetime import datetime, timedelta, timezone
import pytest
class DummyConfigManager:
def __init__(self, values):
self._values = values
def get_config_value(self, section, key, fallback=None):
return self._values.get((section, key), fallback)
def test_check_validity_accepts_candidate_machine_id(monkeypatch):
from devices.utils.license_manager import LicenseManager
lm = LicenseManager(config_manager=DummyConfigManager({}))
monkeypatch.setattr(lm, "get_machine_id_candidates", lambda: ["MID-PRIMARY", "MID-ALT"])
license_data = {
"product": "BodyBalanceEvaluation",
"license_id": "L1",
"license_type": "full",
"machine_id": "MID-ALT",
"issued_at": datetime.now(timezone.utc).isoformat(),
"expires_at": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat().replace("+00:00", "Z"),
"signature": "x",
"features": {"export": True},
}
ok, msg = lm.check_validity(license_data, machine_id="MID-PRIMARY", grace_days=0)
assert ok is True, msg
def test_resolve_license_paths_falls_back_to_persistent(monkeypatch, tmp_path):
from devices.utils.license_manager import LicenseManager
programdata = tmp_path / "programdata"
persistent_dir = programdata / "BodyCheck" / "license"
persistent_dir.mkdir(parents=True)
(persistent_dir / "license.json").write_text('{"a":1}', encoding="utf-8")
(persistent_dir / "license_public_key.pem").write_text("PUB", encoding="utf-8")
cfg_dir = tmp_path / "cfg"
cfg_license_path = str(cfg_dir / "license.json")
cfg_pub_path = str(cfg_dir / "license_public_key.pem")
cfg = DummyConfigManager(
{
("LICENSE", "path"): cfg_license_path,
("LICENSE", "public_key"): cfg_pub_path,
("LICENSE", "grace_days"): "3",
}
)
lm = LicenseManager(config_manager=cfg)
monkeypatch.setenv("PROGRAMDATA", str(programdata))
monkeypatch.setattr("platform.system", lambda: "Windows")
license_path, pub_path, grace_days = lm._resolve_license_paths()
assert license_path == str(persistent_dir / "license.json")
assert pub_path == str(persistent_dir / "license_public_key.pem")
assert grace_days == 3

View File

@ -3,7 +3,7 @@ const path = require('path');
const http = require('http');
const fs = require('fs');
const url = require('url');
const { spawn } = require('child_process');
const { spawn, exec, execSync } = require('child_process');
let mainWindow;
let localServer;
let backendProcess;
@ -11,6 +11,24 @@ let splashWindow;
// app.disableHardwareAcceleration();
app.disableDomainBlockingFor3DAPIs();
console.log('Electron version:', process.versions.electron);
const gotSingleInstanceLock = app.requestSingleInstanceLock();
if (!gotSingleInstanceLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.focus();
return;
}
if (app.isReady()) {
createWindow();
}
});
}
ipcMain.handle('generate-report-pdf', async (event, payload) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) throw new Error('窗口未找到');
@ -134,6 +152,21 @@ ipcMain.handle('generate-report-pdf', async (event, payload) => {
});
function startBackendService() {
if (backendProcess) {
console.log('Backend service already started (tracked).');
return;
}
try {
const tasklistOut = execSync('tasklist /FI "IMAGENAME eq BodyBalanceBackend.exe" /NH', { windowsHide: true }).toString();
if (tasklistOut && tasklistOut.toLowerCase().includes('bodybalancebackend.exe')) {
console.log('Backend service already running (tasklist). Skip spawning a new instance.');
return;
}
} catch (e) {
console.log('Backend process detection failed, continue to spawn:', e && e.message ? e.message : e);
}
// 在打包后的应用中使用process.resourcesPath获取resources目录
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
@ -199,7 +232,6 @@ function stopBackendService() {
// 强制杀死所有BodyBalanceBackend.exe进程
try {
const { exec } = require('child_process');
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
if (error) {
// 如果没有找到进程taskkill会返回错误这是正常的
@ -300,6 +332,11 @@ function createWindow() {
}
function startLocalServer(callback) {
if (localServer) {
console.log('Local server already started on http://localhost:3000');
callback();
return;
}
const staticPath = path.join(__dirname, '../dist/');
localServer = http.createServer((req, res) => {
@ -360,6 +397,7 @@ function startLocalServer(callback) {
// 应用事件处理
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁
// app.disableHardwareAcceleration();
if (gotSingleInstanceLock) {
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
@ -383,3 +421,4 @@ app.on('activate', () => {
app.on('before-quit', () => {
stopBackendService();
});
}

View File

@ -3,6 +3,7 @@
<Header />
<div class="displaycontainer">
<div class="displayleft" style="width: 550px;">
<img src="@/assets/detection/progress.png" alt="" style=" margin-left:10px;margin-right:15px">
<div style="
font-size: 18px;
@ -47,7 +48,7 @@
</div>
</div>
</div>
<div style="width:100%;height: calc(100% - 115px);" ref="contenGridRef">
<div style="width:100%;height: calc(100% - 131px);" ref="contenGridRef">
<!-- 主内容区域 -->
<el-row :gutter="15" style="padding: 10px;padding-top:0" >
<el-col :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
@ -192,7 +193,6 @@
</div>
</div>
<div class="body-footbottom-box" ref="pressureRef">
<div style="width: 100%;height: calc(100% - 57px);display: flex; align-items: center;justify-content: center;">
<div class="body-footbottom-left">
<div style="width:100%;height: 50px;"></div>
<div class="body-footbottom-leftbottom">
@ -208,12 +208,12 @@
{{ footPressure.left_rear }}%
</span>
</div>
<!-- <div class="body-footbottom-leftbox">
<div class="body-footbottom-leftbox">
<span class="currencytext1">左足总压力</span>
<span class="currencytext2">
{{ footPressure.left_total}}%
</span>
</div> -->
</div>
</div>
</div>
<div class="body-footbottom-center">
@ -229,10 +229,8 @@
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开</div>
</div>
<div class="xline"></div>
<div class="yline"></div>
<!-- <div v-if="(pressureStatus === '已连接' && footImgSrc)" class="xline"></div>
<div v-if="(pressureStatus === '已连接' && footImgSrc)" class="yline"></div> -->
<div v-if="(pressureStatus === '已连接' && footImgSrc)" class="xline"></div>
<div v-if="(pressureStatus === '已连接' && footImgSrc)" class="yline"></div>
</div>
</div>
<div class="body-footbottom-left">
@ -250,35 +248,19 @@
{{ footPressure.right_rear }}%
</span>
</div>
<!-- <div class="body-footbottom-leftbox">
<span class="currencytext1">右足总压力</span>
<span class="currencytext2">
{{ footPressure.right_total}}%
</span>
</div> -->
</div>
</div>
</div>
<div style="display: flex;justify-content: center; width: 100%;">
<div class="body-footbottom-leftbox" style="width:calc(22% + 2px)">
<span class="currencytext1">左足总压力</span>
<span class="currencytext2">
{{ footPressure.left_total}}%
</span>
</div>
<div class="body-footbottom-leftbox" style="width:calc(22% + 2px);margin-left: 20px">
<div class="body-footbottom-leftbox">
<span class="currencytext1">右足总压力</span>
<span class="currencytext2">
{{ footPressure.right_total}}%
</span>
</div>
</div>
</div>
</div>
</div>
</el-col>
<el-col v-if=" false || camera1Status === '已连接' && camera2Status === '已连接'"
<el-col v-if="camera1Status === '已连接' && camera2Status === '已连接'"
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px); position: relative;">
<div class="body-userinfo-box" :class="isExpand == true?'body-userinfo-expandbox':''">
<div class="body-title-display">
@ -448,9 +430,9 @@
</div>
</div>
</el-col>
<el-col v-if="true || camera1Status === '已连接' || camera2Status === '已连接'"
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px);position: relative;">
<div class="body-userinfo-box1" :class="isExpand == true?'body-userinfo-expandbox':''">
<el-col v-if="camera1Status === '已连接' || camera2Status === '已连接'"
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
<div class="body-userinfo-box1">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title4.png" alt="" style="margin-right: 8px;">
@ -479,116 +461,61 @@
</div>
</div>
<div class="body-userinfo-content-bottom0" v-if="isExpand == false">
<img src="@/assets/detection/userinfo.png" alt=""
class="userinfo-edit-img" style="cursor: pointer;"
@click="viewClick(true)">
<div class="userinfo-disyplaypadding1 ">
<div class="userinfo-text4 padding10">出生日期</div>
<div class="body-userinfo-content-bottom1">
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">出生日期</div>
<div class="userinfo-text5">
<span v-if="patientInfo && patientInfo.birth_date">
{{ formatDate(patientInfo.birth_date) }}
</span>
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">身高</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">身高</div>
<div class="userinfo-text5">
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">体重</div>
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">体重</div>
<div class="userinfo-text5">
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">鞋码</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">鞋码</div>
<div class="userinfo-text5">
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">电话</div>
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">电话</div>
<div class="userinfo-text5">
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">民族</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">民族</div>
<div class="userinfo-text5">
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">身份证号</div>
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">身份证号</div>
<div class="userinfo-text5">
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">职业</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">职业</div>
<div class="userinfo-text5">
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
</div>
</div>
<div class="body-userinfo-content-bottom2" v-if="isExpand == true">
<img src="@/assets/detection/userinfo.png" alt=""
class="userinfo-edit-img" style="cursor: pointer;"
@click="viewClick(false)">
<div class="userinfo-disyplaypadding1 ">
<div class="userinfo-text4 padding10">出生日期</div>
<div class="userinfo-text5">
<span v-if="patientInfo && patientInfo.birth_date">
{{ formatDate(patientInfo.birth_date) }}
</span>
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">身高</div>
<div class="userinfo-text5">
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">体重</div>
<div class="userinfo-text5">
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">鞋码</div>
<div class="userinfo-text5">
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">电话</div>
<div class="userinfo-text5">
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">民族</div>
<div class="userinfo-text5">
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">身份证号</div>
<div class="userinfo-text5">
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">职业</div>
<div class="userinfo-text5">
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
</div>
<div class="userinfo-disyplaypadding3">
<div class="userinfo-text4 padding10">居住地</div>
<div class="userinfo-disyplaypadding6">
<div class="userinfo-text4">居住地</div>
<div class="userinfo-text5">
{{ patientInfo.residence ==''||patientInfo.residence ==null ?'—':patientInfo.residence}}</div>
</div>
<div class="userinfo-disyplaypadding3">
<div class="userinfo-text4 padding10">邮箱</div>
<div class="userinfo-disyplaypadding6">
<div class="userinfo-text4">邮箱</div>
<div class="userinfo-text5">
{{ patientInfo.email ==''||patientInfo.email ==null ?'—':patientInfo.email}}</div>
</div>
@ -596,8 +523,7 @@
</div>
</div>
</div>
<div class="body-video-box1" style="position: absolute; top: 522px; width: calc(100% - 15px);">
<div class="body-video-box1">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title5.png" alt="" style="margin-right: 8px;">
@ -612,14 +538,14 @@
</div>
</div>
</div>
<div class="body-video-content" style="padding: 0;">
<div class="body-video-content">
<div v-show="camera1Status === '已连接'" class="body-video-imgbox3" ref="camera1Ref" :class="(camera1Status === '已连接' && camera1ImgSrc)?'':'noImageSvg-bg'">
<div v-if="(camera1Status === '已连接' && camera1ImgSrc)"
@click="isBig1 = true" class="big-img">
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera1Status === '已连接' && camera1ImgSrc)" :src="camera1ImgSrc" alt="camera1"
style="width: 100%; height: 100%;object-fit:contain;" />
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开</div>
@ -631,7 +557,7 @@
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera2Status === '已连接' && camera2ImgSrc)" :src="camera2ImgSrc" alt="camera2"
style="width: 100%; height: 100%;object-fit:contain;" />
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开</div>
@ -640,7 +566,7 @@
</div>
</div>
</el-col>
<el-col v-if="false || camera1Status !== '已连接' && camera2Status !== '已连接'" :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
<el-col v-if="camera1Status !== '已连接' && camera2Status !== '已连接'" :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
<div class="body-userinfo-box3">
<div class="body-title-display">
<div class="body-son-display">
@ -787,7 +713,6 @@
<div class="pop-up-tip-text" v-if="!isVideoOperation">本次检测未截图或录像操作不予存档记录</div>
<div class="pop-up-tip-text" v-if="isVideoOperation">本次检测未截图操作存档记录不可生成报告</div>
<div class="tipconfirmbutton-box">
<div class="tipclosebutton" @click="handleCancel">取消</div>
<el-button type="primary" class="tipconfirmbutton" @click="closeTipClick">确定</el-button>
</div>
</div>
@ -2709,23 +2634,9 @@ const isPhotoAlbum = ref(false)
function closePhotoAlbum(){
isPhotoAlbum.value = false
}
function closecreatbox(e,info){
if(e === '编辑'){
patientInfo.value.age = info.age
patientInfo.value.birth_date = info.birth_date
patientInfo.value.email = info.email
patientInfo.value.gender = info.gender
patientInfo.value.height = info.height
patientInfo.value.id = info.id
patientInfo.value.idcode = info.idcode
patientInfo.value.name = info.name
patientInfo.value.nationality = info.nationality
patientInfo.value.occupation = info.occupation
patientInfo.value.phone = info.phone
patientInfo.value.residence = info.residence
patientInfo.value.shoe_size = info.shoe_size
patientInfo.value.weight = info.weight
// loadPatientInfo()
function closecreatbox(e){
if(e === true){
loadPatientInfo()
}
isCloseCreat.value = false
}
@ -2767,7 +2678,7 @@ function viewClick(e){
.displaycontainer {
width: 100%;
height: 46px;
height: 62px;
display: flex;
align-items: center;
justify-content: space-between;
@ -2944,8 +2855,10 @@ function viewClick(e){
text-align: left;
}
.body-footbottom-box{
height: calc(100% - 50px);
padding-bottom: 15px;
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 70px);
}
.body-footbottom-left{
width: 28%;
@ -2958,8 +2871,8 @@ function viewClick(e){
.body-footbottom-leftbox{
min-width: 215px;
width: 80%;
min-height: 57px;
height: 57px;
min-height: 60px;
height: 20%;
background: inherit;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
@ -2969,8 +2882,8 @@ function viewClick(e){
padding: 0px 20px;
}
.body-footbottom-center{
width: 37%;
height: calc(100% - 0px);
width: 40%;
height: calc(100%);
}
.body-footbottom-topbox{
display: flex;
@ -3005,7 +2918,7 @@ function viewClick(e){
display: flex;
justify-content: center;
flex-wrap: wrap;
align-content:space-around;
align-content:space-between ;
}
.body-userinfo-box{
@ -3013,18 +2926,18 @@ function viewClick(e){
z-index: 10;
width: 100%;
height: 346px;
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #242E3D;
border-radius: 4px;
}
.body-userinfo-expandbox{
height: 680px !important;
height: 638px ;
}
.body-userinfo-box1{
position: relative;
z-index: 10;
width: 100%;
height: 524px ;
height: 534px ;
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #242E3D;
border-radius: 4px;
@ -3050,7 +2963,7 @@ function viewClick(e){
}
.body-video-box1{
width: 100%;
height: calc(100% - 534px - 4px) ;
height: calc(100% - 534px - 14px) ;
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #242E3D;
border-radius: 4px;
@ -3187,10 +3100,10 @@ function viewClick(e){
}
.userinfo-disyplaypadding1{
width: calc(64%);
padding-bottom: 20px;
padding-bottom: 15px;
}
.padding10{
padding-bottom: 10px;
padding-bottom: 5px;
}
.userinfo-disyplaypadding2{
width: calc(36%);
@ -3425,25 +3338,6 @@ function viewClick(e){
background:#14aaff;
border:1px solid #14aaff;
}
.tipclosebutton{
width: 80px;
height: 40px;
background-color: #597194;
border-radius: 4px;
color: rgba(255, 255, 255, 0.6);
font-weight: 400;
font-style: normal;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
cursor: pointer;
}
.tipclosebutton:hover{
background-color: #14aaff;
color: #fff;
}
.pop-up-tip-text{
width:100%;
font-weight: 400;