diff --git a/README.md b/README.md deleted file mode 100644 index b2a14b9..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# UI_math_system - diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..859ee8b --- /dev/null +++ b/doc/README.md @@ -0,0 +1,153 @@ +# 数学冒险岛 - 互动式数学学习软件 + +## 📘 项目简介 + +**“数学冒险岛”** 是一款面向小学、初中和高中学生的互动式数学学习软件。 +通过 **游戏化界面设计** 与 **分学段题目设置**,让数学练习更具趣味性与挑战性。 +软件支持用户邮件注册登录、学段选择、自定义题目数量、在线答题及结果反馈等功能, +适合各学段学生进行数学能力训练与提升。 + +--- + +## ✨ 功能特点 + +- **用户系统**:支持账号注册(含邮箱验证码)、登录、密码修改及退出功能 +- **学段划分**:分为小学、初中、高中三个学段,自动匹配不同难度题目 +- **个性化练习**:可自定义题目数量(10–30 题),满足多样化练习需求 +- **友好界面**:采用渐变卡通风格设计,搭配 Emoji 元素增强趣味性 +- **即时反馈**:答题完成后即时展示得分结果,支持重新挑战 +- **数据管理**:自动保存用户试卷记录,实现持久化存储 + +--- +## 📁 项目结构 + +```bash +数学冒险岛/ +├── core/ # 核心逻辑模块 +│ ├── data_handler.py # 数据处理与文件存储 +│ ├── email_service.py # 邮件验证码发送模块 +│ ├── question_bank.py # 题目生成与难度匹配 +│ ├── user_system.py # 用户注册、登录与管理 +│ +├── ui/ # 图形界面模块(PyQt5 实现) +│ ├── login_ui.py # 登录界面 +│ ├── main_window.py # 主窗口与页面切换逻辑 +│ ├── question_ui.py # 题目答题界面 +│ ├── register_ui.py # 注册界面 +│ ├── result_ui.py # 答题结果展示界面 +│ └── user_management_ui.py # 用户信息管理界面 +│ +├── generated_papers/ # 用户完成的试卷存储目录 +├── main.py # 程序入口(运行此文件启动) +``` + +## 安装说明 + +### 环境要求 + +- Python 版本:`3.7+` +- 主要依赖: + - `PyQt5`(界面框架) + - `random`(随机题目生成) + - `smtplib`(邮箱验证码发送) + +--- + +## 安装步骤 + +```bash +# 克隆仓库(若有) +git clone <仓库地址> + +# 进入项目目录 +cd math_adventure_island + +# 安装依赖 +pip install PyQt5 +```` + +--- + + + + + +## 界面展示 + +以下表格展示了软件主要界面的功能与设计亮点: + +| 页面名称 | 界面描述 | 主要功能 | 设计特点 | +|:--------:|:--------:|:--------:|:--------:| +| **主页面** | 登录 / 注册入口页面 | 用户登录、账号注册、退出系统 | 粉绿渐变背景、卡通风格、Emoji 装饰 | +| **学段选择页** | 选择学习阶段 | 小学 / 初中 / 高中学段选择、用户信息显示 | 分学段色彩、清晰布局、渐变背景延续 | +| **题目数量页** | 设置练习题目数量 | 输入题目数量、开始练习、返回上一级 | 简洁输入框、范围提示、友好错误提示 | +| **答题页** | 进行数学题目练习 | 显示题目、选择答案、进度跟踪 | 清晰排版、选项高亮、进度指示、交互反馈 | +| **结果页** | 展示答题成绩和统计 | 显示得分、重新练习、返回主页 | 数据可视化、醒目得分、鼓励性设计 | + +## 使用指南 + +### 注册账号 + +1. 在主页面点击 **“注册账号”** +2. 输入用户名、邮箱和密码 +3. 接收邮箱验证码并填写,完成注册 + +### 登录系统 + +1. 点击主页面 **“🎮 开始冒险”** +2. 输入注册的账号与密码进行登录 + +### 开始练习 + +1. 登录后选择学段: + * 小学乐园 + * 初中城堡 + * 高中太空 +2. 输入题目数量(10–30 之间) +3. 点击 **“ 开始冒险”** 进入答题环节 + +### 查看结果 + +* 完成全部题目后系统自动展示得分结果 +* 可选择 **“重新做题”** 或返回主页继续挑战 + + + +--- + +## 开发说明 + +### 技术栈 + +* **语言:** Python +* **框架:** PyQt5 +* **架构:** 模块化设计 + QStackedWidget 页面切换 + +### 核心模块 + +| 文件名 | 主要功能 | +| ---------------- | ------------ | +| main_window.py | 主窗口逻辑与页面切换管理 | +| question_bank.py | 题目生成与试卷管理 | +| email_service.py | 注册验证码生成与邮件发送 | +| data_handler.py | 文件存储与用户数据处理 | + +--- + +## 界面设计 + +* 使用 **QSS 样式表** 实现渐变背景、圆角按钮、柔和阴影等效果 +* 通过 **QStackedWidget** 实现多页面无缝切换 +* 界面配色采用卡通风格,适合中小学生视觉体验 + +--- + + + +## 联系方式 + +如需反馈问题或提出改进建议,请联系: + **1463365450@qq.com** +或在 **头歌** 提交 Issue + + diff --git a/src/core/data_handler.py b/src/core/data_handler.py new file mode 100644 index 0000000..59d8d28 --- /dev/null +++ b/src/core/data_handler.py @@ -0,0 +1,53 @@ +import json +import os +from pathlib import Path + + +class DataHandler: + def __init__(self): + self.base_dir = Path("math_learning_data") + self.users_file = self.base_dir / "users.json" + self.ensure_data_dir() + + def ensure_data_dir(self): + """确保数据目录和文件存在,并且文件内容是有效的JSON""" + if not self.base_dir.exists(): + self.base_dir.mkdir(parents=True) + + if not self.users_file.exists(): + with open(self.users_file, "w", encoding="utf-8") as f: + json.dump({}, f, ensure_ascii=False) # 写入空对象 + else: + # 如果文件存在但为空,写入空对象 + if os.path.getsize(self.users_file) == 0: + with open(self.users_file, "w", encoding="utf-8") as f: + json.dump({}, f, ensure_ascii=False) + + def load_users(self): + """加载所有用户数据""" + with open(self.users_file, "r", encoding="utf-8") as f: + try: + return json.load(f) + except json.JSONDecodeError: + # 如果文件内容损坏,返回空字典 + return {} + + def save_users(self, users_data): + """保存用户数据""" + with open(self.users_file, "w", encoding="utf-8") as f: + json.dump(users_data, f, ensure_ascii=False, indent=2) + + def get_user(self, email): + """获取用户信息""" + users = self.load_users() + return users.get(email) + + def add_user(self, email, user_data): + """添加新用户""" + users = self.load_users() + users[email] = user_data + self.save_users(users) + + def update_user(self, email, user_data): + """更新用户信息""" + self.add_user(email, user_data) # 直接覆盖更新 \ No newline at end of file diff --git a/src/core/email_service.py b/src/core/email_service.py new file mode 100644 index 0000000..ecca850 --- /dev/null +++ b/src/core/email_service.py @@ -0,0 +1,167 @@ +import random +import time +import smtplib +import logging +from email.mime.text import MIMEText +from email.header import Header +from email.utils import formataddr +from tkinter import messagebox +from typing import Dict +from smtplib import SMTPResponseException + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class EmailService: + def __init__(self, smtp_server: str, smtp_port: int, sender_email: str, sender_password: str): + self.smtp_server = smtp_server + self.smtp_port = smtp_port + self.sender_email = sender_email + self.sender_password = sender_password + self.verification_codes: Dict[str, tuple[str, float]] = {} # 邮箱: (验证码, 过期时间) + self.code_validity = 60 # 验证码有效期1分钟 + self.last_send_time: Dict[str, float] = {} # 记录上次发送时间 + self.send_interval = 60 # 发送间隔60秒 + + def generate_code(self) -> str: + """生成6位数字注册码""" + return str(random.randint(100000, 999999)) + + def can_send_code(self, email: str) -> bool: + """检查是否可以发送注册码(防止频繁发送)""" + if email in self.last_send_time: + elapsed = time.time() - self.last_send_time[email] + return elapsed >= self.send_interval + return True + + def send_verification_code(self, email: str, username: str = "用户") -> bool: + """发送验证码到用户邮箱。 + + Args: + email: 目标邮箱地址。 + username: 用户昵称,用于邮件正文。 + + Returns: + 成功返回 True,失败返回 False。 + """ + if not self.can_send_code(email): + logger.warning("发送过于频繁,邮箱: %s", email) + return False + + code = self.generate_code() + now = time.time() + self.verification_codes[email] = (code, now + self.code_validity) + self.last_send_time[email] = now + + subject = "数学学习软件 - 注册验证码" + formatted_email = self._format_email(email) + body = self._build_email_body(username, code) + + try: + self._send_email(email, subject, body) + logger.info("注册码已发送到 %s", email) + return True + except smtplib.SMTPAuthenticationError as e: + logger.error("邮箱认证失败: %s", e) + self._show_warning("邮箱认证失败", "请检查邮箱账号和授权码是否正确") + return False + except smtplib.SMTPConnectError as e: + logger.error("无法连接到SMTP服务器: %s", e) + self._show_warning("连接失败", "无法连接到邮件服务器,请检查网络连接") + return False + except Exception as e: + logger.error("邮件发送失败: %s", e, exc_info=True) + self._show_warning("发送失败", f"邮件发送失败: {e}") + return False + + + def _format_email(self, email: str) -> str: + """隐藏部分邮箱字符,保护隐私。""" + try: + at_index = email.index('@') + if at_index >= 4: + return email[:3] + '...' + email[at_index - 4:at_index] + return email + except ValueError: + return email + + + def _build_email_body(self, username: str, code: str) -> str: + """构建HTML邮件正文。""" + return f""" + + +
+
+

数学学习软件

+

注册验证码

+
+
+

亲爱的 {username},您好!

+

您正在注册数学学习软件,请使用以下注册码完成注册:

+
+ + {code} + +
+

+ ⚠️ 注册码有效期1分钟,请尽快完成注册。 +

+
+
+

如果这不是您的操作,请忽略此邮件。

+

此为系统邮件,请勿回复

+

数学学习软件团队

