Compare commits

...

7 Commits

63 changed files with 9082 additions and 4505 deletions

View File

@ -1,15 +0,0 @@
{
"product": "BodyBalanceEvaluation",
"version": "1.0.0",
"machine_id": "W10-D13710C7BD317C29",
"platform": "Windows",
"request_time": "2025-11-04T05:35:19.472181+00:00",
"hardware_info": {
"system": "Windows",
"machine": "AMD64",
"processor": "Intel64 Family 6 Model 165 Stepping 2, GenuineIntel",
"node": "MSI"
},
"company_name": "北京天宏博科技有限公司",
"contact_info": "thb@163.com"
}

View File

@ -17,7 +17,7 @@ max_backups = 7
[FILEPATH] [FILEPATH]
path = D:/BodyCheck/file/ path = D:/BodyCheck/file/
[CAMERA] [CAMERA1]
enabled = True enabled = True
device_index = 0 device_index = 0
width = 1280 width = 1280
@ -27,6 +27,16 @@ buffer_size = 1
fourcc = MJPG fourcc = MJPG
backend = directshow backend = directshow
[CAMERA2]
enabled = True
device_index = 1
width = 1280
height = 720
fps = 30
buffer_size = 1
fourcc = MJPG
backend = directshow
[FEMTOBOLT] [FEMTOBOLT]
enabled = True enabled = True
algorithm_type = plt algorithm_type = plt

File diff suppressed because it is too large Load Diff

View File

@ -26,34 +26,49 @@ except ImportError:
class CameraManager(BaseDevice): class CameraManager(BaseDevice):
"""普通相机管理器""" """普通相机管理器"""
def __init__(self, socketio, config_manager: Optional[ConfigManager] = None): def __init__(self, socketio, config_manager: Optional[ConfigManager] = None,
device_name: str = "camera1",
instance_config: Optional[Dict[str, Any]] = None):
""" """
初始化相机管理器 初始化相机管理器
Args: Args:
socketio: SocketIO实例 socketio: SocketIO实例
config_manager: 配置管理器实例 config_manager: 配置管理器实例
device_name: 设备名称仅支持 'camera1' | 'camera2'
instance_config: 覆盖默认配置的实例级配置 device_index分辨率fps
""" """
# 配置管理 # 配置管理
self.config_manager = config_manager or ConfigManager() self.config_manager = config_manager or ConfigManager()
config = self.config_manager.get_device_config('camera') # 校验设备名,仅允许 camera1/camera2
if device_name not in ('camera1', 'camera2'):
raise ValueError(f"不支持的设备名: {device_name},仅支持 'camera1'/'camera2'")
# 根据设备名选择配置源:'camera1' 使用 [CAMERA1]'camera2' 使用 [CAMERA2]
base_key = 'camera1' if device_name == 'camera1' else 'camera2'
base_config = self.config_manager.get_device_config(base_key)
# 合并实例覆盖配置
if instance_config:
try:
base_config = {**base_config, **instance_config}
except Exception:
pass
super().__init__("camera", config) super().__init__(device_name, base_config)
# 保存socketio实例 # 保存socketio实例
self._socketio = socketio self._socketio = socketio
# 相机相关属性 # 相机相关属性
self.cap = None self.cap = None
self.device_index = config.get('device_index', 0) self.device_index = base_config.get('device_index', 0)
self.width = config.get('width', 1280) self.width = base_config.get('width', 1280)
self.height = config.get('height', 720) self.height = base_config.get('height', 720)
self.fps = config.get('fps', 30) self.fps = base_config.get('fps', 30)
self.buffer_size = config.get('buffer_size', 1) self.buffer_size = base_config.get('buffer_size', 1)
self.fourcc = config.get('fourcc', 'MJPG') self.fourcc = base_config.get('fourcc', 'MJPG')
# OpenCV后端配置 (DirectShow性能最佳) # OpenCV后端配置 (DirectShow性能最佳)
backend_name = config.get('backend', 'directshow').lower() backend_name = base_config.get('backend', 'directshow').lower()
self.backend_map = { self.backend_map = {
'directshow': cv2.CAP_DSHOW, 'directshow': cv2.CAP_DSHOW,
'dshow': cv2.CAP_DSHOW, 'dshow': cv2.CAP_DSHOW,
@ -64,12 +79,12 @@ class CameraManager(BaseDevice):
self.backend_name = backend_name self.backend_name = backend_name
# 额外可调的降采样宽度(不改变外部配置语义,仅内部优化传输) # 额外可调的降采样宽度(不改变外部配置语义,仅内部优化传输)
self._tx_max_width = int(config.get('tx_max_width', 1920)) self._tx_max_width = int(base_config.get('tx_max_width', 1920))
# 流控制 # 流控制
self.streaming_thread = None self.streaming_thread = None
# 减小缓存长度保留最近2帧即可避免累计占用 # 减小缓存长度保留最近2帧即可避免累计占用
self.frame_cache = queue.Queue(maxsize=int(config.get('frame_cache_len', 2))) self.frame_cache = queue.Queue(maxsize=int(base_config.get('frame_cache_len', 2)))
self.last_frame = None self.last_frame = None
self.frame_count = 0 self.frame_count = 0
self.dropped_frames = 0 self.dropped_frames = 0
@ -80,13 +95,14 @@ class CameraManager(BaseDevice):
self.actual_fps = 0 self.actual_fps = 0
# 重连与断连检测机制(-1 表示无限重连) # 重连与断连检测机制(-1 表示无限重连)
self.max_reconnect_attempts = int(config.get('max_reconnect_attempts', -1)) self.max_reconnect_attempts = int(base_config.get('max_reconnect_attempts', -1))
self.reconnect_delay = float(config.get('reconnect_delay', 2.0)) self.reconnect_delay = float(base_config.get('reconnect_delay', 2.0))
self.read_fail_threshold = int(config.get('read_fail_threshold', 30)) self.read_fail_threshold = int(base_config.get('read_fail_threshold', 30))
self._last_connected_state = None self._last_connected_state = None
# 设备标识和性能统计 # 设备标识和性能统计
self.device_id = f"camera_{self.device_index}" # 使用设备名作为ID便于前端区分
self.device_id = device_name
self.performance_stats = { self.performance_stats = {
'frames_processed': 0, 'frames_processed': 0,
'actual_fps': 0, 'actual_fps': 0,
@ -373,6 +389,9 @@ class CameraManager(BaseDevice):
total_config_time = (time.time() - config_start) * 1000 total_config_time = (time.time() - config_start) * 1000
# 若未进行性能优化,确保变量存在
optimization_time = locals().get('optimization_time', 0.0)
self.logger.info(f"相机配置完成 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}") self.logger.info(f"相机配置完成 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}")
self.logger.info(f"配置耗时统计 - 缓冲区: {buffer_time:.1f}ms, 优化设置: {optimization_time:.1f}ms, 分辨率: {resolution_time:.1f}ms, 帧率: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms") self.logger.info(f"配置耗时统计 - 缓冲区: {buffer_time:.1f}ms, 优化设置: {optimization_time:.1f}ms, 分辨率: {resolution_time:.1f}ms, 帧率: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
self.logger.debug(f"配置详情 - 分辨率设置: {resolution_time:.1f}ms, FPS设置: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms") self.logger.debug(f"配置详情 - 分辨率设置: {resolution_time:.1f}ms, FPS设置: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms")
@ -852,8 +871,9 @@ class CameraManager(BaseDevice):
# 获取最新配置 # 获取最新配置(按设备名映射,已限制为 camera1/camera2
config = self.config_manager.get_device_config('camera') key = self.device_name
config = self.config_manager.get_device_config(key)
# 更新配置属性 # 更新配置属性
self.device_index = config.get('device_index', 0) self.device_index = config.get('device_index', 0)
@ -880,8 +900,8 @@ class CameraManager(BaseDevice):
# 创建新队列 # 创建新队列
self.frame_cache = queue.Queue(maxsize=frame_cache_len) self.frame_cache = queue.Queue(maxsize=frame_cache_len)
# 更新设备信息 # 更新设备信息设备ID直接使用设备名
self.device_id = f"camera_{self.device_index}" self.device_id = self.device_name
self.logger.info(f"相机配置重新加载成功 - 设备索引: {self.device_index}, 分辨率: {self.width}x{self.height}, FPS: {self.fps}") self.logger.info(f"相机配置重新加载成功 - 设备索引: {self.device_index}, 分辨率: {self.width}x{self.height}, FPS: {self.fps}")
return True return True

View File

@ -113,7 +113,7 @@ class DeviceCoordinator:
# 注册Socket.IO命名空间 # 注册Socket.IO命名空间
self._register_namespaces() self._register_namespaces()
# 初始化设备 # 初始化设备(失败则降级继续)
if not self._initialize_devices(): if not self._initialize_devices():
self.logger.warning("设备初始化失败,将以降级模式继续运行") self.logger.warning("设备初始化失败,将以降级模式继续运行")
@ -163,10 +163,12 @@ class DeviceCoordinator:
future = self.executor.submit(self._init_femtobolt) future = self.executor.submit(self._init_femtobolt)
futures.append(('femtobolt', future)) futures.append(('femtobolt', future))
# 普通相机 # 普通相机初始化两个实例camera1 与 camera2
if self.device_configs.get('camera', {}).get('enabled', False): # camera1 使用 [CAMERA1] 配置camera2 使用 [CAMERA2](若不存在则回退为 device_index+1
future = self.executor.submit(self._init_camera) if self.device_configs.get('camera1', {}).get('enabled', True):
futures.append(('camera', future)) futures.append(('camera1', self.executor.submit(self._init_camera_by_name, 'camera1', 'CAMERA1')))
if self.device_configs.get('camera2', {}).get('enabled', True):
futures.append(('camera2', self.executor.submit(self._init_camera_by_name, 'camera2', 'CAMERA2')))
# IMU传感器 # IMU传感器
if self.device_configs.get('imu', {}).get('enabled', False): if self.device_configs.get('imu', {}).get('enabled', False):
@ -205,21 +207,72 @@ class DeviceCoordinator:
self.logger.error(f"设备初始化失败: {e}") self.logger.error(f"设备初始化失败: {e}")
return False return False
def _init_camera(self) -> bool: def _init_camera_by_name(self, device_name: str, section: str = 'CAMERA1') -> bool:
""" """
初始化普通相机 按名称初始化相机支持 camera1/camera2 并覆盖配置段
Args:
device_name: 设备名称 'camera1''camera2'
section: 配置段名称'CAMERA1' 'CAMERA2'
Returns: Returns:
bool: 初始化是否成功 bool: 初始化是否成功
""" """
try: try:
camera = CameraManager(self.socketio, self.config_manager) # 构造实例覆盖配置:优先读取目标配置段,否则回退到 [CAMERA1]
self.devices['camera'] = camera cfg = {}
parser = getattr(self.config_manager, 'config', None)
base_cam = self.config_manager.get_device_config('camera1')
if parser and parser.has_section(section):
# 读取所有相关键
def get_opt(sec, key, fallback=None):
try:
return parser.get(sec, key)
except Exception:
return fallback
def get_int(sec, key, fallback=None):
try:
return parser.getint(sec, key)
except Exception:
return fallback
def get_bool(sec, key, fallback=None):
try:
return parser.getboolean(sec, key)
except Exception:
return fallback
enabled = get_bool(section, 'enabled', True)
if not enabled:
self.logger.info(f"{device_name} 未启用,跳过初始化")
return False
# 填充覆盖项
idx2 = get_int(section, 'device_index', None)
if idx2 is not None:
cfg['device_index'] = idx2
w = get_int(section, 'width', None)
h = get_int(section, 'height', None)
f = get_int(section, 'fps', None)
buf = get_int(section, 'buffer_size', None)
fourcc = get_opt(section, 'fourcc', None)
backend = get_opt(section, 'backend', None)
if w is not None: cfg['width'] = w
if h is not None: cfg['height'] = h
if f is not None: cfg['fps'] = f
if buf is not None: cfg['buffer_size'] = buf
if fourcc is not None: cfg['fourcc'] = fourcc
if backend is not None: cfg['backend'] = backend
else:
# section 不存在时camera2 默认使用 device_index+1
if device_name.lower() == 'camera2':
cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1
else:
cfg['device_index'] = int(base_cam.get('device_index', 0))
camera = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg)
self.devices[device_name] = camera
if camera.initialize(): if camera.initialize():
return True return True
return False return False
except Exception as e: except Exception as e:
self.logger.error(f"初始化相机失败: {e}") self.logger.error(f"初始化{device_name}失败: {e}")
return False return False
def _init_imu(self) -> bool: def _init_imu(self) -> bool:
@ -441,8 +494,8 @@ class DeviceCoordinator:
success_count = 0 success_count = 0
for device_name, device in self.devices.items(): for device_name, device in self.devices.items():
try: try:
# 对深度相机(femtobolt)和普通相机(camera)直接调用初始化和启动推流 # 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控
if device_name in ['femtobolt', 'camera',"imu"]: if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]:
continue continue
if hasattr(device, '_start_connection_monitor'): if hasattr(device, '_start_connection_monitor'):
@ -475,19 +528,9 @@ class DeviceCoordinator:
success_count = 0 success_count = 0
for device_name, device in self.devices.items(): for device_name, device in self.devices.items():
try: try:
# 对深度相机(femtobolt)和普通相机(camera)直接调用停止推流 # 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控停止
if device_name in ['femtobolt', 'camera',"imu"]: if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]:
self.logger.info(f"停止{device_name}设备推流") self.logger.info(f"停止{device_name}设备推流")
# # 调用设备的cleanup方法清理资源,停止推流
# if hasattr(device, 'cleanup'):
# if device.cleanup():
# success_count += 1
# self.logger.info(f"{device_name}设备推流已停止")
# else:
# self.logger.warning(f"{device_name}设备推流停止失败")
# else:
# self.logger.warning(f"{device_name}设备不支持推流停止")
continue continue
if hasattr(device, '_stop_connection_monitor'): if hasattr(device, '_stop_connection_monitor'):
@ -592,13 +635,58 @@ class DeviceCoordinator:
new_device = None new_device = None
try: try:
# 根据设备类型重新创建实例 # 根据设备类型重新创建实例(仅支持 camera1/camera2
if device_name == 'camera': if device_name in ('camera1', 'camera2'):
try: try:
from .camera_manager import CameraManager from .camera_manager import CameraManager
except ImportError: except ImportError:
from camera_manager import CameraManager from camera_manager import CameraManager
new_device = CameraManager(self.socketio, self.config_manager) # 为 camera1/camera2 构造实例配置
section = 'CAMERA1' if device_name == 'camera1' else 'CAMERA2'
cfg = {}
parser = getattr(self.config_manager, 'config', None)
base_cam = self.config_manager.get_device_config('camera1')
if parser and parser.has_section(section):
def get_opt(sec, key, fallback=None):
try:
return parser.get(sec, key)
except Exception:
return fallback
def get_int(sec, key, fallback=None):
try:
return parser.getint(sec, key)
except Exception:
return fallback
def get_bool(sec, key, fallback=None):
try:
return parser.getboolean(sec, key)
except Exception:
return fallback
enabled = get_bool(section, 'enabled', True)
if not enabled:
raise Exception(f"{device_name} 未启用")
idx2 = get_int(section, 'device_index', None)
if idx2 is not None:
cfg['device_index'] = idx2
w = get_int(section, 'width', None)
h = get_int(section, 'height', None)
f = get_int(section, 'fps', None)
buf = get_int(section, 'buffer_size', None)
fourcc = get_opt(section, 'fourcc', None)
backend = get_opt(section, 'backend', None)
if w is not None: cfg['width'] = w
if h is not None: cfg['height'] = h
if f is not None: cfg['fps'] = f
if buf is not None: cfg['buffer_size'] = buf
if fourcc is not None: cfg['fourcc'] = fourcc
if backend is not None: cfg['backend'] = backend
else:
# section 不存在时camera2 默认使用 [CAMERA1] 的 device_index + 1
if device_name == 'camera2':
cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1
else:
cfg['device_index'] = int(base_cam.get('device_index', 0))
new_device = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg)
elif device_name == 'imu': elif device_name == 'imu':
try: try:
from .imu_manager import IMUManager from .imu_manager import IMUManager
@ -812,7 +900,12 @@ class DeviceCoordinator:
self.executor.shutdown(wait=True) self.executor.shutdown(wait=True)
# 清理Socket管理器 # 清理Socket管理器
self.socket_manager.cleanup() try:
self.socket_manager.cleanup_all()
except Exception:
# 兼容旧接口
if hasattr(self.socket_manager, 'cleanup'):
self.socket_manager.cleanup()
self.logger.info("设备协调器已关闭") self.logger.info("设备协调器已关闭")
@ -837,7 +930,7 @@ def test_restart_device(device_name=None):
Args: Args:
device_name (str, optional): 指定要测试的设备名称如果为None则自动选择第一个可用设备 device_name (str, optional): 指定要测试的设备名称如果为None则自动选择第一个可用设备
可选值: 'camera', 'imu', 'pressure', 'femtobolt' 可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'
""" """
import time import time
import threading import threading
@ -847,22 +940,13 @@ def test_restart_device(device_name=None):
print("设备协调器重启功能测试") print("设备协调器重启功能测试")
print("=" * 60) print("=" * 60)
# 创建模拟的SocketIO和配置管理器 # 创建模拟的SocketIO(使用真实配置文件)
mock_socketio = Mock() mock_socketio = Mock()
mock_config_manager = Mock()
# 模拟配置数据
mock_config_manager.get_device_config.return_value = {
'camera': {'enabled': True, 'device_id': 0, 'fps': 30},
'imu': {'enabled': True, 'device_type': 'mock'},
'pressure': {'enabled': True, 'device_type': 'mock'},
'femtobolt': {'enabled': False}
}
try: try:
# 创建设备协调器实例 # 创建设备协调器实例
print("1. 创建设备协调器...") print("1. 创建设备协调器...")
coordinator = DeviceCoordinator(mock_socketio, mock_config_manager) coordinator = DeviceCoordinator(mock_socketio)
# 初始化设备协调器 # 初始化设备协调器
print("2. 初始化设备协调器...") print("2. 初始化设备协调器...")
@ -882,13 +966,14 @@ def test_restart_device(device_name=None):
print("❌ 没有可用的设备进行测试") print("❌ 没有可用的设备进行测试")
return False return False
# 根据参数选择测试设备 # 根据参数选择测试设备(仅支持 camera1/camera2/imu/pressure/femtobolt
if device_name: if device_name:
if device_name in available_devices: allowed = {'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'}
if device_name in available_devices and device_name in allowed:
test_device = device_name test_device = device_name
print(f"3. 使用指定的测试设备: {test_device}") print(f"3. 使用指定的测试设备: {test_device}")
else: else:
print(f"❌ 指定的设备 '{device_name}' 不存在") print(f"❌ 指定的设备 '{device_name}' 不存在或不受支持")
print(f" 可用设备: {available_devices}") print(f" 可用设备: {available_devices}")
return False return False
else: else:
@ -992,7 +1077,7 @@ if __name__ == "__main__":
) )
# 执行测试 # 执行测试
# 可选值: 'camera', 'imu', 'pressure', 'femtobolt' # 可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'
success = test_restart_device('pressure') success = test_restart_device('pressure')
if success: if success:

View File

@ -305,10 +305,10 @@ class BleIMUDevice:
while self.running: while self.running:
try: try:
logger.info(f"扫描并连接蓝牙IMU: {self.mac_address} ...") # logger.info(f"扫描并连接蓝牙IMU: {self.mac_address} ...")
device = await BleakScanner.find_device_by_address(self.mac_address, cb=dict(use_bdaddr=False)) device = await BleakScanner.find_device_by_address(self.mac_address, cb=dict(use_bdaddr=False))
if device is None: if device is None:
logger.warning(f"未找到设备: {self.mac_address}") # logger.warning(f"未找到设备: {self.mac_address}")
await asyncio.sleep(2.0) await asyncio.sleep(2.0)
continue continue

View File

