This commit is contained in:
zhaozilong12 2025-07-31 17:29:30 +08:00
commit 2f68fae1b7
9 changed files with 1280 additions and 703 deletions

View File

@ -29,6 +29,7 @@ from database import DatabaseManager
from device_manager import DeviceManager, VideoStreamManager from device_manager import DeviceManager, VideoStreamManager
from detection_engine import DetectionEngine, detection_bp from detection_engine import DetectionEngine, detection_bp
from data_processor import DataProcessor from data_processor import DataProcessor
from utils import config as app_config
# 配置日志 # 配置日志
logging.basicConfig( logging.basicConfig(
@ -75,23 +76,17 @@ def init_app():
try: try:
# 创建必要的目录 # 创建必要的目录
os.makedirs('logs', exist_ok=True) os.makedirs('logs', exist_ok=True)
backend_data_dir = os.path.join(os.path.dirname(__file__), 'data') os.makedirs('data', exist_ok=True)
os.makedirs(backend_data_dir, exist_ok=True)
os.makedirs(os.path.join(backend_data_dir, 'patients'), exist_ok=True)
os.makedirs('exports', exist_ok=True) os.makedirs('exports', exist_ok=True)
os.makedirs('videos', exist_ok=True) os.makedirs('videos', exist_ok=True)
# 初始化数据库 - 使用backend/data目录下的数据库 # 初始化数据库
db_path = os.path.join(backend_data_dir, 'body_balance.db') db_manager = DatabaseManager('data/body_balance.db')
db_manager = DatabaseManager(db_path)
db_manager.init_database() db_manager.init_database()
# 初始化设备管理器 # 初始化设备管理器
device_manager = DeviceManager() device_manager = DeviceManager()
# 初始化视频流管理器
video_stream_manager = VideoStreamManager(socketio)
# 初始化检测引擎 # 初始化检测引擎
detection_engine = DetectionEngine() detection_engine = DetectionEngine()
@ -134,22 +129,38 @@ def login():
username = data.get('username') username = data.get('username')
password = data.get('password') password = data.get('password')
remember = data.get('remember', False) remember = data.get('remember', False)
if not username or not password:
return jsonify({
'success': False,
'message': '用户名或密码不能为空'
}), 400
# 简单的模拟登录验证 # 使用数据库验证用户
if username and password: user = db_manager.authenticate_user(username, password)
# 这里可以添加真实的用户验证逻辑
# 目前使用模拟数据 if user:
# 检查用户是否已激活
if not user['is_active']:
return jsonify({
'success': False,
'message': '账户未激活,请联系管理员审核'
}), 403
# 构建用户数据
user_data = { user_data = {
'id': 1, 'id': user['id'],
'username': username, 'username': user['username'],
'name': '医生', 'name': user['name'],
'role': 'doctor', 'role': 'admin' if user['user_type'] == 'admin' else 'user',
'user_type': user['user_type'],
'avatar': '' 'avatar': ''
} }
# 生成简单的token实际项目中应使用JWT等安全token # 生成token实际项目中应使用JWT等安全token
token = f"token_{username}_{int(time.time())}" token = f"token_{username}_{int(time.time())}"
logger.info(f'用户 {username} 登录成功')
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': { 'data': {
@ -159,10 +170,11 @@ def login():
'message': '登录成功' 'message': '登录成功'
}) })
else: else:
logger.warning(f'用户 {username} 登录失败:用户名或密码错误')
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '用户名或密码不能为空' 'message': '用户名或密码错误'
}), 400 }), 401
except Exception as e: except Exception as e:
logger.error(f'登录失败: {e}') logger.error(f'登录失败: {e}')
@ -175,18 +187,42 @@ def register():
data = request.get_json() data = request.get_json()
username = data.get('username') username = data.get('username')
password = data.get('password') password = data.get('password')
email = data.get('email') name = data.get('name') or data.get('email', '')
phone = data.get('phone')
# 简单的模拟注册 if not username or not password:
if username and password: return jsonify({
'success': False,
'message': '用户名和密码不能为空'
}), 400
if len(password) < 6:
return jsonify({
'success': False,
'message': '密码长度不能少于6位'
}), 400
# 构建用户数据字典
user_data = {
'username': username,
'password': password,
'name': name,
'phone': phone
}
# 使用数据库注册用户
result = db_manager.register_user(user_data)
if result['success']:
logger.info(f'用户 {username} 注册成功,等待管理员审核')
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '注册成功,请登录' 'message': '注册成功,请等待管理员审核后登录'
}) })
else: else:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '用户名和密码不能为空' 'message': result['message']
}), 400 }), 400
except Exception as e: except Exception as e:
@ -229,25 +265,154 @@ def verify_token():
@app.route('/api/auth/forgot-password', methods=['POST']) @app.route('/api/auth/forgot-password', methods=['POST'])
def forgot_password(): def forgot_password():
"""忘记密码""" """忘记密码 - 根据用户名和手机号找回密码"""
try: try:
data = request.get_json() data = request.get_json()
email = data.get('email') username = data.get('username')
phone = data.get('phone')
if email: if not username:
return jsonify({
'success': False,
'error': '请输入用户名'
}), 400
if not phone:
return jsonify({
'success': False,
'error': '请输入手机号码'
}), 400
# 验证手机号格式
import re
phone_pattern = r'^1[3-9]\d{9}$'
if not re.match(phone_pattern, phone):
return jsonify({
'success': False,
'error': '手机号格式不正确'
}), 400
# 查询用户信息
conn = db_manager.get_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT username, password, phone FROM users
WHERE username = ? AND phone = ?
''', (username, phone))
user = cursor.fetchone()
if user:
# 用户存在且手机号匹配,返回密码
# 注意:这里返回的是加密后的密码,实际应用中需要解密或重置
# 为了演示,我们假设有一个简单的解密方法
encrypted_password = user['password']
# 这里简化处理,实际应该有更安全的密码找回机制
# 由于使用MD5加密无法直接解密所以返回提示信息
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '重置密码邮件已发送' 'password': '1234567', # 演示用固定密码
'message': '密码找回成功'
})
else:
# 检查用户是否存在
cursor.execute('SELECT username FROM users WHERE username = ?', (username,))
user_exists = cursor.fetchone()
if not user_exists:
return jsonify({
'success': False,
'error': '用户不存在'
}), 400
else:
return jsonify({
'success': False,
'error': '手机号不匹配'
}), 400
except Exception as e:
logger.error(f'忘记密码处理失败: {e}')
return jsonify({'success': False, 'error': '处理失败'}), 500
# ==================== 用户管理API ====================
@app.route('/api/users', methods=['GET'])
def get_users():
"""获取用户列表(管理员功能)"""
try:
# 这里应该验证管理员权限
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
status = request.args.get('status') # active, inactive, all
users = db_manager.get_users(page, size, status)
total = db_manager.get_user_count(status)
return jsonify({
'success': True,
'data': {
'users': users,
'total': total,
'page': page,
'size': size
}
})
except Exception as e:
logger.error(f'获取用户列表失败: {e}')
return jsonify({'success': False, 'message': '获取用户列表失败'}), 500
@app.route('/api/users/<int:user_id>/approve', methods=['POST'])
def approve_user(user_id):
"""审核用户(管理员功能)"""
try:
# 这里应该验证管理员权限
data = request.get_json()
approve = data.get('approve', True)
result = db_manager.approve_user(user_id, approve)
if result['success']:
action = '审核通过' if approve else '审核拒绝'
logger.info(f'用户 {user_id} {action}')
return jsonify({
'success': True,
'message': f'用户{action}成功'
}) })
else: else:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '邮箱不能为空' 'message': result['message']
}), 400 }), 400
except Exception as e: except Exception as e:
logger.error(f'忘记密码处理失败: {e}') logger.error(f'审核用户失败: {e}')
return jsonify({'success': False, 'message': '处理失败'}), 500 return jsonify({'success': False, 'message': '审核用户失败'}), 500
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
"""删除用户(管理员功能)"""
try:
# 这里应该验证管理员权限
result = db_manager.delete_user(user_id)
if result['success']:
logger.info(f'用户 {user_id} 删除成功')
return jsonify({
'success': True,
'message': '用户删除成功'
})
else:
return jsonify({
'success': False,
'message': result['message']
}), 400
except Exception as e:
logger.error(f'删除用户失败: {e}')
return jsonify({'success': False, 'message': '删除用户失败'}), 500
@app.route('/api/auth/reset-password', methods=['POST']) @app.route('/api/auth/reset-password', methods=['POST'])
def reset_password(): def reset_password():

View File

@ -5,7 +5,7 @@ debug = false
log_level = INFO log_level = INFO
[SERVER] [SERVER]
host = 127.0.0.1 host = 0.0.0.0
port = 5000 port = 5000
cors_origins = * cors_origins = *
@ -35,11 +35,7 @@ chart_dpi = 300
export_format = csv export_format = csv
[SECURITY] [SECURITY]
secret_key = 026efbf83a2fe101f168780740da86bf1c9260625458e6782738aa9cf18f8e37 secret_key = 8914333c0adf239da5d7a992e90879e500ab19e9da0d2bc41c6d8ca97ab102e0
session_timeout = 3600 session_timeout = 3600
max_login_attempts = 5 max_login_attempts = 5
[CAMERA]
rtsp_url = rtsp://admin:JY123456@192.168.1.61:554/Streaming/Channels/101

View File

@ -111,12 +111,49 @@ class DatabaseManager:
) )
''') ''')
# 创建用户表
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
register_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 0,
user_type TEXT DEFAULT 'user',
phone TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建索引 # 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_patients_name ON patients (name)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_patients_name ON patients (name)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_patient ON detection_sessions (patient_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_patient ON detection_sessions (patient_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_time ON detection_sessions (start_time)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_time ON detection_sessions (start_time)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_session ON detection_data (session_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_session ON detection_data (session_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_timestamp ON detection_data (timestamp)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_timestamp ON detection_data (timestamp)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_username ON users (username)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_type ON users (user_type)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_phone ON users (phone)')
# 插入默认管理员账户(如果不存在)
cursor.execute('SELECT COUNT(*) FROM users WHERE username = ?', ('admin',))
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()
cursor.execute('''
INSERT INTO users (id, name, username, password, is_active, user_type)
VALUES (?, ?, ?, ?, ?, ?)
''', (admin_id, '系统管理员', 'admin', admin_password, 1, 'admin'))
logger.info('创建默认管理员账户: admin/admin123')
conn.commit() conn.commit()
logger.info('数据库初始化完成') logger.info('数据库初始化完成')
@ -655,6 +692,261 @@ class DatabaseManager:
logger.error(f'设置系统设置失败: {e}') logger.error(f'设置系统设置失败: {e}')
raise raise
# ==================== 用户管理 ====================
def register_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
"""用户注册"""
conn = self.get_connection()
cursor = conn.cursor()
try:
import hashlib
# 检查手机号是否已存在(如果提供了手机号)
if user_data.get('phone'):
cursor.execute('SELECT COUNT(*) FROM users WHERE phone = ?', (user_data['phone'],))
if cursor.fetchone()[0] > 0:
return {
'success': False,
'message': '手机号已存在'
}
user_id = str(uuid.uuid4())
# 密码MD5加密
password_hash = hashlib.md5(user_data['password'].encode()).hexdigest()
cursor.execute('''
INSERT INTO users (id, name, username, password, phone, is_active, user_type)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
user_id,
user_data['name'],
user_data['username'],
password_hash,
user_data.get('phone'), # 手机号可选
0, # 新注册用户默认未激活,需要管理员审核
'user'
))
conn.commit()
logger.info(f'用户注册成功: {user_data["username"]}')
return {
'success': True,
'user_id': user_id,
'message': '注册成功'
}
except sqlite3.IntegrityError:
conn.rollback()
logger.error(f'用户名已存在: {user_data["username"]}')
return {
'success': False,
'message': '用户名已存在'
}
except Exception as e:
conn.rollback()
logger.error(f'用户注册失败: {e}')
return {
'success': False,
'message': '注册失败'
}
def authenticate_user(self, username: str, password: str) -> Optional[Dict]:
"""用户登录验证"""
conn = self.get_connection()
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))
row = cursor.fetchone()
if row:
user = dict(row)
# 不返回密码
del user['password']
logger.info(f'用户登录成功: {username}')
return user
else:
logger.warning(f'用户登录失败: {username}')
return None
except Exception as e:
logger.error(f'用户验证失败: {e}')
return None
def get_user_by_phone(self, phone: str) -> Optional[Dict]:
"""根据手机号查询用户"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('SELECT * FROM users WHERE phone = ?', (phone,))
row = cursor.fetchone()
if row:
user = dict(row)
# 不返回密码
del user['password']
return user
return None
except Exception as e:
logger.error(f'根据手机号查询用户失败: {e}')
return None
def get_users(self, page: int = 1, size: int = 10, status: str = 'all') -> List[Dict]:
"""获取用户列表"""
conn = self.get_connection()
cursor = conn.cursor()
try:
offset = (page - 1) * size
if status == 'pending':
cursor.execute('''
SELECT id, name, username, phone, register_date, is_active, user_type, created_at
FROM users
WHERE is_active = 0 AND user_type = 'user'
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (size, offset))
elif status == 'active':
cursor.execute('''
SELECT id, name, username, phone, register_date, is_active, user_type, created_at
FROM users
WHERE is_active = 1
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (size, offset))
else:
cursor.execute('''
SELECT id, name, username, phone, register_date, is_active, user_type, created_at
FROM users
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (size, offset))
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f'获取用户列表失败: {e}')
return []
def get_users_count(self, status: str = 'all') -> int:
"""获取用户总数"""
conn = self.get_connection()
cursor = conn.cursor()
try:
if status == 'pending':
cursor.execute('SELECT COUNT(*) FROM users WHERE is_active = 0 AND user_type = "user"')
elif status == 'active':
cursor.execute('SELECT COUNT(*) FROM users WHERE is_active = 1')
else:
cursor.execute('SELECT COUNT(*) FROM users')
return cursor.fetchone()[0]
except Exception as e:
logger.error(f'获取用户总数失败: {e}')
return 0
def approve_user(self, user_id: str, approved: bool = True):
"""审核用户"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('''
UPDATE users SET
is_active = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (1 if approved else 0, user_id))
conn.commit()
status = '通过' if approved else '拒绝'
logger.info(f'用户审核{status}: {user_id}')
except Exception as e:
conn.rollback()
logger.error(f'用户审核失败: {e}')
raise
def get_user(self, user_id: str) -> Optional[Dict]:
"""获取单个用户信息"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('''
SELECT id, name, username, phone, register_date, is_active, user_type, created_at, updated_at
FROM users WHERE id = ?
''', (user_id,))
row = cursor.fetchone()
return dict(row) if row else None
except Exception as e:
logger.error(f'获取用户信息失败: {e}')
return None
def update_user(self, user_id: str, user_data: Dict[str, Any]):
"""更新用户信息"""
conn = self.get_connection()
cursor = conn.cursor()
try:
# 如果包含密码,需要加密
if 'password' in user_data:
import hashlib
user_data['password'] = hashlib.md5(user_data['password'].encode()).hexdigest()
# 构建更新语句
fields = []
values = []
for key, value in user_data.items():
if key in ['name', 'username', 'password', 'is_active', 'user_type','phone']:
fields.append(f'{key} = ?')
values.append(value)
if fields:
fields.append('updated_at = CURRENT_TIMESTAMP')
values.append(user_id)
sql = f'UPDATE users SET {', '.join(fields)} WHERE id = ?'
cursor.execute(sql, values)
conn.commit()
logger.info(f'更新用户信息: {user_id}')
except Exception as e:
conn.rollback()
logger.error(f'更新用户信息失败: {e}')
raise
def delete_user(self, user_id: str):
"""删除用户"""
conn = self.get_connection()
cursor = conn.cursor()
try:
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
logger.info(f'删除用户: {user_id}')
except Exception as e:
conn.rollback()
logger.error(f'删除用户失败: {e}')
raise
def close(self): def close(self):
"""关闭数据库连接""" """关闭数据库连接"""
if self.connection: if self.connection:

View File

@ -60,7 +60,7 @@ class Config:
# 数据库配置 # 数据库配置
self.config['DATABASE'] = { self.config['DATABASE'] = {
'path': 'data/balance_system.db', 'path': 'backend/data/body_balance.db',
'backup_interval': '24', # 小时 'backup_interval': '24', # 小时
'max_backups': '7' 'max_backups': '7'
} }

View File

@ -41,7 +41,7 @@
}, },
"database": { "database": {
"type": "sqlite", "type": "sqlite",
"path": "data/database.db", "path": "backend/data/body_balance.db",
"backup": { "backup": {
"enabled": true, "enabled": true,
"interval_hours": 24, "interval_hours": 24,

View File

@ -154,8 +154,10 @@ 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 { useAuthStore } from '../stores/index.js'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore()
// //
const activeNav = ref('detection') const activeNav = ref('detection')
@ -233,10 +235,11 @@ const handleLogout = async () => {
type: 'warning' type: 'warning'
}) })
localStorage.removeItem('userInfo') // logout
localStorage.removeItem('rememberedUser') await authStore.logout()
ElMessage.success('已退出登录') ElMessage.success('已退出登录')
router.push('/login') // 使replacepushDashboard
router.replace('/login')
} catch { } catch {
// //
} }
@ -310,10 +313,12 @@ const loadPatients = async () => {
// //
onMounted(() => { onMounted(() => {
// //
const savedUserInfo = localStorage.getItem('userInfo') if (authStore.currentUser) {
if (savedUserInfo) { Object.assign(userInfo, {
Object.assign(userInfo, JSON.parse(savedUserInfo)) username: authStore.currentUser.username,
avatar: authStore.currentUser.avatar || ''
})
} }
// //

View File

@ -12,8 +12,8 @@
<!-- 系统标题 --> <!-- 系统标题 -->
<h1 class="system-title">平衡体态检测系统</h1> <h1 class="system-title">平衡体态检测系统</h1>
<!-- 登录卡片 --> <!-- 登录页面 -->
<el-card 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">
@ -55,11 +55,155 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="button-group"> <div class="button-group">
<el-button type="primary" class="login-btn" @click="handleLogin" :loading="isLoading">登录</el-button> <el-button type="primary" class="login-btn" @click="handleLogin" :loading="isLoading">登录</el-button>
<el-button class="register-btn" @click="handleRegister">注册</el-button> <el-button class="register-btn" @click="switchToRegister">注册</el-button>
</div> </div>
</el-form> </el-form>
</el-card> </el-card>
<!-- 注册页面 -->
<el-card v-if="isRegisterMode && !isForgotPasswordMode" class="register-card">
<div class="card-header">注册</div>
<div class="register-form">
<div class="form-item">
<div class="input-wrapper">
<el-icon class="input-icon"><User /></el-icon>
<input
v-model="registerForm.name"
type="text"
placeholder="请输入姓名"
class="register-input"
/>
</div>
</div>
<div class="form-item">
<div class="input-wrapper">
<el-icon class="input-icon"><User /></el-icon>
<input
v-model="registerForm.username"
type="text"
placeholder="请输入登录账号"
class="register-input"
/>
</div>
</div>
<div class="form-item">
<div class="input-wrapper">
<el-icon class="input-icon"><Phone /></el-icon>
<input
v-model="registerForm.phone"
type="text"
placeholder="请输入手机号码"
class="register-input"
/>
</div>
</div>
<div class="form-item">
<div class="input-wrapper">
<el-icon class="input-icon"><Lock /></el-icon>
<input
v-model="registerForm.password"
:type="registerPasswordVisible ? 'text' : 'password'"
placeholder="请输入密码"
class="register-input"
/>
<el-icon
class="password-toggle"
@click="registerPasswordVisible = !registerPasswordVisible"
>
<component :is="registerPasswordVisible ? Hide : View" />
</el-icon>
</div>
</div>
<div class="form-item">
<div class="input-wrapper">
<el-icon class="input-icon"><Lock /></el-icon>
<input
v-model="registerForm.confirmPassword"
:type="confirmPasswordVisible ? 'text' : 'password'"
placeholder="请再次确认密码"
class="register-input"
/>
<el-icon
class="password-toggle"
@click="confirmPasswordVisible = !confirmPasswordVisible"
>
<component :is="confirmPasswordVisible ? Hide : View" />
</el-icon>
</div>
</div>
</div>
<div class="register-footer">
<button class="back-btn" @click="switchToLogin">返回登录</button>
<button class="submit-btn" @click="handleRegisterSubmit">注册</button>
</div>
</el-card>
<!-- 忘记密码页面 -->
<el-card v-if="isForgotPasswordMode" class="forgot-password-card">
<div class="card-header">找回密码</div>
<!-- 未显示密码时的输入表单 -->
<div v-if="!showRetrievedPassword" class="forgot-password-form">
<div class="form-item">
<div class="input-wrapper">
<el-icon class="input-icon"><User /></el-icon>
<input
v-model="forgotPasswordForm.username"
type="text"
placeholder="请输入账号"
class="forgot-input"
/>
</div>
</div>
<div class="form-item">
<div class="input-wrapper">
<el-icon class="input-icon"><Phone /></el-icon>
<input
v-model="forgotPasswordForm.phone"
type="text"
placeholder="请输入手机号码"
class="forgot-input"
/>
</div>
</div>
</div>
<!-- 显示找回的密码 -->
<div v-if="showRetrievedPassword" class="password-result">
<div class="result-title">您的密码是</div>
<div class="password-display">{{ retrievedPassword }}</div>
<div class="result-actions">
<button class="copy-btn" @click="copyPassword">复制</button>
</div>
</div>
<div class="forgot-footer">
<button class="back-btn" @click="backToLoginFromForgot">返回</button>
<button v-if="!showRetrievedPassword" class="confirm-btn" @click="handleForgotPasswordSubmit">确认</button>
<button v-if="showRetrievedPassword" class="confirm-btn" @click="backToLoginFromForgot">退出</button>
</div>
</el-card>
</div> </div>
<!-- 自定义错误提示弹窗 -->
<div v-if="showErrorDialog" class="error-dialog-overlay" @click="closeErrorDialog">
<div class="error-dialog" @click.stop>
<div class="dialog-header">
<span class="dialog-title">提示</span>
<button class="close-btn" @click="closeErrorDialog">×</button>
</div>
<div class="dialog-content">
{{ errorMessage }}
</div>
<div class="dialog-footer">
<button class="cancel-btn" @click="closeErrorDialog">取消</button>
<button class="confirm-btn" @click="closeErrorDialog">确认</button>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -68,11 +212,15 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useAuthStore } from '../stores' import { useAuthStore } from '../stores'
import { User, Lock, View, Hide } from '@element-plus/icons-vue' import { User, Lock, View, Hide, Phone } from '@element-plus/icons-vue'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
//
const isRegisterMode = ref(false)
const isForgotPasswordMode = ref(false)
// //
const form = ref({ const form = ref({
account: '', account: '',
@ -86,10 +234,167 @@ const passwordVisible = ref(false)
// //
const isLoading = ref(false) const isLoading = ref(false)
//
const showErrorDialog = ref(false)
const errorMessage = ref('')
//
const showError = (message) => {
errorMessage.value = message
showErrorDialog.value = true
}
//
const closeErrorDialog = () => {
showErrorDialog.value = false
errorMessage.value = ''
}
//
const registerPasswordVisible = ref(false)
const confirmPasswordVisible = ref(false)
//
const registerForm = ref({
name: '',
username: '',
phone: '',
password: '',
confirmPassword: ''
})
//
const forgotPasswordForm = ref({
username: '',
phone: ''
})
//
const retrievedPassword = ref('')
const showRetrievedPassword = ref(false)
//
const switchToRegister = () => {
isRegisterMode.value = true
//
registerForm.value = {
name: '',
username: '',
phone: '',
password: '',
confirmPassword: ''
}
registerPasswordVisible.value = false
confirmPasswordVisible.value = false
}
//
const switchToLogin = () => {
isRegisterMode.value = false
//
registerForm.value = {
name: '',
username: '',
phone: '',
password: '',
confirmPassword: ''
}
registerPasswordVisible.value = false
confirmPasswordVisible.value = false
}
//
const handleRegisterSubmit = async () => {
//
if (!registerForm.value.name.trim()) {
showError('请输入姓名!')
return
}
//
if (!registerForm.value.username.trim()) {
showError('请输入登录账号!')
return
}
//
if (!registerForm.value.phone.trim()) {
showError('请输入手机号码!')
return
}
//
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(registerForm.value.phone)) {
showError('手机号格式不正确,请重新输入!')
return
}
//
if (!registerForm.value.password) {
showError('请输入密码!')
return
}
//
if (registerForm.value.password.length < 6) {
showError('密码长度不能少于6位')
return
}
//
if (!registerForm.value.confirmPassword) {
showError('请确认密码!')
return
}
//
if (registerForm.value.password !== registerForm.value.confirmPassword) {
showError('两次密码输入不一致,请重新输入!')
return
}
// API
try {
const result = await authStore.register({
name: registerForm.value.name,
username: registerForm.value.username,
phone: registerForm.value.phone,
password: registerForm.value.password
})
if (result.success) {
showError('用户注册成功!')
//
setTimeout(() => {
switchToLogin()
}, 2000)
} else {
//
if (result.error && result.error.includes('用户名已存在')) {
showError('用户名已存在,请更换用户名!')
} else if (result.error && result.error.includes('手机号已存在')) {
showError('手机号码已被注册,请更换手机号!')
} else {
showError(result.error || '注册失败,请重试!')
}
}
} catch (error) {
showError('注册失败,请检查网络连接后重试!')
}
}
// //
const handleLogin = async () => { const handleLogin = async () => {
if (!form.value.account || !form.value.password) { //
ElMessage.warning('请输入账号和密码') if (!form.value.account) {
showError('用户名或密码错误,请重新输入!')
return
}
//
if (!form.value.password) {
showError('用户名或密码错误,请重新输入!')
return return
} }
@ -103,25 +408,144 @@ const handleLogin = async () => {
if (result.success) { if (result.success) {
ElMessage.success('登录成功') ElMessage.success('登录成功')
router.push('/detection/1') router.push('/dashboard')
} else { } else {
ElMessage.error(result.error || '登录失败') //
if (result.error && result.error.includes('用户不存在')) {
showError('用户名或密码错误,请重新输入!')
} else if (result.error && result.error.includes('密码错误')) {
showError('用户名或密码错误,请重新输入!')
} else if (result.error && result.error.includes('用户未激活')) {
showError('账户未激活,请联系管理员激活后再登录')
} else {
showError('用户名或密码错误,请重新输入!')
}
} }
} catch (error) { } catch (error) {
ElMessage.error('登录失败:' + (error.message || '未知错误')) showError('用户名或密码错误,请重新输入!')
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
} }
// //
const handleRegister = () => { const switchToForgotPassword = () => {
router.push('/register') isForgotPasswordMode.value = true
isRegisterMode.value = false
//
forgotPasswordForm.value = {
username: '',
phone: ''
}
retrievedPassword.value = ''
showRetrievedPassword.value = false
}
//
const backToLoginFromForgot = () => {
isForgotPasswordMode.value = false
isRegisterMode.value = false
//
forgotPasswordForm.value = {
username: '',
phone: ''
}
retrievedPassword.value = ''
showRetrievedPassword.value = false
} }
// //
const handleForgotPassword = () => { const handleForgotPassword = () => {
router.push('/forgot-password') switchToForgotPassword()
}
//
const handleForgotPasswordSubmit = async () => {
//
if (!forgotPasswordForm.value.username.trim()) {
showError('请输入用户名!')
return
}
//
if (!forgotPasswordForm.value.phone.trim()) {
showError('请输入手机号码!')
return
}
//
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(forgotPasswordForm.value.phone)) {
showError('手机号格式不正确,请重新输入!')
return
}
try {
// API
const response = await fetch('http://127.0.0.1:5000/api/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: forgotPasswordForm.value.username,
phone: forgotPasswordForm.value.phone
})
})
const result = await response.json()
if (result.success) {
//
retrievedPassword.value = result.password
showRetrievedPassword.value = true
} else {
//
if (result.error && result.error.includes('用户不存在')) {
showError('用户名不存在,请检查后重新输入!')
} else if (result.error && result.error.includes('手机号不匹配')) {
showError('手机号码不正确,请输入注册时填写的手机号码!')
} else {
showError('用户名和手机号码不匹配,请重新输入!')
}
}
} catch (error) {
showError('网络连接失败,请检查网络后重试!')
}
}
//
const copyPassword = async () => {
try {
// 使 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(retrievedPassword.value)
showError('密码已复制到剪贴板!')
} else {
// 使 document.execCommand
const textArea = document.createElement('textarea')
textArea.value = retrievedPassword.value
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
textArea.style.top = '-999999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
const successful = document.execCommand('copy')
document.body.removeChild(textArea)
if (successful) {
showError('密码已复制到剪贴板!')
} else {
showError('复制失败,请手动复制密码:' + retrievedPassword.value)
}
}
} catch (error) {
console.error('复制密码失败:', error)
//
showError('复制失败,请手动复制密码:' + retrievedPassword.value)
}
} }
</script> </script>
@ -207,6 +631,17 @@ const handleForgotPassword = () => {
padding: 30px 25px !important; padding: 30px 25px !important;
} }
/* 注册卡片 */
.register-card {
width: 100%;
max-width: 450px;
background-color: #003366 !important;
border: none !important;
border-radius: 12px !important;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2);
padding: 30px 25px !important;
}
/* 卡片头部标题 */ /* 卡片头部标题 */
.card-header { .card-header {
font-size: 1.4rem; font-size: 1.4rem;
@ -304,4 +739,331 @@ const handleForgotPassword = () => {
.register-btn:hover { .register-btn:hover {
background-color: #004080 !important; background-color: #004080 !important;
} }
/* 自定义错误弹窗样式 */
.error-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(5px);
}
.error-dialog {
background-color: #003366;
border: 2px solid #00ffff;
border-radius: 12px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.3);
min-width: 400px;
max-width: 500px;
animation: dialogFadeIn 0.3s ease-out;
}
@keyframes dialogFadeIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 25px 15px;
border-bottom: 1px solid rgba(0, 255, 255, 0.2);
}
.dialog-title {
font-size: 1.2rem;
color: #00ffff;
font-weight: 500;
}
.close-btn {
background: none;
border: none;
color: #00ffff;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.close-btn:hover {
background-color: rgba(0, 255, 255, 0.1);
transform: scale(1.1);
}
.dialog-content {
padding: 25px;
color: #fff;
font-size: 1rem;
line-height: 1.5;
text-align: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 15px;
padding: 15px 25px 25px;
}
.cancel-btn, .confirm-btn {
padding: 8px 20px;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid;
}
.cancel-btn {
background-color: transparent;
border-color: #666;
color: #ccc;
}
.cancel-btn:hover {
background-color: rgba(102, 102, 102, 0.1);
border-color: #999;
}
.confirm-btn {
background-color: #00ffff;
border-color: #00ffff;
color: #003366;
font-weight: 500;
}
.confirm-btn:hover {
background-color: #00e6e6;
border-color: #00e6e6;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
}
/* 注册表单样式 */
.register-form {
padding: 20px 0;
}
.form-item {
margin-bottom: 20px;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
background-color: #004080;
border: 1px solid #00ffff;
border-radius: 6px;
padding: 0 15px;
transition: all 0.2s ease;
}
.input-wrapper:focus-within {
border-color: #00e6e6;
box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
}
.input-icon {
color: #00ffff;
margin-right: 10px;
font-size: 16px;
}
.register-input {
flex: 1;
background: none;
border: none;
outline: none;
color: #fff;
font-size: 14px;
padding: 12px 0;
}
.register-input::placeholder {
color: #aaa;
}
.password-toggle {
color: #00ffff;
cursor: pointer;
margin-left: 10px;
font-size: 16px;
transition: all 0.2s ease;
}
.password-toggle:hover {
color: #00e6e6;
transform: scale(1.1);
}
/* 注册页面底部按钮 */
.register-footer {
display: flex;
justify-content: space-between;
gap: 15px;
margin-top: 25px;
}
.back-btn, .submit-btn {
flex: 1;
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid;
}
.back-btn {
background-color: transparent;
border-color: #00ffff;
color: #00ffff;
}
.back-btn:hover {
background-color: #004080;
transform: translateY(-1px);
}
.submit-btn {
background-color: #00ffff;
border-color: #00ffff;
color: #003366;
}
.submit-btn:hover {
background-color: #00e6e6;
border-color: #00e6e6;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
}
/* 忘记密码页面样式 */
.forgot-password-card {
width: 100%;
max-width: 450px;
background-color: #003366 !important;
border: none !important;
border-radius: 12px !important;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.2);
padding: 30px 25px !important;
}
.forgot-password-form {
padding: 20px 0;
}
.forgot-input {
flex: 1;
background: none;
border: none;
outline: none;
color: #fff;
font-size: 14px;
padding: 12px 0;
}
.forgot-input::placeholder {
color: #aaa;
}
/* 密码显示结果样式 */
.password-result {
text-align: center;
padding: 30px 0;
}
.result-title {
color: #00ffff;
font-size: 16px;
margin-bottom: 20px;
}
.password-display {
background-color: #004080;
border: 2px solid #00ffff;
border-radius: 8px;
padding: 15px 20px;
color: #fff;
font-size: 18px;
font-weight: 500;
letter-spacing: 2px;
margin-bottom: 20px;
word-break: break-all;
}
.result-actions {
margin-top: 15px;
}
.copy-btn {
background-color: #00ffff;
border: 1px solid #00ffff;
color: #003366;
padding: 8px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.copy-btn:hover {
background-color: #00e6e6;
border-color: #00e6e6;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
}
/* 忘记密码页面底部按钮 */
.forgot-footer {
display: flex;
justify-content: space-between;
gap: 15px;
margin-top: 25px;
}
.confirm-btn {
flex: 1;
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid;
background-color: #00ffff;
border-color: #00ffff;
color: #003366;
}
.confirm-btn:hover {
background-color: #00e6e6;
border-color: #00e6e6;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 255, 255, 0.3);
}
</style> </style>

View File

@ -1,222 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>截图功能测试</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.test-area {
border: 2px dashed #ccc;
padding: 20px;
margin: 20px 0;
background: linear-gradient(45deg, #f0f0f0, #e0e0e0);
text-align: center;
}
.button {
background: linear-gradient(to right, rgb(236, 50, 166), rgb(160, 5, 216));
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
.button:hover {
opacity: 0.9;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
</style>
</head>
<body>
<div class="container">
<h1>截图功能测试页面</h1>
<div id="detectare" class="test-area">
<h2>这是要截图的区域</h2>
<p>患者ID: 2101</p>
<p>患者姓名: 张三</p>
<p>测试时间: <span id="currentTime"></span></p>
<div style="display: flex; justify-content: space-around; margin: 20px 0;">
<div style="background: #ff6b6b; color: white; padding: 20px; border-radius: 10px;">
<h3>模块1</h3>
<p>数据: 85%</p>
</div>
<div style="background: #4ecdc4; color: white; padding: 20px; border-radius: 10px;">
<h3>模块2</h3>
<p>数据: 92%</p>
</div>
<div style="background: #45b7d1; color: white; padding: 20px; border-radius: 10px;">
<h3>模块3</h3>
<p>数据: 78%</p>
</div>
</div>
</div>
<div style="text-align: center;">
<button id="screenshotBtn" class="button">📸 截图测试</button>
<button id="checkBackendBtn" class="button">🔍 检查后端</button>
</div>
<div id="status"></div>
<div style="margin-top: 20px;">
<h3>使用说明:</h3>
<ol>
<li>确保后端服务已启动 (python debug_server.py)</li>
<li>点击"检查后端"按钮验证后端连接</li>
<li>点击"截图测试"按钮进行截图</li>
<li>截图将保存到 data/patients/2101_张三/ 文件夹中</li>
</ol>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script>
const BACKEND_URL = 'http://localhost:5000';
// 更新当前时间
function updateTime() {
const now = new Date();
document.getElementById('currentTime').textContent = now.toLocaleString('zh-CN');
}
updateTime();
setInterval(updateTime, 1000);
// 显示状态消息
function showStatus(message, type = 'info') {
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
}
// 检查后端连接
async function checkBackend() {
try {
showStatus('正在检查后端连接...', 'info');
const response = await fetch(`${BACKEND_URL}/health`);
const data = await response.json();
if (data.status === 'ok') {
showStatus('✅ 后端连接正常', 'success');
} else {
showStatus('❌ 后端响应异常', 'error');
}
} catch (error) {
showStatus(`❌ 后端连接失败: ${error.message}`, 'error');
}
}
// 截图功能
async function takeScreenshot() {
const btn = document.getElementById('screenshotBtn');
try {
btn.disabled = true;
btn.textContent = '📸 截图中...';
showStatus('正在生成截图...', 'info');
// 获取要截图的元素
const element = document.getElementById('detectare');
if (!element) {
throw new Error('未找到截图区域');
}
// 使用html2canvas进行截图
const canvas = await html2canvas(element, {
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
scale: 1,
logging: false
});
// 转换为base64
const base64Image = canvas.toDataURL('image/png');
showStatus('正在保存截图...', 'info');
// 生成检查记录ID
const now = new Date();
const sessionId = now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0');
// 调用后端API保存截图
const response = await fetch(`${BACKEND_URL}/api/screenshots/save`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
patientId: '2101',
patientName: '张三',
sessionId: sessionId,
imageData: base64Image
})
});
const result = await response.json();
if (result.success) {
showStatus(`✅ 截图保存成功!<br>文件路径: ${result.filepath}`, 'success');
} else {
throw new Error(result.message || '保存失败');
}
} catch (error) {
console.error('截图失败:', error);
showStatus(`❌ 截图失败: ${error.message}`, 'error');
} finally {
btn.disabled = false;
btn.textContent = '📸 截图测试';
}
}
// 绑定事件
document.getElementById('screenshotBtn').addEventListener('click', takeScreenshot);
document.getElementById('checkBackendBtn').addEventListener('click', checkBackend);
// 页面加载时自动检查后端
window.addEventListener('load', checkBackend);
</script>
</body>
</html>

View File

@ -1,421 +0,0 @@
<template>
<div class="websocket-example">
<div class="header">
<h2>Vue WebSocket连接示例</h2>
<div class="status" :class="connectionStatus">
{{ statusText }}
</div>
</div>
<div class="controls">
<button
@click="connectWebSocket"
:disabled="isConnected"
class="btn btn-primary"
>
连接WebSocket
</button>
<button
@click="disconnectWebSocket"
:disabled="!isConnected"
class="btn btn-secondary"
>
断开连接
</button>
<button
@click="startRtsp"
:disabled="!isConnected || isRtspRunning"
class="btn btn-success"
>
启动RTSP
</button>
<button
@click="stopRtsp"
:disabled="!isConnected || !isRtspRunning"
class="btn btn-danger"
>
停止RTSP
</button>
</div>
<div class="video-container">
<h3>RTSP视频流</h3>
<div v-if="rtspImageSrc" class="video-wrapper">
<img :src="rtspImageSrc" alt="RTSP视频流" class="rtsp-image" />
<div class="frame-info">已接收帧数: {{ frameCount }}</div>
</div>
<div v-else class="no-video">
暂无视频流
</div>
</div>
<div class="log-container">
<h3>连接日志</h3>
<div class="log-content" ref="logContainer">
<div v-for="(log, index) in logs" :key="index" class="log-item">
<span class="log-time">[{{ log.time }}]</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { io } from 'socket.io-client'
//
const socket = ref(null)
const isConnected = ref(false)
const isRtspRunning = ref(false)
const rtspImageSrc = ref('')
const frameCount = ref(0)
const logs = ref([])
const logContainer = ref(null)
//
const BACKEND_URL = 'http://localhost:5000' //
// 使: 'http://192.168.1.173:5000'
//
const connectionStatus = computed(() => {
if (isConnected.value) return 'connected'
return 'disconnected'
})
const statusText = computed(() => {
if (isConnected.value) {
return isRtspRunning.value ? '已连接 - RTSP运行中' : '已连接'
}
return '未连接'
})
//
const addLog = (message) => {
const now = new Date()
const time = now.toLocaleTimeString()
logs.value.push({ time, message })
//
if (logs.value.length > 100) {
logs.value.shift()
}
//
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
})
}
// WebSocket
const connectWebSocket = () => {
try {
addLog(`正在连接到 ${BACKEND_URL}`)
// Socket.IO
socket.value = io(BACKEND_URL, {
transports: ['websocket', 'polling'],
timeout: 10000,
forceNew: true
})
//
socket.value.on('connect', () => {
isConnected.value = true
addLog(`WebSocket连接成功Socket ID: ${socket.value.id}`)
})
//
socket.value.on('connect_error', (error) => {
isConnected.value = false
addLog(`连接失败: ${error.message}`)
})
//
socket.value.on('disconnect', (reason) => {
isConnected.value = false
isRtspRunning.value = false
rtspImageSrc.value = ''
addLog(`连接断开: ${reason}`)
})
// RTSP
socket.value.on('rtsp_status', (data) => {
addLog(`RTSP状态: ${JSON.stringify(data)}`)
if (data.status === 'started') {
isRtspRunning.value = true
frameCount.value = 0
} else if (data.status === 'stopped') {
isRtspRunning.value = false
rtspImageSrc.value = ''
} else if (data.status === 'already_running') {
addLog('RTSP已在运行中')
}
})
// RTSP
socket.value.on('rtsp_frame', (data) => {
if (data.image) {
frameCount.value++
rtspImageSrc.value = 'data:image/jpeg;base64,' + data.image
// 30
if (frameCount.value % 30 === 0) {
addLog(`已接收 ${frameCount.value}`)
}
}
})
} catch (error) {
addLog(`连接异常: ${error.message}`)
}
}
// WebSocket
const disconnectWebSocket = () => {
if (socket.value) {
socket.value.disconnect()
socket.value = null
isConnected.value = false
isRtspRunning.value = false
rtspImageSrc.value = ''
addLog('主动断开连接')
}
}
// RTSP
const startRtsp = () => {
if (socket.value && isConnected.value) {
addLog('发送start_rtsp事件')
socket.value.emit('start_rtsp')
} else {
addLog('WebSocket未连接无法启动RTSP')
}
}
// RTSP
const stopRtsp = () => {
if (socket.value && isConnected.value) {
addLog('发送stop_rtsp事件')
socket.value.emit('stop_rtsp')
} else {
addLog('WebSocket未连接无法停止RTSP')
}
}
//
onMounted(() => {
addLog('组件已挂载可以开始连接WebSocket')
addLog(`后端地址: ${BACKEND_URL}`)
addLog('请确保后端服务已启动')
})
onUnmounted(() => {
//
if (socket.value) {
socket.value.disconnect()
}
})
</script>
<style scoped>
.websocket-example {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
.header h2 {
margin: 0;
color: #333;
}
.status {
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
}
.status.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: #545b62;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover:not(:disabled) {
background-color: #1e7e34;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover:not(:disabled) {
background-color: #c82333;
}
.video-container {
margin-bottom: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.video-container h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.video-wrapper {
text-align: center;
position: relative;
}
.rtsp-image {
max-width: 100%;
height: auto;
border: 2px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.frame-info {
margin-top: 10px;
font-size: 14px;
color: #666;
font-weight: 500;
}
.no-video {
text-align: center;
padding: 40px;
color: #6c757d;
font-style: italic;
background-color: #e9ecef;
border-radius: 8px;
}
.log-container {
margin-top: 20px;
}
.log-container h3 {
margin-bottom: 10px;
color: #333;
}
.log-content {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.log-item {
margin-bottom: 5px;
word-wrap: break-word;
}
.log-time {
color: #6c757d;
font-weight: bold;
}
.log-message {
margin-left: 8px;
color: #333;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.controls {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>