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