parent
d0499bab01
commit
0e10ec4b5a
@ -0,0 +1,196 @@
|
||||
# src/app.py
|
||||
import customtkinter as ctk
|
||||
import random
|
||||
import string
|
||||
from user_manager import UserManager, send_verification_email
|
||||
from quiz_generator import get_generator
|
||||
|
||||
class MathApp(ctk.CTk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("小初高数学学习软件")
|
||||
self.geometry("500x450")
|
||||
ctk.set_appearance_mode("System")
|
||||
|
||||
self.user_manager = UserManager()
|
||||
self.current_user = None
|
||||
|
||||
container = ctk.CTkFrame(self)
|
||||
container.pack(side="top", fill="both", expand=True)
|
||||
container.grid_rowconfigure(0, weight=1)
|
||||
container.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.frames = {}
|
||||
# 还原:只包含基础的五个页面
|
||||
for F in (LoginPage, RegisterPage, MainPage, QuizPage, ScorePage):
|
||||
page_name = F.__name__
|
||||
frame = F(parent=container, controller=self)
|
||||
self.frames[page_name] = frame
|
||||
frame.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
self.show_frame("LoginPage")
|
||||
|
||||
def show_frame(self, page_name):
|
||||
frame = self.frames[page_name]
|
||||
frame.tkraise()
|
||||
|
||||
class LoginPage(ctk.CTkFrame):
|
||||
def __init__(self, parent, controller):
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
ctk.CTkLabel(self, text="登录", font=ctk.CTkFont(size=24, weight="bold")).pack(pady=20)
|
||||
self.email_entry = ctk.CTkEntry(self, placeholder_text="邮箱", width=200)
|
||||
self.email_entry.pack(pady=10)
|
||||
self.password_entry = ctk.CTkEntry(self, placeholder_text="密码", show="*", width=200)
|
||||
self.password_entry.pack(pady=10)
|
||||
self.error_label = ctk.CTkLabel(self, text="", text_color="red")
|
||||
self.error_label.pack(pady=5)
|
||||
ctk.CTkButton(self, text="登录", command=self.login).pack(pady=10)
|
||||
ctk.CTkButton(self, text="没有账户?去注册", fg_color="transparent", command=lambda: controller.show_frame("RegisterPage")).pack()
|
||||
|
||||
def login(self):
|
||||
email, password = self.email_entry.get(), self.password_entry.get()
|
||||
if self.controller.user_manager.validate_login(email, password):
|
||||
self.controller.current_user = email
|
||||
self.error_label.configure(text="")
|
||||
self.controller.show_frame("MainPage")
|
||||
else:
|
||||
self.error_label.configure(text="邮箱或密码错误")
|
||||
|
||||
class RegisterPage(ctk.CTkFrame):
|
||||
def __init__(self, parent, controller):
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
self.code = ""
|
||||
self.cooldown_time = 0
|
||||
ctk.CTkLabel(self, text="注册新账户", font=ctk.CTkFont(size=24, weight="bold")).pack(pady=10)
|
||||
self.email_entry = ctk.CTkEntry(self, placeholder_text="邮箱", width=250)
|
||||
self.email_entry.pack(pady=5)
|
||||
self.send_code_button = ctk.CTkButton(self, text="发送验证码", command=self.send_code, width=250)
|
||||
self.send_code_button.pack(pady=5)
|
||||
self.code_entry = ctk.CTkEntry(self, placeholder_text="输入收到的邮件验证码", width=250)
|
||||
self.code_entry.pack(pady=5)
|
||||
self.password_entry = ctk.CTkEntry(self, placeholder_text="密码 (6-10位,含大小写和数字)", show="*", width=250)
|
||||
self.password_entry.pack(pady=5)
|
||||
self.confirm_password_entry = ctk.CTkEntry(self, placeholder_text="确认密码", show="*", width=250)
|
||||
self.confirm_password_entry.pack(pady=5)
|
||||
self.info_label = ctk.CTkLabel(self, text="", text_color="red")
|
||||
self.info_label.pack(pady=5)
|
||||
ctk.CTkButton(self, text="注册", command=self.register).pack(pady=10)
|
||||
ctk.CTkButton(self, text="返回登录", fg_color="transparent", command=lambda: controller.show_frame("LoginPage")).pack()
|
||||
|
||||
def update_cooldown(self):
|
||||
if self.cooldown_time > 0:
|
||||
self.send_code_button.configure(text=f"{self.cooldown_time}秒后可重试")
|
||||
self.cooldown_time -= 1
|
||||
self.after(1000, self.update_cooldown)
|
||||
else:
|
||||
self.send_code_button.configure(text="发送验证码", state="normal")
|
||||
|
||||
def send_code(self):
|
||||
if self.cooldown_time > 0: return
|
||||
email = self.email_entry.get().strip()
|
||||
if not email:
|
||||
self.info_label.configure(text="请输入邮箱地址。", text_color="red")
|
||||
return
|
||||
if self.controller.user_manager.is_email_registered(email):
|
||||
self.info_label.configure(text="该邮箱已被注册。", text_color="red")
|
||||
return
|
||||
|
||||
self.code = ''.join(random.choices(string.digits, k=6))
|
||||
self.info_label.configure(text="正在发送邮件,请稍候...", text_color="gray")
|
||||
success = send_verification_email(email, self.code)
|
||||
|
||||
if success:
|
||||
self.info_label.configure(text="邮件验证码已发送至您的邮箱,请查收。", text_color="green")
|
||||
self.cooldown_time = 60
|
||||
self.send_code_button.configure(state="disabled")
|
||||
self.update_cooldown()
|
||||
else:
|
||||
self.info_label.configure(text="邮件发送失败,请检查邮箱或网络。", text_color="red")
|
||||
self.code = ""
|
||||
|
||||
def register(self):
|
||||
email, user_code, password, confirm_password = self.email_entry.get().strip(), self.code_entry.get().strip(), self.password_entry.get(), self.confirm_password_entry.get()
|
||||
if not self.code or user_code != self.code:
|
||||
self.info_label.configure(text="邮件验证码错误", text_color="red")
|
||||
return
|
||||
if password != confirm_password:
|
||||
self.info_label.configure(text="两次输入的密码不一致", text_color="red")
|
||||
return
|
||||
if not self.controller.user_manager.validate_password_format(password):
|
||||
self.info_label.configure(text="密码格式不符合要求", text_color="red")
|
||||
return
|
||||
success, msg = self.controller.user_manager.register_user(email, password)
|
||||
self.info_label.configure(text=msg, text_color="green" if success else "red")
|
||||
if success: self.code = ""
|
||||
|
||||
class MainPage(ctk.CTkFrame):
|
||||
def __init__(self, parent, controller):
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
ctk.CTkLabel(self, text="选择学习阶段", font=ctk.CTkFont(size=24, weight="bold")).pack(pady=20)
|
||||
ctk.CTkButton(self, text="小学", command=lambda: self.start_quiz("小学"), height=40, width=150).pack(pady=10)
|
||||
ctk.CTkButton(self, text="初中", command=lambda: self.start_quiz("初中"), height=40, width=150).pack(pady=10)
|
||||
ctk.CTkButton(self, text="高中", command=lambda: self.start_quiz("高中"), height=40, width=150).pack(pady=10)
|
||||
ctk.CTkButton(self, text="退出登录", fg_color="transparent", command=lambda: controller.show_frame("LoginPage")).pack(side="bottom", pady=20)
|
||||
|
||||
def start_quiz(self, level):
|
||||
dialog = ctk.CTkInputDialog(text="请输入题目数量 (1-20):", title="设置题目数量")
|
||||
num_str = dialog.get_input()
|
||||
if num_str:
|
||||
try:
|
||||
num = int(num_str)
|
||||
if 1 <= num <= 20:
|
||||
self.controller.frames["QuizPage"].setup_quiz(level, num)
|
||||
self.controller.show_frame("QuizPage")
|
||||
except ValueError: pass
|
||||
|
||||
class QuizPage(ctk.CTkFrame):
|
||||
def __init__(self, parent, controller):
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
self.question_label = ctk.CTkLabel(self, text="问题", font=ctk.CTkFont(size=18))
|
||||
self.question_label.pack(pady=20, padx=20)
|
||||
self.radio_var = ctk.IntVar(value=-1)
|
||||
self.radio_buttons = []
|
||||
for i in range(4):
|
||||
rb = ctk.CTkRadioButton(self, text="", variable=self.radio_var, value=i, font=ctk.CTkFont(size=14))
|
||||
rb.pack(anchor="w", padx=100, pady=5)
|
||||
self.radio_buttons.append(rb)
|
||||
self.submit_button = ctk.CTkButton(self, text="下一题", command=self.next_question)
|
||||
self.submit_button.pack(pady=20)
|
||||
def setup_quiz(self, level, num_questions):
|
||||
self.questions = get_generator(level).generate_bunch(num_questions)
|
||||
self.current_question_index = 0
|
||||
self.user_answers = []
|
||||
self.display_question()
|
||||
def display_question(self):
|
||||
self.radio_var.set(-1)
|
||||
q = self.questions[self.current_question_index]
|
||||
self.question_label.configure(text=f"{self.current_question_index + 1}. {q.text} = ?")
|
||||
for i, option in enumerate(q.options): self.radio_buttons[i].configure(text=str(option))
|
||||
self.submit_button.configure(text="提交试卷" if self.current_question_index == len(self.questions) - 1 else "下一题")
|
||||
def next_question(self):
|
||||
if self.radio_var.get() == -1: return
|
||||
self.user_answers.append(self.radio_var.get())
|
||||
self.current_question_index += 1
|
||||
if self.current_question_index < len(self.questions): self.display_question()
|
||||
else: self.calculate_score()
|
||||
def calculate_score(self):
|
||||
correct_count = sum(1 for i, q in enumerate(self.questions) if self.user_answers[i] == q.correct_answer_index)
|
||||
total = len(self.questions)
|
||||
score_percent = (correct_count / total) * 100
|
||||
self.controller.frames["ScorePage"].show_score(correct_count, total, score_percent)
|
||||
self.controller.show_frame("ScorePage")
|
||||
|
||||
class ScorePage(ctk.CTkFrame):
|
||||
def __init__(self, parent, controller):
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
self.score_label = ctk.CTkLabel(self, text="", font=ctk.CTkFont(size=24))
|
||||
self.score_label.pack(pady=40, padx=20)
|
||||
ctk.CTkButton(self, text="继续做题", command=lambda: controller.show_frame("MainPage")).pack(pady=10)
|
||||
ctk.CTkButton(self, text="退出程序", command=controller.quit).pack(pady=10)
|
||||
def show_score(self, correct, total, percent):
|
||||
self.score_label.configure(text=f"答对了 {correct} / {total} 题\n\n得分: {percent:.2f}%")
|
||||
@ -0,0 +1,5 @@
|
||||
from app import MathApp
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MathApp()
|
||||
app.mainloop()
|
||||
@ -0,0 +1,96 @@
|
||||
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# 安全的表达式求值函数
|
||||
def safe_eval(expression):
|
||||
try:
|
||||
return eval(expression, {"__builtins__": None}, {})
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
class Question:
|
||||
def __init__(self, text, options, correct_answer_index):
|
||||
self.text = text
|
||||
self.options = options
|
||||
self.correct_answer_index = correct_answer_index
|
||||
|
||||
class QuestionGenerator(ABC):
|
||||
def generate_bunch(self, count):
|
||||
questions = []
|
||||
generated_texts = set()
|
||||
while len(questions) < count:
|
||||
text, answer = self._create_question_and_answer()
|
||||
if text not in generated_texts:
|
||||
options = self._generate_options(answer)
|
||||
correct_index = options.index(answer)
|
||||
|
||||
questions.append(Question(text, options, correct_index))
|
||||
generated_texts.add(text)
|
||||
return questions
|
||||
|
||||
def _generate_options(self, correct_answer):
|
||||
options = {correct_answer}
|
||||
while len(options) < 4:
|
||||
offset = random.randint(-10, 10)
|
||||
if offset == 0:
|
||||
offset = random.choice([-1, 1]) * 10
|
||||
|
||||
distractor = correct_answer + offset
|
||||
options.add(distractor)
|
||||
|
||||
option_list = list(options)
|
||||
random.shuffle(option_list)
|
||||
return option_list
|
||||
|
||||
@abstractmethod
|
||||
def _create_question_and_answer(self):
|
||||
pass
|
||||
|
||||
class PrimarySchoolGenerator(QuestionGenerator):
|
||||
def _create_question_and_answer(self):
|
||||
ops = ['+', '-', '*', '/']
|
||||
op = random.choice(ops)
|
||||
|
||||
if op == '+':
|
||||
num1, num2 = random.randint(1, 100), random.randint(1, 100)
|
||||
expr = f"{num1} + {num2}"
|
||||
elif op == '-':
|
||||
num1, num2 = random.randint(1, 100), random.randint(1, 100)
|
||||
if num1 < num2: num1, num2 = num2, num1
|
||||
expr = f"{num1} - {num2}"
|
||||
elif op == '*':
|
||||
num1, num2 = random.randint(1, 20), random.randint(1, 20)
|
||||
expr = f"{num1} * {num2}"
|
||||
else: # op == '/'
|
||||
divisor = random.randint(2, 10)
|
||||
answer = random.randint(2, 20)
|
||||
dividend = divisor * answer
|
||||
expr = f"{dividend} / {divisor}"
|
||||
|
||||
return expr, int(safe_eval(expr))
|
||||
|
||||
class MiddleSchoolGenerator(PrimarySchoolGenerator):
|
||||
def _create_question_and_answer(self):
|
||||
if random.choice([True, False]):
|
||||
return super()._create_question_and_answer()
|
||||
else:
|
||||
base = random.randint(2, 10)
|
||||
expr = f"{base}**2"
|
||||
return expr, int(safe_eval(expr))
|
||||
|
||||
class HighSchoolGenerator(MiddleSchoolGenerator):
|
||||
def _create_question_and_answer(self):
|
||||
# 简化版高中题,可扩展为三角函数等
|
||||
return super()._create_question_and_answer()
|
||||
|
||||
def get_generator(level):
|
||||
generators = {
|
||||
"小学": PrimarySchoolGenerator,
|
||||
"初中": MiddleSchoolGenerator,
|
||||
"高中": HighSchoolGenerator,
|
||||
}
|
||||
generator_class = generators.get(level)
|
||||
if generator_class:
|
||||
return generator_class()
|
||||
raise ValueError("无效的等级")
|
||||
Loading…
Reference in new issue