16 changed files with 1755 additions and 73 deletions
@ -0,0 +1,12 @@ |
|||||
|
import os |
||||
|
import sys |
||||
|
|
||||
|
from robyn import Robyn |
||||
|
|
||||
|
# 自动将 web_server 目录加入 Python 路径 |
||||
|
web_server_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
||||
|
if web_server_path not in sys.path: |
||||
|
sys.path.insert(0, web_server_path) |
||||
|
|
||||
|
|
||||
|
app = Robyn(__file__) |
||||
@ -0,0 +1,11 @@ |
|||||
|
# MySQL数据库配置 |
||||
|
# 请根据您的实际MySQL配置修改以下参数 |
||||
|
|
||||
|
MYSQL_CONFIG = { |
||||
|
"host": "localhost", # MySQL主机地址 |
||||
|
"port": 3306, # MySQL端口 |
||||
|
"user": "root", # MySQL用户名 |
||||
|
"password": "123456", # MySQL密码 |
||||
|
"database": "kg", # 数据库名 |
||||
|
"charset": "utf8mb4" # 字符 |
||||
|
} |
||||
@ -0,0 +1,486 @@ |
|||||
|
from robyn import jsonify, Response |
||||
|
from app import app |
||||
|
from datetime import datetime, timedelta |
||||
|
import uuid |
||||
|
import json |
||||
|
import os |
||||
|
import base64 |
||||
|
from service.UserService import user_service |
||||
|
|
||||
|
# 临时存储token,用于会话管理 |
||||
|
TEMP_TOKENS = {} |
||||
|
|
||||
|
def generate_token() -> str: |
||||
|
"""生成随机token""" |
||||
|
return str(uuid.uuid4()) |
||||
|
|
||||
|
@app.post("/api/login") |
||||
|
def login_route(request): |
||||
|
"""登录接口""" |
||||
|
try: |
||||
|
request_data = json.loads(request.body) if request.body else {} |
||||
|
username = request_data.get("username", "").strip() |
||||
|
password = request_data.get("password", "").strip() |
||||
|
remember = request_data.get("remember", False) |
||||
|
|
||||
|
# 验证输入 |
||||
|
if not username or not password: |
||||
|
return Response( |
||||
|
status_code=400, |
||||
|
description=jsonify({"success": False, "message": "用户名和密码不能为空"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 验证用户 |
||||
|
user = user_service.verify_user(username, password) |
||||
|
if not user: |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "用户名或密码错误"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 生成token并设置过期时间 |
||||
|
token = generate_token() |
||||
|
expires_at = datetime.now() + timedelta(days=7 if remember else 1) |
||||
|
TEMP_TOKENS[token] = {"user": user, "expires_at": expires_at} |
||||
|
|
||||
|
return Response( |
||||
|
status_code=200, |
||||
|
description=jsonify({"success": True, "message": "登录成功", "token": token, "user": user}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
except Exception as e: |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": f"登录失败: {str(e)}"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
@app.post("/api/logout") |
||||
|
def logout_route(request): |
||||
|
"""登出接口""" |
||||
|
try: |
||||
|
request_data = json.loads(request.body) if request.body else {} |
||||
|
token = request_data.get("token", "") |
||||
|
# 删除token |
||||
|
TEMP_TOKENS.pop(token, None) |
||||
|
|
||||
|
return Response( |
||||
|
status_code=200, |
||||
|
description=jsonify({"success": True, "message": "登出成功"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
except Exception as e: |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": f"登出失败: {str(e)}"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
@app.get("/api/userInfo") |
||||
|
def user_info_route(request): |
||||
|
"""获取用户信息接口""" |
||||
|
try: |
||||
|
query_params = getattr(request, 'query_params', {}) |
||||
|
token = query_params.get("token", "") |
||||
|
|
||||
|
# 验证token是否存在 |
||||
|
if not token or token not in TEMP_TOKENS: |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "未登录或登录已过期"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 检查token是否过期 |
||||
|
if datetime.now() > TEMP_TOKENS[token]["expires_at"]: |
||||
|
del TEMP_TOKENS[token] |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "登录已过期"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
return Response( |
||||
|
status_code=200, |
||||
|
description=jsonify({"success": True, "user": TEMP_TOKENS[token]["user"]}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
except Exception as e: |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": f"获取用户信息失败: {str(e)}"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
@app.post("/api/updateAvatar") |
||||
|
def update_avatar_route(request): |
||||
|
"""更新用户头像接口""" |
||||
|
try: |
||||
|
# 打印调试信息 |
||||
|
print(f"请求类型: {type(request)}") |
||||
|
print(f"请求属性: {dir(request)}") |
||||
|
|
||||
|
# 在Robyn中,文件上传的数据存储在request.files中 |
||||
|
# 表单字段存储在request.form中 |
||||
|
form_data = getattr(request, 'form', {}) |
||||
|
files_data = getattr(request, 'files', {}) |
||||
|
|
||||
|
print(f"表单数据: {form_data}") |
||||
|
print(f"文件数据: {files_data}") |
||||
|
print(f"表单数据类型: {type(form_data)}") |
||||
|
print(f"文件数据类型: {type(files_data)}") |
||||
|
|
||||
|
# 获取token - 尝试多种方式获取 |
||||
|
token = None |
||||
|
|
||||
|
# 方法1: 从form_data中获取 |
||||
|
if isinstance(form_data, dict) and "token" in form_data: |
||||
|
token = form_data["token"] |
||||
|
print(f"从form_data获取token: {token}") |
||||
|
|
||||
|
# 方法2: 如果form_data不是字典,尝试其他方式 |
||||
|
if not token and hasattr(form_data, 'get'): |
||||
|
token = form_data.get("token", "") |
||||
|
print(f"通过form_data.get()获取token: {token}") |
||||
|
|
||||
|
# 方法3: 尝试从request的属性中直接获取 |
||||
|
if not token: |
||||
|
# 尝试从request中获取所有可能的属性 |
||||
|
for attr_name in dir(request): |
||||
|
if 'token' in attr_name.lower() or 'form' in attr_name.lower(): |
||||
|
try: |
||||
|
attr_value = getattr(request, attr_name) |
||||
|
print(f"request.{attr_name}: {type(attr_value)} = {attr_value}") |
||||
|
|
||||
|
# 如果是字典类型,检查是否包含token |
||||
|
if isinstance(attr_value, dict) and 'token' in attr_value: |
||||
|
token = attr_value['token'] |
||||
|
print(f"从request.{attr_name}获取token: {token}") |
||||
|
break |
||||
|
except Exception as e: |
||||
|
print(f"访问request.{attr_name}时出错: {e}") |
||||
|
|
||||
|
# 方法4: 从查询参数中获取 |
||||
|
if not token: |
||||
|
query_params = getattr(request, 'query_params', {}) |
||||
|
if isinstance(query_params, dict) and "token" in query_params: |
||||
|
token = query_params["token"] |
||||
|
print(f"从查询参数获取token: {token}") |
||||
|
|
||||
|
# 方法5: 从headers中获取 |
||||
|
if not token: |
||||
|
headers = getattr(request, 'headers', {}) |
||||
|
if isinstance(headers, dict) and "Authorization" in headers: |
||||
|
auth_header = headers["Authorization"] |
||||
|
if auth_header.startswith("Bearer "): |
||||
|
token = auth_header[7:] |
||||
|
print(f"从Authorization头获取token: {token}") |
||||
|
|
||||
|
print(f"最终获取的token: {token}") |
||||
|
print(f"TEMP_TOKENS中的keys: {list(TEMP_TOKENS.keys())}") |
||||
|
|
||||
|
# 验证token |
||||
|
if not token or token not in TEMP_TOKENS: |
||||
|
print(f"Token验证失败: {token}") |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "未登录或登录已过期"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 检查token是否过期 |
||||
|
if datetime.now() > TEMP_TOKENS[token]["expires_at"]: |
||||
|
del TEMP_TOKENS[token] |
||||
|
print("Token已过期") |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "登录已过期"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 获取上传的文件 |
||||
|
avatar_file = files_data.get("avatar") if isinstance(files_data, dict) else None |
||||
|
|
||||
|
# 如果没有通过"avatar"键获取到文件,尝试直接访问files_data的第一个元素 |
||||
|
if not avatar_file and isinstance(files_data, dict) and len(files_data) > 0: |
||||
|
# 获取第一个文件作为头像文件 |
||||
|
first_key = list(files_data.keys())[0] |
||||
|
avatar_file = files_data[first_key] |
||||
|
print(f"通过备用方法获取文件,键名: {first_key}") |
||||
|
|
||||
|
if not avatar_file: |
||||
|
print("未找到avatar文件") |
||||
|
print(f"可用的文件键: {list(files_data.keys()) if isinstance(files_data, dict) else '无法获取键列表'}") |
||||
|
return Response( |
||||
|
status_code=400, |
||||
|
description=jsonify({"success": False, "message": "未上传头像文件"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
print(f"获取的文件: {avatar_file}") |
||||
|
print(f"文件属性: {dir(avatar_file) if avatar_file else '无文件'}") |
||||
|
|
||||
|
# 检查文件类型 - 使用多种方法验证 |
||||
|
is_valid_image = False |
||||
|
file_extension = "" |
||||
|
|
||||
|
# 方法1: 检查content_type |
||||
|
content_type = getattr(avatar_file, 'content_type', '') |
||||
|
if content_type and content_type.startswith("image/"): |
||||
|
is_valid_image = True |
||||
|
print(f"通过content_type验证: {content_type}") |
||||
|
|
||||
|
# 方法2: 检查文件名和扩展名 |
||||
|
filename = getattr(avatar_file, 'filename', '') |
||||
|
if filename: |
||||
|
file_extension = os.path.splitext(filename)[1].lower() |
||||
|
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'] |
||||
|
if file_extension in valid_extensions: |
||||
|
is_valid_image = True |
||||
|
print(f"通过文件扩展名验证: {file_extension}") |
||||
|
|
||||
|
# 如果两种方法都失败,尝试读取文件头 |
||||
|
if not is_valid_image: |
||||
|
try: |
||||
|
# 读取文件前几个字节来检测文件类型 |
||||
|
file_content = avatar_file.read(1024) |
||||
|
avatar_file.seek(0) # 重置文件指针 |
||||
|
|
||||
|
# 检查常见图片格式的文件头 |
||||
|
if (file_content.startswith(b'\xFF\xD8\xFF') or # JPEG |
||||
|
file_content.startswith(b'\x89PNG\r\n\x1a\n') or # PNG |
||||
|
file_content.startswith(b'GIF87a') or # GIF |
||||
|
file_content.startswith(b'GIF89a') or # GIF |
||||
|
file_content.startswith(b'BM') or # BMP |
||||
|
file_content.startswith(b'RIFF') and b'WEBP' in file_content[:12]): # WebP |
||||
|
is_valid_image = True |
||||
|
print(f"通过文件头验证成功") |
||||
|
else: |
||||
|
print(f"文件头验证失败,文件前16字节: {file_content[:16]}") |
||||
|
except Exception as e: |
||||
|
print(f"读取文件内容进行验证时出错: {e}") |
||||
|
|
||||
|
if not is_valid_image: |
||||
|
print(f"文件类型验证失败 - content_type: {content_type}, filename: {filename}, extension: {file_extension}") |
||||
|
return Response( |
||||
|
status_code=400, |
||||
|
description=jsonify({"success": False, "message": f"文件类型必须是图片 (支持的格式: JPG, PNG, GIF, BMP, WebP, SVG)"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 获取文件内容 |
||||
|
file_content = avatar_file.file_data |
||||
|
print(f"读取的文件大小: {len(file_content)} 字节") |
||||
|
|
||||
|
# 生成唯一的文件名 |
||||
|
import time |
||||
|
import random |
||||
|
filename = getattr(avatar_file, 'filename', '') |
||||
|
file_extension = filename.split('.')[-1] if '.' in filename else 'jpg' |
||||
|
timestamp = int(time.time()) |
||||
|
random_num = random.randint(1000, 9999) |
||||
|
username = TEMP_TOKENS[token]["user"]["username"] |
||||
|
new_filename = f"{username}_{timestamp}_{random_num}.{file_extension}" |
||||
|
|
||||
|
# 定义文件保存路径 |
||||
|
avatar_dir = "D:/zhishitupu/MedKG/resource/avatar" |
||||
|
file_path = f"{avatar_dir}/{new_filename}" |
||||
|
|
||||
|
print(f"保存文件到: {file_path}") |
||||
|
|
||||
|
# 确保目录存在 |
||||
|
import os |
||||
|
os.makedirs(avatar_dir, exist_ok=True) |
||||
|
|
||||
|
# 保存文件到磁盘 |
||||
|
with open(file_path, "wb") as f: |
||||
|
f.write(file_content) |
||||
|
|
||||
|
print(f"文件保存成功") |
||||
|
|
||||
|
# 更新用户头像到数据库,存储相对路径 |
||||
|
avatar_relative_path = f"/resource/avatar/{new_filename}" |
||||
|
success = user_service.update_user_avatar(username, avatar_relative_path) |
||||
|
|
||||
|
if not success: |
||||
|
print("数据库更新失败") |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": "更新头像失败"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
print("数据库更新成功") |
||||
|
|
||||
|
# 更新token中的用户信息 |
||||
|
TEMP_TOKENS[token]["user"]["avatar"] = avatar_relative_path |
||||
|
|
||||
|
return Response( |
||||
|
status_code=200, |
||||
|
description=jsonify({ |
||||
|
"success": True, |
||||
|
"message": "头像更新成功", |
||||
|
"avatar": avatar_relative_path |
||||
|
}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
except Exception as e: |
||||
|
print(f"更新头像异常: {str(e)}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": f"更新头像失败: {str(e)}"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
@app.post("/api/updatePassword") |
||||
|
def update_password_route(request): |
||||
|
"""更新用户密码接口""" |
||||
|
try: |
||||
|
print("开始处理密码更新请求") |
||||
|
|
||||
|
# 解析请求数据 |
||||
|
request_data = json.loads(request.body) if request.body else {} |
||||
|
print(f"请求数据: {request_data}") |
||||
|
|
||||
|
token = request_data.get("token", "") |
||||
|
current_password = request_data.get("currentPassword", "") |
||||
|
new_password = request_data.get("newPassword", "") |
||||
|
|
||||
|
print(f"Token: {token}") |
||||
|
print(f"当前密码长度: {len(current_password)}") |
||||
|
print(f"新密码长度: {len(new_password)}") |
||||
|
|
||||
|
# 验证输入 |
||||
|
if not current_password or not new_password: |
||||
|
print("密码为空") |
||||
|
return Response( |
||||
|
status_code=400, |
||||
|
description=jsonify({"success": False, "message": "当前密码和新密码不能为空"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 验证token |
||||
|
if not token or token not in TEMP_TOKENS: |
||||
|
print(f"Token验证失败: {token}") |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "未登录或登录已过期"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 检查token是否过期 |
||||
|
if datetime.now() > TEMP_TOKENS[token]["expires_at"]: |
||||
|
del TEMP_TOKENS[token] |
||||
|
print("Token已过期") |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "登录已过期"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 获取用户信息 |
||||
|
username = TEMP_TOKENS[token]["user"]["username"] |
||||
|
print(f"用户名: {username}") |
||||
|
|
||||
|
# 验证当前密码 |
||||
|
user = user_service.get_user_by_username(username) |
||||
|
if not user: |
||||
|
print("用户不存在") |
||||
|
return Response( |
||||
|
status_code=404, |
||||
|
description=jsonify({"success": False, "message": "用户不存在"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
print(f"用户信息: {user}") |
||||
|
|
||||
|
# 验证密码 |
||||
|
is_password_valid = user_service.verify_password(current_password, user["password"]) |
||||
|
print(f"密码验证结果: {is_password_valid}") |
||||
|
|
||||
|
if not is_password_valid: |
||||
|
print("当前密码不正确") |
||||
|
return Response( |
||||
|
status_code=401, |
||||
|
description=jsonify({"success": False, "message": "当前密码不正确"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
# 更新密码 |
||||
|
success = user_service.update_user_password(username, new_password) |
||||
|
print(f"密码更新结果: {success}") |
||||
|
|
||||
|
if not success: |
||||
|
print("密码更新失败") |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": "密码更新失败"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
print("密码更新成功") |
||||
|
return Response( |
||||
|
status_code=200, |
||||
|
description=jsonify({"success": True, "message": "密码更新成功"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
except Exception as e: |
||||
|
print(f"密码更新异常: {str(e)}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": f"密码更新失败: {str(e)}"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
@app.get("/api/test/db") |
||||
|
def test_db_connection(request): |
||||
|
"""测试数据库连接""" |
||||
|
try: |
||||
|
# 检查数据库连接状态 |
||||
|
is_connected = user_service.mysql.is_connected() |
||||
|
print(f"数据库连接状态: {is_connected}") |
||||
|
|
||||
|
# 尝试查询用户表 |
||||
|
sql = "SELECT COUNT(*) as user_count FROM users" |
||||
|
result = user_service.mysql.execute_query(sql) |
||||
|
print(f"查询结果: {result}") |
||||
|
|
||||
|
# 打印当前的token信息用于调试 |
||||
|
print(f"当前TEMP_TOKENS: {TEMP_TOKENS}") |
||||
|
|
||||
|
return Response( |
||||
|
status_code=200, |
||||
|
description=jsonify({ |
||||
|
"success": True, |
||||
|
"message": "数据库连接测试成功", |
||||
|
"is_connected": is_connected, |
||||
|
"user_count": result[0]["user_count"] if result else 0, |
||||
|
"tokens": list(TEMP_TOKENS.keys()) # 返回当前有效的token列表 |
||||
|
}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
except Exception as e: |
||||
|
print(f"数据库连接测试失败: {str(e)}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
return Response( |
||||
|
status_code=500, |
||||
|
description=jsonify({"success": False, "message": f"数据库连接测试失败: {str(e)}"}), |
||||
|
headers={"Content-Type": "application/json; charset=utf-8"} |
||||
|
) |
||||
|
|
||||
|
@app.after_request("/") |
||||
|
def add_cors_headers(response): |
||||
|
"""添加CORS头,支持跨域请求""" |
||||
|
response.headers.update({ |
||||
|
"Access-Control-Allow-Origin": "*", |
||||
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", |
||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With" |
||||
|
}) |
||||
|
return response |
||||
|
After Width: | Height: | Size: 48 KiB |
@ -0,0 +1,110 @@ |
|||||
|
import bcrypt |
||||
|
from typing import Dict, Any, Optional |
||||
|
from util.mysql_utils import mysql_client |
||||
|
|
||||
|
class UserService: |
||||
|
"""用户服务类,处理用户相关的业务逻辑""" |
||||
|
|
||||
|
def __init__(self): |
||||
|
self.mysql = mysql_client |
||||
|
|
||||
|
def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: |
||||
|
"""根据用户名获取用户信息""" |
||||
|
try: |
||||
|
sql = "SELECT * FROM users WHERE username = %s" |
||||
|
users = self.mysql.execute_query(sql, (username,)) |
||||
|
return users[0] if users else None |
||||
|
except Exception as e: |
||||
|
print(f"查询用户失败: {e}") |
||||
|
return None |
||||
|
|
||||
|
def verify_user(self, username: str, password: str) -> Optional[Dict[str, Any]]: |
||||
|
"""验证用户登录信息""" |
||||
|
try: |
||||
|
user = self.get_user_by_username(username) |
||||
|
if not user: |
||||
|
return None |
||||
|
|
||||
|
stored_password = user.get("password") |
||||
|
|
||||
|
# 验证密码 - 使用bcrypt验证 |
||||
|
if bcrypt.checkpw(password.encode('utf-8'), stored_password.encode('utf-8')): |
||||
|
return { |
||||
|
"id": user.get("id"), |
||||
|
"username": user.get("username"), |
||||
|
"avatar": user.get("avatar") |
||||
|
} |
||||
|
|
||||
|
return None |
||||
|
except Exception as e: |
||||
|
print(f"验证用户失败: {e}") |
||||
|
return None |
||||
|
|
||||
|
def update_user_avatar(self, username: str, avatar_path: str) -> bool: |
||||
|
"""更新用户头像""" |
||||
|
try: |
||||
|
# 检查数据库连接状态 |
||||
|
if not self.mysql.is_connected(): |
||||
|
print("数据库未连接,尝试重新连接...") |
||||
|
if not self.mysql.connect(): |
||||
|
print("数据库连接失败") |
||||
|
return False |
||||
|
|
||||
|
sql = "UPDATE users SET avatar = %s WHERE username = %s" |
||||
|
print(f"执行SQL: {sql}") |
||||
|
print(f"参数: {avatar_path}, {username}") |
||||
|
|
||||
|
result = self.mysql.execute_update(sql, (avatar_path, username)) |
||||
|
print(f"更新结果: {result}") |
||||
|
return result > 0 |
||||
|
except Exception as e: |
||||
|
print(f"更新用户头像失败: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
return False |
||||
|
|
||||
|
def update_user_password(self, username: str, new_password: str) -> bool: |
||||
|
"""更新用户密码""" |
||||
|
try: |
||||
|
# 检查数据库连接状态 |
||||
|
if not self.mysql.is_connected(): |
||||
|
print("数据库未连接,尝试重新连接...") |
||||
|
if not self.mysql.connect(): |
||||
|
print("数据库连接失败") |
||||
|
return False |
||||
|
|
||||
|
# 使用bcrypt加密新密码 |
||||
|
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') |
||||
|
|
||||
|
sql = "UPDATE users SET password = %s WHERE username = %s" |
||||
|
print(f"执行SQL: {sql}") |
||||
|
print(f"参数: {hashed_password}, {username}") |
||||
|
|
||||
|
result = self.mysql.execute_update(sql, (hashed_password, username)) |
||||
|
print(f"更新结果: {result}") |
||||
|
return result > 0 |
||||
|
except Exception as e: |
||||
|
print(f"更新用户密码失败: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
return False |
||||
|
|
||||
|
def verify_password(self, password: str, hashed_password: str) -> bool: |
||||
|
"""验证密码""" |
||||
|
try: |
||||
|
return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8')) |
||||
|
except Exception as e: |
||||
|
print(f"验证密码失败: {e}") |
||||
|
return False |
||||
|
|
||||
|
# 创建全局用户服务实例 |
||||
|
user_service = UserService() |
||||
|
|
||||
|
# 初始化MySQL连接 |
||||
|
def init_mysql_connection(): |
||||
|
"""初始化MySQL连接""" |
||||
|
try: |
||||
|
return user_service.mysql.connect() |
||||
|
except Exception as e: |
||||
|
print(f"初始化MySQL连接失败: {e}") |
||||
|
return False |
||||
@ -0,0 +1,69 @@ |
|||||
|
import pymysql |
||||
|
from typing import List, Dict, Any |
||||
|
from config import MYSQL_CONFIG |
||||
|
|
||||
|
class MySQLClient: |
||||
|
"""MySQL客户端类""" |
||||
|
|
||||
|
def __init__(self): |
||||
|
self.connection = None |
||||
|
|
||||
|
def connect(self): |
||||
|
"""建立数据库连接""" |
||||
|
try: |
||||
|
self.connection = pymysql.connect( |
||||
|
host=MYSQL_CONFIG["host"], |
||||
|
port=MYSQL_CONFIG["port"], |
||||
|
user=MYSQL_CONFIG["user"], |
||||
|
password=MYSQL_CONFIG["password"], |
||||
|
database=MYSQL_CONFIG["database"], |
||||
|
charset=MYSQL_CONFIG["charset"], |
||||
|
cursorclass=pymysql.cursors.DictCursor |
||||
|
) |
||||
|
print(f"MySQL数据库连接成功 - {MYSQL_CONFIG['host']}:{MYSQL_CONFIG['port']}/{MYSQL_CONFIG['database']}") |
||||
|
return True |
||||
|
except Exception as e: |
||||
|
print(f"MySQL数据库连接失败: {e}") |
||||
|
return False |
||||
|
|
||||
|
def execute_query(self, sql: str, params: tuple = None) -> List[Dict[str, Any]]: |
||||
|
"""执行查询语句""" |
||||
|
if not self.connection and not self.connect(): |
||||
|
return [] |
||||
|
|
||||
|
try: |
||||
|
with self.connection.cursor() as cursor: |
||||
|
cursor.execute(sql, params) |
||||
|
return cursor.fetchall() |
||||
|
except Exception as e: |
||||
|
print(f"执行查询失败: {e}") |
||||
|
return [] |
||||
|
|
||||
|
def execute_update(self, sql: str, params: tuple = None) -> int: |
||||
|
"""执行更新语句(INSERT, UPDATE, DELETE)""" |
||||
|
if not self.connection and not self.connect(): |
||||
|
return 0 |
||||
|
|
||||
|
try: |
||||
|
with self.connection.cursor() as cursor: |
||||
|
result = cursor.execute(sql, params) |
||||
|
self.connection.commit() |
||||
|
print(f"执行更新成功,影响行数: {result}") |
||||
|
return result |
||||
|
except Exception as e: |
||||
|
print(f"执行更新失败: {e}") |
||||
|
self.connection.rollback() |
||||
|
return 0 |
||||
|
|
||||
|
def is_connected(self) -> bool: |
||||
|
"""检查数据库连接状态""" |
||||
|
if not self.connection: |
||||
|
return False |
||||
|
try: |
||||
|
self.connection.ping(reconnect=True) |
||||
|
return True |
||||
|
except: |
||||
|
return False |
||||
|
|
||||
|
# 创建全局MySQL客户端实例 |
||||
|
mysql_client = MySQLClient() |
||||
@ -0,0 +1,40 @@ |
|||||
|
// src/api/login.js
|
||||
|
import request from '@/utils/request'; |
||||
|
|
||||
|
/** |
||||
|
* 用户登录 |
||||
|
* 后端接口:POST /login |
||||
|
* @param {Object} data - 登录信息 |
||||
|
* @param {string} data.username - 用户名 |
||||
|
* @param {string} data.password - 密码 |
||||
|
* @param {boolean} data.remember - 是否记住密码 |
||||
|
*/ |
||||
|
export function login(data) { |
||||
|
return request({ |
||||
|
url: '/login', |
||||
|
method: 'post', |
||||
|
data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 用户登出 |
||||
|
* 后端接口:POST /logout |
||||
|
*/ |
||||
|
export function logout() { |
||||
|
return request({ |
||||
|
url: '/logout', |
||||
|
method: 'post' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取用户信息 |
||||
|
* 后端接口:GET /userInfo |
||||
|
*/ |
||||
|
export function getUserInfo() { |
||||
|
return request({ |
||||
|
url: '/userInfo', |
||||
|
method: 'get' |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// src/api/profile.js
|
||||
|
import request from '@/utils/request'; |
||||
|
|
||||
|
/** |
||||
|
* 获取用户信息 |
||||
|
* 后端接口:GET /api/userInfo |
||||
|
* @param {string} token - 用户登录令牌 |
||||
|
*/ |
||||
|
export function getUserProfile(token) { |
||||
|
return request({ |
||||
|
url: '/userInfo', |
||||
|
method: 'get', |
||||
|
params: { |
||||
|
token |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新用户头像 |
||||
|
* 后端接口:POST /api/updateAvatar |
||||
|
* @param {FormData} formData - 包含头像文件和token的表单数据 |
||||
|
*/ |
||||
|
export function updateAvatar(formData) { |
||||
|
return request({ |
||||
|
url: '/updateAvatar', |
||||
|
method: 'post', |
||||
|
data: formData, |
||||
|
headers: { |
||||
|
'Content-Type': 'multipart/form-data' |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新用户密码 |
||||
|
* 后端接口:POST /api/updatePassword |
||||
|
* @param {Object} data - 包含旧密码、新密码和token的数据 |
||||
|
*/ |
||||
|
export function updatePassword(data) { |
||||
|
return request({ |
||||
|
url: '/updatePassword', |
||||
|
method: 'post', |
||||
|
data |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
import { createRouter, createWebHistory } from 'vue-router' |
||||
|
import Login from '../system/Login.vue' |
||||
|
import Index from '../system/Index.vue' |
||||
|
import Profile from '../system/Profile.vue' |
||||
|
|
||||
|
const routes = [ |
||||
|
{ |
||||
|
path: '/', |
||||
|
redirect: '/login' |
||||
|
}, |
||||
|
{ |
||||
|
path: '/login', |
||||
|
name: 'Login', |
||||
|
component: Login |
||||
|
}, |
||||
|
{ |
||||
|
path: '/index', |
||||
|
name: 'Index', |
||||
|
component: Index |
||||
|
}, |
||||
|
{ |
||||
|
path: '/profile', |
||||
|
name: 'Profile', |
||||
|
component: Profile |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
const router = createRouter({ |
||||
|
history: createWebHistory(), |
||||
|
routes |
||||
|
}) |
||||
|
|
||||
|
export default router |
||||
@ -0,0 +1,639 @@ |
|||||
|
<template> |
||||
|
<div class="app-container"> |
||||
|
<!-- 引入侧边栏组件 --> |
||||
|
<Menu |
||||
|
:initial-active="0" |
||||
|
@menu-click="handleSidebarClick" |
||||
|
/> |
||||
|
|
||||
|
<!-- 主内容区域 --> |
||||
|
<div class="main-content"> |
||||
|
<div class="profile-container"> |
||||
|
<!-- 页面标题 --> |
||||
|
<div class="page-header"> |
||||
|
<h1 class="page-title">个人主页</h1> |
||||
|
<p class="page-subtitle">管理您的个人信息和账户设置</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 个人信息卡片 --> |
||||
|
<div class="profile-card"> |
||||
|
<!-- 头像区域 --> |
||||
|
<div class="avatar-section"> |
||||
|
<div class="avatar-container"> |
||||
|
<img :src="userProfile.avatar" alt="用户头像" class="avatar-image"> |
||||
|
<div class="avatar-overlay" @click="triggerFileInput"> |
||||
|
<span class="camera-icon">📷</span> |
||||
|
<span class="overlay-text">更换头像</span> |
||||
|
</div> |
||||
|
<input |
||||
|
type="file" |
||||
|
ref="fileInput" |
||||
|
@change="handleAvatarChange" |
||||
|
accept="image/*" |
||||
|
class="hidden-input" |
||||
|
> |
||||
|
</div> |
||||
|
<div class="username-display">{{ userProfile.username }}</div> |
||||
|
<div class="avatar-status"> |
||||
|
<p v-if="avatarUploading" class="uploading-text">上传中...</p> |
||||
|
<p v-if="avatarUploadSuccess" class="success-text">头像更新成功</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 用户信息表单 --> |
||||
|
<div class="info-section"> |
||||
|
<h2 class="section-title">修改密码</h2> |
||||
|
<form class="password-form" @submit.prevent="changePassword"> |
||||
|
<div class="form-group"> |
||||
|
<label for="currentPassword" class="form-label">当前密码</label> |
||||
|
<input |
||||
|
type="password" |
||||
|
id="currentPassword" |
||||
|
v-model="passwordForm.currentPassword" |
||||
|
class="form-input" |
||||
|
placeholder="请输入当前密码" |
||||
|
required |
||||
|
> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="newPassword" class="form-label">新密码</label> |
||||
|
<input |
||||
|
type="password" |
||||
|
id="newPassword" |
||||
|
v-model="passwordForm.newPassword" |
||||
|
class="form-input" |
||||
|
placeholder="请输入新密码" |
||||
|
required |
||||
|
> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="confirmPassword" class="form-label">确认新密码</label> |
||||
|
<input |
||||
|
type="password" |
||||
|
id="confirmPassword" |
||||
|
v-model="passwordForm.confirmPassword" |
||||
|
class="form-input" |
||||
|
placeholder="请再次输入新密码" |
||||
|
required |
||||
|
> |
||||
|
<p v-if="passwordMismatch" class="error-text">两次输入的密码不一致</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-actions"> |
||||
|
<button type="submit" class="save-button" :disabled="passwordSaving || passwordMismatch"> |
||||
|
<span v-if="!passwordSaving">修改密码</span> |
||||
|
<span v-else>修改中...</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 操作反馈 --> |
||||
|
<div v-if="successMessage" class="success-message"> |
||||
|
{{ successMessage }} |
||||
|
</div> |
||||
|
<div v-if="errorMessage" class="error-message"> |
||||
|
{{ errorMessage }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { ref, computed, onMounted } from 'vue'; |
||||
|
import Menu from '../components/Menu.vue'; |
||||
|
import { getUserProfile, updateAvatar, updatePassword } from '../api/profile'; |
||||
|
|
||||
|
// 处理侧边栏菜单点击 |
||||
|
const handleSidebarClick = (menuItem) => { |
||||
|
console.log('点击了菜单项:', menuItem); |
||||
|
}; |
||||
|
|
||||
|
// 用户信息数据 |
||||
|
const userProfile = ref({ |
||||
|
username: 'admin', |
||||
|
avatar: '/resource/avatar/1.jpg' |
||||
|
}); |
||||
|
|
||||
|
// 密码表单数据 |
||||
|
const passwordForm = ref({ |
||||
|
currentPassword: '', |
||||
|
newPassword: '', |
||||
|
confirmPassword: '' |
||||
|
}); |
||||
|
|
||||
|
// 状态变量 |
||||
|
const fileInput = ref(null); |
||||
|
const avatarUploading = ref(false); |
||||
|
const avatarUploadSuccess = ref(false); |
||||
|
const profileSaving = ref(false); |
||||
|
const passwordSaving = ref(false); |
||||
|
const successMessage = ref(''); |
||||
|
const errorMessage = ref(''); |
||||
|
|
||||
|
// 计算属性 |
||||
|
const passwordMismatch = computed(() => { |
||||
|
return passwordForm.value.newPassword && |
||||
|
passwordForm.value.confirmPassword && |
||||
|
passwordForm.value.newPassword !== passwordForm.value.confirmPassword; |
||||
|
}); |
||||
|
|
||||
|
// 触发文件选择 |
||||
|
const triggerFileInput = () => { |
||||
|
fileInput.value.click(); |
||||
|
}; |
||||
|
|
||||
|
// 处理头像更改 |
||||
|
const handleAvatarChange = async (event) => { |
||||
|
const file = event.target.files[0]; |
||||
|
if (file) { |
||||
|
// 检查文件类型 |
||||
|
if (!file.type.startsWith('image/')) { |
||||
|
errorMessage.value = '请选择图片文件'; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 检查文件大小 (限制为2MB) |
||||
|
if (file.size > 2 * 1024 * 1024) { |
||||
|
errorMessage.value = '图片大小不能超过2MB'; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 开始上传 |
||||
|
avatarUploading.value = true; |
||||
|
avatarUploadSuccess.value = false; |
||||
|
errorMessage.value = ''; |
||||
|
|
||||
|
try { |
||||
|
// 从localStorage获取token |
||||
|
const token = localStorage.getItem('token'); |
||||
|
|
||||
|
if (!token) { |
||||
|
errorMessage.value = '用户未登录,请先登录'; |
||||
|
avatarUploading.value = false; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 创建FormData对象 |
||||
|
const formData = new FormData(); |
||||
|
formData.append('avatar', file); |
||||
|
formData.append('token', token); |
||||
|
|
||||
|
console.log('准备上传头像文件:', file.name); |
||||
|
console.log('Token:', token); |
||||
|
|
||||
|
// 调用API上传头像 |
||||
|
const response = await updateAvatar(formData); |
||||
|
|
||||
|
console.log('收到响应:', response); |
||||
|
|
||||
|
if (response.success) { |
||||
|
// 使用返回的头像路径 |
||||
|
// 确保头像URL是完整的路径 |
||||
|
let avatarPath = response.avatar; |
||||
|
if (avatarPath.startsWith('/resource/')) { |
||||
|
// 如果是相对路径,保持原样(通过代理访问) |
||||
|
avatarPath = avatarPath; |
||||
|
} |
||||
|
userProfile.value.avatar = avatarPath; |
||||
|
|
||||
|
// 显示成功消息 |
||||
|
avatarUploadSuccess.value = true; |
||||
|
successMessage.value = '头像更新成功'; |
||||
|
|
||||
|
// 3秒后隐藏成功提示 |
||||
|
setTimeout(() => { |
||||
|
avatarUploadSuccess.value = false; |
||||
|
successMessage.value = ''; |
||||
|
}, 3000); |
||||
|
} else { |
||||
|
errorMessage.value = response.message || '头像更新失败'; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('头像上传失败:', error); |
||||
|
console.error('错误详情:', { |
||||
|
message: error.message, |
||||
|
stack: error.stack, |
||||
|
name: error.name |
||||
|
}); |
||||
|
|
||||
|
// 提供更详细的错误信息 |
||||
|
if (error.response) { |
||||
|
// 服务器响应了错误状态码 |
||||
|
console.error('服务器响应:', error.response.status, error.response.data); |
||||
|
errorMessage.value = `服务器错误 ${error.response.status}: ${JSON.stringify(error.response.data)}`; |
||||
|
} else if (error.request) { |
||||
|
// 请求已发出但没有收到响应 |
||||
|
console.error('网络错误,未收到响应:', error.request); |
||||
|
errorMessage.value = '网络错误,请检查网络连接和服务器状态'; |
||||
|
} else { |
||||
|
// 其他错误 |
||||
|
errorMessage.value = `请求配置错误: ${error.message}`; |
||||
|
} |
||||
|
} finally { |
||||
|
avatarUploading.value = false; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 更新个人信息 |
||||
|
const updateProfile = () => { |
||||
|
profileSaving.value = true; |
||||
|
errorMessage.value = ''; |
||||
|
successMessage.value = ''; |
||||
|
|
||||
|
// 模拟API请求 |
||||
|
setTimeout(() => { |
||||
|
profileSaving.value = false; |
||||
|
successMessage.value = '个人信息更新成功'; |
||||
|
|
||||
|
// 3秒后隐藏成功提示 |
||||
|
setTimeout(() => { |
||||
|
successMessage.value = ''; |
||||
|
}, 3000); |
||||
|
}, 1000); |
||||
|
}; |
||||
|
|
||||
|
// 修改密码 |
||||
|
const changePassword = async () => { |
||||
|
if (passwordMismatch.value) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
passwordSaving.value = true; |
||||
|
errorMessage.value = ''; |
||||
|
successMessage.value = ''; |
||||
|
|
||||
|
try { |
||||
|
// 从localStorage获取token |
||||
|
const token = localStorage.getItem('token'); |
||||
|
|
||||
|
if (!token) { |
||||
|
errorMessage.value = '用户未登录,请先登录'; |
||||
|
passwordSaving.value = false; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 调用API修改密码 |
||||
|
const response = await updatePassword({ |
||||
|
token, |
||||
|
currentPassword: passwordForm.value.currentPassword, |
||||
|
newPassword: passwordForm.value.newPassword |
||||
|
}); |
||||
|
|
||||
|
if (response.success) { |
||||
|
successMessage.value = '密码修改成功'; |
||||
|
|
||||
|
// 清空表单 |
||||
|
passwordForm.value = { |
||||
|
currentPassword: '', |
||||
|
newPassword: '', |
||||
|
confirmPassword: '' |
||||
|
}; |
||||
|
|
||||
|
// 3秒后隐藏成功提示 |
||||
|
setTimeout(() => { |
||||
|
successMessage.value = ''; |
||||
|
}, 3000); |
||||
|
} else { |
||||
|
errorMessage.value = response.message || '密码修改失败'; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('密码修改失败:', error); |
||||
|
errorMessage.value = '密码修改时发生错误'; |
||||
|
} finally { |
||||
|
passwordSaving.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 组件挂载时获取用户信息 |
||||
|
onMounted(async () => { |
||||
|
try { |
||||
|
// 从localStorage获取token |
||||
|
const token = localStorage.getItem('token'); |
||||
|
|
||||
|
if (token) { |
||||
|
// 调用API获取用户信息 |
||||
|
const response = await getUserProfile(token); |
||||
|
|
||||
|
if (response.success) { |
||||
|
// 更新用户信息 |
||||
|
// 如果头像路径是相对路径,需要添加服务器前缀 |
||||
|
let avatarUrl = response.user.avatar || '/resource/avatar/1.jpg'; |
||||
|
if (avatarUrl.startsWith('/resource/')) { |
||||
|
avatarUrl = avatarUrl; // 相对路径,直接使用 |
||||
|
} |
||||
|
|
||||
|
userProfile.value = { |
||||
|
username: response.user.username, |
||||
|
avatar: avatarUrl |
||||
|
}; |
||||
|
} else { |
||||
|
errorMessage.value = response.message || '获取用户信息失败'; |
||||
|
} |
||||
|
} else { |
||||
|
errorMessage.value = '用户未登录,请先登录'; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('获取用户信息失败:', error); |
||||
|
errorMessage.value = '获取用户信息时发生错误'; |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.app-container { |
||||
|
display: flex; |
||||
|
height: 100vh; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.main-content { |
||||
|
flex: 1; |
||||
|
padding: 1.5rem; |
||||
|
overflow-y: auto; |
||||
|
height: 100%; |
||||
|
background-color: #f5f7fa; |
||||
|
transition: margin-left 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
/* 确保左侧导航栏完全固定 */ |
||||
|
.app-container > :first-child { |
||||
|
position: fixed; |
||||
|
height: 100vh; |
||||
|
z-index: 10; |
||||
|
} |
||||
|
|
||||
|
/* 为右侧内容添加左边距,避免被固定导航栏遮挡 */ |
||||
|
.main-content { |
||||
|
margin-left: 240px; /* 与Menu.vue中的sidebar-container宽度相同 */ |
||||
|
} |
||||
|
|
||||
|
/* 当菜单折叠时调整内容区域 */ |
||||
|
.app-container:has(.sidebar-container.collapsed) .main-content { |
||||
|
margin-left: 60px; |
||||
|
} |
||||
|
|
||||
|
.profile-container { |
||||
|
max-width: 700px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
/* 页面标题样式 */ |
||||
|
.page-header { |
||||
|
margin-bottom: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.page-title { |
||||
|
font-size: 1.5rem; |
||||
|
font-weight: 600; |
||||
|
color: #1f2937; |
||||
|
margin: 0 0 0.5rem 0; |
||||
|
} |
||||
|
|
||||
|
.page-subtitle { |
||||
|
color: #6b7280; |
||||
|
margin: 0; |
||||
|
font-size: 0.875rem; |
||||
|
} |
||||
|
|
||||
|
/* 密码修改卡片 */ |
||||
|
.password-card { |
||||
|
background-color: #fff; |
||||
|
border-radius: 0.5rem; |
||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
||||
|
padding: 1.5rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
max-width: 600px; |
||||
|
margin-left: auto; |
||||
|
margin-right: auto; |
||||
|
} |
||||
|
|
||||
|
.password-card .section-title { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 个人信息卡片 */ |
||||
|
.profile-card { |
||||
|
background-color: #fff; |
||||
|
border-radius: 0.5rem; |
||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
||||
|
padding: 1.5rem; |
||||
|
margin-bottom: 1.5rem; |
||||
|
max-width: 600px; |
||||
|
margin-left: auto; |
||||
|
margin-right: auto; |
||||
|
} |
||||
|
|
||||
|
/* 头像区域样式 */ |
||||
|
.avatar-section { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
margin-bottom: 2rem; |
||||
|
padding-bottom: 1.5rem; |
||||
|
border-bottom: 1px solid #e5e7eb; |
||||
|
} |
||||
|
|
||||
|
.avatar-container { |
||||
|
position: relative; |
||||
|
width: 100px; |
||||
|
height: 100px; |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
|
||||
|
.avatar-image { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
border-radius: 50%; |
||||
|
object-fit: cover; |
||||
|
border: 3px solid #e5e7eb; |
||||
|
} |
||||
|
|
||||
|
.avatar-overlay { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background-color: rgba(0, 0, 0, 0.5); |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
opacity: 0; |
||||
|
transition: opacity 0.2s; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.avatar-container:hover .avatar-overlay { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.camera-icon { |
||||
|
font-size: 1.5rem; |
||||
|
margin-bottom: 0.25rem; |
||||
|
} |
||||
|
|
||||
|
.overlay-text { |
||||
|
font-size: 0.75rem; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.hidden-input { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.avatar-status { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.username-display { |
||||
|
text-align: center; |
||||
|
font-size: 1.125rem; |
||||
|
font-weight: 600; |
||||
|
color: #1f2937; |
||||
|
margin-top: 0.75rem; |
||||
|
margin-bottom: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
.uploading-text, .success-text { |
||||
|
font-size: 0.875rem; |
||||
|
margin: 0.25rem 0; |
||||
|
} |
||||
|
|
||||
|
.uploading-text { |
||||
|
color: #3b82f6; |
||||
|
} |
||||
|
|
||||
|
.success-text { |
||||
|
color: #10b981; |
||||
|
} |
||||
|
|
||||
|
/* 信息区域样式 */ |
||||
|
.info-section { |
||||
|
margin-bottom: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 1.125rem; |
||||
|
font-weight: 600; |
||||
|
color: #1f2937; |
||||
|
margin: 0 0 1rem 0; |
||||
|
padding-bottom: 0.5rem; |
||||
|
border-bottom: 1px solid #e5e7eb; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 表单样式 */ |
||||
|
.profile-form, .password-form { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 1rem; |
||||
|
max-width: 400px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.form-label { |
||||
|
font-size: 0.875rem; |
||||
|
font-weight: 500; |
||||
|
color: #374151; |
||||
|
margin-bottom: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
.form-input { |
||||
|
padding: 0.625rem 0.875rem; |
||||
|
border: 1px solid #d1d5db; |
||||
|
border-radius: 0.375rem; |
||||
|
font-size: 0.875rem; |
||||
|
transition: border-color 0.2s; |
||||
|
width: 100%; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
.form-input:focus { |
||||
|
outline: none; |
||||
|
border-color: #3b82f6; |
||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); |
||||
|
} |
||||
|
|
||||
|
.form-input[readonly] { |
||||
|
background-color: #f9fafb; |
||||
|
color: #6b7280; |
||||
|
} |
||||
|
|
||||
|
.form-input::placeholder { |
||||
|
color: #9ca3af; |
||||
|
} |
||||
|
|
||||
|
.form-actions { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin-top: 1rem; |
||||
|
} |
||||
|
|
||||
|
.save-button { |
||||
|
background-color: #3b82f6; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 0.375rem; |
||||
|
padding: 0.625rem 1.25rem; |
||||
|
font-size: 0.875rem; |
||||
|
font-weight: 500; |
||||
|
cursor: pointer; |
||||
|
transition: background-color 0.2s; |
||||
|
min-width: 120px; |
||||
|
} |
||||
|
|
||||
|
.save-button:hover:not(:disabled) { |
||||
|
background-color: #2563eb; |
||||
|
} |
||||
|
|
||||
|
.save-button:disabled { |
||||
|
background-color: #9ca3af; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
/* 错误和成功消息样式 */ |
||||
|
.error-text { |
||||
|
color: #ef4444; |
||||
|
font-size: 0.75rem; |
||||
|
margin-top: 0.25rem; |
||||
|
} |
||||
|
|
||||
|
.success-message, .error-message { |
||||
|
padding: 0.75rem 1rem; |
||||
|
border-radius: 0.375rem; |
||||
|
margin-top: 1rem; |
||||
|
font-size: 0.875rem; |
||||
|
} |
||||
|
|
||||
|
.success-message { |
||||
|
background-color: #d1fae5; |
||||
|
color: #065f46; |
||||
|
border: 1px solid #a7f3d0; |
||||
|
} |
||||
|
|
||||
|
.error-message { |
||||
|
background-color: #fee2e2; |
||||
|
color: #991b1b; |
||||
|
border: 1px solid #fca5a5; |
||||
|
} |
||||
|
|
||||
|
/* 响应式调整 */ |
||||
|
@media (max-width: 768px) { |
||||
|
.main-content { |
||||
|
margin-left: 70px; /* 移动端侧边栏宽度 */ |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,14 @@ |
|||||
|
from app import app |
||||
|
import controller.LoginController |
||||
|
from service.UserService import init_mysql_connection |
||||
|
import os |
||||
|
|
||||
|
# 添加静态文件服务 |
||||
|
current_dir = os.path.dirname(os.path.abspath(__file__)) |
||||
|
resource_dir = os.path.join(current_dir, "resource") |
||||
|
if os.path.exists(resource_dir): |
||||
|
app.serve_directory("/resource", resource_dir) |
||||
|
print(f"静态资源目录已配置: {resource_dir}") |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
init_mysql_connection() and app.start(host="0.0.0.0", port=8088) |
||||
Loading…
Reference in new issue