diff --git a/src/services/question_service.py b/src/services/question_service.py index 003636e..a3272cf 100644 --- a/src/services/question_service.py +++ b/src/services/question_service.py @@ -3,15 +3,33 @@ """ import random import math -from typing import List, Dict +from typing import List, Dict, Callable, Tuple, Set, Optional, Union +from enum import Enum + + +class Level(Enum): + """题目难度级别枚举""" + PRIMARY = 'primary' + MIDDLE = 'middle' + HIGH = 'high' class QuestionService: """负责生成不同年级难度的选择题。""" + # 特殊角度和完全平方数常量 + SPECIAL_ANGLES = [0, 30, 45, 60, 90, 120, 135, 150, 180] + PERFECT_SQUARES = [4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225] + def __init__(self) -> None: """初始化题目服务。""" random.seed() + # 映射难度级别到对应的生成函数 + self._generators = { + Level.PRIMARY.value: self._gen_primary, + Level.MIDDLE.value: self._gen_middle, + Level.HIGH.value: self._gen_high, + } def generate_questions(self, level: str, count: int) -> List[Dict]: """生成指定年级与数量的题目列表。 @@ -22,71 +40,83 @@ class QuestionService: 返回: 题目字典列表,每个字典包含 stem, options, answer_index。 """ - generators = { - 'primary': self._gen_primary, - 'middle': self._gen_middle, - 'high': self._gen_high, - } - gen = generators.get(level) + if count <= 0: + raise ValueError('题目数量必须为正整数') + + gen = self._generators.get(level) if not gen: - raise ValueError('无效的年级选项') - seen = set() + raise ValueError(f'无效的年级选项: {level},可选值为 primary, middle, high') + + seen: Set[str] = set() questions: List[Dict] = [] + + # 使用集合去重,避免重复题目 while len(questions) < count: q = gen() if q['stem'] in seen: continue seen.add(q['stem']) questions.append(q) + return questions def _gen_primary(self) -> Dict: """生成一题小学难度的四则运算选择题(可带括号)。""" # 随机选择运算类型:简单四则运算或带括号的复合运算 if random.choice([True, False]): - # 简单四则运算 - a = random.randint(1, 50) - b = random.randint(1, 50) - op = random.choice(['+', '-', '*', '/']) - - if op == '+': - ans = a + b - stem = f"计算:{a} + {b} = ?" - elif op == '-': - ans = a - b - stem = f"计算:{a} - {b} = ?" - elif op == '*': - ans = a * b - stem = f"计算:{a} × {b} = ?" - else: # 除法,确保整除 - ans = a - b = random.randint(1, 10) - a = ans * b # 确保整除 - stem = f"计算:{a} ÷ {b} = ?" + stem, ans = self._gen_simple_arithmetic() else: - # 带括号的复合运算 - a = random.randint(1, 20) - b = random.randint(1, 20) - c = random.randint(1, 20) - - # 随机选择括号运算类型 - bracket_type = random.choice(['add_mul', 'sub_mul', 'mul_add', 'mul_sub']) - - if bracket_type == 'add_mul': - ans = (a + b) * c - stem = f"计算:({a} + {b}) × {c} = ?" - elif bracket_type == 'sub_mul': - ans = (a - b) * c - stem = f"计算:({a} - {b}) × {c} = ?" - elif bracket_type == 'mul_add': - ans = a * (b + c) - stem = f"计算:{a} × ({b} + {c}) = ?" - else: # mul_sub - ans = a * (b - c) - stem = f"计算:{a} × ({b} - {c}) = ?" + stem, ans = self._gen_bracket_arithmetic() options = self._make_options(ans) return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + + def _gen_simple_arithmetic(self) -> Tuple[str, int]: + """生成简单四则运算题目""" + a = random.randint(1, 50) + b = random.randint(1, 50) + op = random.choice(['+', '-', '*', '/']) + + if op == '+': + ans = a + b + stem = f"计算:{a} + {b} = ?" + elif op == '-': + ans = a - b + stem = f"计算:{a} - {b} = ?" + elif op == '*': + ans = a * b + stem = f"计算:{a} × {b} = ?" + else: # 除法,确保整除 + ans = a + b = random.randint(1, 10) + a = ans * b # 确保整除 + stem = f"计算:{a} ÷ {b} = ?" + + return stem, ans + + def _gen_bracket_arithmetic(self) -> Tuple[str, int]: + """生成带括号的复合运算题目""" + a = random.randint(1, 20) + b = random.randint(1, 20) + c = random.randint(1, 20) + + # 随机选择括号运算类型 + bracket_type = random.choice(['add_mul', 'sub_mul', 'mul_add', 'mul_sub']) + + if bracket_type == 'add_mul': + ans = (a + b) * c + stem = f"计算:({a} + {b}) × {c} = ?" + elif bracket_type == 'sub_mul': + ans = (a - b) * c + stem = f"计算:({a} - {b}) × {c} = ?" + elif bracket_type == 'mul_add': + ans = a * (b + c) + stem = f"计算:{a} × ({b} + {c}) = ?" + else: # mul_sub + ans = a * (b - c) + stem = f"计算:{a} × ({b} - {c}) = ?" + + return stem, ans def _gen_middle(self) -> Dict: """生成一题初中难度的题目,至少包含一个平方或开根号运算。""" @@ -94,46 +124,58 @@ class QuestionService: question_type = random.choice(['square', 'sqrt', 'mixed']) if question_type == 'square': - # 平方运算题目 - a = random.randint(2, 15) - b = random.randint(1, 10) - - if random.choice([True, False]): - # (a + b)² - ans = (a + b) ** 2 - stem = f"计算:({a} + {b})² = ?" - else: - # a² + b² - ans = a ** 2 + b ** 2 - stem = f"计算:{a}² + {b}² = ?" - + stem, ans = self._gen_square_question() elif question_type == 'sqrt': - # 开根号运算题目 - # 选择完全平方数确保结果为整数 - perfect_squares = [4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225] - a = random.choice(perfect_squares) - b = random.randint(1, 10) - - if random.choice([True, False]): - # √a + b - ans = int(math.sqrt(a)) + b - stem = f"计算:√{a} + {b} = ?" - else: - # √a × b - ans = int(math.sqrt(a)) * b - stem = f"计算:√{a} × {b} = ?" - + stem, ans = self._gen_sqrt_question() else: # mixed - # 混合运算:既有平方又有开根号 - perfect_square = random.choice([4, 9, 16, 25, 36, 49, 64, 81, 100]) - a = random.randint(2, 8) - - # √perfect_square + a² - ans = int(math.sqrt(perfect_square)) + a ** 2 - stem = f"计算:√{perfect_square} + {a}² = ?" + stem, ans = self._gen_mixed_middle_question() options = self._make_options(ans) return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + + def _gen_square_question(self) -> Tuple[str, int]: + """生成平方运算题目""" + a = random.randint(2, 15) + b = random.randint(1, 10) + + if random.choice([True, False]): + # (a + b)² + ans = (a + b) ** 2 + stem = f"计算:({a} + {b})² = ?" + else: + # a² + b² + ans = a ** 2 + b ** 2 + stem = f"计算:{a}² + {b}² = ?" + + return stem, ans + + def _gen_sqrt_question(self) -> Tuple[str, int]: + """生成开根号运算题目""" + # 选择完全平方数确保结果为整数 + a = random.choice(self.PERFECT_SQUARES) + b = random.randint(1, 10) + + if random.choice([True, False]): + # √a + b + ans = int(math.sqrt(a)) + b + stem = f"计算:√{a} + {b} = ?" + else: + # √a × b + ans = int(math.sqrt(a)) * b + stem = f"计算:√{a} × {b} = ?" + + return stem, ans + + def _gen_mixed_middle_question(self) -> Tuple[str, int]: + """生成混合运算:既有平方又有开根号""" + perfect_square = random.choice(self.PERFECT_SQUARES[:9]) # 使用较小的完全平方数 + a = random.randint(2, 8) + + # √perfect_square + a² + ans = int(math.sqrt(perfect_square)) + a ** 2 + stem = f"计算:√{perfect_square} + {a}² = ?" + + return stem, ans def _gen_high(self) -> Dict: """生成一题高中难度的题目,至少包含一个sin、cos或tan运算符。""" @@ -141,91 +183,147 @@ class QuestionService: question_type = random.choice(['basic_trig', 'trig_calc', 'mixed_trig']) if question_type == 'basic_trig': - # 基础三角函数值计算 - # 使用特殊角度确保结果为常见值 - special_angles = [0, 30, 45, 60, 90, 120, 135, 150, 180] - angle = random.choice(special_angles) - func = random.choice(['sin', 'cos', 'tan']) - - # 计算三角函数值(转换为弧度) - rad = math.radians(angle) - if func == 'sin': - ans = round(math.sin(rad), 2) - stem = f"计算:sin({angle}°) = ?(保留两位小数)" - elif func == 'cos': - ans = round(math.cos(rad), 2) - stem = f"计算:cos({angle}°) = ?(保留两位小数)" - else: # tan - if angle in [90, 270]: # tan在这些角度未定义 - angle = 45 - rad = math.radians(angle) - ans = round(math.tan(rad), 2) - stem = f"计算:tan({angle}°) = ?(保留两位小数)" - + stem, ans = self._gen_basic_trig_question() elif question_type == 'trig_calc': - # 三角函数运算 - angle1 = random.choice([30, 45, 60]) - angle2 = random.choice([30, 45, 60]) - func1 = random.choice(['sin', 'cos']) - func2 = random.choice(['sin', 'cos']) - - rad1 = math.radians(angle1) - rad2 = math.radians(angle2) - - if func1 == 'sin': - val1 = math.sin(rad1) - else: - val1 = math.cos(rad1) - - if func2 == 'sin': - val2 = math.sin(rad2) - else: - val2 = math.cos(rad2) - - # 随机选择运算符 - if random.choice([True, False]): - ans = round(val1 + val2, 2) - stem = f"计算:{func1}({angle1}°) + {func2}({angle2}°) = ?(保留两位小数)" - else: - ans = round(val1 * val2, 2) - stem = f"计算:{func1}({angle1}°) × {func2}({angle2}°) = ?(保留两位小数)" - + stem, ans = self._gen_trig_calc_question() else: # mixed_trig - # 混合运算:三角函数与代数运算 - angle = random.choice([30, 45, 60]) - a = random.randint(2, 5) - func = random.choice(['sin', 'cos', 'tan']) - - rad = math.radians(angle) - if func == 'sin': - trig_val = math.sin(rad) - elif func == 'cos': - trig_val = math.cos(rad) - else: - trig_val = math.tan(rad) - - ans = round(a * trig_val + a, 2) - stem = f"计算:{a} × {func}({angle}°) + {a} = ?(保留两位小数)" + stem, ans = self._gen_mixed_trig_question() options = self._make_options(ans, float_mode=True) correct = f"{ans:.2f}" return {'stem': stem, 'options': options, 'answer_index': options.index(correct)} + + def _gen_basic_trig_question(self) -> Tuple[str, float]: + """生成基础三角函数值计算题目""" + # 使用特殊角度确保结果为常见值 + angle = random.choice(self.SPECIAL_ANGLES) + func = random.choice(['sin', 'cos', 'tan']) + + # 计算三角函数值(转换为弧度) + rad = math.radians(angle) + + if func == 'sin': + ans = round(math.sin(rad), 2) + stem = f"计算:sin({angle}°) = ?(保留两位小数)" + elif func == 'cos': + ans = round(math.cos(rad), 2) + stem = f"计算:cos({angle}°) = ?(保留两位小数)" + else: # tan + if angle in [90, 270]: # tan在这些角度未定义 + angle = 45 + rad = math.radians(angle) + ans = round(math.tan(rad), 2) + stem = f"计算:tan({angle}°) = ?(保留两位小数)" + + return stem, ans + + def _gen_trig_calc_question(self) -> Tuple[str, float]: + """生成三角函数运算题目""" + angle1 = random.choice([30, 45, 60]) + angle2 = random.choice([30, 45, 60]) + func1 = random.choice(['sin', 'cos']) + func2 = random.choice(['sin', 'cos']) + + rad1 = math.radians(angle1) + rad2 = math.radians(angle2) + + val1 = math.sin(rad1) if func1 == 'sin' else math.cos(rad1) + val2 = math.sin(rad2) if func2 == 'sin' else math.cos(rad2) + + # 随机选择运算符 + if random.choice([True, False]): + ans = round(val1 + val2, 2) + stem = f"计算:{func1}({angle1}°) + {func2}({angle2}°) = ?(保留两位小数)" + else: + ans = round(val1 * val2, 2) + stem = f"计算:{func1}({angle1}°) × {func2}({angle2}°) = ?(保留两位小数)" + + return stem, ans + + def _gen_mixed_trig_question(self) -> Tuple[str, float]: + """生成混合运算:三角函数与代数运算""" + angle = random.choice([30, 45, 60]) + a = random.randint(2, 5) + func = random.choice(['sin', 'cos', 'tan']) + + rad = math.radians(angle) + + if func == 'sin': + trig_val = math.sin(rad) + elif func == 'cos': + trig_val = math.cos(rad) + else: + trig_val = math.tan(rad) + + ans = round(a * trig_val + a, 2) + stem = f"计算:{a} × {func}({angle}°) + {a} = ?(保留两位小数)" + + return stem, ans - def _make_options(self, answer, float_mode: bool = False) -> List[str]: - """根据正确答案生成 4 个选项(包含正确答案与3个干扰项)。""" - opts = set() + def _make_options(self, answer: Union[int, float], float_mode: bool = False) -> List[str]: + """根据正确答案生成 4 个选项(包含正确答案与3个干扰项)。 + + 参数: + answer: 正确答案 + float_mode: 是否为浮点数模式 + 返回: + 包含4个选项的列表 + """ if float_mode: - correct = f"{answer:.2f}" - opts.add(correct) - while len(opts) < 4: - delta = random.uniform(-5, 5) - opts.add(f"{answer + delta:.2f}") + return self._make_float_options(answer) else: - correct = str(answer) - opts.add(correct) - while len(opts) < 4: - delta = random.randint(-10, 10) + return self._make_int_options(answer) + + def _make_float_options(self, answer: float) -> List[str]: + """生成浮点数选项""" + opts: Set[str] = set() + correct = f"{answer:.2f}" + opts.add(correct) + + # 生成干扰项,确保不重复 + attempts = 0 + while len(opts) < 4 and attempts < 20: + delta = random.uniform(-5, 5) + # 确保干扰项与正确答案有一定差距 + if abs(delta) < 0.1: + continue + opts.add(f"{answer + delta:.2f}") + attempts += 1 + + # 如果生成的选项不足4个,添加固定偏移的选项 + if len(opts) < 4: + for delta in [0.5, -0.5, 1.0, -1.0]: + if len(opts) >= 4: + break + opts.add(f"{answer + delta:.2f}") + + options = list(opts) + random.shuffle(options) + return options + + def _make_int_options(self, answer: int) -> List[str]: + """生成整数选项""" + opts: Set[str] = set() + correct = str(answer) + opts.add(correct) + + # 生成干扰项,确保不重复且有一定差距 + attempts = 0 + while len(opts) < 4 and attempts < 20: + delta = random.randint(-10, 10) + # 确保干扰项与正确答案有一定差距 + if delta == 0 or abs(delta) < 2: + continue + opts.add(str(answer + delta)) + attempts += 1 + + # 如果生成的选项不足4个,添加固定偏移的选项 + if len(opts) < 4: + for delta in [2, -2, 5, -5]: + if len(opts) >= 4: + break opts.add(str(answer + delta)) + options = list(opts) random.shuffle(options) return options \ No newline at end of file diff --git a/src/storage/config.json b/src/storage/config.json index e358e09..7f5d8ac 100644 --- a/src/storage/config.json +++ b/src/storage/config.json @@ -1,11 +1,11 @@ -{ - "smtp": { - "server": "", - "port": 587, - "username": "", - "password": "", - "use_tls": true, - "use_ssl": false, - "sender_name": "Math Study App" - } +{ + "smtp": { + "server": "", + "port": 587, + "username": "", + "password": "", + "use_tls": true, + "use_ssl": false, + "sender_name": "Math Study App" + } } \ No newline at end of file diff --git a/src/storage/users.json b/src/storage/users.json index 3ffd32d..318369b 100644 --- a/src/storage/users.json +++ b/src/storage/users.json @@ -1,15 +1,15 @@ -{ - "users": [ - { - "email": "shenyongye@163.com", - "verified": true, - "code_salt": "", - "code_hash": "", - "code_time": 1760256425, - "password_salt": "e09f40c04b33d5f482ff682b3d43192b", - "password_hash": "dffeab45544b194e5f7efeb632cbe75eef3b46fbff23531692c7ef0e9a83f24b", - "username": "echo", - "created_at": 1760256425 - } - ] +{ + "users": [ + { + "email": "shenyongye@163.com", + "verified": true, + "code_salt": "", + "code_hash": "", + "code_time": 1760256425, + "password_salt": "e09f40c04b33d5f482ff682b3d43192b", + "password_hash": "dffeab45544b194e5f7efeb632cbe75eef3b46fbff23531692c7ef0e9a83f24b", + "username": "echo", + "created_at": 1760256425 + } + ] } \ No newline at end of file diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 4d5550d..22196b7 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -1,585 +1,585 @@ -from PyQt6.QtWidgets import ( - QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout, - QLabel, QLineEdit, QPushButton, QMessageBox, QRadioButton, QButtonGroup, QSpinBox -) -from PyQt6.QtCore import Qt - -from services.storage_service import StorageService -from services.user_service import UserService -from services.question_service import QuestionService - - -class MainWindow(QMainWindow): - """应用主窗口:组织各个页面并承载业务服务。""" - - def __init__(self) -> None: - """初始化主窗口并构建基础页面。""" - super().__init__() - self.setWindowTitle("数学学习软件") - self.setFixedSize(900, 600) - - # 业务服务初始化 - self.storage_service = StorageService() - self.session_email = None - self.user_service = UserService(self.storage_service) - self.question_service = QuestionService() - - # 页面容器 - self.stack = QStackedWidget() - self.setCentralWidget(self.stack) - - # 构建页面:登录、注册(邮箱/验证码/用户名密码)、选择、答题、结果、修改密码 - self._build_login_page() # index 0 - self._build_register_email_page() # index 1 - self._build_verify_page() # index 2 - self._build_set_credentials_page() # index 3 - self._build_choice_page() # index 4 - self._build_quiz_page() # index 5 - self._build_result_page() # index 6 - self._build_change_password_page() # index 7 - - # 默认显示登录页 - self.stack.setCurrentIndex(0) - - def _build_login_page(self) -> None: - """构建登录页面:支持用户名或邮箱登录。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - title = QLabel("登录") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") - layout.addWidget(title) - - self.login_identifier = QLineEdit() - self.login_identifier.setPlaceholderText("请输入邮箱或用户名") - layout.addWidget(self.login_identifier) - - self.login_password = QLineEdit() - self.login_password.setPlaceholderText("请输入密码") - self.login_password.setEchoMode(QLineEdit.EchoMode.Password) - layout.addWidget(self.login_password) - - btn_row = QHBoxLayout() - self.btn_login = QPushButton("登录") - self.btn_to_register = QPushButton("去注册") - btn_row.addWidget(self.btn_login) - btn_row.addWidget(self.btn_to_register) - layout.addLayout(btn_row) - - self.btn_login.clicked.connect(self._on_login) - self.btn_to_register.clicked.connect(lambda: self.stack.setCurrentIndex(1)) - - self.stack.addWidget(page) - - def _on_login(self) -> None: - """处理登录逻辑:根据输入自动识别邮箱或用户名进行登录,并在成功后进入选择页。""" - identifier = (self.login_identifier.text() or "").strip() - password = self.login_password.text() or "" - if not identifier or not password: - QMessageBox.warning(self, "提示", "账号与密码均不能为空") - return - # 判断是否为邮箱 - if "@" in identifier: - ok, msg = self.user_service.login(identifier, password) - if ok: - self.session_email = identifier - else: - QMessageBox.warning(self, "提示", msg or "登录失败") - return - else: - ok, msg = self.user_service.login_by_username(identifier, password) - if ok: - # 通过用户名获取邮箱,记录会话 - user = self.storage_service.get_user_by_username(identifier) - self.session_email = user.get('email') if user else None - else: - QMessageBox.warning(self, "提示", msg or "登录失败") - return - QMessageBox.information(self, "提示", "登录成功") - # 跳转到选择页 - self.stack.setCurrentIndex(4) - - def _build_register_email_page(self) -> None: - """构建注册第一步:邮箱输入页面。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - title = QLabel("注册 - 邮箱验证") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") - layout.addWidget(title) - - self.register_email = QLineEdit() - self.register_email.setPlaceholderText("请输入邮箱地址") - layout.addWidget(self.register_email) - - btn_row = QHBoxLayout() - self.btn_send_code = QPushButton("获取验证码") - self.btn_back_to_login = QPushButton("返回登录") - btn_row.addWidget(self.btn_send_code) - btn_row.addWidget(self.btn_back_to_login) - layout.addLayout(btn_row) - - self.btn_send_code.clicked.connect(self._on_send_code) - self.btn_back_to_login.clicked.connect(lambda: self.stack.setCurrentIndex(0)) - - self.stack.addWidget(page) - - def _on_send_code(self) -> None: - """处理发送验证码逻辑:验证邮箱格式并生成验证码。""" - email = (self.register_email.text() or "").strip() - if not email: - QMessageBox.warning(self, "提示", "请输入邮箱地址") - return - - ok, msg = self.user_service.request_registration(email) - if ok: - self.session_email = email - QMessageBox.information(self, "提示", msg) - # 跳转到验证码页面 - self.stack.setCurrentIndex(2) - else: - QMessageBox.warning(self, "提示", msg) - - def _build_verify_page(self) -> None: - """构建注册第二步:验证码验证页面。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - title = QLabel("注册 - 验证码验证") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") - layout.addWidget(title) - - self.verify_code = QLineEdit() - self.verify_code.setPlaceholderText("请输入6位验证码") - layout.addWidget(self.verify_code) - - btn_row = QHBoxLayout() - self.btn_verify = QPushButton("验证") - self.btn_back_to_email = QPushButton("返回上一步") - btn_row.addWidget(self.btn_verify) - btn_row.addWidget(self.btn_back_to_email) - layout.addLayout(btn_row) - - self.btn_verify.clicked.connect(self._on_verify_code) - self.btn_back_to_email.clicked.connect(lambda: self.stack.setCurrentIndex(1)) - - self.stack.addWidget(page) - - def _on_verify_code(self) -> None: - """处理验证码验证逻辑。""" - code = (self.verify_code.text() or "").strip() - if not code: - QMessageBox.warning(self, "提示", "请输入验证码") - return - - if not self.session_email: - QMessageBox.warning(self, "提示", "会话已过期,请重新注册") - self.stack.setCurrentIndex(1) - return - - ok, msg = self.user_service.verify_code(self.session_email, code) - if ok: - QMessageBox.information(self, "提示", msg) - # 跳转到设置用户名密码页面 - self.stack.setCurrentIndex(3) - else: - QMessageBox.warning(self, "提示", msg) - - def _build_set_credentials_page(self) -> None: - """构建注册第三步:设置用户名和密码页面。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - title = QLabel("注册 - 设置用户名和密码") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") - layout.addWidget(title) - - self.register_username = QLineEdit() - self.register_username.setPlaceholderText("请输入用户名(3-16位,字母数字下划线)") - layout.addWidget(self.register_username) - - self.register_password = QLineEdit() - self.register_password.setPlaceholderText("请输入密码(6-10位,包含大小写字母和数字)") - self.register_password.setEchoMode(QLineEdit.EchoMode.Password) - layout.addWidget(self.register_password) - - self.register_confirm = QLineEdit() - self.register_confirm.setPlaceholderText("请确认密码") - self.register_confirm.setEchoMode(QLineEdit.EchoMode.Password) - layout.addWidget(self.register_confirm) - - btn_row = QHBoxLayout() - self.btn_complete_register = QPushButton("完成注册") - self.btn_back_to_verify = QPushButton("返回上一步") - btn_row.addWidget(self.btn_complete_register) - btn_row.addWidget(self.btn_back_to_verify) - layout.addLayout(btn_row) - - self.btn_complete_register.clicked.connect(self._on_complete_register) - self.btn_back_to_verify.clicked.connect(lambda: self.stack.setCurrentIndex(2)) - - self.stack.addWidget(page) - - def _on_complete_register(self) -> None: - """处理完成注册逻辑:设置用户名和密码。""" - username = (self.register_username.text() or "").strip() - password = self.register_password.text() or "" - confirm = self.register_confirm.text() or "" - - if not username or not password or not confirm: - QMessageBox.warning(self, "提示", "请填写完整信息") - return - - if not self.session_email: - QMessageBox.warning(self, "提示", "会话已过期,请重新注册") - self.stack.setCurrentIndex(1) - return - - # 设置用户名 - ok, msg = self.user_service.set_username(self.session_email, username) - if not ok: - QMessageBox.warning(self, "提示", msg) - return - - # 设置密码 - ok, msg = self.user_service.set_password(self.session_email, password, confirm) - if not ok: - QMessageBox.warning(self, "提示", msg) - return - - # 完成注册 - ok, msg = self.user_service.complete_registration(self.session_email) - if ok: - QMessageBox.information(self, "提示", "注册成功!请登录") - # 清空表单并跳转到登录页 - self.register_email.clear() - self.verify_code.clear() - self.register_username.clear() - self.register_password.clear() - self.register_confirm.clear() - self.session_email = None - self.stack.setCurrentIndex(0) - else: - QMessageBox.warning(self, "提示", msg) - - def _build_choice_page(self) -> None: - """构建选择页面:选择年级和题目数量。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - title = QLabel("选择题目难度和数量") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") - layout.addWidget(title) - - # 年级选择 - grade_label = QLabel("选择年级:") - layout.addWidget(grade_label) - - self.grade_group = QButtonGroup() - grade_layout = QHBoxLayout() - - self.primary_radio = QRadioButton("小学") - self.middle_radio = QRadioButton("初中") - self.high_radio = QRadioButton("高中") - - self.grade_group.addButton(self.primary_radio, 0) - self.grade_group.addButton(self.middle_radio, 1) - self.grade_group.addButton(self.high_radio, 2) - - grade_layout.addWidget(self.primary_radio) - grade_layout.addWidget(self.middle_radio) - grade_layout.addWidget(self.high_radio) - layout.addLayout(grade_layout) - - # 默认选择小学 - self.primary_radio.setChecked(True) - - # 题目数量选择 - count_label = QLabel("选择题目数量:") - layout.addWidget(count_label) - - self.question_count = QSpinBox() - self.question_count.setMinimum(10) - self.question_count.setMaximum(30) - self.question_count.setValue(15) - layout.addWidget(self.question_count) - - btn_row = QHBoxLayout() - self.btn_start_quiz = QPushButton("开始答题") - self.btn_change_password = QPushButton("修改密码") - self.btn_logout = QPushButton("退出登录") - btn_row.addWidget(self.btn_start_quiz) - btn_row.addWidget(self.btn_change_password) - btn_row.addWidget(self.btn_logout) - layout.addLayout(btn_row) - - self.btn_start_quiz.clicked.connect(self._on_start_quiz) - self.btn_change_password.clicked.connect(lambda: self.stack.setCurrentIndex(7)) - self.btn_logout.clicked.connect(self._on_logout) - - self.stack.addWidget(page) - - def _on_start_quiz(self) -> None: - """开始答题:生成题目并跳转到答题页面。""" - # 获取选择的年级 - grade_map = {0: 'primary', 1: 'middle', 2: 'high'} - grade = grade_map[self.grade_group.checkedId()] - count = self.question_count.value() - - try: - self.questions = self.question_service.generate_questions(grade, count) - self.current_question = 0 - self.user_answers = [] - self._show_current_question() - self.stack.setCurrentIndex(5) - except Exception as e: - QMessageBox.warning(self, "提示", f"生成题目失败:{str(e)}") - - def _on_logout(self) -> None: - """退出登录:清空会话并返回登录页。""" - self.session_email = None - self.login_identifier.clear() - self.login_password.clear() - QMessageBox.information(self, "提示", "已退出登录") - self.stack.setCurrentIndex(0) - - def _build_quiz_page(self) -> None: - """构建答题页面:显示题目和选项。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - # 进度显示 - self.progress_label = QLabel() - self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.progress_label.setStyleSheet("font-size: 16px; margin: 10px 0;") - layout.addWidget(self.progress_label) - - # 题目显示 - self.question_label = QLabel() - self.question_label.setAlignment(Qt.AlignmentFlag.AlignLeft) - self.question_label.setStyleSheet("font-size: 18px; margin: 20px 0; padding: 10px; border: 1px solid #ccc;") - self.question_label.setWordWrap(True) - layout.addWidget(self.question_label) - - # 选项 - self.option_group = QButtonGroup() - self.option_radios = [] - for i in range(4): - radio = QRadioButton() - radio.setStyleSheet("font-size: 16px; margin: 5px 0;") - self.option_group.addButton(radio, i) - self.option_radios.append(radio) - layout.addWidget(radio) - - # 按钮 - btn_row = QHBoxLayout() - self.btn_prev = QPushButton("上一题") - self.btn_next = QPushButton("下一题") - self.btn_submit = QPushButton("提交答案") - btn_row.addWidget(self.btn_prev) - btn_row.addWidget(self.btn_next) - btn_row.addWidget(self.btn_submit) - layout.addLayout(btn_row) - - self.btn_prev.clicked.connect(self._on_prev_question) - self.btn_next.clicked.connect(self._on_next_question) - self.btn_submit.clicked.connect(self._on_submit_quiz) - - self.stack.addWidget(page) - - def _show_current_question(self) -> None: - """显示当前题目。""" - if not hasattr(self, 'questions') or not self.questions: - return - - question = self.questions[self.current_question] - total = len(self.questions) - - # 更新进度 - self.progress_label.setText(f"第 {self.current_question + 1} 题 / 共 {total} 题") - - # 更新题目 - self.question_label.setText(question['stem']) - - # 更新选项 - for i, option in enumerate(question['options']): - self.option_radios[i].setText(f"{chr(65+i)}. {option}") - - # 恢复之前的选择 - if self.current_question < len(self.user_answers): - selected = self.user_answers[self.current_question] - if selected is not None: - self.option_radios[selected].setChecked(True) - else: - # 清空选择 - for radio in self.option_radios: - radio.setChecked(False) - - # 更新按钮状态 - self.btn_prev.setEnabled(self.current_question > 0) - self.btn_next.setEnabled(self.current_question < total - 1) - - def _on_prev_question(self) -> None: - """上一题。""" - self._save_current_answer() - if self.current_question > 0: - self.current_question -= 1 - self._show_current_question() - - def _on_next_question(self) -> None: - """下一题。""" - self._save_current_answer() - if self.current_question < len(self.questions) - 1: - self.current_question += 1 - self._show_current_question() - - def _save_current_answer(self) -> None: - """保存当前题目的答案。""" - selected = self.option_group.checkedId() - # 确保user_answers列表足够长 - while len(self.user_answers) <= self.current_question: - self.user_answers.append(None) - self.user_answers[self.current_question] = selected if selected >= 0 else None - - def _on_submit_quiz(self) -> None: - """提交答案并计算分数。""" - self._save_current_answer() - - # 检查是否有未答题目 - unanswered = [] - for i, answer in enumerate(self.user_answers): - if answer is None: - unanswered.append(i + 1) - - if unanswered: - reply = QMessageBox.question( - self, "提示", - f"还有第 {', '.join(map(str, unanswered))} 题未作答,确定要提交吗?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - if reply == QMessageBox.StandardButton.No: - return - - # 计算分数 - correct = 0 - total = len(self.questions) - for i, question in enumerate(self.questions): - if i < len(self.user_answers) and self.user_answers[i] == question['answer_index']: - correct += 1 - - self.score = correct - self.total_questions = total - self._show_result() - self.stack.setCurrentIndex(6) - - def _build_result_page(self) -> None: - """构建结果页面:显示分数和操作选项。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - - title = QLabel("答题结果") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") - layout.addWidget(title) - - self.score_label = QLabel() - self.score_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.score_label.setStyleSheet("font-size: 24px; margin: 20px 0; color: #2196F3;") - layout.addWidget(self.score_label) - - btn_row = QHBoxLayout() - self.btn_continue = QPushButton("继续做题") - self.btn_exit = QPushButton("退出") - btn_row.addWidget(self.btn_continue) - btn_row.addWidget(self.btn_exit) - layout.addLayout(btn_row) - - self.btn_continue.clicked.connect(lambda: self.stack.setCurrentIndex(4)) - self.btn_exit.clicked.connect(self._on_logout) - - self.stack.addWidget(page) - - def _show_result(self) -> None: - """显示答题结果。""" - if hasattr(self, 'score') and hasattr(self, 'total_questions'): - percentage = (self.score / self.total_questions) * 100 - self.score_label.setText( - f"您答对了 {self.score} 题,共 {self.total_questions} 题\n" - f"正确率:{percentage:.1f}%" - ) - - def _build_change_password_page(self) -> None: - """构建修改密码页面。""" - page = QWidget() - layout = QVBoxLayout(page) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - title = QLabel("修改密码") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") - layout.addWidget(title) - - self.old_password = QLineEdit() - self.old_password.setPlaceholderText("请输入原密码") - self.old_password.setEchoMode(QLineEdit.EchoMode.Password) - layout.addWidget(self.old_password) - - self.new_password = QLineEdit() - self.new_password.setPlaceholderText("请输入新密码(6-10位,包含大小写字母和数字)") - self.new_password.setEchoMode(QLineEdit.EchoMode.Password) - layout.addWidget(self.new_password) - - self.confirm_new_password = QLineEdit() - self.confirm_new_password.setPlaceholderText("请确认新密码") - self.confirm_new_password.setEchoMode(QLineEdit.EchoMode.Password) - layout.addWidget(self.confirm_new_password) - - btn_row = QHBoxLayout() - self.btn_change_pwd = QPushButton("修改密码") - self.btn_back_to_choice = QPushButton("返回") - btn_row.addWidget(self.btn_change_pwd) - btn_row.addWidget(self.btn_back_to_choice) - layout.addLayout(btn_row) - - self.btn_change_pwd.clicked.connect(self._on_change_password) - self.btn_back_to_choice.clicked.connect(lambda: self.stack.setCurrentIndex(4)) - - self.stack.addWidget(page) - - def _on_change_password(self) -> None: - """处理修改密码逻辑。""" - old_pwd = self.old_password.text() or "" - new_pwd = self.new_password.text() or "" - confirm_pwd = self.confirm_new_password.text() or "" - - if not old_pwd or not new_pwd or not confirm_pwd: - QMessageBox.warning(self, "提示", "请填写完整信息") - return - - if not self.session_email: - QMessageBox.warning(self, "提示", "会话已过期,请重新登录") - self.stack.setCurrentIndex(0) - return - - ok, msg = self.user_service.change_password(self.session_email, old_pwd, new_pwd, confirm_pwd) - if ok: - QMessageBox.information(self, "提示", "密码修改成功") - # 清空表单并返回选择页 - self.old_password.clear() - self.new_password.clear() - self.confirm_new_password.clear() - self.stack.setCurrentIndex(4) - else: +from PyQt6.QtWidgets import ( + QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QMessageBox, QRadioButton, QButtonGroup, QSpinBox +) +from PyQt6.QtCore import Qt + +from services.storage_service import StorageService +from services.user_service import UserService +from services.question_service import QuestionService + + +class MainWindow(QMainWindow): + """应用主窗口:组织各个页面并承载业务服务。""" + + def __init__(self) -> None: + """初始化主窗口并构建基础页面。""" + super().__init__() + self.setWindowTitle("数学学习软件") + self.setFixedSize(900, 600) + + # 业务服务初始化 + self.storage_service = StorageService() + self.session_email = None + self.user_service = UserService(self.storage_service) + self.question_service = QuestionService() + + # 页面容器 + self.stack = QStackedWidget() + self.setCentralWidget(self.stack) + + # 构建页面:登录、注册(邮箱/验证码/用户名密码)、选择、答题、结果、修改密码 + self._build_login_page() # index 0 + self._build_register_email_page() # index 1 + self._build_verify_page() # index 2 + self._build_set_credentials_page() # index 3 + self._build_choice_page() # index 4 + self._build_quiz_page() # index 5 + self._build_result_page() # index 6 + self._build_change_password_page() # index 7 + + # 默认显示登录页 + self.stack.setCurrentIndex(0) + + def _build_login_page(self) -> None: + """构建登录页面:支持用户名或邮箱登录。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("登录") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.login_identifier = QLineEdit() + self.login_identifier.setPlaceholderText("请输入邮箱或用户名") + layout.addWidget(self.login_identifier) + + self.login_password = QLineEdit() + self.login_password.setPlaceholderText("请输入密码") + self.login_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.login_password) + + btn_row = QHBoxLayout() + self.btn_login = QPushButton("登录") + self.btn_to_register = QPushButton("去注册") + btn_row.addWidget(self.btn_login) + btn_row.addWidget(self.btn_to_register) + layout.addLayout(btn_row) + + self.btn_login.clicked.connect(self._on_login) + self.btn_to_register.clicked.connect(lambda: self.stack.setCurrentIndex(1)) + + self.stack.addWidget(page) + + def _on_login(self) -> None: + """处理登录逻辑:根据输入自动识别邮箱或用户名进行登录,并在成功后进入选择页。""" + identifier = (self.login_identifier.text() or "").strip() + password = self.login_password.text() or "" + if not identifier or not password: + QMessageBox.warning(self, "提示", "账号与密码均不能为空") + return + # 判断是否为邮箱 + if "@" in identifier: + ok, msg = self.user_service.login(identifier, password) + if ok: + self.session_email = identifier + else: + QMessageBox.warning(self, "提示", msg or "登录失败") + return + else: + ok, msg = self.user_service.login_by_username(identifier, password) + if ok: + # 通过用户名获取邮箱,记录会话 + user = self.storage_service.get_user_by_username(identifier) + self.session_email = user.get('email') if user else None + else: + QMessageBox.warning(self, "提示", msg or "登录失败") + return + QMessageBox.information(self, "提示", "登录成功") + # 跳转到选择页 + self.stack.setCurrentIndex(4) + + def _build_register_email_page(self) -> None: + """构建注册第一步:邮箱输入页面。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("注册 - 邮箱验证") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.register_email = QLineEdit() + self.register_email.setPlaceholderText("请输入邮箱地址") + layout.addWidget(self.register_email) + + btn_row = QHBoxLayout() + self.btn_send_code = QPushButton("获取验证码") + self.btn_back_to_login = QPushButton("返回登录") + btn_row.addWidget(self.btn_send_code) + btn_row.addWidget(self.btn_back_to_login) + layout.addLayout(btn_row) + + self.btn_send_code.clicked.connect(self._on_send_code) + self.btn_back_to_login.clicked.connect(lambda: self.stack.setCurrentIndex(0)) + + self.stack.addWidget(page) + + def _on_send_code(self) -> None: + """处理发送验证码逻辑:验证邮箱格式并生成验证码。""" + email = (self.register_email.text() or "").strip() + if not email: + QMessageBox.warning(self, "提示", "请输入邮箱地址") + return + + ok, msg = self.user_service.request_registration(email) + if ok: + self.session_email = email + QMessageBox.information(self, "提示", msg) + # 跳转到验证码页面 + self.stack.setCurrentIndex(2) + else: + QMessageBox.warning(self, "提示", msg) + + def _build_verify_page(self) -> None: + """构建注册第二步:验证码验证页面。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("注册 - 验证码验证") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.verify_code = QLineEdit() + self.verify_code.setPlaceholderText("请输入6位验证码") + layout.addWidget(self.verify_code) + + btn_row = QHBoxLayout() + self.btn_verify = QPushButton("验证") + self.btn_back_to_email = QPushButton("返回上一步") + btn_row.addWidget(self.btn_verify) + btn_row.addWidget(self.btn_back_to_email) + layout.addLayout(btn_row) + + self.btn_verify.clicked.connect(self._on_verify_code) + self.btn_back_to_email.clicked.connect(lambda: self.stack.setCurrentIndex(1)) + + self.stack.addWidget(page) + + def _on_verify_code(self) -> None: + """处理验证码验证逻辑。""" + code = (self.verify_code.text() or "").strip() + if not code: + QMessageBox.warning(self, "提示", "请输入验证码") + return + + if not self.session_email: + QMessageBox.warning(self, "提示", "会话已过期,请重新注册") + self.stack.setCurrentIndex(1) + return + + ok, msg = self.user_service.verify_code(self.session_email, code) + if ok: + QMessageBox.information(self, "提示", msg) + # 跳转到设置用户名密码页面 + self.stack.setCurrentIndex(3) + else: + QMessageBox.warning(self, "提示", msg) + + def _build_set_credentials_page(self) -> None: + """构建注册第三步:设置用户名和密码页面。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("注册 - 设置用户名和密码") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.register_username = QLineEdit() + self.register_username.setPlaceholderText("请输入用户名(3-16位,字母数字下划线)") + layout.addWidget(self.register_username) + + self.register_password = QLineEdit() + self.register_password.setPlaceholderText("请输入密码(6-10位,包含大小写字母和数字)") + self.register_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.register_password) + + self.register_confirm = QLineEdit() + self.register_confirm.setPlaceholderText("请确认密码") + self.register_confirm.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.register_confirm) + + btn_row = QHBoxLayout() + self.btn_complete_register = QPushButton("完成注册") + self.btn_back_to_verify = QPushButton("返回上一步") + btn_row.addWidget(self.btn_complete_register) + btn_row.addWidget(self.btn_back_to_verify) + layout.addLayout(btn_row) + + self.btn_complete_register.clicked.connect(self._on_complete_register) + self.btn_back_to_verify.clicked.connect(lambda: self.stack.setCurrentIndex(2)) + + self.stack.addWidget(page) + + def _on_complete_register(self) -> None: + """处理完成注册逻辑:设置用户名和密码。""" + username = (self.register_username.text() or "").strip() + password = self.register_password.text() or "" + confirm = self.register_confirm.text() or "" + + if not username or not password or not confirm: + QMessageBox.warning(self, "提示", "请填写完整信息") + return + + if not self.session_email: + QMessageBox.warning(self, "提示", "会话已过期,请重新注册") + self.stack.setCurrentIndex(1) + return + + # 设置用户名 + ok, msg = self.user_service.set_username(self.session_email, username) + if not ok: + QMessageBox.warning(self, "提示", msg) + return + + # 设置密码 + ok, msg = self.user_service.set_password(self.session_email, password, confirm) + if not ok: + QMessageBox.warning(self, "提示", msg) + return + + # 完成注册 + ok, msg = self.user_service.complete_registration(self.session_email) + if ok: + QMessageBox.information(self, "提示", "注册成功!请登录") + # 清空表单并跳转到登录页 + self.register_email.clear() + self.verify_code.clear() + self.register_username.clear() + self.register_password.clear() + self.register_confirm.clear() + self.session_email = None + self.stack.setCurrentIndex(0) + else: + QMessageBox.warning(self, "提示", msg) + + def _build_choice_page(self) -> None: + """构建选择页面:选择年级和题目数量。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("选择题目难度和数量") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + # 年级选择 + grade_label = QLabel("选择年级:") + layout.addWidget(grade_label) + + self.grade_group = QButtonGroup() + grade_layout = QHBoxLayout() + + self.primary_radio = QRadioButton("小学") + self.middle_radio = QRadioButton("初中") + self.high_radio = QRadioButton("高中") + + self.grade_group.addButton(self.primary_radio, 0) + self.grade_group.addButton(self.middle_radio, 1) + self.grade_group.addButton(self.high_radio, 2) + + grade_layout.addWidget(self.primary_radio) + grade_layout.addWidget(self.middle_radio) + grade_layout.addWidget(self.high_radio) + layout.addLayout(grade_layout) + + # 默认选择小学 + self.primary_radio.setChecked(True) + + # 题目数量选择 + count_label = QLabel("选择题目数量:") + layout.addWidget(count_label) + + self.question_count = QSpinBox() + self.question_count.setMinimum(10) + self.question_count.setMaximum(30) + self.question_count.setValue(15) + layout.addWidget(self.question_count) + + btn_row = QHBoxLayout() + self.btn_start_quiz = QPushButton("开始答题") + self.btn_change_password = QPushButton("修改密码") + self.btn_logout = QPushButton("退出登录") + btn_row.addWidget(self.btn_start_quiz) + btn_row.addWidget(self.btn_change_password) + btn_row.addWidget(self.btn_logout) + layout.addLayout(btn_row) + + self.btn_start_quiz.clicked.connect(self._on_start_quiz) + self.btn_change_password.clicked.connect(lambda: self.stack.setCurrentIndex(7)) + self.btn_logout.clicked.connect(self._on_logout) + + self.stack.addWidget(page) + + def _on_start_quiz(self) -> None: + """开始答题:生成题目并跳转到答题页面。""" + # 获取选择的年级 + grade_map = {0: 'primary', 1: 'middle', 2: 'high'} + grade = grade_map[self.grade_group.checkedId()] + count = self.question_count.value() + + try: + self.questions = self.question_service.generate_questions(grade, count) + self.current_question = 0 + self.user_answers = [] + self._show_current_question() + self.stack.setCurrentIndex(5) + except Exception as e: + QMessageBox.warning(self, "提示", f"生成题目失败:{str(e)}") + + def _on_logout(self) -> None: + """退出登录:清空会话并返回登录页。""" + self.session_email = None + self.login_identifier.clear() + self.login_password.clear() + QMessageBox.information(self, "提示", "已退出登录") + self.stack.setCurrentIndex(0) + + def _build_quiz_page(self) -> None: + """构建答题页面:显示题目和选项。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + # 进度显示 + self.progress_label = QLabel() + self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.progress_label.setStyleSheet("font-size: 16px; margin: 10px 0;") + layout.addWidget(self.progress_label) + + # 题目显示 + self.question_label = QLabel() + self.question_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.question_label.setStyleSheet("font-size: 18px; margin: 20px 0; padding: 10px; border: 1px solid #ccc;") + self.question_label.setWordWrap(True) + layout.addWidget(self.question_label) + + # 选项 + self.option_group = QButtonGroup() + self.option_radios = [] + for i in range(4): + radio = QRadioButton() + radio.setStyleSheet("font-size: 16px; margin: 5px 0;") + self.option_group.addButton(radio, i) + self.option_radios.append(radio) + layout.addWidget(radio) + + # 按钮 + btn_row = QHBoxLayout() + self.btn_prev = QPushButton("上一题") + self.btn_next = QPushButton("下一题") + self.btn_submit = QPushButton("提交答案") + btn_row.addWidget(self.btn_prev) + btn_row.addWidget(self.btn_next) + btn_row.addWidget(self.btn_submit) + layout.addLayout(btn_row) + + self.btn_prev.clicked.connect(self._on_prev_question) + self.btn_next.clicked.connect(self._on_next_question) + self.btn_submit.clicked.connect(self._on_submit_quiz) + + self.stack.addWidget(page) + + def _show_current_question(self) -> None: + """显示当前题目。""" + if not hasattr(self, 'questions') or not self.questions: + return + + question = self.questions[self.current_question] + total = len(self.questions) + + # 更新进度 + self.progress_label.setText(f"第 {self.current_question + 1} 题 / 共 {total} 题") + + # 更新题目 + self.question_label.setText(question['stem']) + + # 更新选项 + for i, option in enumerate(question['options']): + self.option_radios[i].setText(f"{chr(65+i)}. {option}") + + # 恢复之前的选择 + if self.current_question < len(self.user_answers): + selected = self.user_answers[self.current_question] + if selected is not None: + self.option_radios[selected].setChecked(True) + else: + # 清空选择 + for radio in self.option_radios: + radio.setChecked(False) + + # 更新按钮状态 + self.btn_prev.setEnabled(self.current_question > 0) + self.btn_next.setEnabled(self.current_question < total - 1) + + def _on_prev_question(self) -> None: + """上一题。""" + self._save_current_answer() + if self.current_question > 0: + self.current_question -= 1 + self._show_current_question() + + def _on_next_question(self) -> None: + """下一题。""" + self._save_current_answer() + if self.current_question < len(self.questions) - 1: + self.current_question += 1 + self._show_current_question() + + def _save_current_answer(self) -> None: + """保存当前题目的答案。""" + selected = self.option_group.checkedId() + # 确保user_answers列表足够长 + while len(self.user_answers) <= self.current_question: + self.user_answers.append(None) + self.user_answers[self.current_question] = selected if selected >= 0 else None + + def _on_submit_quiz(self) -> None: + """提交答案并计算分数。""" + self._save_current_answer() + + # 检查是否有未答题目 + unanswered = [] + for i, answer in enumerate(self.user_answers): + if answer is None: + unanswered.append(i + 1) + + if unanswered: + reply = QMessageBox.question( + self, "提示", + f"还有第 {', '.join(map(str, unanswered))} 题未作答,确定要提交吗?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.No: + return + + # 计算分数 + correct = 0 + total = len(self.questions) + for i, question in enumerate(self.questions): + if i < len(self.user_answers) and self.user_answers[i] == question['answer_index']: + correct += 1 + + self.score = correct + self.total_questions = total + self._show_result() + self.stack.setCurrentIndex(6) + + def _build_result_page(self) -> None: + """构建结果页面:显示分数和操作选项。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + title = QLabel("答题结果") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.score_label = QLabel() + self.score_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.score_label.setStyleSheet("font-size: 24px; margin: 20px 0; color: #2196F3;") + layout.addWidget(self.score_label) + + btn_row = QHBoxLayout() + self.btn_continue = QPushButton("继续做题") + self.btn_exit = QPushButton("退出") + btn_row.addWidget(self.btn_continue) + btn_row.addWidget(self.btn_exit) + layout.addLayout(btn_row) + + self.btn_continue.clicked.connect(lambda: self.stack.setCurrentIndex(4)) + self.btn_exit.clicked.connect(self._on_logout) + + self.stack.addWidget(page) + + def _show_result(self) -> None: + """显示答题结果。""" + if hasattr(self, 'score') and hasattr(self, 'total_questions'): + percentage = (self.score / self.total_questions) * 100 + self.score_label.setText( + f"您答对了 {self.score} 题,共 {self.total_questions} 题\n" + f"正确率:{percentage:.1f}%" + ) + + def _build_change_password_page(self) -> None: + """构建修改密码页面。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("修改密码") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.old_password = QLineEdit() + self.old_password.setPlaceholderText("请输入原密码") + self.old_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.old_password) + + self.new_password = QLineEdit() + self.new_password.setPlaceholderText("请输入新密码(6-10位,包含大小写字母和数字)") + self.new_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.new_password) + + self.confirm_new_password = QLineEdit() + self.confirm_new_password.setPlaceholderText("请确认新密码") + self.confirm_new_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.confirm_new_password) + + btn_row = QHBoxLayout() + self.btn_change_pwd = QPushButton("修改密码") + self.btn_back_to_choice = QPushButton("返回") + btn_row.addWidget(self.btn_change_pwd) + btn_row.addWidget(self.btn_back_to_choice) + layout.addLayout(btn_row) + + self.btn_change_pwd.clicked.connect(self._on_change_password) + self.btn_back_to_choice.clicked.connect(lambda: self.stack.setCurrentIndex(4)) + + self.stack.addWidget(page) + + def _on_change_password(self) -> None: + """处理修改密码逻辑。""" + old_pwd = self.old_password.text() or "" + new_pwd = self.new_password.text() or "" + confirm_pwd = self.confirm_new_password.text() or "" + + if not old_pwd or not new_pwd or not confirm_pwd: + QMessageBox.warning(self, "提示", "请填写完整信息") + return + + if not self.session_email: + QMessageBox.warning(self, "提示", "会话已过期,请重新登录") + self.stack.setCurrentIndex(0) + return + + ok, msg = self.user_service.change_password(self.session_email, old_pwd, new_pwd, confirm_pwd) + if ok: + QMessageBox.information(self, "提示", "密码修改成功") + # 清空表单并返回选择页 + self.old_password.clear() + self.new_password.clear() + self.confirm_new_password.clear() + self.stack.setCurrentIndex(4) + else: QMessageBox.warning(self, "提示", msg) \ No newline at end of file