From 6d566667a65f9f057f66eb107502496781563ebb Mon Sep 17 00:00:00 2001 From: sd <1504629600@qq.com> Date: Mon, 22 Dec 2025 09:38:03 +0800 Subject: [PATCH] 111 --- app.py | 12 + config.py | 11 + controller/LoginController.py | 486 ++++++++++++++++++++++++++++++++ resource/avatar/1.jpg | Bin 0 -> 49413 bytes service/UserService.py | 110 ++++++++ util/mysql_utils.py | 69 +++++ vue/src/api/login.js | 40 +++ vue/src/api/profile.js | 46 +++ vue/src/components/Menu.vue | 196 +++++++++++-- vue/src/router/index.js | 33 +++ vue/src/system/Index.vue | 6 + vue/src/system/Login.vue | 74 ++++- vue/src/system/Profile.vue | 639 ++++++++++++++++++++++++++++++++++++++++++ vue/src/utils/request.js | 48 +--- vue/vue.config.js | 44 ++- web_main.py | 14 + 16 files changed, 1755 insertions(+), 73 deletions(-) create mode 100644 app.py create mode 100644 config.py create mode 100644 controller/LoginController.py create mode 100644 resource/avatar/1.jpg create mode 100644 service/UserService.py create mode 100644 util/mysql_utils.py create mode 100644 vue/src/api/profile.js create mode 100644 vue/src/router/index.js create mode 100644 vue/src/system/Profile.vue create mode 100644 web_main.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..a67e49f --- /dev/null +++ b/app.py @@ -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__) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..ac78c07 --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ +# MySQL数据库配置 +# 请根据您的实际MySQL配置修改以下参数 + +MYSQL_CONFIG = { + "host": "localhost", # MySQL主机地址 + "port": 3306, # MySQL端口 + "user": "root", # MySQL用户名 + "password": "123456", # MySQL密码 + "database": "kg", # 数据库名 + "charset": "utf8mb4" # 字符 +} \ No newline at end of file diff --git a/controller/LoginController.py b/controller/LoginController.py new file mode 100644 index 0000000..aeb87e2 --- /dev/null +++ b/controller/LoginController.py @@ -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 \ No newline at end of file diff --git a/resource/avatar/1.jpg b/resource/avatar/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2c96fbfb21da92bc5a1c3469a2061457fab7dcdd GIT binary patch literal 49413 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 literal 0 HcmV?d00001 diff --git a/service/UserService.py b/service/UserService.py new file mode 100644 index 0000000..1a7b9ac --- /dev/null +++ b/service/UserService.py @@ -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 \ No newline at end of file diff --git a/util/mysql_utils.py b/util/mysql_utils.py new file mode 100644 index 0000000..73856da --- /dev/null +++ b/util/mysql_utils.py @@ -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() \ No newline at end of file diff --git a/vue/src/api/login.js b/vue/src/api/login.js index e69de29..ef40749 100644 --- a/vue/src/api/login.js +++ b/vue/src/api/login.js @@ -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' + }); +} \ No newline at end of file diff --git a/vue/src/api/profile.js b/vue/src/api/profile.js new file mode 100644 index 0000000..c6eeb06 --- /dev/null +++ b/vue/src/api/profile.js @@ -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 + }); +} \ No newline at end of file diff --git a/vue/src/components/Menu.vue b/vue/src/components/Menu.vue index a6a144a..7ae59f4 100644 --- a/vue/src/components/Menu.vue +++ b/vue/src/components/Menu.vue @@ -1,10 +1,18 @@