diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..35410ca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml
new file mode 100644
index 0000000..4ea72a9
--- /dev/null
+++ b/.idea/copilot.data.migration.agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml
new file mode 100644
index 0000000..7ef04e2
--- /dev/null
+++ b/.idea/copilot.data.migration.ask.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml
new file mode 100644
index 0000000..1f2ea11
--- /dev/null
+++ b/.idea/copilot.data.migration.ask2agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml
new file mode 100644
index 0000000..8648f94
--- /dev/null
+++ b/.idea/copilot.data.migration.edit.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/lib.xml b/.idea/libraries/lib.xml
new file mode 100644
index 0000000..7f1aa7e
--- /dev/null
+++ b/.idea/libraries/lib.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..31e1ebc
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..831f033
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index 4a8b08a..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# mathStudyAPP
-
diff --git a/data/users.cfg b/data/users.cfg
new file mode 100644
index 0000000..a790fcf
--- /dev/null
+++ b/data/users.cfg
@@ -0,0 +1,12 @@
+王五3 123 HIGH wangwu3@example.com
+王五2 123 HIGH wangwu2@example.com
+123 Aa123456 PRIMARY 2571400460@qq.com
+王五1 123 HIGH wangwu1@example.com
+scb Aa123456 PRIMARY 3110858122@qq.com
+张三3 123 PRIMARY zhangsan3@example.com
+张三1 123 PRIMARY zhangsan1@example.com
+张三2 123 PRIMARY zhangsan2@example.com
+李四3 123 MIDDLE lishi3@example.com
+sqf Qq123456 PRIMARY sqf090815@hnu.edu.cn
+李四1 123 MIDDLE lishi1@example.com
+李四2 123 MIDDLE lishi2@example.com
diff --git a/doc/MyApp.jar b/doc/MyApp.jar
new file mode 100644
index 0000000..01c6564
Binary files /dev/null and b/doc/MyApp.jar differ
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 0000000..c291673
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,30 @@
+# mathStudyAPP
+## 📂 项目结构
+src/
+├─ Main.java
+├─ controller/
+│ └─ FunctionController.java
+├─ model/
+│ ├─ Login.java
+│ ├─ LanguageSwitch.java
+│ ├─ QuestionGenerator.java
+│ ├─ Generator.java
+│ ├─ LoadFile.java
+│ ├─ Save.java
+│ ├─ Create.java
+│ ├─ ExpressionEvaluator.java
+│ └─ Paper.java
+└─ view/
+ ├─ LoginFrame.java
+ ├─ RegisterFrame.java
+ ├─ MainMenuFrame.java
+ ├─ ExamSetupFrame.java
+ ├─ ExamFrame.java
+ └─ ResultFrame.java
+lib/
+├─ jakarta.activation-2.0.1.jar
+└─ jakarta.mail-2.0.1.jar
+
+---
+
+
diff --git a/lib/jakarta.activation-2.0.1.jar b/lib/jakarta.activation-2.0.1.jar
new file mode 100644
index 0000000..521c7c4
Binary files /dev/null and b/lib/jakarta.activation-2.0.1.jar differ
diff --git a/lib/jakarta.mail-2.0.1.jar b/lib/jakarta.mail-2.0.1.jar
new file mode 100644
index 0000000..17e07cc
Binary files /dev/null and b/lib/jakarta.mail-2.0.1.jar differ
diff --git a/mathStudyApp.iml b/mathStudyApp.iml
new file mode 100644
index 0000000..fb8e866
--- /dev/null
+++ b/mathStudyApp.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/out/production/mathStudyApp/Main.class b/out/production/mathStudyApp/Main.class
new file mode 100644
index 0000000..50bd932
Binary files /dev/null and b/out/production/mathStudyApp/Main.class differ
diff --git a/out/production/mathStudyApp/controller/AssignController.class b/out/production/mathStudyApp/controller/AssignController.class
new file mode 100644
index 0000000..c202f0e
Binary files /dev/null and b/out/production/mathStudyApp/controller/AssignController.class differ
diff --git a/out/production/mathStudyApp/controller/FunctionController.class b/out/production/mathStudyApp/controller/FunctionController.class
new file mode 100644
index 0000000..621af2e
Binary files /dev/null and b/out/production/mathStudyApp/controller/FunctionController.class differ
diff --git a/out/production/mathStudyApp/model/Create.class b/out/production/mathStudyApp/model/Create.class
new file mode 100644
index 0000000..002f264
Binary files /dev/null and b/out/production/mathStudyApp/model/Create.class differ
diff --git a/out/production/mathStudyApp/model/ExpressionEvaluator.class b/out/production/mathStudyApp/model/ExpressionEvaluator.class
new file mode 100644
index 0000000..ab78d34
Binary files /dev/null and b/out/production/mathStudyApp/model/ExpressionEvaluator.class differ
diff --git a/out/production/mathStudyApp/model/Generator$1.class b/out/production/mathStudyApp/model/Generator$1.class
new file mode 100644
index 0000000..2f8516d
Binary files /dev/null and b/out/production/mathStudyApp/model/Generator$1.class differ
diff --git a/out/production/mathStudyApp/model/Generator.class b/out/production/mathStudyApp/model/Generator.class
new file mode 100644
index 0000000..58ca828
Binary files /dev/null and b/out/production/mathStudyApp/model/Generator.class differ
diff --git a/out/production/mathStudyApp/model/LanguageSwitch$1.class b/out/production/mathStudyApp/model/LanguageSwitch$1.class
new file mode 100644
index 0000000..f9b2a2b
Binary files /dev/null and b/out/production/mathStudyApp/model/LanguageSwitch$1.class differ
diff --git a/out/production/mathStudyApp/model/LanguageSwitch.class b/out/production/mathStudyApp/model/LanguageSwitch.class
new file mode 100644
index 0000000..c3c3ca6
Binary files /dev/null and b/out/production/mathStudyApp/model/LanguageSwitch.class differ
diff --git a/out/production/mathStudyApp/model/LoadFile.class b/out/production/mathStudyApp/model/LoadFile.class
new file mode 100644
index 0000000..41b059e
Binary files /dev/null and b/out/production/mathStudyApp/model/LoadFile.class differ
diff --git a/out/production/mathStudyApp/model/Login$Account.class b/out/production/mathStudyApp/model/Login$Account.class
new file mode 100644
index 0000000..aa24861
Binary files /dev/null and b/out/production/mathStudyApp/model/Login$Account.class differ
diff --git a/out/production/mathStudyApp/model/Login$Level.class b/out/production/mathStudyApp/model/Login$Level.class
new file mode 100644
index 0000000..a35c914
Binary files /dev/null and b/out/production/mathStudyApp/model/Login$Level.class differ
diff --git a/out/production/mathStudyApp/model/Login.class b/out/production/mathStudyApp/model/Login.class
new file mode 100644
index 0000000..e914c85
Binary files /dev/null and b/out/production/mathStudyApp/model/Login.class differ
diff --git a/out/production/mathStudyApp/model/Paper.class b/out/production/mathStudyApp/model/Paper.class
new file mode 100644
index 0000000..ee32776
Binary files /dev/null and b/out/production/mathStudyApp/model/Paper.class differ
diff --git a/out/production/mathStudyApp/model/QuestionGenerator.class b/out/production/mathStudyApp/model/QuestionGenerator.class
new file mode 100644
index 0000000..7ca3b27
Binary files /dev/null and b/out/production/mathStudyApp/model/QuestionGenerator.class differ
diff --git a/out/production/mathStudyApp/model/Save.class b/out/production/mathStudyApp/model/Save.class
new file mode 100644
index 0000000..e08888b
Binary files /dev/null and b/out/production/mathStudyApp/model/Save.class differ
diff --git a/out/production/mathStudyApp/view/ExamFrame.class b/out/production/mathStudyApp/view/ExamFrame.class
new file mode 100644
index 0000000..e37f665
Binary files /dev/null and b/out/production/mathStudyApp/view/ExamFrame.class differ
diff --git a/out/production/mathStudyApp/view/ExamSetupFrame.class b/out/production/mathStudyApp/view/ExamSetupFrame.class
new file mode 100644
index 0000000..2850903
Binary files /dev/null and b/out/production/mathStudyApp/view/ExamSetupFrame.class differ
diff --git a/out/production/mathStudyApp/view/LoginFrame.class b/out/production/mathStudyApp/view/LoginFrame.class
new file mode 100644
index 0000000..b8a1070
Binary files /dev/null and b/out/production/mathStudyApp/view/LoginFrame.class differ
diff --git a/out/production/mathStudyApp/view/MainMenuFrame$ChangePasswordDialog.class b/out/production/mathStudyApp/view/MainMenuFrame$ChangePasswordDialog.class
new file mode 100644
index 0000000..1147586
Binary files /dev/null and b/out/production/mathStudyApp/view/MainMenuFrame$ChangePasswordDialog.class differ
diff --git a/out/production/mathStudyApp/view/MainMenuFrame.class b/out/production/mathStudyApp/view/MainMenuFrame.class
new file mode 100644
index 0000000..de0d464
Binary files /dev/null and b/out/production/mathStudyApp/view/MainMenuFrame.class differ
diff --git a/out/production/mathStudyApp/view/RegisterFrame$1.class b/out/production/mathStudyApp/view/RegisterFrame$1.class
new file mode 100644
index 0000000..9dd1b2a
Binary files /dev/null and b/out/production/mathStudyApp/view/RegisterFrame$1.class differ
diff --git a/out/production/mathStudyApp/view/RegisterFrame.class b/out/production/mathStudyApp/view/RegisterFrame.class
new file mode 100644
index 0000000..eecd276
Binary files /dev/null and b/out/production/mathStudyApp/view/RegisterFrame.class differ
diff --git a/out/production/mathStudyApp/view/ResultFrame.class b/out/production/mathStudyApp/view/ResultFrame.class
new file mode 100644
index 0000000..bc6b282
Binary files /dev/null and b/out/production/mathStudyApp/view/ResultFrame.class differ
diff --git a/src/Main.java b/src/Main.java
new file mode 100644
index 0000000..07d6d26
--- /dev/null
+++ b/src/Main.java
@@ -0,0 +1,13 @@
+import javax.swing.SwingUtilities;
+
+import view.LoginFrame;
+
+/**
+ * 程序入口
+ */
+public class Main {
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(() -> new LoginFrame());
+ }
+}
diff --git a/src/controller/FunctionController.java b/src/controller/FunctionController.java
new file mode 100644
index 0000000..ef07cf9
--- /dev/null
+++ b/src/controller/FunctionController.java
@@ -0,0 +1,34 @@
+package controller;
+
+import javax.swing.JOptionPane;
+
+import model.Create;
+import model.Login;
+import model.Paper;
+import view.ExamFrame;
+
+/**
+ * 题目生成控制器:接收题数与用户信息,调用 Create 生成 Paper 并打开 ExamFrame
+ */
+public class FunctionController {
+
+ private Login.Account user;
+
+ public FunctionController(Login.Account user) {
+ this.user = user;
+ }
+
+ public void startExam(int n) {
+ if (n < 10 || n > 30) {
+ JOptionPane.showMessageDialog(null, "题目数量的有效输入范围是 10-30");
+ return;
+ }
+ Paper paper = Create.create(n, user.level, user);
+ if (paper == null || paper.size() == 0) {
+ JOptionPane.showMessageDialog(null, "未能生成题目(可能因去重约束导致)。");
+ return;
+ }
+ JOptionPane.showMessageDialog(null, "已生成 " + paper.size() + " 道题,开始答题。");
+ new ExamFrame(paper, user);
+ }
+}
diff --git a/src/model/Create.java b/src/model/Create.java
new file mode 100644
index 0000000..d39e04e
--- /dev/null
+++ b/src/model/Create.java
@@ -0,0 +1,114 @@
+package model;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import javax.script.ScriptException;
+
+/**
+ * 负责生成试卷并创建选择题选项(四选一)
+ */
+public class Create {
+
+ private static double evalExpression(String expr) throws ScriptException {
+ return ExpressionEvaluator.evaluate(expr);
+ }
+
+ public static Paper create(int n, Login.Level currentLevel, Login.Account user) {
+ List existing = LoadFile.loadExistingQuestions(user.username);
+ List paper = generatePaper(n, currentLevel, existing);
+ if (paper.isEmpty()) return null;
+
+ List optionsList = new ArrayList<>();
+ int[] correctIdx = new int[paper.size()];
+
+ for (int i = 0; i < paper.size(); i++) {
+ String q = paper.get(i);
+ optionsList.add(generateOptions(q, correctIdx, i));
+ }
+
+ savePaper(user.username, paper);
+
+ return new Paper(paper, optionsList, correctIdx);
+ }
+
+ private static List generatePaper(int n, Login.Level level, List existing) {
+ Generator qg = new Generator(level, existing);
+ return qg.generatePaper(n);
+ }
+
+ private static String[] generateOptions(String question, int[] correctIdx, int i) {
+ double correctVal;
+ try {
+ correctVal = evalExpression(question);
+ return generateNumericOptions(correctVal, correctIdx, i);
+ } catch (Exception e) {
+ return generateTextOptions(question, correctIdx, i);
+ }
+ }
+
+ private static String[] generateTextOptions(String question, int[] correctIdx, int i) {
+ String[] opts = new String[]{question, question + " + 1", question + " - 1", "错误:" + question};
+ shuffleArray(opts);
+ correctIdx[i] = findIndex(opts, question);
+ return opts;
+ }
+
+ private static String[] generateNumericOptions(double correctVal, int[] correctIdx, int i) {
+ String correctStr = formatNumber(correctVal);
+ boolean isInt = Math.abs(correctVal - Math.round(correctVal)) < 1e-6;
+ Set optsSet = new LinkedHashSet<>();
+ optsSet.add(correctStr);
+ Random r = new Random();
+
+ int attempts = 0;
+ while (optsSet.size() < 4 && attempts < 50) {
+ attempts++;
+ double delta = isInt ? r.nextInt(10) + 1 : (r.nextGaussian() * Math.max(1, Math.abs(correctVal) * 0.15));
+ if (isInt && r.nextBoolean()) delta = -delta;
+ optsSet.add(formatNumber(correctVal + delta));
+ }
+
+ while (optsSet.size() < 4) {
+ double delta = isInt ? r.nextInt(10) - 5 : r.nextDouble() * 2 - 1;
+ optsSet.add(formatNumber(correctVal + delta));
+ }
+
+ String[] optsArr = optsSet.toArray(new String[0]);
+ shuffleArray(optsArr);
+ correctIdx[i] = findIndex(optsArr, correctStr);
+ return optsArr;
+ }
+
+ private static void savePaper(String username, List paper) {
+ try {
+ Save.savePaper(username, new ArrayList<>(paper));
+ } catch (RuntimeException re) {
+ System.err.println("保存试卷时出错: " + re.getMessage());
+ }
+ }
+
+ private static void shuffleArray(String[] arr) {
+ Random r = new Random();
+ for (int i = arr.length - 1; i > 0; i--) {
+ int j = r.nextInt(i + 1);
+ String t = arr[i];
+ arr[i] = arr[j];
+ arr[j] = t;
+ }
+ }
+
+ private static int findIndex(String[] arr, String target) {
+ for (int i = 0; i < arr.length; i++) if (arr[i].equals(target)) return i;
+ return 0;
+ }
+
+ private static String formatNumber(double v) {
+ if (Math.abs(v - Math.round(v)) < 1e-6) return String.valueOf((long) Math.round(v));
+ return String.format("%.3f", v);
+ }
+}
+
diff --git a/src/model/ExpressionEvaluator.java b/src/model/ExpressionEvaluator.java
new file mode 100644
index 0000000..0fc214d
--- /dev/null
+++ b/src/model/ExpressionEvaluator.java
@@ -0,0 +1,131 @@
+package model;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.StringTokenizer;
+
+
+public class ExpressionEvaluator {
+
+ private static final Map PRECEDENCE = new HashMap<>();
+
+ static {
+ PRECEDENCE.put("+", 1);
+ PRECEDENCE.put("-", 1);
+ PRECEDENCE.put("*", 2);
+ PRECEDENCE.put("/", 2);
+ PRECEDENCE.put("^", 3);
+ }
+
+ // 判断是否是运算符
+ private static boolean isOperator(String token) {
+ return PRECEDENCE.containsKey(token);
+ }
+
+ // 运算符优先级
+ private static int precedence(String op) {
+ return PRECEDENCE.get(op);
+ }
+
+ // 将中缀表达式转为逆波兰表达式(RPN)
+ private static List toRPN(String expr) {
+ List output = new ArrayList<>();
+ Stack stack = new Stack<>();
+ StringTokenizer tokenizer = new StringTokenizer(expr, "+-*/^() ", true);
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken().trim();
+ if (token.isEmpty()) {
+ continue;
+ }
+
+ if (token.matches("[0-9.]+")) { // 数字
+ output.add(token);
+ } else if (token.matches("[a-zA-Z]+")) { // 函数
+ stack.push(token);
+ } else if (isOperator(token)) { // 运算符
+ while (!stack.isEmpty() && isOperator(stack.peek())
+ && precedence(stack.peek()) >= precedence(token)) {
+ output.add(stack.pop());
+ }
+ stack.push(token);
+ } else if (token.equals("(")) {
+ stack.push(token);
+ } else if (token.equals(")")) {
+ while (!stack.isEmpty() && !stack.peek().equals("(")) {
+ output.add(stack.pop());
+ }
+ if (!stack.isEmpty() && stack.peek().equals("(")) {
+ stack.pop();
+ }
+ if (!stack.isEmpty() && stack.peek().matches("[a-zA-Z]+")) {
+ output.add(stack.pop());
+ }
+ }
+ }
+
+ while (!stack.isEmpty()) {
+ output.add(stack.pop());
+ }
+
+ return output;
+ }
+
+ // 计算逆波兰表达式
+ private static double evalRPN(List rpn) {
+ Stack stack = new Stack<>();
+ for (String token : rpn) {
+ if (token.matches("[0-9.]+")) {
+ stack.push(Double.parseDouble(token));
+ } else if (isOperator(token)) {
+ double b = stack.pop();
+ double a = stack.pop();
+ switch (token) {
+ case "+":
+ stack.push(a + b);
+ break;
+ case "-":
+ stack.push(a - b);
+ break;
+ case "*":
+ stack.push(a * b);
+ break;
+ case "/":
+ stack.push(a / b);
+ break;
+ case "^":
+ stack.push(Math.pow(a, b));
+ break;
+ }
+ } else { // 函数
+ double a = stack.pop();
+ switch (token.toLowerCase()) {
+ case "sin":
+ stack.push(Math.sin(Math.toRadians(a)));
+ break;
+ case "cos":
+ stack.push(Math.cos(Math.toRadians(a)));
+ break;
+ case "tan":
+ stack.push(Math.tan(Math.toRadians(a)));
+ break;
+ case "sqrt":
+ stack.push(Math.sqrt(a));
+ break;
+ default:
+ throw new RuntimeException("未知函数: " + token);
+ }
+ }
+ }
+ return stack.pop();
+ }
+
+ // 对外接口:传入表达式字符串,返回计算结果
+ public static double evaluate(String expr) {
+ List rpn = toRPN(expr);
+ return evalRPN(rpn);
+ }
+
+}
diff --git a/src/model/Generator.java b/src/model/Generator.java
new file mode 100644
index 0000000..da2f306
--- /dev/null
+++ b/src/model/Generator.java
@@ -0,0 +1,162 @@
+package model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 具体题目生成器
+ */
+public class Generator extends QuestionGenerator {
+
+ public static final Random RAND = new Random();
+ public static final int MAX_ATTEMPTS = 2000;
+
+ public final Login.Level level;
+ public final Set existing;
+
+ public Generator(Login.Level level, List existingQuestions) {
+ this.level = level;
+ this.existing = existingQuestions.stream().map(String::trim).collect(Collectors.toSet());
+ }
+
+ public List generatePaper(int n) {
+ Set generated = new LinkedHashSet<>();
+ int attempts = 0;
+ while (generated.size() < n && attempts < MAX_ATTEMPTS) {
+ attempts++;
+ String q = generateOneQuestion();
+ String key = normalize(q);
+ if (!existing.contains(key) && !generated.contains(key)) {
+ generated.add(q);
+ }
+ }
+ if (generated.size() < n) {
+ System.out.println(
+ "注意:无法生成足够的不重复题目,已生成 " + generated.size() + " 道题(请求 " + n + " 道)");
+ }
+ return new ArrayList<>(generated);
+ }
+
+ public String normalize(String s) {
+ return s.replaceAll("\\s+", "").toLowerCase();
+ }
+
+ public String generateOneQuestion() {
+ int operands = RAND.nextInt(5) + 1; // 1..5
+ int operands_p = RAND.nextInt(4) + 2; // 2..5
+ return switch (level) {
+ case PRIMARY -> genPrimary(operands_p);
+ case MIDDLE -> genMiddle(operands);
+ case HIGH -> genHigh(operands);
+ };
+ }
+
+ public String genPrimary(int operands) {
+ if (operands == 1) {
+ return String.valueOf(randInt(1, 100));
+ }
+ List ops = Arrays.asList("+", "-", "*", "/");
+ StringBuilder sb = new StringBuilder();
+ boolean useParens = RAND.nextBoolean();
+ if (useParens && operands >= 3 && RAND.nextBoolean()) {
+ sb.append("(");
+ sb.append(randInt(1, 100)).append(" ").append(randomChoice(ops)).append(" ")
+ .append(randInt(1, 100));
+ sb.append(")");
+ for (int i = 2; i < operands; i++) {
+ sb.append(" ").append(randomChoice(ops)).append(" ").append(randInt(1, 100));
+ }
+ } else {
+ sb.append(randInt(1, 100));
+ for (int i = 1; i < operands; i++) {
+ sb.append(" ").append(randomChoice(ops)).append(" ").append(randInt(1, 100));
+ }
+ }
+ return sb.toString();
+ }
+
+ public String genMiddle(int operands) {
+ String expr = genPrimary(operands);
+ if (RAND.nextBoolean()) {
+ expr = applySquare(expr);
+ } else {
+ expr = applySqrt(expr);
+ }
+ return expr;
+ }
+
+ public String genHigh(int operands) {
+ String expr = genPrimary(operands);
+ expr = applyTrig(expr);
+ return expr;
+ }
+
+ public String applySquare(String expr) {
+ List spans = findNumberSpans(expr);
+ if (spans.isEmpty()) {
+ return expr + "^2";
+ }
+ int[] s = spans.get(RAND.nextInt(spans.size()));
+ String before = expr.substring(0, s[0]);
+ String num = expr.substring(s[0], s[1]);
+ String after = expr.substring(s[1]);
+ return before + "(" + num + ")^2" + after;
+ }
+
+ public String applySqrt(String expr) {
+ List spans = findNumberSpans(expr);
+ if (spans.isEmpty()) {
+ return "sqrt(" + expr + ")";
+ }
+ int[] s = spans.get(RAND.nextInt(spans.size()));
+ String before = expr.substring(0, s[0]);
+ String num = expr.substring(s[0], s[1]);
+ String after = expr.substring(s[1]);
+ return before + "sqrt(" + num + ")" + after;
+ }
+
+ public String applyTrig(String expr) {
+ List spans = findNumberSpans(expr);
+ String func = randomChoice(Arrays.asList("sin", "cos", "tan"));
+ if (spans.isEmpty()) {
+ return func + "(" + expr + ")";
+ }
+ int[] s = spans.get(RAND.nextInt(spans.size()));
+ String before = expr.substring(0, s[0]);
+ String num = expr.substring(s[0], s[1]);
+ String after = expr.substring(s[1]);
+ return before + func + "(" + num + ")" + after;
+ }
+
+ public List findNumberSpans(String expr) {
+ List spans = new ArrayList<>();
+ char[] chs = expr.toCharArray();
+ int i = 0, n = chs.length;
+ while (i < n) {
+ if (Character.isDigit(chs[i])) {
+ int j = i;
+ while (j < n && (Character.isDigit(chs[j]))) {
+ j++;
+ }
+ spans.add(new int[]{i, j});
+ i = j;
+ } else {
+ i++;
+ }
+ }
+ return spans;
+ }
+
+ public int randInt(int a, int b) {
+ return RAND.nextInt(b - a + 1) + a;
+ }
+
+ public T randomChoice(List list) {
+ return list.get(RAND.nextInt(list.size()));
+ }
+}
diff --git a/src/model/LanguageSwitch.java b/src/model/LanguageSwitch.java
new file mode 100644
index 0000000..a3c0501
--- /dev/null
+++ b/src/model/LanguageSwitch.java
@@ -0,0 +1,23 @@
+package model;
+
+public class LanguageSwitch {
+
+ public static String levelToChinese(Login.Level l) {
+ return switch (l) {
+ case PRIMARY -> "小学";
+ case MIDDLE -> "初中";
+ case HIGH -> "高中";
+ default -> "未知";
+ };
+ }
+
+ public static Login.Level chineseToLevel(String s) {
+ s = s.trim();
+ return switch (s) {
+ case "小学" -> Login.Level.PRIMARY;
+ case "初中" -> Login.Level.MIDDLE;
+ case "高中" -> Login.Level.HIGH;
+ default -> null;
+ };
+ }
+}
diff --git a/src/model/LoadFile.java b/src/model/LoadFile.java
new file mode 100644
index 0000000..cb8a7a5
--- /dev/null
+++ b/src/model/LoadFile.java
@@ -0,0 +1,60 @@
+package model;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * 读取用户文件夹下已有题目的所有题目文本(用于查重)
+ */
+public class LoadFile {
+
+ public static List loadExistingQuestions(String username) {
+ List all = new ArrayList<>();
+ Path userDir = Paths.get("data", username);
+ if (!Files.exists(userDir)) {
+ return all;
+ }
+ try (DirectoryStream ds = Files.newDirectoryStream(userDir, "*.txt")) {
+ for (Path p : ds) {
+ List lines = Files.readAllLines(p, StandardCharsets.UTF_8);
+ StringBuilder cur = new StringBuilder();
+ for (String line : lines) {
+ if (line.matches("^\\s*\\d+\\..*")) {
+ if (!cur.isEmpty()) {
+ all.add(cur.toString().trim());
+ }
+ cur.setLength(0);
+ cur.append(line.replaceFirst("^\\s*\\d+\\.", "").trim());
+ } else {
+ if (line.trim().isEmpty()) {
+ if (!cur.isEmpty()) {
+ all.add(cur.toString().trim());
+ cur.setLength(0);
+ }
+ } else {
+ if (!cur.isEmpty()) {
+ cur.append(" ");
+ }
+ cur.append(line.trim());
+ }
+ }
+ }
+ if (!cur.isEmpty()) {
+ all.add(cur.toString().trim());
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("读取题目文件失败:" + e.getMessage(), e);
+ }
+ return all.stream().map(String::trim).filter(s -> !s.isEmpty()).distinct()
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/model/Login.java b/src/model/Login.java
new file mode 100644
index 0000000..54192f2
--- /dev/null
+++ b/src/model/Login.java
@@ -0,0 +1,177 @@
+package model;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * 用户管理(包含预设账户与简单文件持久化)
+ */
+public class Login {
+
+ public enum Level {PRIMARY, MIDDLE, HIGH}
+
+ public static class Account implements Serializable {
+
+ public String username;
+ public String password; // 明文存储(课程项目允许),真实项目请哈希
+ public Level level;
+ public String email;
+
+ public Account(String u, String p, Level l, String email) {
+ this.username = u;
+ this.password = p;
+ this.level = l;
+ this.email = email;
+ }
+ }
+
+ // 内存账户表
+ private static final Map accounts = new HashMap<>();
+ private static final Path USERS_FILE = Paths.get("data", "users.cfg"); // 简单序列化
+
+ static {
+ // 预设账号(与你的个人项目一致)
+ accounts.put("张三1", new Account("张三1", "123", Level.PRIMARY, "zhangsan1@example.com"));
+ accounts.put("张三2", new Account("张三2", "123", Level.PRIMARY, "zhangsan2@example.com"));
+ accounts.put("张三3", new Account("张三3", "123", Level.PRIMARY, "zhangsan3@example.com"));
+
+ accounts.put("李四1", new Account("李四1", "123", Level.MIDDLE, "lishi1@example.com"));
+ accounts.put("李四2", new Account("李四2", "123", Level.MIDDLE, "lishi2@example.com"));
+ accounts.put("李四3", new Account("李四3", "123", Level.MIDDLE, "lishi3@example.com"));
+
+ accounts.put("王五1", new Account("王五1", "123", Level.HIGH, "wangwu1@example.com"));
+ accounts.put("王五2", new Account("王五2", "123", Level.HIGH, "wangwu2@example.com"));
+ accounts.put("王五3", new Account("王五3", "123", Level.HIGH, "wangwu3@example.com"));
+
+ // 加载自文件的额外用户
+ loadFromFile();
+ }
+
+ // 登录验证(GUI 调用)
+ public static Account login(String username, String password) {
+ Account acc = accounts.get(username);
+ if (acc != null && acc.password.equals(password)) {
+ return acc;
+ }
+ return null;
+ }
+
+ // 注册(GUI 调用),若用户名已存在返回 false
+ // 注册(GUI 调用),若用户名或邮箱已存在返回 false
+ public static synchronized boolean register(String username, String password, Level level,
+ String email) {
+ // 检查用户名是否重复
+ if (accounts.containsKey(username)) {
+ return false;
+ }
+
+ // 检查邮箱是否重复
+ for (Account existing : accounts.values()) {
+ if (existing.email != null && existing.email.equalsIgnoreCase(email)) {
+ // 邮箱重复,不区分大小写
+ return false;
+ }
+ }
+
+ // 都不重复,则注册成功
+ Account acc = new Account(username, password, level, email);
+ accounts.put(username, acc);
+ persistToFile();
+ return true;
+ }
+
+
+ // 修改密码(需提供原密码),返回是否成功
+ public static synchronized boolean changePassword(String username, String oldPwd, String newPwd) {
+ Account acc = accounts.get(username);
+ if (acc == null) {
+ return false;
+ }
+ if (!acc.password.equals(oldPwd)) {
+ return false;
+ }
+ acc.password = newPwd;
+ persistToFile();
+ return true;
+ }
+
+ // 文件持久化(非常简单的 CSV-like 方案)
+ private static void persistToFile() {
+ try {
+ Path dir = USERS_FILE.getParent();
+ if (dir != null && !Files.exists(dir)) {
+ Files.createDirectories(dir);
+ }
+ try (BufferedWriter bw = Files.newBufferedWriter(USERS_FILE, StandardCharsets.UTF_8)) {
+ for (Account a : accounts.values()) {
+ // 格式: username\tpassword\tlevel\temail
+ bw.write(
+ a.username + "\t" + a.password + "\t" + a.level.name() + "\t" + (a.email == null ? ""
+ : a.email));
+ bw.newLine();
+ }
+ }
+ } catch (IOException e) {
+ System.err.println("保存用户文件失败: " + e.getMessage());
+ }
+ }
+
+ private static void loadFromFile() {
+ if (!Files.exists(USERS_FILE)) {
+ return;
+ }
+ try {
+ for (String line : Files.readAllLines(USERS_FILE, StandardCharsets.UTF_8)) {
+ if (line.trim().isEmpty()) {
+ continue;
+ }
+ String[] p = line.split("\t");
+ if (p.length >= 3) {
+ String username = p[0], password = p[1];
+ Level level = Level.valueOf(p[2]);
+ String email = p.length >= 4 ? p[3] : "";
+ // 不覆盖预设同名账号(以文件为准覆盖预设)
+ accounts.put(username, new Account(username, password, level, email));
+ }
+ }
+ } catch (IOException e) {
+ System.err.println("读取用户文件失败: " + e.getMessage());
+ } catch (Exception ex) {
+ System.err.println("解析用户文件异常: " + ex.getMessage());
+ }
+ }
+
+ // 验证密码复杂度:6-10 位,必须含大写、小写和数字
+ public static boolean validatePasswordRules(String pwd) {
+ if (pwd == null) {
+ return false;
+ }
+ if (pwd.length() < 6 || pwd.length() > 10) {
+ return false;
+ }
+ boolean hasUpper = false, hasLower = false, hasDigit = false;
+ for (char c : pwd.toCharArray()) {
+ if (Character.isUpperCase(c)) {
+ hasUpper = true;
+ } else if (Character.isLowerCase(c)) {
+ hasLower = true;
+ } else if (Character.isDigit(c)) {
+ hasDigit = true;
+ }
+ }
+ return hasUpper && hasLower && hasDigit;
+ }
+
+ // 供界面显示用户列表(调试)
+ public static Map getAccounts() {
+ return accounts;
+ }
+}
diff --git a/src/model/Paper.java b/src/model/Paper.java
new file mode 100644
index 0000000..f2e73a4
--- /dev/null
+++ b/src/model/Paper.java
@@ -0,0 +1,23 @@
+package model;
+
+import java.util.List;
+
+/**
+ * 试卷封装:每一道题为字符串(题干),每题有四个选项(字符串),且标记正确选项索引
+ */
+public class Paper {
+
+ public final List questions;
+ public final List options; // 每题 options[i] 长度为4
+ public final int[] correctIndex; // 每题正确选项下标 0..3
+
+ public Paper(List questions, List options, int[] correctIndex) {
+ this.questions = questions;
+ this.options = options;
+ this.correctIndex = correctIndex;
+ }
+
+ public int size() {
+ return questions.size();
+ }
+}
diff --git a/src/model/QuestionGenerator.java b/src/model/QuestionGenerator.java
new file mode 100644
index 0000000..8bdba3f
--- /dev/null
+++ b/src/model/QuestionGenerator.java
@@ -0,0 +1,33 @@
+package model;
+
+import java.util.List;
+
+public abstract class QuestionGenerator {
+
+ protected QuestionGenerator() {
+ }
+
+ public abstract List generatePaper(int n);
+
+ public abstract String normalize(String s);
+
+ public abstract String generateOneQuestion();
+
+ public abstract String genPrimary(int operands);
+
+ public abstract String genMiddle(int operands);
+
+ public abstract String genHigh(int operands);
+
+ public abstract String applySquare(String expr);
+
+ public abstract String applySqrt(String expr);
+
+ public abstract String applyTrig(String expr);
+
+ public abstract java.util.List findNumberSpans(String expr);
+
+ public abstract int randInt(int a, int b);
+
+ public abstract T randomChoice(java.util.List list);
+}
diff --git a/src/model/Save.java b/src/model/Save.java
new file mode 100644
index 0000000..8b5cd7d
--- /dev/null
+++ b/src/model/Save.java
@@ -0,0 +1,37 @@
+package model;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+public class Save {
+
+ public static String savePaper(String username, List paper) {
+ Path userDir = Paths.get("data", username);
+ try {
+ if (!Files.exists(userDir)) {
+ Files.createDirectories(userDir);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("无法创建用户文件夹:" + e.getMessage(), e);
+ }
+ String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date());
+ Path file = userDir.resolve(timestamp + ".txt");
+ try (BufferedWriter bw = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
+ for (int i = 0; i < paper.size(); i++) {
+ bw.write((i + 1) + ". " + paper.get(i));
+ bw.newLine();
+ bw.newLine();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("保存文件失败:" + e.getMessage(), e);
+ }
+ return file.toString();
+ }
+}
diff --git a/src/view/ExamFrame.java b/src/view/ExamFrame.java
new file mode 100644
index 0000000..eda873e
--- /dev/null
+++ b/src/view/ExamFrame.java
@@ -0,0 +1,172 @@
+package view;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.ButtonGroup;
+import javax.swing.BorderFactory;
+import javax.swing.SwingConstants;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+
+import model.Paper;
+import model.Login;
+
+/**
+ * 答题界面(选择题,四选一)
+ */
+public class ExamFrame extends JFrame {
+
+ private Paper paper;
+ private Login.Account user;
+ private int index = 0;
+ private int score = 0;
+
+ private JLabel qLabel;
+ private JRadioButton[] radioBtns = new JRadioButton[4];
+ private ButtonGroup group;
+ private JButton submitBtn;
+ private JButton quitBtn;
+
+ private JPanel centerPanel;
+ private JPanel bottomPanel;
+
+ public ExamFrame(Paper paper, Login.Account user) {
+ this.paper = paper;
+ this.user = user;
+
+ setTitle("答题 - " + user.username);
+ setSize(700, 400);
+ setLocationRelativeTo(null);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ qLabel = new JLabel("", SwingConstants.LEFT);
+ qLabel.setFont(new Font("Serif", Font.PLAIN, 16));
+ JPanel top = new JPanel(new BorderLayout());
+ top.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ top.add(qLabel, BorderLayout.CENTER);
+
+ centerPanel = new JPanel(new GridLayout(4, 1, 6, 6));
+ group = new ButtonGroup();
+ for (int i = 0; i < 4; i++) {
+ radioBtns[i] = new JRadioButton();
+ group.add(radioBtns[i]);
+ centerPanel.add(radioBtns[i]);
+ }
+
+ bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ submitBtn = new JButton("提交当前题");
+ quitBtn = new JButton("结束并查看成绩");
+ bottomPanel.add(quitBtn);
+ bottomPanel.add(submitBtn);
+
+ add(top, BorderLayout.NORTH);
+ add(centerPanel, BorderLayout.CENTER);
+ add(bottomPanel, BorderLayout.SOUTH);
+
+ submitBtn.addActionListener(e -> submitAnswer());
+ quitBtn.addActionListener(e -> finishExam());
+
+ showQuestion();
+ setVisible(true);
+ }
+
+ /**
+ * 显示一道题
+ */
+ private void showQuestion() {
+ if (index >= paper.size()) {
+ showSubmitPage();
+ return;
+ }
+ String q = paper.questions.get(index);
+ qLabel.setText("第 " + (index + 1) + " 题: " + q + "");
+ String[] opts = paper.options.get(index);
+
+ for (int i = 0; i < 4; i++) {
+ radioBtns[i].setText(opts[i]);
+ }
+ // 清除上一次选择
+ group.clearSelection();
+ }
+
+ /**
+ * 提交当前题
+ */
+ private void submitAnswer() {
+ int selected = -1;
+ for (int i = 0; i < 4; i++) {
+ if (radioBtns[i].isSelected()) {
+ selected = i;
+ }
+ }
+ if (selected == -1) {
+ JOptionPane.showMessageDialog(this, "请选择一个选项后再提交");
+ return;
+ }
+ // 判断对错
+ if (selected == paper.correctIndex[index]) {
+ score++;
+ }
+
+ index++;
+ if (index < paper.size()) {
+ showQuestion();
+ } else {
+ // 做完所有题 → 显示提交确认页面
+ showSubmitPage();
+ }
+ }
+
+ /**
+ * 做完所有题目后显示确认提交界面
+ */
+ private void showSubmitPage() {
+ // 移除中间题目区域和按钮
+ getContentPane().remove(centerPanel);
+ getContentPane().remove(bottomPanel);
+
+ JLabel finishLabel = new JLabel("已完成所有题目,是否确认提交?", SwingConstants.CENTER);
+ finishLabel.setFont(new Font("Serif", Font.BOLD, 18));
+ add(finishLabel, BorderLayout.CENTER);
+
+ JPanel confirmPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
+ JButton confirmBtn = new JButton("确认提交");
+ JButton cancelBtn = new JButton("返回最后一题");
+ confirmPanel.add(cancelBtn);
+ confirmPanel.add(confirmBtn);
+ add(confirmPanel, BorderLayout.SOUTH);
+
+ confirmBtn.addActionListener(e -> finishExam());
+ cancelBtn.addActionListener(e -> {
+ // 返回最后一题继续查看/修改
+ getContentPane().remove(finishLabel);
+ getContentPane().remove(confirmPanel);
+ add(centerPanel, BorderLayout.CENTER);
+ add(bottomPanel, BorderLayout.SOUTH);
+ index = paper.size() - 1;
+ showQuestion();
+ revalidate();
+ repaint();
+ });
+
+ revalidate();
+ repaint();
+ }
+
+ /**
+ * 完成考试 → 进入成绩界面
+ */
+ private void finishExam() {
+ int total = paper.size();
+ double percent = total == 0 ? 0 : (100.0 * score / total);
+ dispose();
+ new ResultFrame(user, score, total, percent);
+ }
+}
diff --git a/src/view/ExamSetupFrame.java b/src/view/ExamSetupFrame.java
new file mode 100644
index 0000000..0da4e57
--- /dev/null
+++ b/src/view/ExamSetupFrame.java
@@ -0,0 +1,76 @@
+package view;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.BorderFactory;
+
+import java.awt.GridLayout;
+
+import controller.FunctionController;
+import model.Login;
+import model.LanguageSwitch;
+
+/**
+ * 出题设置:选择/切换难度(显示当前账户默认难度),输入题数(10-30)
+ */
+public class ExamSetupFrame extends JFrame {
+
+ private JTextField numberField;
+ private JComboBox levelBox;
+ private FunctionController controller;
+ private Login.Account user;
+
+ public ExamSetupFrame(Login.Account user) {
+ this.user = user;
+ this.controller = new FunctionController(user);
+
+ setTitle("出题设置 - " + user.username);
+ setSize(380, 200);
+ setLocationRelativeTo(null);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ JPanel p = new JPanel(new GridLayout(4, 2, 8, 8));
+ p.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+ p.add(new JLabel("当前难度:"));
+ levelBox = new JComboBox<>(new String[]{"小学", "初中", "高中"});
+ levelBox.setSelectedItem(LanguageSwitch.levelToChinese(user.level));
+ p.add(levelBox);
+
+ p.add(new JLabel("输入题目数量 (10-30):"));
+ numberField = new JTextField("10");
+ p.add(numberField);
+
+ JButton startBtn = new JButton("开始出题");
+ JButton backBtn = new JButton("返回登录");
+ p.add(startBtn);
+ p.add(backBtn);
+
+ add(p);
+
+ startBtn.addActionListener(e -> {
+ try {
+ int n = Integer.parseInt(numberField.getText().trim());
+ // 更新用户难度为界面选择
+ String lv = (String) levelBox.getSelectedItem();
+ Login.Level newLv = LanguageSwitch.chineseToLevel(lv);
+ user.level = newLv;
+ controller.startExam(n);
+ dispose();
+ } catch (NumberFormatException ex) {
+ JOptionPane.showMessageDialog(this, "请输入有效整数");
+ }
+ });
+
+ backBtn.addActionListener(e -> {
+ dispose();
+ new LoginFrame();
+ });
+
+ setVisible(true);
+ }
+}
diff --git a/src/view/LoginFrame.java b/src/view/LoginFrame.java
new file mode 100644
index 0000000..bda2dbb
--- /dev/null
+++ b/src/view/LoginFrame.java
@@ -0,0 +1,78 @@
+package view;
+
+import java.awt.GridLayout;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.JPasswordField;
+import javax.swing.BorderFactory;
+
+import model.Login;
+
+/**
+ * 登录窗口
+ */
+public class LoginFrame extends JFrame {
+
+ private JTextField usernameField;
+ private JPasswordField passwordField;
+
+ public LoginFrame() {
+ setTitle("数学卷子生成器 - 登录");
+ setSize(380, 220);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setLocationRelativeTo(null);
+
+ JPanel p = new JPanel(new GridLayout(4, 2, 8, 8));
+ p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ p.add(new JLabel("用户名:"));
+ usernameField = new JTextField();
+ p.add(usernameField);
+
+ p.add(new JLabel("密码:"));
+ passwordField = new JPasswordField();
+ p.add(passwordField);
+
+ JButton loginBtn = new JButton("登录");
+ JButton regBtn = new JButton("注册");
+ JButton exitBtn = new JButton("退出");
+
+ p.add(loginBtn);
+ p.add(regBtn);
+ p.add(new JLabel());
+ p.add(exitBtn);
+
+ add(p);
+
+ loginBtn.addActionListener(e -> {
+ String u = usernameField.getText().trim();
+ String pwd = new String(passwordField.getPassword()).trim();
+ if (u.isEmpty() || pwd.isEmpty()) {
+ JOptionPane.showMessageDialog(this, "请输入用户名和密码", "提示",
+ JOptionPane.INFORMATION_MESSAGE);
+ return;
+ }
+ Login.Account acc = Login.login(u, pwd);
+ if (acc != null) {
+ dispose();
+ new MainMenuFrame(acc); // 登录后先进入主菜单
+ } else {
+ JOptionPane.showMessageDialog(this, "用户名或密码错误", "登录失败",
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ });
+
+ regBtn.addActionListener(e -> {
+ new RegisterFrame(this);
+ });
+
+ exitBtn.addActionListener(e -> System.exit(0));
+
+ setVisible(true);
+ }
+}
diff --git a/src/view/MainMenuFrame.java b/src/view/MainMenuFrame.java
new file mode 100644
index 0000000..7fdc377
--- /dev/null
+++ b/src/view/MainMenuFrame.java
@@ -0,0 +1,97 @@
+package view;
+
+import java.awt.GridLayout;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.BorderFactory;
+
+import model.Login;
+
+
+/**
+ * 登录成功后的主菜单(可修改密码、开始出题、登出) 现在由 ExamSetupFrame 直接替代,但保留此类可拓展
+ */
+public class MainMenuFrame extends JFrame {
+
+ public MainMenuFrame(Login.Account user) {
+ setTitle("主菜单 - " + user.username);
+ setSize(400, 200);
+ setLocationRelativeTo(null);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ JPanel p = new JPanel(new GridLayout(3, 1, 10, 10));
+ JButton changePwd = new JButton("修改密码");
+ JButton start = new JButton("出题设置");
+ JButton logout = new JButton("退出登录");
+ p.add(start);
+ p.add(changePwd);
+ p.add(logout);
+ add(p);
+
+ start.addActionListener(e -> {
+ new ExamSetupFrame(user);
+ dispose();
+ });
+ changePwd.addActionListener(e -> new ChangePasswordDialog(this, user));
+ logout.addActionListener(e -> {
+ dispose();
+ new LoginFrame();
+ });
+
+ setVisible(true);
+ }
+
+ // 内部类:修改密码对话框
+ static class ChangePasswordDialog extends JDialog {
+
+ public ChangePasswordDialog(JFrame owner, Login.Account user) {
+ super(owner, "修改密码", true);
+ setSize(350, 200);
+ setLocationRelativeTo(owner);
+ JPanel p = new JPanel(new GridLayout(4, 2, 6, 6));
+ p.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
+ p.add(new JLabel("旧密码:"));
+ JPasswordField oldp = new JPasswordField();
+ p.add(oldp);
+ p.add(new JLabel("新密码:"));
+ JPasswordField newp = new JPasswordField();
+ p.add(newp);
+ p.add(new JLabel("再次输入新密码:"));
+ JPasswordField newp2 = new JPasswordField();
+ p.add(newp2);
+ JButton ok = new JButton("确定");
+ JButton cancel = new JButton("取消");
+ p.add(ok);
+ p.add(cancel);
+ add(p);
+ ok.addActionListener(a -> {
+ String oldPwd = new String(oldp.getPassword()).trim();
+ String np = new String(newp.getPassword()).trim();
+ String np2 = new String(newp2.getPassword()).trim();
+ if (!np.equals(np2)) {
+ JOptionPane.showMessageDialog(this, "两次密码不一致");
+ return;
+ }
+ if (!Login.validatePasswordRules(np)) {
+ JOptionPane.showMessageDialog(this, "密码不满足规则");
+ return;
+ }
+ boolean okr = Login.changePassword(user.username, oldPwd, np);
+ if (okr) {
+ JOptionPane.showMessageDialog(this, "修改成功");
+ dispose();
+ } else {
+ JOptionPane.showMessageDialog(this, "旧密码错误");
+ }
+ });
+ cancel.addActionListener(a -> dispose());
+ setVisible(true);
+ }
+ }
+}
diff --git a/src/view/RegisterFrame.java b/src/view/RegisterFrame.java
new file mode 100644
index 0000000..a08c1a6
--- /dev/null
+++ b/src/view/RegisterFrame.java
@@ -0,0 +1,166 @@
+package view;
+
+import java.awt.GridLayout;
+import java.util.Properties;
+import java.util.Random;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JTextField;
+import javax.swing.BorderFactory;
+
+import jakarta.mail.Authenticator;
+import jakarta.mail.Message;
+import jakarta.mail.PasswordAuthentication;
+import jakarta.mail.Session;
+import jakarta.mail.Transport;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+
+import model.Login;
+import model.LanguageSwitch;
+
+/**
+ * 注册界面(使用 QQ 邮箱发送验证码)
+ */
+public class RegisterFrame extends JDialog {
+
+ private JTextField usernameField;
+ private JTextField emailField;
+ private JComboBox levelBox;
+ private JPasswordField pwdField;
+ private JPasswordField pwdField2;
+ private JTextField codeField;
+ private String lastCode;
+
+ // QQ 邮箱配置
+ private static final String FROM_EMAIL = "songqifeng.sqf@qq.com";
+ private static final String AUTH_CODE = "gcyschltjgxedgjd"; // ⚠️ 在 QQ 邮箱里申请的授权码
+
+ public RegisterFrame(JFrame owner) {
+ super(owner, "注册新用户", true);
+ setSize(420, 320);
+ setLocationRelativeTo(owner);
+
+ JPanel p = new JPanel(new GridLayout(7, 2, 6, 6));
+ p.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+ p.add(new JLabel("用户名:"));
+ usernameField = new JTextField();
+ p.add(usernameField);
+
+ p.add(new JLabel("邮箱(接收注册码):"));
+ emailField = new JTextField();
+ p.add(emailField);
+
+ p.add(new JLabel("年级:"));
+ levelBox = new JComboBox<>(new String[]{"小学", "初中", "高中"});
+ p.add(levelBox);
+
+ p.add(new JLabel("密码 (6-10位,含大小写与数字):"));
+ pwdField = new JPasswordField();
+ p.add(pwdField);
+
+ p.add(new JLabel("再次输入密码:"));
+ pwdField2 = new JPasswordField();
+ p.add(pwdField2);
+
+ JButton sendCodeBtn = new JButton("发送注册码");
+ p.add(sendCodeBtn);
+ codeField = new JTextField();
+ p.add(codeField);
+
+ JButton regBtn = new JButton("注册");
+ p.add(regBtn);
+ JButton cancelBtn = new JButton("取消");
+ p.add(cancelBtn);
+
+ add(p);
+
+ sendCodeBtn.addActionListener(e -> {
+ String email = emailField.getText().trim();
+ if (email.isEmpty() || !email.contains("@")) {
+ JOptionPane.showMessageDialog(this, "请输入有效邮箱");
+ return;
+ }
+ lastCode = String.format("%04d", new Random().nextInt(10000));
+ boolean sent = sendEmail(email, lastCode);
+ if (sent) {
+ JOptionPane.showMessageDialog(this, "注册码已发送,请检查邮箱。");
+ } else {
+ JOptionPane.showMessageDialog(this, "发送邮件失败,请检查网络或邮箱配置。");
+ }
+ });
+
+ regBtn.addActionListener(e -> {
+ String u = usernameField.getText().trim();
+ String email = emailField.getText().trim();
+ String pwd = new String(pwdField.getPassword()).trim();
+ String pwd2 = new String(pwdField2.getPassword()).trim();
+ String code = codeField.getText().trim();
+
+ if (u.isEmpty() || email.isEmpty() || pwd.isEmpty() || pwd2.isEmpty() || code.isEmpty()) {
+ JOptionPane.showMessageDialog(this, "请填写完整信息并输入注册码");
+ return;
+ }
+ if (!code.equals(lastCode)) {
+ JOptionPane.showMessageDialog(this, "注册码错误,请重新输入");
+ return;
+ }
+ if (!pwd.equals(pwd2)) {
+ JOptionPane.showMessageDialog(this, "两次密码不一致");
+ return;
+ }
+ if (!Login.validatePasswordRules(pwd)) {
+ JOptionPane.showMessageDialog(this, "密码不满足要求:6-10位且包含大写、小写和数字");
+ return;
+ }
+ String levelStr = (String) levelBox.getSelectedItem();
+ Login.Level lv = LanguageSwitch.chineseToLevel(levelStr);
+ boolean ok = Login.register(u, pwd, lv, email);
+ if (!ok) {
+ JOptionPane.showMessageDialog(this, "用户名或邮箱已存在,请换一个用户名");
+ return;
+ }
+ JOptionPane.showMessageDialog(this, "注册成功,请用新用户登录");
+ dispose();
+ });
+
+ cancelBtn.addActionListener(e -> dispose());
+
+ setVisible(true);
+ }
+
+ private boolean sendEmail(String to, String code) {
+ try {
+ Properties props = new Properties();
+ props.put("mail.smtp.host", "smtp.qq.com");
+ props.put("mail.smtp.port", "465");
+ props.put("mail.smtp.auth", "true");
+ props.put("mail.smtp.ssl.enable", "true"); // ← 开启 SSL
+
+ Session session = Session.getInstance(props, new Authenticator() {
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(FROM_EMAIL, AUTH_CODE);
+ }
+ });
+
+ Message message = new MimeMessage(session);
+ message.setFrom(new InternetAddress(FROM_EMAIL));
+ message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
+ message.setSubject("数学卷子生成器 - 注册验证码");
+ message.setText("您的注册码为: " + code + "\n有效期 5 分钟。");
+
+ Transport.send(message);
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+}
diff --git a/src/view/ResultFrame.java b/src/view/ResultFrame.java
new file mode 100644
index 0000000..a5186c4
--- /dev/null
+++ b/src/view/ResultFrame.java
@@ -0,0 +1,47 @@
+package view;
+
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import model.Login;
+
+/**
+ * 显示成绩并提供“退出或继续做题”的选择
+ */
+public class ResultFrame extends JFrame {
+
+ public ResultFrame(Login.Account user, int score, int total, double percent) {
+ setTitle("成绩 - " + user.username);
+ setSize(360, 220);
+ setLocationRelativeTo(null);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ JPanel p = new JPanel(new GridLayout(5, 1, 6, 6));
+ p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ p.add(new JLabel("用户名: " + user.username));
+ p.add(new JLabel("得分: " + score + " / " + total));
+ p.add(new JLabel(String.format("百分比: %.2f%%", percent)));
+
+ JPanel btns = new JPanel(new FlowLayout());
+ JButton exit = new JButton("退出");
+ JButton again = new JButton("继续做题");
+ btns.add(again);
+ btns.add(exit);
+ p.add(btns);
+
+ add(p);
+ exit.addActionListener(e -> System.exit(0));
+ again.addActionListener(e -> {
+ dispose();
+ new ExamSetupFrame(user);
+ });
+
+ setVisible(true);
+ }
+}