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