@ -242,7 +242,7 @@ class RecordingManager:
return frame return frame
def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]: def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera1_location: List[int], camera2_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]:
""" """
启动同步录制 启动同步录制
@ -250,9 +250,10 @@ class RecordingManager:
session_id: 检测会话ID session_id: 检测会话ID
patient_id: 患者ID patient_id: 患者ID
screen_location: 屏幕录制区域 [x, y, w, h] screen_location: 屏幕录制区域 [x, y, w, h]
camera_location: 相机录制区域 [x, y, w, h] camera1_location: 相机1录制区域 [x, y, w, h]
camera2_location: 相机2录制区域 [x, y, w, h]
femtobolt_location: FemtoBolt录制区域 [x, y, w, h] femtobolt_location: FemtoBolt录制区域 [x, y, w, h]
recording_types: 录制类型列表 ['screen', 'feet', 'femtobolt']默认全部录制 recording_types: 录制类型列表 ['screen', 'camera1', 'camera2', 'feet', 'femtobolt']默认全部录制
Returns: Returns:
Dict: 录制启动状态和信息 Dict: 录制启动状态和信息
@ -263,6 +264,8 @@ class RecordingManager:
'patient_id': patient_id, 'patient_id': patient_id,
'recording_start_time': None, 'recording_start_time': None,
'video_paths': { 'video_paths': {
'camera1_video': None,
'camera2_video': None,
'feet_video': None, 'feet_video': None,
'screen_video': None, 'screen_video': None,
'femtobolt_video': None 'femtobolt_video': None
@ -277,7 +280,7 @@ class RecordingManager:
return result return result
# 设置默认录制类型 # 设置默认录制类型
recording_types = ['screen', 'feet'] recording_types = ['screen', 'camera1' ,'camera2']
# 验证录制区域参数(仅对启用的录制类型进行验证) # 验证录制区域参数(仅对启用的录制类型进行验证)
if 'screen' in recording_types: if 'screen' in recording_types:
@ -286,10 +289,16 @@ class RecordingManager:
result['message'] = '屏幕录制区域参数无效或缺失必须是包含4个元素的数组[x, y, w, h]' result['message'] = '屏幕录制区域参数无效或缺失必须是包含4个元素的数组[x, y, w, h]'
return result return result
if 'feet' in recording_types: if 'camera1' in recording_types:
if not camera_location or not isinstance(camera_location, list) or len(camera_location) != 4: if not camera1_location or not isinstance(camera1_location, list) or len(camera1_location) != 4:
result['success'] = False result['success'] = False
result['message'] = '相机录制区域参数无效或缺失必须是包含4个元素的数组[x, y, w, h]' result['message'] = '相机1录制区域参数无效或缺失必须是包含4个元素的数组[x, y, w, h]'
return result
if 'camera2' in recording_types:
if not camera2_location or not isinstance(camera2_location, list) or len(camera2_location) != 4:
result['success'] = False
result['message'] = '相机2录制区域参数无效或缺失必须是包含4个元素的数组[x, y, w, h]'
return result return result
if 'femtobolt' in recording_types: if 'femtobolt' in recording_types:
@ -303,19 +312,22 @@ class RecordingManager:
# self.logger.info(f'检测sessionID................: {self.current_session_id}') # self.logger.info(f'检测sessionID................: {self.current_session_id}')
self.current_patient_id = patient_id self.current_patient_id = patient_id
self.screen_region = tuple(screen_location) # [x, y, w, h] -> (x, y, w, h) self.screen_region = tuple(screen_location) # [x, y, w, h] -> (x, y, w, h)
self.camera_region = tuple(camera_location) # [x, y, w, h] -> (x, y, w, h) self.camera1_region = tuple(camera1_location) # [x, y, w, h] -> (x, y, w, h)
self.camera2_region = tuple(camera2_location) # [x, y, w, h] -> (x, y, w, h)
self.femtobolt_region = tuple(femtobolt_location) # [x, y, w, h] -> (x, y, w, h) self.femtobolt_region = tuple(femtobolt_location) # [x, y, w, h] -> (x, y, w, h)
# 根据录制区域大小设置自适应帧率 # 根据录制区域大小设置自适应帧率
if 'screen' in recording_types: if 'screen' in recording_types:
self._set_adaptive_fps_by_region('screen', self.screen_region) self._set_adaptive_fps_by_region('screen', self.screen_region)
if 'feet' in recording_types: if 'camera1' in recording_types:
self._set_adaptive_fps_by_region('camera', self.camera_region) self._set_adaptive_fps_by_region('camera1', self.camera1_region)
if 'camera2' in recording_types:
self._set_adaptive_fps_by_region('camera2', self.camera2_region)
if 'femtobolt' in recording_types: if 'femtobolt' in recording_types:
self._set_adaptive_fps_by_region('femtobolt', self.femtobolt_region) self._set_adaptive_fps_by_region('femtobolt', self.femtobolt_region)
# 设置录制同步 # 设置录制同步
active_recording_count = len([t for t in recording_types if t in ['screen', 'feet', 'femtobolt']]) active_recording_count = len([t for t in recording_types if t in ['screen', 'camera1', 'camera2', 'femtobolt']])
self.recording_sync_barrier = threading.Barrier(active_recording_count) self.recording_sync_barrier = threading.Barrier(active_recording_count)
self.recording_start_sync.clear() self.recording_start_sync.clear()
self.global_recording_start_time = None self.global_recording_start_time = None
@ -340,7 +352,8 @@ class RecordingManager:
return result return result
feet_video_path = os.path.join(base_path, 'feet.mp4') camera1_video_path = os.path.join(base_path, 'camera1.mp4')
camera2_video_path = os.path.join(base_path, 'camera2.mp4')
screen_video_path = os.path.join(base_path, 'screen.mp4') screen_video_path = os.path.join(base_path, 'screen.mp4')
femtobolt_video_path = os.path.join(base_path, 'femtobolt.mp4') femtobolt_video_path = os.path.join(base_path, 'femtobolt.mp4')
@ -350,7 +363,8 @@ class RecordingManager:
'session_id': session_id, 'session_id': session_id,
'status': 'recording', 'status': 'recording',
'video_paths': { 'video_paths': {
'normal_video_path': os.path.join(db_base_path, 'feet.mp4'), 'camera1_video_path': os.path.join(db_base_path, 'camera1.mp4'),
'camera2_video_path': os.path.join(db_base_path, 'camera2.mp4'),
'screen_video_path': os.path.join(db_base_path, 'screen.mp4'), 'screen_video_path': os.path.join(db_base_path, 'screen.mp4'),
'femtobolt_video_path': os.path.join(db_base_path, 'femtobolt.mp4') 'femtobolt_video_path': os.path.join(db_base_path, 'femtobolt.mp4')
} }
@ -373,7 +387,20 @@ class RecordingManager:
# 根据录制类型选择性地初始化视频写入器,使用各自的自适应帧率 # 根据录制类型选择性地初始化视频写入器,使用各自的自适应帧率
self.screen_video_writer = None self.screen_video_writer = None
self.femtobolt_video_writer = None self.femtobolt_video_writer = None
self.feet_video_writer = None self.camera1_video_writer = None
self.camera2_video_writer = None
if 'camera1' in recording_types:
self.camera1_video_writer = cv2.VideoWriter(
camera1_video_path, fourcc, self.camera1_current_fps, (self.camera1_region[2], self.camera1_region[3])
)
self.logger.info(f'相机1视频写入器使用帧率: {self.camera1_current_fps}fps')
if 'camera2' in recording_types:
self.camera2_video_writer = cv2.VideoWriter(
camera2_video_path, fourcc, self.camera2_current_fps, (self.camera2_region[2], self.camera2_region[3])
)
self.logger.info(f'相机2视频写入器使用帧率: {self.camera2_current_fps}fps')
if 'screen' in recording_types: if 'screen' in recording_types:
self.screen_video_writer = cv2.VideoWriter( self.screen_video_writer = cv2.VideoWriter(
@ -387,22 +414,23 @@ class RecordingManager:
) )
self.logger.info(f'FemtoBolt视频写入器使用帧率: {self.femtobolt_current_fps}fps') self.logger.info(f'FemtoBolt视频写入器使用帧率: {self.femtobolt_current_fps}fps')
if 'feet' in recording_types:
self.feet_video_writer = cv2.VideoWriter(
feet_video_path, fourcc, self.camera_current_fps, (self.camera_region[2], self.camera_region[3])
)
self.logger.info(f'足部视频写入器使用帧率: {self.camera_current_fps}fps')
# 检查视频写入器状态(仅检查启用的录制类型) # 检查相机1视频写入器
# 检查足部视频写入器 if 'camera1' in recording_types:
if 'feet' in recording_types: if self.camera1_video_writer and self.camera1_video_writer.isOpened():
if self.feet_video_writer and self.feet_video_writer.isOpened(): self.logger.info(f'相机1视频写入器初始化成功: {camera1_video_path}')
self.logger.info(f'足部视频写入器初始化成功: {feet_video_path}')
else: else:
self.logger.error(f'足部视频写入器初始化失败: {feet_video_path}') self.logger.error(f'相机1视频写入器初始化失败: {camera1_video_path}')
else:
self.logger.info('相机1录制功能已禁用')
# 检查相机2视频写入器
if 'camera2' in recording_types:
if self.camera2_video_writer and self.camera2_video_writer.isOpened():
self.logger.info(f'相机2视频写入器初始化成功: {camera2_video_path}')
else:
self.logger.error(f'相机2视频写入器初始化失败: {camera2_video_path}')
else: else:
self.logger.info('足部录制功能已禁用') self.logger.info('足部录制功能已禁用')
# 检查屏幕视频写入器 # 检查屏幕视频写入器
if 'screen' in recording_types: if 'screen' in recording_types:
if self.screen_video_writer and self.screen_video_writer.isOpened(): if self.screen_video_writer and self.screen_video_writer.isOpened():
@ -426,15 +454,25 @@ class RecordingManager:
self.sync_recording = True self.sync_recording = True
# 根据录制类型启动对应的录制线程 # 根据录制类型启动对应的录制线程
if 'feet' in recording_types and self.feet_video_writer and self.feet_video_writer.isOpened(): if 'camera1' in recording_types and self.camera1_video_writer and self.camera1_video_writer.isOpened():
self.feet_recording_thread = threading.Thread( self.camera1_recording_thread = threading.Thread(
target=self._generic_recording_thread, target=self._generic_recording_thread,
args=('camera', self.camera_region, feet_video_path, self.feet_video_writer), args=('camera1', self.camera1_region, camera1_video_path, self.camera1_video_writer),
daemon=True, daemon=True,
name='FeetRecordingThread' name='Camera1RecordingThread'
) )
self.feet_recording_thread.start() self.camera1_recording_thread.start()
# self.logger.info(f'足部录制线程已启动 - 区域: {self.camera_region}, 输出文件: {feet_video_path}') # self.logger.info(f'相机1录制线程已启动 - 区域: {self.camera1_region}, 输出文件: {camera1_video_path}')
if 'camera2' in recording_types and self.camera2_video_writer and self.camera2_video_writer.isOpened():
self.camera2_recording_thread = threading.Thread(
target=self._generic_recording_thread,
args=('camera2', self.camera2_region, camera2_video_path, self.camera2_video_writer),
daemon=True,
name='Camera2RecordingThread'
)
self.camera2_recording_thread.start()
# self.logger.info(f'相机2录制线程已启动 - 区域: {self.camera2_region}, 输出文件: {camera2_video_path}')
if 'screen' in recording_types and self.screen_video_writer and self.screen_video_writer.isOpened(): if 'screen' in recording_types and self.screen_video_writer and self.screen_video_writer.isOpened():
self.screen_recording_thread = threading.Thread( self.screen_recording_thread = threading.Thread(
@ -506,8 +544,10 @@ class RecordingManager:
# 收集活跃的录制线程 # 收集活跃的录制线程
active_threads = [] active_threads = []
if hasattr(self, 'feet_recording_thread') and self.feet_recording_thread and self.feet_recording_thread.is_alive(): if hasattr(self, 'camera1_recording_thread') and self.camera1_recording_thread and self.camera1_recording_thread.is_alive():
active_threads.append(('feet', self.feet_recording_thread)) active_threads.append(('camera1', self.camera1_recording_thread))
if hasattr(self, 'camera2_recording_thread') and self.camera2_recording_thread and self.camera2_recording_thread.is_alive():
active_threads.append(('camera2', self.camera2_recording_thread))
if hasattr(self, 'screen_recording_thread') and self.screen_recording_thread and self.screen_recording_thread.is_alive(): if hasattr(self, 'screen_recording_thread') and self.screen_recording_thread and self.screen_recording_thread.is_alive():
active_threads.append(('screen', self.screen_recording_thread)) active_threads.append(('screen', self.screen_recording_thread))
if hasattr(self, 'femtobolt_recording_thread') and self.femtobolt_recording_thread and self.femtobolt_recording_thread.is_alive(): if hasattr(self, 'femtobolt_recording_thread') and self.femtobolt_recording_thread and self.femtobolt_recording_thread.is_alive():
@ -535,9 +575,12 @@ class RecordingManager:
if thread_name == 'screen': if thread_name == 'screen':
expected_frames = int(actual_recording_duration * self.screen_current_fps) expected_frames = int(actual_recording_duration * self.screen_current_fps)
self.logger.info(f' 屏幕录制预期帧数: {expected_frames}帧 (帧率{self.screen_current_fps}fps)') self.logger.info(f' 屏幕录制预期帧数: {expected_frames}帧 (帧率{self.screen_current_fps}fps)')
elif thread_name == 'feet': elif thread_name == 'camera1':
expected_frames = int(actual_recording_duration * self.camera_current_fps) expected_frames = int(actual_recording_duration * self.camera_current_fps)
self.logger.info(f' 足部录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)') self.logger.info(f' 相机1录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)')
elif thread_name == 'camera2':
expected_frames = int(actual_recording_duration * self.camera_current_fps)
self.logger.info(f' 相机2录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)')
elif thread_name == 'femtobolt': elif thread_name == 'femtobolt':
expected_frames = int(actual_recording_duration * self.femtobolt_current_fps) expected_frames = int(actual_recording_duration * self.femtobolt_current_fps)
self.logger.info(f' FemtoBolt录制预期帧数: {expected_frames}帧 (帧率{self.femtobolt_current_fps}fps)') self.logger.info(f' FemtoBolt录制预期帧数: {expected_frames}帧 (帧率{self.femtobolt_current_fps}fps)')
@ -575,7 +618,7 @@ class RecordingManager:
通用录制线程支持屏幕相机和FemtoBolt录制 通用录制线程支持屏幕相机和FemtoBolt录制
Args: Args:
recording_type: 录制类型 ('screen', 'camera', 'femtobolt') recording_type: 录制类型 ('screen', 'camera1', 'camera2', 'femtobolt')
region: 录制区域 (x, y, width, height) region: 录制区域 (x, y, width, height)
output_file_name: 输出文件名 output_file_name: 输出文件名
video_writer: 视频写入器对象 video_writer: 视频写入器对象
@ -587,8 +630,10 @@ class RecordingManager:
# 根据录制类型获取对应的自适应帧率 # 根据录制类型获取对应的自适应帧率
if recording_type == 'screen': if recording_type == 'screen':
target_fps = self.screen_current_fps target_fps = self.screen_current_fps
elif recording_type == 'camera': elif recording_type == 'camera1':
target_fps = self.camera_current_fps target_fps = self.camera1_current_fps
elif recording_type == 'camera2':
target_fps = self.camera2_current_fps
elif recording_type == 'femtobolt': elif recording_type == 'femtobolt':
target_fps = self.femtobolt_current_fps target_fps = self.femtobolt_current_fps
else: else:
@ -789,9 +834,9 @@ class RecordingManager:
def collect_detection_data(self, session_id: str, patient_id: str, detection_data: Dict[str, Any]) -> Dict[str, Any]: def save_detection_images(self, session_id: str, patient_id: str, detection_data: Dict[str, Any]) -> Dict[str, Any]:
""" """
保存前端传入的检测数据和图片 保存前端传入的检测图片到指定目录
Args: Args:
session_id: 检测会话ID session_id: 检测会话ID
@ -824,7 +869,8 @@ class RecordingManager:
'body_image': None, 'body_image': None,
'foot_data': detection_data.get('foot_data'), 'foot_data': detection_data.get('foot_data'),
'foot_data_image': None, 'foot_data_image': None,
'foot_image': None, 'foot1_image': None,
'foot2_image': None,
'screen_image': None, 'screen_image': None,
'timestamp': timestamp 'timestamp': timestamp
} }
@ -833,7 +879,8 @@ class RecordingManager:
# 保存图片数据 # 保存图片数据
image_fields = [ image_fields = [
('body_image', 'body'), ('body_image', 'body'),
('foot_image', 'foot'), ('foot1_image', 'foot1'),
('foot2_image', 'foot2'),
('foot_data_image', 'foot_data') ('foot_data_image', 'foot_data')
] ]

View File

@ -84,7 +84,8 @@ class DeviceTestServer:
# 设备管理器和模拟数据生成器 # 设备管理器和模拟数据生成器
self.device_managers = { self.device_managers = {
'camera': CameraManager(self.socketio, self.config_manager), 'camera1': CameraManager(self.socketio, self.config_manager, device_name='camera1'),
'camera2': CameraManager(self.socketio, self.config_manager, device_name='camera2'),
'femtobolt': FemtoBoltManager(self.socketio, self.config_manager), 'femtobolt': FemtoBoltManager(self.socketio, self.config_manager),
'imu': IMUManager(self.socketio, self.config_manager), 'imu': IMUManager(self.socketio, self.config_manager),
'pressure': PressureManager(self.socketio, self.config_manager) 'pressure': PressureManager(self.socketio, self.config_manager)
@ -340,7 +341,8 @@ class DeviceTestServer:
def _get_event_name(self, device_name: str) -> str: def _get_event_name(self, device_name: str) -> str:
"""获取设备对应的事件名称""" """获取设备对应的事件名称"""
event_map = { event_map = {
'camera': 'camera_frame', 'camera1': 'camera_frame',
'camera2': 'camera_frame',
'femtobolt': 'femtobolt_frame', 'femtobolt': 'femtobolt_frame',
'imu': 'imu_data', 'imu': 'imu_data',
'pressure': 'pressure_data' 'pressure': 'pressure_data'

View File

@ -1,227 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
配置API测试脚本
用于测试设备配置HTTP API的功能
"""
import requests
import json
import time
class ConfigAPITester:
"""配置API测试器"""
def __init__(self, base_url="http://localhost:5002"):
"""
初始化测试器
Args:
base_url: API基础URL
"""
self.base_url = base_url
self.api_url = f"{base_url}/api/config"
def test_get_all_configs(self):
"""测试获取所有设备配置"""
print("\n=== 测试获取所有设备配置 ===")
try:
response = requests.get(f"{self.api_url}/devices")
result = response.json()
if result['success']:
print("✓ 获取所有设备配置成功")
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
else:
print(f"✗ 获取失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_get_single_config(self, device_name):
"""测试获取单个设备配置"""
print(f"\n=== 测试获取{device_name}设备配置 ===")
try:
response = requests.get(f"{self.api_url}/devices/{device_name}")
result = response.json()
if result['success']:
print(f"✓ 获取{device_name}配置成功")
print(json.dumps(result['data'], indent=2, ensure_ascii=False))
else:
print(f"✗ 获取失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_set_imu_config(self):
"""测试设置IMU配置"""
print("\n=== 测试设置IMU配置 ===")
try:
data = {
"device_type": "real",
"port": "COM6",
"baudrate": 9600
}
response = requests.post(f"{self.api_url}/devices/imu", json=data)
result = response.json()
if result['success']:
print("✓ 设置IMU配置成功")
print(f"消息: {result['message']}")
print("更新后的配置:")
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
else:
print(f"✗ 设置失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_set_pressure_config(self):
"""测试设置压力板配置"""
print("\n=== 测试设置压力板配置 ===")
try:
data = {
"device_type": "real",
"use_mock": False,
"port": "COM5",
"baudrate": 115200
}
response = requests.post(f"{self.api_url}/devices/pressure", json=data)
result = response.json()
if result['success']:
print("✓ 设置压力板配置成功")
print(f"消息: {result['message']}")
print("更新后的配置:")
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
else:
print(f"✗ 设置失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_set_camera_config(self):
"""测试设置相机配置"""
print("\n=== 测试设置相机配置 ===")
try:
data = {
"device_index": 0,
"width": 1280,
"height": 720,
"fps": 30
}
response = requests.post(f"{self.api_url}/devices/camera", json=data)
result = response.json()
if result['success']:
print("✓ 设置相机配置成功")
print(f"消息: {result['message']}")
print("更新后的配置:")
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
else:
print(f"✗ 设置失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_set_femtobolt_config(self):
"""测试设置FemtoBolt配置"""
print("\n=== 测试设置FemtoBolt配置 ===")
try:
data = {
"color_resolution": "1080P",
"depth_mode": "NFOV_UNBINNED",
"fps": 30,
"depth_range_min": 1200,
"depth_range_max": 1500
}
response = requests.post(f"{self.api_url}/devices/femtobolt", json=data)
result = response.json()
if result['success']:
print("✓ 设置FemtoBolt配置成功")
print(f"消息: {result['message']}")
print("更新后的配置:")
print(json.dumps(result['config'], indent=2, ensure_ascii=False))
else:
print(f"✗ 设置失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_validate_config(self):
"""测试验证配置"""
print("\n=== 测试验证配置 ===")
try:
response = requests.get(f"{self.api_url}/validate")
result = response.json()
if result['success']:
print("✓ 配置验证成功")
validation_result = result['data']
print(f"配置有效性: {validation_result['valid']}")
if validation_result['errors']:
print(f"错误: {validation_result['errors']}")
if validation_result['warnings']:
print(f"警告: {validation_result['warnings']}")
else:
print(f"✗ 验证失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def test_reload_config(self):
"""测试重新加载配置"""
print("\n=== 测试重新加载配置 ===")
try:
response = requests.post(f"{self.api_url}/reload")
result = response.json()
if result['success']:
print("✓ 重新加载配置成功")
print(f"消息: {result['message']}")
else:
print(f"✗ 重新加载失败: {result['message']}")
except Exception as e:
print(f"✗ 请求异常: {e}")
def run_all_tests(self):
"""运行所有测试"""
print("开始配置API功能测试...")
print(f"API地址: {self.api_url}")
# 等待服务启动
print("\n等待API服务启动...")
time.sleep(2)
# 运行测试
self.test_get_all_configs()
# 测试获取单个设备配置
for device in ['imu', 'pressure', 'camera', 'femtobolt']:
self.test_get_single_config(device)
# 测试设置配置
self.test_set_imu_config()
self.test_set_pressure_config()
self.test_set_camera_config()
self.test_set_femtobolt_config()
# 测试其他功能
self.test_validate_config()
self.test_reload_config()
print("\n=== 测试完成 ===")
if __name__ == "__main__":
# 创建测试器并运行测试
tester = ConfigAPITester()
tester.run_all_tests()

View File

@ -102,8 +102,8 @@ class ConfigManager:
'pressure_baudrate': '115200' 'pressure_baudrate': '115200'
} }
# 默认相机配置 # 默认相机1配置
self.config['CAMERA'] = { self.config['CAMERA1'] = {
'device_index': '0', 'device_index': '0',
'width': '1280', 'width': '1280',
'height': '720', 'height': '720',
@ -111,6 +111,15 @@ class ConfigManager:
'backend': 'directshow' 'backend': 'directshow'
} }
# 默认相机2配置
self.config['CAMERA2'] = {
'device_index': '1',
'width': '1280',
'height': '720',
'fps': '30',
'backend': 'directshow'
}
# 默认FemtoBolt配置 # 默认FemtoBolt配置
self.config['FEMTOBOLT'] = { self.config['FEMTOBOLT'] = {
'color_resolution': '1080P', 'color_resolution': '1080P',
@ -134,7 +143,7 @@ class ConfigManager:
获取设备配置 获取设备配置
Args: Args:
device_name: 设备名称 (camera, femtobolt, imu, pressure) device_name: 设备名称 (camera1, camera2, femtobolt, imu, pressure)
Returns: Returns:
Dict[str, Any]: 设备配置字典 Dict[str, Any]: 设备配置字典
@ -144,8 +153,10 @@ class ConfigManager:
config = {} config = {}
if device_name == 'camera': if device_name == 'camera1':
config = self._get_camera_config() config = self._get_camera1_config()
elif device_name == 'camera2':
config = self._get_camera2_config()
elif device_name == 'femtobolt': elif device_name == 'femtobolt':
config = self._get_femtobolt_config() config = self._get_femtobolt_config()
elif device_name == 'imu': elif device_name == 'imu':
@ -159,7 +170,7 @@ class ConfigManager:
self._device_configs[device_name] = config self._device_configs[device_name] = config
return config.copy() return config.copy()
def _get_camera_config(self) -> Dict[str, Any]: def _get_camera1_config(self) -> Dict[str, Any]:
""" """
获取相机配置 获取相机配置
@ -167,16 +178,32 @@ class ConfigManager:
Dict[str, Any]: 相机配置 Dict[str, Any]: 相机配置
""" """
return { return {
'enabled': self.config.getboolean('CAMERA', 'enabled', fallback=True), 'enabled': self.config.getboolean('CAMERA1', 'enabled', fallback=True),
'device_index': self.config.getint('CAMERA', 'device_index', fallback=0), 'device_index': self.config.getint('CAMERA1', 'device_index', fallback=0),
'width': self.config.getint('CAMERA', 'width', fallback=1280), 'width': self.config.getint('CAMERA1', 'width', fallback=1280),
'height': self.config.getint('CAMERA', 'height', fallback=720), 'height': self.config.getint('CAMERA1', 'height', fallback=720),
'fps': self.config.getint('CAMERA', 'fps', fallback=30), 'fps': self.config.getint('CAMERA1', 'fps', fallback=30),
'buffer_size': self.config.getint('CAMERA', 'buffer_size', fallback=1), 'buffer_size': self.config.getint('CAMERA1', 'buffer_size', fallback=1),
'fourcc': self.config.get('CAMERA', 'fourcc', fallback='MJPG'), 'fourcc': self.config.get('CAMERA1', 'fourcc', fallback='MJPG'),
'backend': self.config.get('CAMERA', 'backend', fallback='directshow') 'backend': self.config.get('CAMERA1', 'backend', fallback='directshow')
} }
def _get_camera2_config(self) -> Dict[str, Any]:
"""
获取相机配置
Returns:
Dict[str, Any]:
"""
return {
'enabled': self.config.getboolean('CAMERA2', 'enabled', fallback=True),
'device_index': self.config.getint('CAMERA2', 'device_index', fallback=0),
'width': self.config.getint('CAMERA2', 'width', fallback=1280),
'height': self.config.getint('CAMERA2', 'height', fallback=720),
'fps': self.config.getint('CAMERA2', 'fps', fallback=30),
'buffer_size': self.config.getint('CAMERA2', 'buffer_size', fallback=1),
'fourcc': self.config.get('CAMERA2', 'fourcc', fallback='MJPG'),
'backend': self.config.get('CAMERA2', 'backend', fallback='directshow')
}
def _get_femtobolt_config(self) -> Dict[str, Any]: def _get_femtobolt_config(self) -> Dict[str, Any]:
""" """
获取FemtoBolt配置 获取FemtoBolt配置
@ -332,7 +359,7 @@ class ConfigManager:
warnings = [] warnings = []
# 验证必需的配置段 # 验证必需的配置段
required_sections = ['DEVICES', 'CAMERA', 'FEMTOBOLT', 'SYSTEM'] required_sections = ['DEVICES', 'CAMERA1', 'CAMERA2', 'FEMTOBOLT', 'SYSTEM']
for section in required_sections: for section in required_sections:
if not self.config.has_section(section): if not self.config.has_section(section):
errors.append(f"缺少必需的配置段: {section}") errors.append(f"缺少必需的配置段: {section}")
@ -438,7 +465,7 @@ class ConfigManager:
'message': f'设置压力板配置失败: {str(e)}' 'message': f'设置压力板配置失败: {str(e)}'
} }
def set_camera_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: def set_camera1_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
""" """
设置相机设备配置 设置相机设备配置
@ -457,15 +484,15 @@ class ConfigManager:
try: try:
# 验证必需参数 # 验证必需参数
if 'device_index' in config_data: if 'device_index' in config_data:
self.set_config_value('CAMERA', 'device_index', str(config_data['device_index'])) self.set_config_value('CAMERA1', 'device_index', str(config_data['device_index']))
if 'width' in config_data: if 'width' in config_data:
self.set_config_value('CAMERA', 'width', str(config_data['width'])) self.set_config_value('CAMERA1', 'width', str(config_data['width']))
if 'height' in config_data: if 'height' in config_data:
self.set_config_value('CAMERA', 'height', str(config_data['height'])) self.set_config_value('CAMERA1', 'height', str(config_data['height']))
if 'fps' in config_data: if 'fps' in config_data:
self.set_config_value('CAMERA', 'fps', str(config_data['fps'])) self.set_config_value('CAMERA1', 'fps', str(config_data['fps']))
if 'backend' in config_data: if 'backend' in config_data:
self.set_config_value('CAMERA', 'backend', str(config_data['backend'])) self.set_config_value('CAMERA1', 'backend', str(config_data['backend']))
# 保存配置 # 保存配置
self.save_config() self.save_config()
@ -474,7 +501,7 @@ class ConfigManager:
return { return {
'success': True, 'success': True,
'message': '相机配置更新成功', 'message': '相机配置更新成功',
'config': self.get_device_config('camera') 'config': self.get_device_config('camera1')
} }
except Exception as e: except Exception as e:
self.logger.error(f"设置相机配置失败: {e}") self.logger.error(f"设置相机配置失败: {e}")
@ -482,7 +509,50 @@ class ConfigManager:
'success': False, 'success': False,
'message': f'设置相机配置失败: {str(e)}' 'message': f'设置相机配置失败: {str(e)}'
} }
def set_camera2_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
"""
设置相机设备配置
Args:
config_data: 相机配置数据
{
'device_index': 1,
'width': 1280,
'height': 720,
'fps': 30
}
Returns:
Dict[str, Any]: 设置结果
"""
try:
# 验证必需参数
if 'device_index' in config_data:
self.set_config_value('CAMERA2', 'device_index', str(config_data['device_index']))
if 'width' in config_data:
self.set_config_value('CAMERA2', 'width', str(config_data['width']))
if 'height' in config_data:
self.set_config_value('CAMERA2', 'height', str(config_data['height']))
if 'fps' in config_data:
self.set_config_value('CAMERA2', 'fps', str(config_data['fps']))
if 'backend' in config_data:
self.set_config_value('CAMERA2', 'backend', str(config_data['backend']))
# 保存配置
self.save_config()
self.logger.info(f"相机配置已更新: {config_data}")
return {
'success': True,
'message': '相机配置更新成功',
'config': self.get_device_config('camera2')
}
except Exception as e:
self.logger.error(f"设置相机配置失败: {e}")
return {
'success': False,
'message': f'设置相机配置失败: {str(e)}'
}
def set_femtobolt_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: def set_femtobolt_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
""" """
设置FemtoBolt设备配置 设置FemtoBolt设备配置
@ -541,7 +611,8 @@ class ConfigManager:
return { return {
'imu': self.get_device_config('imu'), 'imu': self.get_device_config('imu'),
'pressure': self.get_device_config('pressure'), 'pressure': self.get_device_config('pressure'),
'camera': self.get_device_config('camera'), 'camera1': self.get_device_config('camera1'),
'camera2': self.get_device_config('camera2'),
'femtobolt': self.get_device_config('femtobolt') 'femtobolt': self.get_device_config('femtobolt')
} }
@ -610,27 +681,27 @@ class ConfigManager:
self.logger.error(error_msg) self.logger.error(error_msg)
# 相机配置 # 相机配置
if 'camera' in configs: if 'camera1' in configs:
try: try:
config_data = configs['camera'] config_data = configs['camera1']
if 'device_index' in config_data: if 'device_index' in config_data:
self.set_config_value('CAMERA', 'device_index', str(config_data['device_index'])) self.set_config_value('CAMERA1', 'device_index', str(config_data['device_index']))
if 'width' in config_data: if 'width' in config_data:
self.set_config_value('CAMERA', 'width', str(config_data['width'])) self.set_config_value('CAMERA1', 'width', str(config_data['width']))
if 'height' in config_data: if 'height' in config_data:
self.set_config_value('CAMERA', 'height', str(config_data['height'])) self.set_config_value('CAMERA1', 'height', str(config_data['height']))
if 'fps' in config_data: if 'fps' in config_data:
self.set_config_value('CAMERA', 'fps', str(config_data['fps'])) self.set_config_value('CAMERA1', 'fps', str(config_data['fps']))
if 'buffer_size' in config_data: if 'buffer_size' in config_data:
self.set_config_value('CAMERA', 'buffer_size', str(config_data['buffer_size'])) self.set_config_value('CAMERA1', 'buffer_size', str(config_data['buffer_size']))
if 'fourcc' in config_data: if 'fourcc' in config_data:
self.set_config_value('CAMERA', 'fourcc', config_data['fourcc']) self.set_config_value('CAMERA1', 'fourcc', config_data['fourcc'])
if 'tx_max_width' in config_data: if 'tx_max_width' in config_data:
self.set_config_value('CAMERA', 'tx_max_width', str(config_data['tx_max_width'])) self.set_config_value('CAMERA1', 'tx_max_width', str(config_data['tx_max_width']))
if 'backend' in config_data: if 'backend' in config_data:
self.set_config_value('CAMERA', 'backend', str(config_data['backend'])) self.set_config_value('CAMERA1', 'backend', str(config_data['backend']))
results['camera'] = { results['camera1'] = {
'success': True, 'success': True,
'message': '相机配置更新成功', 'message': '相机配置更新成功',
'config': config_data 'config': config_data
@ -638,10 +709,42 @@ class ConfigManager:
self.logger.info(f"相机配置已更新: {config_data}") self.logger.info(f"相机配置已更新: {config_data}")
except Exception as e: except Exception as e:
error_msg = f'设置相机配置失败: {str(e)}' error_msg = f'设置相机配置失败: {str(e)}'
results['camera'] = {'success': False, 'message': error_msg} results['camera1'] = {'success': False, 'message': error_msg}
errors.append(f"相机: {error_msg}") errors.append(f"相机: {error_msg}")
self.logger.error(error_msg) self.logger.error(error_msg)
if 'camera2' in configs:
try:
config_data = configs['camera2']
if 'device_index' in config_data:
self.set_config_value('CAMERA2', 'device_index', str(config_data['device_index']))
if 'width' in config_data:
self.set_config_value('CAMERA2', 'width', str(config_data['width']))
if 'height' in config_data:
self.set_config_value('CAMERA2', 'height', str(config_data['height']))
if 'fps' in config_data:
self.set_config_value('CAMERA2', 'fps', str(config_data['fps']))
if 'buffer_size' in config_data:
self.set_config_value('CAMERA2', 'buffer_size', str(config_data['buffer_size']))
if 'fourcc' in config_data:
self.set_config_value('CAMERA2', 'fourcc', config_data['fourcc'])
if 'tx_max_width' in config_data:
self.set_config_value('CAMERA2', 'tx_max_width', str(config_data['tx_max_width']))
if 'backend' in config_data:
self.set_config_value('CAMERA2', 'backend', str(config_data['backend']))
results['camera2'] = {
'success': True,
'message': '相机配置更新成功',
'config': config_data
}
self.logger.info(f"相机配置已更新: {config_data}")
except Exception as e:
error_msg = f'设置相机配置失败: {str(e)}'
results['camera2'] = {'success': False, 'message': error_msg}
errors.append(f"相机2: {error_msg}")
self.logger.error(error_msg)
# FemtoBolt配置 # FemtoBolt配置
if 'femtobolt' in configs: if 'femtobolt' in configs:
try: try:
@ -703,7 +806,8 @@ class ConfigManager:
{ {
'imu': {'device_type': 'real', 'port': 'COM7', 'baudrate': 9600}, 'imu': {'device_type': 'real', 'port': 'COM7', 'baudrate': 9600},
'pressure': {'device_type': 'real', 'port': 'COM8', 'baudrate': 115200}, 'pressure': {'device_type': 'real', 'port': 'COM8', 'baudrate': 115200},
'camera': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30}, 'camera1': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30},
'camera2': {'device_index': 1, 'width': 1280, 'height': 720, 'fps': 30},
'femtobolt': {'color_resolution': '1080P', 'depth_mode': 'NFOV_UNBINNED', 'fps': 15} 'femtobolt': {'color_resolution': '1080P', 'depth_mode': 'NFOV_UNBINNED', 'fps': 15}
} }

View File

@ -25,6 +25,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 导入模块 # 导入模块
from database import DatabaseManager from database import DatabaseManager
from utils import config as app_config from utils import config as app_config
from utils import DataValidator # 添加数据验证器导入
from devices.camera_manager import CameraManager from devices.camera_manager import CameraManager
from devices.imu_manager import IMUManager from devices.imu_manager import IMUManager
from devices.pressure_manager import PressureManager from devices.pressure_manager import PressureManager
@ -163,8 +164,9 @@ class AppServer:
config_path = os.path.join(os.path.dirname(__file__), 'config.ini') config_path = os.path.join(os.path.dirname(__file__), 'config.ini')
self.config.read(config_path, encoding='utf-8') self.config.read(config_path, encoding='utf-8')
device_index = self.config.get('CAMERA', 'device_index', fallback=None) camera1_index = self.config.get('CAMERA1', 'device_index', fallback=None)
print(f"设备号: {device_index}") camera2_index = self.config.get('CAMERA2', 'device_index', fallback=None)
print(f"相机1设备号: {camera1_index}, 相机2设备号: {camera2_index}")
def init_app(self): def init_app(self):
"""初始化应用组件""" """初始化应用组件"""
@ -243,16 +245,21 @@ class AppServer:
# 初始化录制管理器 # 初始化录制管理器
self.logger.info('正在初始化录制管理器...') self.logger.info('正在初始化录制管理器...')
camera_manager = self.device_managers.get('camera') camera1_manager = self.device_managers.get('camera1')
if camera_manager: camera2_manager = self.device_managers.get('camera2')
self.recording_manager = RecordingManager( femtobolt_manager = self.device_managers.get('femtobolt')
camera_manager=camera_manager, pressure_manager = self.device_managers.get('pressure')
db_manager=self.db_manager
) # 录制管理器当前采用屏幕区域截取方式进行相机录制,不依赖 CameraManager
self.logger.info('录制管理器初始化完成') # 但保留其他设备管理器以便后续扩展如FemtoBolt、压力传感器
else: self.recording_manager = RecordingManager(
self.recording_manager = None camera_manager=None,
self.logger.warning('相机设备未初始化,录制管理器将不可用') db_manager=self.db_manager,
femtobolt_manager=femtobolt_manager,
pressure_manager=pressure_manager,
config_manager=self.config_manager
)
self.logger.info('录制管理器初始化完成')
# 启动Flask应用 # 启动Flask应用
host = self.host host = self.host
@ -895,14 +902,15 @@ class AppServer:
if status not in ['approved', 'rejected']: if status not in ['approved', 'rejected']:
return jsonify({'success': False, 'error': '无效的审核状态'}), 400 return jsonify({'success': False, 'error': '无效的审核状态'}), 400
result = self.db_manager.update_user_status(user_id, status) # 使用数据库层已有的审核方法
if result: try:
self.db_manager.approve_user(user_id, approved=(status == 'approved'))
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'用户已{"通过" if status == "approved" else "拒绝"}审核' 'message': f'用户已{"通过" if status == "approved" else "拒绝"}审核'
}) })
else: except Exception:
return jsonify({'success': False, 'error': '用户不存在'}), 404 return jsonify({'success': False, 'error': '用户不存在或审核失败'}), 404
except Exception as e: except Exception as e:
self.logger.error(f'审核用户失败: {e}') self.logger.error(f'审核用户失败: {e}')
@ -952,47 +960,51 @@ class AppServer:
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
elif flask_request.method == 'POST': elif flask_request.method == 'POST':
# 创建患者 # 创建患者
try: try:
# 检查Content-Type data = flask_request.get_json()
if not flask_request.is_json:
return jsonify({'success': False, 'message': '请求Content-Type必须为application/json'}), 415
data = flask_request.get_json(force=True) # 验证患者数据
validation_result = DataValidator.validate_patient_data(data)
required_fields = ['name', 'gender', 'age'] if not validation_result['valid']:
for field in required_fields: return jsonify({
if not data.get(field): 'success': False,
return jsonify({'success': False, 'error': f'{field}不能为空'}), 400 'error': '; '.join(validation_result['errors'])
}), 400
# 准备患者数据
patient_data = { patient_data = {
'name': data['name'], 'name': validation_result['data'].get('name'),
'gender': data['gender'], 'gender': validation_result['data'].get('gender'),
'age': data['age'], 'birth_date': validation_result['data'].get('birth_date'),
'birth_date': data.get('birth_date'), 'nationality': validation_result['data'].get('nationality'),
'nationality': data.get('nationality'), 'residence': validation_result['data'].get('residence'),
'residence': data.get('residence'), 'height': validation_result['data'].get('height'),
'height': data.get('height'), 'weight': validation_result['data'].get('weight'),
'weight': data.get('weight'), 'shoe_size': validation_result['data'].get('shoe_size'),
'shoe_size': data.get('shoe_size'), 'phone': validation_result['data'].get('phone'),
'phone': data.get('phone'), 'email': validation_result['data'].get('email'),
'email': data.get('email'), 'occupation': validation_result['data'].get('occupation'),
'occupation': data.get('occupation'), 'workplace': validation_result['data'].get('workplace'),
'workplace': data.get('workplace'), 'idcode': validation_result['data'].get('idcode'),
'medical_history': data.get('medical_history', ''), 'medical_history': validation_result['data'].get('medical_history'),
'notes': data.get('notes', '') 'notes': validation_result['data'].get('notes')
} }
# 创建患者
patient_id = self.db_manager.create_patient(patient_data) patient_id = self.db_manager.create_patient(patient_data)
if patient_id: # 获取创建的患者信息
return jsonify({ patient = self.db_manager.get_patient(patient_id)
'success': True,
'message': '患者创建成功', return jsonify({
'data': {'patient_id': patient_id} 'success': True,
}) 'message': '患者创建成功',
else: 'data': {
return jsonify({'success': False, 'error': '患者创建失败'}), 500 'patient_id': patient_id,
'patient': patient
}
})
except Exception as e: except Exception as e:
self.logger.error(f'创建患者失败: {e}') self.logger.error(f'创建患者失败: {e}')
@ -1005,10 +1017,7 @@ class AppServer:
# 获取患者详情 # 获取患者详情
try: try:
patient = self.db_manager.get_patient(patient_id) patient = self.db_manager.get_patient(patient_id)
if patient: return jsonify({'success': True, 'data': patient})
return jsonify({'success': True, 'data': patient})
else:
return jsonify({'success': False, 'error': '患者不存在'}), 404
except Exception as e: except Exception as e:
self.logger.error(f'获取患者详情失败: {e}') self.logger.error(f'获取患者详情失败: {e}')
@ -1033,17 +1042,13 @@ class AppServer:
'email': data.get('email'), 'email': data.get('email'),
'occupation': data.get('occupation'), 'occupation': data.get('occupation'),
'workplace': data.get('workplace'), 'workplace': data.get('workplace'),
'idcode': data.get('idcode'),
'medical_history': data.get('medical_history'), 'medical_history': data.get('medical_history'),
'notes': data.get('notes') 'notes': data.get('notes')
} }
self.db_manager.update_patient(patient_id, patient_data) self.db_manager.update_patient(patient_id, patient_data)
result = True return jsonify({'success': True, 'message': '患者信息更新成功'})
if result:
return jsonify({'success': True, 'message': '患者信息更新成功'})
else:
return jsonify({'success': False, 'error': '患者不存在'}), 404
except Exception as e: except Exception as e:
self.logger.error(f'更新患者信息失败: {e}') self.logger.error(f'更新患者信息失败: {e}')
@ -1053,10 +1058,7 @@ class AppServer:
# 删除患者 # 删除患者
try: try:
result = self.db_manager.delete_patient(patient_id) result = self.db_manager.delete_patient(patient_id)
if result: return jsonify({'success': True, 'message': '患者已删除'})
return jsonify({'success': True, 'message': '患者已删除'})
else:
return jsonify({'success': False, 'error': '患者不存在'}), 404
except Exception as e: except Exception as e:
self.logger.error(f'删除患者失败: {e}') self.logger.error(f'删除患者失败: {e}')
@ -1078,19 +1080,6 @@ class AppServer:
self.logger.error(f'获取设备状态失败: {e}') self.logger.error(f'获取设备状态失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@self.app.route('/api/devices/refresh', methods=['POST'])
def refresh_devices():
"""刷新设备"""
try:
if self.device_coordinator:
result = self.device_coordinator.refresh_all_devices()
return jsonify({'success': True, 'data': result})
else:
return jsonify({'success': False, 'error': '设备协调器未初始化'}), 500
except Exception as e:
self.logger.error(f'刷新设备失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 设备配置API ==================== # ==================== 设备配置API ====================
@ -1223,6 +1212,19 @@ class AppServer:
self.logger.error(f'开始检测失败: {e}') self.logger.error(f'开始检测失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@self.app.route('/api/detection/<session_id>/has_data', methods=['GET'])
def has_session_detection_data(session_id: str):
"""检查指定会话是否存在检测数据,用于判断单次检测是否有效"""
try:
if not self.db_manager:
return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500
exists = self.db_manager.has_session_detection_data(session_id)
return jsonify({'success': True, 'session_id': session_id, 'has_data': bool(exists)})
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', methods=['POST']) @self.app.route('/api/detection/<session_id>/stop', methods=['POST'])
def stop_detection(session_id): def stop_detection(session_id):
"""停止检测""" """停止检测"""
@ -1246,24 +1248,22 @@ class AppServer:
'message': '空白会话已删除' 'message': '空白会话已删除'
}) })
else: else:
# 正常会话的停止流程 # 正常会话的停止流程:调用数据库层结束检测,自动计算时长并写入结束信息
# 如果提供了duration更新到数据库
data = flask_request.get_json() or {} data = flask_request.get_json() or {}
duration = data.get('duration') diagnosis_info = data.get('diagnosis_info')
if duration is not None and isinstance(duration, (int, float)): treatment_info = data.get('treatment_info')
try: suggestion_info = data.get('suggestion_info')
self.db_manager.update_session_duration(session_id, int(duration)) success = self.db_manager.update_session_endcheck(
self.logger.info(f'更新会话持续时间: {session_id} -> {duration}') session_id,
except Exception as duration_error: diagnosis_info=diagnosis_info,
self.logger.error(f'更新会话持续时间失败: {duration_error}') treatment_info=treatment_info,
suggestion_info=suggestion_info
# 更新会话状态为已完成 )
success = self.db_manager.update_session_status(session_id, 'completed')
if success: if success:
self.logger.info(f'检测会话已停止 - 会话ID: {session_id}') self.logger.info(f'检测会话已结束检查 - 会话ID: {session_id}')
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '检测已停止' 'message': '检测已结束并已写入总结信息'
}) })
else: else:
self.logger.error('停止检测失败,更新会话状态失败') self.logger.error('停止检测失败,更新会话状态失败')
@ -1285,31 +1285,34 @@ class AppServer:
data = flask_request.get_json() data = flask_request.get_json()
patient_id = data.get('patient_id') patient_id = data.get('patient_id')
screen_location = data.get('screen_location') # [0,0,1920,1080] 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] femtobolt_location = data.get('femtobolt_location') # [0,0,640,480]
camera1_location = data.get('camera1_location') # [0,0,640,480]
camera2_location = data.get('camera2_location') # [0,0,640,480]
if not patient_id: if not patient_id:
return jsonify({'success': False, 'error': '缺少患者ID'}), 400 return jsonify({'success': False, 'error': '缺少患者ID'}), 400
# 开始视频录制 # 开始视频录制
recording_response = None recording_response = None
try: try:
recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera_location,femtobolt_location) recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera1_location,camera2_location,femtobolt_location)
# 处理录制管理器返回的数据库更新信息 # 处理录制管理器返回的数据库更新信息
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']
try: try:
# 更新会话状态 # 保存检测视频记录(映射到 detection_video 表字段)
if not self.db_manager.update_session_status(db_updates['session_id'], db_updates['status']): video_paths = db_updates.get('video_paths', {})
self.logger.error(f'更新会话状态失败 - 会话ID: {db_updates["session_id"]}, 状态: {db_updates["status"]}') video_record = {
'screen_video_path': video_paths.get('screen_video_path'),
'femtobolt_video_path': video_paths.get('femtobolt_video_path'),
'camera1_video_path': video_paths.get('camera1_video_path'),
'camera2_video_path': video_paths.get('camera2_video_path'),
}
# 更新视频文件路径 try:
video_paths = db_updates['video_paths'] self.db_manager.save_detection_video(db_updates['session_id'], video_record)
self.db_manager.update_session_normal_video_path(db_updates['session_id'], video_paths['normal_video_path']) except Exception as video_err:
self.db_manager.update_session_screen_video_path(db_updates['session_id'], video_paths['screen_video_path']) self.logger.error(f'保存检测视频记录失败: {video_err}')
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"]}') self.logger.info(f'数据库更新成功 - 会话ID: {db_updates["session_id"]}')
except Exception as db_error: except Exception as db_error:
@ -1329,55 +1332,20 @@ class AppServer:
def stop_record(session_id): def stop_record(session_id):
"""停止视频录制""" """停止视频录制"""
try: try:
if not self.db_manager or not self.device_coordinator:
self.logger.error('数据库管理器或设备管理器未初始化')
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
if not session_id: if not session_id:
self.logger.error('缺少会话ID') self.logger.error('缺少会话ID')
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': '缺少会话ID' 'error': '缺少会话ID'
}), 400 }), 400
# 停止同步录制,传递视频数据 # 停止同步录制,传递视频数据
try: try:
restrt = self.recording_manager.stop_recording(session_id) restrt = self.recording_manager.stop_recording(session_id)
self.logger.info(f'停止录制结果: {restrt}') self.logger.info(f'停止录制结果: {restrt}')
# 处理录制管理器返回的数据库更新信息
if restrt and restrt.get('success') and 'database_updates' in restrt:
db_updates = restrt['database_updates']
try:
# 更新会话状态
success = self.db_manager.update_session_status(db_updates['session_id'], db_updates['status'])
self.logger.info(f'会话状态已更新为: {db_updates["status"]} - 会话ID: {db_updates["session_id"]}')
except Exception as db_error:
self.logger.error(f'处理停止录制的数据库更新失败: {db_error}')
success = False
else:
# 如果录制管理器没有返回数据库更新信息,则手动更新
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, 'recorded')
raise raise
return jsonify({'success': True,'msg': '停止录制成功'})
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: 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
@ -1475,8 +1443,8 @@ class AppServer:
self.logger.error(f'保存会话信息失败: {e}') self.logger.error(f'保存会话信息失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@self.app.route('/api/detection/<session_id>/collect', methods=['POST']) @self.app.route('/api/detection/<session_id>/save-data', methods=['POST'])
def collect_detection_data(session_id): def save_detection_data(session_id):
"""采集检测数据""" """采集检测数据"""
try: try:
if not self.db_manager: if not self.db_manager:
@ -1506,8 +1474,8 @@ class AppServer:
'error': '无法获取患者ID' 'error': '无法获取患者ID'
}), 400 }), 400
# 调用录制管理器采集数据 # 调用录制管理器保存检测截图到文件
collected_data = self.recording_manager.collect_detection_data( collected_data = self.recording_manager.save_detection_images(
session_id=session_id, session_id=session_id,
patient_id=patient_id, patient_id=patient_id,
detection_data=data detection_data=data
@ -1543,15 +1511,6 @@ class AppServer:
sessions = self.db_manager.get_detection_sessions(page, size, patient_id) sessions = self.db_manager.get_detection_sessions(page, size, patient_id)
total = self.db_manager.get_sessions_count(patient_id) total = self.db_manager.get_sessions_count(patient_id)
# 为每个会话补充最新的检测数据
for session in sessions:
session_id = session.get('id')
if session_id:
latest_data = self.db_manager.get_latest_detection_data(session_id, 5)
session['latest_detection_data'] = latest_data
else:
session['latest_detection_data'] = []
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': { 'data': {
@ -1584,50 +1543,40 @@ class AppServer:
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@self.app.route('/api/detection/data/details', methods=['GET'])
@self.app.route('/api/detection/data/<session_id>/latest', methods=['GET']) def get_detection_data_by_ids():
def get_latest_detection_data(session_id): """根据多个主键ID查询检测数据详情ids为逗号分隔"""
"""获取最新的检测数据"""
try: try:
limit = int(flask_request.args.get('limit', 5)) ids_param = flask_request.args.get('ids')
data = self.db_manager.get_latest_detection_data(session_id, limit) if not ids_param:
return jsonify({'success': False, 'error': '缺少ids参数'}), 400
return jsonify({ ids = [i.strip() for i in ids_param.split(',') if i.strip()]
'success': True, data_list = self.db_manager.get_detection_data_by_ids(ids)
'data': data return jsonify({'success': True, 'data': data_list})
})
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
@self.app.route('/api/detection/data/detail/<data_id>', methods=['GET'])
def get_detection_data_by_id(data_id):
"""根据主键ID查询检测数据详情"""
try:
data = self.db_manager.get_detection_data_by_id(data_id)
if data is None:
return jsonify({'success': False, 'error': '检测数据不存在'}), 404
return jsonify({
'success': True,
'data': data
})
except Exception as e:
self.logger.error(f'获取检测数据详情失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@self.app.route('/api/detection/data/<data_id>', methods=['DELETE']) @self.app.route('/api/detection/data/<data_id>', methods=['DELETE'])
def delete_detection_data(data_id): def delete_detection_data(data_id):
"""删除检测数据记录""" """删除检测数据记录支持单个或多个ID多个用逗号分隔"""
try: try:
self.db_manager.delete_detection_data(data_id) if not data_id:
return jsonify({'success': False, 'error': '未提供检测数据ID'}), 400
return jsonify({ # 支持批量:逗号分隔
'success': True, ids = [i.strip() for i in str(data_id).split(',') if i.strip()]
'message': '检测数据删除成功' payload = ids if len(ids) > 1 else (ids[0] if ids else data_id)
})
success = self.db_manager.delete_detection_data(payload)
if success:
return jsonify({
'success': True,
'message': '检测数据删除成功',
'deleted_ids': ids
})
else:
return jsonify({'success': False, 'error': '检测数据删除失败'}), 500
except ValueError as e: except ValueError as e:
return jsonify({'success': False, 'error': str(e)}), 404 return jsonify({'success': False, 'error': str(e)}), 404
@ -1635,6 +1584,33 @@ class AppServer:
self.logger.error(f'删除检测数据失败: {e}') self.logger.error(f'删除检测数据失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@self.app.route('/api/detection/video/<video_id>', methods=['DELETE'])
def delete_detection_video(video_id):
"""删除检测视频记录支持单个或多个ID多个用逗号分隔"""
try:
if not video_id:
return jsonify({'success': False, 'error': '未提供检测视频ID'}), 400
# 支持批量:逗号分隔
ids = [i.strip() for i in str(video_id).split(',') if i.strip()]
payload = ids if len(ids) > 1 else (ids[0] if ids else video_id)
success = self.db_manager.delete_detection_video(payload)
if success:
return jsonify({
'success': True,
'message': '检测视频删除成功',
'deleted_ids': ids
})
else:
return jsonify({'success': False, 'error': '检测视频删除失败'}), 500
except ValueError as e:
return jsonify({'success': False, 'error': str(e)}), 404
except Exception as e:
self.logger.error(f'删除检测视频失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@self.app.route('/api/detection/sessions/<session_id>', methods=['DELETE']) @self.app.route('/api/detection/sessions/<session_id>', methods=['DELETE'])
def delete_detection_session(session_id): def delete_detection_session(session_id):
"""删除检测会话及其相关的检测数据""" """删除检测会话及其相关的检测数据"""
@ -1687,7 +1663,7 @@ class AppServer:
def handle_subscribe_device(data): def handle_subscribe_device(data):
"""订阅特定设备数据""" """订阅特定设备数据"""
device_type = data.get('device_type') device_type = data.get('device_type')
if device_type in ['camera', 'femtobolt', 'imu', 'pressure']: if device_type in ['camera1', 'camera2', 'femtobolt', 'imu', 'pressure']:
self.logger.info(f'客户端订阅{device_type}设备数据') self.logger.info(f'客户端订阅{device_type}设备数据')
emit('subscription_status', { emit('subscription_status', {
'device_type': device_type, 'device_type': device_type,
@ -2010,7 +1986,7 @@ def main():
"""主函数""" """主函数"""
# 解析命令行参数 # 解析命令行参数
parser = argparse.ArgumentParser(description='Body Balance Evaluation System Backend') parser = argparse.ArgumentParser(description='Body Balance Evaluation System Backend')
parser.add_argument('--host', default='localhost', help='Host address to bind to') parser.add_argument('--host', default='0.0.0.0', help='Host address to bind to')
parser.add_argument('--port', type=int, default=5000, help='Port number to bind to') parser.add_argument('--port', type=int, default=5000, help='Port number to bind to')
parser.add_argument('--debug', action='store_true', help='Enable debug mode') parser.add_argument('--debug', action='store_true', help='Enable debug mode')
args = parser.parse_args() args = parser.parse_args()

View File

@ -222,16 +222,24 @@ class DataValidator:
# 性别验证 # 性别验证
if data.get('gender'): if data.get('gender'):
if data['gender'] not in ['male', 'female', 'other']: # 支持中文和英文性别值
errors.append('性别值无效') gender_map = {'': 'male', '': 'female', 'male': 'male', 'female': 'female'}
gender_value = data['gender'].strip()
if gender_value in gender_map:
data['gender'] = gender_map[gender_value]
else:
errors.append('性别值无效应为男、女、male、female')
# 出生日期验证 # 出生日期验证
if data.get('birth_date'): if data.get('birth_date'):
try: try:
birth_date = datetime.fromisoformat(data['birth_date'].replace('Z', '+00:00')) birth_dt = datetime.fromisoformat(data['birth_date'].replace('Z', '+00:00'))
if birth_date > datetime.now(): birth = birth_dt.date()
today = datetime.now().date()
lower = datetime(1900, 1, 1).date()
if birth > today:
errors.append('出生日期不能是未来时间') errors.append('出生日期不能是未来时间')
if birth_date < datetime(1900, 1, 1): if birth < lower:
errors.append('出生日期过早') errors.append('出生日期过早')
except ValueError: except ValueError:
errors.append('出生日期格式无效') errors.append('出生日期格式无效')

View File

@ -0,0 +1,259 @@
# Web 接口调用说明
本文档基于 `backend/main.py` 中注册的路由,整理对外提供的 Web API 调用方式、参数与返回示例,便于前端或第三方系统集成。
## 总览
- 基础与健康检查
- `GET /health`
- `GET /api/health`
- 授权相关
- `GET /api/license/info`
- `POST /api/license/activation-request`
- `POST /api/license/verify`
- `POST /api/license/activate-package`
- 认证与用户
- `POST /api/auth/login`
- `POST /api/auth/register`
- `POST /api/auth/logout`
- `GET /api/auth/verify`
- `POST /api/auth/forgot-password`
- `GET /api/users`
- `POST /api/users/<user_id>/approve`
- `DELETE /api/users/<user_id>`
- 设备与配置
- `GET /api/devices/status`
- `POST /api/devices/refresh`
- `GET /api/config/devices`
- `POST /api/config/devices/all`
- `POST /api/devices/calibrate`
- `POST /api/devices/calibrate/imu`
- 患者管理
- `GET /api/patients`
- `GET|PUT|DELETE /api/patients/<patient_id>`
- 检测流程
- `POST /api/detection/start`
- `GET /api/detection/<session_id>/has_data`
- `POST /api/detection/<session_id>/start_record`
- `POST /api/detection/<session_id>/stop_record`
- `POST /api/detection/<session_id>/save-data`
- `POST /api/detection/<session_id>/save-info`
- `GET /api/detection/<session_id>/status`
- `POST /api/detection/<session_id>/stop`
- 历史与数据查询
- `GET /api/history/sessions`
- `GET /api/history/sessions/<session_id>`
- `GET /api/detection/data/details?ids=<id1,id2,...>`
- 删除操作
- `DELETE /api/detection/data/<data_id[,data_id2,...]>`
- `DELETE /api/detection/video/<video_id[,video_id2,...]>`
- `DELETE /api/detection/sessions/<session_id>`
## 统一响应约定
- 成功:`{ "success": true, ... }`
- 失败:`{ "success": false, "error": "错误信息" }`
- 时间字段统一使用 ISO 文本或 `YYYY-MM-DD HH:mm:ss` 字符串。
## 基础与授权
### GET /health | GET /api/health
- 功能:健康检查与服务存活状态。
- 示例响应:
```json
{ "status": "healthy", "timestamp": "2024-01-01T12:00:00", "version": "1.0.0" }
```
### GET /api/license/info
- 功能:获取授权状态与基础信息。
- 响应字段:`valid`, `message`, `license_type`, `license_id`, `expires_at`, `features`, `machine_id`
### POST /api/license/activation-request
- 功能:生成离线激活请求文件。
- Body`{ "company_name": "公司名", "contact_info": "联系方式" }`
- 返回:`request_file` 路径与 `content` 文本。
### POST /api/license/verify
- 功能:上传并验证授权文件。
- Form-Data`license_file`(文件)。
- 返回:授权验证结果与解析出的授权信息。
### POST /api/license/activate-package
- 功能:上传激活包进行离线激活。
- 细节见服务端实现,返回激活状态与信息。
## 认证与用户
### POST /api/auth/login
- 功能:用户登录。
- Body`{ "username": "string", "password": "string" }`
### POST /api/auth/register
- 功能:用户注册。
- Body包含用户名、密码、手机号等注册信息。
### POST /api/auth/logout
- 功能:登出当前会话。
### GET /api/auth/verify
- 功能:登录状态校验。
### POST /api/auth/forgot-password
- 功能:忘记密码,根据用户名和手机号找回。
- Body`{ "username": "string", "phone": "string" }`
### GET /api/users
- 功能:获取用户列表。
- Query 可选:分页参数。
### POST /api/users/<user_id>/approve
- 功能:批准用户,使其 `is_active = 1`
- Body 可选:`{ "approved_by": 123 }`
### DELETE /api/users/<user_id>
- 功能:删除用户。
## 患者管理
### GET /api/patients
- 功能:获取患者列表。
- Query 可选:分页筛选。
### POST /api/patients
- 功能:创建新患者。
- Body患者基本信息字段姓名、性别、出生日期等
- 必填字段:`name`(姓名)、`gender`(性别)、`birth_date`(出生日期)
- 可选字段:`phone`(电话)、`email`(邮箱)、`height`(身高)、`weight`(体重)、`nationality`(民族)、`residence`(居住地)、`occupation`(职业)、`workplace`(工作单位)、`idcode`(身份证号)、`medical_history`(病史)、`notes`(备注)
### GET|PUT|DELETE /api/patients/<patient_id>
- 功能:获取/更新/删除单个患者。
- PUT Body患者基本信息字段姓名、性别、出生日期、联系方式等
## 设备与配置
### GET /api/devices/status
- 功能:获取各设备连接与工作状态。
### POST /api/devices/refresh
- 功能:刷新设备状态(重扫/重连)。
### GET /api/config/devices
- 功能:获取当前设备配置。
### POST /api/config/devices/all
- 功能:批量设置设备配置。
- Body设备配置对象集合。
### POST /api/devices/calibrate
- 功能:触发设备标定(通用)。
### POST /api/devices/calibrate/imu
- 功能:仅触发 IMU 设备标定。
## 检测流程
### POST /api/detection/start
- 功能:创建检测会话并启动设备连接监控。
- Body`{ "patient_id": "string", "creator_id": "string" }`
- 返回:`session_id`。
### GET /api/detection/<session_id>/has_data
- 功能:会话是否存在检测数据,用于判断单次检测是否有效。
- 返回:`{ "has_data": true|false }`
### GET /api/detection/<session_id>/status
- 功能:获取会话最新状态与聚合数据(源自 `get_session_data`)。
### POST /api/detection/<session_id>/stop
- 功能:结束检测并写入总结信息(自动计算时长与结束时间)。
- Body 可选:
```json
{
"diagnosis_info": "string",
"treatment_info": "string",
"remark_info": "string"
}
```
- 说明:服务端已统一走 `update_session_endcheck` 数据库流程。
### POST /api/detection/<session_id>/start_record
- 功能:开始同步录制(屏幕/相机/设备流)。
- Body 示例:
```json
{
"patient_id": "p001",
"screen_location": [0,0,1920,1080],
"femtobolt_location": [0,0,640,480],
"camera1_location": [0,0,640,480],
"camera2_location": [0,0,640,480]
}
```
- 返回:`recording` 含 `database_updates.video_paths`;服务端会自动调用 `save_detection_video` 记录视频路径(`screen_video_path`, `femtobolt_video_path`, `camera1_video_path`, `camera2_video_path`)。
### POST /api/detection/<session_id>/stop_record
- 功能:停止同步录制。
### POST /api/detection/<session_id>/save-data
- 功能:采集检测数据与截图并持久化。
- Body检测数据载荷结构由实际采集模块决定
- 返回:`timestamp` 与是否采集成功。
### POST /api/detection/<session_id>/save-info
- 功能:保存会话信息(诊断、处理、建议、状态)。
- Body
```json
{
"diagnosis_info": "string",
"treatment_info": "string",
"remark_info": "string",
"status": "completed|running|..."
}
```
## 历史与数据查询
### GET /api/history/sessions
- 功能:分页获取检测会话历史。
- Query`page`(默认 1、`size`(默认 10、`patient_id`(可选)。
- 返回:`sessions` 与 `total`
### GET /api/history/sessions/<session_id>
- 功能:获取会话详细数据(聚合会话、检测数据、视频)。
### GET /api/detection/data/details?ids=<id1,id2,...>
- 功能:根据多个逗号分隔的 ID 批量获取检测数据详情。
- Query`ids` 逗号分隔字符串。
## 删除操作
### DELETE /api/detection/data/<data_id[,data_id2,...]>
- 功能:删除检测数据记录,支持多个 ID 逗号分隔。
- 返回:`deleted_ids` 列表。
### DELETE /api/detection/video/<video_id[,video_id2,...]>
- 功能:删除检测视频记录,支持多个 ID 逗号分隔。
- 返回:`deleted_ids` 列表。
### DELETE /api/detection/sessions/<session_id>
- 功能:删除检测会话,同时清理关联的检测数据与视频记录。
## 错误码与常见失败
- 400缺少必要参数或请求体格式错误。
- 403授权校验失败或未授权功能。
- 404会话或资源不存在。
- 500服务内部错误设备不可用、数据库失败等
## 典型调用序列(示例)
1. 创建会话:`POST /api/detection/start`
2. 开始录制:`POST /api/detection/<session_id>/start_record`
3. 采集数据:多次 `POST /api/detection/<session_id>/save-data`
4. 停止录制:`POST /api/detection/<session_id>/stop_record`
5. 保存诊断建议:`POST /api/detection/<session_id>/save-info`
6. 结束检测:`POST /api/detection/<session_id>/stop`
7. 校验有效性:`GET /api/detection/<session_id>/has_data`
8. 查询历史:`GET /api/history/sessions`
9. 查看详情:`GET /api/history/sessions/<session_id>`
> 注:具体字段与数据结构以采集模块与数据库模型为准,本文档以主干流程为纲要,建议结合实际返回进行前端适配与校验。

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1881 -82 )">
<path d="M 15.9203959888077 14.5936879851829 L 15.9203959888077 9.2868945127217 C 15.9221961012911 9.26482295156643 15.9221961012911 9.24264146272398 15.9203959888077 9.22056990156871 C 15.9220017133507 9.19738041784726 15.9220017133507 9.17410764417035 15.9203959888077 9.15091816044888 L 14.0364720927064 0.995026809521264 C 13.8850151331878 0.4084253440992 13.3554163441829 -0.00105990688953739 12.7495786070392 0 L 3.17081738213301 0 C 2.55696540559302 5.80938669827447E-05 2.02331072533525 0.421222612478488 1.88059678203786 1.01825442140363 L 0.0165889307122598 9.13765634456156 C 0.0149832061692905 9.16084582828303 0.0149832061692905 9.18411860195991 0.0165889307122598 9.20730808568138 C 0.00842320058131918 9.23321838391806 0.00286597143963263 9.25987953700835 0 9.2868945127217 L 0 14.5936724379193 C 0 15.3264166600063 0.593981351320735 15.9203980098591 1.32669448069123 15.9203980098591 C 1.32669448069123 15.9203980098591 1.32669448105548 15.9203980098591 1.32669448105548 16 L 14.593685960853 16 C 14.5936911432079 15.9203980099199 14.5936963258525 15.9203980099502 14.5937015082077 15.9203980099502 C 15.3264146375782 15.9203980099502 15.9203959888987 15.3264166600974 15.9203959888987 14.5937035325377 Z M 1.49584869436012 8.62355505542087 L 3.17081738213301 1.32669653817167 L 12.746267039875 1.32669653817167 L 14.4245317475484 8.62355505542087 L 11.6086117071895 8.62355505542087 C 11.2422630001633 8.623563650138 10.9452808429667 8.92054580660088 10.9452722482495 9.2868945127217 L 10.9452722482495 10.9452742815956 L 4.97512374092253 10.9452742815956 L 4.97512374092253 9.2868945127217 C 4.97511514596226 8.92053973513232 4.67812351331463 8.62355506390642 4.31176873471895 8.62355505542087 L 1.49584869436012 8.62355505542087 Z M 3.9800990052488 7.29684501519738 C 3.61374422058179 7.2968450152982 3.31675257845859 6.99986034155748 3.31674398349787 6.63350555789654 C 3.31674398349787 6.26714469656511 3.61373814301214 5.9701505377848 3.9800990052488 5.9701505377848 L 11.9402969994705 5.9701505377848 C 12.3053994333072 5.97191897464336 12.6004389839914 6.2683910607167 12.6004389839914 6.6334977764911 C 12.6004389839914 6.99860449226549 12.3053994333072 7.29507657833882 11.9402969994705 7.29684501519738 L 3.9800990052488 7.29684501519738 Z M 4.97512374092253 4.31177078445816 C 4.60876895625574 4.31177077597241 4.31177732109313 4.01478609527105 4.31176873471895 3.64843131161005 C 4.31176874330549 3.2820704624218 4.60876289082921 2.9850763156321 4.97512374092253 2.98507630704557 L 10.9452722482495 2.98507630704557 C 11.3103746820859 2.98684474390417 11.6054142327703 3.28331682997749 11.6054142327703 3.64842354575187 C 11.6054142327703 4.01353026152624 11.3103746820859 4.31000234759958 10.9452722482495 4.31177078445816 L 4.97512374092253 4.31177078445816 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1881 82 )" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1015 -82 )">
<path d="M 1023 82 C 1027.48 82 1031 85.52 1031 90 C 1031 94.48 1027.48 98 1023 98 C 1018.52 98 1015 94.48 1015 90 C 1015 85.52 1018.52 82 1023 82 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" fill-opacity="0" />
<path d="M 1023 82.5 C 1027.2 82.5 1030.5 85.8 1030.5 90 C 1030.5 94.2 1027.2 97.5 1023 97.5 C 1018.8 97.5 1015.5 94.2 1015.5 90 C 1015.5 85.8 1018.8 82.5 1023 82.5 Z " stroke-width="1" stroke="#ff3300" fill="none" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="14px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -8 -9 )">
<path d="M 18 3.09091796875001 C 18 2.63636718750001 17.7545390624998 2.22726562499999 17.3863476562499 2.00000000000001 C 17.0181738281251 1.772734375 16.5681738281251 1.8181640625 16.240904296875 2.13636718750003 L 13.5 4.40908203124998 L 13.5 2.59091796875002 C 13.5 1.18181640624999 12.4772695312499 0 11.168173828125 0 L 2.33182617187504 0 C 1.02273046875007 0.0454492187499937 0 1.18183593750001 0 2.63636718750001 L 0 11.40908203125 C 0 12.81818359375 1.02273046875007 14 2.33182617187504 14 L 11.168173828125 14 C 12.436365234375 14 13.5 12.8636328125 13.5 11.40908203125 L 13.5 9.59091796875002 L 16.240904296875 11.90908203125 C 16.4454609375 12.09091796875 16.6499999999999 12.1363671875 16.8954609375 12.1363671875 C 17.059095703125 12.1363671875 17.2227304687501 12.09091796875 17.386365234375 12 C 17.7545390624998 11.772734375 18 11.3636328125 18 10.90908203125 L 18 3.09091796875001 Z M 12.1909218750002 2.63636718750001 L 12.1909218750002 11.3636328125 C 12.1909218750002 12 11.7409218749999 12.5 11.1681914062501 12.5 L 2.33184374999996 12.5 C 1.75911328124994 12.5 1.30911328125012 12 1.30911328125012 11.3636328125 L 1.30911328125012 2.63636718750001 C 1.30911328125012 2.00000000000001 1.75911328124994 1.49999999999999 2.33184374999996 1.49999999999999 L 11.1681914062501 1.49999999999999 C 11.7409218749999 1.49999999999999 12.1909218750002 2.00000000000001 12.1909218750002 2.63636718750001 Z M 16.6909218750002 10.45455078125 L 13.5 7.77271484374999 L 13.5 6.27271484375001 L 16.7318261718749 3.59091796874999 C 16.690904296875 3.54544921874999 16.690904296875 10.45455078125 16.690904296875 10.45455078125 Z M 4.5 9.33333333333333 L 4.5 4.66666666666667 L 9 4.66666666666667 L 9 9.33333333333333 L 4.5 9.33333333333333 Z " fill-rule="nonzero" fill="#ff3300" stroke="none" transform="matrix(1 0 0 1 8 9 )" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -35 -486 )">
<path d="M 17.9488636363636 9 C 17.9488636363636 4.05814985795453 13.9418501420455 0.051136363636374 9 0.051136363636374 C 4.05814985795453 0.051136363636374 0.051136363636374 4.05814985795453 0.051136363636374 9 C 0.051136363636374 13.9418501420455 4.05814985795453 17.9488636363636 9 17.9488636363636 C 13.9418501420455 17.9488636363636 17.9488636363636 13.9418501420455 17.9488636363636 9 Z M 14.1136363636364 10.2784090909091 C 14.1136363636364 10.6319691051136 13.8279918323864 10.9176136363636 13.4744318181818 10.9176136363636 L 7.72159090909091 10.9176136363636 C 7.36803089488637 10.9176136363636 7.08238636363637 10.6319691051136 7.08238636363637 10.2784090909091 L 7.08238636363637 4.52556818181819 C 7.08238636363637 4.17200816761363 7.36803089488637 3.88636363636363 7.72159090909091 3.88636363636363 C 8.07515092329547 3.88636363636363 8.36079545454547 4.17200816761363 8.36079545454547 4.52556818181819 L 8.36079545454547 9.63920454545453 L 13.4744318181818 9.63920454545453 C 13.8279918323864 9.63920454545453 14.1136363636364 9.92484907670453 14.1136363636364 10.2784090909091 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 35 486 )" />
</g>
</svg>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="14px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -929 -83 )">
<path d="M 15.93125 4.01406249999999 C 15.93125 2.91763139204546 15.0426846590909 2.02904829545454 13.9463423295455 2.02904829545454 L 12.1645063920455 2.02904829545454 C 11.6868607954545 2.02904829545454 11.2775213068181 1.689453125 11.1890802556818 1.21962002840909 L 11.0076349431819 0.245703124999999 C 10.9859019886363 0.127894176136365 10.8836115056819 0.0441406249999972 10.7642045454545 0.0441406249999972 L 5.22031249999998 0.0441406249999972 C 5.1008877840909 0.0441406249999972 4.99850852272732 0.129385653409088 4.9767933238636 0.245703124999999 L 4.79385653409088 1.21803977272727 C 4.70539772727273 1.68796164772727 4.29607599431813 2.0275390625 3.81841264204547 2.0275390625 L 2.0365767045455 2.0275390625 C 0.940234375000045 2.02904829545454 0.0516690340908781 2.91763139204546 0.0516690340908781 4.01406249999999 L 0.0516690340908781 11.9538174715909 C 0.0516690340908781 13.0502485795455 0.94025213068187 13.9388316761364 2.0365767045455 14 L 13.9463423295455 14 C 15.0426846590909 13.9388316761364 15.93125 13.0502485795455 15.93125 11.9538174715909 L 15.93125 4.01406249999999 Z M 2.53284801136363 4.51024502840909 C 2.25836292613633 4.51024502840909 2.0365767045455 4.28854758522728 2.0365767045455 4.01406249999999 C 2.0365767045455 3.87761008522727 2.09245383522727 3.75349786931818 2.18242187500005 3.66354758522728 C 2.27230113636358 3.57357954545454 2.3964133522727 3.5177734375 2.53284801136363 3.5177734375 L 3.52530184659088 3.5177734375 C 3.79978693181818 3.5177734375 4.02157315340912 3.73955965909091 4.02157315340912 4.01404474431818 C 4.02157315340912 4.1504971590909 3.96576704545453 4.27450284090909 3.87579900568187 4.36447088068182 C 3.78584872159092 4.45443892045455 3.6617365056818 4.51022727272728 3.52528409090905 4.51022727272728 L 2.53284801136363 4.51022727272728 Z M 12.2094815340909 8.23203125000001 C 12.2094815340909 10.5612571022727 10.3207208806818 12.4500887784091 7.9915127840909 12.4500887784091 C 5.66226917613642 12.4500887784091 3.77345525568182 10.5612571022727 3.77345525568182 8.23203125000001 C 3.77345525568182 5.90280539772728 5.66228693181813 4.01406249999999 7.9915127840909 4.01406249999999 C 10.3207386363637 4.01406249999999 12.2094815340909 5.90280539772728 12.2094815340909 8.23203125000001 Z M 10.2726207386364 5.95090553977273 C 10.88203125 6.56036931818183 11.2170099431818 7.36988636363637 11.2170099431818 8.23203125000001 C 11.2170099431818 9.09426491477274 10.88203125 9.90379971590909 10.2726207386364 10.5131747159091 C 9.6631569602273 11.12265625 8.85363991477277 11.4576349431818 7.99149502840908 11.4576349431818 C 7.12926136363637 11.4576349431818 6.31972656250002 11.12265625 5.71035156250002 10.5131747159091 C 5.10087002840908 9.90379971590909 4.76589133522725 9.09426491477274 4.76589133522725 8.23203125000001 C 4.76589133522725 7.36990411931819 5.10087002840908 6.56036931818183 5.71035156250002 5.95090553977273 C 6.31974431818185 5.3414950284091 7.12926136363637 5.00651633522727 7.99149502840908 5.00651633522727 C 8.85363991477277 5.00651633522727 9.6631569602273 5.3414950284091 10.2726207386364 5.95090553977273 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 929 83 )" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -373 -138 )">
<path d="M 8.07210783203124 6.46195381835938 L 10.2949266738281 4.23849897460937 C 9.45069847884821 3.20458041024776 8.29262410526201 2.47383480857755 6.99599638281251 2.15687204687501 C 4.48952168164061 1.65570431250001 3.16091835937499 2.15687204687501 0 4.30909493164063 C 1.28411899004271 1.62462621176789 4.02088248674812 -0.0594785244032039 6.99599638281251 0.00401316015626207 C 9.01198728514521 -0.0631719713191501 10.9636875416396 0.718523317659617 12.3759175996094 2.15878003906249 L 13.991992765625 0.542704873046887 L 13.991992765625 6.46195381835938 L 8.07210783203124 6.46195381835938 Z M 5.91988493359372 7.53806526757813 L 3.6970660917969 9.76088410937501 C 4.5414496054143 10.7946271618647 5.69946022307948 11.5253325335117 6.99599638281251 11.8425110371094 C 9.50247108398435 12.3443147734375 10.83107440625 11.8425110371094 13.991992765625 9.69092415429688 C 12.7078737755823 12.3753928741696 9.97111027887684 14.0594976103407 6.99599638281251 13.9960059257812 C 4.9802932407564 14.0629719430221 3.02883496413972 13.28183496075 1.61607516601561 11.8425110371094 L 0 13.4573141992188 L 0 7.53806526757813 L 5.91988493359372 7.53806526757813 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 373 138 )" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1839 -82 )">
<path d="M 15.3518281249999 10.057 L 13.6746718750001 8.74328125 C 13.7077187499999 8.4886875 13.7290937499999 8.234109375 13.7290937499999 7.96203125 C 13.7290937499999 7.69190624999999 13.703828125 7.43731249999999 13.6746718750001 7.180796875 L 15.35375 5.86707812500001 C 15.505328125 5.74851562500001 15.5461406249999 5.532796875 15.4489687499999 5.35789062500001 L 13.857359375 2.604125 C 13.762125 2.42923437499999 13.5464218750001 2.36509375 13.371515625 2.42923437499999 L 11.3892499999999 3.226015625 C 10.9753125 2.9073125 10.52834375 2.644953125 10.0444375 2.444765625 L 9.74128124999993 0.334281250000004 C 9.71603125000001 0.141874999999999 9.55081249999989 0 9.35065624999993 0 L 6.16546874999995 0 C 5.96723437500009 0 5.80009375000009 0.143812499999996 5.77484374999995 0.334265625 L 5.47167187500008 2.44478125000001 C 4.9858281249999 2.643 4.54079687500007 2.91509375 4.12685937500009 3.226015625 L 2.14457812499995 2.42921875 C 1.95996874999992 2.35731250000001 1.75396875000001 2.42921875 1.65873437499999 2.604140625 L 0.0671250000000327 5.35789062500001 C -0.0378124999999727 5.532796875 0.0127187499999764 5.74851562500001 0.162359375000051 5.86707812500001 L 1.84143749999998 7.180796875 C 1.80840624999996 7.43537499999999 1.78703124999993 7.69773437500001 1.78703124999993 7.962046875 C 1.78703124999993 8.22632812500001 1.81228125000007 8.48675 1.84143749999998 8.74328125 L 0.160421875000111 10.057 C 0.00881249999997635 10.175546875 -0.0319843750000928 10.391265625 0.0651875000000928 10.56615625 L 1.65681249999989 13.3199375 C 1.75203125000007 13.49484375 1.96775000000002 13.55896875 2.14265625000007 13.49484375 L 4.12492187499993 12.69803125 C 4.5388593749999 13.016765625 4.9858281249999 13.279125 5.46973437499992 13.47928125 L 5.77289062499995 15.5898125 C 5.79815624999992 15.78025 5.96335937499998 15.9240625 6.16351562499995 15.9240625 L 9.34870312499993 15.9240625 C 9.54693750000001 15.9240625 9.71407812500001 15.78025 9.73932812499993 15.5898125 L 10.0425 13.479296875 C 10.52834375 13.2810625 10.973375 13.008984375 11.3873281250001 12.698046875 L 13.3695781250001 13.49484375 C 13.5541874999999 13.56675 13.7602031250001 13.49484375 13.85540625 13.3199375 L 15.44703125 10.56615625 C 15.542265625 10.391265625 15.5014531249999 10.175546875 15.3518281249999 10.057 Z M 10.5458437499999 7.963984375 C 10.5458437499999 9.50120312499999 9.2962500000001 10.75078125 7.75903124999991 10.75078125 C 6.22181249999994 10.75078125 4.97221874999991 9.50118749999999 4.97221874999991 7.963984375 C 4.97221874999991 6.42678125 6.22181249999994 5.177171875 7.75903124999991 5.177171875 C 9.2962500000001 5.177171875 10.5458437499999 6.426765625 10.5458437499999 7.963984375 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1839 82 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -10 -7 )">
<path d="M 6.85372340217753 8.92622950819672 C 6.93351063622007 9.0218579213627 7.06648935754654 9.0218579213627 7.14627659782247 8.92622950819672 L 9.95212765957447 5.31967213114754 C 10.0452127638797 5.19672131147541 9.9654255298371 5.00546447873976 9.80585106175199 5.00546447873976 L 4.19414893824801 5.00546447873976 C 4.0345744701629 5.00546447873976 3.95478723612035 5.19672131147541 4.04787234042553 5.31967213114754 L 6.85372340217753 8.92622950819672 Z M 10.90908203125 18 C 11.3636328125 18 11.772734375 17.7545390624998 12 17.3863476562499 C 12.227265625 17.0181738281251 12.1818359375 16.5681738281251 11.8636328125 16.240904296875 L 9.59091796875002 13.5 L 11.40908203125 13.5 C 12.81818359375 13.5 14 12.4772695312499 14 11.168173828125 L 14 2.33182617187504 C 13.95455078125 1.02273046875007 12.8181640625 0 11.3636328125 0 L 2.59091796875002 0 C 1.18181640624999 0 0 1.02273046875007 0 2.33182617187504 L 0 11.168173828125 C 0 12.436365234375 1.13636718749999 13.5 2.59091796875002 13.5 L 4.40908203124998 13.5 L 2.09091796874999 16.240904296875 C 1.90908203124999 16.4454609375 1.86363281250003 16.6499999999999 1.86363281250003 16.8954609375 C 1.86363281250003 17.059095703125 1.90908203124999 17.2227304687501 2.00000000000001 17.386365234375 C 2.22726562499999 17.7545390624998 2.63636718750001 18 3.09091796875001 18 L 10.90908203125 18 Z M 11.3636328125 12.1909218750002 L 2.63636718750001 12.1909218750002 C 2.00000000000001 12.1909218750002 1.49999999999999 11.7409218749999 1.49999999999999 11.1681914062501 L 1.49999999999999 2.33184374999996 C 1.49999999999999 1.75911328124994 2.00000000000001 1.30911328125012 2.63636718750001 1.30911328125012 L 11.3636328125 1.30911328125012 C 12 1.30911328125012 12.5 1.75911328124994 12.5 2.33184374999996 L 12.5 11.1681914062501 C 12.5 11.7409218749999 12 12.1909218750002 11.3636328125 12.1909218750002 Z M 3.54544921874999 16.6909218750002 L 6.22728515625001 13.5 L 7.72728515624999 13.5 L 10.40908203125 16.7318261718749 C 10.45455078125 16.690904296875 3.54544921874999 16.690904296875 3.54544921874999 16.690904296875 Z " fill-rule="nonzero" fill="#00cc00" stroke="none" transform="matrix(1 0 0 1 10 7 )" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -36 -136 )">
<path d="M 7.76028865245167 0.0137816773135455 C 6.70033338656769 0.0137816773135455 5.84026964347377 0.873868281681638 5.8402696240504 1.93380070571477 C 5.8402696240504 2.99387023912293 6.70024196089435 3.85381971469268 7.76028865245167 3.85381971469268 C 8.82035818585982 3.85381971469268 9.68030766142956 2.99387023912293 9.68030766142956 1.93380070571477 C 9.68030766142956 0.873754014157456 8.82026676018646 0.0137816967368849 7.76028865245167 0.0137816773135455 Z M 15.4402047177141 5.45976267265193 C 15.4402275789882 4.92979646063536 15.0101614058184 4.50178727771063 14.4802866388985 4.50178727771063 L 1.04026780473065 4.50178727771063 C 0.510301592714082 4.50178727771063 0.0803268646408704 4.93185343145717 0.0803268646408704 5.46172821780041 C 0.0803268646408704 5.99183155861535 0.510301592714082 6.42180628668854 1.04026780473065 6.42180628668854 L 4.97826319708217 6.42180628668854 C 5.20228979627072 6.46182573377071 5.50628670580112 6.5758045688018 5.66634161343232 6.99183764891229 C 5.85229146883633 7.47385366885359 5.76029931370858 8.33586009582181 5.64428634970649 9.06183244345648 L 5.49428772228937 9.91787367058012 C 5.49428772228937 9.92180476087708 5.49223075146754 9.92580441557321 5.49223075146754 9.92980408969268 L 4.27825434219612 16.8197304320615 C 4.18626218706837 17.34181160221 4.53430105965126 17.8397348152624 5.05638222979971 17.9318183960635 C 5.57825768732734 18.023719125518 6.07035284875692 17.6757716786084 6.1622307363605 17.1538047759841 L 7.00227332527626 12.3558599274862 L 7.00227332527626 12.3618022660566 C 7.00227332527626 12.3618022660566 7.25226341721341 11.0617989209254 7.74024463699929 11.0617989209254 L 7.78428660005178 11.0617989209254 C 8.28223267437845 11.0617989209254 8.52228077304904 12.3618022660566 8.52228077304904 12.3618022660566 L 8.52228077304904 12.3578026113605 L 9.36220909444062 17.1537819341333 C 9.45431551709255 17.6757488173342 9.94826193672307 18.0236962642438 10.470228819924 17.9317955347894 C 10.992195703125 17.8396891121374 11.3402345757079 17.3417658796616 11.2462997367058 16.8197075707873 L 10.0302663760359 9.92781568327003 L 10.0302663760359 9.9257587124482 C 10.0302663760359 9.92175905775207 10.0282094052141 9.91775940305594 10.0282094052141 9.91373688708563 L 9.87626807449932 9.05769564053867 C 9.76023226864642 8.33378026372583 9.66826297479282 7.47177383675759 9.85430423644682 6.98973497496546 C 10.0142448765539 6.57383904307666 10.3202301925069 6.45986018862223 10.5422683852728 6.41984074154006 L 14.4802637776243 6.41984074154006 C 15.0102299896409 6.41984074154006 15.4402047177141 5.98975172651934 15.4402047177141 5.45976267265193 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 36 136 )" />
</g>
</svg>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="17px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -500 -616 )">
<path d="M 2.81716015429686 0 C 4.36078439648429 0 5.61137635546879 1.25053867968745 5.61137635546879 2.79327425976567 C 5.61137635546879 4.33683490429689 4.36078611914064 5.58739077539065 2.81716015429686 5.58739077539065 C 1.27435923632811 5.58739077539065 0.0237346347656171 4.33681773046874 0.0237346347656171 2.79327425976567 C 0.0237346347656171 1.25053866210931 1.27435925390625 0 2.81716015429686 0 Z M 7.98573373242186 0 C 8.75754585351558 0 9.3828504375 0.625277074218729 9.3828504375 1.39705825781255 C 9.3828504375 2.16880505859376 8.75754587109373 2.79409932421868 7.98573373242186 2.79409932421868 C 7.21392159375 2.79409932421868 6.58863421874997 2.16882225000006 6.58863421874997 1.39705825781255 C 6.58863421874997 0.625277074218729 7.21392161132815 0 7.98573373242186 0 Z M 12.0362949492187 0.348641050781225 C 12.8081087929688 0.348625582031218 13.4333944628906 0.973919830078103 13.4333944628906 1.74569930859377 C 13.4333944628906 2.51748049218747 12.8081070703125 3.14275756640632 12.0362949492187 3.14275756640632 C 11.264482828125 3.14275756640632 10.6391782441406 2.51748049218747 10.6391782441406 1.74569930859377 C 10.6391782441406 0.973918125000068 11.2644828105468 0.348641050781225 12.0362949492187 0.348641050781225 Z M 15.3191332089845 2.16473306835928 C 16.09092815625 2.16473306835928 16.7162499140625 2.79001186523431 16.7162499140625 3.56179132617183 C 16.7162499140625 4.33357250976565 16.0909453476563 4.95884958398437 15.3191332089845 4.95884958398437 C 14.5473210878906 4.95884958398437 13.9220336953125 4.33357250976565 13.9220336953125 3.56179132617183 C 13.9220336953125 2.79001014257813 14.5473210878906 2.16473306835928 15.3191332089845 2.16473306835928 Z M 7.56585943945311 11.6633984765625 C 6.88489969921869 10.7729433281251 3.48011648437495 10.5110448574219 4.37059395703125 7.15877815429678 C 4.37059395703125 7.15877815429678 5.26107144726564 4.69695111328122 8.66587013085939 4.27791735351559 C 8.66587013085939 4.27791735351559 13.9220336953125 3.91125744140618 14.8272487382812 9.09351147656253 C 15.2692139003906 11.6257295214845 15.0850700507812 17.1108151171875 11.0230336933594 17.9455996054687 C 10.0425116953124 18.1469492929688 7.19918404101566 17.8965070664062 7.56585943945311 15.3299357753906 C 7.69517317968746 14.4222851074219 8.54638143750003 12.9450279902344 7.56585943945311 11.6633984765625 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 500 616 )" />
</g>
</svg>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="12px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1475 -512 )">
<path d="M 12.717571297148 2.13837329676619 L 12.717571297148 3.61361156723547 C 12.717571297148 3.67013410633388 12.6582226310948 3.76904854975606 12.5395252989881 3.9103548975022 C 12.4208279668812 4.05166124524828 12.2936522539098 4.21840273558871 12.1579981600737 4.41057936852343 C 12.0223440662373 4.60275600145809 11.8951683532659 4.8118893961223 11.7764710211591 5.03797955251605 C 11.6577736890526 5.2640697089098 11.598425022999 5.49581211921327 11.598425022999 5.73320678342674 C 11.598425022999 5.97060144764021 11.6577736890526 6.19951773098893 11.7764710211591 6.4199556334728 C 11.8951683532659 6.64039353595672 12.0223440662373 6.84104854975612 12.1579981600737 7.02192067487113 C 12.2936522539098 7.20279279998613 12.4208279668812 7.35257752859707 12.5395252989881 7.47127486070366 C 12.6582226310948 7.58997219281036 12.717571297148 7.66627762059329 12.717571297148 7.70019114405238 L 12.717571297148 9.48065112565303 C 12.717571297148 9.67282775858774 12.6469181232751 9.89043953411681 12.5056117755291 10.13348645224 C 12.3643054277829 10.3765333703633 12.1890855565778 10.605449653712 11.9799521619134 10.8202353022861 C 11.7708187672495 11.0350209508601 11.5390763569458 11.2130669490202 11.2847249310028 11.3543732967662 C 11.0303735050597 11.4956796445123 10.7901527138915 11.5663328183854 10.5640625574977 11.5663328183854 L 2.13655197792104 11.5663328183854 C 1.83133026678934 11.5663328183854 1.55154369825209 11.5126364062418 1.2971922723093 11.4052435819548 C 1.04284084636606 11.2978507576679 0.81957681692711 11.148066029057 0.627400183992449 10.9558893961223 C 0.435223551057788 10.7637127631876 0.285438822447077 10.5376226067938 0.178045998160087 10.277618926941 C 0.07065317387287 10.0176152470882 0.0169567617294888 9.73500255159604 0.0169567617294888 9.42978084046445 L 0.0169567617294888 2.1214165350367 C 0.0169567617294888 1.89532637864295 0.0678270469181825 1.66075784138445 0.169567617295343 1.41771092326121 C 0.271308187672503 1.17466400513791 0.415440662373612 0.951399975699132 0.601965041398216 0.747918834944755 C 0.788489420423275 0.544437694190378 1.00892732290708 0.374870076895036 1.2632787488501 0.239215983058784 C 1.51763017479288 0.103561889222533 1.79741674333036 0.0357348423044641 2.10263845446184 0.0357348423044641 L 10.5301490340387 0.0357348423044641 C 10.8353707451702 0.0357348423044641 11.1208095676172 0.0894312544479021 11.3864655013799 0.196824078734949 C 11.6521214351426 0.304216903021938 11.8838638454463 0.451175504677906 12.0816927322908 0.637699883702737 C 12.2795216191353 0.824224262727569 12.4349586016558 1.04748829216646 12.5480036798526 1.30749197201931 C 12.6610487580497 1.56749565187198 12.717571297148 1.84445609345431 12.717571297148 2.13837329676619 Z M 17.9911241950322 1.98576244120045 L 17.9911241950322 2.01967596465948 L 17.9911241950322 9.76891607505507 C 17.9911241950322 10.0176152470882 17.9261232750689 10.2380531495721 17.7961214351426 10.4302297825068 C 17.6661195952161 10.6224064154415 17.4711168353265 10.7184947319088 17.2111131554739 10.7184947319088 C 17.1206770929161 10.7184947319088 17.0048058877646 10.6845812084498 16.8634995400182 10.6167541615317 C 16.7221931922722 10.5489271146135 16.580886844526 10.4697955598757 16.4395804967801 10.3793594973182 C 16.2982741490339 10.2889234347607 16.1654461821527 10.1984873722032 16.0410965961362 10.1080513096457 C 15.9167470101197 10.0176152470882 15.8263109475622 9.94413594626025 15.7697884084637 9.88761340716178 C 15.6228298068077 9.76326382114524 15.3995657773689 9.54847817257121 15.0999963201473 9.24325646143973 C 14.8004268629254 8.93803475030819 14.4980312787488 8.59324726180773 14.1928095676174 8.2088939959383 C 13.8875878564859 7.82454073006897 13.6191057957683 7.42888295637988 13.3873633854646 7.02192067487113 C 13.1556209751611 6.61495839336237 13.0397497700092 6.24190963531271 13.0397497700092 5.90277440072202 C 13.0397497700092 5.56363916613145 13.1640993560259 5.17645977330722 13.4127985280591 4.7412362222492 C 13.661497700092 4.30601267119124 13.9610671573137 3.87926750099808 14.311506899724 3.46100071166967 C 14.6619466421344 3.0427339223412 15.029343146274 2.65555452951696 15.4136964121435 2.29946253319679 C 15.7980496780128 1.94337053687667 16.120228150874 1.67488847615903 16.3802318307269 1.49401635104414 C 16.481972401104 1.42618930412601 16.6204526218951 1.3442316224332 16.7956724931003 1.24814330596587 C 16.9708923643054 1.15205498949854 17.1263293468262 1.10401083126493 17.2619834406626 1.10401083126493 C 17.567205151794 1.10401083126493 17.7650340386385 1.18879463991254 17.855470101196 1.35836225720789 C 17.9459061637535 1.52792987450306 17.9911241950322 1.73706326916727 17.9911241950322 1.98576244120045 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1475 512 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1255 -468 )">
<path d="M 15 14.9 C 15 15.7 14.8 16.5 14.3 17.1 C 13.9 17.7 13.2 18 12.5 18 L 2.5 18 C 1.8 18 1.1 17.7 0.7 17.1 C 0.2 16.5 0 15.7 0 14.9 C 0 14.3 0 13.6 0.1 13 C 0.2 12.4 0.3 11.8 0.5 11.2 C 0.7 10.7 0.9 10.2 1.2 9.7 C 1.5 9.3 1.9 8.9 2.3 8.7 C 2.8 8.4 3.3 8.3 3.9 8.3 C 4.9 9.3 6.2 9.8 7.6 9.8 C 9 9.8 10.3 9.3 11.3 8.3 C 11.9 8.3 12.4 8.4 12.9 8.7 C 13.3 9 13.7 9.3 14 9.7 C 14.3 10.2 14.5 10.7 14.7 11.2 C 14.9 11.8 15 12.4 15.1 13 C 15 13.6 15 14.3 15 14.9 Z M 10.7 1.3 C 11.6 2.1 12 3.3 12 4.5 C 12 5.7 11.5 6.8 10.7 7.7 C 9.8 8.5 8.7 9 7.5 9 C 6.3 9 5.2 8.5 4.3 7.7 C 3.5 6.8 3 5.7 3 4.5 C 3 3.3 3.5 2.2 4.3 1.3 C 5.2 0.5 6.3 0 7.5 0 C 8.7 0 9.8 0.5 10.7 1.3 Z " fill-rule="nonzero" fill="#cccccc" stroke="none" transform="matrix(1 0 0 1 1255 468 )" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1256 -538 )">
<path d="M 7.05 1.6 C 8.75 1.6 10.15 3 10.15 4.7 L 11.75 4.7 C 11.65 2.1 9.55 0 7.05 0 C 4.45 0 2.45 2.1 2.45 4.6 L 4.05 4.6 C 3.95 2.9 5.35 1.6 7.05 1.6 Z M 13.95 17 L 13.95 8.3 C 13.85 7.7 13.35 7.2 12.75 7.1 L 1.35 7.1 C 0.65 7.2 0.0499999999999997 7.7 0.0499999999999997 8.4 L 0.0499999999999997 16.8 C 0.15 17.4 0.55 17.8 1.15 18 L 12.95 18 C 13.45 17.9 13.85 17.5 13.95 17 Z " fill-rule="nonzero" fill="#cccccc" stroke="none" transform="matrix(1 0 0 1 1256 538 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="98px" height="103px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1677 -169 )">
<path d="M 0 103 L 98 103 L 98 87.0421585160202 C 97.1281618887015 83.6694772344013 94.9696458684653 79.9595278246206 92.0016863406408 77.8010118043845 C 85.7284991568295 72.6745362563238 66.7740303541314 64.3102866779089 66.7740303541314 64.3102866779089 L 54.4974704890387 86.637436762226 L 52.7436762225971 81.1736930860034 L 55.7116357504215 74.0236087689713 L 48.9662731871838 67.2782462057336 L 42.2209106239461 74.0236087689713 L 45.5935919055648 80.7689713322091 L 43.5025295109613 86.2327150084317 L 31.6306913996627 63.5008431703204 C 31.6306913996627 63.5008431703204 12.7436762225971 71.8650927487353 6.40303541315347 76.9915682967959 C 3.43507588532884 79.4873524451939 1.27655986509285 82.7925801011804 0 87.0421585160202 L 0 103 Z M 76.0151770657671 30.58347386172 C 76.0151770657671 19.5885328836425 76.0151770657671 0.229342327150079 50.3153456998314 0.229342327150079 C 24.6829679595278 0.229342327150079 24.6829679595278 19.5885328836425 24.6155143338954 30.58347386172 C 24.5480607082632 41.9831365935919 35.1382799325463 62.2192242833052 50.2478920741989 62.2192242833052 C 65.8971332209107 61.8145025295109 76.0151770657671 41.5784148397976 76.0151770657671 30.58347386172 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1677 169 )" />
</g>
</svg>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="12px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1764 -306 )">
<path d="M 2.51332895992346 8.2419489503817 L 9.44528268129761 1.68571087786256 C 9.85230796755718 1.3006023377863 10.4941555343512 1.31625715648852 10.8823950381679 1.72328244274809 C 11.2737655057254 2.13030772900765 11.258110687023 2.77841722328242 10.847954437023 3.16665672709928 L 3.57472566793899 10.0798246660306 L 1.77129055343516 10.3146469465649 L 2.51332895992346 8.2419489503817 Z M 11.8060293416033 4.17482705152673 C 12.7734971374045 3.25432371183206 12.8110687022902 1.72328244274809 11.8874343988548 0.758945610687078 C 10.9700620229009 -0.199129293893179 9.44841364503804 -0.236700858778647 8.48720777671747 0.674409589694619 L 1.43627743320599 7.34336235687027 C 1.35800333969473 7.41850548664121 1.29538406488541 7.51243439885502 1.2578125 7.61575620229013 L 0.0743082061069344 10.9157919847328 C -0.00709685114520653 11.1443523377862 0.0367366412215233 11.3979604007633 0.187022900763395 11.5889491889313 C 0.340440124045927 11.7799379770993 0.578393368320803 11.873866889313 0.819477576335657 11.8425572519084 L 3.97861999045813 11.4292700381679 C 4.12577528625957 11.4104842557252 4.26040672709905 11.3447340171756 4.36685949427465 11.244543177481 L 11.8060293416033 4.17482705152673 Z M 13.2681894083967 10.4555403148855 L 7.69507395038158 10.4555403148855 C 7.30996541030527 10.4555403148855 6.99686903625957 10.7686366889313 6.99686903625957 11.1537452290077 C 6.99686903625957 11.538853769084 7.30996541030527 11.8519501431298 7.69507395038158 11.8519501431298 L 13.2681894083967 11.8519501431298 C 13.6532979484732 11.8519501431298 13.9663943225191 11.538853769084 13.9663943225191 11.1537452290077 C 13.9663943225191 10.7686366889313 13.6532979484732 10.4555403148855 13.2681894083967 10.4555403148855 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1764 306 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="12px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1318 -254 )">
<path d="M 1.1717647058822 0.240000000000009 L 6 5.06823529411764 L 10.8282352941178 0.240000000000009 C 10.9517949764427 0.116440317674687 11.119377868552 0.047025210941456 11.2941176470588 0.047025210941456 C 11.6579943989025 0.047025210941456 11.9529747890585 0.342005601097583 11.9529747890585 0.705882352941217 C 11.9529747890585 0.880622131448035 11.8835596823253 1.0482050235571 11.7599999999998 1.17176470588242 L 6.93176470588219 6 L 11.7599999999998 10.8282352941176 C 11.8835596823253 10.9517949764429 11.9529747890585 11.119377868552 11.9529747890585 11.2941176470588 C 11.9529747890585 11.6579943989024 11.6579943989025 11.9529747890585 11.2941176470588 11.9529747890585 C 11.119377868552 11.9529747890585 10.9517949764427 11.8835596823253 10.8282352941178 11.76 L 6 6.93176470588236 L 1.1717647058822 11.76 C 1.04820502355733 11.8835596823253 0.880622131448035 11.9529747890585 0.705882352941217 11.9529747890585 C 0.342005601097526 11.9529747890585 0.047025210941456 11.6579943989024 0.047025210941456 11.2941176470588 C 0.047025210941456 11.119377868552 0.116440317674687 10.9517949764429 0.240000000000236 10.8282352941176 L 5.06823529411781 6 L 0.240000000000236 1.17176470588242 C 0.116440317674687 1.0482050235571 0.047025210941456 0.880622131448035 0.047025210941456 0.705882352941217 C 0.047025210941456 0.342005601097583 0.342005601097526 0.047025210941456 0.705882352941217 0.047025210941456 C 0.880622131448035 0.047025210941456 1.04820502355733 0.116440317674687 1.1717647058822 0.240000000000009 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1318 254 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="30px" height="30px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1519 -247 )">
<path d="M 0 30 L 30 30 L 30 25.3520850046661 C 29.7331107822556 24.3697506507965 29.0723405719793 23.28918286154 28.163781532849 22.6604888750634 C 26.2434181092335 21.1673406571817 20.4410297002444 18.7311514595851 20.4410297002444 18.7311514595851 L 16.6828991292978 25.2342048822017 L 16.1460233334481 23.642823228933 L 17.0545823725779 21.5602743987295 L 14.9896754654644 19.5956056909904 L 12.9247685583507 21.5602743987295 L 13.9572220119076 23.5249431064687 L 13.3171008707022 25.1163247597374 L 9.68286471418241 18.4953912146564 C 9.68286471418241 18.4953912146564 3.90112537426421 20.931580412253 1.96011288157752 22.4247286301347 C 1.05155384244745 23.1516560519982 0.390783632171406 24.1143437187904 0 25.3520850046661 L 0 30 Z M 23.26995216299 8.90780792088933 C 23.26995216299 5.7053979272745 23.26995216299 0.0667987360631344 15.4026568468871 0.0667987360631344 C 7.55601059985543 0.0667987360631344 7.55601059985543 5.7053979272745 7.53536153078426 8.90780792088933 C 7.51471246171332 12.2280980369685 10.7566163058816 18.122104160186 15.3820077778159 18.122104160186 C 20.1725918023196 18.0042240377216 23.26995216299 12.1102179145042 23.26995216299 8.90780792088933 Z " fill-rule="nonzero" fill="#b7bac0" stroke="none" transform="matrix(1 0 0 1 1519 247 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="12px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1857 -205 )">
<path d="M 2.51332895992346 8.2419489503817 L 9.44528268129761 1.68571087786256 C 9.85230796755718 1.3006023377863 10.4941555343512 1.31625715648852 10.8823950381679 1.72328244274809 C 11.2737655057254 2.13030772900765 11.258110687023 2.77841722328242 10.847954437023 3.16665672709928 L 3.57472566793899 10.0798246660306 L 1.77129055343516 10.3146469465649 L 2.51332895992346 8.2419489503817 Z M 11.8060293416033 4.17482705152673 C 12.7734971374045 3.25432371183206 12.8110687022902 1.72328244274809 11.8874343988548 0.758945610687078 C 10.9700620229009 -0.199129293893179 9.44841364503804 -0.236700858778647 8.48720777671747 0.674409589694619 L 1.43627743320599 7.34336235687027 C 1.35800333969473 7.41850548664121 1.29538406488541 7.51243439885502 1.2578125 7.61575620229013 L 0.0743082061069344 10.9157919847328 C -0.00709685114520653 11.1443523377862 0.0367366412215233 11.3979604007633 0.187022900763395 11.5889491889313 C 0.340440124045927 11.7799379770993 0.578393368320803 11.873866889313 0.819477576335657 11.8425572519084 L 3.97861999045813 11.4292700381679 C 4.12577528625957 11.4104842557252 4.26040672709905 11.3447340171756 4.36685949427465 11.244543177481 L 11.8060293416033 4.17482705152673 Z M 13.2681894083967 10.4555403148855 L 7.69507395038158 10.4555403148855 C 7.30996541030527 10.4555403148855 6.99686903625957 10.7686366889313 6.99686903625957 11.1537452290077 C 6.99686903625957 11.538853769084 7.30996541030527 11.8519501431298 7.69507395038158 11.8519501431298 L 13.2681894083967 11.8519501431298 C 13.6532979484732 11.8519501431298 13.9663943225191 11.538853769084 13.9663943225191 11.1537452290077 C 13.9663943225191 10.7686366889313 13.6532979484732 10.4555403148855 13.2681894083967 10.4555403148855 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1857 205 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1860 -349 )">
<path d="M 15.9545454545457 8 C 15.9545454545457 12.393939399858 12.3939393998578 15.9545454545454 8 15.9545454545454 C 3.6060606001422 15.9545454545454 0.0454545454542767 12.393939399858 0.0454545454542767 8 C 0.0454545454542767 3.60606060014203 3.6060606001422 0.045454545454561 8 0.045454545454561 C 12.3939393998578 0.045454545454561 15.9545454545457 3.60606060014203 15.9545454545457 8 Z M 14.4393939453128 8 C 14.4393939453128 4.43939392755681 11.5606060546872 1.56060605468747 8 1.56060605468747 C 4.43939394531276 1.56060605468747 1.56060605468724 4.43939394531253 1.56060605468724 8 C 1.56060605468724 11.5606060546875 4.43939392755669 14.4393939453125 8 14.4393939453125 C 11.5606060724433 14.4393939453125 14.4393939453128 11.5606060724432 14.4393939453128 8 Z M 3.45929338997712 8 C 3.45929338997712 8.63118257123517 3.95446233025609 9.14285714285717 4.56528417338677 9.14285714285717 C 5.17610601651791 9.14285714285717 5.67127495679711 8.63118257123517 5.67127495679711 8 C 5.67127495679711 7.36881742876483 5.17610601651791 6.85714285714283 4.56528417338677 6.85714285714283 C 3.95446233025609 6.85714285714283 3.45929338997712 7.36881742876483 3.45929338997712 8 Z M 6.85714285714289 8.00000001702989 C 6.85714285714289 8.63118257885975 7.36881742876471 9.14285714285717 8 9.14285714285717 C 8.63118257123529 9.14285714285717 9.14285714285711 8.63118257885975 9.14285714285711 8.00000001702989 C 9.14285714285711 7.36881742114025 8.63118257123529 6.85714285714283 8 6.85714285714283 C 7.36881742876471 6.85714285714283 6.85714285714289 7.36881742114025 6.85714285714289 7.99999998297011 Z M 10.3287250432029 8 C 10.3287250432029 8.63118257123517 10.8238939834821 9.14285714285717 11.4347158266132 9.14285714285717 C 12.0455376697439 9.14285714285717 12.5407066100229 8.63118257123517 12.5407066100229 8 C 12.5407066100229 7.36881742876483 12.0455376697439 6.85714285714283 11.4347158266132 6.85714285714283 C 10.8238939834821 6.85714285714283 10.3287250432029 7.36881742876483 10.3287250432029 8 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 1860 349 )" />
</g>
</svg>

View File

@ -7,7 +7,7 @@
} }
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Noto Sans SC', sans-serif;
background-color: #f5f5f5; background-color: #f5f5f5;
color: #333; color: #333;
line-height: 1.6; line-height: 1.6;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,203 @@
<template>
<div class="DiagnosticMessage-container">
<!-- 表单内容 -->
<div class="form-container">
<div class="form-container-header">
<div>诊断信息</div>
<img src="@/assets/new/u264.svg" alt="" style="cursor: pointer;" @click="handleCancel">
</div>
<div style="padding:20px 40px;">
<el-form :model="diagnosticForm" label-width="60px">
<el-form-item label="记录">
<el-input v-model="diagnosticForm.diagnosis_info" :rows="6" type="textarea" placeholder="请输入" />
</el-form-item>
<el-form-item label="处理">
<el-input v-model="diagnosticForm.treatment_info" :rows="6" type="textarea" placeholder="请输入" />
</el-form-item>
<el-form-item label="建议">
<el-input v-model="diagnosticForm.suggestion_info" :rows="6" type="textarea" placeholder="请输入" />
</el-form-item>
</el-form>
<div class="form-actions-display">
<el-button @click="handleCancel" class="formreturnCancel">退出</el-button>
<el-button type="primary" class="formsaveCancel"
@click="handleDiagnosticInfo('completed')">
保存
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getBackendUrl, patientAPI } from '../services/api.js'
const emit = defineEmits([ 'closeDiagnosticMessage']);
const router = useRouter()
//
const BACKEND_URL = getBackendUrl()
const props = defineProps({
selectedPatient: {
required: false,
type: Object,
default: null
},
})
const diagnosticForm =ref({})
//
onMounted(() => {
})
const handleCancel = async () => {
emit('closeDiagnosticMessage',false)
}
async function handleDiagnosticInfo(status) {
try {
// ID
if (!selectedPatient.value.sessionId) {
throw new Error('缺少会话Id')
}
// API
const response = await fetch(`${BACKEND_URL}/api/detection/${selectedPatient.value.sessionId}/save-info`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
diagnosis_info: diagnosticForm.diagnosis_info,
treatment_info: diagnosticForm.treatment_info,
suggestion_info: diagnosticForm.suggestion_info,
status: status,
session_id: selectedPatient.value.sessionId,
})
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result = await response.json()
if (result.success) {
//
ElMessage.success({
message: status + '诊断信息成功',
duration: 5000
})
emit('closeDiagnosticMessage',true)
} else {
throw new Error(result.message || '诊断信息失败')
}
} catch (error) {
ElMessage.error({
message:'诊断信息失败',
duration: 5000
})
} finally {
}
}
</script>
<style scoped>
.DiagnosticMessage-container {
width: 800px;
height: 620px;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
margin: auto;
background: #1b1b1b;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(80, 80, 80, 1);
border-width: 1px;
border-style: solid;
border-color: rgba(148, 148, 148, 1);
}
.formreturnCancel {
width: 80px;
height: 40px;
background: #313131;
border: 1px solid rgb(148, 148, 148);
font-family: 'Noto Sans SC';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgb(148, 148, 148);
}
.formreturnCancel:hover {
background: #1e2c49;
color: #266fff;
border: 1px solid #266fff;
}
.formsaveCancel {
width: 80px;
height: 40px;
background: #266fff;
font-family: 'Noto Sans SC';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #FFFFFF;
}
</style>
<style>
.DiagnosticMessage-container .el-form-item__label{
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #787878;
margin-right: 10px;
}
.DiagnosticMessage-container .el-textarea__inner{
background: #282828;
box-shadow: 0 0 0 1px rgb(54, 54, 54) inset;
border-radius: 4px;
}
.DiagnosticMessage-container .el-textarea__inner:hover{
box-shadow: 0 0 0 1px #266fff inset;
}
.DiagnosticMessage-container .form-container {
width: 100%;
}
.DiagnosticMessage-container .form-container-header{
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: rgba(46, 52, 59, 1);
box-sizing: border-box;
padding: 0 20px;
font-family: 'Noto Sans SC';
font-weight: 700;
font-style: normal;
font-size: 16px;
color: #FFFFFF;
text-align: left;
border-radius:10px 10px 0 0;
}
.DiagnosticMessage-container .form-actions-display{
display: flex;
justify-content: flex-end;
padding-top: 20px;
padding-right: 0px;
}
</style>

View File

@ -14,16 +14,13 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<div style="color:#fff;margin-right: 20px;">登录时间{{ time }} </div> <div style="color:#fff;margin-right: 20px;">登录时间{{ time }} </div>
<div class="user-line"></div>
<div class="user-info"> <div class="user-info">
<img src="@/assets/svg/avatar.svg" alt="Avatar" style="width: 20px;height: 20px;"> <img src="@/assets/new/u13.png" alt="Avatar" style="width: 30px;height: 30px;">
<!-- <el-avatar :size="40" :src="userInfo.avatar">
<el-icon>
<User />
</el-icon>
</el-avatar> -->
<span class="username">{{ userInfo.name }}</span> <span class="username">{{ userInfo.name }}</span>
<el-dropdown @command="handleUserCommand"> <div class="user-line"></div>
<span class="username user-return" @click="handleUserCommand">退出</span>
<!-- <el-dropdown @command="handleUserCommand">
<el-button link class="user-dropdown"> <el-button link class="user-dropdown">
<el-icon> <el-icon>
<ArrowDown /> <ArrowDown />
@ -35,7 +32,7 @@
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item> <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown> -->
</div> </div>
</div> </div>
</div> </div>
@ -86,18 +83,19 @@
const handleUserCommand = (command) => { const handleUserCommand = (command) => {
switch (command) { handleLogout()
case 'profile': // switch (command) {
viewInfoClick() // case 'profile':
// // viewInfoClick()
break // //
case 'settings': // break
// // case 'settings':
break // //
case 'logout': // break
handleLogout() // case 'logout':
break // handleLogout()
} // break
// }
} }
const dialogVisible =ref(false) const dialogVisible =ref(false)
function viewInfoClick(){ function viewInfoClick(){
@ -214,7 +212,7 @@
} }
.header { .header {
height: 50px; height: 60px;
background: #323232; background: #323232;
border-bottom: none; border-bottom: none;
display: flex; display: flex;
@ -240,17 +238,18 @@
width: 26px; width: 26px;
height: 26px; height: 26px;
border-radius: 4px; border-radius: 4px;
background: #0099ff; background: rgba(38, 111, 255, 1);
text-align: center; text-align: center;
padding-top: 3px; padding-top: 3px;
} }
.system-title { .system-title {
margin: 0; margin: 0;
font-family: 微软雅黑, sans-serif; font-family: 'Noto Sans SC';
font-weight: 400; font-weight: 700;
font-style: normal; font-style: normal;
font-size: 22px; font-size: 22px;
color: #FFFFFF;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
display: flex; display: flex;
align-items: center; align-items: center;
@ -287,15 +286,15 @@
font-size: 12px; font-size: 12px;
margin-left: 8px; margin-left: 8px;
} }
.badge-invalid { background-color: #8b0000; color: #fff; } .badge-invalid { background-color: rgba(67, 67, 67, 1); color: #949494; }
.badge-trial { background-color: #ff8c00; color: #fff; } .badge-trial { background-color: rgba(67, 67, 67, 1); color: #949494; }
.badge-valid { background-color: #2e8b57; color: #fff; } .badge-valid { background-color: rgba(38, 111, 255, 1); color: #fff; }
.activate-btn { .activate-btn {
margin-left: 8px; margin-left: 8px;
padding: 2px 10px; padding: 2px 10px;
font-size: 12px; font-size: 12px;
border: 1px solid #409EFF; border: 1px solid rgba(38, 111, 255, 1);
background-color: #409EFF; background-color: rgba(38, 111, 255, 1);
color: #fff; color: #fff;
border-radius: 12px; border-radius: 12px;
cursor: pointer; cursor: pointer;
@ -316,4 +315,15 @@
box-shadow: 0 0 0 1px transparent inset; box-shadow: 0 0 0 1px transparent inset;
border-color: transparent !important; border-color: transparent !important;
} }
.user-line{
width: 1px;
height: 15px;
margin: 0 10px;
background-color: #949494;
}
.user-return:hover{
cursor: pointer;
color: rgb(0, 140, 255);
text-decoration: underline;
}
</style> </style>

View File

@ -1,67 +1,69 @@
<template> <template>
<div class="login-page" :style="{ backgroundImage: `url(${bg})` }"> <div class="login-page" :style="{ backgroundImage: `url(${bg})` }">
<!-- 页面主内容 --> <!-- 页面主内容 -->
<div class="login-content"> <div class="login-content">
<!-- 系统标题 --> <!-- 系统标题 -->
<h1 class="system-title">平衡体态检测系统</h1> <div style="margin-right: 150px;">
<h1 class="system-title">平衡体态检测系统</h1>
<!-- 登录页面 --> <!-- 登录页面 -->
<el-card v-if="!isRegisterMode && !isForgotPasswordMode" class="login-card"> <el-card v-if="!isRegisterMode && !isForgotPasswordMode" class="login-card">
<div class="card-header">登录</div> <div class="card-header">欢迎登录</div>
<el-form class="login-form"> <el-form class="login-form">
<!-- 账号输入框 --> <!-- 账号输入框 -->
<el-form-item> <el-form-item>
<div class="input-box"> <div class="input-box">
<img src="@/assets/username.png" alt="" srcset="" height="30" style="margin-left: 10px;"> <img src="@/assets/new/u10.svg" alt="" srcset="" height="20" style="margin-left: 10px;">
<el-input <el-input
v-model="form.account" v-model="form.account"
placeholder="请输入账号" placeholder="请输入账号"
class="custom-input" class="custom-input"
/> />
</div>
</el-form-item>
<!-- 密码输入框带显示切换 -->
<el-form-item>
<div class="input-box">
<img src="@/assets/new/u16.svg" alt="" srcset="" height="20" style="margin-left: 10px;">
<el-input
v-model="form.password"
:type="passwordVisible ? 'text' : 'password'"
placeholder="请输入密码"
class="custom-input"
@keyup.enter="handleLogin"
>
<template #suffix>
<el-icon
class="password-toggle"
@click="passwordVisible = !passwordVisible"
>
<component :is="passwordVisible ? Hide : View" />
</el-icon>
</template>
</el-input>
</div>
</el-form-item>
<!-- 记住密码 & 忘记密码 -->
<div class="form-footer">
<el-checkbox v-model="form.remember" class="remember-checkbox" @change="handleRememberChange">记住账号及密码</el-checkbox>
<a href="#" class="forgot-link" @click="handleForgotPassword">忘记密码</a>
</div> </div>
</el-form-item> <!-- 操作按钮 -->
<div class="button-group">
<!-- 密码输入框带显示切换 --> <el-button type="primary" class="login-btn" @click="handleLogin" :loading="isLoading">{{ isLoading ? '登录中...' : '登录' }}</el-button>
<el-form-item> <!-- <el-button class="register-btn" @click="switchToRegister">注册</el-button> -->
<div class="input-box">
<img src="@/assets/password.png" alt="" srcset="" height="30" style="margin-left: 10px;">
<el-input
v-model="form.password"
:type="passwordVisible ? 'text' : 'password'"
placeholder="请输入密码"
class="custom-input"
@keyup.enter="handleLogin"
>
<template #suffix>
<el-icon
class="password-toggle"
@click="passwordVisible = !passwordVisible"
>
<component :is="passwordVisible ? Hide : View" />
</el-icon>
</template>
</el-input>
</div> </div>
<div style="display:flex ;justify-content: flex-end;width: 100%;margin-top: 10px;">
</el-form-item> <span class="register-btn" @click="switchToRegister">注册</span>
</div>
<!-- 记住密码 & 忘记密码 --> </el-form>
<div class="form-footer"> </el-card>
<el-checkbox v-model="form.remember" class="remember-checkbox" @change="handleRememberChange">记住账号及密码</el-checkbox>
<a href="#" class="forgot-link" @click="handleForgotPassword">忘记密码</a>
</div>
<!-- 操作按钮 -->
<div class="button-group">
<el-button type="primary" class="login-btn" @click="handleLogin" :loading="isLoading">{{ isLoading ? '登录中...' : '登录' }}</el-button>
<el-button class="register-btn" @click="switchToRegister">注册</el-button>
</div>
</el-form>
</el-card>
<!-- 注册页面 --> <!-- 注册页面 -->
<el-card v-if="isRegisterMode && !isForgotPasswordMode" class="register-card"> <el-card v-if="isRegisterMode && !isForgotPasswordMode" class="register-card">
<div class="card-header">注册</div> <div class="card-header">注册</div>
@ -140,7 +142,6 @@
<button class="submit-btn" @click="handleRegisterSubmit">注册</button> <button class="submit-btn" @click="handleRegisterSubmit">注册</button>
</div> </div>
</el-card> </el-card>
<!-- 忘记密码页面 --> <!-- 忘记密码页面 -->
<el-card v-if="isForgotPasswordMode" class="forgot-password-card"> <el-card v-if="isForgotPasswordMode" class="forgot-password-card">
<div class="card-header">找回密码</div> <div class="card-header">找回密码</div>
@ -186,6 +187,10 @@
<button v-if="showRetrievedPassword" class="confirm-btn" @click="backToLoginFromForgot">退出</button> <button v-if="showRetrievedPassword" class="confirm-btn" @click="backToLoginFromForgot">退出</button>
</div> </div>
</el-card> </el-card>
</div>
</div> </div>
<!-- 自定义错误提示弹窗 --> <!-- 自定义错误提示弹窗 -->
@ -216,7 +221,7 @@ import { ElMessage } from 'element-plus'
import { useAuthStore } from '../stores' import { useAuthStore } from '../stores'
import { User, Lock, View, Hide, Phone } from '@element-plus/icons-vue' import { User, Lock, View, Hide, Phone } from '@element-plus/icons-vue'
import bg from '@/assets/bg.png' import bg from '@/assets/new/newbg.jpg'
import { getBackendUrl,systemAPI } from '../services/api.js' import { getBackendUrl,systemAPI } from '../services/api.js'
const BACKEND_URL = getBackendUrl() const BACKEND_URL = getBackendUrl()
const router = useRouter() const router = useRouter()
@ -665,58 +670,58 @@ const copyPassword = async () => {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; /* flex-direction: column; */
align-items: center; align-items: center;
justify-content: center; justify-content: flex-end;
padding: 0 20px; padding: 0 20px;
} }
/* 系统标题 */ /* 系统标题 */
.system-title { .system-title {
font-size: 2rem; text-align: center;
color: #00ffff; font-family: "阿里妈妈数黑体 Bold", 阿里妈妈数黑体, sans-serif;
text-shadow: 0 0 15px rgba(0, 255, 255, 0.8); font-weight: 700;
margin-bottom: 2.5rem; font-style: normal;
font-size: 40px;
color: rgb(255, 255, 255);
margin-bottom: 30px;
letter-spacing: 2px; letter-spacing: 2px;
} }
/* 登录卡片 */ /* 登录卡片 */
.login-card { .login-card {
width: 482px; width: 482px;
height: 471px; height: 450px;
background: inherit; background-color: #1D1C21;
background-color: rgba(10, 68, 131, 0.9);
box-sizing: border-box; box-sizing: border-box;
border-width: 3px; border-width: 3px;
border-style: solid; border-style: solid;
border-color: rgba(11, 92, 168, 1); border-color: #1D1C21;
border-radius: 5px; border-radius: 5px;
padding: 40px 25px 0; padding: 40px 25px 0;
} }
/* 注册卡片 */ /* 注册卡片 */
.register-card { .register-card {
width: 100%; width: 482px;
max-width: 450px; background-color: #1D1C21;
background-color: #003366 !important; box-sizing: border-box;
border: none !important; border-width: 3px;
border-radius: 12px !important; border-style: solid;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2); border-color: #1D1C21;
border-radius: 5px;
padding: 30px 25px !important; padding: 30px 25px !important;
} }
/* 卡片头部标题 */ /* 卡片头部标题 */
.card-header { .card-header {
/* font-size: 1.4rem; font-family: 微软雅黑, sans-serif;
color: #00ffff; font-weight: 400;
text-align: center;
margin-bottom: 25px; */
font-size: 24px;
font-family: 'Arial Negreta', 'Arial Normal', 'Arial', sans-serif;
font-weight: 700;
font-style: normal; font-style: normal;
color: #00FFFF; font-size: 30px;
color: rgb(255, 255, 255);
text-align: center;
margin-bottom: 20px;
} }
/* 登录表单 */ /* 登录表单 */
@ -741,7 +746,7 @@ const copyPassword = async () => {
} }
/* 密码显示图标 */ /* 密码显示图标 */
.password-icon { .password-icon {
color: #00ffff !important; color: #cccccc !important;
cursor: pointer; cursor: pointer;
padding: 0 8px; padding: 0 8px;
} }
@ -769,7 +774,7 @@ const copyPassword = async () => {
/* 忘记密码链接 */ /* 忘记密码链接 */
.forgot-link { .forgot-link {
color: #00ffff; color: #ffffff;
text-decoration: none; text-decoration: none;
} }
.forgot-link:hover { .forgot-link:hover {
@ -785,30 +790,28 @@ const copyPassword = async () => {
/* 登录按钮 */ /* 登录按钮 */
.login-btn { .login-btn {
flex: 1; flex: 1;
background-color: #00ffff !important; background-color: #266fff !important;
border-color: #00ffff !important; border-color: #266fff !important;
color: #003366 !important; color: #fff !important;
font-weight: 500 !important; font-weight: 500 !important;
border-radius: 6px !important; border-radius: 6px !important;
height: 50px;
font-size: 16px;
} }
.login-btn:hover { .login-btn:hover {
background-color: #00e6e6 !important; background-color: #266fff !important;
border-color: #00e6e6 !important; border-color: #266fff !important;
} }
/* 注册按钮 */ /* 注册按钮 */
.register-btn { .register-btn {
flex: 1; font-family: 微软雅黑, sans-serif;
background-color: transparent !important; font-weight: 400;
border-color: #00ffff !important; font-style: normal;
color: #00ffff !important; font-size: 14px;
font-weight: 500 !important; color: rgb(255, 255, 255);
border-radius: 6px !important; cursor: pointer;
} }
.register-btn:hover {
background-color: #004080 !important;
}
/* 自定义错误弹窗样式 */ /* 自定义错误弹窗样式 */
.error-dialog-overlay { .error-dialog-overlay {
position: fixed; position: fixed;
@ -825,10 +828,10 @@ const copyPassword = async () => {
} }
.error-dialog { .error-dialog {
background-color: #003366; background-color: #1D1C21;
border: 2px solid #00ffff; border: 2px solid #1D1C29;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.3); box-shadow: 0 0 30px 1D1C29;
min-width: 400px; min-width: 400px;
max-width: 500px; max-width: 500px;
animation: dialogFadeIn 0.3s ease-out; animation: dialogFadeIn 0.3s ease-out;
@ -850,19 +853,19 @@ const copyPassword = async () => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 25px 15px; padding: 20px 25px 15px;
border-bottom: 1px solid rgba(0, 255, 255, 0.2); border-bottom: 1px solid rgba(0, 136, 255, 0.2);
} }
.dialog-title { .dialog-title {
font-size: 1.2rem; font-size: 1.2rem;
color: #00ffff; color: #fff;
font-weight: 500; font-weight: 500;
} }
.close-btn { .close-btn {
background: none; background: none;
border: none; border: none;
color: #00ffff; color: #ffffff;
font-size: 1.5rem; font-size: 1.5rem;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
@ -876,7 +879,7 @@ const copyPassword = async () => {
} }
.close-btn:hover { .close-btn:hover {
background-color: rgba(0, 255, 255, 0.1); background-color: rgba(0, 140, 255, 0.1);
transform: scale(1.1); transform: scale(1.1);
} }
@ -916,17 +919,14 @@ const copyPassword = async () => {
} }
.confirm-btn { .confirm-btn {
background-color: #00ffff; background-color: #266fff ;
border-color: #00ffff; border-color: #266fff ;
color: #003366; color: #fff;
font-weight: 500; font-weight: 500;
} }
.confirm-btn:hover { .confirm-btn:hover {
background-color: #00e6e6;
border-color: #00e6e6;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
} }
/* 注册表单样式 */ /* 注册表单样式 */
@ -942,20 +942,25 @@ const copyPassword = async () => {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #004080; background: transparent;
border: 1px solid #00ffff; color: #fff;
border-radius: 6px; font-size: 16px;
height: 49px;
/* background-color: #004080;
border-radius: 6px; */
border: 1px solid rgb(67, 67, 67);
padding: 0 15px; padding: 0 15px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.input-wrapper:focus-within { .input-wrapper:focus-within {
border-color: #00e6e6; border-color: #cccccc;
box-shadow: 0 0 8px rgba(0, 255, 255, 0.3); box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
} }
.input-icon { .input-icon {
color: #00ffff; color: #cccccc;
margin-right: 10px; margin-right: 10px;
font-size: 16px; font-size: 16px;
} }
@ -975,7 +980,7 @@ const copyPassword = async () => {
} }
.password-toggle { .password-toggle {
color: #00ffff; color: #cccccc;
cursor: pointer; cursor: pointer;
margin-left: 10px; margin-left: 10px;
font-size: 16px; font-size: 16px;
@ -983,7 +988,7 @@ const copyPassword = async () => {
} }
.password-toggle:hover { .password-toggle:hover {
color: #00e6e6; color: #cccccc;
transform: scale(1.1); transform: scale(1.1);
} }
@ -1008,36 +1013,35 @@ const copyPassword = async () => {
.back-btn { .back-btn {
background-color: transparent; background-color: transparent;
border-color: #00ffff; border-color: #266fff;
color: #00ffff; color: #266fff;
} }
.back-btn:hover { .back-btn:hover {
background-color: #004080; /* background-color: #004080;
transform: translateY(-1px); */
transform: translateY(-1px);
} }
.submit-btn { .submit-btn {
background-color: #00ffff; background-color: #266fff;
border-color: #00ffff; border-color: #266fff;
color: #003366; color: #fff;
} }
.submit-btn:hover { .submit-btn:hover {
background-color: #00e6e6;
border-color: #00e6e6;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
} }
/* 忘记密码页面样式 */ /* 忘记密码页面样式 */
.forgot-password-card { .forgot-password-card {
width: 100%; width: 482px;
max-width: 450px; background-color: #1D1C21;
background-color: #003366 !important; box-sizing: border-box;
border: none !important; border-width: 3px;
border-radius: 12px !important; border-style: solid;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2); border-color: #1D1C21;
border-radius: 5px;
padding: 30px 25px !important; padding: 30px 25px !important;
} }
@ -1124,16 +1128,12 @@ const copyPassword = async () => {
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
border: 1px solid; border: 1px solid;
background-color: #00ffff; background-color: #266fff;
border-color: #00ffff; border-color: #266fff;
color: #003366; color: #fff;
} }
.confirm-btn:hover { .confirm-btn:hover {
background-color: #00e6e6;
border-color: #00e6e6;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
} }
.input-box{ .input-box{
@ -1142,11 +1142,11 @@ const copyPassword = async () => {
width: 100%; width: 100%;
height: 49px; height: 49px;
background-color: rgba(255, 255, 255, 0); background-color: rgba(255, 255, 255, 0);
box-sizing: border-box; box-sizing: border-box;
border-width: 2px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: rgba(0, 153, 204, 1); border-color: rgb(67, 67, 67);
border-radius: 3px; border-radius: 3px;
} }
</style> </style>
<style> <style>
@ -1171,7 +1171,7 @@ const copyPassword = async () => {
.login-card .el-button{ .login-card .el-button{
padding: 12px 20px !important; padding: 12px 20px !important;
height: 40px; height: 50px;
} }
</style> </style>

View File

@ -1,28 +1,13 @@
<template> <template>
<div class="patient-create-container"> <div class="patient-create-container">
<Header />
<div class="nav-container">
<div class="nav-container-title" @click="goBack">
<img src="@/assets/svg/goback.svg" alt="">
<div style="margin-left: 20px;">
建档页
</div>
</div>
<div class="nav-container-info">
<!-- <div>测试时间2025-08-03 17:13:18<span></span></div>
<div style="margin-left: 15px;">测试医生<span>李医生</span></div> -->
</div>
</div>
<!-- 表单内容 --> <!-- 表单内容 -->
<div class="form-container"> <div class="form-container">
<div class="form-container-header">
<div>{{ patienttitle }} </div>
<img src="@/assets/new/u264.svg" alt="" style="cursor: pointer;" @click="handleCancel">
</div>
<el-form ref="patientFormRef" :model="patientForm" :rules="formRules" label-width="120px" class="patient-form"> <el-form ref="patientFormRef" :model="patientForm" :rules="formRules" label-width="120px" class="patient-form">
<div class="form-section"> <div class="form-section">
<div class="section-title">
<div class="section-title-text">
基本信息
</div>
</div>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="测试者ID" prop="testerId"> <el-form-item label="测试者ID" prop="testerId">
@ -30,10 +15,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="患者姓名" prop="name" required>
</el-col>
<el-col :span="12">
<el-form-item label="姓名" prop="name" required>
<el-input v-model="patientForm.name" placeholder="请输入" clearable /> <el-input v-model="patientForm.name" placeholder="请输入" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -48,7 +30,7 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item label="出生日期" prop="birth_date" required> <el-form-item label="出生日期" prop="birth_date" required>
<el-date-picker v-model="patientForm.birth_date" type="date" placeholder="请选择" style="width: 100%" <el-date-picker v-model="patientForm.birth_date" type="date" placeholder="请选择" style="width: 100%"
@change="calculateAgeres" /> @change="calculateAgeres" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@ -65,7 +47,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="长期居住地" prop="residence"> <el-form-item label="居住地" prop="residence">
<el-input v-model="patientForm.residence" placeholder="请输入" clearable /> <el-input v-model="patientForm.residence" placeholder="请输入" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -87,9 +69,6 @@
<el-form-item label="鞋码" prop="shoe_size"> <el-form-item label="鞋码" prop="shoe_size">
<el-input v-model="patientForm.shoe_size" placeholder="请输入" clearable /> <el-input v-model="patientForm.shoe_size" placeholder="请输入" clearable />
</el-form-item> </el-form-item>
</el-col>
<el-col :span="12">
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="电话号码" prop="phone" required> <el-form-item label="电话号码" prop="phone" required>
@ -110,43 +89,52 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="单位" prop="workplace"> <el-form-item label="证件号" prop="workplace">
<el-input v-model="patientForm.workplace" placeholder="请输入" clearable /> <el-input v-model="patientForm.workplace" placeholder="请输入" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</el-form> </el-form>
<div class="form-actions-display">
<el-button @click="handleCancel" class="formreturnCancel">退出</el-button>
<el-button type="primary" :loading="saveLoading" @click="handleSave"
class="formsaveCancel">
保存
</el-button>
</div>
</div> </div>
<!-- 底部操作按钮 --> <!-- 底部操作按钮 -->
<div class="footer-actions"> <!-- <div class="footer-actions">
<el-button @click="handleCancel">退出</el-button>
<el-button type="primary" :loading="saveLoading" @click="handleSave">
保存
</el-button>
<el-button type="success" :loading="saveAndDetectLoading" @click="handleSaveAndDetect"> <el-button type="success" :loading="saveAndDetectLoading" @click="handleSaveAndDetect">
保存并开始检测 保存并开始检测
</el-button> </el-button>
</div> </div> -->
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { patientAPI } from '../services/api.js' import { patientAPI } from '../services/api.js'
import Header from '@/views/Header.vue' const emit = defineEmits([ 'closecreatbox']);
const router = useRouter() const router = useRouter()
const props = defineProps({
selectedPatient: {
required: false,
type: Object,
default: null
},
patienttype: {
required: false,
type: String,
default: null
},
})
// //
const patientFormRef = ref() const patientFormRef = ref()
//
const saveLoading = ref(false)
const saveAndDetectLoading = ref(false)
// //
const patientForm = reactive({ const patientForm = reactive({
id: '', id: '',
@ -163,6 +151,21 @@ const patientForm = reactive({
workplace: '', workplace: '',
email: '' email: ''
}) })
//
const saveLoading = ref(false)
const saveAndDetectLoading = ref(false)
const patienttitle = ref("新建患者信息")
//
onMounted(() => {
//
if (props.patienttype == 'edit') {
patienttitle.value = '编辑患者信息'
Object.assign(patientForm, props.selectedPatient)
}
})
const occupationOptions = ref(["学生", "教师", "医生", "护士", "工程师", "程序员", "设计师", const occupationOptions = ref(["学生", "教师", "医生", "护士", "工程师", "程序员", "设计师",
"会计师", "律师", "警察", "消防员", "军人", "公务员", "销售", "市场营销", "会计师", "律师", "警察", "消防员", "军人", "公务员", "销售", "市场营销",
"人力资源", "行政", "财务", "咨询师", "建筑师", "科研人员", "记者", "编辑", "人力资源", "行政", "财务", "咨询师", "建筑师", "科研人员", "记者", "编辑",
@ -206,10 +209,6 @@ const formRules = {
] ]
} }
//
const goBack = () => {
router.go(-1)
}
const calculatedAge = ref('') const calculatedAge = ref('')
// calculateAgeres // calculateAgeres
const calculateAgeres = (date) => { const calculateAgeres = (date) => {
@ -224,27 +223,7 @@ const calculateAgeres = (date) => {
calculatedAge.value = age calculatedAge.value = age
} }
const handleCancel = async () => { const handleCancel = async () => {
// emit('closecreatbox',false)
const hasData = Object.values(patientForm).some(value => value !== '')
if (hasData) {
try {
await ElMessageBox.confirm(
'您有未保存的数据,确定要退出吗?',
'提示',
{
confirmButtonText: '确定退出',
cancelButtonText: '取消',
type: 'warning'
}
)
router.go(-1)
} catch {
//
}
} else {
router.go(-1)
}
} }
const validateForm = async () => { const validateForm = async () => {
@ -258,27 +237,34 @@ const validateForm = async () => {
} }
const savePatient = async () => { const savePatient = async () => {
//
const genderMap = { '男': 'male', '女': 'female' }
const genderValue = genderMap[patientForm.gender] || patientForm.gender
const patientData = { const patientData = {
id: patientForm.id, id: patientForm.id,
name: patientForm.name, name: patientForm.name,
gender: patientForm.gender, gender: genderValue,
age: calculatedAge.value,
birth_date: patientForm.birth_date, birth_date: patientForm.birth_date,
height: patientForm.height, height: parseFloat(patientForm.height) || null,
weight: patientForm.weight, weight: parseFloat(patientForm.weight) || null,
shoe_size: patientForm.shoe_size, shoe_size: patientForm.shoe_size ? parseFloat(patientForm.shoe_size) : null,
phone: patientForm.phone, phone: patientForm.phone,
occupation: patientForm.occupation, occupation: patientForm.occupation,
email: patientForm.email, email: patientForm.email,
nationality: patientForm.nationality, nationality: patientForm.nationality,
residence: patientForm.residence, residence: patientForm.residence,
workplace: patientForm.workplace workplace: patientForm.workplace,
medical_history: '', //
notes: '' //
} }
try { try {
const response = await patientAPI.create(patientData) const response = await patientAPI.create(patientData)
if (response.success) { if (response.success) {
emit('closecreatbox',true)
return response.data return response.data
} else { } else {
throw new Error(response.message || '保存失败') throw new Error(response.message || '保存失败')
} }
@ -321,10 +307,15 @@ const handleSaveAndDetect = async () => {
<style scoped> <style scoped>
.patient-create-container { .patient-create-container {
height: 100vh; width: 800px;
display: flex; height:620px;
flex-direction: column; position: absolute;
background: #000000; top: 0;
right: 0;
left: 0;
bottom: 0;
margin: auto;
background: #1b1b1b;
} }
.nav-container { .nav-container {
@ -335,7 +326,7 @@ const handleSaveAndDetect = async () => {
} }
.nav-container-title { .nav-container-title {
font-family: 'Arial Negreta', 'Arial Normal', 'Arial', sans-serif; font-family: 'Noto Sans SC';
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
color: #FFFFFF; color: #FFFFFF;
@ -349,7 +340,7 @@ const handleSaveAndDetect = async () => {
display: flex; display: flex;
font-size: 16px; font-size: 16px;
color: #FFFFFF; color: #FFFFFF;
font-family: 'Arial Normal', 'Arial', sans-serif; font-family: 'Noto Sans SC';
} }
.back-btn { .back-btn {
@ -372,16 +363,11 @@ const handleSaveAndDetect = async () => {
} }
.form-container { .form-container {
overflow-y: auto;
padding: 20px 20px 0px;
width: 100%; width: 100%;
} }
.patient-form { .patient-form {
max-width: 1000px;
margin: 0 auto; margin: 0 auto;
background: #333333;
border-radius: 8px;
padding: 30px; padding: 30px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
@ -442,20 +428,16 @@ const handleSaveAndDetect = async () => {
:deep(.el-form-item__label) { :deep(.el-form-item__label) {
font-size: 14px !important; font-size: 14px !important;
font-family: '苹方 粗体', '苹方 中等', '苹方', sans-serif !important; font-family: 'Noto Sans SC';
font-weight: 700 !important; font-weight: 700 !important;
font-style: normal !important; font-style: normal !important;
color: #FFFFFF !important; color: #787878 !important;
} }
:deep(.el-form-item__content) { :deep(.el-form-item__content) {
line-height: 32px; line-height: 32px;
} }
:deep(.el-input__wrapper) {
border-radius: 6px;
}
:deep(.el-textarea__inner) { :deep(.el-textarea__inner) {
border-radius: 6px; border-radius: 6px;
} }
@ -480,39 +462,42 @@ const handleSaveAndDetect = async () => {
margin-right: 4px; margin-right: 4px;
} }
:deep(.el-input__wrapper) { .patient-create-container :deep(.el-input__wrapper) {
background-color: rgba(51, 51, 51, 1); background-color: #282828;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: rgba(127, 127, 127, 1); border-color: rgb(54, 54, 54);
border-radius: 4px; border-radius: 4px;
box-shadow: none; box-shadow: none;
height: 40px;
} }
:deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background-color: rgba(51, 51, 51, 1); background-color: #282828;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: rgba(127, 127, 127, 1); border-color: rgb(54, 54, 54);
border-radius: 4px; border-radius: 4px;
box-shadow: none; box-shadow: none;
height: 40px;
} }
:deep(.el-form-item__label) { :deep(.el-form-item__label) {
font-size: 14px; font-size: 14px;
font-family: '苹方 粗体', '苹方 中等', '苹方', sans-serif; font-family: 'Noto Sans SC';
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
color: #FFFFFF; color: #FFFFFF;
} }
:deep(.el-col-12) { :deep(.el-col-12) {
margin-bottom: 15px; margin-bottom: 5px;
} }
:deep(.el-input__inner) { :deep(.el-input__inner) {
color: #ffffff; color: #ffffff;
font-size: 16px; font-size: 16px;
height: 40px;
} }
:deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
@ -549,4 +534,54 @@ const handleSaveAndDetect = async () => {
box-shadow: none; box-shadow: none;
border: 1px solid #f56c6c; border: 1px solid #f56c6c;
} }
.form-container-header{
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: rgba(46, 52, 59, 1);
box-sizing: border-box;
padding: 0 20px;
font-family: 'Noto Sans SC';
font-weight: 700;
font-style: normal;
font-size: 16px;
color: #FFFFFF;
text-align: left;
}
.form-actions-display{
display: flex;
justify-content: flex-end;
padding-right: 40px;
}
.formreturnCancel{
width: 80px;
height: 40px;
background: #313131;
border: 1px solid rgb(148, 148, 148);
font-family: 'Noto Sans SC';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgb(148, 148, 148);
}
.formreturnCancel:hover{
background: #1e2c49;
color: #266fff;
border: 1px solid #266fff;
}
.formsaveCancel{
width: 80px;
height: 40px;
background: #266fff;
font-family: 'Noto Sans SC';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #FFFFFF;
}
.el-date-editor.el-input{
height: 40px;
}
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff