diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..18c02f67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# 日志文件 +logs/ +*.log + +# 患者数据文件 +data/patients/*/ +!data/patients/.gitkeep + +# Node.js 依赖 +frontend/src/renderer/node_modules/ + +# Python 缓存文件 +__pycache__/ +*.pyc +*.pyo +*.pyd + +# 数据库文件(如果不需要版本控制) +*.db +*.sqlite +*.sqlite3 + +# 环境配置文件 +.env +.env.local + +# IDE 文件 +.vscode/settings.json +.idea/ + +# 系统文件 +.DS_Store +Thumbs.db + +# 构建输出 +dist/ +build/ + +# 临时文件 +*.tmp +*.temp \ No newline at end of file diff --git a/DEBUG_GUIDE.md b/DEBUG_GUIDE.md index 1eecf38b..cbb3045c 100644 --- a/DEBUG_GUIDE.md +++ b/DEBUG_GUIDE.md @@ -124,8 +124,8 @@ def handle_connect(): ### 2. RTSP流问题 ```python -# 在generate_rtsp_frames函数中设置断点 -def generate_rtsp_frames(): +# 在generate_video_frames函数中设置断点 +def generate_video_frames(): print(f'RTSP URL: {rtsp_url}') # 调试输出 # 设置断点检查rtsp_url值 cap = cv2.VideoCapture(rtsp_url) @@ -177,7 +177,7 @@ logging.getLogger().setLevel(logging.DEBUG) // 在浏览器控制台中测试WebSocket连接 const socket = io('http://127.0.0.1:5000'); socket.on('connect', () => console.log('连接成功')); -socket.emit('start_rtsp', {}); +socket.emit('start_video', {}); ``` ## 性能调试 diff --git a/MEMORY_OPTIMIZATION_GUIDE.md b/MEMORY_OPTIMIZATION_GUIDE.md deleted file mode 100644 index 9024fb41..00000000 --- a/MEMORY_OPTIMIZATION_GUIDE.md +++ /dev/null @@ -1,226 +0,0 @@ -# 内存优化指南 - -## 概述 - -本指南介绍了身体平衡评估系统中WebSocket视频流的内存优化功能,帮助您解决浏览器内存占用缓慢增长的问题。 - -## 问题描述 - -在长时间运行WebSocket视频流时,可能会遇到以下问题: -- 浏览器内存使用量持续增长 -- 页面响应速度逐渐变慢 -- 系统资源占用过高 -- 可能导致浏览器崩溃 - -## 优化方案 - -### 1. 前端优化 - -#### 1.1 帧率控制 -- **最大FPS限制**: 默认15fps,可根据需要调整 -- **跳帧机制**: 每3帧显示1帧,减少处理负担 -- **自适应帧率**: 根据内存使用情况动态调整 - -#### 1.2 内存监控 -- **实时监控**: 每10秒检查一次内存使用情况 -- **自动清理**: 内存使用率超过80%时自动垃圾回收 -- **手动控制**: 提供手动垃圾回收按钮 - -#### 1.3 图像缓存优化 -- **缓存复用**: 重用Image对象,避免频繁创建 -- **及时清理**: 及时释放不再使用的图像资源 -- **强制重绘**: 使用requestAnimationFrame优化渲染 - -### 2. 后端优化 - -#### 2.1 图像压缩 -- **尺寸压缩**: 最大分辨率320x240 -- **质量压缩**: JPEG质量设为50% -- **编码优化**: 禁用渐进式JPEG,启用优化 - -#### 2.2 内存管理 -- **内存限制**: 后端最大内存使用100MB -- **定期检查**: 每50帧检查一次内存使用 -- **强制清理**: 内存超限时强制垃圾回收 - -#### 2.3 帧处理优化 -- **跳帧处理**: 每3帧处理1帧 -- **队列管理**: 队列大小限制为1,确保实时性 -- **及时释放**: 处理完成后立即释放帧内存 - -## 使用方法 - -### 1. 启动优化版本 - -确保使用最新的优化版本: -```bash -# 启动后端(已包含内存优化) -python backend/app.py - -# 打开前端页面 -# 访问 frontend_websocket_example.html -``` - -### 2. 监控内存使用 - -在前端页面中: -1. 点击 **"内存使用情况"** 按钮查看当前内存状态 -2. 观察以下指标: - - 已使用内存 - - 总分配内存 - - 内存使用率 - - 接收帧数和跳帧率 - -### 3. 手动优化 - -当发现内存使用过高时: -1. 点击 **"强制垃圾回收"** 按钮 -2. 观察内存使用情况的变化 -3. 如果问题持续,考虑刷新页面 - -### 4. 配置调整 - -编辑 `memory_config.py` 文件来调整优化参数: - -```python -# 前端配置 -MAX_FPS = 15 # 降低可减少内存使用 -FRAME_SKIP_THRESHOLD = 2 # 增加可减少处理负担 -MAX_MEMORY_USAGE_PERCENT = 80 # 调整自动清理阈值 - -# 后端配置 -JPEG_QUALITY = 50 # 降低可减少内存使用 -MAX_FRAME_WIDTH = 320 # 降低可减少内存使用 -MAX_BACKEND_MEMORY_MB = 100 # 调整内存限制 -``` - -## 性能调优建议 - -### 1. 根据硬件调整 - -**低配置设备**: -```python -MAX_FPS = 10 -FRAME_SKIP_THRESHOLD = 4 -JPEG_QUALITY = 40 -MAX_FRAME_WIDTH = 240 -``` - -**高配置设备**: -```python -MAX_FPS = 20 -FRAME_SKIP_THRESHOLD = 1 -JPEG_QUALITY = 60 -MAX_FRAME_WIDTH = 480 -``` - -### 2. 根据网络调整 - -**慢速网络**: -- 降低帧率和质量 -- 增加跳帧比例 -- 减小图像尺寸 - -**快速网络**: -- 可适当提高帧率和质量 -- 减少跳帧比例 - -### 3. 长时间运行优化 - -对于需要长时间运行的场景: -1. 设置更严格的内存限制 -2. 增加自动清理频率 -3. 定期刷新页面(建议每2-4小时) - -## 故障排除 - -### 1. 内存仍然增长 - -**可能原因**: -- 浏览器不支持强制垃圾回收 -- 其他页面或扩展占用内存 -- 系统内存不足 - -**解决方案**: -1. 使用Chrome浏览器并启用 `--enable-precise-memory-info` 标志 -2. 关闭其他不必要的标签页 -3. 降低优化参数设置 -4. 定期刷新页面 - -### 2. 视频卡顿 - -**可能原因**: -- 帧率设置过低 -- 跳帧比例过高 -- 网络延迟 - -**解决方案**: -1. 适当提高MAX_FPS -2. 减少FRAME_SKIP_THRESHOLD -3. 检查网络连接 -4. 优化后端处理性能 - -### 3. 图像质量差 - -**可能原因**: -- JPEG质量设置过低 -- 图像尺寸压缩过度 - -**解决方案**: -1. 提高JPEG_QUALITY(建议不超过70) -2. 增加MAX_FRAME_WIDTH和MAX_FRAME_HEIGHT -3. 平衡质量和内存使用 - -## 监控和日志 - -### 1. 前端监控 - -在浏览器控制台中查看: -```javascript -// 查看内存使用 -console.log(performance.memory); - -// 查看优化统计 -console.log('帧数:', frameCount); -console.log('跳帧数:', frameSkipCount); -``` - -### 2. 后端日志 - -查看后端日志中的内存相关信息: -``` -[INFO] 内存使用: 45.2MB -[WARNING] 内存使用过高: 105.3MB,强制清理 -[INFO] 垃圾回收完成,内存降至: 38.7MB -``` - -## 最佳实践 - -1. **定期监控**: 每隔一段时间检查内存使用情况 -2. **合理配置**: 根据实际需求调整优化参数 -3. **及时清理**: 发现内存异常时及时进行清理 -4. **版本更新**: 保持使用最新的优化版本 -5. **硬件匹配**: 根据硬件配置调整参数 - -## 技术原理 - -### 1. 内存泄漏原因 -- 频繁创建Image对象 -- Base64字符串累积 -- 事件监听器未正确清理 -- 浏览器垃圾回收不及时 - -### 2. 优化原理 -- 对象复用减少创建开销 -- 及时释放减少内存占用 -- 跳帧处理减少处理负担 -- 强制垃圾回收释放内存 - -### 3. 性能平衡 -- 内存使用 vs 视频质量 -- 实时性 vs 资源占用 -- 用户体验 vs 系统稳定性 - ---- - -如有其他问题,请查看项目文档或联系技术支持。 \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index bfefc4c0..071f5195 100644 --- a/backend/app.py +++ b/backend/app.py @@ -45,7 +45,7 @@ logger = logging.getLogger(__name__) # 创建Flask应用 app = Flask(__name__) app.config['SECRET_KEY'] = 'body-balance-detection-system-2024' -socketio = SocketIO(app, cors_allowed_origins='*', async_mode='threading') +socketio = SocketIO(app, cors_allowed_origins='*', async_mode='threading', logger=False, engineio_logger=False, ping_timeout=60, ping_interval=25) # 启用CORS支持 CORS(app, origins='*', supports_credentials=True, allow_headers=['Content-Type', 'Authorization'], methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) @@ -55,8 +55,8 @@ app.register_blueprint(detection_bp) # 读取RTSP配置 config = configparser.ConfigParser() -config.read(os.path.join(os.path.dirname(__file__), 'config.ini'), encoding='utf-8') -rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None) +config.read(os.path.join(os.path.dirname(__file__), '..', 'config.ini'), encoding='utf-8') +device_index = config.get('CAMERA', 'device_index', fallback=None) # 全局变量 db_manager = None @@ -76,12 +76,21 @@ def init_app(): try: # 创建必要的目录 os.makedirs('logs', exist_ok=True) - os.makedirs('data', exist_ok=True) - os.makedirs('exports', exist_ok=True) - os.makedirs('videos', exist_ok=True) + os.makedirs('data', exist_ok=True) # 从配置文件读取数据库路径 - db_path = app_config.get('DATABASE', 'path', 'backend/data/body_balance.db') + db_path_config = app_config.get('DATABASE', 'path', 'backend/data/body_balance.db') + # 如果是相对路径,基于当前脚本目录解析 + if not os.path.isabs(db_path_config): + # 获取当前脚本所在目录(backend目录) + current_dir = os.path.dirname(os.path.abspath(__file__)) + # 如果配置路径以 'backend/' 开头,去掉这个前缀 + if db_path_config.startswith('backend/'): + db_path_config = db_path_config[8:] # 去掉 'backend/' 前缀 + db_path = os.path.join(current_dir, db_path_config) + else: + db_path = db_path_config + # 确保数据库目录存在 db_dir = os.path.dirname(db_path) os.makedirs(db_dir, exist_ok=True) @@ -313,16 +322,14 @@ def forgot_password(): user = cursor.fetchone() if user: - # 用户存在且手机号匹配,返回密码 - # 注意:这里返回的是加密后的密码,实际应用中需要解密或重置 - # 为了演示,我们假设有一个简单的解密方法 - encrypted_password = user['password'] + # 用户存在且手机号匹配,返回数据库中存储的实际密码 + actual_password = user['password'] + + logger.info(f'用户 {username} 密码查询成功') - # 这里简化处理,实际应该有更安全的密码找回机制 - # 由于使用MD5加密,无法直接解密,所以返回提示信息 return jsonify({ 'success': True, - 'password': '1234567', # 演示用固定密码 + 'password': actual_password, # 返回数据库中存储的实际密码 'message': '密码找回成功' }) else: @@ -949,35 +956,35 @@ if __name__ == '__main__': # ==================== WebSocket 事件处理 ==================== -@socketio.on('start_rtsp') -def handle_start_rtsp(data=None): - logger.info(f'收到start_rtsp事件,客户端ID: {request.sid}, 数据: {data}') +@socketio.on('start_video') +def handle_start_video(data=None): + logger.info(f'收到start_video事件,客户端ID: {request.sid}, 数据: {data}') try: if video_stream_manager: - result = video_stream_manager.start_rtsp_stream() - emit('rtsp_status', result) + result = video_stream_manager.start_video_stream() + emit('video_status', result) else: - emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'}) + emit('video_status', {'status': 'error', 'message': '视频流管理器未初始化'}) except Exception as e: - logger.error(f'启动RTSP失败: {e}') - emit('rtsp_status', {'status': 'error', 'message': f'启动失败: {str(e)}'}) + logger.error(f'启动视频流失败: {e}') + emit('video_status', {'status': 'error', 'message': f'启动失败: {str(e)}'}) -@socketio.on('stop_rtsp') -def handle_stop_rtsp(data=None): - logger.info(f'收到stop_rtsp事件,客户端ID: {request.sid}, 数据: {data}') +@socketio.on('stop_video') +def handle_stop_video(data=None): + logger.info(f'收到stop_video事件,客户端ID: {request.sid}, 数据: {data}') try: if video_stream_manager: - result = video_stream_manager.stop_rtsp_stream() - emit('rtsp_status', result) + result = video_stream_manager.stop_video_stream() + emit('video_status', result) else: - emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'}) + emit('video_status', {'status': 'error', 'message': '视频流管理器未初始化'}) except Exception as e: - logger.error(f'停止RTSP失败: {e}') - emit('rtsp_status', {'status': 'error', 'message': f'停止失败: {str(e)}'}) + logger.error(f'停止视频流失败: {e}') + emit('video_status', {'status': 'error', 'message': f'停止失败: {str(e)}'}) @socketio.on('connect') def handle_connect(): diff --git a/backend/check_body_balance_db.py b/backend/check_body_balance_db.py deleted file mode 100644 index ce962258..00000000 --- a/backend/check_body_balance_db.py +++ /dev/null @@ -1,84 +0,0 @@ -from database import DatabaseManager -import os - -# 检查backend/data目录下的数据库 -db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db') -db = DatabaseManager(db_path) -db.init_database() -conn = db.get_connection() -cursor = conn.cursor() - -# 检查患者总数 -cursor.execute('SELECT COUNT(*) FROM patients') -count = cursor.fetchone()[0] -print(f'body_balance.db中的患者总数: {count}') - -# 查看前5条患者数据 -cursor.execute('SELECT * FROM patients LIMIT 5') -rows = cursor.fetchall() -print('前5条患者数据:') -for row in rows: - print(dict(row)) - -conn.close() - -# 如果没有数据,添加测试数据 -if count == 0: - print('\n数据库中没有患者数据,添加测试数据...') - test_patients = [ - { - 'name': '张三', - 'gender': '男', - 'age': 30, - 'birth_date': '1994-01-15', - 'nationality': '汉族', - 'height': 175.0, - 'weight': 70.0, - 'phone': '13800138001', - 'shoe_size': '42', - 'medical_history': '无', - 'notes': '测试患者1' - }, - { - 'name': '李四', - 'gender': '女', - 'age': 25, - 'birth_date': '1999-03-22', - 'nationality': '汉族', - 'height': 165.0, - 'weight': 55.0, - 'phone': '13800138002', - 'shoe_size': '37', - 'medical_history': '高血压', - 'notes': '测试患者2' - }, - { - 'name': '王五', - 'gender': '男', - 'age': 35, - 'birth_date': '1989-07-08', - 'nationality': '回族', - 'height': 180.0, - 'weight': 80.0, - 'phone': '13800138003', - 'shoe_size': '44', - 'medical_history': '糖尿病', - 'notes': '测试患者3' - } - ] - - for patient in test_patients: - patient_id = db.create_patient(patient) - print(f'添加患者: {patient["name"]}, ID: {patient_id}') - - print('\n重新测试get_patients方法:') - patients = db.get_patients(page=1, size=10, keyword='') - print(f'查询结果: {len(patients)}条记录') - for p in patients: - print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)') -else: - print('\n数据库中已有患者数据,测试get_patients方法:') - patients = db.get_patients(page=1, size=10, keyword='') - print(f'查询结果: {len(patients)}条记录') - for p in patients: - print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)') \ No newline at end of file diff --git a/backend/check_db.py b/backend/check_db.py deleted file mode 100644 index 0d7a9b2e..00000000 --- a/backend/check_db.py +++ /dev/null @@ -1,30 +0,0 @@ -from database import DatabaseManager -import os - -# 使用backend/data目录下的数据库路径 -db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db') -db = DatabaseManager(db_path) -db.init_database() -conn = db.get_connection() -cursor = conn.cursor() - -# 检查患者总数 -cursor.execute('SELECT COUNT(*) FROM patients') -count = cursor.fetchone()[0] -print(f'患者总数: {count}') - -# 查看前5条患者数据 -cursor.execute('SELECT * FROM patients LIMIT 5') -rows = cursor.fetchall() -print('前5条患者数据:') -for row in rows: - print(dict(row)) - -# 检查表结构 -cursor.execute("PRAGMA table_info(patients)") -columns = cursor.fetchall() -print('\npatients表结构:') -for col in columns: - print(dict(col)) - -conn.close() \ No newline at end of file diff --git a/backend/config.ini b/backend/config.ini deleted file mode 100644 index 17ebd21c..00000000 --- a/backend/config.ini +++ /dev/null @@ -1,45 +0,0 @@ -[APP] -name = Body Balance Evaluation System -version = 1.0.0 -debug = false -log_level = INFO - -[SERVER] -host = 127.0.0.1 -port = 5000 -cors_origins = * - -[DATABASE] -path = backend/data/body_balance.db -backup_interval = 24 -max_backups = 7 - -[DEVICES] -camera_index = 0 -camera_width = 640 -camera_height = 480 -camera_fps = 30 -imu_port = COM3 -pressure_port = COM4 - -[DETECTION] -default_duration = 60 -sampling_rate = 30 -balance_threshold = 0.2 -posture_threshold = 5.0 - -[DATA_PROCESSING] -filter_window = 5 -outlier_threshold = 2.0 -chart_dpi = 300 -export_format = csv - -[SECURITY] -secret_key = 026efbf83a2fe101f168780740da86bf1c9260625458e6782738aa9cf18f8e37 -session_timeout = 3600 -max_login_attempts = 5 - - -[CAMERA] -rtsp_url = rtsp://admin:JY123456@192.168.1.61:554/Streaming/Channels/101 - diff --git a/backend/data/body_balance.db b/backend/data/body_balance.db index ffda48d7..3527aae8 100644 Binary files a/backend/data/body_balance.db and b/backend/data/body_balance.db differ diff --git a/backend/database.py b/backend/database.py index 19b66892..4254458c 100644 --- a/backend/database.py +++ b/backend/database.py @@ -8,7 +8,7 @@ import sqlite3 import json import uuid -from datetime import datetime +from datetime import datetime, timezone, timedelta from typing import List, Dict, Optional, Any import logging @@ -20,6 +20,12 @@ class DatabaseManager: def __init__(self, db_path: str): self.db_path = db_path self.connection = None + # 设置中国上海时区 (UTC+8) + self.china_tz = timezone(timedelta(hours=8)) + + def get_china_time(self) -> str: + """获取中国时区的当前时间字符串""" + return datetime.now(self.china_tz).strftime('%Y-%m-%d %H:%M:%S') def get_connection(self) -> sqlite3.Connection: """获取数据库连接""" @@ -143,10 +149,9 @@ class DatabaseManager: admin_exists = cursor.fetchone()[0] if admin_exists == 0: - import hashlib admin_id = str(uuid.uuid4()) - # 默认密码为 admin123,使用MD5加密 - admin_password = hashlib.md5('admin123'.encode()).hexdigest() + # 默认密码为 admin123,明文存储 + admin_password = 'admin123' cursor.execute(''' INSERT INTO users (id, name, username, password, is_active, user_type) @@ -172,12 +177,14 @@ class DatabaseManager: try: patient_id = str(uuid.uuid4()) + # 使用中国时区时间 + china_time = self.get_china_time() cursor.execute(''' INSERT INTO patients ( id, name, gender, age, birth_date, height, weight, - phone, shoe_size, medical_history, notes - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + phone, shoe_size, medical_history, notes, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( patient_id, patient_data.get('name'), @@ -189,7 +196,9 @@ class DatabaseManager: patient_data.get('phone'), patient_data.get('shoe_size'), patient_data.get('medical_history'), - patient_data.get('notes') + patient_data.get('notes'), + china_time, + china_time )) conn.commit() @@ -270,11 +279,14 @@ class DatabaseManager: cursor = conn.cursor() try: + # 使用中国时区时间 + china_time = self.get_china_time() + cursor.execute(''' UPDATE patients SET name = ?, gender = ?, age = ?, birth_date = ?, height = ?, weight = ?, phone = ?, shoe_size = ?, medical_history = ?, notes = ?, - updated_at = CURRENT_TIMESTAMP + updated_at = ? WHERE id = ? ''', ( patient_data.get('name'), @@ -287,6 +299,7 @@ class DatabaseManager: patient_data.get('shoe_size'), patient_data.get('medical_history'), patient_data.get('notes'), + china_time, patient_id )) @@ -344,17 +357,22 @@ class DatabaseManager: try: session_id = str(uuid.uuid4()) + # 使用中国时区时间 + china_time = self.get_china_time() + cursor.execute(''' INSERT INTO detection_sessions ( - id, patient_id, duration, frequency, settings, status - ) VALUES (?, ?, ?, ?, ?, ?) + id, patient_id, duration, frequency, settings, status, start_time, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', ( session_id, patient_id, settings.get('duration', 60), settings.get('frequency', 60), json.dumps(settings), - 'created' + 'created', + china_time, + china_time )) conn.commit() @@ -373,11 +391,13 @@ class DatabaseManager: try: if status in ['completed', 'stopped', 'error']: + # 使用中国时区时间 + china_time = self.get_china_time() cursor.execute(''' UPDATE detection_sessions SET - status = ?, data_points = ?, end_time = CURRENT_TIMESTAMP + status = ?, data_points = ?, end_time = ? WHERE id = ? - ''', (status, data_points, session_id)) + ''', (status, data_points, china_time, session_id)) else: cursor.execute(''' UPDATE detection_sessions SET @@ -553,12 +573,15 @@ class DatabaseManager: cursor = conn.cursor() try: + # 使用中国时区时间 + china_time = self.get_china_time() + # 保存不同类型的数据 for data_type, data_value in data.items(): cursor.execute(''' - INSERT INTO detection_data (session_id, data_type, data_value) - VALUES (?, ?, ?) - ''', (session_id, data_type, json.dumps(data_value))) + INSERT INTO detection_data (session_id, data_type, data_value, timestamp) + VALUES (?, ?, ?, ?) + ''', (session_id, data_type, json.dumps(data_value), china_time)) conn.commit() @@ -679,10 +702,13 @@ class DatabaseManager: try: value_str = json.dumps(value) if not isinstance(value, str) else value + # 使用中国时区时间 + china_time = self.get_china_time() + cursor.execute(''' INSERT OR REPLACE INTO system_settings (key, value, description, updated_at) - VALUES (?, ?, ?, CURRENT_TIMESTAMP) - ''', (key, value_str, description)) + VALUES (?, ?, ?, ?) + ''', (key, value_str, description, china_time)) conn.commit() logger.info(f'设置系统设置: {key}') @@ -700,8 +726,6 @@ class DatabaseManager: cursor = conn.cursor() try: - import hashlib - # 检查手机号是否已存在(如果提供了手机号) if user_data.get('phone'): cursor.execute('SELECT COUNT(*) FROM users WHERE phone = ?', (user_data['phone'],)) @@ -712,20 +736,25 @@ class DatabaseManager: } user_id = str(uuid.uuid4()) - # 密码MD5加密 - password_hash = hashlib.md5(user_data['password'].encode()).hexdigest() + # 密码明文存储 + password = user_data['password'] + # 使用中国时区时间 + china_time = self.get_china_time() cursor.execute(''' - INSERT INTO users (id, name, username, password, phone, is_active, user_type) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO users (id, name, username, password, phone, is_active, user_type, register_date, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( user_id, user_data['name'], user_data['username'], - password_hash, + password, user_data.get('phone'), # 手机号可选 - 0, # 新注册用户默认未激活,需要管理员审核 - 'user' + 1, # 新注册用户默认激活 + 'user', + china_time, + china_time, + china_time )) conn.commit() @@ -757,14 +786,10 @@ class DatabaseManager: cursor = conn.cursor() try: - import hashlib - - password_hash = hashlib.md5(password.encode()).hexdigest() - cursor.execute(''' SELECT * FROM users WHERE username = ? AND password = ? AND is_active = 1 - ''', (username, password_hash)) + ''', (username, password)) row = cursor.fetchone() if row: @@ -863,12 +888,15 @@ class DatabaseManager: cursor = conn.cursor() try: + # 使用中国时区时间 + china_time = self.get_china_time() + cursor.execute(''' UPDATE users SET is_active = ?, - updated_at = CURRENT_TIMESTAMP + updated_at = ? WHERE id = ? - ''', (1 if approved else 0, user_id)) + ''', (1 if approved else 0, china_time, user_id)) conn.commit() status = '通过' if approved else '拒绝' @@ -903,10 +931,7 @@ class DatabaseManager: cursor = conn.cursor() try: - # 如果包含密码,需要加密 - if 'password' in user_data: - import hashlib - user_data['password'] = hashlib.md5(user_data['password'].encode()).hexdigest() + # 密码明文存储,无需加密处理 # 构建更新语句 fields = [] @@ -918,7 +943,10 @@ class DatabaseManager: values.append(value) if fields: - fields.append('updated_at = CURRENT_TIMESTAMP') + # 使用中国时区时间 + china_time = self.get_china_time() + fields.append('updated_at = ?') + values.append(china_time) values.append(user_id) sql = f"UPDATE users SET {', '.join(fields)} WHERE id = ?" diff --git a/backend/device_manager.py b/backend/device_manager.py index 2700e97c..fcf7e398 100644 --- a/backend/device_manager.py +++ b/backend/device_manager.py @@ -492,9 +492,9 @@ class VideoStreamManager: def __init__(self, socketio=None): self.socketio = socketio - self.rtsp_url = None - self.rtsp_thread = None - self.rtsp_running = False + self.device_index = None + self.video_thread = None + self.video_running = False # 用于异步编码的线程池和队列 self.encoding_executor = ThreadPoolExecutor(max_workers=2) @@ -515,13 +515,14 @@ class VideoStreamManager: """加载RTSP配置""" try: config = configparser.ConfigParser() - config_path = os.path.join(os.path.dirname(__file__), 'config.ini') + config_path = os.path.join(os.path.dirname(__file__), '..', 'config.ini') config.read(config_path, encoding='utf-8') - self.rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None) - logger.info(f'RTSP配置加载完成: {self.rtsp_url}') + device_index_str = config.get('CAMERA', 'device_index', fallback='0') + self.device_index = int(device_index_str) if device_index_str else 0 + logger.info(f'视频监控设备配置加载完成,设备号: {self.device_index}') except Exception as e: - logger.error(f'加载RTSP配置失败: {e}') - self.rtsp_url = None + logger.error(f'视频监控设备配置失败: {e}') + self.device_index = None def get_memory_usage(self): """获取当前进程内存使用量(字节)""" @@ -585,7 +586,7 @@ class VideoStreamManager: # 发送数据 if self.socketio: - self.socketio.emit('rtsp_frame', { + self.socketio.emit('video_frame', { 'image': jpg_as_text, 'frame_id': frame_count, 'timestamp': time.time() @@ -603,7 +604,7 @@ class VideoStreamManager: def frame_encoding_worker(self): """帧编码工作线程""" - while self.rtsp_running: + while self.video_running: try: # 从队列获取帧 frame, frame_count = self.frame_queue.get(timeout=1) @@ -641,22 +642,22 @@ class VideoStreamManager: return frame - def generate_rtsp_frames(self): - """生成RTSP帧""" + def generate_video_frames(self): + """生成视频监控帧""" frame_count = 0 error_count = 0 use_test_mode = False last_frame_time = time.time() - logger.info(f'开始生成RTSP帧,URL: {self.rtsp_url}') + logger.info(f'开始生成视频监控帧,设备号: {self.device_index}') try: - cap = cv2.VideoCapture(self.rtsp_url) + cap = cv2.VideoCapture(self.device_index) if not cap.isOpened(): - logger.warning(f'无法打开RTSP流: {self.rtsp_url},切换到测试模式') + logger.warning(f'无法打开视频监控流: {self.device_index},切换到测试模式') use_test_mode = True if self.socketio: - self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用测试视频源'}) + self.socketio.emit('video_status', {'status': 'started', 'message': '使用测试视频源'}) else: # 最激进的实时优化设置 cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) # 完全禁用缓冲区 @@ -665,40 +666,40 @@ class VideoStreamManager: # 设置更低的分辨率以减少处理时间 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) - logger.info('RTSP流已打开,开始推送帧(激进实时模式)') + logger.info('视频监控流已打开,开始推送帧(激进实时模式)') if self.socketio: - self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用RTSP视频源(激进实时模式)'}) + self.socketio.emit('video_status', {'status': 'started', 'message': '使用视频监控视频源(激进实时模式)'}) - self.rtsp_running = True + self.video_running = True # 启动帧编码工作线程 encoding_thread = threading.Thread(target=self.frame_encoding_worker) encoding_thread.daemon = True encoding_thread.start() - while self.rtsp_running: + while self.video_running: if use_test_mode: # 使用测试模式生成帧 frame = self.generate_test_frame(frame_count) ret = True else: - # 使用RTSP流,添加帧跳过机制减少延迟 + # 使用视频监控流,添加帧跳过机制减少延迟 ret, frame = cap.read() if not ret: error_count += 1 - logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...') + logger.warning(f'视频监控读取帧失败(第{error_count}次),尝试重连...') if 'cap' in locals(): cap.release() if error_count > 5: - logger.warning('RTSP连接失败次数过多,切换到测试模式') + logger.warning('视频监控连接失败次数过多,切换到测试模式') use_test_mode = True if self.socketio: - self.socketio.emit('rtsp_status', {'status': 'switched', 'message': '已切换到测试视频源'}) + self.socketio.emit('video_status', {'status': 'switched', 'message': '已切换到测试视频源'}) continue # 立即重连,不等待 - cap = cv2.VideoCapture(self.rtsp_url) + cap = cv2.VideoCapture(self.device_index) if cap.isOpened(): # 重连时应用相同的激进实时设置 cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) @@ -766,67 +767,68 @@ class VideoStreamManager: logger.error(f'帧队列处理失败: {e}') except Exception as e: - logger.error(f'RTSP推流异常: {e}') + logger.error(f'监控视频推流异常: {e}') if self.socketio: - self.socketio.emit('rtsp_status', {'status': 'error', 'message': f'推流异常: {str(e)}'}) + self.socketio.emit('video_status', {'status': 'error', 'message': f'推流异常: {str(e)}'}) finally: if 'cap' in locals(): cap.release() - self.rtsp_running = False - logger.info(f'RTSP推流结束,总共推送了 {frame_count} 帧') + self.video_running = False + logger.info(f'视频监控推流结束,总共推送了 {frame_count} 帧') - def start_rtsp_stream(self): - """启动RTSP推流""" + def start_video_stream(self): + """启动视频监控推流""" try: - if self.rtsp_thread and self.rtsp_thread.is_alive(): - logger.warning('RTSP线程已在运行') - return {'status': 'already_running', 'message': 'RTSP已在运行'} + if self.video_thread and self.video_thread.is_alive(): + logger.warning('视频监控线程已在运行') + return {'status': 'already_running', 'message': '视频监控已在运行'} - if not self.rtsp_url: - logger.error('RTSP URL未配置') - return {'status': 'error', 'message': 'RTSP URL未配置'} + if not self.device_index: + logger.error('视频监控相机未配置') + return {'status': 'error', 'message': '视频监控相机未配置'} - logger.info(f'启动RTSP线程,URL: {self.rtsp_url}') - self.rtsp_thread = threading.Thread(target=self.generate_rtsp_frames) - self.rtsp_thread.daemon = True - self.rtsp_thread.start() + logger.info(f'视频启动监控线程,设备号: {self.device_index}') + self.video_thread = threading.Thread(target=self.generate_video_frames) + self.video_thread.daemon = True + self.video_thread.start() + self.video_running = True - logger.info('RTSP线程已启动') - return {'status': 'started', 'message': 'RTSP推流已启动'} + logger.info('视频监控线程已启动') + return {'status': 'started', 'message': '视频监控线程已启动'} except Exception as e: - logger.error(f'启动RTSP失败: {e}') - return {'status': 'error', 'message': f'启动失败: {str(e)}'} + logger.error(f'视频监控线程启动失败: {e}') + return {'status': 'error', 'message': f'视频监控线程启动失败: {str(e)}'} - def stop_rtsp_stream(self): - """停止RTSP推流""" + def stop_video_stream(self): + """停止视频监控推流""" try: - self.rtsp_running = False - logger.info('RTSP推流已停止') - return {'status': 'stopped', 'message': 'RTSP推流已停止'} + self.video_running = False + logger.info('视频监控推流已停止') + return {'status': 'stopped', 'message': '视频监控推流已停止'} except Exception as e: - logger.error(f'停止RTSP失败: {e}') + logger.error(f'停止视频监控推流失败: {e}') return {'status': 'error', 'message': f'停止失败: {str(e)}'} def is_streaming(self): """检查是否正在推流""" - return self.rtsp_running + return self.video_running def get_stream_status(self): """获取推流状态""" return { - 'running': self.rtsp_running, - 'rtsp_url': self.rtsp_url, - 'thread_alive': self.rtsp_thread.is_alive() if self.rtsp_thread else False + 'running': self.video_running, + 'device_index': self.device_index, + 'thread_alive': self.video_thread.is_alive() if self.video_thread else False } def cleanup(self): """清理资源""" try: - self.rtsp_running = False - if self.rtsp_thread and self.rtsp_thread.is_alive(): - self.rtsp_thread.join(timeout=2) + self.video_running = False + if self.video_thread and self.video_thread.is_alive(): + self.video_thread.join(timeout=2) self.encoding_executor.shutdown(wait=False) diff --git a/backend/test_get_patients_simple.py b/backend/test_get_patients_simple.py deleted file mode 100644 index 0dab895c..00000000 --- a/backend/test_get_patients_simple.py +++ /dev/null @@ -1,60 +0,0 @@ -from database import DatabaseManager -import os - -# 使用backend/data目录下的数据库路径 -db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db') -db = DatabaseManager(db_path) -db.init_database() - -print('测试get_patients方法:') - -# 测试1: 无参数调用 -print('\n1. 无参数调用 get_patients():') -try: - patients = db.get_patients() - print(f' 结果: {len(patients)}条记录') - for i, p in enumerate(patients[:3]): # 只显示前3条 - print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)') -except Exception as e: - print(f' 错误: {e}') - -# 测试2: 带分页参数 -print('\n2. 带分页参数 get_patients(page=1, size=5):') -try: - patients = db.get_patients(page=1, size=5) - print(f' 结果: {len(patients)}条记录') - for i, p in enumerate(patients): - print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)') -except Exception as e: - print(f' 错误: {e}') - -# 测试3: 带关键字搜索 -print('\n3. 带关键字搜索 get_patients(keyword="张"):') -try: - patients = db.get_patients(keyword='张') - print(f' 结果: {len(patients)}条记录') - for i, p in enumerate(patients): - print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)') -except Exception as e: - print(f' 错误: {e}') - -# 测试4: 模拟app.py中的调用方式 -print('\n4. 模拟app.py中的调用方式:') -try: - page = 1 - size = 10 - keyword = '' - - patients = db.get_patients(page, size, keyword) - total = db.get_patients_count(keyword) - - print(f' 患者列表: {len(patients)}条记录') - print(f' 总数: {total}') - print(f' 页码: {page}, 每页: {size}') - - for i, p in enumerate(patients[:3]): # 只显示前3条 - print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)') -except Exception as e: - print(f' 错误: {e}') - -print('\n测试完成!') \ No newline at end of file diff --git a/backend/test_patients.py b/backend/test_patients.py deleted file mode 100644 index 9a550212..00000000 --- a/backend/test_patients.py +++ /dev/null @@ -1,78 +0,0 @@ -from database import DatabaseManager -import os -import uuid -from datetime import datetime - -# 使用backend/data目录下的数据库路径 -db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db') -db = DatabaseManager(db_path) -db.init_database() - -# 添加一些测试患者数据 -test_patients = [ - { - 'name': '张三', - 'gender': '男', - 'age': 30, - 'birth_date': '1994-01-15', - 'nationality': '汉族', - 'height': 175.0, - 'weight': 70.0, - 'phone': '13800138001', - 'shoe_size': '42', - 'medical_history': '无', - 'notes': '测试患者1' - }, - { - 'name': '李四', - 'gender': '女', - 'age': 25, - 'birth_date': '1999-03-22', - 'nationality': '汉族', - 'height': 165.0, - 'weight': 55.0, - 'phone': '13800138002', - 'shoe_size': '37', - 'medical_history': '高血压', - 'notes': '测试患者2' - }, - { - 'name': '王五', - 'gender': '男', - 'age': 35, - 'birth_date': '1989-07-08', - 'nationality': '回族', - 'height': 180.0, - 'weight': 80.0, - 'phone': '13800138003', - 'shoe_size': '44', - 'medical_history': '糖尿病', - 'notes': '测试患者3' - } -] - -print('添加测试患者数据...') -for patient in test_patients: - patient_id = db.create_patient(patient) - print(f'添加患者: {patient["name"]}, ID: {patient_id}') - -print('\n测试get_patients方法:') -# 测试无关键字查询 -patients = db.get_patients(page=1, size=10, keyword='') -print(f'无关键字查询结果: {len(patients)}条记录') -for p in patients: - print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)') - -# 测试关键字查询 -patients = db.get_patients(page=1, size=10, keyword='张') -print(f'\n关键字"张"查询结果: {len(patients)}条记录') -for p in patients: - print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)') - -# 测试电话号码查询 -patients = db.get_patients(page=1, size=10, keyword='13800138002') -print(f'\n电话号码查询结果: {len(patients)}条记录') -for p in patients: - print(f' - {p["name"]} ({p["phone"]})') - -print('\n测试完成!') \ No newline at end of file diff --git a/backend/utils.py b/backend/utils.py index cc9f5ba1..437dc9e9 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) class Config: """配置管理器""" - def __init__(self, config_file: str = 'config.ini'): + def __init__(self, config_file: str = '../config.ini'): self.config_file = Path(config_file) self.config = configparser.ConfigParser() self._load_config() diff --git a/config.ini b/config.ini index 8abc071a..26c4c0c7 100644 --- a/config.ini +++ b/config.ini @@ -39,3 +39,5 @@ secret_key = 79fcc4983d478c2ee672f3305d5e12c7c84fd1b58a18acb650e9f8125bfa805f session_timeout = 3600 max_login_attempts = 5 +[CAMERA] +device_index = 1 diff --git a/config.json b/config.json index ca3c3780..dfd0bdb6 100644 --- a/config.json +++ b/config.json @@ -27,7 +27,7 @@ "debug": false, "cors": { "enabled": true, - "origins": ["http://localhost:5173", "http://127.0.0.1:5173"] + "origins": ["http://192.168.1.38:3000"] }, "ssl": { "enabled": false, diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 00000000..3224dd60 --- /dev/null +++ b/data/.gitkeep @@ -0,0 +1,3 @@ +# 数据目录 +# 此文件用于确保 data 目录在版本控制中被保留 +# 实际的数据文件会被 .gitignore 忽略 \ No newline at end of file diff --git a/debug_server.py b/debug_server.py index 2968744f..b51141e4 100644 --- a/debug_server.py +++ b/debug_server.py @@ -13,6 +13,7 @@ import os import sys import logging +import socket from pathlib import Path # 添加项目路径 @@ -45,6 +46,16 @@ def setup_debug_logging(): logger.info('调试日志已启用') return logger +def get_local_ip(): + """获取本机IP地址""" + try: + # 创建一个UDP socket连接到外部地址来获取本机IP + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + return s.getsockname()[0] + except Exception: + return "127.0.0.1" + def check_debug_environment(): """检查调试环境""" logger = logging.getLogger(__name__) @@ -57,7 +68,7 @@ def check_debug_environment(): # 检查必要文件 required_files = [ 'backend/app.py', - 'backend/config.ini', + 'config.ini', 'backend/requirements.txt' ] @@ -86,17 +97,21 @@ def start_debug_server(): logger.info('初始化应用...') init_app() + # 获取本机IP地址 + local_ip = get_local_ip() + # 启动调试服务器 logger.info('启动调试服务器...') logger.info('调试模式已启用 - 可以在IDE中设置断点') - logger.info('服务器地址: http://127.0.0.1:5000') + logger.info('本地访问: http://127.0.0.1:5000') + logger.info(f'远程访问: http://{local_ip}:5000') logger.info('健康检查: http://127.0.0.1:5000/health') logger.info('按 Ctrl+C 停止服务器') - # 启动SocketIO服务器(支持调试) + # 启动SocketIO服务器(支持调试和远程访问) socketio.run( app, - host='127.0.0.1', + host='0.0.0.0', # 允许所有IP访问 port=5000, debug=True, use_reloader=True, # 启用热重载 diff --git a/document/姿态检测页.png b/document/姿态检测页.png new file mode 100644 index 00000000..4a356376 Binary files /dev/null and b/document/姿态检测页.png differ diff --git a/document/开始检测中的录屏界面.png b/document/开始检测中的录屏界面.png new file mode 100644 index 00000000..cd3a3bc0 Binary files /dev/null and b/document/开始检测中的录屏界面.png differ diff --git a/document/登录进入的起始页.png b/document/登录进入的起始页.png new file mode 100644 index 00000000..80f7e96f Binary files /dev/null and b/document/登录进入的起始页.png differ diff --git a/document/登录页.png b/document/登录页.png new file mode 100644 index 00000000..f345fc72 Binary files /dev/null and b/document/登录页.png differ diff --git a/frontend/src/renderer/package-lock.json b/frontend/src/renderer/package-lock.json index 16597499..bb8c043f 100644 --- a/frontend/src/renderer/package-lock.json +++ b/frontend/src/renderer/package-lock.json @@ -14,6 +14,7 @@ "element-plus": "^2.3.9", "html2canvas": "^1.4.1", "pinia": "^2.1.6", + "socket.io-client": "^4.7.2", "vue": "^3.3.4", "vue-echarts": "^6.6.1", "vue-router": "^4.2.4" @@ -767,6 +768,12 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", @@ -1024,6 +1031,23 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1092,6 +1116,28 @@ "vue": "^3.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1467,6 +1513,12 @@ "node": ">= 0.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1621,6 +1673,34 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1826,6 +1906,35 @@ "vue": "^3.2.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/zrender": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", diff --git a/frontend/src/renderer/package.json b/frontend/src/renderer/package.json index d01ebb49..ea8f5ff2 100644 --- a/frontend/src/renderer/package.json +++ b/frontend/src/renderer/package.json @@ -14,6 +14,7 @@ "element-plus": "^2.3.9", "html2canvas": "^1.4.1", "pinia": "^2.1.6", + "socket.io-client": "^4.7.2", "vue": "^3.3.4", "vue-echarts": "^6.6.1", "vue-router": "^4.2.4" diff --git a/frontend/src/renderer/public/logo.png b/frontend/src/renderer/public/logo.png new file mode 100644 index 00000000..19e01fcd --- /dev/null +++ b/frontend/src/renderer/public/logo.png @@ -0,0 +1,58 @@ + \ No newline at end of file diff --git a/frontend/src/renderer/src/services/api.js b/frontend/src/renderer/src/services/api.js index 6d7683f2..5e0747c6 100644 --- a/frontend/src/renderer/src/services/api.js +++ b/frontend/src/renderer/src/services/api.js @@ -108,13 +108,13 @@ export const deviceAPI = { }, // 校准设备 - calibrateDevice(deviceType) { - return api.post(`/api/devices/${deviceType}/calibrate`) + calibrateDevice() { + return api.post('/api/devices/calibrate') }, // 测试设备 - testDevice(deviceType) { - return api.post(`/api/devices/${deviceType}/test`) + testDevice() { + return api.post('/api/devices/test') } } diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index ce687361..910e71e6 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -428,14 +428,15 @@ function connectWebSocket() { } }) - // 监听RTSP状态事件 - socket.on('rtsp_status', (data) => { - console.log('📺 RTSP状态:', data) + // 监听视频状态事件 + socket.on('video_status', (data) => { + console.log('📺 视频状态:', data) }) - // 监听RTSP帧数据 - socket.on('rtsp_frame', (data) => { + // 监听视频帧数据 + socket.on('video_frame', (data) => { frameCount++ + console.log(`📺 收到视频帧 #${frameCount}, 数据大小: ${data.image ? data.image.length : 0} 字符`) displayFrame(data.image) }) @@ -461,16 +462,16 @@ function disconnectWebSocket() { } } -// 启动RTSP +// 启动视频流 function startRtsp() { if (socket && socket.connected) { - console.log('🚀 发送start_rtsp事件') + console.log('🚀 发送start_video事件') - socket.emit('start_rtsp', {}, (ack) => { + socket.emit('start_video', {}, (ack) => { if (ack) { - console.log('✅ start_rtsp事件已确认:', ack) + console.log('✅ start_video事件已确认:', ack) } else { - console.log('⚠️ start_rtsp事件无确认响应') + console.log('⚠️ start_video事件无确认响应') } }) @@ -484,23 +485,23 @@ function startRtsp() { }, 5000) } else { - console.error('❌ WebSocket未连接,无法启动RTSP') + console.error('❌ WebSocket未连接,无法启动视频流') } } -// 停止RTSP +// 停止视频流 function stopRtsp() { if (socket && socket.connected) { - console.log('🛑 发送stop_rtsp事件') - socket.emit('stop_rtsp', {}, (ack) => { + console.log('🛑 发送stop_video事件') + socket.emit('stop_video', {}, (ack) => { if (ack) { - console.log('✅ stop_rtsp事件已确认:', ack) + console.log('✅ stop_video事件已确认:', ack) } else { - console.log('⚠️ stop_rtsp事件无确认响应') + console.log('⚠️ stop_video事件无确认响应') } }) } else { - console.error('❌ WebSocket未连接,无法停止RTSP') + console.error('❌ WebSocket未连接,无法停止视频流') } } @@ -511,7 +512,12 @@ function stopRtsp() { // 简单的帧显示函数 function displayFrame(base64Image) { - rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image + if (base64Image && base64Image.length > 0) { + rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image + console.log(`🖼️ 视频帧已设置到img元素,base64长度: ${base64Image.length}`) + } else { + console.warn('⚠️ 收到空的视频帧数据') + } } diff --git a/frontend_websocket_example.html b/frontend_websocket_example.html deleted file mode 100644 index b34cb13d..00000000 --- a/frontend_websocket_example.html +++ /dev/null @@ -1,538 +0,0 @@ - - -
- - -