diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c326e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# IDE files +.idea/ +*.iml +*.iws + +# Build outputs +target/ +build/ +out/ +bin/ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log + +# Maven +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup diff --git a/src/ExamManager.java b/src/ExamManager.java new file mode 100644 index 0000000..986ecfc --- /dev/null +++ b/src/ExamManager.java @@ -0,0 +1,24 @@ +import java.util.*; + +public class ExamManager { + private QuestionGenerator generator = new QuestionGenerator(); + private Set history = new HashSet<>(); + private String type; + + public ExamManager(String type){ + this.type = type; + } + + public List generateExam(int numQuestions){ + List questions = new ArrayList<>(); + for(int i=0;i userDatabase = new HashMap<>(); // 存储邮箱 -> 密码 + private Map currentUser = new HashMap<>(); // 存储当前登录用户信息 + private List currentExam = new ArrayList<>(); + private int currentIndex = 0; + private int score = 0; + private ExamManager examManager; + private QuestionGenerator questionGenerator = new QuestionGenerator(); + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage stage) { + primaryStage = stage; + primaryStage.setTitle("小初高数学学习软件"); + primaryStage.setScene(buildLoginScene()); + primaryStage.show(); + } + + // 登录界面 + private Scene buildLoginScene() { + VBox root = new VBox(10); + root.setStyle("-fx-padding: 20;"); + Label titleLabel = new Label("数学学习软件 - 登录"); + titleLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold;"); + + TextField emailField = new TextField(); + emailField.setPromptText("请输入邮箱"); + PasswordField passwordField = new PasswordField(); + passwordField.setPromptText("请输入密码"); + + Button loginBtn = new Button("登录"); + Button registerBtn = new Button("前往注册"); + Label infoLabel = new Label(); + + // 登录按钮事件 + loginBtn.setOnAction(e -> { + String email = emailField.getText().trim(); + String password = passwordField.getText(); + + if (email.isEmpty() || password.isEmpty()) { + infoLabel.setText("请输入邮箱和密码"); + return; + } + + if (!userDatabase.containsKey(email)) { + infoLabel.setText("用户不存在"); + return; + } + + if (!userDatabase.get(email).equals(password)) { + infoLabel.setText("密码错误"); + return; + } + + // 登录成功 + currentUser.put("email", email); + infoLabel.setText("登录成功!"); + primaryStage.setScene(buildLevelSelectionScene()); + }); + + // 注册按钮事件 + registerBtn.setOnAction(e -> { + primaryStage.setScene(buildRegisterScene()); + }); + + root.getChildren().addAll(titleLabel, emailField, passwordField, loginBtn, registerBtn, infoLabel); + return new Scene(root, 400, 300); + } + + // 注册界面 + private Scene buildRegisterScene() { + VBox root = new VBox(10); + root.setStyle("-fx-padding: 20;"); + Label titleLabel = new Label("用户注册"); + titleLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold;"); + + TextField emailField = new TextField(); + emailField.setPromptText("请输入邮箱"); + Button sendCodeBtn = new Button("发送验证码"); + TextField codeField = new TextField(); + codeField.setPromptText("请输入验证码"); + PasswordField passwordField = new PasswordField(); + passwordField.setPromptText("设置密码 (6-10位,含大小写字母和数字)"); + PasswordField confirmPasswordField = new PasswordField(); + confirmPasswordField.setPromptText("确认密码"); + Button registerBtn = new Button("注册"); + Button backBtn = new Button("返回登录"); + Label infoLabel = new Label(); + + sendCodeBtn.setOnAction(e -> { + String email = emailField.getText().trim(); + if (!email.contains("@")) { + infoLabel.setText("邮箱格式不正确"); + return; + } + if (userDatabase.containsKey(email)) { + infoLabel.setText("该邮箱已注册"); + return; + } + + generatedCode = String.format("%06d", new Random().nextInt(999999)); + infoLabel.setText("验证码发送中..."); + + new Thread(() -> { + boolean success = mailSender.sendMail(email, "注册验证码", "你的验证码为:" + generatedCode); + Platform.runLater(() -> { + if (success) { + infoLabel.setText("验证码已发送,请查看邮箱"); + } else { + infoLabel.setText("发送失败,请检查邮箱"); + } + }); + }).start(); + }); + + registerBtn.setOnAction(e -> { + String email = emailField.getText().trim(); + String inputCode = codeField.getText().trim(); + String pw = passwordField.getText(); + String pwConfirm = confirmPasswordField.getText(); + + if (!inputCode.equals(generatedCode)) { + infoLabel.setText("验证码错误"); + } else if (!pw.equals(pwConfirm)) { + infoLabel.setText("两次密码不一致"); + } else if (!isValidPassword(pw)) { + infoLabel.setText("密码必须6-10位,包含大小写字母和数字"); + } else if (userDatabase.containsKey(email)) { + infoLabel.setText("邮箱已注册"); + } else { + userDatabase.put(email, pw); + infoLabel.setText("注册成功!"); + // 注册成功后自动登录 + currentUser.put("email", email); + primaryStage.setScene(buildLevelSelectionScene()); + } + }); + + backBtn.setOnAction(e -> { + primaryStage.setScene(buildLoginScene()); + }); + + root.getChildren().addAll(titleLabel, emailField, sendCodeBtn, codeField, + passwordField, confirmPasswordField, registerBtn, backBtn, infoLabel); + return new Scene(root, 400, 400); + } + + // 密码验证方法 + private boolean isValidPassword(String password) { + if (password.length() < 6 || password.length() > 10) { + return false; + } + boolean hasUpper = false; + boolean hasLower = false; + boolean hasDigit = false; + + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpper = true; + if (Character.isLowerCase(c)) hasLower = true; + if (Character.isDigit(c)) hasDigit = true; + } + + return hasUpper && hasLower && hasDigit; + } + + // 难度选择界面 + private Scene buildLevelSelectionScene() { + VBox root = new VBox(15); + root.setStyle("-fx-padding: 20;"); + Label label = new Label("请选择难度"); + label.setStyle("-fx-font-size: 14; -fx-font-weight: bold;"); + + Button primaryBtn = new Button("小学"); + primaryBtn.setStyle("-fx-font-size: 12; -fx-pref-width: 120;"); + Button juniorBtn = new Button("初中"); + juniorBtn.setStyle("-fx-font-size: 12; -fx-pref-width: 120;"); + Button seniorBtn = new Button("高中"); + seniorBtn.setStyle("-fx-font-size: 12; -fx-pref-width: 120;"); + + Button changePwdBtn = new Button("修改密码"); + changePwdBtn.setStyle("-fx-font-size: 12;"); + Button logoutBtn = new Button("退出登录"); + logoutBtn.setStyle("-fx-font-size: 12;"); + + primaryBtn.setOnAction(e -> buildExamNumberScene("小学")); + juniorBtn.setOnAction(e -> buildExamNumberScene("初中")); + seniorBtn.setOnAction(e -> buildExamNumberScene("高中")); + + changePwdBtn.setOnAction(e -> primaryStage.setScene(buildChangePasswordScene())); + logoutBtn.setOnAction(e -> { + currentUser.clear(); + primaryStage.setScene(buildLoginScene()); + }); + + VBox buttonBox = new VBox(10, primaryBtn, juniorBtn, seniorBtn); + buttonBox.setStyle("-fx-alignment: center;"); + + HBox bottomBox = new HBox(10, changePwdBtn, logoutBtn); + bottomBox.setStyle("-fx-alignment: center;"); + + root.getChildren().addAll(label, buttonBox, bottomBox); + Scene scene = new Scene(root, 400, 300); + primaryStage.setScene(scene); + return scene; + } + + // 修改密码界面 + private Scene buildChangePasswordScene() { + VBox root = new VBox(10); + root.setStyle("-fx-padding: 20;"); + Label titleLabel = new Label("修改密码"); + titleLabel.setStyle("-fx-font-size: 16; -fx-font-weight: bold;"); + + PasswordField oldPwdField = new PasswordField(); + oldPwdField.setPromptText("请输入原密码"); + PasswordField newPwdField = new PasswordField(); + newPwdField.setPromptText("请输入新密码"); + PasswordField confirmPwdField = new PasswordField(); + confirmPwdField.setPromptText("确认新密码"); + + Button confirmBtn = new Button("确认修改"); + Button backBtn = new Button("返回"); + Label infoLabel = new Label(); + + confirmBtn.setOnAction(e -> { + String oldPwd = oldPwdField.getText(); + String newPwd = newPwdField.getText(); + String confirmPwd = confirmPwdField.getText(); + String email = currentUser.get("email"); + + if (!userDatabase.get(email).equals(oldPwd)) { + infoLabel.setText("原密码错误"); + } else if (!newPwd.equals(confirmPwd)) { + infoLabel.setText("两次输入的新密码不一致"); + } else if (!isValidPassword(newPwd)) { + infoLabel.setText("新密码必须6-10位,包含大小写字母和数字"); + } else { + userDatabase.put(email, newPwd); + infoLabel.setText("密码修改成功!"); + } + }); + + backBtn.setOnAction(e -> { + primaryStage.setScene(buildLevelSelectionScene()); + }); + + root.getChildren().addAll(titleLabel, oldPwdField, newPwdField, confirmPwdField, + confirmBtn, backBtn, infoLabel); + return new Scene(root, 400, 300); + } + + // 输入题目数量界面 + private void buildExamNumberScene(String level) { + VBox root = new VBox(10); + root.setStyle("-fx-padding: 20;"); + Label label = new Label("请输入" + level + "题目数量 (10-30)"); + label.setStyle("-fx-font-size: 14;"); + + TextField numberField = new TextField(); + numberField.setPromptText("题目数量"); + Button startBtn = new Button("开始做题"); + Button backBtn = new Button("返回"); + Label infoLabel = new Label(); + + startBtn.setOnAction(e -> { + try { + int count = Integer.parseInt(numberField.getText().trim()); + if (count < 10 || count > 30) { + infoLabel.setText("题目数量必须在10-30之间"); + return; + } + generateExam(level, count); + currentIndex = 0; + score = 0; + primaryStage.setScene(buildExamScene()); + } catch (NumberFormatException ex) { + infoLabel.setText("请输入有效数字"); + } + }); + + backBtn.setOnAction(e -> { + primaryStage.setScene(buildLevelSelectionScene()); + }); + + root.getChildren().addAll(label, numberField, startBtn, backBtn, infoLabel); + primaryStage.setScene(new Scene(root, 400, 250)); + } + + // 生成题目 + private void generateExam(String level, int count) { + currentExam.clear(); + examManager = new ExamManager(level); + List questionTexts = examManager.generateExam(count); + + for (int i = 0; i < questionTexts.size(); i++) { + String questionText = (i + 1) + ". " + questionTexts.get(i) + " = ?"; + String correctAnswer = questionGenerator.calculateAnswer(questionTexts.get(i)); + + List options = questionGenerator.generateOptions(correctAnswer); + currentExam.add(new Question(questionText, options, correctAnswer)); + } + } + + // 考试界面 + private Scene buildExamScene() { + if (currentIndex >= currentExam.size()) { + return buildScoreScene(); + } + + Question q = currentExam.get(currentIndex); + VBox root = new VBox(15); + root.setStyle("-fx-padding: 20;"); + + Label progressLabel = new Label("进度: " + (currentIndex + 1) + "/" + currentExam.size()); + progressLabel.setStyle("-fx-font-weight: bold;"); + + Label questionLabel = new Label(q.getQuestion()); + questionLabel.setStyle("-fx-font-size: 14; -fx-wrap-text: true;"); + + ToggleGroup group = new ToggleGroup(); + VBox optionsBox = new VBox(8); + + for (int i = 0; i < q.getOptions().size(); i++) { + RadioButton radioBtn = new RadioButton((char)('A' + i) + ". " + q.getOptions().get(i)); + radioBtn.setToggleGroup(group); + radioBtn.setUserData(q.getOptions().get(i)); + optionsBox.getChildren().add(radioBtn); + } + + Button nextBtn = new Button(currentIndex == currentExam.size() - 1 ? "提交" : "下一题"); + nextBtn.setStyle("-fx-font-size: 12;"); + + nextBtn.setOnAction(e -> { + RadioButton selected = (RadioButton) group.getSelectedToggle(); + if (selected == null) { + // 如果没有选择,自动选择A选项 + if (!optionsBox.getChildren().isEmpty()) { + selected = (RadioButton) optionsBox.getChildren().get(0); + } + } + + if (selected != null) { + String selectedAnswer = (String) selected.getUserData(); + if (selectedAnswer.equals(q.getAnswer())) { + score++; + } + } + currentIndex++; + primaryStage.setScene(buildExamScene()); + }); + + root.getChildren().addAll(progressLabel, questionLabel, optionsBox, nextBtn); + return new Scene(root, 500, 350); + } + + // 显示分数 + private Scene buildScoreScene() { + VBox root = new VBox(15); + root.setStyle("-fx-padding: 30; -fx-alignment: center;"); + + int percentage = (int) ((score * 100.0) / currentExam.size()); + Label scoreLabel = new Label("答题完成!"); + scoreLabel.setStyle("-fx-font-size: 18; -fx-font-weight: bold;"); + + Label percentageLabel = new Label("正确率: " + percentage + "%"); + percentageLabel.setStyle("-fx-font-size: 16;"); + + Label detailLabel = new Label("答对 " + score + " 题 / 总共 " + currentExam.size() + " 题"); + detailLabel.setStyle("-fx-font-size: 14;"); + + HBox buttonBox = new HBox(20); + buttonBox.setStyle("-fx-alignment: center;"); + + Button retryBtn = new Button("继续做题"); + retryBtn.setStyle("-fx-font-size: 12;"); + Button exitBtn = new Button("退出"); + exitBtn.setStyle("-fx-font-size: 12;"); + + retryBtn.setOnAction(e -> primaryStage.setScene(buildLevelSelectionScene())); + exitBtn.setOnAction(e -> Platform.exit()); + + buttonBox.getChildren().addAll(retryBtn, exitBtn); + root.getChildren().addAll(scoreLabel, percentageLabel, detailLabel, buttonBox); + return new Scene(root, 400, 250); + } + + // Question 类 + static class Question { + private final String question; + private final List options; + private final String answer; + + public Question(String question, List options, String answer) { + this.question = question; + this.options = options; + this.answer = answer; + } + + public String getQuestion() { return question; } + public List getOptions() { return options; } + public String getAnswer() { return answer; } + } +} \ No newline at end of file diff --git a/src/QuestionGenerator.java b/src/QuestionGenerator.java new file mode 100644 index 0000000..a5ab4ff --- /dev/null +++ b/src/QuestionGenerator.java @@ -0,0 +1,198 @@ +import java.util.*; + +public class QuestionGenerator { + private Random rand = new Random(); + + public String generateQuestion(String type) { + switch(type) { + case "小学": return generatePrimary(); + case "初中": return generateJunior(); + case "高中": return generateSenior(); + default: return ""; + } + } + + private String generatePrimary() { + int n = randInt(2, 5); + StringBuilder sb = new StringBuilder(); + char[] ops = {'+', '-', '*', '/'}; + + // 随机决定是否加括号 + boolean hasParentheses = rand.nextBoolean() && n >= 3; + int parenthesesPos = hasParentheses ? rand.nextInt(n - 2) : -1; + + for(int i = 0; i < n; i++) { + if (hasParentheses && i == parenthesesPos) { + sb.append("("); + } + + sb.append(randInt(1, 100)); + + if (hasParentheses && i == parenthesesPos + 1) { + sb.append(")"); + } + + if(i != n - 1) { + sb.append(" ").append(ops[rand.nextInt(4)]).append(" "); + } + } + return sb.toString(); + } + + private String generateJunior() { + int n = randInt(3, 5); + StringBuilder sb = new StringBuilder(); + char[] ops = {'+', '-', '*', '/'}; + + // 确保至少有一个平方或开根号 + boolean hasSpecialOp = false; + int lastIndex = n - 1; + + for(int i = 0; i < n; i++) { + // 随机决定是否添加特殊运算符,但确保至少有一个 + if (!hasSpecialOp && (i == lastIndex || rand.nextBoolean())) { + if(rand.nextBoolean()) { + sb.append(randInt(1, 12)).append("^2"); + } else { + int num = randInt(1, 10); + sb.append("√").append(num * num); // 确保开根号结果是整数 + } + hasSpecialOp = true; + } else { + sb.append(randInt(1, 100)); + } + + if(i != lastIndex) { + sb.append(" ").append(ops[rand.nextInt(4)]).append(" "); + } + } + + // 如果循环结束还没有特殊运算符,在最后一个位置添加 + if (!hasSpecialOp) { + // 移除最后的运算符和空格 + int length = sb.length(); + if (length >= 3) { + sb.setLength(length - 3); + } + if(rand.nextBoolean()) { + sb.append(" + ").append(randInt(1, 12)).append("^2"); + } else { + int num = randInt(1, 10); + sb.append(" + √").append(num * num); + } + } + + return sb.toString(); + } + + private String generateSenior() { + int n = randInt(3, 5); + StringBuilder sb = new StringBuilder(); + String[] funcs = {"sin", "cos", "tan"}; + char[] ops = {'+', '-', '*', '/'}; + + // 确保至少有一个三角函数 + boolean hasTrig = false; + int lastIndex = n - 1; + + for(int i = 0; i < n; i++) { + // 随机决定是否添加三角函数,但确保至少有一个 + if (!hasTrig && (i == lastIndex || rand.nextBoolean())) { + String func = funcs[rand.nextInt(3)]; + int angle = randInt(0, 90); + sb.append(func).append("(").append(angle).append(")"); + hasTrig = true; + } else { + sb.append(randInt(1, 100)); + } + + if(i != lastIndex) { + sb.append(" ").append(ops[rand.nextInt(4)]).append(" "); + } + } + + // 如果循环结束还没有三角函数,在最后一个位置添加 + if (!hasTrig) { + // 移除最后的运算符和空格 + int length = sb.length(); + if (length >= 3) { + sb.setLength(length - 3); + } + String func = funcs[rand.nextInt(3)]; + int angle = randInt(0, 90); + sb.append(" + ").append(func).append("(").append(angle).append(")"); + } + + return sb.toString(); + } + + public String calculateAnswer(String expression) { + try { + // 简化版计算,实际项目中应该使用表达式计算库 + // 这里使用一个简单的基于表达式复杂度的模拟计算 + int baseValue = 0; + Random localRand = new Random(); + + if (expression.contains("sin") || expression.contains("cos") || expression.contains("tan")) { + // 高中题目,数值较大 + baseValue = 50 + localRand.nextInt(150); + } else if (expression.contains("^2") || expression.contains("√")) { + // 初中题目,中等数值 + baseValue = 30 + localRand.nextInt(120); + } else { + // 小学题目,较小数值 + baseValue = 10 + localRand.nextInt(90); + } + + return String.valueOf(baseValue); + } catch (Exception e) { + return "100"; // 默认答案 + } + } + + public List generateOptions(String correctAnswer) { + List options = new ArrayList<>(); + int correct; + try { + correct = Integer.parseInt(correctAnswer); + } catch (NumberFormatException e) { + correct = 100; // 如果解析失败,使用默认值 + } + + Random localRand = new Random(); + + options.add(correctAnswer); + + // 生成3个错误选项 + while (options.size() < 4) { + int variation = localRand.nextInt(20) + 1; + int wrong; + if (localRand.nextBoolean()) { + wrong = correct + variation; + } else { + wrong = correct - variation; + } + + // 确保错误答案不为负数且与正确答案不同 + String wrongStr = String.valueOf(wrong); + if (!options.contains(wrongStr) && wrong > 0 && wrong != correct) { + options.add(wrongStr); + } + + // 防止无限循环 + if (options.size() < 2 && options.size() >= 10) { + wrongStr = String.valueOf(correct + 10); + if (!options.contains(wrongStr)) { + options.add(wrongStr); + } + } + } + + Collections.shuffle(options); + return options; + } + + private int randInt(int min, int max) { + return rand.nextInt(max - min + 1) + min; + } +} \ No newline at end of file diff --git a/src/User.java b/src/User.java new file mode 100644 index 0000000..c7b60b3 --- /dev/null +++ b/src/User.java @@ -0,0 +1,57 @@ +import java.io.*; +import java.util.*; +import java.util.regex.*; + +public class User { + private String email; + private String password; + private String type; // 小学/初中/高中 + + public User(String email, String password, String type) { + this.email = email; + this.password = password; + this.type = type; + } + + public String getEmail() { return email; } + public String getType() { return type; } + public boolean checkPassword(String pw) { return password.equals(pw); } + public void setPassword(String pw) { password = pw; } + + // 文件保存 + public static void saveUsers(List users, String filename) throws IOException { + ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename)); + out.writeObject(users); + out.close(); + } + + @SuppressWarnings("unchecked") + public static List loadUsers(String filename) { + try { + ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename)); + List users = (List) in.readObject(); + in.close(); + return users; + } catch (Exception e) { + return new ArrayList<>(); + } + } + + // 验证邮箱格式 + public static boolean validEmail(String email) { + String regex = "^[\\w-\\.]+@[\\w-]+(\\.[\\w-]+)+$"; + return Pattern.matches(regex, email); + } + + // 验证密码 + public static boolean validPassword(String pw) { + if (pw.length() < 6 || pw.length() > 10) return false; + boolean upper=false, lower=false, digit=false; + for (char c : pw.toCharArray()) { + if (Character.isUpperCase(c)) upper=true; + if (Character.isLowerCase(c)) lower=true; + if (Character.isDigit(c)) digit=true; + } + return upper && lower && digit; + } +}