20 changed files with 2533 additions and 110 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 |
|||
}); |
|||
} |
|||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 724 B |
|
After Width: | Height: | Size: 422 B |
|
After Width: | Height: | Size: 675 KiB |
@ -0,0 +1,423 @@ |
|||
<template> |
|||
<div class="sidebar-container" :class="{ 'collapsed': isCollapsed }"> |
|||
<!-- 折叠按钮 --> |
|||
<button @click="toggleCollapse" class="collapse-btn"> |
|||
<span class="collapse-icon">{{ isCollapsed ? '▶' : '◀' }}</span> |
|||
</button> |
|||
|
|||
<!-- 系统标题区域 --> |
|||
<div class="sidebar-header"> |
|||
<h1 class="sidebar-title" v-show="!isCollapsed" @click="goToIndex"> |
|||
面向疾病预测的知识图谱应用系统 |
|||
</h1> |
|||
<h1 class="sidebar-title-collapsed" v-show="isCollapsed" @click="goToIndex"> |
|||
医疗知识图谱 |
|||
</h1> |
|||
</div> |
|||
|
|||
<!-- 菜单列表 --> |
|||
<nav class="sidebar-nav"> |
|||
<ul> |
|||
<li |
|||
v-for="(item, index) in menuItems" |
|||
:key="index" |
|||
class="menu-item" |
|||
> |
|||
<a |
|||
@click.prevent="handleMenuClick(index)" |
|||
class="menu-link" |
|||
:class="{ 'active': activeIndex === index }" |
|||
:title="isCollapsed ? item.name : ''" |
|||
> |
|||
<span class="menu-icon">{{ item.icon }}</span> |
|||
<span v-show="!isCollapsed">{{ item.name }}</span> |
|||
</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
|
|||
<!-- 底部用户信息和退出登录区域 --> |
|||
<div class="sidebar-footer"> |
|||
<div class="user-info"> |
|||
<div class="user-avatar"> |
|||
<span class="avatar-text">管</span> |
|||
</div> |
|||
<div class="user-details" v-show="!isCollapsed"> |
|||
<p class="user-name">管理员</p> |
|||
<p class="user-email">admin@example.com</p> |
|||
</div> |
|||
<div class="action-buttons" v-show="!isCollapsed"> |
|||
<button @click="handleProfile" class="profile-btn-inline" title="个人主页"> |
|||
个人主页 |
|||
</button> |
|||
<button @click="handleLogout" class="logout-btn-inline" title="退出登录"> |
|||
退出登录 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, defineProps, defineEmits } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// 定义组件属性 |
|||
const props = defineProps({ |
|||
// 初始选中的菜单索引 |
|||
initialActive: { |
|||
type: Number, |
|||
default: 0 |
|||
} |
|||
}); |
|||
|
|||
// 定义组件事件 |
|||
const emit = defineEmits(['menu-click']); |
|||
|
|||
// 菜单数据 |
|||
const menuItems = ref([ |
|||
{ |
|||
name: '医疗知识图谱', |
|||
path: '/medical-kg', |
|||
icon: '🏥' |
|||
}, |
|||
{ |
|||
name: '知识图谱构建', |
|||
path: '/kg-construction', |
|||
icon: '🔧' |
|||
}, |
|||
{ |
|||
name: '知识图谱问答', |
|||
path: '/kg-qa', |
|||
icon: '💬' |
|||
}, |
|||
{ |
|||
name: '知识图谱数据', |
|||
path: '/kg-data', |
|||
icon: '📊' |
|||
} |
|||
]); |
|||
|
|||
// 活跃菜单索引 |
|||
const activeIndex = ref(props.initialActive); |
|||
|
|||
// 菜单折叠状态 |
|||
const isCollapsed = ref(false); |
|||
|
|||
// 切换菜单折叠状态 |
|||
const toggleCollapse = () => { |
|||
isCollapsed.value = !isCollapsed.value; |
|||
}; |
|||
|
|||
// 处理菜单点击 |
|||
const handleMenuClick = (index) => { |
|||
activeIndex.value = index; |
|||
emit('menu-click', menuItems.value[index]); |
|||
}; |
|||
|
|||
// 处理个人主页点击 |
|||
const handleProfile = () => { |
|||
// 使用Vue Router跳转到个人主页页面 |
|||
router.push('/profile'); |
|||
}; |
|||
|
|||
// 处理退出登录 |
|||
const handleLogout = () => { |
|||
// 使用Vue Router跳转到登录页面 |
|||
router.push('/login'); |
|||
}; |
|||
|
|||
// 跳转到首页 |
|||
const goToIndex = () => { |
|||
router.push('/index'); |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.sidebar-container { |
|||
width: 240px; |
|||
height: 100vh; |
|||
background-color: #1e40af; |
|||
color: white; |
|||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|||
display: flex; |
|||
flex-direction: column; |
|||
position: relative; |
|||
transition: width 0.3s ease; |
|||
} |
|||
|
|||
.sidebar-container.collapsed { |
|||
width: 60px; |
|||
} |
|||
|
|||
.collapse-btn { |
|||
position: absolute; |
|||
top: 50%; |
|||
right: -15px; |
|||
width: 30px; |
|||
height: 30px; |
|||
background-color: #1e40af; |
|||
border: 2px solid white; |
|||
border-radius: 50%; |
|||
color: white; |
|||
cursor: pointer; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 100; |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); |
|||
transform: translateY(-50%); |
|||
} |
|||
|
|||
.collapse-btn:hover { |
|||
background-color: #1e3a8a; |
|||
} |
|||
|
|||
.collapse-icon { |
|||
font-size: 14px; |
|||
line-height: 1; |
|||
} |
|||
|
|||
.sidebar-header { |
|||
padding: 1.25rem; |
|||
border-bottom: 1px solid #1e3a8a; |
|||
} |
|||
|
|||
.sidebar-title { |
|||
font-size: 1.25rem; |
|||
font-weight: bold; |
|||
letter-spacing: -0.025em; |
|||
transition: opacity 0.3s ease; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.sidebar-title:hover { |
|||
color: #93c5fd; |
|||
} |
|||
|
|||
.sidebar-title-collapsed { |
|||
font-size: 0.75rem; |
|||
font-weight: bold; |
|||
text-align: center; |
|||
margin: 0; |
|||
padding: 0.5rem 0; |
|||
writing-mode: vertical-rl; |
|||
text-orientation: mixed; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.sidebar-title-collapsed:hover { |
|||
color: #93c5fd; |
|||
} |
|||
|
|||
.sidebar-nav { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 1rem 0; |
|||
} |
|||
|
|||
.sidebar-nav ul { |
|||
padding-left: 0; |
|||
margin: 0; |
|||
} |
|||
|
|||
.menu-item { |
|||
margin-bottom: 0.25rem; |
|||
list-style-type: none; |
|||
} |
|||
|
|||
.menu-link { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 0.75rem 1.25rem; |
|||
color: white; |
|||
text-decoration: none; |
|||
transition: background-color 0.2s; |
|||
font-size: 1.1rem; |
|||
} |
|||
|
|||
.menu-link:hover { |
|||
background-color: #1e3a8a; |
|||
} |
|||
|
|||
.menu-link.active { |
|||
background-color: #1e3a8a; |
|||
border-left: 4px solid #60a5fa; |
|||
} |
|||
|
|||
.menu-icon { |
|||
margin-right: 0.75rem; |
|||
color: #93c5fd; |
|||
font-size: 1.4rem; |
|||
transition: margin 0.3s ease; |
|||
} |
|||
|
|||
.sidebar-container.collapsed .menu-icon { |
|||
margin-right: 0; |
|||
justify-content: center; |
|||
display: flex; |
|||
} |
|||
|
|||
.menu-link:hover .menu-icon { |
|||
color: white; |
|||
} |
|||
|
|||
.sidebar-footer { |
|||
padding: 1rem; |
|||
border-top: 1px solid #1e3a8a; |
|||
} |
|||
|
|||
.user-info { |
|||
display: flex; |
|||
align-items: center; |
|||
position: relative; |
|||
justify-content: center; |
|||
padding: 0.5rem 0; |
|||
} |
|||
|
|||
.sidebar-container.collapsed .user-info { |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.action-buttons { |
|||
position: absolute; |
|||
bottom: 100%; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
display: flex; |
|||
flex-direction: column; |
|||
opacity: 0; |
|||
transition: opacity 0.3s ease, bottom 0.3s ease; |
|||
background-color: white; |
|||
border-radius: 8px; |
|||
padding: 8px 0; |
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
|||
margin-bottom: 10px; |
|||
min-width: 120px; |
|||
} |
|||
|
|||
.user-info:hover .action-buttons { |
|||
opacity: 1; |
|||
bottom: 100%; |
|||
} |
|||
|
|||
.user-avatar { |
|||
width: 2rem; |
|||
height: 2rem; |
|||
border-radius: 50%; |
|||
background-color: #274eb8; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-right: 0.75rem; |
|||
} |
|||
|
|||
.avatar-text { |
|||
font-size: 0.875rem; |
|||
} |
|||
|
|||
.user-details { |
|||
flex: 1; |
|||
} |
|||
|
|||
.user-name { |
|||
font-size: 0.875rem; |
|||
font-weight: 500; |
|||
margin: 0; |
|||
} |
|||
|
|||
.user-email { |
|||
font-size: 0.75rem; |
|||
color: #93c5fd; |
|||
margin: 0; |
|||
} |
|||
|
|||
/* 退出登录按钮样式 */ |
|||
.profile-btn-inline, .logout-btn-inline { |
|||
background: transparent; |
|||
border: none; |
|||
color: #333; |
|||
cursor: pointer; |
|||
padding: 10px 16px; |
|||
transition: all 0.2s; |
|||
display: block; |
|||
width: 100%; |
|||
text-align: left; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.profile-btn-inline:hover, .logout-btn-inline:hover { |
|||
background-color: #f5f7fa; |
|||
color: #1e40af; |
|||
} |
|||
|
|||
.profile-icon, .logout-icon { |
|||
font-size: 1.2rem; |
|||
} |
|||
|
|||
/* 响应式调整 */ |
|||
@media (max-width: 768px) { |
|||
.sidebar-container { |
|||
width: 70px; |
|||
} |
|||
|
|||
.sidebar-container.collapsed { |
|||
width: 60px; |
|||
} |
|||
|
|||
.collapse-btn { |
|||
right: -10px; |
|||
width: 25px; |
|||
height: 25px; |
|||
} |
|||
|
|||
.sidebar-title, |
|||
.sidebar-title-collapsed, |
|||
.menu-link span:not(.menu-icon), |
|||
.user-details { |
|||
display: none; |
|||
} |
|||
|
|||
.menu-link { |
|||
justify-content: center; |
|||
padding: 0.5rem; |
|||
} |
|||
|
|||
.menu-icon { |
|||
margin-right: 0; |
|||
font-size: 1.2rem; |
|||
} |
|||
|
|||
.menu-link.active { |
|||
border-left-width: 2px; |
|||
} |
|||
|
|||
.action-buttons { |
|||
position: static; |
|||
opacity: 1; |
|||
transform: none; |
|||
background-color: transparent; |
|||
padding: 0; |
|||
box-shadow: none; |
|||
margin-top: 0.5rem; |
|||
flex-direction: row; |
|||
justify-content: center; |
|||
min-width: auto; |
|||
} |
|||
|
|||
.user-info:hover .action-buttons { |
|||
bottom: auto; |
|||
} |
|||
|
|||
.profile-btn-inline, .logout-btn-inline { |
|||
padding: 6px 8px; |
|||
font-size: 12px; |
|||
margin: 0 2px; |
|||
display: inline-block; |
|||
width: auto; |
|||
} |
|||
} |
|||
</style> |
|||
@ -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,60 @@ |
|||
<template> |
|||
<div class="app-container"> |
|||
<!-- 引入侧边栏组件 --> |
|||
<Menu |
|||
:initial-active="0" |
|||
@menu-click="handleSidebarClick" |
|||
/> |
|||
|
|||
<!-- 主内容区域 --> |
|||
<div class="main-content"> |
|||
<h1 class="text-2xl font-bold mb-4">首页</h1> |
|||
<div class="bg-white p-6 rounded-lg shadow"> |
|||
<p>欢迎使用面向疾病预测的知识图谱应用系统</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import Menu from '../components/Menu.vue'; |
|||
|
|||
// 处理侧边栏菜单点击 |
|||
const handleSidebarClick = (menuItem) => { |
|||
console.log('点击了菜单项:', menuItem); |
|||
// 这里可以添加路由跳转或其他逻辑 |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.app-container { |
|||
display: flex; |
|||
height: 100vh; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.main-content { |
|||
flex: 1; |
|||
padding: 1.5rem; |
|||
overflow-y: auto; |
|||
height: 100%; |
|||
} |
|||
|
|||
/* 确保左侧导航栏完全固定 */ |
|||
.app-container > :first-child { |
|||
position: fixed; |
|||
height: 100vh; |
|||
z-index: 10; |
|||
} |
|||
|
|||
/* 为右侧内容添加左边距,避免被固定导航栏遮挡 */ |
|||
.main-content { |
|||
margin-left: 240px; /* 与Menu.vue中的sidebar-container宽度相同 */ |
|||
transition: margin-left 0.3s ease; |
|||
} |
|||
|
|||
/* 当菜单折叠时调整内容区域 */ |
|||
.app-container:has(.sidebar-container.collapsed) .main-content { |
|||
margin-left: 60px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,543 @@ |
|||
<template> |
|||
<div class="login-container"> |
|||
<!-- 左上角Logo和标题 --> |
|||
<div class="logo-header"> |
|||
<img src="@/assets/logo.png" alt="Logo" class="logo"> |
|||
<h1 class="login-title">面向疾病预测的知识图谱应用系统</h1> |
|||
</div> |
|||
|
|||
<!-- 左侧登录区域 --> |
|||
<div class="login-form-container"> |
|||
<div class="login-header"> |
|||
</div> |
|||
|
|||
<div class="login-form"> |
|||
<h2 class="form-title">登录</h2> |
|||
<p class="form-description">请输入您的电子邮件地址和密码以访问账户。</p> |
|||
|
|||
<form class="form" @submit.prevent="handleLogin"> |
|||
<!-- 错误信息显示 --> |
|||
<div v-if="errorMessage" class="error-message"> |
|||
{{ errorMessage }} |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="username" class="form-label">用户名</label> |
|||
<input |
|||
type="text" |
|||
id="username" |
|||
v-model="loginForm.username" |
|||
placeholder="输入您的用户名" |
|||
class="form-input" |
|||
> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<div class="password-header"> |
|||
<label for="password" class="form-label">密码</label> |
|||
<a href="#" class="forgot-password">忘记密码?</a> |
|||
</div> |
|||
<input |
|||
type="password" |
|||
id="password" |
|||
v-model="loginForm.password" |
|||
placeholder="输入您的密码" |
|||
class="form-input" |
|||
> |
|||
</div> |
|||
|
|||
<div class="form-checkbox"> |
|||
<input |
|||
type="checkbox" |
|||
id="remember" |
|||
v-model="loginForm.remember" |
|||
class="checkbox" |
|||
> |
|||
<label for="remember" class="checkbox-label">记住密码</label> |
|||
</div> |
|||
|
|||
<button |
|||
type="submit" |
|||
class="login-button" |
|||
:disabled="loading" |
|||
> |
|||
<img v-if="!loading" src="@/assets/登录.png" alt="登录" class="login-icon"> |
|||
<span v-if="!loading">登录</span> |
|||
<span v-else>登录中...</span> |
|||
</button> |
|||
</form> |
|||
|
|||
<div class="social-login"> |
|||
<p class="social-text">使用其他方式登录</p> |
|||
</div> |
|||
|
|||
<div class="register-link"> |
|||
<p>还没有账户? <a href="#" class="register"> 立即注册</a></p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 右侧知识图谱可视化区域 --> |
|||
<div class="graph-container"> |
|||
<!-- 背景装饰 --> |
|||
<div class="background-decoration"> |
|||
<div class="bg-circle circle-1"></div> |
|||
<div class="bg-circle circle-2"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { login } from '@/api/login'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// 登录表单数据 |
|||
const loginForm = ref({ |
|||
username: '', |
|||
password: '', |
|||
remember: false |
|||
}); |
|||
|
|||
// 登录状态 |
|||
const loading = ref(false); |
|||
const errorMessage = ref(''); |
|||
|
|||
// 登录处理函数 |
|||
const handleLogin = async () => { |
|||
// 表单验证 |
|||
if (!loginForm.value.username || !loginForm.value.password) { |
|||
errorMessage.value = '请输入用户名和密码'; |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
loading.value = true; |
|||
errorMessage.value = ''; |
|||
|
|||
// 调用登录API |
|||
const response = await login({ |
|||
username: loginForm.value.username, |
|||
password: loginForm.value.password, |
|||
remember: loginForm.value.remember |
|||
}); |
|||
|
|||
// 登录成功处理 |
|||
console.log('登录成功:', response); |
|||
|
|||
// 如果后端返回了token,可以存储到localStorage |
|||
if (response.token) { |
|||
localStorage.setItem('token', response.token); |
|||
} |
|||
|
|||
// 如果需要记住密码,可以存储用户名 |
|||
if (loginForm.value.remember) { |
|||
localStorage.setItem('username', loginForm.value.username); |
|||
} else { |
|||
localStorage.removeItem('username'); |
|||
} |
|||
|
|||
// 登录成功后跳转到index页面 |
|||
router.push('/index'); |
|||
|
|||
} catch (error) { |
|||
// 登录失败处理 |
|||
console.error('登录失败:', error); |
|||
errorMessage.value = error.response?.data?.message || '登录失败,请检查用户名和密码'; |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
/* 全局样式,防止页面滚动 */ |
|||
body, html { |
|||
margin: 0; |
|||
padding: 0; |
|||
overflow: hidden; |
|||
height: 100%; |
|||
} |
|||
</style> |
|||
|
|||
<style scoped> |
|||
/* 基础容器样式 */ |
|||
.login-container { |
|||
display: flex; |
|||
height: 100vh; |
|||
overflow: hidden; |
|||
flex-direction: row; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
/* 左上角Logo和标题样式 */ |
|||
.logo-header { |
|||
position: fixed; |
|||
top: 40px; |
|||
left: 25px; |
|||
display: flex; |
|||
align-items: center; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.logo { |
|||
height: 15px; |
|||
width: 15px; |
|||
margin-right: 7px; |
|||
} |
|||
|
|||
.login-title { |
|||
font-size: 17px; |
|||
font-weight: 900; |
|||
font-family: 'SimSun Bold', '宋体', serif; |
|||
color: #1f2937; |
|||
margin: 0; |
|||
white-space: nowrap; |
|||
text-shadow: 0.4px 0.4px 0 #1f2937; |
|||
} |
|||
|
|||
/* 左侧登录区域样式 */ |
|||
.login-form-container { |
|||
width: 25%; |
|||
background-color: #ffffff; |
|||
padding: 2rem; |
|||
padding-left: 40px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
position: relative; |
|||
} |
|||
|
|||
.login-header { |
|||
margin-bottom: 1.5rem; |
|||
width: 100%; |
|||
max-width: 24rem; |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.login-form { |
|||
max-width: 24rem; |
|||
width: 100%; |
|||
text-align: left; |
|||
} |
|||
|
|||
.form-title { |
|||
font-size: 18px; |
|||
font-weight: 900; |
|||
color: #333333; |
|||
margin-top: -7px; |
|||
margin-bottom: 10px; |
|||
margin-left: 13px; |
|||
text-shadow: 0.2px 0.2px 0 #1f2937; |
|||
text-align: left; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.form-description { |
|||
color: #B5B5B5; |
|||
margin-bottom: 2rem; |
|||
margin-left: 13px; |
|||
text-align: left; |
|||
font-size: 11px; |
|||
font-weight: bold; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 1rem; |
|||
width: 100%; |
|||
} |
|||
|
|||
.error-message { |
|||
color: #ef4444; |
|||
font-size: 11px; |
|||
padding: 0.5rem; |
|||
background-color: #fef2f2; |
|||
border: 1px solid #fecaca; |
|||
border-radius: 0.375rem; |
|||
margin-bottom: 0.5rem; |
|||
} |
|||
|
|||
.form-group { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
margin-bottom: 0.5rem; |
|||
} |
|||
|
|||
.form-label { |
|||
display: block; |
|||
font-size: 11px; |
|||
font-weight: 700; |
|||
color: #374151; |
|||
margin-bottom: 0.3rem; |
|||
text-align: left; |
|||
font-family:'STSong', '宋体', serif; |
|||
} |
|||
|
|||
.form-input { |
|||
width: 100%; |
|||
padding: 0.6rem 0.8rem; |
|||
border-radius: 0.5rem; |
|||
border: 2px solid #A3A3A3; |
|||
transition: all 0.2s; |
|||
font-size: 9px; |
|||
box-sizing: border-box; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
background-color: #FFFFFF; |
|||
} |
|||
|
|||
.form-input:focus { |
|||
outline: none; |
|||
border-color: #2563eb; |
|||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); |
|||
} |
|||
|
|||
.form-input::placeholder { |
|||
color: #9ca3af; |
|||
} |
|||
|
|||
.password-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
margin-bottom: 0.1rem; |
|||
} |
|||
|
|||
.forgot-password { |
|||
font-size: 9px; |
|||
color: #B5B5B5; |
|||
text-decoration: none; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.forgot-password:hover { |
|||
color: #1d4ed8; |
|||
} |
|||
|
|||
.form-checkbox { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-top: -5px; |
|||
margin-bottom: -5px; |
|||
} |
|||
|
|||
.checkbox { |
|||
height: 0.8rem; |
|||
width: 0.8rem; |
|||
color: #2563eb; |
|||
border-radius: 0.25rem; |
|||
border: 1px solid #d1d5db; |
|||
} |
|||
|
|||
.checkbox-label { |
|||
margin-left: 0.1rem; |
|||
font-size: 11px; |
|||
color: #444040ba; |
|||
font-weight: bold; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.login-button { |
|||
width: 100%; |
|||
background-color: #409EFF; |
|||
color: white; |
|||
font-weight: 500; |
|||
font-size: 11px; |
|||
padding: 0.6rem 0.8rem; |
|||
border-radius: 0; |
|||
border: none; |
|||
cursor: pointer; |
|||
transition: background-color 0.2s; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
box-sizing: border-box; |
|||
font-family: 'SimSun', '宋体', 'STSong', '华文宋体', serif; |
|||
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
.login-icon { |
|||
height: 0.9rem; |
|||
width: auto; |
|||
margin-right: 8px; |
|||
} |
|||
|
|||
.login-button:hover { |
|||
background-color: #1d4ed8; |
|||
} |
|||
|
|||
.arrow-icon { |
|||
margin-left: 0.5rem; |
|||
} |
|||
|
|||
/* 分割线样式 */ |
|||
.divider { |
|||
display: flex; |
|||
align-items: center; |
|||
margin: 1.5rem 0; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.divider::before, |
|||
.divider::after { |
|||
content: ''; |
|||
flex: 1; |
|||
height: 1px; |
|||
background-color: #e5e7eb; |
|||
} |
|||
|
|||
.divider span { |
|||
padding: 0 1rem; |
|||
font-size: 11px; |
|||
color: #B5B5B5; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.social-login { |
|||
margin-top: 2rem; |
|||
} |
|||
|
|||
.social-text { |
|||
text-align: center; |
|||
color: #B5B5B5; |
|||
margin-bottom: 1rem; |
|||
font-size: 11px; |
|||
font-weight: bold; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.social-icons { |
|||
display: flex; |
|||
justify-content: center; |
|||
gap: 1rem; |
|||
} |
|||
|
|||
.social-icon { |
|||
width: 2.5rem; |
|||
height: 2.5rem; |
|||
border-radius: 50%; |
|||
background-color: #f3f4f6; |
|||
border: none; |
|||
cursor: pointer; |
|||
transition: background-color 0.2s; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
color: #6b7280; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.social-icon:hover { |
|||
background-color: #e5e7eb; |
|||
} |
|||
|
|||
.register-link { |
|||
position: absolute; |
|||
bottom: 7px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
text-align: center; |
|||
} |
|||
|
|||
.register-link p { |
|||
color: #B5B5B5; |
|||
font-size: 11px; |
|||
font-weight: bold; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.register { |
|||
color: #B5B5B5; |
|||
font-weight: 500; |
|||
text-decoration: none; |
|||
font-weight: bold; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.register:hover { |
|||
color: #1d4ed8; |
|||
} |
|||
|
|||
/* 右侧知识图谱可视化区域样式 */ |
|||
.graph-container { |
|||
width: 75%; |
|||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%), |
|||
url('@/assets/背景.png'); |
|||
background-size: cover; |
|||
background-position: center; |
|||
background-blend-mode: overlay; |
|||
padding: 2rem; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.background-decoration { |
|||
position: absolute; |
|||
inset: 0; |
|||
opacity: 0.2; |
|||
} |
|||
|
|||
.bg-circle { |
|||
position: absolute; |
|||
border-radius: 50%; |
|||
filter: blur(3rem); |
|||
} |
|||
|
|||
.circle-1 { |
|||
top: 25%; |
|||
left: 25%; |
|||
width: 16rem; |
|||
height: 16rem; |
|||
background-color: #60a5fa; |
|||
} |
|||
|
|||
.circle-2 { |
|||
bottom: 33%; |
|||
right: 33%; |
|||
width: 20rem; |
|||
height: 20rem; |
|||
background-color: #818cf8; |
|||
} |
|||
|
|||
.graph-content { |
|||
position: relative; |
|||
z-index: 10; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: 100%; |
|||
font-family: 'SimSun', '宋体', serif; |
|||
} |
|||
|
|||
.graph-wrapper { |
|||
position: relative; |
|||
width: 100%; |
|||
max-width: 48rem; |
|||
aspect-ratio: 1 / 1; |
|||
} |
|||
|
|||
/* 响应式设计 */ |
|||
@media (max-width: 768px) { |
|||
.login-container { |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.login-form-container, |
|||
.graph-container { |
|||
width: 100%; |
|||
} |
|||
|
|||
.login-form-container { |
|||
padding: 2rem; |
|||
} |
|||
|
|||
.graph-container { |
|||
min-height: 400px; |
|||
} |
|||
} |
|||
</style> |
|||
@ -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