From bb657f7c87c616d76be5cc8297245db6a769d463 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Fri, 10 Oct 2025 15:58:08 +0800 Subject: [PATCH 01/20] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/data_handler.py diff --git a/src/data_handler.py b/src/data_handler.py new file mode 100644 index 0000000..9168701 --- /dev/null +++ b/src/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 -- 2.34.1 From ed06bbea8f29ec07ced908a03c44bda26ffcc574 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Fri, 10 Oct 2025 15:58:53 +0800 Subject: [PATCH 02/20] =?UTF-8?q?=E5=88=A0=E9=99=A4=20'src/data=5Fhandler.?= =?UTF-8?q?py'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 53 --------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 src/data_handler.py diff --git a/src/data_handler.py b/src/data_handler.py deleted file mode 100644 index 9168701..0000000 --- a/src/data_handler.py +++ /dev/null @@ -1,53 +0,0 @@ -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 -- 2.34.1 From 8eefc87fc2692a5864401befc873c140551d7219 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Fri, 10 Oct 2025 15:59:28 +0800 Subject: [PATCH 03/20] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=20'src/core'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/data_handler.py | 53 ++++++++++++ src/core/email_service.py | 111 ++++++++++++++++++++++++ src/core/question_bank.py | 175 ++++++++++++++++++++++++++++++++++++++ src/core/user_system.py | 90 ++++++++++++++++++++ 4 files changed, 429 insertions(+) create mode 100644 src/core/data_handler.py create mode 100644 src/core/email_service.py create mode 100644 src/core/question_bank.py create mode 100644 src/core/user_system.py diff --git a/src/core/data_handler.py b/src/core/data_handler.py new file mode 100644 index 0000000..9168701 --- /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..7ecdb82 --- /dev/null +++ b/src/core/email_service.py @@ -0,0 +1,111 @@ +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 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 = 5 * 60 # 验证码有效期5分钟 + 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) -> bool: + """发送验证码到邮箱""" + if not self.can_send_code(email): + logger.warning(f"发送过于频繁,邮箱: {email}") + return False + + code = self.generate_code() + expire_time = time.time() + self.code_validity + self.verification_codes[email] = (code, expire_time) + self.last_send_time[email] = time.time() + + # 邮件内容 + subject = "数学学习软件验证码" + + # 格式化邮箱显示,保护隐私 + try: + at_index = email.index('@') + formatted_email = email[:3] + '...' + email[at_index - 4:at_index] if at_index >= 4 else email + except ValueError: + formatted_email = email + + message = f"""【数学学习软件】您正在登录 {formatted_email} +您的验证码是:{code} +此验证码5分钟内有效,请尽快完成操作。 +如非本人操作,请忽略此邮件。""" + + try: + msg = MIMEText(message, "plain", "utf-8") + msg["Subject"] = Header(subject, "utf-8") + # 发件人用英文名称,避免QQ邮箱拒绝 + msg["From"] = formataddr(("MathSoftware", self.sender_email)) + msg["To"] = email + + logger.info(f"尝试发送邮件到 {email},SMTP服务器: {self.smtp_server}:{self.smtp_port}") + + with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server: + server.set_debuglevel(1) + server.login(self.sender_email, self.sender_password) + server.sendmail(self.sender_email, email, msg.as_string()) + + logger.info(f"验证码已发送到 {email}") + return True + + except SMTPResponseException as e: + # 忽略 QQ 邮箱提前关闭连接的异常 + if e.code == -1 and e.message == b'\x00\x00\x00': + logger.info(f"邮件已成功发送到 {email} (忽略连接关闭异常)") + return True + logger.error(f"SMTP响应异常: {e}") + return False + + except smtplib.SMTPAuthenticationError: + logger.error("邮箱认证失败,请检查账号和授权码是否正确") + return False + + except smtplib.SMTPConnectError: + logger.error("无法连接到SMTP服务器,请检查服务器地址和端口") + return False + + except Exception as e: + logger.error(f"邮件发送失败: {str(e)}", exc_info=True) + return False + + 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..437c74f --- /dev/null +++ b/src/core/question_bank.py @@ -0,0 +1,175 @@ +import random +import math +from typing import List, Tuple, Dict + + +class Question: + def __init__(self, content: str, options: List[str], answer: str): + self.content = content + self.options = options + self.answer = answer + + +class QuestionBank: + def __init__(self): + self.operators = { + "primary": ["+", "-", "*", "/"], + "middle": ["+", "-", "*", "/", "^2", "sqrt"], + "high": ["+", "-", "*", "/", "sin", "cos", "tan"] + } + self.generated_questions = [] # 存储已生成题目避免重复 + + def generate_number(self, level: str) -> int: + """根据学段生成数字范围""" + if level == "primary": + return random.randint(1, 100) + elif level == "middle": + return random.randint(1, 500) + else: # high + return random.randint(1, 10) # 三角函数用小数字 + + def generate_expression(self, level: str) -> Tuple[str, float]: + """生成表达式和计算结果,小学学段避免负数""" + op_count = random.randint(1, 3) # 操作数数量 + nums = [self.generate_number(level) for _ in range(op_count + 1)] + ops = [] + + # 根据学段确保特殊运算符 + if level == "middle": + # 确保至少有一个平方或开根号 + special_op = random.choice(["^2", "sqrt"]) + ops = random.choices(self.operators[level], k=op_count - 1) + ops.append(special_op) + random.shuffle(ops) + elif level == "high": + # 确保至少有一个三角函数 + special_op = random.choice(["sin", "cos", "tan"]) + ops = random.choices(self.operators[level], k=op_count - 1) + ops.append(special_op) + random.shuffle(ops) + else: # primary + ops = random.choices(self.operators[level], k=op_count) + # 小学学段处理减法,确保被减数 >= 减数,避免负数 + for i in range(len(ops)): + if ops[i] == "-": + # 保证前面的数大于等于后面的数 + if nums[i] < nums[i + 1]: + nums[i], nums[i + 1] = nums[i + 1], nums[i] + + # 构建表达式字符串和计算结果 + expr_parts = [str(nums[0])] + result = nums[0] + + for i in range(op_count): + op = ops[i] + num = nums[i + 1] if i < len(nums) - 1 else None # 处理特殊运算符 + + expr_parts.append(op) + if op not in ["^2", "sqrt", "sin", "cos", "tan"]: + expr_parts.append(str(num)) + + # 计算结果 + if op == "+": + result += num + elif op == "-": + result -= num + elif op == "*": + result *= num + elif op == "/": + num = max(num, 1) # 避免除零 + result = round(result / num, 2) + elif op == "^2": + result **= 2 + result = round(result, 2) + elif op == "sqrt": + result = round(math.sqrt(abs(result)), 2) + elif op == "sin": + result = round(math.sin(math.radians(result)), 2) + elif op == "cos": + result = round(math.cos(math.radians(result)), 2) + elif op == "tan": + result = round(math.tan(math.radians(result)), 2) + + # 小学题目添加括号(可选,若需要可保留) + if level == "primary" and len(expr_parts) > 3: + # 随机插入括号,同时确保括号内运算结果非负(这里简单处理,可根据实际细化) + start = random.randint(0, len(expr_parts) // 2) * 2 + end = start + 4 + if end < len(expr_parts): + expr_parts.insert(start, "(") + expr_parts.insert(end, ")") + + expr = " ".join(expr_parts) + + try: + # 用 eval 计算实际结果(安全地) + result = eval(expr.replace("^2", "**2")) + result = round(float(result), 2) + except Exception: + # 如果计算出错就重新生成 + return self.generate_expression(level) + + return expr, round(result, 2) + + + + + + def generate_options(self, correct_answer: float) -> Tuple[List[str], str]: + """生成4个选项(1个正确,3个干扰项)- 修复版:确保正确答案在选项中""" + options = [] + # 1. 先添加正确答案,确保核心选项不丢失 + correct_rounded = round(correct_answer, 2) + options.append(correct_rounded) + + # 2. 生成3个干扰项,严格控制范围(正确答案±30%,避免差异过大) + for _ in range(3): + # 计算偏移量:正确答案的±30%范围内,确保干扰项合理 + max_offset = abs(correct_rounded) * 0.3 if correct_rounded != 0 else 10 # 0的情况固定±10 + offset = random.uniform(-max_offset, max_offset) + wrong_answer = round(correct_rounded + offset, 2) + + # 避免干扰项与已有选项重复(包括正确答案) + while wrong_answer in options: + offset = random.uniform(-max_offset, max_offset) + wrong_answer = round(correct_rounded + offset, 2) + + options.append(wrong_answer) + + # 3. 二次确认:强制检查正确答案是否在选项中(兜底逻辑) + if correct_rounded not in options: + # 若意外丢失,替换第一个干扰项为正确答案 + options[0] = correct_rounded + + # 4. 打乱选项顺序并转换为字符串(保留2位小数格式) + random.shuffle(options) + options_str = [f"{opt:.2f}" for opt in options] + correct_str = f"{correct_rounded:.2f}" + + return options_str, correct_str + + def generate_question(self, level: str) -> Question: + """生成单个选择题""" + while True: + expr, answer = self.generate_expression(level) + + if level == "primary" and answer < 0: + continue + # 避免重复题目 + if expr not in [q.content for q in self.generated_questions]: + break + + options, correct = self.generate_options(answer) + question = Question(f"计算:{expr}", options, correct) + self.generated_questions.append(question) + return question + + def generate_paper(self, level: str, count: int) -> List[Question]: + """生成试卷""" + self.generated_questions = [] # 清空历史 + return [self.generate_question(level) for _ in range(count)] + + def calculate_score(self, answers: List[bool]) -> int: + """计算得分(正确率百分比)""" + correct_count = sum(1 for a in answers if a) + return round((correct_count / len(answers)) * 100) if answers else 0 \ 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..f175d27 --- /dev/null +++ b/src/core/user_system.py @@ -0,0 +1,90 @@ +import re +import json +import os +from typing import Dict, Optional +from .email_service import EmailService + +# 项目根目录 -> src所在的路径 +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +USER_DATA_FILE = os.path.join(PROJECT_ROOT, "users.json") # 绝对路径 + +class UserSystem: + def __init__(self, email_service: EmailService): + self.email_service = email_service + self.users: Dict[str, dict] = self.load_users() # {email: {"password": "...", "level": None}} + self.current_user: Optional[str] = None # 当前登录用户邮箱 + self.current_level: Optional[str] = None + + def load_users(self) -> Dict[str, dict]: + """从文件加载用户数据""" + if os.path.exists(USER_DATA_FILE): + with open(USER_DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + return {} + + def save_users(self): + """保存用户数据到文件""" + with open(USER_DATA_FILE, "w", encoding="utf-8") as f: + json.dump(self.users, f, ensure_ascii=False, indent=4) + + 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 send_verification(self, email: str) -> bool: + """发送验证码(邮箱未注册才可以)""" + if email in self.users: + return False # 邮箱已注册 + return self.email_service.send_verification_code(email) + + def register(self, email: str, code: str, password: str) -> bool: + """注册新用户""" + if email in self.users: + return False + if not self.is_valid_password(password): + return False + if not self.email_service.verify_code(email, code): + return False + + self.users[email] = {"password": password, "level": None} + self.save_users() + return True + + def login(self, email: str, password: str) -> bool: + """登录验证""" + user = self.users.get(email) + if user and user["password"] == password: + self.current_user = email + 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_level(self, level: str): + """设置当前用户学段""" + if self.current_user: + self.users[self.current_user]["level"] = level + self.current_level = level + self.save_users() \ No newline at end of file -- 2.34.1 From 8539729c35800d3c30e5ffe8de539f1207780c8c Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Sun, 12 Oct 2025 16:12:24 +0800 Subject: [PATCH 04/20] Update data_handler.py --- src/core/data_handler.py | 104 +++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/core/data_handler.py b/src/core/data_handler.py index 9168701..59d8d28 100644 --- a/src/core/data_handler.py +++ b/src/core/data_handler.py @@ -1,53 +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): - """更新用户信息""" +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 -- 2.34.1 From c7d0a4e427449de27ddacfba161cbe6f45d66289 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Sun, 12 Oct 2025 16:13:18 +0800 Subject: [PATCH 05/20] Update email_service.py --- src/core/email_service.py | 276 +++++++++++++++++++++++--------------- 1 file changed, 166 insertions(+), 110 deletions(-) diff --git a/src/core/email_service.py b/src/core/email_service.py index 7ecdb82..ecca850 100644 --- a/src/core/email_service.py +++ b/src/core/email_service.py @@ -1,111 +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 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 = 5 * 60 # 验证码有效期5分钟 - 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) -> bool: - """发送验证码到邮箱""" - if not self.can_send_code(email): - logger.warning(f"发送过于频繁,邮箱: {email}") - return False - - code = self.generate_code() - expire_time = time.time() + self.code_validity - self.verification_codes[email] = (code, expire_time) - self.last_send_time[email] = time.time() - - # 邮件内容 - subject = "数学学习软件验证码" - - # 格式化邮箱显示,保护隐私 - try: - at_index = email.index('@') - formatted_email = email[:3] + '...' + email[at_index - 4:at_index] if at_index >= 4 else email - except ValueError: - formatted_email = email - - message = f"""【数学学习软件】您正在登录 {formatted_email} -您的验证码是:{code} -此验证码5分钟内有效,请尽快完成操作。 -如非本人操作,请忽略此邮件。""" - - try: - msg = MIMEText(message, "plain", "utf-8") - msg["Subject"] = Header(subject, "utf-8") - # 发件人用英文名称,避免QQ邮箱拒绝 - msg["From"] = formataddr(("MathSoftware", self.sender_email)) - msg["To"] = email - - logger.info(f"尝试发送邮件到 {email},SMTP服务器: {self.smtp_server}:{self.smtp_port}") - - with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server: - server.set_debuglevel(1) - server.login(self.sender_email, self.sender_password) - server.sendmail(self.sender_email, email, msg.as_string()) - - logger.info(f"验证码已发送到 {email}") - return True - - except SMTPResponseException as e: - # 忽略 QQ 邮箱提前关闭连接的异常 - if e.code == -1 and e.message == b'\x00\x00\x00': - logger.info(f"邮件已成功发送到 {email} (忽略连接关闭异常)") - return True - logger.error(f"SMTP响应异常: {e}") - return False - - except smtplib.SMTPAuthenticationError: - logger.error("邮箱认证失败,请检查账号和授权码是否正确") - return False - - except smtplib.SMTPConnectError: - logger.error("无法连接到SMTP服务器,请检查服务器地址和端口") - return False - - except Exception as e: - logger.error(f"邮件发送失败: {str(e)}", exc_info=True) - return False - - 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 - +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 -- 2.34.1 From 3364a313d234807136627b4745581a9b48313810 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Sun, 12 Oct 2025 16:13:59 +0800 Subject: [PATCH 06/20] Update question_bank.py --- src/core/question_bank.py | 451 +++++++++++++++++++++++--------------- 1 file changed, 276 insertions(+), 175 deletions(-) diff --git a/src/core/question_bank.py b/src/core/question_bank.py index 437c74f..2d75ede 100644 --- a/src/core/question_bank.py +++ b/src/core/question_bank.py @@ -1,175 +1,276 @@ -import random -import math -from typing import List, Tuple, Dict - - -class Question: - def __init__(self, content: str, options: List[str], answer: str): - self.content = content - self.options = options - self.answer = answer - - -class QuestionBank: - def __init__(self): - self.operators = { - "primary": ["+", "-", "*", "/"], - "middle": ["+", "-", "*", "/", "^2", "sqrt"], - "high": ["+", "-", "*", "/", "sin", "cos", "tan"] - } - self.generated_questions = [] # 存储已生成题目避免重复 - - def generate_number(self, level: str) -> int: - """根据学段生成数字范围""" - if level == "primary": - return random.randint(1, 100) - elif level == "middle": - return random.randint(1, 500) - else: # high - return random.randint(1, 10) # 三角函数用小数字 - - def generate_expression(self, level: str) -> Tuple[str, float]: - """生成表达式和计算结果,小学学段避免负数""" - op_count = random.randint(1, 3) # 操作数数量 - nums = [self.generate_number(level) for _ in range(op_count + 1)] - ops = [] - - # 根据学段确保特殊运算符 - if level == "middle": - # 确保至少有一个平方或开根号 - special_op = random.choice(["^2", "sqrt"]) - ops = random.choices(self.operators[level], k=op_count - 1) - ops.append(special_op) - random.shuffle(ops) - elif level == "high": - # 确保至少有一个三角函数 - special_op = random.choice(["sin", "cos", "tan"]) - ops = random.choices(self.operators[level], k=op_count - 1) - ops.append(special_op) - random.shuffle(ops) - else: # primary - ops = random.choices(self.operators[level], k=op_count) - # 小学学段处理减法,确保被减数 >= 减数,避免负数 - for i in range(len(ops)): - if ops[i] == "-": - # 保证前面的数大于等于后面的数 - if nums[i] < nums[i + 1]: - nums[i], nums[i + 1] = nums[i + 1], nums[i] - - # 构建表达式字符串和计算结果 - expr_parts = [str(nums[0])] - result = nums[0] - - for i in range(op_count): - op = ops[i] - num = nums[i + 1] if i < len(nums) - 1 else None # 处理特殊运算符 - - expr_parts.append(op) - if op not in ["^2", "sqrt", "sin", "cos", "tan"]: - expr_parts.append(str(num)) - - # 计算结果 - if op == "+": - result += num - elif op == "-": - result -= num - elif op == "*": - result *= num - elif op == "/": - num = max(num, 1) # 避免除零 - result = round(result / num, 2) - elif op == "^2": - result **= 2 - result = round(result, 2) - elif op == "sqrt": - result = round(math.sqrt(abs(result)), 2) - elif op == "sin": - result = round(math.sin(math.radians(result)), 2) - elif op == "cos": - result = round(math.cos(math.radians(result)), 2) - elif op == "tan": - result = round(math.tan(math.radians(result)), 2) - - # 小学题目添加括号(可选,若需要可保留) - if level == "primary" and len(expr_parts) > 3: - # 随机插入括号,同时确保括号内运算结果非负(这里简单处理,可根据实际细化) - start = random.randint(0, len(expr_parts) // 2) * 2 - end = start + 4 - if end < len(expr_parts): - expr_parts.insert(start, "(") - expr_parts.insert(end, ")") - - expr = " ".join(expr_parts) - - try: - # 用 eval 计算实际结果(安全地) - result = eval(expr.replace("^2", "**2")) - result = round(float(result), 2) - except Exception: - # 如果计算出错就重新生成 - return self.generate_expression(level) - - return expr, round(result, 2) - - - - - - def generate_options(self, correct_answer: float) -> Tuple[List[str], str]: - """生成4个选项(1个正确,3个干扰项)- 修复版:确保正确答案在选项中""" - options = [] - # 1. 先添加正确答案,确保核心选项不丢失 - correct_rounded = round(correct_answer, 2) - options.append(correct_rounded) - - # 2. 生成3个干扰项,严格控制范围(正确答案±30%,避免差异过大) - for _ in range(3): - # 计算偏移量:正确答案的±30%范围内,确保干扰项合理 - max_offset = abs(correct_rounded) * 0.3 if correct_rounded != 0 else 10 # 0的情况固定±10 - offset = random.uniform(-max_offset, max_offset) - wrong_answer = round(correct_rounded + offset, 2) - - # 避免干扰项与已有选项重复(包括正确答案) - while wrong_answer in options: - offset = random.uniform(-max_offset, max_offset) - wrong_answer = round(correct_rounded + offset, 2) - - options.append(wrong_answer) - - # 3. 二次确认:强制检查正确答案是否在选项中(兜底逻辑) - if correct_rounded not in options: - # 若意外丢失,替换第一个干扰项为正确答案 - options[0] = correct_rounded - - # 4. 打乱选项顺序并转换为字符串(保留2位小数格式) - random.shuffle(options) - options_str = [f"{opt:.2f}" for opt in options] - correct_str = f"{correct_rounded:.2f}" - - return options_str, correct_str - - def generate_question(self, level: str) -> Question: - """生成单个选择题""" - while True: - expr, answer = self.generate_expression(level) - - if level == "primary" and answer < 0: - continue - # 避免重复题目 - if expr not in [q.content for q in self.generated_questions]: - break - - options, correct = self.generate_options(answer) - question = Question(f"计算:{expr}", options, correct) - self.generated_questions.append(question) - return question - - def generate_paper(self, level: str, count: int) -> List[Question]: - """生成试卷""" - self.generated_questions = [] # 清空历史 - return [self.generate_question(level) for _ in range(count)] - - def calculate_score(self, answers: List[bool]) -> int: - """计算得分(正确率百分比)""" - correct_count = sum(1 for a in answers if a) - return round((correct_count / len(answers)) * 100) if answers else 0 \ No newline at end of file +# -*- 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 -- 2.34.1 From be89766749310c852f1c9877e36553141e7d6e85 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Sun, 12 Oct 2025 16:14:31 +0800 Subject: [PATCH 07/20] Update user_system.py --- src/core/user_system.py | 226 ++++++++++++++++++++++++---------------- 1 file changed, 136 insertions(+), 90 deletions(-) diff --git a/src/core/user_system.py b/src/core/user_system.py index f175d27..f9c38c8 100644 --- a/src/core/user_system.py +++ b/src/core/user_system.py @@ -1,90 +1,136 @@ -import re -import json -import os -from typing import Dict, Optional -from .email_service import EmailService - -# 项目根目录 -> src所在的路径 -PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -USER_DATA_FILE = os.path.join(PROJECT_ROOT, "users.json") # 绝对路径 - -class UserSystem: - def __init__(self, email_service: EmailService): - self.email_service = email_service - self.users: Dict[str, dict] = self.load_users() # {email: {"password": "...", "level": None}} - self.current_user: Optional[str] = None # 当前登录用户邮箱 - self.current_level: Optional[str] = None - - def load_users(self) -> Dict[str, dict]: - """从文件加载用户数据""" - if os.path.exists(USER_DATA_FILE): - with open(USER_DATA_FILE, "r", encoding="utf-8") as f: - return json.load(f) - return {} - - def save_users(self): - """保存用户数据到文件""" - with open(USER_DATA_FILE, "w", encoding="utf-8") as f: - json.dump(self.users, f, ensure_ascii=False, indent=4) - - 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 send_verification(self, email: str) -> bool: - """发送验证码(邮箱未注册才可以)""" - if email in self.users: - return False # 邮箱已注册 - return self.email_service.send_verification_code(email) - - def register(self, email: str, code: str, password: str) -> bool: - """注册新用户""" - if email in self.users: - return False - if not self.is_valid_password(password): - return False - if not self.email_service.verify_code(email, code): - return False - - self.users[email] = {"password": password, "level": None} - self.save_users() - return True - - def login(self, email: str, password: str) -> bool: - """登录验证""" - user = self.users.get(email) - if user and user["password"] == password: - self.current_user = email - 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_level(self, level: str): - """设置当前用户学段""" - if self.current_user: - self.users[self.current_user]["level"] = level - self.current_level = level - self.save_users() \ No newline at end of file +import re +import json +import os +from typing import Dict, Optional +from .email_service import EmailService + +# 项目根目录 -> src所在的路径 +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +USER_DATA_FILE = os.path.join(PROJECT_ROOT, "users.json") # 绝对路径 + +class UserSystem: + def __init__(self, email_service: EmailService): + self.email_service = email_service + self.users: Dict[str, dict] = self.load_users() # {username: {"password": "...", "email": "...", "level": None}} + self.current_user: Optional[str] = None # 当前登录用户名 + self.current_level: Optional[str] = None + + def load_users(self) -> Dict[str, dict]: + """从文件加载用户数据""" + if os.path.exists(USER_DATA_FILE): + with open(USER_DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + return {} + + def save_users(self): + """保存用户数据到文件""" + with open(USER_DATA_FILE, "w", encoding="utf-8") as f: + json.dump(self.users, f, ensure_ascii=False, indent=4) + + 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 -- 2.34.1 From c7a78df7b47193317701e141a456550d1782a972 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Sun, 12 Oct 2025 19:27:41 +0800 Subject: [PATCH 08/20] Update user_system.py --- src/core/user_system.py | 47 ++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/core/user_system.py b/src/core/user_system.py index f9c38c8..d7074a3 100644 --- a/src/core/user_system.py +++ b/src/core/user_system.py @@ -1,31 +1,58 @@ import re import json import os +import sys from typing import Dict, Optional from .email_service import EmailService -# 项目根目录 -> src所在的路径 -PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -USER_DATA_FILE = os.path.join(PROJECT_ROOT, "users.json") # 绝对路径 +# 获取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 - self.users: Dict[str, dict] = self.load_users() # {username: {"password": "...", "email": "...", "level": None}} - self.current_user: Optional[str] = None # 当前登录用户名 + # 确保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): - with open(USER_DATA_FILE, "r", encoding="utf-8") as f: - return json.load(f) - return {} + 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): """保存用户数据到文件""" - with open(USER_DATA_FILE, "w", encoding="utf-8") as f: - json.dump(self.users, f, ensure_ascii=False, indent=4) + 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: """验证邮箱格式""" -- 2.34.1 From 7fa865b9aab629bc55667c10448cf7afe7f6ba8c Mon Sep 17 00:00:00 2001 From: hnu202326010330 <168584091@qq.com> Date: Sun, 12 Oct 2025 20:21:32 +0800 Subject: [PATCH 12/20] ADD file via upload --- src/ui/login_ui.py | 674 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 src/ui/login_ui.py 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 -- 2.34.1 From 63fcd0def4afefe836b3cf96d1a639d0ecd64bd8 Mon Sep 17 00:00:00 2001 From: hnu202326010330 <168584091@qq.com> Date: Sun, 12 Oct 2025 20:22:00 +0800 Subject: [PATCH 13/20] ADD file via upload --- src/ui/main_window.py | 764 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 764 insertions(+) create mode 100644 src/ui/main_window.py 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 -- 2.34.1 From 3cf4b9b83ec73a0a9479ec7a219f18d89abff681 Mon Sep 17 00:00:00 2001 From: hnu202326010330 <168584091@qq.com> Date: Sun, 12 Oct 2025 20:22:40 +0800 Subject: [PATCH 14/20] ADD file via upload --- src/ui/question_ui.py | 455 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 src/ui/question_ui.py 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 -- 2.34.1 From a8716b76c357e9ea09bd09ac4c74fc9a4d418a50 Mon Sep 17 00:00:00 2001 From: hnu202326010330 <168584091@qq.com> Date: Sun, 12 Oct 2025 20:23:02 +0800 Subject: [PATCH 15/20] ADD file via upload --- src/ui/register_ui.py | 589 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 589 insertions(+) create mode 100644 src/ui/register_ui.py 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 -- 2.34.1 From b22b0ac225e16467c2f48d39f9289f2c77d0c5e4 Mon Sep 17 00:00:00 2001 From: hnu202326010330 <168584091@qq.com> Date: Sun, 12 Oct 2025 20:23:41 +0800 Subject: [PATCH 16/20] ADD file via upload --- src/ui/result_ui.py | 296 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 src/ui/result_ui.py 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 -- 2.34.1 From 4e3fc84cbb7c0c8eb06c0be5888c5a9297416df0 Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Sun, 12 Oct 2025 20:24:15 +0800 Subject: [PATCH 17/20] ADD file via upload --- src/main.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main.py 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 -- 2.34.1 From d532bc131972f400e473a3c6c8d137dbdc826289 Mon Sep 17 00:00:00 2001 From: hnu202326010330 <168584091@qq.com> Date: Sun, 12 Oct 2025 20:24:19 +0800 Subject: [PATCH 18/20] ADD file via upload --- src/ui/user_management_ui.py | 297 +++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/ui/user_management_ui.py 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 -- 2.34.1 From 42708295fdfa99c2582a64c508af1ad70e89212a Mon Sep 17 00:00:00 2001 From: hnu202326010322 <1463365450@qq.com> Date: Sun, 12 Oct 2025 20:31:06 +0800 Subject: [PATCH 19/20] =?UTF-8?q?=E5=88=A0=E9=99=A4=20'README.md'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 README.md 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 - -- 2.34.1 From 44e5347a4207098608dceb6fc26a85d1537f6a5a Mon Sep 17 00:00:00 2001 From: hnu202326010330 <168584091@qq.com> Date: Sun, 12 Oct 2025 20:31:16 +0800 Subject: [PATCH 20/20] ADD file via upload --- doc/README.md | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 doc/README.md 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 + + -- 2.34.1