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 pressure_baudrate = 115200
[REMOTE] [REMOTE]
enable = False enable = True
port = COM6 port = COM6
baudrate = 115200 baudrate = 115200
timeout = 0.1 timeout = 0.1

View File

@ -830,7 +830,7 @@ class DatabaseManager:
cursor.execute(''' cursor.execute('''
SELECT * FROM detection_data SELECT * FROM detection_data
WHERE session_id = ? WHERE session_id = ?
ORDER BY timestamp ORDER BY timestamp desc
''', (session_id,)) ''', (session_id,))
data_rows = cursor.fetchall() data_rows = cursor.fetchall()
@ -849,7 +849,7 @@ class DatabaseManager:
cursor.execute(''' cursor.execute('''
SELECT * FROM detection_video SELECT * FROM detection_video
WHERE session_id = ? WHERE session_id = ?
ORDER BY timestamp ORDER BY timestamp desc
''', (session_id,)) ''', (session_id,))
video_rows = cursor.fetchall() video_rows = cursor.fetchall()
@ -1103,12 +1103,15 @@ class DatabaseManager:
cursor.execute(''' cursor.execute('''
INSERT INTO detection_video ( INSERT INTO detection_video (
id, session_id, screen_video, timestamp id, session_id, screen_video, body_video, foot_video1, foot_video2, timestamp
) VALUES (?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
video_id, video_id,
session_id, session_id,
video.get('screen_video_path'), video.get('screen_video_path'),
video.get('body_video_path'),
video.get('foot_video1_path'),
video.get('foot_video2_path'),
china_time china_time
)) ))
@ -1120,6 +1123,50 @@ class DatabaseManager:
logger.error(f'保存检测视频失败: {e}') logger.error(f'保存检测视频失败: {e}')
return False 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: def delete_detection_video(self, video_ids: Union[str, List[str]]) -> bool:
"""删除检测视频记录支持单个或多个ID""" """删除检测视频记录支持单个或多个ID"""
conn = self.get_connection() conn = self.get_connection()

View File

@ -14,6 +14,7 @@ from typing import Optional, Dict, Any
import logging import logging
import queue import queue
import gc import gc
import os
try: try:
from .base_device import BaseDevice 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 = {} self._property_cache = {}
@ -126,6 +139,130 @@ class CameraManager(BaseDevice):
self.logger.info(f"相机管理器初始化完成 - 设备索引: {self.device_index}") 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): def _set_property_optimized(self, prop, value):
""" """
优化的属性设置方法使用缓存避免重复设置 优化的属性设置方法使用缓存避免重复设置
@ -649,23 +786,6 @@ class CameraManager(BaseDevice):
# 更新心跳时间,防止连接监控线程判定为超时 # 更新心跳时间,防止连接监控线程判定为超时
self.update_heartbeat() 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) processed_frame = self._process_frame(frame)
@ -740,6 +860,7 @@ class CameraManager(BaseDevice):
# 转换为bytes再做base64减少中间numpy对象的长生命周期 # 转换为bytes再做base64减少中间numpy对象的长生命周期
frame_bytes = buffer.tobytes() frame_bytes = buffer.tobytes()
self._maybe_enqueue_recording(time.time(), frame_bytes)
frame_data = base64.b64encode(frame_bytes).decode('utf-8') frame_data = base64.b64encode(frame_bytes).decode('utf-8')
# 发送数据 # 发送数据
@ -986,6 +1107,21 @@ class CameraManager(BaseDevice):
except queue.Empty: except queue.Empty:
break 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 self.last_frame = None
super().cleanup() super().cleanup()

View File

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

View File

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

View File

