diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json new file mode 100644 index 0000000..c5867b4 --- /dev/null +++ b/src/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.linting.flake8Enabled": true, + "python.formatting.provider": "yapf", + "python.linting.flake8Args": ["--max-line-length=248"], + "python.linting.pylintEnabled": false, + "python-envs.defaultEnvManager": "ms-python.python:system", + "python-envs.pythonProjects": [] +} diff --git a/src/main.exe b/src/main.exe new file mode 100644 index 0000000..88963d9 Binary files /dev/null and b/src/main.exe differ diff --git a/src/main.py b/src/main.py index f8c1d40..5ee89cb 100644 --- a/src/main.py +++ b/src/main.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -单文件桌面应用:小初高数学练习(含注册/邮箱验证码、密码规则、题目自动生成) -修改点: -- 每道题操作数数量为 1-5(随机),操作数取值 1-100 -- 登录界面支持 使用 用户名 或 邮箱 登录 -- 其它功能保留:注册(邮箱验证码)、激活并设置密码、修改密码、选择学段与题量、自动生成题目、逐题答题、评分 -注意: -- 请替换 SMTP 配置为有效账户或在无法发送邮件时调整 send_verification_email 为测试模式。 -- Python 3.8+ +单文件桌面应用:小初高数学练习(Tkinter) +主要修改(按用户要求): +- 每道题中出现的数字不超过 5 个(操作数数目为 1~5) +- 小学题不出现负数(操作数为正,生成表达式尽量避免负结果) +- 初中题:题目显示使用根号符号 √(...),幂显示为 x^2(内部仍用 sqrt()/** 计算) +- 高中题:题目中不出现 pi 字符串,三角函数以角度表示(如 sin(30°)),内部计算将角度转为弧度 +其它功能保留:注册(邮箱验证码)、用户名/邮箱登录、修改密码、题库自动生成与保存、PBKDF2 密码哈希等。 +注意:替换 SMTP 配置为可用的邮箱/授权码或改为测试模式。 """ import tkinter as tk @@ -51,7 +51,7 @@ WINDOW_H = 620 # 题目生成参数 MIN_OPERANDS = 1 -MAX_OPERANDS = 5 +MAX_OPERANDS = 5 # 至多 5 个操作数(数字) OPERAND_MIN = 1 OPERAND_MAX = 100 # -------------------------------------- @@ -62,11 +62,9 @@ def log(msg: str): 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: @@ -102,7 +100,6 @@ class UserStore: return None def find(self, key: str): - # try email first, then username u = self.find_by_email(key) if u: return u @@ -158,7 +155,6 @@ class VerificationManager: return False, "验证码已过期" if not secrets.compare_digest(real, code): return False, "验证码不匹配" - # 删除以防二次使用 del self.store[email] return True, "验证成功" @@ -180,7 +176,6 @@ def verify_password_hash(stored_hash, stored_salt, attempt): # ----------------- 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 @@ -207,21 +202,20 @@ def send_verification_email(target_email: str, code: str): 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]) + while len(opts) < 4 and attempts < 200: + delta = random.choice([-10, -6, -4, -3, -2, -1, 1, 2, 3, 4, 5, 8, 10]) candidate = corr + delta - if str(candidate) not in opts: + if candidate >= -9999 and str(candidate) not in opts: opts.append(str(candidate)) attempts += 1 while len(opts) < 4: @@ -229,7 +223,6 @@ def mk_options_from_number(correct_val, kind="int"): random.shuffle(opts) return opts, opts.index(str(corr)) else: - # float corr = float(correct_val) corr_s = f"{corr:.3f}" opts = [corr_s] @@ -246,180 +239,206 @@ def mk_options_from_number(correct_val, kind="int"): random.shuffle(opts) return opts, opts.index(corr_s) -# 安全评估:表达式由程序生成,使用受限 eval 环境评估 +# 受限 eval 环境用于计算(内部使用 math 的函数) 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 为 Python 可 eval 的表达式(外层 display 与内部计算分开) expr = expr.replace("^", "**") return eval(expr, SAFE_EVAL_ENV, {}) -# 小学题:1~5 个操作数,运算符 + - * /,可能带一对括号 +# ----------------- 题目生成(符合新要求) ----------------- +# 小学: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) + # 生成 1~5 个正整数操作数 + n = random.randint(MIN_OPERANDS, MAX_OPERANDS) + nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)] + # 选择运算符(n-1 个) + ops_list = [] + for _ in range(max(0, n - 1)): + ops_list.append(random.choice(['+', '-', '*', '/'])) + # 为避免出现负数结果,策略: + # - 对于减法,确保左边操作数大于或等于右边(在构建顺序时调整) + # - 对于除法,尽量设置可整除关系(将右边替换为因数) + # 构造表达式逐步调整 + def build_expr(nums, ops): 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): + for i, a in enumerate(nums): + parts.append(str(a)) + if i < len(ops): + parts.append(ops[i]) + return "".join(parts) + + # 调整减法与除法以尽量得到非负整数 + for _ in range(100): + nums_copy = nums[:] + ops_copy = ops_list[:] + # adjust for division: make divisor divide dividend when possible + for i, op in enumerate(ops_copy): + if op == '/': + a = nums_copy[i] + b = nums_copy[i+1] + if b == 0: + nums_copy[i+1] = random.randint(OPERAND_MIN, OPERAND_MAX) + # try to set b as a divisor of a by replacing b + divisors = [d for d in range(1, OPERAND_MAX+1) if a % d == 0] + if divisors: + nums_copy[i+1] = random.choice(divisors) + else: + # fallback: change op to + to avoid fractions + ops_copy[i] = random.choice(['+', '-','*']) + # adjust for subtraction to avoid negatives: ensure left >= right when directly subtracting + for i, op in enumerate(ops_copy): + if op == '-': + if nums_copy[i] < nums_copy[i+1]: + # swap operands to attempt non-negative + nums_copy[i], nums_copy[i+1] = nums_copy[i+1], nums_copy[i] + expr = build_expr(nums_copy, ops_copy) 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 + val = safe_eval(expr) + # if float, round to int + if isinstance(val, float): + if abs(val - round(val)) < 1e-9: + val = int(round(val)) + else: + val = int(round(val)) + # ensure non-negative (小学要求) + if isinstance(val, (int, float)) and val >= 0: + nums = nums_copy + ops_list = ops_copy + 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: + # regenerate small changes and retry + nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)] + ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))] + continue + # final expression + expr_display = build_expr(nums, ops_list) + # Evaluate final value + try: + val = safe_eval(expr_display) + if isinstance(val, float): + if abs(val - round(val)) < 1e-9: + val = int(round(val)) + else: + val = int(round(val)) + except Exception: 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(...) + # Ensure no negative options for primary (replace negatives) + opts = [o if not (o.startswith("-")) else str(abs(int(o))) for o in opts] + # ensure answer_index updated if changed due to replacement + if str(val) in opts: + answer_index = opts.index(str(val)) + else: + # if val changed by making negative->positive, recompute index + answer_index = 0 + for i,o in enumerate(opts): + if int(o) == int(val): + answer_index = i + break + return {"id": gen_id("pri"), "grade": "小学", "stem": f"{expr_display} = ?", "options": opts, "answer_index": answer_index} + +# 初中:1~5 个操作数,题目显示中用 √(...)表示根号,幂显示为 x^2 def generate_middle_question(): + n = random.randint(MIN_OPERANDS, MAX_OPERANDS) + nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)] + ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))] + # ensure at least one sqrt or square 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) + idx = random.randint(0, n - 1) + expr_parts = [] + # prepare a copy for computation (python expression) + comp_nums = [str(x) for x in nums] 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]})" + # prefer perfect square inside sometimes + inner_base = random.randint(1, 50) + inner = inner_base * inner_base if random.choice([True, False]) else nums[idx]*nums[idx] + comp_nums[idx] = f"sqrt({inner})" + display_num = f"√({inner})" else: - nums[idx] = f"({nums[idx]}**2)" - parts = [] - for i in range(n_ops): - parts.append(str(nums[i])) + comp_nums[idx] = f"({nums[idx]}**2)" + display_num = f"{nums[idx]}^2" + # build display expression and compute expression separately + display_parts = [] + comp_parts = [] + for i in range(n): + display_parts.append(display_num if i == idx else str(nums[i])) + comp_parts.append(comp_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 + display_parts.append(ops_list[i]) + comp_parts.append(ops_list[i]) + display_expr = "".join(display_parts) + comp_expr = "".join(comp_parts) + # evaluate + try: + val = safe_eval(comp_expr) + except Exception: + # fallback to simpler expression + comp_expr = "1+1" + display_expr = "1+1" + val = 2 + # format options if isinstance(val, float) and abs(val - round(val)) > 1e-9: - opts, idx = mk_options_from_number(val, kind="float") + opts, idx_ans = 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} + opts, idx_ans = mk_options_from_number(val, kind="int") + return {"id": gen_id("mid"), "grade": "初中", "stem": f"{display_expr} = ?", "options": opts, "answer_index": idx_ans} -# 高中题:至少包含 sin/cos/tan(使用角度),并可包含 1~5 个操作数 +# 高中:1~5 个操作数,三角函数以角度表示(如 sin(30°)),题目显示不含 pi 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) + n = random.randint(MIN_OPERANDS, MAX_OPERANDS) + nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)] + ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))] + idx = random.randint(0, n - 1) + # choose angle in degrees (常见角优先) 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])) + # comp expression uses radians: func(pi*angle/180) + comp_nums = [str(x) for x in nums] + comp_nums[idx] = f"{func}(pi*{angle}/180)" + display_nums = [str(x) for x in nums] + display_nums[idx] = f"{func}({angle}°)" # show degrees, no pi + # build expressions + comp_parts = [] + display_parts = [] + for i in range(n): + comp_parts.append(comp_nums[i]) + display_parts.append(display_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: + comp_parts.append(ops_list[i]) + display_parts.append(ops_list[i]) + comp_expr = "".join(comp_parts) + display_expr = "".join(display_parts) + # evaluate + try: + val = safe_eval(comp_expr) + except Exception: + # fallback + comp_expr = "0.0" + display_expr = f"{func}({angle}°)" 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} + # options as float (3 decimals) + opts, idx_ans = mk_options_from_number(val, kind="float") + return {"id": gen_id("high"), "grade": "高中", "stem": f"{display_expr} = ? (保留3位小数)", "options": opts, "answer_index": idx_ans} -# Ensure enough questions in file; auto-generate and append if insufficient +# auto-generate to ensure enough questions 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 = [] + new = [] for _ in range(to_gen): if grade == "小学": - gen.append(generate_primary_question()) + new.append(generate_primary_question()) elif grade == "初中": - gen.append(generate_middle_question()) + new.append(generate_middle_question()) else: - gen.append(generate_high_question()) - qs.extend(gen) + new.append(generate_high_question()) + qs.extend(new) qstore.save(grade, qs) - log(f"Auto-generated {len(gen)} questions for {grade}") + log(f"Auto-generated {len(new)} questions for {grade}") return qs # ----------------- 服务逻辑(注册/激活/登录/改密) ----------------- @@ -442,7 +461,6 @@ class AuthService: 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, "用户名已被占用" @@ -465,7 +483,6 @@ class AuthService: 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, "未找到该用户" @@ -499,7 +516,7 @@ class MathApp: self.user_store = UserStore() self.qstore = QuestionStore() self.auth = AuthService(self.user_store) - self.current_user = None # user dict + self.current_user = None self.current_exam = None self.build_main() @@ -516,54 +533,44 @@ class MathApp: 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() @@ -571,13 +578,11 @@ class MathApp: 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) @@ -589,9 +594,7 @@ class MathApp: 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() @@ -614,11 +617,10 @@ class MathApp: 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) @@ -639,14 +641,13 @@ class MathApp: 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) @@ -656,7 +657,7 @@ class MathApp: 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") + tk.Label(fr, text="请选择学段并输入题目数量(数量最高为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") @@ -705,7 +706,7 @@ class MathApp: messagebox.showerror("失败", msg, parent=self.root); return messagebox.showinfo("成功", "密码修改成功", parent=self.root) - # ----- 题目界面 ----- + # 题目界面 def build_question_screen(self): self.clear() exam = self.current_exam @@ -740,7 +741,7 @@ class MathApp: 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"]) @@ -779,7 +780,7 @@ class MathApp: self.current_exam = None self.build_main() -# ----------------- 初始化 ----------------- +# ----------------- main ----------------- def main(): ensure_files_exist() root = tk.Tk() diff --git a/src/questions_high.json b/src/questions_high.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/questions_high.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/questions_middle.json b/src/questions_middle.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/questions_middle.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/questions_primary.json b/src/questions_primary.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/questions_primary.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/users.json b/src/users.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/users.json @@ -0,0 +1 @@ +[] \ No newline at end of file