diff --git a/src/MathLearningApp.java b/src/MathLearningApp.java new file mode 100644 index 0000000..83d6d67 --- /dev/null +++ b/src/MathLearningApp.java @@ -0,0 +1,21 @@ +import controller.NavigationController; +import javax.swing.UIManager; +import javax.swing.SwingUtilities; + +public class MathLearningApp { + public static void main(String[] args) { + // Set system look and feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + + // Start the application + SwingUtilities.invokeLater(() -> { + // 获取 NavigationController 实例并显示登录界面 + NavigationController navigationController = NavigationController.getInstance(); + navigationController.showLoginView(); + }); + } +} \ No newline at end of file diff --git a/src/controller/BaseController.java b/src/controller/BaseController.java new file mode 100644 index 0000000..8f73b70 --- /dev/null +++ b/src/controller/BaseController.java @@ -0,0 +1,41 @@ +package controller; + +import javax.swing.JOptionPane; + +public abstract class BaseController { + + protected void validateNotNull(Object obj, String fieldName) { + if (obj == null) { + throw new IllegalArgumentException(fieldName + "不能为空"); + } + } + + protected void validateStringNotEmpty(String str, String fieldName) { + if (str == null || str.trim().isEmpty()) { + throw new IllegalArgumentException(fieldName + "不能为空"); + } + } + + protected void validateStringLength(String str, String fieldName, int min, int max) { + validateStringNotEmpty(str, fieldName); + if (str.length() < min || str.length() > max) { + throw new IllegalArgumentException(fieldName + "长度应在" + min + "-" + max + "之间"); + } + } + + protected void showError(String message) { + JOptionPane.showMessageDialog(null, message, "错误", JOptionPane.ERROR_MESSAGE); + } + + protected void showSuccess(String message) { + JOptionPane.showMessageDialog(null, message, "成功", JOptionPane.INFORMATION_MESSAGE); + } + + protected void logInfo(String message) { + //System.out.println("ℹ️ " + message); + } + + protected void logError(String message) { + System.err.println("❌ " + message); + } +} diff --git a/src/controller/NavigationController.java b/src/controller/NavigationController.java new file mode 100644 index 0000000..eb5c3fb --- /dev/null +++ b/src/controller/NavigationController.java @@ -0,0 +1,127 @@ +package controller; + +import model.User; +import view.LoginView; +import view.RegisterView; +import view.MainView; +import view.ExamView; +import view.ResultView; + +public class NavigationController extends BaseController implements NavigationService { + private static NavigationController instance; + private LoginView loginView; + private RegisterView registerView; + private MainView mainView; + private ExamView examView; + private ResultView resultView; + private User currentUser; + + // 单例模式 + public static NavigationController getInstance() { + if (instance == null) { + instance = new NavigationController(); + } + return instance; + } + + private NavigationController() { + // 私有构造函数 + } + + @Override + public void showLoginView() { + if (loginView == null) { + loginView = new LoginView(); + } + loginView.setVisible(true); + hideAllExcept(loginView); + logInfo("显示登录界面"); + } + + @Override + public void showRegisterView() { + if (registerView == null) { + registerView = new RegisterView(); + } + registerView.setVisible(true); + hideAllExcept(registerView); + logInfo("显示注册界面"); + } + + @Override + public void showMainView(User user) { + setCurrentUser(user); + if (mainView != null) { + mainView.dispose(); + } + mainView = new MainView(user); + mainView.setVisible(true); + hideAllExcept(mainView); + logInfo("显示主界面 - 用户: " + user.getUsername()); + } + + @Override + public void showMainView() { + if (currentUser != null) { + showMainView(currentUser); + } else { + showLoginView(); + } + } + + @Override + public void showExamView(String difficulty, int questionCount) { + try { + validateStringNotEmpty(difficulty, "难度"); + if (questionCount <= 0) { + throw new IllegalArgumentException("题目数量必须大于0"); + } + + if (examView == null) { + examView = new ExamView(); + } + examView.startNewExam(difficulty, questionCount); + examView.setVisible(true); + hideAllExcept(examView); + logInfo("显示考试界面 - " + difficulty + "难度, " + questionCount + "道题"); + } catch (Exception e) { + logError("显示考试界面异常: " + e.getMessage()); + showError("无法开始考试: " + e.getMessage()); + } + } + + @Override + public void showResultView(int score, int total, double percentage) { + try { + if (resultView == null) { + resultView = new ResultView(); + } + resultView.setResults(score, total, percentage); + resultView.setVisible(true); + hideAllExcept(resultView); + logInfo("显示成绩界面 - 得分: " + score + "/" + total); + } catch (Exception e) { + logError("显示成绩界面异常: " + e.getMessage()); + } + } + + @Override + public void setCurrentUser(User user) { + this.currentUser = user; + logInfo("设置当前用户: " + (user != null ? user.getUsername() : "null")); + } + + @Override + public User getCurrentUser() { + return currentUser; + } + + private void hideAllExcept(javax.swing.JFrame visibleFrame) { + javax.swing.JFrame[] frames = {loginView, registerView, mainView, examView, resultView}; + for (javax.swing.JFrame frame : frames) { + if (frame != null && frame != visibleFrame) { + frame.setVisible(false); + } + } + } +} \ No newline at end of file diff --git a/src/controller/NavigationService.java b/src/controller/NavigationService.java new file mode 100644 index 0000000..53adf3a --- /dev/null +++ b/src/controller/NavigationService.java @@ -0,0 +1,14 @@ +package controller; + +import model.User; + +public interface NavigationService { + void showLoginView(); + void showRegisterView(); + void showMainView(User user); + void showMainView(); + void showExamView(String difficulty, int questionCount); + void showResultView(int score, int total, double percentage); + void setCurrentUser(User user); + User getCurrentUser(); +} diff --git a/src/controller/QuestionController.java b/src/controller/QuestionController.java new file mode 100644 index 0000000..026081e --- /dev/null +++ b/src/controller/QuestionController.java @@ -0,0 +1,117 @@ +package controller; + +import model.Question; +import model.QuestionGenerator; +import utils.FileUtil; +import java.util.List; +import java.util.Arrays; + +public class QuestionController extends BaseController implements QuestionService { + private QuestionGenerator generator = new QuestionGenerator(); + private List currentQuestions; + private int currentQuestionIndex = 0; + private int score = 0; + private int[] userAnswers; + private String currentDifficulty; + private int currentQuestionCount; + + @Override + public void startNewExam(String difficulty, int questionCount) { + try { + validateStringNotEmpty(difficulty, "难度"); + if (questionCount <= 0 || questionCount > 100) { + throw new IllegalArgumentException("题目数量必须在1-100之间"); + } + + currentQuestions = generator.generateQuestions(questionCount, difficulty); + currentQuestionIndex = 0; + score = 0; + userAnswers = new int[questionCount]; + Arrays.fill(userAnswers, -1); + currentDifficulty = difficulty; + currentQuestionCount = questionCount; + + FileUtil.saveCurrentExam(currentQuestions, difficulty, questionCount); + logInfo("生成新试卷: " + difficulty + " 难度, " + questionCount + " 道题"); + } catch (Exception e) { + logError("开始新考试异常: " + e.getMessage()); + throw e; + } + } + + @Override + public String getCurrentExamInfo() { + if (currentDifficulty != null && currentQuestionCount > 0) { + return currentDifficulty + "难度 - " + currentQuestionCount + "道题"; + } + return "暂无试卷"; + } + + @Override + public Question getCurrentQuestion() { + if (currentQuestions == null || currentQuestionIndex >= currentQuestions.size()) { + return null; + } + return currentQuestions.get(currentQuestionIndex); + } + + @Override + public void submitAnswer(int answer) { + if (currentQuestionIndex < userAnswers.length) { + userAnswers[currentQuestionIndex] = answer; + logInfo("提交答案: 第" + getCurrentQuestionNumber() + "题, 答案: " + answer); + } + } + + @Override + public boolean nextQuestion() { + currentQuestionIndex++; + boolean hasNext = currentQuestionIndex < currentQuestions.size(); + logInfo("下一题: " + (hasNext ? "有" : "无")); + return hasNext; + } + + @Override + public boolean previousQuestion() { + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + logInfo("上一题: 第" + getCurrentQuestionNumber() + "题"); + return true; + } + return false; + } + + @Override + public int calculateScore() { + score = 0; + for (int i = 0; i < currentQuestions.size(); i++) { + if (userAnswers[i] != -1 && currentQuestions.get(i).isCorrect(userAnswers[i])) { + score++; + } + } + logInfo("计算得分: " + score + "/" + currentQuestions.size()); + return score; + } + + @Override + public double getPercentage() { + double percentage = (double) score / currentQuestions.size() * 100; + logInfo("正确率: " + String.format("%.1f%%", percentage)); + return percentage; + } + + @Override + public int getCurrentQuestionNumber() { + return currentQuestionIndex + 1; + } + + @Override + public int getTotalQuestions() { + return currentQuestions != null ? currentQuestions.size() : 0; + } + + @Override + public int getUserAnswerForCurrentQuestion() { + return userAnswers[currentQuestionIndex]; + } +} \ No newline at end of file diff --git a/src/controller/QuestionService.java b/src/controller/QuestionService.java new file mode 100644 index 0000000..a509ca9 --- /dev/null +++ b/src/controller/QuestionService.java @@ -0,0 +1,17 @@ +package controller; + +import model.Question; + +public interface QuestionService { + void startNewExam(String difficulty, int questionCount); + Question getCurrentQuestion(); + void submitAnswer(int answer); + boolean nextQuestion(); + boolean previousQuestion(); + int calculateScore(); + double getPercentage(); + int getCurrentQuestionNumber(); + int getTotalQuestions(); + int getUserAnswerForCurrentQuestion(); + String getCurrentExamInfo(); +} diff --git a/src/controller/UserController.java b/src/controller/UserController.java new file mode 100644 index 0000000..e1bc1e2 --- /dev/null +++ b/src/controller/UserController.java @@ -0,0 +1,156 @@ +package controller; + +import model.User; +import utils.FileUtil; +import utils.ValidationUtil; +import utils.EmailUtil; + +public class UserController extends BaseController implements UserService { + + @Override + public String registerUser(String username, String email) { + try { + validateStringLength(username, "用户名", 2, 20); + validateStringNotEmpty(email, "邮箱"); + + if (!ValidationUtil.isValidEmail(email)) { + return "邮箱格式不正确"; + } + + if (FileUtil.usernameExists(username)) { + return "用户名已被使用"; + } + + if (FileUtil.emailExists(email)) { + return "该邮箱已被注册"; + } + + String registrationCode = EmailUtil.generateRegistrationCode(); + User user = new User(username, email, registrationCode); + + boolean sendSuccess = EmailUtil.sendRegistrationCode(email, username, registrationCode); + + if (sendSuccess) { + FileUtil.saveUser(user); + logInfo("用户注册成功: " + username); + return "注册码已发送到您的邮箱,请查收!"; + } else { + return "邮件发送失败,请检查邮箱地址或稍后重试"; + } + } catch (IllegalArgumentException e) { + return e.getMessage(); + } catch (Exception e) { + logError("用户注册异常: " + e.getMessage()); + return "系统错误,请稍后重试"; + } + } + + @Override + public String completeRegistration(String username, String code, String password, String confirmPassword) { + try { + validateStringNotEmpty(username, "用户名"); + validateStringNotEmpty(code, "注册码"); + validateStringNotEmpty(password, "密码"); + validateStringNotEmpty(confirmPassword, "确认密码"); + + User user = FileUtil.loadUserByUsername(username); + if (user == null) { + return "用户不存在或注册码未发送"; + } + + if (!user.getRegistrationCode().equals(code)) { + return "注册码不正确"; + } + + if (!password.equals(confirmPassword)) { + return "两次输入的密码不一致"; + } + + if (!ValidationUtil.isValidPassword(password)) { + return "密码必须为6-10位,且包含大小写字母和数字"; + } + + user.setPassword(password); + user.setRegistered(true); + FileUtil.saveUser(user); + + logInfo("用户完成注册: " + username); + return "注册成功!"; + } catch (Exception e) { + logError("完成注册异常: " + e.getMessage()); + return "系统错误,请稍后重试"; + } + } + + @Override + public User login(String loginId, String password) { + try { + validateStringNotEmpty(loginId, "登录ID"); + validateStringNotEmpty(password, "密码"); + + User user; + if (loginId.contains("@")) { + user = FileUtil.loadUserByEmail(loginId); + } else { + user = FileUtil.loadUserByUsername(loginId); + } + + if (user == null) { + logInfo("登录失败: 用户不存在 - " + loginId); + return null; + } + + if (user.getPassword() == null || !user.getPassword().equals(password)) { + logInfo("登录失败: 密码错误 - " + loginId); + return null; + } + + logInfo("登录成功: " + user.getUsername()); + return user; + } catch (Exception e) { + logError("登录异常: " + e.getMessage()); + return null; + } + } + + @Override + public String changePassword(User user, String oldPassword, String newPassword, String confirmPassword) { + try { + validateNotNull(user, "用户"); + validateStringNotEmpty(oldPassword, "原密码"); + validateStringNotEmpty(newPassword, "新密码"); + validateStringNotEmpty(confirmPassword, "确认密码"); + + if (!user.getPassword().equals(oldPassword)) { + return "原密码不正确"; + } + + if (!newPassword.equals(confirmPassword)) { + return "两次输入的新密码不一致"; + } + + if (!ValidationUtil.isValidPassword(newPassword)) { + return "新密码必须为6-10位,且包含大小写字母和数字"; + } + + user.setPassword(newPassword); + FileUtil.saveUser(user); + + logInfo("密码修改成功: " + user.getUsername()); + return "密码修改成功"; + } catch (Exception e) { + logError("修改密码异常: " + e.getMessage()); + return "系统错误,请稍后重试"; + } + } + + @Override + public void cleanupUnregisteredUsers() { + try { + FileUtil.cleanupUnregisteredUsers(); + logInfo("执行未注册用户清理"); + } catch (Exception e) { + logError("清理未注册用户异常: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/controller/UserService.java b/src/controller/UserService.java new file mode 100644 index 0000000..75f7f01 --- /dev/null +++ b/src/controller/UserService.java @@ -0,0 +1,11 @@ +package controller; + +import model.User; + +public interface UserService { + String registerUser(String username, String email); + String completeRegistration(String username, String code, String password, String confirmPassword); + User login(String loginId, String password); + String changePassword(User user, String oldPassword, String newPassword, String confirmPassword); + void cleanupUnregisteredUsers(); +} \ No newline at end of file diff --git a/src/model/Question.java b/src/model/Question.java new file mode 100644 index 0000000..09e3faa --- /dev/null +++ b/src/model/Question.java @@ -0,0 +1,36 @@ +package model; + +import java.io.Serializable; + +public class Question implements Serializable { + private static final long serialVersionUID = 1L; + + private String question; + private String[] options; + private int correctAnswer; + private String difficulty; + + public Question(String question, String[] options, int correctAnswer, String difficulty) { + this.question = question; + this.options = options; + this.correctAnswer = correctAnswer; + this.difficulty = difficulty; + } + + // Getters + public String getQuestion() { return question; } + public String[] getOptions() { return options; } + + + public boolean isCorrect(int selectedAnswer) { + return selectedAnswer == correctAnswer; + } + + @Override + public String toString() { + return "Question{" + + "question='" + question + '\'' + + ", difficulty='" + difficulty + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/model/QuestionGenerator.java b/src/model/QuestionGenerator.java new file mode 100644 index 0000000..442751c --- /dev/null +++ b/src/model/QuestionGenerator.java @@ -0,0 +1,282 @@ +package model; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; +import java.util.Random; +import java.text.DecimalFormat; + +public class QuestionGenerator { + private Random random = new Random(); + private DecimalFormat df = new DecimalFormat("#.##"); // 保留两位小数的格式化器 + public List generateQuestions(int count, String difficulty) { + List questions = new ArrayList<>(); + Set existingQuestions = new HashSet<>(); + + while (questions.size() < count) { + Question question = generateQuestion(difficulty); + String questionKey = question.getQuestion() + Arrays.toString(question.getOptions()); + + if (!existingQuestions.contains(questionKey)) { + questions.add(question); + existingQuestions.add(questionKey); + } + } + + return questions; + } + + private Question generateQuestion(String difficulty) { + switch (difficulty) { + case "小学": + return generatePrimaryQuestion(); + case "初中": + return generateMiddleSchoolQuestion(); + case "高中": + return generateHighSchoolQuestion(); + default: + return generatePrimaryQuestion(); + } + } + + private Question generatePrimaryQuestion() { + int num1 = random.nextInt(50) + 1; + int num2 = random.nextInt(50) + 1; + int num3 = random.nextInt(50) + 1; + char[] operators = {'+', '-', '*', '/'}; + char operator1 = operators[random.nextInt(operators.length)]; + char operator2 = operators[random.nextInt(operators.length)]; + String question; + double correctAnswer; + // 随机决定是否使用括号 + if (random.nextBoolean()) { + // 有括号的情况 + switch (random.nextInt(3)) { + case 0: // (a op b) op c + correctAnswer = calculate(calculate(num1, num2, operator1), num3, operator2); + question = "(" + num1 + " " + getOperatorSymbol(operator1) + " " + num2 + ") " + + getOperatorSymbol(operator2) + " " + num3 + " = ?"; + break; + case 1: // a op (b op c) + correctAnswer = calculate(num1, calculate(num2, num3, operator2), operator1); + question = num1 + " " + getOperatorSymbol(operator1) + " (" + num2 + " " + + getOperatorSymbol(operator2) + " " + num3 + ") = ?"; + break; + default: // 无括号 + correctAnswer = calculate(calculate(num1, num2, operator1), num3, operator2); + question = num1 + " " + getOperatorSymbol(operator1) + " " + num2 + " " + + getOperatorSymbol(operator2) + " " + num3 + " = ?"; + break; + } + } else { + if (operator1 == '/') { + num2 = random.nextInt(10) + 1; // 避免除零 + } + correctAnswer = calculate(num1, num2, operator1); + question = num1 + " " + getOperatorSymbol(operator1) + " " + num2 + " = ?"; + } + correctAnswer = Double.parseDouble(df.format(correctAnswer)); + String[] options = generateOptions(correctAnswer); + return new Question(question, options, getCorrectOptionIndex(options, correctAnswer), "小学"); + } + + private Question generateMiddleSchoolQuestion() { + String question; + double correctAnswer; + + if (random.nextBoolean()) { + // 平方或平方根题目 + int num = random.nextInt(20) + 1; + if (random.nextBoolean()) { + correctAnswer = Math.pow(num, 2); + question = num + "² = ?"; + } else { + int squared = num * num; + correctAnswer = num; + question = "√" + squared + " = ?"; + } + } else { + // 带加减乘除的题目 + int num1 = random.nextInt(50) + 1; + int num2 = random.nextInt(50) + 1; + int num3 = random.nextInt(20) + 1; + char[] operators = {'+', '-', '*', '/'}; + char operator1 = operators[random.nextInt(operators.length)]; + char operator2 = operators[random.nextInt(operators.length)]; + + // 随机组合平方、平方根和基本运算 + if (random.nextBoolean()) { + // 平方与基本运算组合 + correctAnswer = calculate(Math.pow(num1, 2), num2, operator1); + question = num1 + "² " + getOperatorSymbol(operator1) + " " + num2 + " = ?"; + } else { + // 平方根与基本运算组合 + int squared = num3 * num3; + correctAnswer = calculate(num3, num2, operator1); + question = "√" + squared + " " + getOperatorSymbol(operator1) + " " + num2 + " = ?"; + } + } + correctAnswer = Double.parseDouble(df.format(correctAnswer)); + String[] options = generateOptions(correctAnswer); + return new Question(question, options, getCorrectOptionIndex(options, correctAnswer), "初中"); + } + + private Question generateHighSchoolQuestion() { + + if (random.nextBoolean()) { + return HighSchoolQuestionOne(); + } else { + // 三角函数与基本运算组合 + return HighSchoolQuestionTwo(); + } + + } + + private Question HighSchoolQuestionOne() { + String question; + double correctAnswer; + double angle = random.nextInt(360); + String[] trigFunctions = {"sin", "cos", "tan"}; + String trigFunction = trigFunctions[random.nextInt(trigFunctions.length)]; + + switch (trigFunction) { + case "sin": + correctAnswer = Math.sin(Math.toRadians(angle)); + question = "sin(" + angle + "°) = ?"; + break; + case "cos": + correctAnswer = Math.cos(Math.toRadians(angle)); + question = "cos(" + angle + "°) = ?"; + break; + case "tan": + angle = getSafeTanAngle(); + correctAnswer = Math.tan(Math.toRadians(angle)); + question = "tan(" + angle + "°) = ?"; + break; + default: + correctAnswer = 0; + question = ""; + } + correctAnswer = Double.parseDouble(df.format(correctAnswer)); + String[] options = generateOptions(correctAnswer); + return new Question(question, options, getCorrectOptionIndex(options, correctAnswer), "高中"); + } + private Question HighSchoolQuestionTwo() { + String question; + double correctAnswer; + double angle = random.nextInt(360); + int num = random.nextInt(20) + 1; + char[] operators = {'+', '-', '*', '/'}; + char operator = operators[random.nextInt(operators.length)]; + String[] trigFunctions = {"sin", "cos", "tan"}; + String trigFunction = trigFunctions[random.nextInt(trigFunctions.length)]; + + double trigValue; + switch (trigFunction) { + case "sin": + trigValue = Math.sin(Math.toRadians(angle)); + question = "sin(" + angle + "°) " + getOperatorSymbol(operator) + " " + num + " = ?"; + break; + case "cos": + trigValue = Math.cos(Math.toRadians(angle)); + question = "cos(" + angle + "°) " + getOperatorSymbol(operator) + " " + num + " = ?"; + break; + case "tan": + angle = getSafeTanAngle(); + trigValue = Math.tan(Math.toRadians(angle)); + question = "tan(" + angle + "°) " + getOperatorSymbol(operator) + " " + num + " = ?"; + break; + default: + trigValue = 0; + question = ""; + } + trigValue = Double.parseDouble(df.format(trigValue)); + correctAnswer = calculate(trigValue, num, operator); + String[] options = generateOptions(correctAnswer); + return new Question(question, options, getCorrectOptionIndex(options, correctAnswer), "高中"); + } + // 获取安全的tan角度(更严格的范围) + private double getSafeTanAngle() { + // 为tan函数提供更安全的角度范围,避免无限大的值 + // 排除:85°-95°, 265°-275° 等接近90°和270°的角度 + List safeTanAngles = new ArrayList<>(); + for (int i = 0; i < 360; i++) { + if (isSafeForTan(i)) { + safeTanAngles.add((double) i); + } + } + return safeTanAngles.get(random.nextInt(safeTanAngles.size())); + } + // 检查角度是否对tan函数安全(更严格的条件) + private boolean isSafeForTan(double angle) { + // 排除接近90°、270°的角度,以及排除tan值过大的角度 + return !((angle >= 80 && angle <= 100) || + (angle >= 260 && angle <= 280) || + (angle >= 170 && angle <= 190) || + (angle >= 350 || angle <= 10)); + } + + // 计算方法 + private double calculate(double a, double b, char operator) { + switch (operator) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + return b != 0 ? a / b : a; // 避免除零 + default: + return 0; + } + } + + // 获取运算符符号 + private String getOperatorSymbol(char operator) { + switch (operator) { + case '+': + return "+"; + case '-': + return "-"; + case '*': + return "×"; + case '/': + return "÷"; + default: + return ""; + } + } + + private String[] generateOptions(double correctAnswer) { + String[] options = new String[4]; + int correctIndex = random.nextInt(4); + + for (int i = 0; i < 4; i++) { + if (i == correctIndex) { + options[i] = df.format(correctAnswer); + } else { + double wrongAnswer; + do { + double variation = (random.nextDouble() - 0.5) * 10; + wrongAnswer = correctAnswer + variation; + wrongAnswer = Double.parseDouble(df.format(wrongAnswer)); + } while (Math.abs(wrongAnswer - correctAnswer) < 0.1); + options[i] = String.valueOf(wrongAnswer); + } + } + + return options; + } + + private int getCorrectOptionIndex(String[] options, double correctAnswer) { + for (int i = 0; i < options.length; i++) { + if (Double.parseDouble(options[i]) == correctAnswer) { + return i; + } + } + return 0; + } +} \ No newline at end of file diff --git a/src/model/User.java b/src/model/User.java new file mode 100644 index 0000000..ade2948 --- /dev/null +++ b/src/model/User.java @@ -0,0 +1,30 @@ +package model; + +public class User { + private String username; // 新增:用户名 + private String email; + private String password; + private String registrationCode; + private boolean isRegistered; + + public User(String username, String email, String registrationCode) { + this.username = username; + this.email = email; + this.registrationCode = registrationCode; + this.isRegistered = false; + } + + // Getters and Setters + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getEmail() { return email; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + public String getRegistrationCode() { return registrationCode; } + + public boolean isRegistered() { return isRegistered; } + public void setRegistered(boolean registered) { isRegistered = registered; } +} \ No newline at end of file diff --git a/src/utils/EmailUtil.java b/src/utils/EmailUtil.java new file mode 100644 index 0000000..0b2d557 --- /dev/null +++ b/src/utils/EmailUtil.java @@ -0,0 +1,111 @@ +package utils; + +import javax.mail.Authenticator; +import javax.mail.Message; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.util.Properties; +import java.util.Random; + +public class EmailUtil { + private static final Random random = new Random(); + + // SMTP服务器配置 - 请修改为您的邮箱配置 + private static final String SMTP_HOST = "smtp.qq.com"; // QQ邮箱SMTP + private static final String SMTP_PORT = "587"; // 端口 + private static final String FROM_EMAIL = "2536082954@qq.com"; // 发件邮箱 + private static final String EMAIL_PASSWORD = "uihuasdsosbvdiia"; // 授权码 + private static final String FROM_NAME = "数学学习软件"; + + public static String generateRegistrationCode() { + return String.format("%06d", random.nextInt(1000000)); + } + + /** + * 发送注册码到用户输入的邮箱 + */ + public static boolean sendRegistrationCode(String userEmail, String username, String code) { + // 配置邮件服务器 + Properties props = new Properties(); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.host", SMTP_HOST); + props.put("mail.smtp.port", SMTP_PORT); + + // 超时设置 + props.put("mail.smtp.timeout", "10000"); + props.put("mail.smtp.connectiontimeout", "10000"); + + // 创建认证器 + Authenticator authenticator = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(FROM_EMAIL, EMAIL_PASSWORD); + } + }; + + Session session = Session.getInstance(props, authenticator); + + try { + // 创建邮件 + MimeMessage message = new MimeMessage(session); + message.setFrom(new InternetAddress(FROM_EMAIL, FROM_NAME, "UTF-8")); + message.setRecipient(Message.RecipientType.TO, new InternetAddress(userEmail)); + message.setSubject("数学学习软件 - 注册验证码", "UTF-8"); + + // 邮件内容(HTML格式) + String htmlContent = buildEmailContent(username, code); + message.setContent(htmlContent, "text/html;charset=UTF-8"); + + // 发送邮件 + Transport.send(message); + System.out.println("✅ 注册码已成功发送到: " + userEmail); + return true; + + } catch (Exception e) { + System.err.println("❌ 邮件发送失败: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + /** + * 构建邮件内容 + */ + private static String buildEmailContent(String username, String code) { + return "" + + "" + + "" + + " " + + " " + + "" + + "" + + "
" + + "
" + + "

数学学习软件 - 注册验证码

" + + "
" + + "

亲爱的 " + username + ",您好!

" + + "

您正在注册数学学习软件,请使用以下验证码完成注册:

" + + "
" + + "
" + code + "
" + + "
" + + "

如果这不是您的操作,请忽略此邮件。

" + + "
" + + "

此为系统邮件,请勿回复

" + + "

数学学习软件团队

" + + "
" + + "
" + + "" + + ""; + } +} \ No newline at end of file diff --git a/src/utils/FileUtil.java b/src/utils/FileUtil.java new file mode 100644 index 0000000..b56ee79 --- /dev/null +++ b/src/utils/FileUtil.java @@ -0,0 +1,200 @@ +package utils; + +import model.Question; +import model.User; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class FileUtil { + private static final String USERS_FILE = "data/users.json"; + private static final String CURRENT_EXAM_FILE = "data/current_exam.json"; + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + static { + new File("data").mkdirs(); + } + + /** + * 清理未注册成功的用户(未设置密码的用户) + */ + public static void cleanupUnregisteredUsers() { + try { + Map users = loadAllUsers(); + boolean hasChanges = false; + long currentTime = System.currentTimeMillis(); + + // 使用迭代器安全删除 + Iterator> iterator = users.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + User user = entry.getValue(); + + // 删除条件:未设置密码 或 未完成注册 且 创建时间超过24小时 + if ((user.getPassword() == null || user.getPassword().isEmpty() || !user.isRegistered())) { + iterator.remove(); + hasChanges = true; + System.out.println("清理未注册用户: " + user.getUsername() + " (" + user.getEmail() + ")"); + } + } + + if (hasChanges) { + saveAllUsers(users); + + } else { + System.out.println("没有需要清理的未注册用户"); + } + + } catch (Exception e) { + System.err.println("清理未注册用户时出错: " + e.getMessage()); + e.printStackTrace(); + } + } + + public static void saveAllUsers(Map users) { + try (FileWriter writer = new FileWriter(USERS_FILE)) { + gson.toJson(users, writer); + } catch (IOException e) { + System.err.println("保存用户数据失败: " + e.getMessage()); + } + } + + // 用户相关方法保持不变... + public static void saveUser(User user) { + try { + Map users = loadAllUsers(); + users.put(user.getUsername(), user); + saveAllUsers(users); + } catch (Exception e) { + System.err.println("保存用户数据失败: " + e.getMessage()); + } + } + + public static Map loadAllUsers() { + try { + File file = new File(USERS_FILE); + if (!file.exists()) { + return new HashMap<>(); + } + + try (FileReader reader = new FileReader(file)) { + Type type = new TypeToken>(){}.getType(); + Map users = gson.fromJson(reader, type); + + // 修复现有数据中的问题 + if (users != null) { + for (Map.Entry entry : users.entrySet()) { + User user = entry.getValue(); + // 确保用户名不为空 + if (user.getUsername() == null) { + user.setUsername(entry.getKey()); + } + // 确保密码不为null + if (user.getPassword() == null) { + user.setPassword(""); + } + } + } + + return users != null ? users : new HashMap<>(); + } + } catch (IOException e) { + System.err.println("加载用户数据失败: " + e.getMessage()); + return new HashMap<>(); + } + } + + public static User loadUserByUsername(String username) { + Map users = loadAllUsers(); + return users.get(username); + } + + public static User loadUserByEmail(String email) { + Map users = loadAllUsers(); + for (User user : users.values()) { + if (user.getEmail().equals(email)) { + return user; + } + } + return null; + } + + public static boolean usernameExists(String username) { + return loadAllUsers().containsKey(username); + } + + public static boolean emailExists(String email) { + return loadUserByEmail(email) != null; + } + + // ========== 试卷保存相关方法 ========== + + /** + * 保存当前试卷到本地文件(覆盖之前的试卷) + */ + public static void saveCurrentExam(List questions, String difficulty, int questionCount) { + try { + ExamData examData = new ExamData(questions, difficulty, questionCount, new Date()); + + try (FileWriter writer = new FileWriter(CURRENT_EXAM_FILE)) { + gson.toJson(examData, writer); + } + System.out.println("✅ 试卷已保存到本地: " + CURRENT_EXAM_FILE); + + } catch (IOException e) { + System.err.println("保存试卷失败: " + e.getMessage()); + } + } + + /** + * 从本地文件加载当前试卷 + */ + public static ExamData loadCurrentExam() { + try { + File file = new File(CURRENT_EXAM_FILE); + if (!file.exists()) { + return null; + } + + try (FileReader reader = new FileReader(file)) { + return gson.fromJson(reader, ExamData.class); + } + } catch (IOException e) { + System.err.println("加载试卷失败: " + e.getMessage()); + return null; + } + } + + /** + * 试卷数据包装类 + */ + public static class ExamData { + private List questions; + private String difficulty; + private int questionCount; + private Date generateTime; + + public ExamData(List questions, String difficulty, int questionCount, Date generateTime) { + this.questions = questions; + this.difficulty = difficulty; + this.questionCount = questionCount; + this.generateTime = generateTime; + } + + // Getters + public List getQuestions() { return questions; } + public String getDifficulty() { return difficulty; } + public int getQuestionCount() { return questionCount; } + public Date getGenerateTime() { return generateTime; } + } +} \ No newline at end of file diff --git a/src/utils/ValidationUtil.java b/src/utils/ValidationUtil.java new file mode 100644 index 0000000..54e454e --- /dev/null +++ b/src/utils/ValidationUtil.java @@ -0,0 +1,39 @@ +package utils; + +import java.util.regex.Pattern; + +public class ValidationUtil { + + public static boolean isValidEmail(String email) { + String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$"; + return Pattern.compile(emailRegex).matcher(email).matches(); + } + + public static boolean isValidPassword(String password) { + // 6-10位,必须包含大小写字母和数字 + if (password.length() < 6 || password.length() > 10) { + return false; + } + + boolean hasUpperCase = false; + boolean hasLowerCase = false; + boolean hasDigit = false; + + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpperCase = true; + if (Character.isLowerCase(c)) hasLowerCase = true; + if (Character.isDigit(c)) hasDigit = true; + } + + return hasUpperCase && hasLowerCase && hasDigit; + } + + public static boolean isNumeric(String str) { + try { + Double.parseDouble(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/src/view/ChangePasswordDialog.java b/src/view/ChangePasswordDialog.java new file mode 100644 index 0000000..494b81e --- /dev/null +++ b/src/view/ChangePasswordDialog.java @@ -0,0 +1,131 @@ +package view; + +import controller.UserController; +import model.User; +import utils.ValidationUtil; + +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JPasswordField; +import javax.swing.JButton; +import javax.swing.BorderFactory; +import javax.swing.JOptionPane; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.Font; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class ChangePasswordDialog extends JDialog { + private User currentUser; + private UserController userController; + + private JPasswordField oldPasswordField; + private JPasswordField newPasswordField; + private JPasswordField confirmPasswordField; + + public ChangePasswordDialog(JFrame parent, User user) { + super(parent, "修改密码", true); + this.currentUser = user; + this.userController = new UserController(); + initializeUI(); + } + + private void initializeUI() { + setSize(400, 300); + setLocationRelativeTo(getParent()); + setResizable(false); + + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Title + JLabel titleLabel = new JLabel("修改密码", JLabel.CENTER); + titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 18)); + mainPanel.add(titleLabel, BorderLayout.NORTH); + + // Form panel + JPanel formPanel = new JPanel(new GridLayout(4, 2, 10, 10)); + + JLabel oldPasswordLabel = new JLabel("原密码:"); + oldPasswordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + oldPasswordField = new JPasswordField(); + + JLabel newPasswordLabel = new JLabel("新密码:"); + newPasswordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + newPasswordField = new JPasswordField(); + + JLabel confirmPasswordLabel = new JLabel("确认新密码:"); + confirmPasswordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + confirmPasswordField = new JPasswordField(); + + JButton confirmButton = new JButton("确认修改"); + confirmButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + JButton cancelButton = new JButton("取消"); + cancelButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + formPanel.add(oldPasswordLabel); + formPanel.add(oldPasswordField); + formPanel.add(newPasswordLabel); + formPanel.add(newPasswordField); + formPanel.add(confirmPasswordLabel); + formPanel.add(confirmPasswordField); + formPanel.add(confirmButton); + formPanel.add(cancelButton); + + mainPanel.add(formPanel, BorderLayout.CENTER); + + // 密码要求提示 + JLabel hintLabel = new JLabel( + "" + + "密码要求:6-10位,必须包含大小写字母和数字
" + + "例如:Abc123、Test456" + + "", JLabel.CENTER); + hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12)); + hintLabel.setForeground(Color.GRAY); + mainPanel.add(hintLabel, BorderLayout.SOUTH); + + // Add action listeners + confirmButton.addActionListener(new ConfirmAction()); + cancelButton.addActionListener(e -> dispose()); + + // Enter key support + oldPasswordField.addActionListener(new ConfirmAction()); + newPasswordField.addActionListener(new ConfirmAction()); + confirmPasswordField.addActionListener(new ConfirmAction()); + + add(mainPanel); + } + + private class ConfirmAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String oldPassword = new String(oldPasswordField.getPassword()); + String newPassword = new String(newPasswordField.getPassword()); + String confirmPassword = new String(confirmPasswordField.getPassword()); + + // 验证输入 + if (oldPassword.isEmpty() || newPassword.isEmpty() || confirmPassword.isEmpty()) { + JOptionPane.showMessageDialog(ChangePasswordDialog.this, + "请填写所有密码字段", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + // 调用控制器修改密码 + String result = userController.changePassword(currentUser, oldPassword, newPassword, confirmPassword); + + if (result.equals("密码修改成功")) { + JOptionPane.showMessageDialog(ChangePasswordDialog.this, + "密码修改成功!", "成功", JOptionPane.INFORMATION_MESSAGE); + dispose(); // 关闭对话框 + } else { + JOptionPane.showMessageDialog(ChangePasswordDialog.this, + result, "错误", JOptionPane.ERROR_MESSAGE); + } + } + } +} \ No newline at end of file diff --git a/src/view/ExamView.java b/src/view/ExamView.java new file mode 100644 index 0000000..2962c13 --- /dev/null +++ b/src/view/ExamView.java @@ -0,0 +1,281 @@ +package view; + +import controller.NavigationController; +import controller.NavigationService; +import controller.QuestionController; +import model.Question; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JRadioButton; +import javax.swing.JButton; +import javax.swing.ButtonGroup; +import javax.swing.BorderFactory; +import javax.swing.JOptionPane; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +public class ExamView extends JFrame { + private QuestionController questionController; + private NavigationService navigationService; + private JLabel questionLabel; + private JLabel progressLabel; + private ButtonGroup optionGroup; + private JRadioButton[] optionButtons; + private JButton prevButton; + private JButton nextButton; + private JButton submitButton; + private JLabel hintLabel; + + public ExamView() { + questionController = new QuestionController(); + navigationService = NavigationController.getInstance(); + initializeUI(); + } + + private void initializeUI() { + setTitle("数学学习软件 - 答题界面"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(600, 450); + setLocationRelativeTo(null); + setResizable(false); + + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Progress label + progressLabel = new JLabel("", JLabel.CENTER); + progressLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); + mainPanel.add(progressLabel, BorderLayout.NORTH); + + // Question panel + JPanel questionPanel = new JPanel(new BorderLayout(10, 10)); + questionLabel = new JLabel("", JLabel.CENTER); + questionLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18)); + questionPanel.add(questionLabel, BorderLayout.NORTH); + + // Options panel + JPanel optionsPanel = new JPanel(new GridLayout(4, 1, 10, 10)); + optionGroup = new ButtonGroup(); + optionButtons = new JRadioButton[4]; + + for (int i = 0; i < 4; i++) { + optionButtons[i] = new JRadioButton(); + optionButtons[i].setFont(new Font("微软雅黑", Font.PLAIN, 14)); + optionGroup.add(optionButtons[i]); + optionsPanel.add(optionButtons[i]); + + optionButtons[i].addItemListener(new OptionSelectionListener()); + } + + questionPanel.add(optionsPanel, BorderLayout.CENTER); + mainPanel.add(questionPanel, BorderLayout.CENTER); + + // Hint label + hintLabel = new JLabel("请选择答案后继续", JLabel.CENTER); + hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + hintLabel.setForeground(Color.RED); + hintLabel.setVisible(false); + mainPanel.add(hintLabel, BorderLayout.SOUTH); + + // Button panel + JPanel buttonPanel = new JPanel(new FlowLayout()); + prevButton = new JButton("上一题"); + nextButton = new JButton("下一题"); + submitButton = new JButton("提交试卷"); + + prevButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + nextButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + submitButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + nextButton.setEnabled(false); + + buttonPanel.add(prevButton); + buttonPanel.add(nextButton); + buttonPanel.add(submitButton); + + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + // Add action listeners + prevButton.addActionListener(new PrevButtonAction()); + nextButton.addActionListener(new NextButtonAction()); + submitButton.addActionListener(new SubmitButtonAction()); + + add(mainPanel); + } + + public void startNewExam(String difficulty, int questionCount) { + questionController.startNewExam(difficulty, questionCount); + displayCurrentQuestion(); + updateNavigationButtons(); + updateNextButtonState(); + } + + private void displayCurrentQuestion() { + Question question = questionController.getCurrentQuestion(); + if (question == null) return; + + progressLabel.setText(String.format("第 %d 题 / 共 %d 题", + questionController.getCurrentQuestionNumber(), + questionController.getTotalQuestions())); + + questionLabel.setText(question.getQuestion()); + + String[] options = question.getOptions(); + for (int i = 0; i < 4; i++) { + optionButtons[i].setText((char)('A' + i) + ". " + options[i]); + } + + // Restore user's previous selection + int userAnswer = questionController.getUserAnswerForCurrentQuestion(); + optionGroup.clearSelection(); + if (userAnswer != -1) { + optionButtons[userAnswer].setSelected(true); + nextButton.setEnabled(canGoNext()); + hintLabel.setVisible(false); + } else { + nextButton.setEnabled(false); + } + } + + private void updateNavigationButtons() { + int current = questionController.getCurrentQuestionNumber(); + int total = questionController.getTotalQuestions(); + + prevButton.setEnabled(current > 1); + + // 修改这里:最后一题时禁用下一题按钮 + boolean isLastQuestion = current == total; + nextButton.setEnabled(!isLastQuestion && isCurrentQuestionAnswered()); + submitButton.setEnabled(isLastQuestion && isCurrentQuestionAnswered()); + } + + private void updateNextButtonState() { + boolean isLastQuestion = questionController.getCurrentQuestionNumber() == questionController.getTotalQuestions(); + + if (isLastQuestion) { + // 最后一题:禁用下一题,启用提交按钮(如果已回答) + nextButton.setEnabled(false); + submitButton.setEnabled(isCurrentQuestionAnswered()); + } else { + // 不是最后一题:根据是否回答控制下一题按钮 + nextButton.setEnabled(isCurrentQuestionAnswered()); + submitButton.setEnabled(false); + } + + hintLabel.setVisible(!isCurrentQuestionAnswered()); + } + + private boolean canGoNext() { + int current = questionController.getCurrentQuestionNumber(); + int total = questionController.getTotalQuestions(); + return current < total && isCurrentQuestionAnswered(); + } + + private boolean isCurrentQuestionAnswered() { + for (int i = 0; i < 4; i++) { + if (optionButtons[i].isSelected()) { + return true; + } + } + return false; + } + + private void saveCurrentAnswer() { + for (int i = 0; i < 4; i++) { + if (optionButtons[i].isSelected()) { + questionController.submitAnswer(i); + break; + } + } + } + + // 选项选择监听器 + private class OptionSelectionListener implements ItemListener { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + saveCurrentAnswer(); // 立即保存答案 + updateNextButtonState(); + updateNavigationButtons(); + } + } + } + + private class PrevButtonAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + saveCurrentAnswer(); + if (questionController.previousQuestion()) { + displayCurrentQuestion(); + updateNavigationButtons(); + updateNextButtonState(); + } + } + } + + private class NextButtonAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + int current = questionController.getCurrentQuestionNumber(); + int total = questionController.getTotalQuestions(); + + // 检查是否是最后一题 + if (current >= total) { + JOptionPane.showMessageDialog(ExamView.this, + "已经是最后一题了,请点击提交试卷!", "提示", + JOptionPane.INFORMATION_MESSAGE); + return; + } + + if (!isCurrentQuestionAnswered()) { + JOptionPane.showMessageDialog(ExamView.this, + "请先选择答案!", "提示", JOptionPane.WARNING_MESSAGE); + return; + } + + saveCurrentAnswer(); + if (questionController.nextQuestion()) { + displayCurrentQuestion(); + updateNavigationButtons(); + updateNextButtonState(); + } else { + // 如果无法切换到下一题,可能是最后一题 + JOptionPane.showMessageDialog(ExamView.this, + "无法切换到下一题,请点击提交试卷!", "提示", + JOptionPane.INFORMATION_MESSAGE); + } + } + } + + private class SubmitButtonAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + if (!isCurrentQuestionAnswered()) { + JOptionPane.showMessageDialog(ExamView.this, + "请先完成本题!", "提示", JOptionPane.WARNING_MESSAGE); + return; + } + + saveCurrentAnswer(); + + int score = questionController.calculateScore(); + int total = questionController.getTotalQuestions(); + double percentage = questionController.getPercentage(); + + // 关闭当前窗口 + dispose(); + + // 显示结果页面 + navigationService.showResultView(score, total, percentage); // 修改 + } + } +} \ No newline at end of file diff --git a/src/view/LoginView.java b/src/view/LoginView.java new file mode 100644 index 0000000..ae12ac3 --- /dev/null +++ b/src/view/LoginView.java @@ -0,0 +1,130 @@ +package view; + +import controller.NavigationController; +import controller.NavigationService; +import controller.UserController; +import controller.UserService; +import model.User; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.JPasswordField; +import javax.swing.JButton; +import javax.swing.BorderFactory; +import javax.swing.JOptionPane; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class LoginView extends JFrame { + private JTextField usernameField; + private JPasswordField passwordField; + private UserService userController; + private NavigationService navigationService; + + public LoginView() { + userController = new UserController(); + navigationService = NavigationController.getInstance(); + initializeUI(); + cleanupUnregisteredUsers(); + } + + private void cleanupUnregisteredUsers() { + try { + userController.cleanupUnregisteredUsers(); + } catch (Exception e) { + System.err.println("清理未注册用户时出错: " + e.getMessage()); + } + } + + private void initializeUI() { + setTitle("数学学习软件 - 登录"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(400, 300); + setLocationRelativeTo(null); + setResizable(false); + + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Title + JLabel titleLabel = new JLabel("用户登录", JLabel.CENTER); + titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24)); + mainPanel.add(titleLabel, BorderLayout.NORTH); + + // Form panel + JPanel formPanel = new JPanel(new GridLayout(3, 2, 10, 10)); + + JLabel usernameLabel = new JLabel("用户名:"); + usernameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + usernameField = new JTextField(); + + JLabel passwordLabel = new JLabel("密码:"); + passwordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + passwordField = new JPasswordField(); + + formPanel.add(usernameLabel); + formPanel.add(usernameField); + formPanel.add(passwordLabel); + formPanel.add(passwordField); + + // Buttons + JPanel buttonPanel = new JPanel(new FlowLayout()); + JButton loginButton = new JButton("登录"); + JButton registerButton = new JButton("注册"); + + loginButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + registerButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + buttonPanel.add(loginButton); + buttonPanel.add(registerButton); + + formPanel.add(new JLabel()); // Empty cell + formPanel.add(buttonPanel); + + mainPanel.add(formPanel, BorderLayout.CENTER); + + // Add action listeners + loginButton.addActionListener(new LoginAction()); + registerButton.addActionListener(e -> { + + navigationService.showRegisterView(); + }); + + // Enter key support + usernameField.addActionListener(new LoginAction()); + passwordField.addActionListener(new LoginAction()); + + add(mainPanel); + } + + private class LoginAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String username = usernameField.getText().trim(); + String password = new String(passwordField.getPassword()); + + if (username.isEmpty() || password.isEmpty()) { + JOptionPane.showMessageDialog(LoginView.this, + "请输入用户名和密码", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + User user = userController.login(username, password); + if (user != null) { + JOptionPane.showMessageDialog(LoginView.this, + "登录成功!", "成功", JOptionPane.INFORMATION_MESSAGE); + + navigationService.showMainView(user); + } else { + JOptionPane.showMessageDialog(LoginView.this, + "用户名或密码错误", "错误", JOptionPane.ERROR_MESSAGE); + } + } + } +} \ No newline at end of file diff --git a/src/view/MainView.java b/src/view/MainView.java new file mode 100644 index 0000000..77fc4ba --- /dev/null +++ b/src/view/MainView.java @@ -0,0 +1,126 @@ +package view; + +import controller.NavigationController; +import controller.NavigationService; +import controller.UserController; +import model.User; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JComboBox; +import javax.swing.JTextField; +import javax.swing.JButton; +import javax.swing.BorderFactory; +import javax.swing.JOptionPane; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class MainView extends JFrame { + private JComboBox difficultyComboBox; + private JTextField countField; + private User currentUser; // 保存当前登录用户 + private NavigationService navigationService; + + public MainView(User user) { + this.currentUser = user; + this.navigationService = NavigationController.getInstance(); + initializeUI(); + } + + private void initializeUI() { + setTitle("数学学习软件 - 主界面"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(450, 350); + setLocationRelativeTo(null); + setResizable(false); + + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Title - 显示欢迎信息 + JLabel titleLabel = new JLabel("欢迎 " + currentUser.getUsername() + "!", JLabel.CENTER); + titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24)); + mainPanel.add(titleLabel, BorderLayout.NORTH); + + // Form panel + JPanel formPanel = new JPanel(new GridLayout(3, 2, 10, 10)); + + JLabel difficultyLabel = new JLabel("难度级别:"); + difficultyLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + String[] difficulties = {"小学", "初中", "高中"}; + difficultyComboBox = new JComboBox<>(difficulties); + difficultyComboBox.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + JLabel countLabel = new JLabel("题目数量:"); + countLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + countField = new JTextField("10"); + countField.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + JButton startButton = new JButton("开始答题"); + startButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + formPanel.add(difficultyLabel); + formPanel.add(difficultyComboBox); + formPanel.add(countLabel); + formPanel.add(countField); + formPanel.add(new JLabel()); // Empty cell + formPanel.add(startButton); + + mainPanel.add(formPanel, BorderLayout.CENTER); + + + JPanel buttonPanel = new JPanel(new FlowLayout()); + JButton changePasswordButton = new JButton("修改密码"); + JButton logoutButton = new JButton("退出登录"); + + changePasswordButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + logoutButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + buttonPanel.add(changePasswordButton); + buttonPanel.add(logoutButton); + + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + // Add action listeners + startButton.addActionListener(new StartExamAction()); + changePasswordButton.addActionListener(new ChangePasswordAction()); + logoutButton.addActionListener(e -> navigationService.showLoginView()); // 修改 + + add(mainPanel); + } + + private class StartExamAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String difficulty = (String) difficultyComboBox.getSelectedItem(); + String countText = countField.getText().trim(); + + try { + int count = Integer.parseInt(countText); + if (count < 10 || count > 30) { + JOptionPane.showMessageDialog(MainView.this, + "题目数量必须在10-30之间", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + navigationService.showExamView(difficulty, count); // 修改 + } catch (NumberFormatException ex) { + JOptionPane.showMessageDialog(MainView.this, + "请输入有效的数字", "错误", JOptionPane.ERROR_MESSAGE); + } + } + } + + private class ChangePasswordAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + // 打开修改密码对话框 + new ChangePasswordDialog(MainView.this, currentUser).setVisible(true); + } + } +} \ No newline at end of file diff --git a/src/view/RegisterView.java b/src/view/RegisterView.java new file mode 100644 index 0000000..14c8ec2 --- /dev/null +++ b/src/view/RegisterView.java @@ -0,0 +1,164 @@ +package view; + +import controller.NavigationController; +import controller.NavigationService; +import controller.UserController; +import model.User; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.JPasswordField; +import javax.swing.JButton; +import javax.swing.BorderFactory; +import javax.swing.JOptionPane; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class RegisterView extends JFrame { + private JTextField usernameField; // 用户名输入框 + private JTextField emailField; + private JTextField codeField; + private JPasswordField passwordField; + private JPasswordField confirmPasswordField; + private UserController userController; + private NavigationService navigationService; // 新增 + private String currentUsername; // 保存当前注册的用户名 + + public RegisterView() { + userController = new UserController(); + navigationService = NavigationController.getInstance(); // 新增 + initializeUI(); + } + + private void initializeUI() { + setTitle("数学学习软件 - 注册"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(550, 500); + setLocationRelativeTo(null); + setResizable(false); + + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Title + JLabel titleLabel = new JLabel("用户注册", JLabel.CENTER); + titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24)); + mainPanel.add(titleLabel, BorderLayout.NORTH); + + // Form panel - 增加用户名字段 + JPanel formPanel = new JPanel(new GridLayout(8, 2, 10, 10)); + + JLabel usernameLabel = new JLabel("用户名:"); + usernameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + usernameField = new JTextField(); + + JLabel emailLabel = new JLabel("邮箱:"); + emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + emailField = new JTextField(); + + JLabel codeLabel = new JLabel("注册码:"); + codeLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + codeField = new JTextField(); + + JLabel passwordLabel = new JLabel("密码:"); + passwordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + passwordField = new JPasswordField(); + + JLabel confirmPasswordLabel = new JLabel("确认密码:"); + confirmPasswordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + confirmPasswordField = new JPasswordField(); + + JButton sendCodeButton = new JButton("发送注册码"); + sendCodeButton.setFont(new Font("微软雅黑", Font.PLAIN, 12)); + + JButton registerButton = new JButton("完成注册"); + registerButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + // 布局表单 + formPanel.add(usernameLabel); + formPanel.add(usernameField); + formPanel.add(emailLabel); + formPanel.add(emailField); + formPanel.add(sendCodeButton); + formPanel.add(new JLabel()); + formPanel.add(codeLabel); + formPanel.add(codeField); + formPanel.add(passwordLabel); + formPanel.add(passwordField); + formPanel.add(confirmPasswordLabel); + formPanel.add(confirmPasswordField); + + formPanel.add(registerButton); + + mainPanel.add(formPanel, BorderLayout.CENTER); + + // Back button + JButton backButton = new JButton("返回登录"); + backButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + backButton.addActionListener(e -> navigationService.showLoginView()); // 修改 + + JPanel bottomPanel = new JPanel(new FlowLayout()); + bottomPanel.add(backButton); + mainPanel.add(bottomPanel, BorderLayout.SOUTH); + + // Add action listeners + sendCodeButton.addActionListener(new SendCodeAction()); + registerButton.addActionListener(new RegisterAction()); + + add(mainPanel); + } + + private class SendCodeAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + + if (username.isEmpty() || email.isEmpty()) { + JOptionPane.showMessageDialog(RegisterView.this, + "请输入用户名和邮箱", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + String result = userController.registerUser(username, email); + JOptionPane.showMessageDialog(RegisterView.this, result); + + if (result.contains("发送")) { + currentUsername = username; + } + } + } + + private class RegisterAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + String username = usernameField.getText().trim(); + String code = codeField.getText().trim(); + String password = new String(passwordField.getPassword()); + String confirmPassword = new String(confirmPasswordField.getPassword()); + + if (username.isEmpty() || code.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) { + JOptionPane.showMessageDialog(RegisterView.this, + "请填写所有字段", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + + String result = userController.completeRegistration(username, code, password, confirmPassword); + + if (result.equals("注册成功!")) { + JOptionPane.showMessageDialog(RegisterView.this, + result, "成功", JOptionPane.INFORMATION_MESSAGE); + navigationService.showLoginView(); // 修改 + } else { + JOptionPane.showMessageDialog(RegisterView.this, + result, "错误", JOptionPane.ERROR_MESSAGE); + } + } + } +} \ No newline at end of file diff --git a/src/view/ResultView.java b/src/view/ResultView.java new file mode 100644 index 0000000..c1b4393 --- /dev/null +++ b/src/view/ResultView.java @@ -0,0 +1,79 @@ +package view; + +import controller.NavigationController; +import controller.NavigationService; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JButton; +import javax.swing.BorderFactory; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class ResultView extends JFrame { + private JLabel scoreLabel; + private JLabel percentageLabel; + private NavigationService navigationService; + + public ResultView() { + navigationService = NavigationController.getInstance(); // 新增 + initializeUI(); + } + + private void initializeUI() { + setTitle("数学学习软件 - 成绩界面"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(400, 300); + setLocationRelativeTo(null); + setResizable(false); + + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Title + JLabel titleLabel = new JLabel("考试结果", JLabel.CENTER); + titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24)); + mainPanel.add(titleLabel, BorderLayout.NORTH); + + // Result panel + JPanel resultPanel = new JPanel(new GridLayout(2, 1, 10, 10)); + + scoreLabel = new JLabel("", JLabel.CENTER); + scoreLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18)); + + percentageLabel = new JLabel("", JLabel.CENTER); + percentageLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18)); + + resultPanel.add(scoreLabel); + resultPanel.add(percentageLabel); + mainPanel.add(resultPanel, BorderLayout.CENTER); + + // Button panel + JPanel buttonPanel = new JPanel(new FlowLayout()); + JButton continueButton = new JButton("继续做题"); + JButton exitButton = new JButton("退出系统"); + + continueButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + exitButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + + buttonPanel.add(continueButton); + buttonPanel.add(exitButton); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + // Add action listeners + continueButton.addActionListener(e -> navigationService.showMainView()); // 修改 + exitButton.addActionListener(e -> System.exit(0)); + + add(mainPanel); + } + + public void setResults(int score, int total, double percentage) { + scoreLabel.setText(String.format("得分: %d / %d", score, total)); + percentageLabel.setText(String.format("正确率: %.1f%%", percentage)); + } +} \ No newline at end of file