@ -13,7 +13,8 @@ import signal
import base64 import base64
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional, Callable
import threading
try: try:
import pyautogui import pyautogui
@ -39,11 +40,38 @@ class RecordingManager:
# FFmpeg进程管理 # FFmpeg进程管理
self._ffmpeg_processes = {} self._ffmpeg_processes = {}
self._ffmpeg_meta = {} self._ffmpeg_meta = {}
self._transcode_threads = {}
# 默认参数 # 默认参数
self.screen_fps = 25 self.screen_fps = 25
self.screen_size = self._get_screen_size() 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): def _get_screen_size(self):
try: try:
import pyautogui import pyautogui
@ -82,20 +110,10 @@ class RecordingManager:
os.makedirs(base_path, exist_ok=True) os.makedirs(base_path, exist_ok=True)
screen_video_path = os.path.join(base_path, 'screen.mp4') screen_video_path = os.path.join(base_path, 'screen.mp4')
target_fps = fps or self.screen_fps target_fps = fps or self.screen_fps
ffmpeg_path = None ffmpeg_path = self._resolve_ffmpeg_path()
if self.config_manager: if not ffmpeg_path:
ffmpeg_path = ( result['message'] = '未配置有效的ffmpeg_path请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or return result
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:
result['message'] = '未配置有效的ffmpeg_path请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
return result
cmd = [ cmd = [
str(ffmpeg_path), str(ffmpeg_path),
@ -136,7 +154,7 @@ class RecordingManager:
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0) creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
) )
self._ffmpeg_processes['screen'] = proc 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['success'] = True
result['message'] = 'ffmpeg录制已启动' result['message'] = 'ffmpeg录制已启动'
result['database_updates'] = { result['database_updates'] = {
@ -151,6 +169,86 @@ class RecordingManager:
result['message'] = f'ffmpeg启动失败: {e}' result['message'] = f'ffmpeg启动失败: {e}'
return result 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]: def stop_recording_ffmpeg(self, session_id: str = None) -> Dict[str, Any]:
result = {'success': False, 'message': ''} result = {'success': False, 'message': ''}
try: try:

View File

@ -434,6 +434,8 @@ class ConfigManager:
self.set_config_value('DEVICES', 'imu_use_mock', str(config_data['use_mock'])) self.set_config_value('DEVICES', 'imu_use_mock', str(config_data['use_mock']))
if 'mac_address' in config_data: if 'mac_address' in config_data:
self.set_config_value('DEVICES', 'imu_mac_address', config_data['mac_address']) 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'] = { results['imu'] = {
'success': True, '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.hazmat.primitives.asymmetric import rsa, padding
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
import base64 import base64
import shutil
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -51,85 +52,345 @@ class LicenseManager:
def __init__(self, config_manager=None): def __init__(self, config_manager=None):
self.config_manager = config_manager self.config_manager = config_manager
self._machine_id = None self._machine_id = None
self._machine_id_candidates = None
self._license_cache = None self._license_cache = None
self._cache_timestamp = 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: def get_machine_id(self) -> str:
"""生成机器硬件指纹""" """生成机器硬件指纹"""
if self._machine_id: if self._machine_id:
return self._machine_id return self._machine_id
try: try:
core_info = [] candidates = self._build_machine_id_candidates()
aux_info = [] if not candidates:
try: raise RuntimeError("无法生成机器指纹")
if platform.system() == "Windows": self._machine_id_candidates = candidates
result = subprocess.run(['wmic', 'cpu', 'get', 'ProcessorId', '/value'], capture_output=True, text=True, timeout=10) self._machine_id = candidates[0]
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}"
logger.info(f"生成机器指纹: {self._machine_id}") logger.info(f"生成机器指纹: {self._machine_id}")
return self._machine_id return self._machine_id
except Exception as e: except Exception as e:
@ -137,8 +398,14 @@ class LicenseManager:
fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}" fallback_info = f"{platform.system()}-{platform.node()}-{platform.machine()}"
fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper() fallback_id = hashlib.md5(fallback_info.encode('utf-8')).hexdigest()[:12].upper()
self._machine_id = f"FB-{fallback_id}" self._machine_id = f"FB-{fallback_id}"
self._machine_id_candidates = [self._machine_id]
return 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]]: def load_license(self, license_path: str) -> Optional[Dict[str, Any]]:
"""加载授权文件""" """加载授权文件"""
try: try:
@ -231,7 +498,13 @@ class LicenseManager:
# 检查机器绑定 # 检查机器绑定
license_machine_id = license_data.get('machine_id', '') 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})" return False, f"授权文件与当前机器不匹配 (当前: {machine_id}, 授权: {license_machine_id})"
# 检查有效期 # 检查有效期
@ -269,18 +542,7 @@ class LicenseManager:
if not self.config_manager: if not self.config_manager:
return LicenseStatus(valid=False, message="配置管理器未初始化") return LicenseStatus(valid=False, message="配置管理器未初始化")
license_path = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json') license_path, public_key_path, grace_days = self._resolve_license_paths()
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)
# 获取机器指纹 # 获取机器指纹
machine_id = self.get_machine_id() machine_id = self.get_machine_id()
@ -309,6 +571,8 @@ class LicenseManager:
self._cache_timestamp = datetime.now().timestamp() self._cache_timestamp = datetime.now().timestamp()
return status 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) is_valid, message = self.check_validity(license_data, machine_id, grace_days)
@ -362,14 +626,7 @@ class LicenseManager:
if not self.config_manager: if not self.config_manager:
return False, "配置管理器未初始化" return False, "配置管理器未初始化"
# 解析公钥路径与宽限期 _, public_key_path, grace_days = self._resolve_license_paths()
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)
if not os.path.exists(license_path): if not os.path.exists(license_path):
return False, f"授权文件不存在: {license_path}" return False, f"授权文件不存在: {license_path}"