+
+
+ + + """ + + + def _send_email(self, to_email: str, subject: str, body: str) -> None: + """发送邮件。""" + msg = MIMEText(body, "html", "utf-8") + msg["Subject"] = Header(subject, "utf-8") + msg["From"] = formataddr(("数学学习软件", self.sender_email)) + msg["To"] = to_email + + if self.smtp_port == 587: + server = smtplib.SMTP(self.smtp_server, self.smtp_port, timeout=10) + server.starttls() + else: + server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port, timeout=10) + + server.set_debuglevel(1) + server.login(self.sender_email, self.sender_password) + server.sendmail(self.sender_email, to_email, msg.as_string()) + server.quit() + + + def _show_warning(self, title: str, message: str) -> None: + """弹出警告框。""" + messagebox.warning(None, title, message) + + def verify_code(self, email: str, code: str) -> bool: + """验证注册码是否有效""" + if email not in self.verification_codes: + return False + + stored_code, expire_time = self.verification_codes[email] + if time.time() > expire_time: + del self.verification_codes[email] + return False + + return stored_code == code \ No newline at end of file diff --git a/src/core/question_bank.py b/src/core/question_bank.py new file mode 100644 index 0000000..2d75ede --- /dev/null +++ b/src/core/question_bank.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +"""数学学习软件 - 题目生成与试卷管理模块 + +提供小学、初中、高中三个学段的数学选择题自动生成、查重、保存功能。 +""" + +from __future__ import annotations + +import json +import math +import os +import random +from datetime import datetime +from typing import List, Tuple + +# 模块级常量 +PRIMARY_OPS = ("+", "-", "*", "/") +MIDDLE_OPS = ("+", "-", "*", "/", "^2", "sqrt") +HIGH_OPS = ("+", "-", "*", "/", "sin", "cos", "tan") +PRIMARY_RANGE = (1, 50) +MIDDLE_RANGE = (1, 100) +HIGH_ANGLE_RANGE = (0, 90) # 角度制 +MAX_OPTS = 4 +MAX_TRIES = 100 +PAPERS_DIR = "generated_papers" + + +class Question: + """单道选择题.""" + + __slots__ = ("content", "options", "answer") + + def __init__(self, content: str, options: List[str], answer: str): + """初始化. + + Args: + content: 题干(含表达式)。 + options: 四个选项文本。 + answer: 正确答案文本。 + """ + self.content = content + self.options = options + self.answer = answer + + +class QuestionBank: + """题库:按学段生成不重复的选择题,并保存为文本试卷.""" + + def __init__(self) -> None: + """初始化.""" + self.operators = { + "primary": PRIMARY_OPS, + "middle": MIDDLE_OPS, + "high": HIGH_OPS, + } + self.generated_questions: List[Question] = [] + self.papers_dir = PAPERS_DIR + os.makedirs(self.papers_dir, exist_ok=True) + + # ------------------------------------------------------------------ + # 公共接口 + # ------------------------------------------------------------------ + + def generate_question(self, level: str) -> Question: + for attempt in range(MAX_TRIES): + try: + expr, value = self._make_expression(level) + if not self._is_valid_value(value) or expr in [q.content for q in self.generated_questions]: + continue + opts, ans = self._make_options(value, level) + return Question(f"计算:{expr}", opts, ans) + except Exception as e: + print(f"[WARNING] 题目生成失败,尝试 {attempt + 1}/{MAX_TRIES},错误:{e}") + continue + + # 兜底题目 + print("[WARNING] 使用兜底题目") + expr, value = self._fallback_expression(level) + opts, ans = self._make_options(value, level) + return Question(f"计算:{expr}", opts, ans) + + def generate_paper(self, level: str, count: int, username: str = "unknown") -> List[Question]: + """生成整张试卷并落盘. + + Args: + level: 学段 + count: 题目数量 + username: 用户昵称 + + Returns: + 题目列表 + """ + self.generated_questions.clear() + questions = [self.generate_question(level) for _ in range(count)] + self._save_paper(questions, level, username) + return questions + + def calculate_score(self, answers: List[bool]) -> int: + """计算百分制得分. + + Args: + answers: 每题是否正确 + + Returns: + 0~100 + """ + if not answers: + return 0 + return round(sum(answers) / len(answers) * 100) + + # ------------------------------------------------------------------ + # 私有辅助 + # ------------------------------------------------------------------ + + def _make_expression(self, level: str) -> Tuple[str, float]: + """返回表达式字符串与数值结果.""" + if level == "primary": + return self._primary_expr() + if level == "middle": + return self._middle_expr() + return self._high_expr() + + def _primary_expr(self) -> Tuple[str, float]: + """小学表达式:整数结果.""" + nums = [random.randint(*PRIMARY_RANGE) for _ in range(random.randint(2, 3))] + ops = list(random.choices(PRIMARY_OPS, k=len(nums) - 1)) + self._ensure_int_div_sub(nums, ops) + + parts = [str(nums[0])] + for o, n in zip(ops, nums[1:]): + parts.extend([o, str(n)]) + expr = self._add_parentheses(parts) + val = self._safe_int_eval(expr) + if val is not None: + return expr, val + + a, b = sorted(random.randint(*PRIMARY_RANGE) for _ in range(2)) + op = random.choice(["+", "-", "*"]) + expr = f"{a} {op} {b}" + return expr, eval(expr) + + def _middle_expr(self) -> Tuple[str, float]: + """初中表达式:含平方或开方.""" + base = random.randint(*MIDDLE_RANGE) + if random.choice([True, False]): + inner = f"{base}^2" + val = base ** 2 + else: + inner = f"sqrt({base})" + val = math.sqrt(base) + + if random.choice([True, False]): + n2 = random.randint(*MIDDLE_RANGE) + op = random.choice(PRIMARY_OPS) + inner = f"({inner} {op} {n2})" + val = eval(f"{val} {op} {n2}") + + return inner, val + + def _high_expr(self) -> Tuple[str, float]: + """高中表达式:含三角函数.""" + angle = random.randint(*HIGH_ANGLE_RANGE) + func = random.choice(["sin", "cos", "tan"]) + expr = f"{func}({angle})" + val = getattr(math, func)(math.radians(angle)) + + if random.choice([True, False]): + n2 = random.randint(1, 90) + op = random.choice(PRIMARY_OPS) + expr = f"({expr} {op} {n2})" + val = eval(f"{val} {op} {n2}") + + return expr, val + + def _fallback_expression(self, level: str) -> Tuple[str, float]: + """兜底简单表达式.""" + if level == "primary": + a, b = sorted(random.randint(*PRIMARY_RANGE) for _ in range(2)) + expr = f"{a} + {b}" + return expr, eval(expr) + if level == "middle": + n = random.randint(1, 10) + expr = f"{n}^2" + return expr, n ** 2 + angle = random.randint(1, 89) + expr = f"sin({angle})" + return expr, math.sin(math.radians(angle)) + + # -- 工具 ----------------------------------------------------------- + + @staticmethod + def _ensure_int_div_sub(nums: List[int], ops: List[str]) -> None: + """调整 nums/ops 保证整数结果.""" + for i, op in enumerate(ops): + if op == "/": + nums[i] *= nums[i + 1] + elif op == "-" and nums[i] < nums[i + 1]: + nums[i], nums[i + 1] = nums[i + 1], nums[i] + + @staticmethod + def _add_parentheses(parts: List[str]) -> str: + """随机给表达式加括号.""" + if len(parts) >= 5 and random.choice([True, False]): + start = random.randint(0, len(parts) - 4) + if start % 2 == 0: + parts.insert(start, "(") + parts.insert(start + 4, ")") + return " ".join(parts) + + @staticmethod + def _safe_int_eval(expr: str) -> float | None: + """安全计算并返回整数结果.""" + try: + val = eval(expr) + if abs(val - round(val)) < 1e-4: + return round(val) + except Exception: + pass + return None + + @staticmethod + def _is_valid_value(val: float) -> bool: + """检查数值是否合法.""" + return not (math.isnan(val) or math.isinf(val) or abs(val) > 1e10) + + # -- 选项 & 保存 ------------------------------------------------------ + + def _make_options(self, correct: float, level: str) -> Tuple[List[str], str]: + """生成四个选项(1正确+3干扰)。""" + if level == "primary": + correct_val = int(round(correct)) + else: + correct_val = round(correct, 2) + + opts = [correct_val] + while len(opts) < 4: + distractor = self._make_distractor(opts, correct_val, level) + if distractor not in opts: + opts.append(distractor) + + random.shuffle(opts) + + # ✅ 确保答案是从 opts 中获取的,而不是原始浮点数 + ans = str(opts[opts.index(correct_val)]) + return [str(o) for o in opts], ans + + def _make_distractor(self, existing: List[float], correct: float, level: str) -> float: + """生成一个不重复的干扰项.""" + while True: + if level == "primary": + d = int(correct) + random.randint(-5, 5) + if d != correct and d > 0: + return d + else: + delta = abs(correct) * 0.3 if correct else 1 + d = round(correct + random.uniform(-delta, delta), 2) + if d not in existing: + return d + + def _save_paper(self, questions: List[Question], level: str, username: str) -> None: + """试卷落盘.""" + level_name = {"primary": "小学", "middle": "初中", "high": "高中"}.get(level, level) + timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + path = os.path.join(self.papers_dir, f"{timestamp}.txt") + with open(path, "w", encoding="utf-8") as f: + f.write(f"数学学习软件 - {level_name}数学试卷\n") + f.write(f"生成时间:{datetime.now()}\n") + f.write("=" * 50 + "\n\n") + for idx, q in enumerate(questions, 1): + f.write(f"第{idx}题:{q.content}\n选项:\n") + for i, opt in enumerate(q.options): + f.write(f" {chr(65+i)}. {opt}\n") + f.write(f"正确答案:{q.answer}\n\n") + f.write("=" * 50 + "\n") + f.write(f"共{len(questions)}题\n") + print(f"试卷已保存到:{path}") \ No newline at end of file diff --git a/src/core/user_system.py b/src/core/user_system.py new file mode 100644 index 0000000..d7074a3 --- /dev/null +++ b/src/core/user_system.py @@ -0,0 +1,163 @@ +import re +import json +import os +import sys +from typing import Dict, Optional +from .email_service import EmailService + +# 获取exe所在目录的绝对路径 +if getattr(sys, 'frozen', False): + # 如果是打包后的exe + BASE_DIR = os.path.dirname(sys.executable) +else: + # 如果是开发环境 + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +USER_DATA_FILE = os.path.join(BASE_DIR, "data", "users.json") + +class UserSystem: + def __init__(self, email_service: EmailService): + self.email_service = email_service + # 确保data目录存在 + os.makedirs(os.path.dirname(USER_DATA_FILE), exist_ok=True) + self.users: Dict[str, dict] = self.load_users() + self.current_user: Optional[str] = None + self.current_level: Optional[str] = None + + def load_users(self) -> Dict[str, dict]: + """从文件加载用户数据""" + print(f"尝试加载用户文件: {USER_DATA_FILE}") # 调试信息 + print(f"文件是否存在: {os.path.exists(USER_DATA_FILE)}") # 调试信息 + + if os.path.exists(USER_DATA_FILE): + try: + with open(USER_DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError) as e: + print(f"加载用户文件失败: {e}") # 调试信息 + return {} + else: + print("用户文件不存在,创建空用户字典") # 调试信息 + return {} + + def save_users(self): + """保存用户数据到文件""" + print(f"保存用户数据到: {USER_DATA_FILE}") # 调试信息 + # 确保目录存在 + os.makedirs(os.path.dirname(USER_DATA_FILE), exist_ok=True) + try: + with open(USER_DATA_FILE, "w", encoding="utf-8") as f: + json.dump(self.users, f, ensure_ascii=False, indent=4) + print("用户数据保存成功") # 调试信息 + except Exception as e: + print(f"保存用户数据失败: {e}") # 调试信息 + + # 其他方法保持不变... + + def is_valid_email(self, email: str) -> bool: + """验证邮箱格式""" + pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$" + return re.match(pattern, email) is not None + + def is_valid_password(self, password: str) -> bool: + """密码要求:6-10位,包含大小写字母和数字""" + if len(password) < 6 or len(password) > 10: + return False + has_upper = any(c.isupper() for c in password) + has_lower = any(c.islower() for c in password) + has_digit = any(c.isdigit() for c in password) + return has_upper and has_lower and has_digit + + def is_valid_username(self, username: str) -> bool: + """用户名要求:2-10位中文、字母或数字""" + if len(username) < 2 or len(username) > 10: + return False + pattern = r"^[\u4e00-\u9fa5a-zA-Z0-9]+$" + return re.match(pattern, username) is not None + + def is_username_exists(self, username: str) -> bool: + """检查用户名是否已存在""" + return username in self.users + + def send_verification(self, email: str, username: str = "用户") -> bool: + """发送验证码""" + # 检查邮箱是否已被其他用户使用 + for user_data in self.users.values(): + if user_data.get("email") == email: + return False + return self.email_service.send_verification_code(email, username) + + def register(self, username: str, email: str, code: str, password: str) -> bool: + """注册新用户""" + if self.is_username_exists(username): + return False + if not self.is_valid_password(password): + return False + if not self.is_valid_username(username): + return False + if not self.is_valid_email(email): + return False + # 检查邮箱是否已被使用 + for user_data in self.users.values(): + if user_data.get("email") == email: + return False + if not self.email_service.verify_code(email, code): + return False + + self.users[username] = { + "password": password, + "email": email, + "level": None + } + self.save_users() + return True + + def login(self, username: str, password: str) -> bool: + """登录验证 - 使用用户名登录""" + user = self.users.get(username) + if user and user["password"] == password: + self.current_user = username + self.current_level = user.get("level") + return True + return False + + def change_password(self, old_password: str, new_password: str) -> bool: + """修改当前登录用户密码""" + if not self.current_user: + return False + user = self.users.get(self.current_user) + if not user or user["password"] != old_password: + return False + if not self.is_valid_password(new_password): + return False + user["password"] = new_password + self.save_users() + return True + + def set_username(self, new_username: str) -> bool: + """修改当前用户名""" + if not self.current_user: + return False + if not self.is_valid_username(new_username): + return False + if self.is_username_exists(new_username): + return False + + # 更新用户名 + user_data = self.users.pop(self.current_user) + self.users[new_username] = user_data + self.current_user = new_username + self.save_users() + return True + + def set_level(self, level: str): + """设置当前用户学段""" + if self.current_user: + self.users[self.current_user]["level"] = level + self.current_level = level + self.save_users() + + def get_user_email(self, username: str) -> Optional[str]: + """获取用户的邮箱""" + user = self.users.get(username) + return user.get("email") if user else None \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..903488a --- /dev/null +++ b/src/main.py @@ -0,0 +1,26 @@ +import sys +from PyQt5.QtWidgets import QApplication +from ui.main_window import MainWindow +from core.user_system import UserSystem +from core.email_service import EmailService + +def main(): + """程序入口函数""" + app = QApplication(sys.argv) + + email_service = EmailService( + smtp_server="smtp.qq.com", + smtp_port=465, + sender_email="1463365450@qq.com", + sender_password="pbpmkecsnahubaba" + ) + + user_system = UserSystem(email_service) + main_window = MainWindow(user_system) + main_window.show() + + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/ui/login_ui.py b/src/ui/login_ui.py new file mode 100644 index 0000000..017c00b --- /dev/null +++ b/src/ui/login_ui.py @@ -0,0 +1,674 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QMessageBox, QFrame, QSizePolicy,QDialog) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont + +class LoginPage(QWidget): # 改为继承 QWidget + def __init__(self, parent=None): + super().__init__(parent) + self.parent_window = parent + self.user_system = parent.user_system if parent else None + self.setup_ui() + + def setup_ui(self): + # 移除 setWindowTitle 和 setMinimumSize,使用父窗口的尺寸 + + # 设置可爱的渐变背景 + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setSpacing(0) + + # 添加返回按钮区域 + back_button_layout = QHBoxLayout() + back_button_layout.setContentsMargins(0, 0, 0, 10) + + self.back_btn = QPushButton("←返回") + self.back_btn.setMinimumHeight(50) + self.back_btn.setMaximumWidth(120) + self.back_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #A0E7E5, stop:1 #6CD6D3); + color: white; + font: bold 11pt '微软雅黑'; + border-radius: 15px; + border: 2px solid #8ADBD9; + padding: 5px 15px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8ADBD9, stop:1 #5AC7C4); + border: 2px solid #6CD6D3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #5AC7C4, stop:1 #4BB5B2); + } + """) + self.back_btn.clicked.connect(self.go_back) + back_button_layout.addWidget(self.back_btn) + back_button_layout.addStretch() # 将按钮推到左边 + + main_layout.addLayout(back_button_layout) + + # 卡片容器 - 自适应尺寸 + card = QFrame() + card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + card.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 white, stop:1 #FFF9FA); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(card) + + # 卡片布局 + card_layout = QVBoxLayout(card) + card_layout.setContentsMargins(30, 25, 30, 25) + card_layout.setSpacing(0) + + # 顶部弹性空间 + card_layout.addStretch(1) + + # 标题区域 + title_frame = QFrame() + title_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_frame.setStyleSheet("background: transparent; border: none;") + title_layout = QVBoxLayout(title_frame) + title_layout.setSpacing(8) + + # 装饰性emoji + emoji_label = QLabel("🔐✨🎮") + emoji_label.setStyleSheet(""" + QLabel { + font: bold 18pt 'Arial'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + emoji_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_layout.addWidget(emoji_label) + + # 主标题 + title_label = QLabel("用户登录") + title_label.setStyleSheet(""" + QLabel { + font: bold 22pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_layout.addWidget(title_label) + + subtitle_label = QLabel("🌟 欢迎回到数学冒险岛 🌟") + subtitle_label.setStyleSheet(""" + QLabel { + font: 12pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + subtitle_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_layout.addWidget(subtitle_label) + + card_layout.addWidget(title_frame) + card_layout.addSpacing(30) + + # 输入区域 + input_frame = QFrame() + input_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + input_frame.setStyleSheet("background: transparent; border: none;") + input_layout = QVBoxLayout(input_frame) + input_layout.setSpacing(20) + + # 用户名区域 + username_layout = QVBoxLayout() + username_layout.setSpacing(5) + + username_label = QLabel("👤 用户名:") + username_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + username_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + username_layout.addWidget(username_label) + + self.username_input = QLineEdit() + self.username_input.setMinimumHeight(40) + self.username_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.username_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 11pt '微软雅黑'; + } + """) + self.username_input.setPlaceholderText("请输入用户名...") + username_layout.addWidget(self.username_input) + + input_layout.addLayout(username_layout) + + # 密码区域 + password_layout = QVBoxLayout() + password_layout.setSpacing(5) + + password_label = QLabel("🔒 密码:") + password_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + password_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + password_layout.addWidget(password_label) + + self.password_input = QLineEdit() + self.password_input.setMinimumHeight(40) + self.password_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.password_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 11pt '微软雅黑'; + } + """) + self.password_input.setEchoMode(QLineEdit.Password) + self.password_input.setPlaceholderText("请输入密码...") + password_layout.addWidget(self.password_input) + + input_layout.addLayout(password_layout) + + card_layout.addWidget(input_frame) + card_layout.addSpacing(25) + + # 按钮区域 + button_frame = QFrame() + button_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + button_frame.setStyleSheet("background: transparent; border: none;") + button_layout = QVBoxLayout(button_frame) + button_layout.setSpacing(12) + + # 登录按钮 + self.login_btn = QPushButton("🚀 开始冒险") + self.login_btn.setMinimumHeight(45) + self.login_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.login_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF9EBC, stop:1 #FF6B9C); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 20px; + border: 2px solid #FF85A1; + max-width: 300px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF85A1, stop:1 #FF5784); + border: 2px solid #FF6B9C; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF5784, stop:1 #FF3D6D); + } + """) + self.login_btn.clicked.connect(self.do_login) + button_layout.addWidget(self.login_btn, alignment=Qt.AlignCenter) + + # 修改密码按钮 + self.change_pwd_btn = QPushButton("🔑 修改密码") + self.change_pwd_btn.setMinimumHeight(40) + self.change_pwd_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.change_pwd_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #A0E7E5, stop:1 #6CD6D3); + color: white; + font: bold 12pt '微软雅黑'; + border-radius: 18px; + border: 2px solid #8ADBD9; + max-width: 280px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8ADBD9, stop:1 #5AC7C4); + border: 2px solid #6CD6D3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #5AC7C4, stop:1 #4BB5B2); + } + """) + self.change_pwd_btn.clicked.connect(self.show_change_pwd) + button_layout.addWidget(self.change_pwd_btn, alignment=Qt.AlignCenter) + + card_layout.addWidget(button_frame) + card_layout.addStretch(2) + + # 装饰性底部 + decoration_label = QLabel("✨🌈🎯 数学冒险岛 🌟🎮🚀") + decoration_label.setStyleSheet(""" + QLabel { + font: bold 10pt 'Arial'; + color: #FF9EBC; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + decoration_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + card_layout.addWidget(decoration_label) + + # 绑定回车键 + self.username_input.returnPressed.connect(self.do_login) + self.password_input.returnPressed.connect(self.do_login) + + # 设置焦点 + self.username_input.setFocus() + + def go_back(self): + """返回上一级页面""" + if self.parent_window: + self.parent_window.show_main_page() # 返回欢迎页面 + + def do_login(self): + """执行登录""" + username = self.username_input.text().strip() + password = self.password_input.text() + + if not username: + QMessageBox.warning(self, "⚠️ 提示", "请输入用户名") + self.username_input.setFocus() + return + + if not password: + QMessageBox.warning(self, "⚠️ 提示", "请输入密码") + self.password_input.setFocus() + return + + if self.user_system.login(username, password): + QMessageBox.information(self, "🎉 登录成功", f"欢迎 {username} 回到数学冒险岛!") + # 不再使用 self.accept(),而是通知父窗口切换页面 + if self.parent_window: + self.parent_window.show_level_page() + else: + QMessageBox.warning(self, "❌ 登录失败", "用户名或密码错误,请重试") + self.password_input.clear() + self.password_input.setFocus() + + def show_change_pwd(self): + """显示修改密码界面""" + from .login_ui import ChangePasswordUI + change_pwd_ui = ChangePasswordUI(self) + change_pwd_ui.exec_() # 修改密码还是用弹窗,因为涉及敏感操作 + + def showEvent(self, event): + """显示页面时清空输入框""" + super().showEvent(event) + self.username_input.clear() + self.password_input.clear() + self.username_input.setFocus() + + +class ChangePasswordUI(QDialog): # 这个保持为 QDialog,因为是敏感操作 + def __init__(self, parent=None): + super().__init__(parent) + self.parent_window = parent + self.user_system = parent.user_system if parent else None + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("🔑 修改密码 - 数学冒险岛 🔑") + self.setMinimumSize(450, 550) + + # 设置可爱的渐变背景 + self.setStyleSheet(""" + QDialog { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setSpacing(0) + + # 卡片容器 - 自适应尺寸 + card = QFrame() + card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + card.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 white, stop:1 #FFF9FA); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(card) + + # 卡片布局 + card_layout = QVBoxLayout(card) + card_layout.setContentsMargins(30, 25, 30, 25) + card_layout.setSpacing(0) + + # 顶部弹性空间 + card_layout.addStretch(1) + + # 标题区域 + title_frame = QFrame() + title_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_frame.setStyleSheet("background: transparent; border: none;") + title_layout = QVBoxLayout(title_frame) + title_layout.setSpacing(8) + + # 装饰性emoji + emoji_label = QLabel("🔑✨🛡️") + emoji_label.setStyleSheet(""" + QLabel { + font: bold 18pt 'Arial'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + emoji_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_layout.addWidget(emoji_label) + + # 主标题 + title_label = QLabel("修改密码") + title_label.setStyleSheet(""" + QLabel { + font: bold 22pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_layout.addWidget(title_label) + + subtitle_label = QLabel("🔒 保护你的冒险账户安全 🔒") + subtitle_label.setStyleSheet(""" + QLabel { + font: 12pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + subtitle_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_layout.addWidget(subtitle_label) + + card_layout.addWidget(title_frame) + card_layout.addSpacing(30) + + # 输入区域 + input_frame = QFrame() + input_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + input_frame.setStyleSheet("background: transparent; border: none;") + input_layout = QVBoxLayout(input_frame) + input_layout.setSpacing(20) + + # 原密码 + old_pwd_layout = QVBoxLayout() + old_pwd_layout.setSpacing(5) + + old_pwd_label = QLabel("🔓 原密码:") + old_pwd_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + old_pwd_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + old_pwd_layout.addWidget(old_pwd_label) + + self.old_pwd_input = QLineEdit() + self.old_pwd_input.setMinimumHeight(38) + self.old_pwd_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.old_pwd_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + """) + self.old_pwd_input.setEchoMode(QLineEdit.Password) + self.old_pwd_input.setPlaceholderText("请输入原密码...") + old_pwd_layout.addWidget(self.old_pwd_input) + + input_layout.addLayout(old_pwd_layout) + + # 新密码 + new_pwd_layout = QVBoxLayout() + new_pwd_layout.setSpacing(5) + + new_pwd_label = QLabel("🆕 新密码:") + new_pwd_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + new_pwd_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + new_pwd_layout.addWidget(new_pwd_label) + + self.new_pwd_input = QLineEdit() + self.new_pwd_input.setMinimumHeight(38) + self.new_pwd_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.new_pwd_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + """) + self.new_pwd_input.setEchoMode(QLineEdit.Password) + self.new_pwd_input.setPlaceholderText("请输入新密码...") + new_pwd_layout.addWidget(self.new_pwd_input) + + input_layout.addLayout(new_pwd_layout) + + # 确认新密码 + confirm_pwd_layout = QVBoxLayout() + confirm_pwd_layout.setSpacing(5) + + confirm_pwd_label = QLabel("✅ 确认新密码:") + confirm_pwd_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + confirm_pwd_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + confirm_pwd_layout.addWidget(confirm_pwd_label) + + self.confirm_pwd_input = QLineEdit() + self.confirm_pwd_input.setMinimumHeight(38) + self.confirm_pwd_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.confirm_pwd_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + """) + self.confirm_pwd_input.setEchoMode(QLineEdit.Password) + self.confirm_pwd_input.setPlaceholderText("请再次输入新密码...") + confirm_pwd_layout.addWidget(self.confirm_pwd_input) + + input_layout.addLayout(confirm_pwd_layout) + + card_layout.addWidget(input_frame) + card_layout.addSpacing(15) + + # 密码提示 + hint_label = QLabel("📝 密码要求:6-10位,必须包含大小写字母和数字") + hint_label.setStyleSheet(""" + QLabel { + font: 9pt '微软雅黑'; + color: #FF9EBC; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + hint_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + card_layout.addWidget(hint_label) + card_layout.addSpacing(25) + + # 确认按钮 + button_frame = QFrame() + button_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + button_frame.setStyleSheet("background: transparent; border: none;") + button_layout = QVBoxLayout(button_frame) + + self.confirm_btn = QPushButton("✨ 确认修改") + self.confirm_btn.setMinimumHeight(45) + self.confirm_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.confirm_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #B5EAD7, stop:1 #8CD9B3); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 20px; + border: 2px solid #A0E7E5; + max-width: 300px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8CD9B3, stop:1 #6CD6D3); + border: 2px solid #8CD9B3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #6CD6D3, stop:1 #5AC7C4); + } + """) + self.confirm_btn.clicked.connect(self.change_password) + button_layout.addWidget(self.confirm_btn, alignment=Qt.AlignCenter) + + card_layout.addWidget(button_frame) + card_layout.addStretch(2) + + # 装饰性底部 + decoration_label = QLabel("🔐🌟🛡️ 账户安全最重要 🌈✨🎯") + decoration_label.setStyleSheet(""" + QLabel { + font: bold 10pt 'Arial'; + color: #FF9EBC; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + decoration_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + card_layout.addWidget(decoration_label) + + # 绑定回车键 + self.old_pwd_input.returnPressed.connect(self.change_password) + self.new_pwd_input.returnPressed.connect(self.change_password) + self.confirm_pwd_input.returnPressed.connect(self.change_password) + + # 设置焦点 + self.old_pwd_input.setFocus() + + def change_password(self): + old_password = self.old_pwd_input.text() + new_password = self.new_pwd_input.text() + confirm_password = self.confirm_pwd_input.text() + + if not old_password or not new_password or not confirm_password: + QMessageBox.warning(self, "⚠️ 提示", "请填写完整信息") + return + + if new_password != confirm_password: + QMessageBox.warning(self, "⚠️ 提示", "两次输入的新密码不一致") + self.new_pwd_input.clear() + self.confirm_pwd_input.clear() + self.new_pwd_input.setFocus() + return + + # 检查是否已登录 + if not self.user_system.current_user: + QMessageBox.warning(self, "⚠️ 提示", "请先登录后再修改密码") + self.close() + return + + if self.user_system.change_password(old_password, new_password): + QMessageBox.information(self, "🎉 修改成功", "密码修改成功!") + self.accept() + else: + QMessageBox.warning(self, "❌ 修改失败", "原密码错误或新密码不符合要求") + self.old_pwd_input.clear() + self.new_pwd_input.clear() + self.confirm_pwd_input.clear() + self.old_pwd_input.setFocus() \ No newline at end of file diff --git a/src/ui/main_window.py b/src/ui/main_window.py new file mode 100644 index 0000000..d703ec6 --- /dev/null +++ b/src/ui/main_window.py @@ -0,0 +1,764 @@ +import sys +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QPushButton, QLineEdit, + QMessageBox, QFrame, QGridLayout, QSizePolicy, QStackedWidget) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QPalette, QColor + +# 导入各个页面(需要先修改这些类继承QWidget) +from .login_ui import LoginPage +from .register_ui import RegisterPage +from .question_ui import QuestionPage +from .result_ui import ResultPage + + +class MainWindow(QMainWindow): + def __init__(self,user_system): + super().__init__() + self.user_system = user_system + self.menu_bar = None + self.current_level = None + self.question_count = None + + + # 创建堆叠窗口 + self.stacked_widget = QStackedWidget() + + self.init_ui() + + def init_ui(self): + """初始化主窗口界面""" + self.setWindowTitle("🎮 数学冒险岛 🎮") + # self.setMinimumSize(900, 650) + # 设置固定大小(例如 900×650) + # self.setFixedSize(900, 1200) + # 获取字体高度来设置相对大小 + font_metrics = self.fontMetrics() + char_height = font_metrics.height() + + # 基于字符高度设置尺寸 + self.setFixedSize(40 * char_height, 55 * char_height) + + # 设置可爱的渐变背景色 + palette = self.palette() + palette.setColor(QPalette.Window, QColor(255, 240, 245)) + self.setPalette(palette) + + # 设置中央部件为堆叠窗口 + self.setCentralWidget(self.stacked_widget) + + # 初始化各个页面 + self.init_pages() + + # 显示主页面 + self.show_main_page() + + def init_pages(self): + """初始化所有页面""" + # 主页面 + self.main_page = self.create_main_page() + self.stacked_widget.addWidget(self.main_page) + + # 登录页面 + self.login_page = LoginPage(self) + self.stacked_widget.addWidget(self.login_page) + + # 注册页面 + self.register_page = RegisterPage(self) + self.stacked_widget.addWidget(self.register_page) + + # 学段选择页面 + self.level_page = self.create_level_page() + self.stacked_widget.addWidget(self.level_page) + + # 题目数量页面 + self.count_page = self.create_count_page() + self.stacked_widget.addWidget(self.count_page) + + def show_main_page(self): + """显示主页面""" + self.stacked_widget.setCurrentWidget(self.main_page) + self.clear_menu_bar() + + def show_login_page(self): + """显示登录页面""" + self.stacked_widget.setCurrentWidget(self.login_page) + self.clear_menu_bar() + + def show_register_page(self): + """显示注册页面""" + self.stacked_widget.setCurrentWidget(self.register_page) + self.clear_menu_bar() + + def show_level_page(self): + """显示学段选择页面""" + if not self.user_system or not self.user_system.current_user: + QMessageBox.warning(self, "提示", "请先登录") + return + self.stacked_widget.setCurrentWidget(self.level_page) + self.create_user_menu() + + def show_count_page(self): + """显示题目数量输入页面""" + if not self.user_system or not self.user_system.current_user: + QMessageBox.warning(self, "提示", "请先登录") + return + self.update_count_page(self.current_level) + self.stacked_widget.setCurrentWidget(self.count_page) + + def show_question_page(self, level: str, count: int): + try: + # ✅ 每次创建新页面前,清理旧的 QuestionPage(如果有) + for i in reversed(range(self.stacked_widget.count())): + widget = self.stacked_widget.widget(i) + if isinstance(widget, QuestionPage): + self.stacked_widget.removeWidget(widget) + widget.deleteLater() # 释放内存 + + # ✅ 再创建新页面 + question_page = QuestionPage(self, level, count) + self.stacked_widget.addWidget(question_page) + self.stacked_widget.setCurrentWidget(question_page) + + except Exception as e: + QMessageBox.critical(self, "❌ 程序错误", f"生成题目时发生错误:\n{str(e)}") + print(f"[ERROR] 题目生成失败: {e}") + try: + question_page = QuestionPage(self, level, count) + self.stacked_widget.addWidget(question_page) + self.stacked_widget.setCurrentWidget(question_page) + except Exception as e: + QMessageBox.critical(self, "❌ 程序错误", f"生成题目时发生错误:\n{str(e)}") + print(f"[ERROR] 题目生成失败: {e}") + + def show_result_page(self, score: int, level: str, count: int): + """显示结果页面""" + result_page = ResultPage(self, score, level, count) + self.stacked_widget.addWidget(result_page) + self.stacked_widget.setCurrentWidget(result_page) + + def remove_current_page(self): + """移除当前页面(用于答题和结果页面)""" + current_index = self.stacked_widget.currentIndex() + # 只移除动态添加的页面(索引大于固定页面数量) + if current_index >= 5: # 主页面、登录、注册、学段、题目数量 + current_widget = self.stacked_widget.currentWidget() + self.stacked_widget.removeWidget(current_widget) + + def create_main_page(self): + """创建主页面""" + widget = QWidget() + + # 主布局 - 使用弹性布局 + main_layout = QVBoxLayout(widget) + main_layout.setContentsMargins(30, 20, 30, 20) + main_layout.setSpacing(0) + + # 主容器框架 - 自适应尺寸 + container = QFrame() + container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + container.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(container) + + # 容器布局 - 使用弹性布局 + container_layout = QVBoxLayout(container) + container_layout.setContentsMargins(40, 30, 40, 30) + container_layout.setSpacing(0) + + # 顶部弹性空间 + container_layout.addStretch(1) + + # --- 标题区域 --- + title_frame = QFrame() + title_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_frame.setStyleSheet("background: transparent; border: none;") + title_layout = QVBoxLayout(title_frame) + title_layout.setSpacing(10) + + # 装饰性emoji + emoji_label = QLabel("✨🎓🧮🌟") + emoji_label.setStyleSheet(""" + QLabel { + font: bold 20pt 'Arial'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(emoji_label) + + # 主标题 + title_label = QLabel("数学冒险岛") + title_label.setStyleSheet(""" + QLabel { + font: bold 28pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(title_label) + + # 副标题 + subtitle_label = QLabel("🚀 开启你的数学冒险之旅! 🚀") + subtitle_label.setStyleSheet(""" + QLabel { + font: 14pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(subtitle_label) + + container_layout.addWidget(title_frame) + container_layout.addSpacing(40) + + # --- 按钮区域 --- + button_frame = QFrame() + button_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + button_frame.setStyleSheet("background: transparent; border: none;") + button_layout = QVBoxLayout(button_frame) + button_layout.setSpacing(20) + + # 登录按钮 + login_btn = QPushButton("🎮 开始冒险") + login_btn.setMinimumSize(250, 60) + login_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + login_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF9EBC, stop:1 #FF6B9C); + color: white; + font: bold 16pt '微软雅黑'; + border-radius: 30px; + border: 3px solid #FF85A1; + max-width: 300px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF85A1, stop:1 #FF5784); + border: 3px solid #FF6B9C; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF5784, stop:1 #FF3D6D); + } + """) + login_btn.clicked.connect(self.show_login_page) # 改为页面切换 + button_layout.addWidget(login_btn, alignment=Qt.AlignCenter) + + # 注册按钮 + register_btn = QPushButton("📝 注册账号") + register_btn.setMinimumSize(250, 60) + register_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + register_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #A0E7E5, stop:1 #6CD6D3); + color: white; + font: bold 16pt '微软雅黑'; + border-radius: 30px; + border: 3px solid #8ADBD9; + max-width: 300px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8ADBD9, stop:1 #5AC7C4); + border: 3px solid #6CD6D3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #5AC7C4, stop:1 #4BB5B2); + } + """) + register_btn.clicked.connect(self.show_register_page) # 改为页面切换 + button_layout.addWidget(register_btn, alignment=Qt.AlignCenter) + + container_layout.addWidget(button_frame) + container_layout.addStretch(2) + + # --- 装饰性元素 --- + decoration_frame = QFrame() + decoration_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + decoration_frame.setStyleSheet("background: transparent; border: none;") + decoration_layout = QHBoxLayout(decoration_frame) + + math_symbols = QLabel("🔢 ➕ ➖ ✖️ ➗ 📐 📊 🧮") + math_symbols.setStyleSheet(""" + QLabel { + font: bold 16pt 'Arial'; + color: #FF9EBC; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + decoration_layout.addWidget(math_symbols) + + container_layout.addWidget(decoration_frame) + container_layout.addSpacing(20) + + # --- 底部信息 --- + footer_frame = QFrame() + footer_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + footer_frame.setStyleSheet("background: transparent; border: none;") + footer_layout = QHBoxLayout(footer_frame) + + footer_label = QLabel("🌈 版权所有 © 2025 数学冒险岛 🌈") + footer_label.setStyleSheet(""" + QLabel { + font: 10pt '微软雅黑'; + color: #888888; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + footer_layout.addWidget(footer_label) + + container_layout.addWidget(footer_frame) + + return widget + + def create_level_page(self): + """创建学段选择页面""" + widget = QWidget() + + # 主布局 + main_layout = QVBoxLayout(widget) + main_layout.setContentsMargins(30, 20, 30, 20) + main_layout.setSpacing(0) + + # 主容器 + frame = QFrame() + frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + frame.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(frame) + + frame_layout = QVBoxLayout(frame) + frame_layout.setContentsMargins(40, 30, 40, 30) + frame_layout.setSpacing(0) + + # 顶部空间 + frame_layout.addStretch(1) + + # 欢迎信息 + welcome_text = f"🎉 欢迎回来! 🎉" + self.welcome_label = QLabel(welcome_text) + self.welcome_label.setStyleSheet(""" + QLabel { + font: bold 18pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + self.welcome_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + frame_layout.addWidget(self.welcome_label) + + frame_layout.addSpacing(40) + + # 学段选择标题 + select_label = QLabel("🎯 选择你的冒险地图 🎯") + select_label.setStyleSheet(""" + QLabel { + font: bold 20pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + select_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + frame_layout.addWidget(select_label) + + frame_layout.addSpacing(40) + + # 按钮区域 + btn_frame = QFrame() + btn_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + btn_frame.setStyleSheet("background: transparent; border: none;") + btn_layout = QGridLayout(btn_frame) + btn_layout.setSpacing(20) + btn_layout.setVerticalSpacing(25) + + levels = [("🏫 小学乐园", "primary"), ("🏰 初中城堡", "middle"), ("🚀 高中太空", "high")] + colors = [ + ("#FF9EBC", "#FF6B9C"), # 粉色 + ("#A0E7E5", "#6CD6D3"), # 青色 + ("#B5EAD7", "#8CD9B3") # 绿色 + ] + + for i, (text, level) in enumerate(levels): + color_pair = colors[i % len(colors)] + btn = QPushButton(text) + btn.setMinimumSize(280, 70) + btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + btn.setStyleSheet(f""" + QPushButton {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {color_pair[0]}, stop:1 {color_pair[1]}); + color: white; + font: bold 18pt '微软雅黑'; + border-radius: 35px; + border: 3px solid {color_pair[0]}; + max-width: 320px; + }} + QPushButton:hover {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 {color_pair[1]}, stop:1 {color_pair[0]}); + }} + QPushButton:pressed {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #555555, stop:1 {color_pair[1]}); + }} + """) + btn.clicked.connect(lambda checked, l=level: self.on_level_selected(l)) + btn_layout.addWidget(btn, i, 0, alignment=Qt.AlignCenter) + + frame_layout.addWidget(btn_frame) + frame_layout.addStretch(1) + + # 功能按钮区域 + func_frame = QFrame() + func_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + func_frame.setStyleSheet("background: transparent; border: none;") + func_layout = QHBoxLayout(func_frame) + func_layout.setSpacing(20) + + # 修改密码按钮 + change_pwd_btn = QPushButton("🔑 修改密码") + change_pwd_btn.setMinimumSize(180, 50) + change_pwd_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + change_pwd_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFDAC1, stop:1 #FFB347); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 25px; + border: 3px solid #FFB347; + max-width: 200px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFB347, stop:1 #FF9800); + } + """) + change_pwd_btn.clicked.connect(self.open_change_password) + func_layout.addWidget(change_pwd_btn) + + # 返回上级按钮 + back_btn = QPushButton("🔙 返回主页") + back_btn.setMinimumSize(180, 50) + back_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + back_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #C7CEEA, stop:1 #9FA8DA); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 25px; + border: 3px solid #9FA8DA; + max-width: 200px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #9FA8DA, stop:1 #7986CB); + } + """) + back_btn.clicked.connect(self.show_main_page) # 改为页面切换 + func_layout.addWidget(back_btn) + + frame_layout.addWidget(func_frame, alignment=Qt.AlignCenter) + frame_layout.addStretch(1) + + return widget + + def create_count_page(self): + """创建题目数量页面""" + widget = QWidget() + + # 主布局 + main_layout = QVBoxLayout(widget) + main_layout.setContentsMargins(30, 20, 30, 20) + main_layout.setSpacing(0) + + # 主容器 + frame = QFrame() + frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + frame.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(frame) + + frame_layout = QVBoxLayout(frame) + frame_layout.setContentsMargins(40, 30, 40, 30) + frame_layout.setSpacing(0) + + # 顶部空间 + frame_layout.addStretch(1) + + # 显示当前学段 + self.level_label = QLabel() + self.level_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.level_label.setStyleSheet(""" + QLabel { + font: bold 16pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + frame_layout.addWidget(self.level_label) + + frame_layout.addSpacing(30) + + # 输入提示 + count_label = QLabel("🎯 请输入题目数量(10-30)") + count_label.setStyleSheet(""" + QLabel { + font: bold 18pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + count_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + frame_layout.addWidget(count_label) + + # 装饰性emoji + emoji_label = QLabel("🔢 ✨ 📚 💫") + emoji_label.setStyleSheet(""" + QLabel { + font: bold 16pt 'Arial'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + emoji_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + frame_layout.addWidget(emoji_label) + + frame_layout.addSpacing(30) + + # 输入框 + self.count_input = QLineEdit() + self.count_input.setFont(QFont("微软雅黑", 14)) + self.count_input.setMinimumSize(200, 50) + self.count_input.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.count_input.setAlignment(Qt.AlignCenter) + self.count_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 3px solid #FF9EBC; + border-radius: 20px; + font: bold 14pt '微软雅黑'; + color: #FF6B9C; + padding: 5px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 3px solid #FF6B9C; + background: #FFF9FA; + } + """) + frame_layout.addWidget(self.count_input, alignment=Qt.AlignCenter) + + frame_layout.addSpacing(40) + + # 按钮区域 + btn_frame = QFrame() + btn_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + btn_frame.setStyleSheet("background: transparent; border: none;") + btn_layout = QHBoxLayout(btn_frame) + btn_layout.setSpacing(30) + + # 开始答题按钮 + start_btn = QPushButton("🚀 开始冒险") + start_btn.setMinimumSize(210, 60) + start_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + start_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF9EBC, stop:1 #FF6B9C); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 30px; + border: 3px solid #FF85A1; + max-width: 220px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF85A1, stop:1 #FF5784); + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF5784, stop:1 #FF3D6D); + } + """) + start_btn.clicked.connect(self.start_question) + btn_layout.addWidget(start_btn) + + # 返回按钮 + back_btn = QPushButton("🔙 返回选择") + back_btn.setMinimumSize(210, 60) + back_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + back_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #C7CEEA, stop:1 #9FA8DA); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 30px; + border: 3px solid #9FA8DA; + max-width: 220px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #9FA8DA, stop:1 #7986CB); + } + """) + back_btn.clicked.connect(self.show_level_page) # 改为页面切换 + btn_layout.addWidget(back_btn) + + frame_layout.addWidget(btn_frame, alignment=Qt.AlignCenter) + frame_layout.addStretch(2) + + return widget + + def update_level_page(self): + """更新学段选择页面的欢迎信息""" + if self.user_system and self.user_system.current_user: + welcome_text = f"🎉 欢迎回来,{self.user_system.current_user}! 🎉" + self.welcome_label.setText(welcome_text) + + def update_count_page(self, level: str): + """更新题目数量页面的学段信息""" + level_text = {"primary": "🏫 小学乐园", "middle": "🏰 初中城堡", "high": "🚀 高中太空"}.get(level, level) + self.level_label.setText(f"🗺️ 当前地图:{level_text}") + self.count_input.clear() + self.count_input.setFocus() + + # 修改原有方法 + def open_login(self): + """打开登录界面 - 改为页面切换""" + self.show_login_page() + + def open_register(self): + """打开注册界面 - 改为页面切换""" + self.show_register_page() + + def on_level_selected(self, level: str): + """学段选择后进入题目数量输入""" + if self.user_system: + self.user_system.set_level(level) + self.current_level = level + self.update_count_page(level) + self.show_count_page() + + def start_question(self): + """开始答题""" + try: + count = int(self.count_input.text()) + if 10 <= count <= 30: + self.question_count = count + self.show_question_page(self.current_level, count) + else: + QMessageBox.warning(self, "⚠️ 提示", "请输入10-30之间的数字哦!") + except ValueError: + QMessageBox.warning(self, "⚠️ 提示", "请输入有效的数字!") + + # 其他方法保持不变... + def create_user_menu(self): + """创建用户菜单栏""" + if self.menu_bar: + self.menu_bar.clear() + else: + self.menu_bar = self.menuBar() + self.menu_bar.setStyleSheet(""" + QMenuBar { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + color: #FF6B9C; + font: bold 12pt '微软雅黑'; + border-bottom: 2px solid #FF9EBC; + } + QMenuBar::item { + background: transparent; + padding: 5px 10px; + } + QMenuBar::item:selected { + background: #FF9EBC; + border-radius: 10px; + } + """) + + # 用户菜单 + user_menu = self.menu_bar.addMenu(f"🎮 欢迎(点这里哦),{self.user_system.current_user}") + user_menu.setStyleSheet(""" + QMenu { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + } + QMenu::item { + padding: 8px 20px; + font: 11pt '微软雅黑'; + } + QMenu::item:selected { + background: #FFE4EC; + border-radius: 10px; + } + """) + user_menu.addAction("🔑 修改密码", self.open_change_password) + user_menu.addSeparator() + user_menu.addAction("🚪 退出登录", self.logout) + + def open_change_password(self): + """打开修改密码界面""" + if self.user_system and self.user_system.current_user: + from .login_ui import ChangePasswordUI + change_password_ui = ChangePasswordUI(self) + change_password_ui.exec_() # 这个还是弹窗,因为涉及敏感操作 + else: + QMessageBox.warning(self, "提示", "请先登录") + + def logout(self): + """退出登录""" + if self.user_system: + self.user_system.current_user = None + self.user_system.current_level = None + QMessageBox.information(self, "🎮 再见", "期待下次与你一起冒险!") + self.show_main_page() + + def clear_menu_bar(self): + """清空菜单栏""" + if self.menu_bar: + self.menu_bar.clear() + self.menu_bar = None + + +if __name__ == "__main__": + app = QApplication(sys.argv) + app.setStyle('Fusion') + window = MainWindow() + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/src/ui/question_ui.py b/src/ui/question_ui.py new file mode 100644 index 0000000..bdeb524 --- /dev/null +++ b/src/ui/question_ui.py @@ -0,0 +1,455 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QPushButton, QMessageBox, QFrame, QRadioButton, + QButtonGroup, QScrollArea, QWidget, QProgressBar, QSizePolicy) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont +from .result_ui import ResultPage +from core.question_bank import QuestionBank + + +class QuestionPage(QWidget): # 改为继承 QWidget + def __init__(self, parent, level: str, count: int): + super().__init__(parent) + try: + self.parent_window = parent + self.level = level + self.count = count + self.current_idx = 0 + self.answers = [None] * count + + self.question_bank = QuestionBank() + self.paper = self.question_bank.generate_paper(level, count) + + self.setup_ui() + self.show_question() + except Exception as e: + print(f"[ERROR] QuestionPage 初始化失败: {e}") + raise e # 重新抛出,供上层捕获 + + def setup_ui(self): + """初始化界面""" + # 移除 setWindowTitle 和 setMinimumSize,使用父窗口的尺寸 + + # 设置可爱的渐变背景 + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(25, 20, 25, 20) + main_layout.setSpacing(0) + + # 顶部信息区域 + top_frame = QFrame() + top_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + top_frame.setStyleSheet(""" + QFrame { + background: transparent; + border: none; + } + """) + top_layout = QVBoxLayout(top_frame) + top_layout.setSpacing(15) + + # 标题 + title_label = QLabel("🧮 数学冒险岛 - 答题挑战 🧮") + title_label.setStyleSheet(""" + QLabel { + font: bold 24pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + top_layout.addWidget(title_label) + + # 进度显示 + self.progress_label = QLabel() + self.progress_label.setStyleSheet(""" + QLabel { + font: bold 16pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + self.progress_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + top_layout.addWidget(self.progress_label) + + # 进度条 + self.progress_bar = QProgressBar() + self.progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.progress_bar.setFixedHeight(25) + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 3px solid #FF9EBC; + border-radius: 12px; + background-color: white; + text-align: center; + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + } + QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #FF9EBC, stop:0.5 #FF6B9C, stop:1 #FF3D6D); + border-radius: 8px; + } + """) + top_layout.addWidget(self.progress_bar) + + main_layout.addWidget(top_frame) + main_layout.addSpacing(20) + + # 题目内容框架(可滚动) + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setStyleSheet(""" + QScrollArea { + border: none; + background: transparent; + } + QScrollBar:vertical { + background: white; + width: 15px; + margin: 0px; + } + QScrollBar::handle:vertical { + background: #FF9EBC; + border-radius: 7px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background: #FF6B9C; + } + """) + main_layout.addWidget(scroll_area) + + # 滚动区域的内容部件 + scroll_content = QWidget() + scroll_area.setWidget(scroll_content) + + content_layout = QVBoxLayout(scroll_content) + content_layout.setContentsMargins(0, 0, 0, 0) + + # 题目卡片 + question_card = QFrame() + question_card.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 white, stop:1 #FFF9FA); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + question_layout = QVBoxLayout(question_card) + question_layout.setContentsMargins(30, 25, 30, 25) + question_layout.setSpacing(20) + + # 题干 + self.question_label = QLabel() + self.question_label.setStyleSheet(""" + QLabel { + font: bold 18pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + line-height: 1.5; + } + """) + self.question_label.setWordWrap(True) + self.question_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) + question_layout.addWidget(self.question_label) + + # 选项框架 + self.options_frame = QFrame() + self.options_frame.setStyleSheet(""" + QFrame { + background: transparent; + border: none; + } + """) + self.options_layout = QVBoxLayout(self.options_frame) + self.options_layout.setSpacing(12) + question_layout.addWidget(self.options_frame) + + content_layout.addWidget(question_card) + + main_layout.addSpacing(20) + + # 底部按钮框架 + button_frame = QFrame() + button_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + button_frame.setStyleSheet("background: transparent; border: none;") + button_layout = QHBoxLayout(button_frame) + button_layout.setSpacing(20) + + # 上一题按钮 + self.prev_btn = QPushButton("⬅️ 上一题") + self.prev_btn.setMinimumSize(120, 50) + self.prev_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.prev_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #C7CEEA, stop:1 #9FA8DA); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 25px; + border: 3px solid #9FA8DA; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #9FA8DA, stop:1 #7986CB); + border: 3px solid #7986CB; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #7986CB, stop:1 #5C6BC0); + } + QPushButton:disabled { + background: #E0E0E0; + color: #9E9E9E; + border: 3px solid #CCCCCC; + } + """) + self.prev_btn.clicked.connect(self.prev_question) + button_layout.addWidget(self.prev_btn) + + button_layout.addStretch(1) + + # 下一题按钮 + self.next_btn = QPushButton("下一题 ➡️") + self.next_btn.setMinimumSize(120, 50) + self.next_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.next_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #A0E7E5, stop:1 #6CD6D3); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 25px; + border: 3px solid #8ADBD9; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8ADBD9, stop:1 #5AC7C4); + border: 3px solid #6CD6D3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #5AC7C4, stop:1 #4BB5B2); + } + """) + self.next_btn.clicked.connect(self.next_question) + button_layout.addWidget(self.next_btn) + + button_layout.addStretch(1) + + # 提交按钮 + self.submit_btn = QPushButton("🎯 提交试卷") + self.submit_btn.setMinimumSize(140, 50) + self.submit_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.submit_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF9EBC, stop:1 #FF6B9C); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 25px; + border: 3px solid #FF85A1; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF85A1, stop:1 #FF5784); + border: 3px solid #FF6B9C; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF5784, stop:1 #FF3D6D); + } + """) + self.submit_btn.clicked.connect(self.submit_paper) + button_layout.addWidget(self.submit_btn) + + main_layout.addWidget(button_frame) + + # 选项按钮组 + self.option_group = QButtonGroup(self) + self.option_group.buttonClicked.connect(self.on_option_selected) + + def show_question(self): + """显示当前题目""" + if self.current_idx >= len(self.paper): + return + + question = self.paper[self.current_idx] + + # 更新进度 + progress_text = f"📊 第 {self.current_idx + 1} 题 / 共 {self.count} 题" + self.progress_label.setText(progress_text) + + # 更新进度条 + progress_percent = int((self.current_idx + 1) / self.count * 100) + self.progress_bar.setValue(progress_percent) + self.progress_bar.setFormat(f"🚀 进度: {progress_percent}%") + + # 显示题干 + self.question_label.setText(question.content) + + # 清空选项框架 + for i in reversed(range(self.options_layout.count())): + widget = self.options_layout.itemAt(i).widget() + if widget: + widget.deleteLater() + + # 移除所有按钮 + self.option_group = QButtonGroup(self) + self.option_group.buttonClicked.connect(self.on_option_selected) + + # 创建选项按钮 + option_labels = ["A", "B", "C", "D"] + option_emojis = ["🔸", "🔹", "🔸", "🔹"] + + for i, option_text in enumerate(question.options): + # 选项卡片 + option_card = QFrame() + option_card.setStyleSheet(""" + QFrame { + background: white; + border: 2px solid #FFD1DC; + border-radius: 15px; + } + QFrame:hover { + background: #FFF9FA; + border: 2px solid #FF9EBC; + } + """) + option_card.setFixedHeight(60) + option_layout = QHBoxLayout(option_card) + option_layout.setContentsMargins(20, 10, 20, 10) + + # 单选按钮 + radio_btn = QRadioButton(f"{option_emojis[i]} {option_labels[i]}. {option_text}") + radio_btn.setStyleSheet(""" + QRadioButton { + font: 16pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + spacing: 15px; + } + QRadioButton::indicator { + width: 24px; + height: 24px; + } + QRadioButton::indicator:unchecked { + border: 3px solid #FF9EBC; + border-radius: 12px; + background-color: white; + } + QRadioButton::indicator:checked { + border: 3px solid #FF6B9C; + border-radius: 12px; + background-color: #FF6B9C; + } + QRadioButton:hover { + color: #FF6B9C; + } + """) + + self.option_group.addButton(radio_btn, i) + option_layout.addWidget(radio_btn) + option_layout.addStretch(1) + + self.options_layout.addWidget(option_card) + + # 恢复已选答案 + if self.answers[self.current_idx] is not None: + button = self.option_group.button(self.answers[self.current_idx]) + if button: + button.setChecked(True) + + # 更新按钮状态 + self.update_button_states() + + def on_option_selected(self, button): + """选项被选择时的处理""" + self.save_current_answer() + + def update_button_states(self): + """更新按钮状态""" + # 上一题按钮 + self.prev_btn.setEnabled(self.current_idx > 0) + + # 下一题按钮 + if self.current_idx == self.count - 1: + self.next_btn.setText("最后一题 🏁") + else: + self.next_btn.setText("下一题 ➡️") + + def save_current_answer(self): + """保存当前题目的答案""" + selected_button = self.option_group.checkedButton() + if selected_button: + self.answers[self.current_idx] = self.option_group.id(selected_button) + + def prev_question(self): + """上一题""" + self.save_current_answer() + if self.current_idx > 0: + self.current_idx -= 1 + self.show_question() + + def next_question(self): + """下一题""" + self.save_current_answer() + if self.current_idx < self.count - 1: + self.current_idx += 1 + self.show_question() + else: + # 如果是最后一题,提示提交 + QMessageBox.information(self, "🎉 完成", "太棒了!你已经完成了所有题目,请提交试卷查看成绩!") + + def submit_paper(self): + """提交试卷""" + self.save_current_answer() + + # 检查是否所有题目都已作答 + unanswered = [i+1 for i, answer in enumerate(self.answers) if answer is None] + if unanswered: + reply = QMessageBox.question( + self, + "🤔 确认提交", + f"还有 {len(unanswered)} 题未作答(题号:{unanswered}),确定要提交吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply != QMessageBox.Yes: + return + + # 计算得分 + correct_answers = [] + for i, question in enumerate(self.paper): + selected_index = self.answers[i] + if selected_index is not None: + selected_answer = question.options[selected_index] + is_correct = (selected_answer == question.answer) + correct_answers.append(is_correct) + else: + correct_answers.append(False) + + score = self.question_bank.calculate_score(correct_answers) + + # 显示结果页面 + if self.parent_window: + self.parent_window.show_result_page(score, self.level, self.count) + + def showEvent(self, event): + """显示页面时重置状态""" + super().showEvent(event) + # 重置答题状态 + self.current_idx = 0 + self.answers = [None] * self.count + self.show_question() \ No newline at end of file diff --git a/src/ui/register_ui.py b/src/ui/register_ui.py new file mode 100644 index 0000000..48ddf4f --- /dev/null +++ b/src/ui/register_ui.py @@ -0,0 +1,589 @@ +# 标准库 +import sys + +# 第三方库 +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QMessageBox, QFrame, QGridLayout, QSizePolicy +) +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QFont + +class RegisterPage(QWidget): # 改为继承 QWidget + def __init__(self, parent=None): + super().__init__(parent) + self.parent_window = parent + self.user_system = parent.user_system if parent else None + self.countdown_active = False + self.countdown_seconds = 60 + self.countdown_timer = QTimer() + self.countdown_timer.timeout.connect(self.update_countdown) + + self.setup_ui() + + def setup_ui(self): + """初始化注册界面""" + # 移除 setWindowTitle 和 setMinimumSize,使用父窗口的尺寸 + + # 设置可爱的渐变背景 + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(25, 20, 25, 20) + main_layout.setSpacing(0) + + # 卡片容器 + card = QFrame() + card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + card.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 white, stop:1 #FFF9FA); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(card) + + # 卡片布局 + card_layout = QVBoxLayout(card) + card_layout.setContentsMargins(35, 30, 35, 30) + card_layout.setSpacing(0) + + # 顶部按钮栏 + top_button_layout = QHBoxLayout() + top_button_layout.setAlignment(Qt.AlignLeft) + + # 返回主页按钮 + self.back_button = QPushButton("← 返回主页") + self.back_button.setMinimumSize(100, 35) + self.back_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.back_button.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #B5EAD7, stop:1 #8CD9C7); + color: white; + font: bold 11pt '微软雅黑'; + border-radius: 15px; + border: 2px solid #A0E7E5; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #A0E7E5, stop:1 #7BCFBD); + border: 2px solid #8CD9C7; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #7BCFBD, stop:1 #6AC0AE); + } + """) + self.back_button.clicked.connect(self.go_back_to_main) + top_button_layout.addWidget(self.back_button) + top_button_layout.addStretch(1) # 将按钮推到左侧 + + card_layout.addLayout(top_button_layout) + card_layout.addSpacing(10) + + # 顶部弹性空间 + card_layout.addStretch(1) + + # 标题区域 + title_frame = QFrame() + title_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_frame.setStyleSheet("background: transparent; border: none;") + title_layout = QVBoxLayout(title_frame) + title_layout.setSpacing(10) + + # 装饰性emoji + emoji_label = QLabel("🎉✨📝") + emoji_label.setStyleSheet(""" + QLabel { + font: bold 20pt 'Arial'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(emoji_label) + + # 主标题 + title_label = QLabel("用户注册") + title_label.setStyleSheet(""" + QLabel { + font: bold 24pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(title_label) + + subtitle_label = QLabel("🚀 加入数学冒险岛大家庭 🚀") + subtitle_label.setStyleSheet(""" + QLabel { + font: 12pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(subtitle_label) + + card_layout.addWidget(title_frame) + card_layout.addSpacing(30) + + # 表单区域 - 使用垂直布局包装网格布局 + form_container = QFrame() + form_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + form_container.setStyleSheet("background: transparent; border: none;") + form_container_layout = QVBoxLayout(form_container) + + # 网格布局 + form_layout = QGridLayout() + form_layout.setVerticalSpacing(15) # 减少垂直间距 + form_layout.setHorizontalSpacing(15) + form_layout.setColumnStretch(1, 1) + + # 用户名输入 + username_label = QLabel("👤 用户名:") + username_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + form_layout.addWidget(username_label, 0, 0, Qt.AlignLeft) + + self.username_input = QLineEdit() + self.username_input.setMinimumHeight(40) + self.username_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.username_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 11pt '微软雅黑'; + } + """) + self.username_input.setPlaceholderText("请输入用户名...") + form_layout.addWidget(self.username_input, 0, 1) + + # 用户名提示 + username_hint = QLabel("📝 用户名要求:2-10位中文、字母或数字") + username_hint.setStyleSheet(""" + QLabel { + font: 9pt '微软雅黑'; + color: #FF9EBC; + background: transparent; + } + """) + form_layout.addWidget(username_hint, 1, 1, Qt.AlignLeft) + + # 添加间距行 + form_layout.addWidget(QLabel(""), 2, 0, 1, 2) # 空行作为间距 + + # 邮箱输入 + email_label = QLabel("📧 邮箱:") + email_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + form_layout.addWidget(email_label, 3, 0, Qt.AlignLeft) + + self.email_input = QLineEdit() + self.email_input.setMinimumHeight(40) + self.email_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.email_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 11pt '微软雅黑'; + } + """) + self.email_input.setPlaceholderText("请输入邮箱...") + form_layout.addWidget(self.email_input, 3, 1) + + # 添加间距行 + form_layout.addWidget(QLabel(""), 4, 0, 1, 2) # 空行作为间距 + + # 注册码 + code_label = QLabel("🔐 注册码:") + code_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + form_layout.addWidget(code_label, 5, 0, Qt.AlignLeft) + + code_input_layout = QHBoxLayout() + code_input_layout.setSpacing(10) + + self.code_input = QLineEdit() + self.code_input.setMinimumHeight(55) + self.code_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.code_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 11pt '微软雅黑'; + } + """) + self.code_input.setPlaceholderText("请输入注册码...") + code_input_layout.addWidget(self.code_input) + + self.code_btn = QPushButton("获取注册码") + self.code_btn.setMinimumSize(120, 40) + self.code_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.code_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #A0E7E5, stop:1 #6CD6D3); + color: white; + font: bold 11pt '微软雅黑'; + border-radius: 15px; + border: 2px solid #8ADBD9; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8ADBD9, stop:1 #5AC7C4); + border: 2px solid #6CD6D3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #5AC7C4, stop:1 #4BB5B2); + } + QPushButton:disabled { + background: #E0E0E0; + color: #9E9E9E; + border: 2px solid #CCCCCC; + } + """) + self.code_btn.clicked.connect(self.send_code) + code_input_layout.addWidget(self.code_btn) + + form_layout.addLayout(code_input_layout, 5, 1) + + # 添加间距行 + form_layout.addWidget(QLabel(""), 6, 0, 1, 2) # 空行作为间距 + + # 密码 + pwd_label = QLabel("🔒 密码:") + pwd_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + form_layout.addWidget(pwd_label, 7, 0, Qt.AlignLeft) + + self.pwd_input = QLineEdit() + self.pwd_input.setMinimumHeight(40) + self.pwd_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.pwd_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 11pt '微软雅黑'; + } + """) + self.pwd_input.setEchoMode(QLineEdit.Password) + self.pwd_input.setPlaceholderText("请输入密码...") + form_layout.addWidget(self.pwd_input, 7, 1) + + # 密码提示 + pwd_hint = QLabel("📝 密码要求:6-10位,必须包含大小写字母和数字") + pwd_hint.setStyleSheet(""" + QLabel { + font: 9pt '微软雅黑'; + color: #FF9EBC; + background: transparent; + } + """) + form_layout.addWidget(pwd_hint, 8, 1, Qt.AlignLeft) + + # 添加间距行 + form_layout.addWidget(QLabel(""), 9, 0, 1, 2) # 空行作为间距 + + # 确认密码 + confirm_pwd_label = QLabel("✅ 确认密码:") + confirm_pwd_label.setStyleSheet(""" + QLabel { + font: bold 12pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + form_layout.addWidget(confirm_pwd_label, 10, 0, Qt.AlignLeft) + + self.confirm_pwd_input = QLineEdit() + self.confirm_pwd_input.setMinimumHeight(40) + self.confirm_pwd_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.confirm_pwd_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 15px; + font: 12pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 15px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 11pt '微软雅黑'; + } + """) + self.confirm_pwd_input.setEchoMode(QLineEdit.Password) + self.confirm_pwd_input.setPlaceholderText("请再次输入密码...") + form_layout.addWidget(self.confirm_pwd_input, 10, 1) + + form_container_layout.addLayout(form_layout) + form_container_layout.addStretch(1) + + card_layout.addWidget(form_container) + card_layout.addStretch(1) + + # 按钮布局 + button_layout = QHBoxLayout() + button_layout.setSpacing(20) + button_layout.setAlignment(Qt.AlignCenter) + + # 返回主页按钮(底部) + back_button_bottom = QPushButton("🔙 返回主页") + back_button_bottom.setMinimumSize(120, 45) + back_button_bottom.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + back_button_bottom.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #C9C9C9, stop:1 #A8A8A8); + color: white; + font: bold 12pt '微软雅黑'; + border-radius: 20px; + border: 2px solid #B8B8B8; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #B8B8B8, stop:1 #989898); + border: 2px solid #A8A8A8; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #989898, stop:1 #888888); + } + """) + back_button_bottom.clicked.connect(self.go_back_to_main) + button_layout.addWidget(back_button_bottom) + + # 注册按钮 + register_btn = QPushButton("🎉 立即注册") + register_btn.setMinimumSize(150, 50) + register_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + register_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF9EBC, stop:1 #FF6B9C); + color: white; + font: bold 14pt '微软雅黑'; + border-radius: 25px; + border: 3px solid #FF85A1; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF85A1, stop:1 #FF5784); + border: 3px solid #FF6B9C; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF5784, stop:1 #FF3D6D); + } + """) + register_btn.clicked.connect(self.do_register) + button_layout.addWidget(register_btn) + + card_layout.addLayout(button_layout) + card_layout.addSpacing(10) + + # 装饰性底部 + decoration_label = QLabel("✨🌈🎮 开启你的数学冒险之旅 🌟📚🚀") + decoration_label.setStyleSheet(""" + QLabel { + font: bold 10pt 'Arial'; + color: #FF9EBC; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + card_layout.addWidget(decoration_label) + + # 设置焦点 + self.username_input.setFocus() + + def go_back_to_main(self): + """返回主页""" + if self.parent_window: + self.parent_window.show_main_page() + + def send_code(self): + """发送注册码""" + if self.countdown_active: + return + + email = self.email_input.text().strip() + username = self.username_input.text().strip() or "用户" + + if not email: + QMessageBox.warning(self, "⚠️ 提示", "请输入邮箱地址") + self.email_input.setFocus() + return + + if not self.user_system.is_valid_email(email): + QMessageBox.warning(self, "⚠️ 提示", "请输入有效的邮箱地址") + self.email_input.setFocus() + return + + if self.user_system.send_verification(email, username): + QMessageBox.information(self, "📧 发送成功", "注册码已发送到您的邮箱,请查收!") + self.start_countdown() + else: + QMessageBox.warning(self, "❌ 发送失败", "邮箱已被使用或发送失败,请重试") + + def start_countdown(self): + """注册码按钮倒计时""" + print("开始倒计时") # 调试信息 + self.countdown_active = True + self.code_btn.setEnabled(False) + self.countdown_seconds = 60 + self.countdown_timer.start(1000) + self.update_countdown() + + def update_countdown(self): + """更新倒计时显示""" + print(f"倒计时更新: {self.countdown_seconds}秒") # 调试信息 + + if self.countdown_seconds > 0: + self.code_btn.setText(f"重新发送({self.countdown_seconds}s)") + self.countdown_seconds -= 1 + # 强制更新UI + self.code_btn.repaint() + else: + self.countdown_timer.stop() + self.code_btn.setText("获取注册码") + self.code_btn.setEnabled(True) + self.countdown_active = False + print("倒计时结束") # 调试信息 + + def do_register(self): + """执行注册""" + username = self.username_input.text().strip() + email = self.email_input.text().strip() + code = self.code_input.text().strip() + pwd = self.pwd_input.text() + confirm_pwd = self.confirm_pwd_input.text() + + if not username or not email or not code or not pwd or not confirm_pwd: + QMessageBox.warning(self, "⚠️ 提示", "请填写完整信息") + return + + if pwd != confirm_pwd: + QMessageBox.warning(self, "⚠️ 提示", "两次输入的密码不一致") + self.pwd_input.clear() + self.confirm_pwd_input.clear() + self.pwd_input.setFocus() + return + + if self.user_system.register(username, email, code, pwd): + QMessageBox.information(self, "🎉 注册成功", + "注册成功!现在可以使用用户名登录,开始你的数学冒险之旅!") + # 不再使用 self.accept(),而是通知父窗口切换页面 + if self.parent_window: + self.parent_window.show_main_page() # 注册成功后回到主页面 + else: + QMessageBox.warning(self, "❌ 注册失败", + "注册失败:注册码错误、用户名已存在或信息不符合要求") + + def showEvent(self, event): + """显示页面时清空输入框""" + super().showEvent(event) + self.username_input.clear() + self.email_input.clear() + self.code_input.clear() + self.pwd_input.clear() + self.confirm_pwd_input.clear() + self.username_input.setFocus() + + # 重置倒计时 + if self.countdown_timer.isActive(): + self.countdown_timer.stop() + self.code_btn.setText("获取注册码") + self.code_btn.setEnabled(True) + self.countdown_active = False + + def closeEvent(self, event): + """关闭事件处理""" + if self.countdown_timer.isActive(): + self.countdown_timer.stop() + event.accept() \ No newline at end of file diff --git a/src/ui/result_ui.py b/src/ui/result_ui.py new file mode 100644 index 0000000..7c942ef --- /dev/null +++ b/src/ui/result_ui.py @@ -0,0 +1,296 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QPushButton, QFrame, QMessageBox) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont + +class ResultPage(QWidget): # 改为继承 QWidget + def __init__(self, parent, score: int, level: str, count: int): + super().__init__(parent) + self.parent_window = parent + self.level = level + self.count = count + self.score = score + self.setup_ui() + + def setup_ui(self): + """初始化界面""" + # 移除 setWindowTitle 和 setMinimumSize,使用父窗口的尺寸 + + # 设置可爱的渐变背景 + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(30, 30, 30, 30) + main_layout.setSpacing(0) + + # 卡片容器 + card = QFrame() + card.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 white, stop:1 #FFF9FA); + border-radius: 25px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(card) + + # 卡片布局 + card_layout = QVBoxLayout(card) + card_layout.setContentsMargins(40, 40, 40, 40) + card_layout.setSpacing(0) + + # 顶部弹性空间 + card_layout.addStretch(1) + + # 标题区域 + title_frame = QFrame() + title_frame.setStyleSheet("background: transparent; border: none;") + title_layout = QVBoxLayout(title_frame) + title_layout.setSpacing(15) + + # 装饰性emoji + emoji_label = QLabel("🎊✨🏆") + emoji_label.setStyleSheet(""" + QLabel { + font: bold 28pt 'Arial'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(emoji_label) + + # 主标题 + title_label = QLabel("冒险完成!") + title_label.setStyleSheet(""" + QLabel { + font: bold 32pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(title_label) + + # 副标题 + level_text = {"primary": "小学乐园", "middle": "初中城堡", "high": "高中太空"}.get(self.level, self.level) + subtitle_label = QLabel(f"🎯 你在 {level_text} 完成了 {self.count} 道题目 🎯") + subtitle_label.setStyleSheet(""" + QLabel { + font: 16pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(subtitle_label) + + card_layout.addWidget(title_frame) + card_layout.addSpacing(40) + + # 得分区域 + score_frame = QFrame() + score_frame.setStyleSheet("background: transparent; border: none;") + score_layout = QVBoxLayout(score_frame) + score_layout.setSpacing(10) + + # 得分标题 + score_title_label = QLabel("你的冒险得分") + score_title_label.setStyleSheet(""" + QLabel { + font: bold 18pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + score_layout.addWidget(score_title_label) + + # 得分显示 - 根据分数显示不同颜色和评价 + if self.score >= 90: + score_color = "#FF6B9C" + score_emoji = "🎉" + evaluation = "太棒了!你是数学小天才!" + elif self.score >= 80: + score_color = "#FF9EBC" + score_emoji = "🌟" + evaluation = "优秀!继续加油!" + elif self.score >= 70: + score_color = "#A0E7E5" + score_emoji = "👍" + evaluation = "良好!表现不错!" + elif self.score >= 60: + score_color = "#B5EAD7" + score_emoji = "💪" + evaluation = "及格!还有进步空间!" + else: + score_color = "#C7CEEA" + score_emoji = "📚" + evaluation = "加油!多练习会更好!" + + score_display_frame = QFrame() + score_display_frame.setStyleSheet(f""" + QFrame {{ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 white, stop:1 #FFF9FA); + border-radius: 20px; + border: 3px solid {score_color}; + }} + """) + score_display_layout = QVBoxLayout(score_display_frame) + score_display_layout.setContentsMargins(30, 20, 30, 20) + + # 得分数字 + score_value_label = QLabel(f"{score_emoji} {self.score}分 {score_emoji}") + score_value_label.setStyleSheet(f""" + QLabel {{ + font: bold 48pt '微软雅黑'; + color: {score_color}; + background: transparent; + qproperty-alignment: AlignCenter; + }} + """) + score_display_layout.addWidget(score_value_label) + + # 评价 + evaluation_label = QLabel(evaluation) + evaluation_label.setStyleSheet(f""" + QLabel {{ + font: bold 16pt '微软雅黑'; + color: {score_color}; + background: transparent; + qproperty-alignment: AlignCenter; + }} + """) + score_display_layout.addWidget(evaluation_label) + + score_layout.addWidget(score_display_frame) + card_layout.addWidget(score_frame) + card_layout.addSpacing(40) + + # 按钮框架 + button_frame = QFrame() + button_frame.setStyleSheet("background: transparent; border: none;") + button_layout = QHBoxLayout(button_frame) + button_layout.setSpacing(20) + + # 按钮样式 + button_style = """ + QPushButton { + font: bold 14pt '微软雅黑'; + border-radius: 25px; + min-width: 140px; + min-height: 50px; + border: 3px solid; + } + """ + + # 再做一套按钮 + again_btn = QPushButton("🔄 再来一次") + again_btn.setStyleSheet(button_style + """ + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #A0E7E5, stop:1 #6CD6D3); + color: white; + border-color: #8ADBD9; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8ADBD9, stop:1 #5AC7C4); + border-color: #6CD6D3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #5AC7C4, stop:1 #4BB5B2); + } + """) + again_btn.clicked.connect(self.do_again) + button_layout.addWidget(again_btn) + + # 返回学段按钮 + level_btn = QPushButton("🗺️ 选择地图") + level_btn.setStyleSheet(button_style + """ + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFDAC1, stop:1 #FFB347); + color: white; + border-color: #FFB347; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFB347, stop:1 #FF9800); + border-color: #FF9800; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FF9800, stop:1 #F57C00); + } + """) + level_btn.clicked.connect(self.exit_to_level) + button_layout.addWidget(level_btn) + + # 退出程序按钮 + quit_btn = QPushButton("🚪 结束冒险") + quit_btn.setStyleSheet(button_style + """ + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #C7CEEA, stop:1 #9FA8DA); + color: white; + border-color: #9FA8DA; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #9FA8DA, stop:1 #7986CB); + border-color: #7986CB; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #7986CB, stop:1 #5C6BC0); + } + """) + quit_btn.clicked.connect(self.quit_app) + button_layout.addWidget(quit_btn) + + card_layout.addWidget(button_frame) + card_layout.addStretch(1) + + # 装饰性底部 + decoration_label = QLabel("✨🌈🎮 数学冒险岛 - 学数学,真有趣! 🌟📚🚀") + decoration_label.setStyleSheet(""" + QLabel { + font: bold 12pt 'Arial'; + color: #FF9EBC; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + card_layout.addWidget(decoration_label) + + def quit_app(self): + """退出程序""" + reply = QMessageBox.question(self, "🎮 结束冒险", + "确定要结束数学冒险吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + if reply == QMessageBox.Yes: + if self.parent_window: + self.parent_window.close() + + def do_again(self): + """重新做题""" + if self.parent_window: + self.parent_window.remove_current_page() # 移除结果页面 + self.parent_window.show_count_page() # 回到题目数量页面 + + def exit_to_level(self): + """返回学段选择""" + if self.parent_window: + self.parent_window.remove_current_page() # 移除结果页面 + self.parent_window.show_level_page() # 回到学段选择页面 \ No newline at end of file diff --git a/src/ui/user_management_ui.py b/src/ui/user_management_ui.py new file mode 100644 index 0000000..46ff5f7 --- /dev/null +++ b/src/ui/user_management_ui.py @@ -0,0 +1,297 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QMessageBox, QFrame, QSizePolicy) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont + +class ChangeUsernamePage(QWidget): # 改为继承 QWidget + def __init__(self, parent=None): + super().__init__(parent) + self.parent_window = parent + self.user_system = parent.user_system if parent and hasattr(parent, 'user_system') else None + self.setup_ui() + + def setup_ui(self): + """初始化界面""" + # 移除 setWindowTitle 和 setMinimumSize,使用父窗口的尺寸 + + # 设置可爱的渐变背景 + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFD1DC, stop:1 #B5EAD7); + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(25, 20, 25, 20) + main_layout.setSpacing(0) + + # 卡片容器 + card = QFrame() + card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + card.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 white, stop:1 #FFF9FA); + border-radius: 20px; + border: 3px solid #FF9EBC; + } + """) + main_layout.addWidget(card) + + # 卡片布局 + card_layout = QVBoxLayout(card) + card_layout.setContentsMargins(35, 30, 35, 30) + card_layout.setSpacing(0) + + # 顶部弹性空间 + card_layout.addStretch(1) + + # 标题区域 + title_frame = QFrame() + title_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + title_frame.setStyleSheet("background: transparent; border: none;") + title_layout = QVBoxLayout(title_frame) + title_layout.setSpacing(10) + + # 装饰性emoji + emoji_label = QLabel("👤✨🆕") + emoji_label.setStyleSheet(""" + QLabel { + font: bold 20pt 'Arial'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(emoji_label) + + # 主标题 + title_label = QLabel("修改用户名") + title_label.setStyleSheet(""" + QLabel { + font: bold 24pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(title_label) + + subtitle_label = QLabel("🎯 给你的冒险角色换个新名字吧! 🎯") + subtitle_label.setStyleSheet(""" + QLabel { + font: 12pt '微软雅黑'; + color: #5A5A5A; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + title_layout.addWidget(subtitle_label) + + card_layout.addWidget(title_frame) + card_layout.addSpacing(30) + + # 当前用户名显示 + current_user_frame = QFrame() + current_user_frame.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #FFF9FA, stop:1 #FFE4EC); + border-radius: 15px; + border: 2px solid #FFD1DC; + } + """) + current_user_layout = QVBoxLayout(current_user_frame) + current_user_layout.setContentsMargins(20, 15, 20, 15) + + current_title_label = QLabel("🎮 当前冒险者") + current_title_label.setStyleSheet(""" + QLabel { + font: bold 14pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + current_user_layout.addWidget(current_title_label) + + # 检查用户系统是否存在 + if self.user_system and self.user_system.current_user: + current_username = self.user_system.current_user + else: + current_username = "未登录" + + current_user_label = QLabel(f"✨ {current_username} ✨") + current_user_label.setStyleSheet(""" + QLabel { + font: bold 18pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + current_user_layout.addWidget(current_user_label) + + card_layout.addWidget(current_user_frame) + card_layout.addSpacing(25) + + # 新用户名输入区域 + input_frame = QFrame() + input_frame.setStyleSheet("background: transparent; border: none;") + input_layout = QVBoxLayout(input_frame) + input_layout.setSpacing(8) + + # 新用户名标签 + new_username_label = QLabel("🆕 新用户名:") + new_username_label.setStyleSheet(""" + QLabel { + font: bold 14pt '微软雅黑'; + color: #FF6B9C; + background: transparent; + } + """) + input_layout.addWidget(new_username_label) + + # 新用户名输入框 + self.new_username_input = QLineEdit() + self.new_username_input.setMinimumHeight(45) + self.new_username_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.new_username_input.setStyleSheet(""" + QLineEdit { + background: white; + border: 2px solid #FF9EBC; + border-radius: 18px; + font: 14pt '微软雅黑'; + color: #FF6B9C; + padding: 8px 20px; + selection-background-color: #FFE4EC; + } + QLineEdit:focus { + border: 2px solid #FF6B9C; + background: #FFF9FA; + } + QLineEdit::placeholder { + color: #CCCCCC; + font: 12pt '微软雅黑'; + } + """) + self.new_username_input.setPlaceholderText("请输入新的冒险者名字...") + input_layout.addWidget(self.new_username_input) + + # 用户名提示 + hint_label = QLabel("📝 用户名要求:2-10位中文、字母或数字") + hint_label.setStyleSheet(""" + QLabel { + font: 10pt '微软雅黑'; + color: #FF9EBC; + background: transparent; + } + """) + input_layout.addWidget(hint_label) + + card_layout.addWidget(input_frame) + card_layout.addSpacing(30) + + # 确认按钮 + button_frame = QFrame() + button_frame.setStyleSheet("background: transparent; border: none;") + button_layout = QVBoxLayout(button_frame) + + self.confirm_btn = QPushButton("✨ 确认修改") + self.confirm_btn.setMinimumSize(200, 50) + self.confirm_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.confirm_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #B5EAD7, stop:1 #8CD9B3); + color: white; + font: bold 16pt '微软雅黑'; + border-radius: 25px; + border: 3px solid #A0E7E5; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #8CD9B3, stop:1 #6CD6D3); + border: 3px solid #8CD9B3; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #6CD6D3, stop:1 #5AC7C4); + } + """) + self.confirm_btn.clicked.connect(self.change_username) + button_layout.addWidget(self.confirm_btn, alignment=Qt.AlignCenter) + + card_layout.addWidget(button_frame) + card_layout.addStretch(1) + + # 装饰性底部 + decoration_label = QLabel("🌟🌈🎮 换个名字,开启新的冒险! 📚✨🚀") + decoration_label.setStyleSheet(""" + QLabel { + font: bold 10pt 'Arial'; + color: #FF9EBC; + background: transparent; + qproperty-alignment: AlignCenter; + } + """) + card_layout.addWidget(decoration_label) + + # 绑定回车键 + self.new_username_input.returnPressed.connect(self.change_username) + + # 设置焦点 + self.new_username_input.setFocus() + + def change_username(self): + """修改用户名""" + if not self.user_system: + QMessageBox.warning(self, "❌ 错误", "用户系统未初始化") + return + + if not self.user_system.current_user: + QMessageBox.warning(self, "⚠️ 提示", "请先登录") + if self.parent_window: + self.parent_window.show_main_page() # 回到主页面 + return + + new_username = self.new_username_input.text().strip() + + if not new_username: + QMessageBox.warning(self, "⚠️ 提示", "请输入新用户名") + self.new_username_input.setFocus() + return + + if new_username == self.user_system.current_user: + QMessageBox.warning(self, "⚠️ 提示", "新用户名与当前用户名相同,请换个不同的名字吧!") + self.new_username_input.clear() + self.new_username_input.setFocus() + return + + if self.user_system.set_username(new_username): + QMessageBox.information(self, "🎉 修改成功", + f"用户名修改成功!\n\n" + f"从现在开始,你就是勇敢的冒险者:\n" + f"✨ {new_username} ✨") + # 不再使用 self.accept(),而是通知父窗口切换页面 + if self.parent_window: + self.parent_window.show_level_page() # 回到学段选择页面 + else: + QMessageBox.warning(self, "❌ 修改失败", + "用户名修改失败:\n" + "• 用户名不符合要求(2-10位中文、字母或数字)\n" + "• 或用户名已被其他冒险者使用") + self.new_username_input.clear() + self.new_username_input.setFocus() + + def showEvent(self, event): + """显示页面时重置状态""" + super().showEvent(event) + self.new_username_input.clear() + self.new_username_input.setFocus() + + # 更新当前用户名显示 + if hasattr(self, 'current_user_label') and self.user_system and self.user_system.current_user: + self.current_user_label.setText(f"✨ {self.user_system.current_user} ✨") \ No newline at end of file