Merge branch 'main' of http://121.37.111.42:3000/ThbTech/BodyBalanceEvaluation into main
This commit is contained in:
commit
439b248c27
@ -641,10 +641,10 @@ class DatabaseManager:
|
|||||||
creator_id,
|
creator_id,
|
||||||
settings.get('duration', 60),
|
settings.get('duration', 60),
|
||||||
json.dumps(settings),
|
json.dumps(settings),
|
||||||
'running',
|
'checking',
|
||||||
settings.get('diagnosis_info', ''),
|
'',
|
||||||
settings.get('treatment_info', ''),
|
'',
|
||||||
settings.get('suggestion_info', ''),
|
'',
|
||||||
china_time,
|
china_time,
|
||||||
china_time
|
china_time
|
||||||
))
|
))
|
||||||
|
@ -44,6 +44,12 @@ class BaseDevice(ABC):
|
|||||||
# 状态变化回调
|
# 状态变化回调
|
||||||
self._status_change_callbacks = []
|
self._status_change_callbacks = []
|
||||||
|
|
||||||
|
# 设备连接监控
|
||||||
|
self._connection_monitor_thread = None
|
||||||
|
self._monitor_stop_event = threading.Event()
|
||||||
|
self._connection_check_interval = config.get('connection_check_interval', 5.0) # 默认5秒检查一次
|
||||||
|
self._connection_timeout = config.get('connection_timeout', 30.0) # 默认30秒超时
|
||||||
|
|
||||||
# 设备状态信息
|
# 设备状态信息
|
||||||
self._device_info = {
|
self._device_info = {
|
||||||
'name': device_name,
|
'name': device_name,
|
||||||
@ -55,7 +61,7 @@ class BaseDevice(ABC):
|
|||||||
|
|
||||||
# 性能统计
|
# 性能统计
|
||||||
self._stats = {
|
self._stats = {
|
||||||
'frames_processed': 0,
|
'frames_processed': 0,
|
||||||
'errors_count': 0,
|
'errors_count': 0,
|
||||||
'start_time': None,
|
'start_time': None,
|
||||||
'last_frame_time': None
|
'last_frame_time': None
|
||||||
@ -137,6 +143,16 @@ class BaseDevice(ABC):
|
|||||||
bool: 重新加载是否成功
|
bool: 重新加载是否成功
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def check_hardware_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查设备硬件连接状态
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 设备是否物理连接
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def set_socketio(self, socketio):
|
def set_socketio(self, socketio):
|
||||||
"""
|
"""
|
||||||
@ -193,6 +209,12 @@ class BaseDevice(ABC):
|
|||||||
# 只有状态真正改变时才触发回调
|
# 只有状态真正改变时才触发回调
|
||||||
if old_status != is_connected:
|
if old_status != is_connected:
|
||||||
self._notify_status_change(is_connected)
|
self._notify_status_change(is_connected)
|
||||||
|
|
||||||
|
# 启动或停止连接监控
|
||||||
|
if is_connected and not self._connection_monitor_thread:
|
||||||
|
self._start_connection_monitor()
|
||||||
|
elif not is_connected and self._connection_monitor_thread:
|
||||||
|
self._stop_connection_monitor()
|
||||||
|
|
||||||
def emit_data(self, event: str, data: Any, namespace: Optional[str] = None):
|
def emit_data(self, event: str, data: Any, namespace: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
@ -305,6 +327,64 @@ class BaseDevice(ABC):
|
|||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._stats['start_time'] = None
|
self._stats['start_time'] = None
|
||||||
|
|
||||||
|
def _start_connection_monitor(self):
|
||||||
|
"""
|
||||||
|
启动连接监控线程
|
||||||
|
"""
|
||||||
|
if self._connection_monitor_thread and self._connection_monitor_thread.is_alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._monitor_stop_event.clear()
|
||||||
|
self._connection_monitor_thread = threading.Thread(
|
||||||
|
target=self._connection_monitor_worker,
|
||||||
|
name=f"{self.device_name}_connection_monitor",
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
self._connection_monitor_thread.start()
|
||||||
|
self.logger.info(f"设备 {self.device_name} 连接监控线程已启动")
|
||||||
|
|
||||||
|
def _stop_connection_monitor(self):
|
||||||
|
"""
|
||||||
|
停止连接监控线程
|
||||||
|
"""
|
||||||
|
if self._connection_monitor_thread:
|
||||||
|
self._monitor_stop_event.set()
|
||||||
|
if self._connection_monitor_thread.is_alive():
|
||||||
|
self._connection_monitor_thread.join(timeout=2.0)
|
||||||
|
self._connection_monitor_thread = None
|
||||||
|
self.logger.info(f"设备 {self.device_name} 连接监控线程已停止")
|
||||||
|
|
||||||
|
def _connection_monitor_worker(self):
|
||||||
|
"""
|
||||||
|
连接监控工作线程
|
||||||
|
"""
|
||||||
|
self.logger.info(f"设备 {self.device_name} 连接监控开始")
|
||||||
|
|
||||||
|
while not self._monitor_stop_event.is_set():
|
||||||
|
try:
|
||||||
|
# 检查硬件连接状态
|
||||||
|
hardware_connected = self.check_hardware_connection()
|
||||||
|
|
||||||
|
# 如果硬件断开但软件状态仍为连接,则更新状态
|
||||||
|
if not hardware_connected and self.is_connected:
|
||||||
|
self.logger.warning(f"检测到设备 {self.device_name} 硬件连接断开")
|
||||||
|
self.set_connected(False)
|
||||||
|
break # 硬件断开后停止监控
|
||||||
|
|
||||||
|
# 检查心跳超时
|
||||||
|
if self.is_connected and time.time() - self._last_heartbeat > self._connection_timeout:
|
||||||
|
self.logger.warning(f"设备 {self.device_name} 心跳超时,判定为断开连接")
|
||||||
|
self.set_connected(False)
|
||||||
|
break # 超时后停止监控
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"设备 {self.device_name} 连接监控异常: {e}")
|
||||||
|
|
||||||
|
# 等待下次检查
|
||||||
|
self._monitor_stop_event.wait(self._connection_check_interval)
|
||||||
|
|
||||||
|
self.logger.info(f"设备 {self.device_name} 连接监控结束")
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""
|
"""
|
||||||
@ -318,5 +398,11 @@ class BaseDevice(ABC):
|
|||||||
"""
|
"""
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
|
def _cleanup_monitoring(self):
|
||||||
|
"""
|
||||||
|
清理监控线程
|
||||||
|
"""
|
||||||
|
self._stop_connection_monitor()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__}(name='{self.device_name}', connected={self.is_connected}, streaming={self.is_streaming})>"
|
return f"<{self.__class__.__name__}(name='{self.device_name}', connected={self.is_connected}, streaming={self.is_streaming})>"
|
@ -612,40 +612,61 @@ class CameraManager(BaseDevice):
|
|||||||
self.logger.error(f"重新加载相机配置失败: {e}")
|
self.logger.error(f"重新加载相机配置失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_hardware_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查相机硬件连接状态
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 相机是否物理连接
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.cap and self.cap.isOpened():
|
||||||
|
# 尝试读取一帧来验证连接
|
||||||
|
ret, _ = self.cap.read()
|
||||||
|
return ret
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"检查相机硬件连接时发生异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""
|
"""
|
||||||
清理资源
|
清理资源
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.stop_streaming()
|
self.logger.info("开始清理相机资源")
|
||||||
|
|
||||||
|
# 清理监控线程
|
||||||
|
self._cleanup_monitoring()
|
||||||
|
|
||||||
|
# 停止流
|
||||||
|
if self.is_streaming:
|
||||||
|
self.stop_streaming()
|
||||||
|
|
||||||
|
# 断开连接
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
if self.cap:
|
|
||||||
try:
|
|
||||||
self.cap.release()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.cap = None
|
|
||||||
|
|
||||||
# 清理帧缓存
|
# 清理帧缓存
|
||||||
while not self.frame_cache.empty():
|
while not self.frame_cache.empty():
|
||||||
try:
|
try:
|
||||||
self.frame_cache.get_nowait()
|
self.frame_cache.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
self.last_frame = None
|
|
||||||
|
|
||||||
# 清理帧队列
|
# 清理全局帧队列
|
||||||
while not self.frame_queue.empty():
|
while not self.frame_queue.empty():
|
||||||
try:
|
try:
|
||||||
self.frame_queue.get_nowait()
|
self.frame_queue.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self.last_frame = None
|
||||||
|
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
self.logger.info("相机资源清理完成")
|
self.logger.info("相机资源清理完成")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"清理相机资源失败: {e}")
|
self.logger.error(f"清理相机资源时发生错误: {e}")
|
||||||
|
|
||||||
def _save_frame_to_cache(self, frame, frame_type='camera'):
|
def _save_frame_to_cache(self, frame, frame_type='camera'):
|
||||||
"""保存帧到全局缓存"""
|
"""保存帧到全局缓存"""
|
||||||
|
@ -830,11 +830,40 @@ class FemtoBoltManager(BaseDevice):
|
|||||||
self.logger.error(f"重新加载FemtoBolt配置失败: {e}")
|
self.logger.error(f"重新加载FemtoBolt配置失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_hardware_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查FemtoBolt设备硬件连接状态
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 设备是否物理连接
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.sdk_initialized or not self.device_handle:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 尝试获取设备状态来验证连接
|
||||||
|
if hasattr(self.femtobolt, 'device_get_capture'):
|
||||||
|
try:
|
||||||
|
# 尝试获取一帧数据来验证设备连接
|
||||||
|
capture = self.femtobolt.device_get_capture(self.device_handle, timeout_in_ms=1000)
|
||||||
|
if capture:
|
||||||
|
self.femtobolt.capture_release(capture)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"检查FemtoBolt硬件连接时发生异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""
|
"""
|
||||||
清理资源
|
清理资源
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 清理监控线程
|
||||||
|
self._cleanup_monitoring()
|
||||||
|
|
||||||
self.stop_streaming()
|
self.stop_streaming()
|
||||||
self._cleanup_device()
|
self._cleanup_device()
|
||||||
|
|
||||||
|
@ -892,11 +892,51 @@ class IMUManager(BaseDevice):
|
|||||||
self.logger.error(f"重新加载IMU配置失败: {e}")
|
self.logger.error(f"重新加载IMU配置失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_hardware_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查IMU硬件连接状态
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.imu_device:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 对于真实设备,检查串口连接状态
|
||||||
|
if hasattr(self.imu_device, 'ser') and self.imu_device.ser:
|
||||||
|
# 检查串口是否仍然打开
|
||||||
|
if not self.imu_device.ser.is_open:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 尝试读取数据来验证连接
|
||||||
|
try:
|
||||||
|
# 保存当前超时设置
|
||||||
|
original_timeout = self.imu_device.ser.timeout
|
||||||
|
self.imu_device.ser.timeout = 0.1 # 设置短超时
|
||||||
|
|
||||||
|
# 尝试读取少量数据
|
||||||
|
test_data = self.imu_device.ser.read(1)
|
||||||
|
|
||||||
|
# 恢复原始超时设置
|
||||||
|
self.imu_device.ser.timeout = original_timeout
|
||||||
|
|
||||||
|
return True # 如果没有异常,认为连接正常
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 对于模拟设备,总是返回True
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"检查IMU硬件连接时出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""
|
"""
|
||||||
清理资源
|
清理资源
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 停止连接监控
|
||||||
|
self._cleanup_monitoring()
|
||||||
|
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
# 清理缓冲区
|
# 清理缓冲区
|
||||||
|
@ -998,9 +998,46 @@ class PressureManager(BaseDevice):
|
|||||||
self.logger.error(f"重新加载压力板配置失败: {e}")
|
self.logger.error(f"重新加载压力板配置失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_hardware_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查压力板硬件连接状态
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 硬件连接是否正常
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.device:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 对于真实设备,检查DLL和设备句柄状态
|
||||||
|
if hasattr(self.device, 'dll') and hasattr(self.device, 'device_handle'):
|
||||||
|
if not self.device.dll or not self.device.device_handle:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查设备连接状态
|
||||||
|
if not self.device.is_connected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 尝试读取一次数据来验证连接
|
||||||
|
try:
|
||||||
|
test_data = self.device.read_data()
|
||||||
|
return test_data is not None and 'foot_pressure' in test_data
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 对于模拟设备,总是返回True
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(f"检查压力板硬件连接时出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
"""清理资源"""
|
"""清理资源"""
|
||||||
try:
|
try:
|
||||||
|
# 停止连接监控
|
||||||
|
self._cleanup_monitoring()
|
||||||
|
|
||||||
self.stop_streaming()
|
self.stop_streaming()
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self.logger.info("压力板设备资源清理完成")
|
self.logger.info("压力板设备资源清理完成")
|
||||||
|
@ -547,7 +547,7 @@ class RecordingManager:
|
|||||||
if self.current_session_id:
|
if self.current_session_id:
|
||||||
result['database_updates'] = {
|
result['database_updates'] = {
|
||||||
'session_id': self.current_session_id,
|
'session_id': self.current_session_id,
|
||||||
'status': 'checked'
|
'status': 'recorded'
|
||||||
}
|
}
|
||||||
self.logger.info(f'数据库更新信息已准备 - 会话ID: {self.current_session_id}')
|
self.logger.info(f'数据库更新信息已准备 - 会话ID: {self.current_session_id}')
|
||||||
|
|
||||||
@ -647,12 +647,7 @@ class RecordingManager:
|
|||||||
# 写入视频帧
|
# 写入视频帧
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
video_writer.write(frame)
|
video_writer.write(frame)
|
||||||
frame_count += 1
|
frame_count += 1
|
||||||
|
|
||||||
# # 每100帧记录一次进度
|
|
||||||
# if frame_count % 100 == 0:
|
|
||||||
# elapsed_recording_time = current_time - recording_start_time
|
|
||||||
# self.logger.debug(f'{recording_type}录制进度: {frame_count}帧, 已录制{elapsed_recording_time:.1f}秒, 目标帧率{target_fps}fps')
|
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f'{recording_type}获取帧失败,跳过此帧')
|
self.logger.warning(f'{recording_type}获取帧失败,跳过此帧')
|
||||||
|
|
||||||
@ -664,17 +659,8 @@ class RecordingManager:
|
|||||||
|
|
||||||
# 计算录制统计信息
|
# 计算录制统计信息
|
||||||
if self.global_recording_start_time:
|
if self.global_recording_start_time:
|
||||||
total_recording_time = time.time() - self.global_recording_start_time
|
total_recording_time = time.time() - self.global_recording_start_time
|
||||||
actual_fps = frame_count / total_recording_time if total_recording_time > 0 else 0
|
expected_frames = int(total_recording_time * target_fps)
|
||||||
expected_frames = int(total_recording_time * target_fps)
|
|
||||||
|
|
||||||
self.logger.info(f'{recording_type}录制线程结束统计:')
|
|
||||||
self.logger.info(f' 实际录制帧数: {frame_count}帧')
|
|
||||||
self.logger.info(f' 预期录制帧数: {expected_frames}帧')
|
|
||||||
self.logger.info(f' 目标帧率: {target_fps}fps')
|
|
||||||
self.logger.info(f' 实际平均帧率: {actual_fps:.2f}fps')
|
|
||||||
self.logger.info(f' 录制时长: {total_recording_time:.3f}秒')
|
|
||||||
|
|
||||||
if abs(frame_count - expected_frames) > target_fps * 0.1: # 如果帧数差异超过0.1秒的帧数
|
if abs(frame_count - expected_frames) > target_fps * 0.1: # 如果帧数差异超过0.1秒的帧数
|
||||||
self.logger.warning(f'{recording_type}帧数异常: 实际{frame_count}帧 vs 预期{expected_frames}帧,差异{frame_count - expected_frames}帧')
|
self.logger.warning(f'{recording_type}帧数异常: 实际{frame_count}帧 vs 预期{expected_frames}帧,差异{frame_count - expected_frames}帧')
|
||||||
else:
|
else:
|
||||||
|
127
backend/main.py
127
backend/main.py
@ -977,47 +977,15 @@ class AppServer:
|
|||||||
|
|
||||||
data = flask_request.get_json()
|
data = flask_request.get_json()
|
||||||
patient_id = data.get('patient_id')
|
patient_id = data.get('patient_id')
|
||||||
creator_id = data.get('creator_id')
|
creator_id = data.get('creator_id')
|
||||||
screen_location = data.get('screen_location') # [0,0,1920,1080]
|
|
||||||
camera_location = data.get('camera_location') # [0,0,640,480]
|
|
||||||
femtobolt_location = data.get('femtobolt_location') # [0,0,640,480]
|
|
||||||
|
|
||||||
|
|
||||||
if not patient_id or not creator_id:
|
if not patient_id or not creator_id:
|
||||||
return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400
|
return jsonify({'success': False, 'error': '缺少患者ID或创建人ID'}), 400
|
||||||
|
|
||||||
# 调用create_detection_session方法,settings传空字典
|
# 调用create_detection_session方法,settings传空字典
|
||||||
session_id = self.db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id)
|
session_id = self.db_manager.create_detection_session(patient_id, settings={}, creator_id=creator_id)
|
||||||
|
return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time})
|
||||||
# 开始同步录制
|
|
||||||
recording_response = None
|
|
||||||
try:
|
|
||||||
recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera_location,femtobolt_location)
|
|
||||||
|
|
||||||
# 处理录制管理器返回的数据库更新信息
|
|
||||||
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
|
|
||||||
db_updates = recording_response['database_updates']
|
|
||||||
try:
|
|
||||||
# 更新会话状态
|
|
||||||
if not self.db_manager.update_session_status(db_updates['session_id'], db_updates['status']):
|
|
||||||
self.logger.error(f'更新会话状态失败 - 会话ID: {db_updates["session_id"]}, 状态: {db_updates["status"]}')
|
|
||||||
|
|
||||||
# 更新视频文件路径
|
|
||||||
video_paths = db_updates['video_paths']
|
|
||||||
self.db_manager.update_session_normal_video_path(db_updates['session_id'], video_paths['normal_video_path'])
|
|
||||||
self.db_manager.update_session_screen_video_path(db_updates['session_id'], video_paths['screen_video_path'])
|
|
||||||
self.db_manager.update_session_femtobolt_video_path(db_updates['session_id'], video_paths['femtobolt_video_path'])
|
|
||||||
|
|
||||||
self.logger.info(f'数据库更新成功 - 会话ID: {db_updates["session_id"]}')
|
|
||||||
except Exception as db_error:
|
|
||||||
self.logger.error(f'处理数据库更新失败: {db_error}')
|
|
||||||
|
|
||||||
except Exception as rec_e:
|
|
||||||
self.logger.error(f'开始同步录制失败: {rec_e}')
|
|
||||||
|
|
||||||
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time, 'recording': recording_response})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f'开始检测失败: {e}')
|
self.logger.error(f'开始检测失败: {e}')
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
@ -1049,6 +1017,89 @@ class AppServer:
|
|||||||
except Exception as duration_error:
|
except Exception as duration_error:
|
||||||
self.logger.error(f'更新会话持续时间失败: {duration_error}')
|
self.logger.error(f'更新会话持续时间失败: {duration_error}')
|
||||||
|
|
||||||
|
success = self.db_manager.update_session_status(session_id, 'completed')
|
||||||
|
if success:
|
||||||
|
self.logger.info(f'检测会话已停止 - 会话ID: {session_id}')
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': '检测已停止'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.logger.error('停止检测失败,更新会话状态失败')
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '停止检测失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@self.app.route('/api/detection/<session_id>/start_record', methods=['POST'])
|
||||||
|
def start_record(session_id):
|
||||||
|
"""开始视频录制"""
|
||||||
|
try:
|
||||||
|
if not self.db_manager or not self.device_coordinator:
|
||||||
|
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
|
||||||
|
|
||||||
|
data = flask_request.get_json()
|
||||||
|
patient_id = data.get('patient_id')
|
||||||
|
screen_location = data.get('screen_location') # [0,0,1920,1080]
|
||||||
|
camera_location = data.get('camera_location') # [0,0,640,480]
|
||||||
|
femtobolt_location = data.get('femtobolt_location') # [0,0,640,480]
|
||||||
|
|
||||||
|
|
||||||
|
if not patient_id:
|
||||||
|
return jsonify({'success': False, 'error': '缺少患者ID'}), 400
|
||||||
|
|
||||||
|
# 开始视频录制
|
||||||
|
recording_response = None
|
||||||
|
try:
|
||||||
|
recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera_location,femtobolt_location)
|
||||||
|
|
||||||
|
# 处理录制管理器返回的数据库更新信息
|
||||||
|
if recording_response and recording_response.get('success') and 'database_updates' in recording_response:
|
||||||
|
db_updates = recording_response['database_updates']
|
||||||
|
try:
|
||||||
|
# 更新会话状态
|
||||||
|
if not self.db_manager.update_session_status(db_updates['session_id'], db_updates['status']):
|
||||||
|
self.logger.error(f'更新会话状态失败 - 会话ID: {db_updates["session_id"]}, 状态: {db_updates["status"]}')
|
||||||
|
|
||||||
|
# 更新视频文件路径
|
||||||
|
video_paths = db_updates['video_paths']
|
||||||
|
self.db_manager.update_session_normal_video_path(db_updates['session_id'], video_paths['normal_video_path'])
|
||||||
|
self.db_manager.update_session_screen_video_path(db_updates['session_id'], video_paths['screen_video_path'])
|
||||||
|
self.db_manager.update_session_femtobolt_video_path(db_updates['session_id'], video_paths['femtobolt_video_path'])
|
||||||
|
|
||||||
|
self.logger.info(f'数据库更新成功 - 会话ID: {db_updates["session_id"]}')
|
||||||
|
except Exception as db_error:
|
||||||
|
self.logger.error(f'处理数据库更新失败: {db_error}')
|
||||||
|
|
||||||
|
except Exception as rec_e:
|
||||||
|
self.logger.error(f'开始同步录制失败: {rec_e}')
|
||||||
|
|
||||||
|
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'session_id': session_id, 'detectionStartTime': start_time, 'recording': recording_response})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'开始录制失败: {e}')
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@self.app.route('/api/detection/<session_id>/stop_record', methods=['POST'])
|
||||||
|
def stop_record(session_id):
|
||||||
|
"""停止视频录制"""
|
||||||
|
try:
|
||||||
|
if not self.db_manager or not self.device_coordinator:
|
||||||
|
self.logger.error('数据库管理器或设备管理器未初始化')
|
||||||
|
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
|
||||||
|
|
||||||
|
if not session_id:
|
||||||
|
self.logger.error('缺少会话ID')
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '缺少会话ID'
|
||||||
|
}), 400
|
||||||
|
|
||||||
# 停止同步录制,传递视频数据
|
# 停止同步录制,传递视频数据
|
||||||
try:
|
try:
|
||||||
restrt = self.recording_manager.stop_recording(session_id)
|
restrt = self.recording_manager.stop_recording(session_id)
|
||||||
@ -1066,12 +1117,12 @@ class AppServer:
|
|||||||
success = False
|
success = False
|
||||||
else:
|
else:
|
||||||
# 如果录制管理器没有返回数据库更新信息,则手动更新
|
# 如果录制管理器没有返回数据库更新信息,则手动更新
|
||||||
success = self.db_manager.update_session_status(session_id, 'completed')
|
success = self.db_manager.update_session_status(session_id, 'recorded')
|
||||||
|
|
||||||
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)
|
||||||
# 即使录制停止失败,也尝试更新数据库状态
|
# 即使录制停止失败,也尝试更新数据库状态
|
||||||
success = self.db_manager.update_session_status(session_id, 'completed')
|
success = self.db_manager.update_session_status(session_id, 'recorded')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
@ -1090,7 +1141,7 @@ class AppServer:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
self.logger.error(f'停止检测失败: {e}', exc_info=True)
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
@self.app.route('/api/detection/<session_id>/status', methods=['GET'])
|
@self.app.route('/api/detection/<session_id>/status', methods=['GET'])
|
||||||
def get_detection_status(session_id):
|
def get_detection_status(session_id):
|
||||||
"""获取检测状态"""
|
"""获取检测状态"""
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<!-- <el-icon class="back-icon" @click="handleBack"><ArrowLeft /></el-icon> -->
|
<!-- <el-icon class="back-icon" @click="handleBack"><ArrowLeft /></el-icon> -->
|
||||||
<span class="page-title">实时检测</span>
|
<span class="page-title">实时检测</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="padding-left: 10px;">检测中</div>
|
||||||
<img src="@/assets/sz.png" alt="" title="编辑相机参数" v-if="isConnected == true"
|
<img src="@/assets/sz.png" alt="" title="编辑相机参数" v-if="isConnected == true"
|
||||||
style="margin-left: 20px;cursor: pointer; width: 24px;height: 24px;"
|
style="margin-left: 20px;cursor: pointer; width: 24px;height: 24px;"
|
||||||
@click="cameraUpdate">
|
@click="cameraUpdate">
|
||||||
@ -41,12 +42,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="top-bar-right">
|
<div class="top-bar-right">
|
||||||
<el-icon class="top-icon">
|
<el-icon class="top-icon" @click="routerClick">
|
||||||
<Clock />
|
<Clock />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<el-icon class="top-icon">
|
<!-- <el-icon class="top-icon">
|
||||||
<Grid />
|
<Grid />
|
||||||
</el-icon>
|
</el-icon> -->
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -61,10 +62,11 @@
|
|||||||
身体姿态
|
身体姿态
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :style="{ color: femtoboltStatus == '已连接' ? '#00CC33' : '#808080' }" style="font-size: 14px;">
|
<div :style="{ color: femtoboltStatus == '已连接' ? '#00CC33' : '#808080' }"
|
||||||
|
style="font-size: 14px;">
|
||||||
{{ femtoboltStatus }}</div>
|
{{ femtoboltStatus }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="wholeBodyRef" style="display: flex;justify-content: center;height: 100%;padding-top: 0px;">
|
<div ref="wholeBodyRef" style="display: flex;justify-content: center;height:calc( 100% - 40px);padding-top: 0px;">
|
||||||
<!-- 使用深度相机视频流替换静态图片 -->
|
<!-- 使用深度相机视频流替换静态图片 -->
|
||||||
<img :src="(femtoboltStatus === '已连接' && depthCameraImgSrc) ? depthCameraImgSrc : noImageSvg" alt="深度相机视频流"
|
<img :src="(femtoboltStatus === '已连接' && depthCameraImgSrc) ? depthCameraImgSrc : noImageSvg" alt="深度相机视频流"
|
||||||
style="width: 100%;height: calc(100% - 40px);object-fit:contain;background:#323232;">
|
style="width: 100%;height: calc(100% - 40px);object-fit:contain;background:#323232;">
|
||||||
@ -75,7 +77,7 @@
|
|||||||
align-content: space-between;
|
align-content: space-between;
|
||||||
">
|
">
|
||||||
|
|
||||||
<div class="module-card" style=" height:50%;min-height: 425px;margin-bottom: 1px;">
|
<div class="module-card" style=" height:calc( 45%);margin-bottom: 1px;">
|
||||||
<!-- 头部姿态模块 -->
|
<!-- 头部姿态模块 -->
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="module-header">
|
<div class="module-header">
|
||||||
@ -108,66 +110,54 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 仪表盘区域 -->
|
<!-- 仪表盘区域 -->
|
||||||
<div style="display: flex;justify-content: space-between;padding: 0px 10px;padding-top: 10px;">
|
<div style="height:calc(100% - 40px) ; display: flex;justify-content: space-between;align-items: center; padding:10px;">
|
||||||
<div style="width: 33%;position: relative;">
|
<div style="width: 33%;position: relative;">
|
||||||
<div class="chart-title">旋转角</div>
|
<div class="chart-title">旋转角</div>
|
||||||
<div class="chart-titles">{{ headlist.rotation }}</div>
|
|
||||||
<div id="rotationChartId" style="width: 100%;height: 140px;"></div>
|
<div id="rotationChartId" style="width: 100%;height: 140px;"></div>
|
||||||
<div class="gauge-group-box">
|
<div class="rotation-titles">{{ headlist.rotation }}°</div>
|
||||||
<div class="gauge-group-box-text1">左:<span class="gauge-group-box-text2">{{
|
<div class="gauge-group-box" style="justify-content: center;">
|
||||||
|
<div class="gauge-group-box-text1">左最大:<span class="gauge-group-box-text2">{{
|
||||||
headPoseMaxValues.rotationLeftMax.toFixed(1) }}°</span></div>
|
headPoseMaxValues.rotationLeftMax.toFixed(1) }}°</span></div>
|
||||||
<div class="gauge-group-box-text1" style="margin-left: 20px;">右:<span class="gauge-group-box-text2">{{
|
|
||||||
headPoseMaxValues.rotationRightMax.toFixed(1) }}°</span></div>
|
</div>
|
||||||
|
<div class="gauge-group-box" style="justify-content: center;">
|
||||||
|
<div class="gauge-group-box-text1">左最大:<span class="gauge-group-box-text2">{{
|
||||||
|
headPoseMaxValues.rotationLeftMax.toFixed(1) }}°</span></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 33%;position: relative;">
|
<div style="width: 33%;position: relative;">
|
||||||
<div class="chart-title">倾斜角</div>
|
<div class="chart-title">倾斜角</div>
|
||||||
<div class="chart-titles">{{ headlist.tilt }}</div>
|
|
||||||
<div id="tiltChartId" style="width: 100%;height: 140px;"></div>
|
<div id="tiltChartId" style="width: 100%;height: 140px;"></div>
|
||||||
<div class="gauge-group-box">
|
<div class="rotation-titles">{{ headlist.tilt }}°</div>
|
||||||
<div class="gauge-group-box-text1">左:<span class="gauge-group-box-text2">{{
|
<div class="gauge-group-box" style="justify-content: center;">
|
||||||
|
<div class="gauge-group-box-text1">左最大:<span class="gauge-group-box-text2">{{
|
||||||
headPoseMaxValues.tiltLeftMax.toFixed(1) }}°</span></div>
|
headPoseMaxValues.tiltLeftMax.toFixed(1) }}°</span></div>
|
||||||
<div class="gauge-group-box-text1" style="margin-left: 20px;">右:<span class="gauge-group-box-text2">{{
|
|
||||||
|
</div>
|
||||||
|
<div class="gauge-group-box" style="justify-content: center;">
|
||||||
|
<div class="gauge-group-box-text1" style="">右最大:<span class="gauge-group-box-text2">{{
|
||||||
headPoseMaxValues.tiltRightMax.toFixed(1) }}°</span></div>
|
headPoseMaxValues.tiltRightMax.toFixed(1) }}°</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 33%;position: relative;">
|
<div style="width: 33%;position: relative;">
|
||||||
<div class="chart-title">俯仰角</div>
|
<div class="chart-title">俯仰角</div>
|
||||||
<div class="chart-titles">{{ headlist.pitch }}</div>
|
|
||||||
<div id="pitchChartId" style="width: 100%;height: 140px;"></div>
|
<div id="pitchChartId" style="width: 100%;height: 140px;"></div>
|
||||||
<div class="gauge-group-box">
|
<div class="rotation-titles">{{ headlist.pitch }}°</div>
|
||||||
<div class="gauge-group-box-text1">俯:<span class="gauge-group-box-text2">{{
|
<div class="gauge-group-box" style="justify-content: center;">
|
||||||
|
<div class="gauge-group-box-text1">俯最大:<span class="gauge-group-box-text2">{{
|
||||||
|
headPoseMaxValues.pitchDownMax.toFixed(1) }}°</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="gauge-group-box" style="justify-content: center;">
|
||||||
|
<div class="gauge-group-box-text1">俯最大:<span class="gauge-group-box-text2">{{
|
||||||
headPoseMaxValues.pitchDownMax.toFixed(1) }}°</span></div>
|
headPoseMaxValues.pitchDownMax.toFixed(1) }}°</span></div>
|
||||||
<div class="gauge-group-box-text1" style="margin-left: 20px;">仰:<span class="gauge-group-box-text2">{{
|
|
||||||
headPoseMaxValues.pitchUpMax.toFixed(1) }}°</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-box">
|
|
||||||
<div class="icon-box-mark"></div>
|
|
||||||
<div class="icon-box-text">历史数据</div>
|
|
||||||
</div>
|
|
||||||
<!-- 历史数据表格 -->
|
|
||||||
<div style="display: flex;justify-content: center;padding: 0px 10px;margin-top: 5px;height: 100%;">
|
|
||||||
<el-table :data="historyData" border style="width: 100%;overflow: auto;height: calc(100% - 280px);">
|
|
||||||
<el-table-column prop="id" label="ID" align="center" width="60" />
|
|
||||||
<el-table-column label="最大旋转角" align="center">
|
|
||||||
<el-table-column prop="rotLeft" label="左" min-width="60" align="center" />
|
|
||||||
<el-table-column prop="rotRight" label="右" min-width="60" align="center" />
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最大倾斜角" align="center">
|
|
||||||
<el-table-column prop="tiltLeft" label="左" min-width="60" align="center" />
|
|
||||||
<el-table-column prop="tiltRight" label="右" min-width="60" align="center" />
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最大仰视角" align="center">
|
|
||||||
<el-table-column prop="pitchDown" label="下" min-width="60" align="center" />
|
|
||||||
<el-table-column prop="pitchUp" label="上" min-width="60" align="center" />
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 足部压力模块 -->
|
<!-- 足部压力模块 -->
|
||||||
<div class="module-card" style=" height:50%;">
|
<div class="module-card" style=" height:calc( 55% - 1px);overflow: hidden;">
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="module-header">
|
<div class="module-header">
|
||||||
<div class="module-title">
|
<div class="module-title">
|
||||||
@ -180,68 +170,75 @@
|
|||||||
pressureStatus }}</div>
|
pressureStatus }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="foot-container">
|
<div style="
|
||||||
<div class="foot-container-left" style="font-size: 18px;">
|
height: calc(100% - 40px);
|
||||||
<div style="width: 190px;">
|
display: flex;
|
||||||
<span>左前足</span>
|
align-items: center;
|
||||||
<span class="foot-container-paddingcolor">{{ footPressure.left_front
|
justify-content: center;">
|
||||||
}}%</span>
|
<div class="foot-container" style="height:calc(100%) ">
|
||||||
</div>
|
<div class="foot-container-left" style="font-size: 18px;text-align: right;">
|
||||||
<div class="foot-container-margintop" style="width: 190px;">
|
<div style="width: 190px;">
|
||||||
<span>左后足</span>
|
<span>左前足</span>
|
||||||
<span class="foot-container-paddingcolor">{{ footPressure.left_rear }}%</span>
|
<span class="foot-container-paddingcolor">{{ footPressure.left_front
|
||||||
</div>
|
}}%</span>
|
||||||
</div>
|
|
||||||
<div class="foot-container-content">
|
|
||||||
<div style="display: flex;justify-content: center;margin-bottom: 8px;font-size: 20px;">
|
|
||||||
<div>
|
|
||||||
<span>左足</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="foot-container-marginleft">
|
<div class="foot-container-margintop" style="width: 190px;">
|
||||||
<span>右足</span>
|
<span>左后足</span>
|
||||||
|
<span class="foot-container-paddingcolor">{{ footPressure.left_rear }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="position: relative;width: 300px;height: 300px;">
|
<div class="foot-container-content">
|
||||||
<img :src="(pressureStatus === '已连接' && footImgSrc) ? footImgSrc : noImageSvg" style="width: 300px;height: 300px;" alt="">
|
<div style="display: flex;justify-content: center;margin-bottom: 8px;font-size: 20px;">
|
||||||
<div class="xline"></div>
|
<div>
|
||||||
<div class="yline"></div>
|
<span>左足</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="foot-container-marginleft">
|
||||||
style="display: flex;justify-content: center;margin-top: 8px;font-size: 18px;width: 470px;margin-left: -85px;">
|
<span>右足</span>
|
||||||
<div style="width: 215px;display: flex;align-items: center;">
|
</div>
|
||||||
<div style="width:95px;">左足总压力</div>
|
|
||||||
<div style="width:130px;" class="foot-container-paddingcolor">{{ footPressure.left_total
|
|
||||||
}}%</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="foot-container-marginleft" style="width: 215px;display: flex;align-items: center;">
|
<div style="position: relative;width: 100%;height:calc(100% - 80px) ;">
|
||||||
<div style="width:95px;">右足总压力</div>
|
<img :src="(pressureStatus === '已连接' && footImgSrc) ? footImgSrc : noImageSvg" style="width: 100%;height: 100%;" alt="">
|
||||||
<div style="width:130px;" class="foot-container-paddingcolor">{{ footPressure.right_total
|
<div class="xline"></div>
|
||||||
}}%</div>
|
<div class="yline"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="display: flex;justify-content: center;font-size: 18px;width: 100%;">
|
||||||
|
<div style="width: 215px;display: flex;align-items: center;">
|
||||||
|
<div style="width:95px;">左足总压力</div>
|
||||||
|
<div style="width:130px;" class="foot-container-paddingcolor">{{ footPressure.left_total
|
||||||
|
}}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="foot-container-marginleft" style="width: 215px;display: flex;align-items: center;">
|
||||||
|
<div style="width:95px;">右足总压力</div>
|
||||||
|
<div style="width:130px;" class="foot-container-paddingcolor">{{ footPressure.right_total
|
||||||
|
}}%</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="foot-container-right" style="font-size: 18px;">
|
||||||
<div class="foot-container-right" style="font-size: 18px;">
|
<div style="width: 190px;">
|
||||||
<div style="width: 190px;">
|
<span>右前足</span>
|
||||||
<span>右前足</span>
|
<span class="foot-container-paddingcolor">{{ footPressure.right_front
|
||||||
<span class="foot-container-paddingcolor">{{ footPressure.right_front
|
}}%</span>
|
||||||
}}%</span>
|
</div>
|
||||||
</div>
|
<div class="foot-container-margintop" style="width: 190px;">
|
||||||
<div class="foot-container-margintop" style="width: 190px;">
|
<span>右后足</span>
|
||||||
<span>右后足</span>
|
<span class="foot-container-paddingcolor">{{ footPressure.right_rear
|
||||||
<span class="foot-container-paddingcolor">{{ footPressure.right_rear
|
}}%</span>
|
||||||
}}%</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="body-posture" style="width: 30%;display: flex;
|
<div class="body-posture" style="width: 30%;display: flex;height:calc(100vh - 100px);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-content: space-between;
|
align-content: space-between;
|
||||||
">
|
">
|
||||||
<!-- 基础信息模块 -->
|
<!-- 基础信息模块 -->
|
||||||
<div class="module-card" style="padding-bottom: 40px; height: 50%;min-height: 425px;margin-bottom: 1px;">
|
<div class="module-card" style="height:calc( 45%);margin-bottom: 1px;">
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="module-header">
|
<div class="module-header">
|
||||||
<div class="module-title">
|
<div class="module-title">
|
||||||
@ -322,7 +319,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 视频模块 -->
|
<!-- 视频模块 -->
|
||||||
<div class="module-card" style="height: 50%;">
|
<div class="module-card" style="height: calc( 55% - 1px)">
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="module-header">
|
<div class="module-header">
|
||||||
<div class="module-title">
|
<div class="module-title">
|
||||||
@ -334,7 +331,7 @@
|
|||||||
<div :style="{ color: cameraStatus == '已连接' ? '#00CC33' : '#808080' }" style="font-size: 14px;">{{ cameraStatus }}</div>
|
<div :style="{ color: cameraStatus == '已连接' ? '#00CC33' : '#808080' }" style="font-size: 14px;">{{ cameraStatus }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="videoImgRef" style="width: 100%;height: calc(100% - 40px)">
|
<div ref="videoImgRef" style="width: 100%;height: calc(100% - 47px)">
|
||||||
<img :src="(cameraStatus === '已连接' && rtspImgSrc) ? rtspImgSrc : noImageSvg" alt=""
|
<img :src="(cameraStatus === '已连接' && rtspImgSrc) ? rtspImgSrc : noImageSvg" alt=""
|
||||||
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" />
|
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" />
|
||||||
</div>
|
</div>
|
||||||
@ -586,19 +583,13 @@ const screenshotLoading = ref(false)
|
|||||||
const dataCollectionLoading = ref(false)
|
const dataCollectionLoading = ref(false)
|
||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
const cameraDialogVisible =ref(false) // 设置相机参数弹框
|
const cameraDialogVisible =ref(false) // 设置相机参数弹框
|
||||||
|
|
||||||
|
|
||||||
const contenGridRef =ref(null) // 实时检查整体box
|
const contenGridRef =ref(null) // 实时检查整体box
|
||||||
|
|
||||||
const wholeBodyRef = ref(null) // 身体姿态ref
|
const wholeBodyRef = ref(null) // 身体姿态ref
|
||||||
const videoImgRef =ref(null) // 视频流图片ref
|
const videoImgRef =ref(null) // 视频流图片ref
|
||||||
|
|
||||||
// 录像相关变量
|
// 录像相关变量
|
||||||
let mediaRecorder = null
|
let mediaRecorder = null
|
||||||
let recordedChunks = []
|
let recordedChunks = []
|
||||||
let recordingStream = null
|
let recordingStream = null
|
||||||
|
|
||||||
|
|
||||||
// 患者信息(从页面获取或通过API获取)
|
// 患者信息(从页面获取或通过API获取)
|
||||||
const patientInfo = ref({
|
const patientInfo = ref({
|
||||||
id: '',
|
id: '',
|
||||||
@ -693,11 +684,7 @@ const calculatedAge = ref(null)
|
|||||||
//修改
|
//修改
|
||||||
|
|
||||||
// 模拟历史数据
|
// 模拟历史数据
|
||||||
const historyData = ref([
|
const historyData = ref([])
|
||||||
// { id: 3, rotLeft: '-55.2°', rotRight: '54.2°', tiltLeft: '-17.7°', tiltRight: '18.2°', pitchDown: '-20.2°', pitchUp: '10.5°' },
|
|
||||||
// { id: 2, rotLeft: '-55.8°', rotRight: '56.2°', tiltLeft: '-17.5°', tiltRight: '17.9°', pitchDown: '-21.2°', pitchUp: '12.1°' },
|
|
||||||
// { id: 1, rotLeft: '-56.1°', rotRight: '55.7°', tiltLeft: '-17.5°', tiltRight: '18.5°', pitchDown: '-22.2°', pitchUp: '11.5°' }
|
|
||||||
])
|
|
||||||
const chartoption = ref({
|
const chartoption = ref({
|
||||||
backgroundColor: '#242424',
|
backgroundColor: '#242424',
|
||||||
grid: { top: 0, right: 0, bottom: 0, left: 0 },
|
grid: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||||
@ -825,7 +812,7 @@ const startTimer = () => {
|
|||||||
if (seconds.value >= 60) {
|
if (seconds.value >= 60) {
|
||||||
console.log('⏰ 检测时长超过10分钟,自动停止检测');
|
console.log('⏰ 检测时长超过10分钟,自动停止检测');
|
||||||
ElMessage.warning('检测时长已达到10分钟,自动停止检测');
|
ElMessage.warning('检测时长已达到10分钟,自动停止检测');
|
||||||
stopDetection();
|
stopRecord()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1683,28 +1670,23 @@ async function handleStartStop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isStart.value) {
|
if (isStart.value) {
|
||||||
// 停止检测
|
// 停止录制视频
|
||||||
await stopDetection()
|
await stopRecord()
|
||||||
} else {
|
} else {
|
||||||
patientInfo.value.sessionId = null
|
patientInfo.value.sessionId = null
|
||||||
// 开始检测
|
// 开始录制视频
|
||||||
await startDetection()
|
await startRecord()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 开始检测
|
// 开始检测
|
||||||
async function startDetection() {
|
async function startDetection() {
|
||||||
try {
|
try {
|
||||||
console.log('🚀 正在开始检测...')
|
console.log('🚀 正在开始检测...')
|
||||||
isRecording.value = true
|
|
||||||
startTimer()
|
startTimer()
|
||||||
// 验证患者信息
|
// 验证患者信息
|
||||||
if (!patientInfo.value || !patientInfo.value.id) {
|
if (!patientInfo.value || !patientInfo.value.id) {
|
||||||
throw new Error('缺少患者信息,无法开始检测')
|
throw new Error('缺少患者信息,无法开始检测')
|
||||||
}
|
}
|
||||||
let screen_location = contenGridRef.value.getBoundingClientRect()
|
|
||||||
let femtobolt_location = wholeBodyRef.value.getBoundingClientRect()
|
|
||||||
let camera_location = videoImgRef.value.getBoundingClientRect()
|
|
||||||
let titile_height = 24
|
|
||||||
// 调用后端API开始检测
|
// 调用后端API开始检测
|
||||||
const response = await fetch(`${BACKEND_URL}/api/detection/start`, {
|
const response = await fetch(`${BACKEND_URL}/api/detection/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -1715,10 +1697,6 @@ async function startDetection() {
|
|||||||
patient_id: patientInfo.value.id,
|
patient_id: patientInfo.value.id,
|
||||||
// 可以添加其他检测参数
|
// 可以添加其他检测参数
|
||||||
creator_id: creatorId.value,
|
creator_id: creatorId.value,
|
||||||
screen_location:[Math.round(screen_location.x), Math.round(screen_location.y) + titile_height, Math.round(screen_location.width), Math.round(screen_location.height-titile_height)],
|
|
||||||
camera_location:[Math.round(camera_location.x), Math.round(camera_location.y)+ titile_height, Math.round(camera_location.width), Math.round(camera_location.height-titile_height)],
|
|
||||||
femtobolt_location:[Math.round(femtobolt_location.x), Math.round(femtobolt_location.y) + titile_height, Math.round(femtobolt_location.width), Math.round(femtobolt_location.height-titile_height)],
|
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -1729,14 +1707,8 @@ async function startDetection() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('✅ 检测开始成功')
|
console.log('✅ 检测开始成功')
|
||||||
|
|
||||||
// 保存会话ID和检测开始时间
|
// 保存会话ID和检测开始时间
|
||||||
patientInfo.value.sessionId = result.session_id
|
patientInfo.value.sessionId = result.session_id
|
||||||
patientInfo.value.detectionStartTime = Date.now()
|
|
||||||
console.log('✅ 检测会话创建成功,会话ID:', patientInfo.value.sessionId)
|
|
||||||
|
|
||||||
isStart.value = true
|
|
||||||
ElMessage.success('检测已开始')
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || '开始检测失败')
|
throw new Error(result.message || '开始检测失败')
|
||||||
}
|
}
|
||||||
@ -1751,14 +1723,8 @@ async function startDetection() {
|
|||||||
// 停止检测
|
// 停止检测
|
||||||
async function stopDetection() {
|
async function stopDetection() {
|
||||||
try {
|
try {
|
||||||
console.log('🛑 停止检测,会话ID:', patientInfo.value.sessionId)
|
|
||||||
resetTimer()
|
|
||||||
// 计算检测持续时间
|
// 计算检测持续时间
|
||||||
let duration = 0
|
let duration = 0
|
||||||
if (patientInfo.value.detectionStartTime) {
|
|
||||||
duration = Math.floor((Date.now() - patientInfo.value.detectionStartTime) / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用后端API停止检测
|
// 调用后端API停止检测
|
||||||
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop`, {
|
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -1773,8 +1739,6 @@ async function stopDetection() {
|
|||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
}
|
}
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
isStart.value = false
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 停止检测失败:', error)
|
console.error('❌ 停止检测失败:', error)
|
||||||
ElMessage.error(`停止检测失败: ${error.message}`)
|
ElMessage.error(`停止检测失败: ${error.message}`)
|
||||||
@ -1979,15 +1943,11 @@ const getDevicesInit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
console.log(wholeBodyRef.value.getBoundingClientRect())
|
|
||||||
console.log(videoImgRef.value.getBoundingClientRect())
|
|
||||||
// 加载患者信息
|
// 加载患者信息
|
||||||
loadPatientInfo()
|
loadPatientInfo()
|
||||||
|
|
||||||
// 页面加载时自动连接WebSocket
|
// 页面加载时自动连接WebSocket
|
||||||
connectWebSocket()
|
connectWebSocket()
|
||||||
|
startDetection()
|
||||||
// 监听页面关闭或刷新事件
|
// 监听页面关闭或刷新事件
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
if (authStore.currentUser) {
|
if (authStore.currentUser) {
|
||||||
@ -2005,11 +1965,12 @@ onUnmounted(() => {
|
|||||||
if (isRecording.value) {
|
if (isRecording.value) {
|
||||||
stopRecording()
|
stopRecording()
|
||||||
}
|
}
|
||||||
// 停止检测(如果正在检测)
|
if(isStart.value == true){
|
||||||
if (isStart.value) {
|
|
||||||
stopDetection()
|
stopRecord()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopDetection()
|
||||||
// 页面关闭时断开WebSocket连接
|
// 页面关闭时断开WebSocket连接
|
||||||
disconnectWebSocket()
|
disconnectWebSocket()
|
||||||
|
|
||||||
@ -2042,6 +2003,90 @@ onUnmounted(() => {
|
|||||||
// 移除页面关闭事件监听器
|
// 移除页面关闭事件监听器
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const startRecord = async () => { // 开始录屏
|
||||||
|
try {
|
||||||
|
console.log('🚀 正在开始录屏...')
|
||||||
|
|
||||||
|
// 验证患者信息
|
||||||
|
if (!patientInfo.value || !patientInfo.value.sessionId) {
|
||||||
|
throw new Error('缺少患者信息,无法开始录屏')
|
||||||
|
}
|
||||||
|
isRecording.value = true
|
||||||
|
let screen_location = contenGridRef.value.getBoundingClientRect()
|
||||||
|
let femtobolt_location = wholeBodyRef.value.getBoundingClientRect()
|
||||||
|
let camera_location = videoImgRef.value.getBoundingClientRect()
|
||||||
|
let titile_height = 24
|
||||||
|
// 调用后端API开始录屏
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/start_record`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
patient_id: patientInfo.value.sessionId,
|
||||||
|
// 可以添加其他录屏参数
|
||||||
|
creator_id: creatorId.value,
|
||||||
|
screen_location:[Math.round(screen_location.x), Math.round(screen_location.y) + titile_height, Math.round(screen_location.width), Math.round(screen_location.height-titile_height)],
|
||||||
|
camera_location:[Math.round(camera_location.x), Math.round(camera_location.y)+ titile_height, Math.round(camera_location.width), Math.round(camera_location.height-titile_height)],
|
||||||
|
femtobolt_location:[Math.round(femtobolt_location.x), Math.round(femtobolt_location.y) + titile_height, Math.round(femtobolt_location.width), Math.round(femtobolt_location.height-titile_height)],
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 保存会话ID和检测开始时间
|
||||||
|
patientInfo.value.detectionStartTime = Date.now()
|
||||||
|
console.log('✅ 录屏会话创建成功,会话ID:', patientInfo.value.sessionId)
|
||||||
|
isStart.value = true
|
||||||
|
ElMessage.success('录屏已开始')
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '开始录屏失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`开始录屏失败: ${error.message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopRecord = async () => { // 停止录屏
|
||||||
|
try {
|
||||||
|
resetTimer()
|
||||||
|
// 计算检测持续时间
|
||||||
|
let duration = 0
|
||||||
|
if (patientInfo.value.detectionStartTime) {
|
||||||
|
duration = Math.floor((Date.now() - patientInfo.value.detectionStartTime) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用后端API停止检测
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop_record`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
duration: duration
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
isRecording.value = false
|
||||||
|
isStart.value = false
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 停止检测失败:', error)
|
||||||
|
ElMessage.error(`停止检测失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function routerClick(){
|
||||||
|
router.push(`/patient/${patientInfo.value.id}`)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -2458,7 +2503,8 @@ onUnmounted(() => {
|
|||||||
.foot-container-content {
|
.foot-container-content {
|
||||||
margin: 0px 20px;
|
margin: 0px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 300px;
|
/* width: 300px; */
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.foot-container-margintop {
|
.foot-container-margintop {
|
||||||
@ -2480,7 +2526,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
/* justify-content: center;
|
/* justify-content: center;
|
||||||
box-sizing: border-box; */
|
box-sizing: border-box; */
|
||||||
padding-top: 15px;
|
padding-top: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@ -2499,14 +2545,14 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.basic-info-text2 {
|
.basic-info-text2 {
|
||||||
width: 180px;
|
width: 80%;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
background-color: rgba(36, 36, 36, 1);
|
background-color: rgba(36, 36, 36, 1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
height: 46px;
|
height: 40px;
|
||||||
line-height: 46px;
|
line-height: 40px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2526,7 +2572,7 @@ onUnmounted(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-content: center; */
|
align-content: center; */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: calc(100% - 40px) ;
|
||||||
/* display: flex; */
|
/* display: flex; */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -2607,7 +2653,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.xline {
|
.xline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 350px;
|
width: calc(100% + 50px) ;
|
||||||
/* height: 1px; */
|
/* height: 1px; */
|
||||||
border-top: 1px dashed red;
|
border-top: 1px dashed red;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@ -2617,7 +2663,7 @@ onUnmounted(() => {
|
|||||||
.yline {
|
.yline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
/* width:1px; */
|
/* width:1px; */
|
||||||
height: 350px;
|
height: calc(100% + 50px);
|
||||||
/* height: 1px; */
|
/* height: 1px; */
|
||||||
border-left: 1px dashed red;
|
border-left: 1px dashed red;
|
||||||
top: -25px;
|
top: -25px;
|
||||||
@ -2809,5 +2855,14 @@ onUnmounted(() => {
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
}
|
}
|
||||||
|
.rotation-titles{
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Arial Negreta', 'Arial Normal', 'Arial', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #30F3FF;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -1174,6 +1174,7 @@ onMounted(() => {
|
|||||||
.main-content {
|
.main-content {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -983,6 +983,7 @@ onUnmounted(() => {
|
|||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user