View File

@ -10,6 +10,7 @@ import sys
import json import json
import time import time
import threading import threading
import shutil
from datetime import datetime from datetime import datetime
from flask import Flask, jsonify from flask import Flask, jsonify
from flask import request as flask_request from flask import request as flask_request
@ -378,6 +379,10 @@ class AppServer:
'error': '授权管理器未初始化' 'error': '授权管理器未初始化'
}), 500 }), 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({ return jsonify({
'success': True, 'success': True,
'data': { 'data': {
@ -548,22 +553,14 @@ class AppServer:
if is_valid: if is_valid:
# 覆盖系统授权文件为上传的文件 # 覆盖系统授权文件为上传的文件
try: ok, result = self.license_manager.install_license_file(temp_path)
license_path_cfg = self.config_manager.get_config_value('LICENSE', 'path', 'data/license.json') if self.config_manager else 'data/license.json' if not ok:
# 解析目标路径为绝对路径 self.logger.error(f'保存授权文件失败: {result}')
if not os.path.isabs(license_path_cfg): return jsonify({'success': False, 'error': f'保存授权文件失败: {result}'}), 500
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
# 更新授权状态(强制刷新) # 更新授权状态(强制刷新)
self.license_status = self.license_manager.get_license_status(force_reload=True) self.license_status = self.license_manager.get_license_status(force_reload=True)
self.app.license_status = self.license_status
return jsonify({ return jsonify({
'success': True, 'success': True,
@ -631,8 +628,11 @@ class AppServer:
return jsonify({'success': False, 'error': '压缩包包含非法路径'}), 400 return jsonify({'success': False, 'error': '压缩包包含非法路径'}), 400
zip_ref.extractall(target_dir) 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.license_status = self.license_manager.get_license_status(force_reload=True)
self.app.license_status = self.license_status
return jsonify({ return jsonify({
'success': True, 'success': True,
@ -1449,6 +1449,30 @@ class AppServer:
# 使用新的ffmpeg录制方法 # 使用新的ffmpeg录制方法
recording_response = self.recording_manager.start_recording_ffmpeg(session_id, patient_id, screen_location) 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: if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
db_updates = recording_response['database_updates'] db_updates = recording_response['database_updates']
@ -1456,7 +1480,10 @@ class AppServer:
# 保存检测视频记录(映射到 detection_video 表字段) # 保存检测视频记录(映射到 detection_video 表字段)
video_paths = db_updates.get('video_paths', {}) video_paths = db_updates.get('video_paths', {})
video_record = { 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: try:
@ -1495,6 +1522,76 @@ class AppServer:
except Exception as rec_e: except Exception as rec_e:
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True) self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
raise 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': '停止录制成功'}) return jsonify({'success': True,'msg': '停止录制成功'})
except Exception as e: except Exception as e:
self.logger.error(f'停止检测失败: {e}', exc_info=True) 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 http = require('http');
const fs = require('fs'); const fs = require('fs');
const url = require('url'); const url = require('url');
const { spawn } = require('child_process'); const { spawn, exec, execSync } = require('child_process');
let mainWindow; let mainWindow;
let localServer; let localServer;
let backendProcess; let backendProcess;
@ -11,6 +11,24 @@ let splashWindow;
// app.disableHardwareAcceleration(); // app.disableHardwareAcceleration();
app.disableDomainBlockingFor3DAPIs(); app.disableDomainBlockingFor3DAPIs();
console.log('Electron version:', process.versions.electron); 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) => { ipcMain.handle('generate-report-pdf', async (event, payload) => {
const win = BrowserWindow.fromWebContents(event.sender); const win = BrowserWindow.fromWebContents(event.sender);
if (!win) throw new Error('窗口未找到'); if (!win) throw new Error('窗口未找到');
@ -134,6 +152,21 @@ ipcMain.handle('generate-report-pdf', async (event, payload) => {
}); });
function startBackendService() { 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目录 // 在打包后的应用中使用process.resourcesPath获取resources目录
const resourcesPath = process.resourcesPath || path.join(__dirname, '../..'); const resourcesPath = process.resourcesPath || path.join(__dirname, '../..');
const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe'); const backendPath = path.join(resourcesPath, 'backend/BodyBalanceBackend/BodyBalanceBackend.exe');
@ -199,7 +232,6 @@ function stopBackendService() {
// 强制杀死所有BodyBalanceBackend.exe进程 // 强制杀死所有BodyBalanceBackend.exe进程
try { try {
const { exec } = require('child_process');
exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => { exec('taskkill /f /im BodyBalanceBackend.exe', (error, stdout, stderr) => {
if (error) { if (error) {
// 如果没有找到进程taskkill会返回错误这是正常的 // 如果没有找到进程taskkill会返回错误这是正常的
@ -300,6 +332,11 @@ function createWindow() {
} }
function startLocalServer(callback) { function startLocalServer(callback) {
if (localServer) {
console.log('Local server already started on http://localhost:3000');
callback();
return;
}
const staticPath = path.join(__dirname, '../dist/'); const staticPath = path.join(__dirname, '../dist/');
localServer = http.createServer((req, res) => { localServer = http.createServer((req, res) => {
@ -360,26 +397,28 @@ function startLocalServer(callback) {
// 应用事件处理 // 应用事件处理
// 关闭硬件加速以规避 GPU 进程异常导致的闪烁 // 关闭硬件加速以规避 GPU 进程异常导致的闪烁
// app.disableHardwareAcceleration(); // app.disableHardwareAcceleration();
app.whenReady().then(createWindow); if (gotSingleInstanceLock) {
app.whenReady().then(createWindow);
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
if (localServer) { if (localServer) {
localServer.close(); localServer.close();
}
// 关闭后端服务
stopBackendService();
app.quit();
} }
// 关闭后端服务 });
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// 应用退出前清理资源
app.on('before-quit', () => {
stopBackendService(); stopBackendService();
app.quit(); });
} }
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// 应用退出前清理资源
app.on('before-quit', () => {
stopBackendService();
});

View File

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