v3.0 #1

Merged
hnu202326010326 merged 4 commits from develop into main 4 months ago

@ -1,2 +0,0 @@
# double_program

@ -0,0 +1,6 @@
**结对编程项目**
操作系统: Windows11 22H4
编码语言: python3.8
中文编码格式: UTF-8
个人项目参考: 张勇进的个人项目

@ -0,0 +1,790 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
单文件桌面应用小初高数学练习含注册/邮箱验证码密码规则题目自动生成
修改点
- 每道题操作数数量为 1-5随机操作数取值 1-100
- 登录界面支持 使用 用户名 邮箱 登录
- 其它功能保留注册邮箱验证码激活并设置密码修改密码选择学段与题量自动生成题目逐题答题评分
注意
- 请替换 SMTP 配置为有效账户或在无法发送邮件时调整 send_verification_email 为测试模式
- Python 3.8+
"""
import tkinter as tk
from tkinter import messagebox, simpledialog, scrolledtext
import json
import os
import secrets
import hashlib
import binascii
import random
import re
import math
import threading
from datetime import datetime, timedelta
import smtplib
from email.mime.text import MIMEText
# ---------------- 配置 ----------------
USERS_FILE = "users.json"
QUESTION_FILES = {
"小学": "questions_primary.json",
"初中": "questions_middle.json",
"高中": "questions_high.json",
}
LOG_FILE = "app_log.txt"
# SMTP 配置 —— 请替换为可用的 SMTP 服务账号/密码/端口
SMTP_SERVER = "smtp.126.com"
SMTP_PORT = 25
SENDER_EMAIL = "hape233@126.com" # <-- 请替换
SENDER_PASSWORD = "ZKcbV37TdRwgnsZC" # <-- 请替换或使用授权码
CODE_TTL_SECONDS = 300 # 验证码有效期 5 分钟
SEND_COOLDOWN = 60 # 发送按钮冷却 60 秒
MAX_QUESTIONS = 20 # 题目上限
DEFAULT_QUESTIONS = 3 # 默认题目数
WINDOW_W = 820
WINDOW_H = 620
# 题目生成参数
MIN_OPERANDS = 1
MAX_OPERANDS = 5
OPERAND_MIN = 1
OPERAND_MAX = 100
# --------------------------------------
# ----------------- 辅助与存储 -----------------
def log(msg: str):
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"{datetime.utcnow().isoformat()} {msg}\n")
def ensure_files_exist():
# users file
if not os.path.exists(USERS_FILE):
with open(USERS_FILE, "w", encoding="utf-8") as f:
json.dump([], f, ensure_ascii=False, indent=2)
# question files
for k, p in QUESTION_FILES.items():
if not os.path.exists(p):
with open(p, "w", encoding="utf-8") as f:
json.dump([], f, ensure_ascii=False, indent=2)
# User store
class UserStore:
def __init__(self, path=USERS_FILE):
self.path = path
self._load()
def _load(self):
try:
with open(self.path, "r", encoding="utf-8") as f:
self.users = json.load(f)
except Exception:
self.users = []
def _save(self):
with open(self.path, "w", encoding="utf-8") as f:
json.dump(self.users, f, ensure_ascii=False, indent=2)
def find_by_email(self, email: str):
for u in self.users:
if u.get("email") == email:
return u
return None
def find_by_username(self, username: str):
for u in self.users:
if u.get("username") == username:
return u
return None
def find(self, key: str):
# try email first, then username
u = self.find_by_email(key)
if u:
return u
return self.find_by_username(key)
def add_or_update(self, user: dict):
existing = self.find_by_email(user["email"])
if existing:
existing.update(user)
else:
self.users.append(user)
self._save()
# Question store
class QuestionStore:
def __init__(self, files_map=QUESTION_FILES):
self.files_map = files_map
def load(self, grade: str):
p = self.files_map.get(grade)
try:
with open(p, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return []
def save(self, grade: str, qs):
p = self.files_map.get(grade)
with open(p, "w", encoding="utf-8") as f:
json.dump(qs, f, ensure_ascii=False, indent=2)
# ----------------- 验证码管理 & 密码哈希 -----------------
EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
def is_valid_email(e: str) -> bool:
return bool(EMAIL_RE.match(e or ""))
class VerificationManager:
def __init__(self):
self.store = {} # email -> (code, expiry)
def gen(self, email):
code = f"{random.randint(100000, 999999)}"
expiry = datetime.utcnow() + timedelta(seconds=CODE_TTL_SECONDS)
self.store[email] = (code, expiry)
return code
def verify(self, email, code):
rec = self.store.get(email)
if not rec:
return False, "未发送验证码"
real, expiry = rec
if datetime.utcnow() > expiry:
return False, "验证码已过期"
if not secrets.compare_digest(real, code):
return False, "验证码不匹配"
# 删除以防二次使用
del self.store[email]
return True, "验证成功"
verif_mgr = VerificationManager()
def hash_password(password: str, salt: bytes = None):
if salt is None:
salt = secrets.token_bytes(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000)
return binascii.hexlify(dk).decode("utf-8"), binascii.hexlify(salt).decode("utf-8")
def verify_password_hash(stored_hash, stored_salt, attempt):
try:
salt = binascii.unhexlify(stored_salt)
at_hash, _ = hash_password(attempt, salt)
return secrets.compare_digest(at_hash, stored_hash)
except Exception:
return False
# ----------------- SMTP 发送 -----------------
def send_verification_email(target_email: str, code: str):
"""同步发送;上层在线程中调用"""
msg = MIMEText(f"您的注册码6位{code}\n有效期 {CODE_TTL_SECONDS//60} 分钟。")
msg['Subject'] = "注册码"
msg['From'] = SENDER_EMAIL
msg['To'] = target_email
try:
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=30)
try:
server.starttls()
except Exception:
pass
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.sendmail(SENDER_EMAIL, [target_email], msg.as_string())
try:
server.quit()
except Exception:
server.close()
log(f"Sent code {code} to {target_email}")
return True, "发送成功"
except Exception as e:
try:
server.close()
except:
pass
log(f"Failed send code to {target_email}: {e}")
return False, str(e)
# ----------------- 题目生成1~5 个操作数,操作数范围 1~100 -----------------
def gen_id(prefix: str) -> str:
return f"{prefix}_{secrets.token_hex(4)}"
def mk_options_from_number(correct_val, kind="int"):
# produce 4 unique options including correct
opts = []
if kind == "int":
corr = int(round(correct_val))
opts = [str(corr)]
attempts = 0
while len(opts) < 4 and attempts < 100:
delta = random.choice([-10, -6, -3, -2, -1, 1, 2, 3, 5, 8, 10])
candidate = corr + delta
if str(candidate) not in opts:
opts.append(str(candidate))
attempts += 1
while len(opts) < 4:
opts.append(str(corr + random.randint(11, 50)))
random.shuffle(opts)
return opts, opts.index(str(corr))
else:
# float
corr = float(correct_val)
corr_s = f"{corr:.3f}"
opts = [corr_s]
attempts = 0
while len(opts) < 4 and attempts < 200:
delta = random.choice([-0.8, -0.5, -0.3, -0.2, -0.1, 0.1, 0.2, 0.3, 0.5, 0.8])
cand = corr + delta
s = f"{cand:.3f}"
if s not in opts:
opts.append(s)
attempts += 1
while len(opts) < 4:
opts.append(f"{(corr + random.uniform(1.0,3.0)):.3f}")
random.shuffle(opts)
return opts, opts.index(corr_s)
# 安全评估:表达式由程序生成,使用受限 eval 环境评估
SAFE_EVAL_ENV = {"__builtins__": None, "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos, "tan": math.tan, "pi": math.pi}
def safe_eval(expr: str):
# 只包含数字、运算符、sqrt, sin, cos, tan, (, ), ^ 替换为 **
expr = expr.replace("^", "**")
return eval(expr, SAFE_EVAL_ENV, {})
# 小学题1~5 个操作数,运算符 + - * /,可能带一对括号
def generate_primary_question():
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS) # number of operands
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
# Build expression, maybe with parentheses
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if n_ops >= 2 and random.choice([True, False]):
idx = random.randint(0, n_ops - 2)
parts = []
for i in range(n_ops):
p = str(nums[i])
if i == idx:
p = "(" + p
if i == idx + 1:
p = p + ")"
parts.append(p)
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
# evaluate and prefer integer
val = None
for _ in range(60):
try:
v = safe_eval(expr)
if v is None or isinstance(v, complex):
raise ValueError
if isinstance(v, float):
v = int(round(v))
val = v
break
except Exception:
# regenerate simpler
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if val is None:
val = 0
opts, idx = mk_options_from_number(val, kind="int")
return {"id": gen_id("pri"), "grade": "小学", "stem": f"{expr} = ?", "options": opts, "answer_index": idx}
# 初中题:至少包含平方或开根号 sqrt(...)
def generate_middle_question():
use_sqrt = random.choice([True, False])
use_square = not use_sqrt if random.random() < 0.3 else random.choice([True, False])
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
if use_sqrt:
# try to use perfect square inside sometimes
inside = random.randint(1, 50)
if random.choice([True, False]):
nums[idx] = f"sqrt({inside*inside})"
else:
nums[idx] = f"sqrt({nums[idx]})"
else:
nums[idx] = f"({nums[idx]}**2)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
val = None
for _ in range(60):
try:
v = safe_eval(expr)
if v is None or isinstance(v, complex):
raise ValueError
val = v
break
except Exception:
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
if random.choice([True, False]):
nums[idx] = f"sqrt({nums[idx]*nums[idx]})"
else:
nums[idx] = f"({nums[idx]}**2)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if val is None:
val = 0
if isinstance(val, float) and abs(val - round(val)) > 1e-9:
opts, idx = mk_options_from_number(val, kind="float")
else:
opts, idx = mk_options_from_number(val, kind="int")
return {"id": gen_id("mid"), "grade": "初中", "stem": f"{expr} = ?", "options": opts, "answer_index": idx}
# 高中题:至少包含 sin/cos/tan使用角度并可包含 1~5 个操作数
def generate_high_question():
func = random.choice(["sin", "cos", "tan"])
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
angle = random.choice([0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330])
nums[idx] = f"{func}(pi*{angle}/180)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
val = None
for _ in range(60):
try:
v = safe_eval(expr)
if v is None or isinstance(v, complex):
raise ValueError
val = v
break
except Exception:
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
angle = random.choice([30,45,60,90,120])
func = random.choice(["sin", "cos", "tan"])
nums[idx] = f"{func}(pi*{angle}/180)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if val is None:
val = 0.0
opts, idx = mk_options_from_number(val, kind="float")
stem = f"{expr} = ? (保留3位小数)"
return {"id": gen_id("high"), "grade": "高中", "stem": stem, "options": opts, "answer_index": idx}
# Ensure enough questions in file; auto-generate and append if insufficient
def ensure_questions_for_grade(grade: str, needed: int, qstore: QuestionStore):
qs = qstore.load(grade)
if len(qs) >= needed:
return qs
to_gen = needed - len(qs)
gen = []
for _ in range(to_gen):
if grade == "小学":
gen.append(generate_primary_question())
elif grade == "初中":
gen.append(generate_middle_question())
else:
gen.append(generate_high_question())
qs.extend(gen)
qstore.save(grade, qs)
log(f"Auto-generated {len(gen)} questions for {grade}")
return qs
# ----------------- 服务逻辑(注册/激活/登录/改密) -----------------
class AuthService:
def __init__(self, store: UserStore):
self.store = store
def send_code(self, email: str):
if not is_valid_email(email):
return False, "邮箱格式不正确"
u = self.store.find_by_email(email)
if u and u.get("activated"):
return False, "该邮箱已注册并激活"
code = verif_mgr.gen(email)
ok, msg = send_verification_email(email, code)
return ok, msg
def register_with_code(self, email: str, code: str, username: str, password: str):
if not is_valid_email(email):
return False, "邮箱格式不正确"
if not username:
return False, "用户名不能为空"
# check username uniqueness
existing_user = self.store.find_by_username(username)
if existing_user and existing_user.get("email") != email:
return False, "用户名已被占用"
if not (6 <= len(password) <= 10 and any(c.isupper() for c in password) and any(c.islower() for c in password) and any(c.isdigit() for c in password)):
return False, "密码规则不满足6-10位含大小写字母和数字"
ok, msg = verif_mgr.verify(email, code)
if not ok:
return False, msg
ph, salt = hash_password(password)
rec = {
"email": email,
"username": username,
"password_hash": ph,
"salt": salt,
"activated": True,
"created_at": datetime.utcnow().isoformat()
}
self.store.add_or_update(rec)
log(f"user registered: {email} / {username}")
return True, "注册并设置密码成功"
def login(self, key: str, password: str):
# key can be email or username
u = self.store.find(key)
if not u:
return False, "未找到该用户"
if not u.get("activated"):
return False, "该账号未激活"
if not verify_password_hash(u.get("password_hash"), u.get("salt"), password):
return False, "密码错误"
return True, u
def change_password(self, email: str, old_pw: str, new_pw: str):
u = self.store.find_by_email(email)
if not u:
return False, "未找到用户"
if not verify_password_hash(u.get("password_hash"), u.get("salt"), old_pw):
return False, "原密码错误"
if not (6 <= len(new_pw) <= 10 and any(c.isupper() for c in new_pw) and any(c.islower() for c in new_pw) and any(c.isdigit() for c in new_pw)):
return False, "新密码不符合规则"
ph, salt = hash_password(new_pw)
u.update({"password_hash": ph, "salt": salt})
self.store._save()
log(f"user changed password: {email}")
return True, "修改成功"
# ----------------- GUI -----------------
class MathApp:
def __init__(self, root):
self.root = root
self.root.title("小初高数学练习")
self.root.geometry(f"{WINDOW_W}x{WINDOW_H}")
ensure_files_exist()
self.user_store = UserStore()
self.qstore = QuestionStore()
self.auth = AuthService(self.user_store)
self.current_user = None # user dict
self.current_exam = None
self.build_main()
def clear(self):
for w in self.root.winfo_children():
w.destroy()
def build_main(self):
self.clear()
frm = tk.Frame(self.root, padx=20, pady=20)
frm.pack(fill="both", expand=True)
tk.Label(frm, text="数学学习应用", font=("Arial", 18)).pack(pady=12)
tk.Button(frm, text="注册", width=40, command=self.ui_register_flow).pack(pady=6)
tk.Button(frm, text="登录", width=40, command=self.ui_login).pack(pady=6)
tk.Button(frm, text="退出", width=40, command=self.root.quit).pack(pady=6)
# ----- 注册流程 -----
def ui_register_flow(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
tk.Label(fr, text="邮箱(注册邮箱):").grid(row=0, column=0, sticky="e")
email_var = tk.StringVar()
email_entry = tk.Entry(fr, textvariable=email_var, width=36)
email_entry.grid(row=0, column=1, padx=6)
send_btn = tk.Button(fr, text="发送验证码")
send_btn.grid(row=0, column=2, padx=6)
tk.Label(fr, text="验证码:").grid(row=1, column=0, sticky="e")
code_var = tk.StringVar()
code_entry = tk.Entry(fr, textvariable=code_var, width=20)
code_entry.grid(row=1, column=1, padx=6)
tk.Label(fr, text="用户名:").grid(row=2, column=0, sticky="e")
username_var = tk.StringVar()
username_entry = tk.Entry(fr, textvariable=username_var, width=30)
username_entry.grid(row=2, column=1, padx=6)
tk.Label(fr, text="密码:").grid(row=3, column=0, sticky="e")
pw_var = tk.StringVar()
pw_entry = tk.Entry(fr, textvariable=pw_var, show="*", width=30)
pw_entry.grid(row=3, column=1, padx=6)
tk.Label(fr, text="确认密码:").grid(row=4, column=0, sticky="e")
pw2_var = tk.StringVar()
pw2_entry = tk.Entry(fr, textvariable=pw2_var, show="*", width=30)
pw2_entry.grid(row=4, column=1, padx=6)
btn_fr = tk.Frame(fr)
btn_fr.grid(row=6, column=0, columnspan=3, pady=18)
tk.Button(btn_fr, text="确定", width=12, command=lambda: on_confirm()).pack(side="left", padx=8)
tk.Button(btn_fr, text="返回", width=12, command=self.build_main).pack(side="left", padx=8)
fr.grid_columnconfigure(1, weight=1)
cooldown = {"left": 0, "timer": None}
def update_send_btn():
if cooldown["left"] > 0:
send_btn.config(text=f"重新发送({cooldown['left']})", state="disabled")
else:
send_btn.config(text="发送验证码", state="normal")
def countdown_tick():
if cooldown["left"] <= 0:
update_send_btn()
return
cooldown["left"] -= 1
update_send_btn()
cooldown["timer"] = self.root.after(1000, countdown_tick)
def on_send():
email = email_var.get().strip()
if not is_valid_email(email):
messagebox.showwarning("邮箱错误", "请输入正确的邮箱地址", parent=self.root)
return
# generate and send code asynchronously
def worker():
send_btn.config(state="disabled", text="发送中...")
ok, msg = self.auth.send_code(email)
if ok:
messagebox.showinfo("发送成功", f"验证码已发送到 {email}(若未收到, 请检查垃圾邮件)。", parent=self.root)
cooldown["left"] = SEND_COOLDOWN
countdown_tick()
else:
messagebox.showerror("发送失败", f"发送失败: {msg}", parent=self.root)
update_send_btn()
threading.Thread(target=worker, daemon=True).start()
send_btn.config(command=on_send)
def on_confirm():
email = email_var.get().strip()
code = code_var.get().strip()
username = username_var.get().strip()
pw = pw_var.get()
pw2 = pw2_var.get()
if not is_valid_email(email):
messagebox.showwarning("错误", "请输入合法邮箱", parent=self.root); return
if not code:
messagebox.showwarning("错误", "请输入验证码", parent=self.root); return
if not username:
messagebox.showwarning("错误", "请输入用户名", parent=self.root); return
if not pw or not pw2:
messagebox.showwarning("错误", "请输入密码并确认", parent=self.root); return
if pw != pw2:
messagebox.showwarning("错误", "两次密码不一致", parent=self.root); return
if not (6 <= len(pw) <= 10 and any(c.isupper() for c in pw) and any(c.islower() for c in pw) and any(c.isdigit() for c in pw)):
messagebox.showwarning("错误", "密码需6-10位且包含大写、小写字母与数字", parent=self.root); return
ok, msg = self.auth.register_with_code(email, code, username, pw)
if not ok:
messagebox.showerror("失败", msg, parent=self.root); return
messagebox.showinfo("成功", "注册并设置密码成功,跳转到选择界面", parent=self.root)
# login and go to choice screen
self.current_user = self.user_store.find_by_email(email)
self.build_choice_screen()
# ----- 登录界面(用户名/邮箱登录) -----
def ui_login(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
tk.Label(fr, text="登录", font=("Arial", 16)).pack(pady=8)
tk.Label(fr, text="用户名/邮箱:").pack(anchor="w")
key_var = tk.StringVar()
tk.Entry(fr, textvariable=key_var, width=50).pack(pady=4)
tk.Label(fr, text="密码:").pack(anchor="w")
pw_var = tk.StringVar()
tk.Entry(fr, textvariable=pw_var, show="*", width=50).pack(pady=4)
btn_fr = tk.Frame(fr); btn_fr.pack(pady=12)
def do_login():
key = key_var.get().strip()
pw = pw_var.get()
if not key:
messagebox.showwarning("输入错误", "请输入用户名或邮箱", parent=self.root); return
ok, res = self.auth.login(key, pw)
if not ok:
messagebox.showerror("登录失败", res, parent=self.root); return
# res is user dict
self.current_user = res
messagebox.showinfo("登录成功", f"欢迎,{self.current_user.get('username')}", parent=self.root)
self.build_choice_screen()
tk.Button(btn_fr, text="登录", width=12, command=do_login).pack(side="left", padx=6)
tk.Button(btn_fr, text="返回", width=12, command=self.build_main).pack(side="left", padx=6)
# ----- 选择学段与题量 -----
def build_choice_screen(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
uname = self.current_user.get("username") if self.current_user else ""
email = self.current_user.get("email") if self.current_user else ""
tk.Label(fr, text=f"已登录: {uname} ({email})", font=("Arial", 12)).pack(anchor="w")
tk.Button(fr, text="修改密码", command=self.ui_change_password).pack(anchor="w", pady=4)
tk.Button(fr, text="注销", command=self.logout).pack(anchor="w", pady=4)
tk.Label(fr, text="请选择学段并输入题目数量默认3上限20:", font=("Arial", 12)).pack(pady=8, anchor="w")
grade_var = tk.StringVar(value="小学")
for g in QUESTION_FILES.keys():
tk.Radiobutton(fr, text=g, variable=grade_var, value=g).pack(anchor="w")
num_entry = tk.Entry(fr, width=8)
num_entry.pack(pady=6, anchor="w")
num_entry.delete(0, "end"); num_entry.insert(0, str(DEFAULT_QUESTIONS))
def start():
try:
n = int(num_entry.get())
if n <= 0:
raise ValueError()
if n > MAX_QUESTIONS:
messagebox.showwarning("超过上限", f"题目数量上限为 {MAX_QUESTIONS}", parent=self.root); return
except Exception:
messagebox.showwarning("输入错误", "请输入正整数题目数量", parent=self.root); return
grade = grade_var.get()
qs = self.qstore.load(grade)
if len(qs) < n:
qs = ensure_questions_for_grade(grade, n, self.qstore)
chosen = random.sample(qs, n)
self.current_exam = {"grade": grade, "questions": chosen, "index": 0, "answers": [None]*n}
self.build_question_screen()
tk.Button(fr, text="开始做题", command=start).pack(pady=8)
def logout(self):
self.current_user = None
self.current_exam = None
self.build_main()
def ui_change_password(self):
if not self.current_user:
messagebox.showwarning("未登录", "请先登录", parent=self.root); return
old = simpledialog.askstring("修改密码", "请输入原密码:", show="*", parent=self.root)
if old is None:
return
new1 = simpledialog.askstring("新密码", "请输入新密码6-10位含大小写字母与数字", show="*", parent=self.root)
if new1 is None:
return
new2 = simpledialog.askstring("确认新密码", "请再次输入新密码:", show="*", parent=self.root)
if new2 is None:
return
if new1 != new2:
messagebox.showwarning("不一致", "两次新密码不一致", parent=self.root); return
ok, msg = self.auth.change_password(self.current_user.get("email"), old, new1)
if not ok:
messagebox.showerror("失败", msg, parent=self.root); return
messagebox.showinfo("成功", "密码修改成功", parent=self.root)
# ----- 题目界面 -----
def build_question_screen(self):
self.clear()
exam = self.current_exam
idx = exam["index"]
q = exam["questions"][idx]
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
header = tk.Label(fr, text=f"学段:{exam['grade']} 题目 {idx+1}/{len(exam['questions'])}", font=("Arial", 12))
header.pack(anchor="w")
stem = tk.Label(fr, text=q["stem"], font=("Arial", 14), wraplength=WINDOW_W-80, justify="left")
stem.pack(pady=8, anchor="w")
chosen_var = tk.IntVar(value=-1 if exam["answers"][idx] is None else exam["answers"][idx])
for i, opt in enumerate(q["options"]):
tk.Radiobutton(fr, text=opt, variable=chosen_var, value=i).pack(anchor="w")
nav = tk.Frame(fr); nav.pack(pady=10, anchor="w")
def save_choice():
v = chosen_var.get()
exam["answers"][idx] = None if v == -1 else int(v)
return True
def prev_q():
if idx == 0:
messagebox.showinfo("提示", "已经是第一题", parent=self.root); return
save_choice(); exam["index"] -= 1; self.build_question_screen()
def next_or_finish():
save_choice()
if idx + 1 < len(exam["questions"]):
exam["index"] += 1
self.build_question_screen()
else:
self.show_result_screen()
tk.Button(nav, text="上一题", command=prev_q).pack(side="left", padx=6)
tk.Button(nav, text="下一题/提交", command=next_or_finish).pack(side="left", padx=6)
tk.Button(nav, text="放弃并返回", command=self.build_choice_screen).pack(side="left", padx=6)
# ----- 结果界面 -----
def show_result_screen(self):
exam = self.current_exam
total = len(exam["questions"])
correct = 0
for i, q in enumerate(exam["questions"]):
ans = exam["answers"][i]
if ans is not None and ans == q["answer_index"]:
correct += 1
percent = round(correct / total * 100, 2) if total > 0 else 0.0
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
tk.Label(fr, text=f"考试结束 - 得分:{percent}%", font=("Arial", 14)).pack(pady=6)
tk.Label(fr, text=f"答对:{correct} / {total}").pack(pady=4)
def show_details():
win = tk.Toplevel(self.root); win.title("答题详情")
txt = scrolledtext.ScrolledText(win, width=100, height=30)
txt.pack(fill="both", expand=True)
for i, q in enumerate(exam["questions"]):
chosen = exam["answers"][i]
chosen_text = q["options"][chosen] if chosen is not None and 0 <= chosen < len(q["options"]) else "未作答"
correct_text = q["options"][q["answer_index"]]
status = "正确" if chosen == q["answer_index"] else "错误"
txt.insert("end", f"{i+1}: {q['stem']}\n你的答案: {chosen_text}\n正确答案: {correct_text} [{status}]\n\n")
txt.config(state="disabled")
tk.Button(fr, text="查看答题详情", command=show_details).pack(pady=6)
tk.Button(fr, text="继续做题", command=self._continue_again).pack(pady=6)
tk.Button(fr, text="退出登录并返回主界面", command=self._logout_and_return).pack(pady=6)
def _continue_again(self):
self.current_exam = None
self.build_choice_screen()
def _logout_and_return(self):
self.current_user = None
self.current_exam = None
self.build_main()
# ----------------- 初始化 -----------------
def main():
ensure_files_exist()
root = tk.Tk()
app = MathApp(root)
root.mainloop()
if __name__ == "__main__":
main()
Loading…
Cancel
Save