Merge branch 'main' of http://121.37.111.42:3000/ThbTech/BodyBalanceEvaluation into main
This commit is contained in:
commit
2f68fae1b7
227
backend/app.py
227
backend/app.py
@ -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:
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': '邮箱不能为空'
|
'error': '手机号不匹配'
|
||||||
}), 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, '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:
|
||||||
|
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/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():
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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')
|
// 使用replace而不是push,避免返回按钮回到Dashboard
|
||||||
|
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 || ''
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载患者列表
|
// 加载患者列表
|
||||||
|
@ -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,12 +55,156 @@
|
|||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<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>
|
</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 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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -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>
|
@ -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>
|
|
@ -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>
|
|
Loading…
Reference in New Issue
Block a user