diff --git a/pom.xml b/pom.xml index 4d69a1a..5b323ad 100644 --- a/pom.xml +++ b/pom.xml @@ -61,25 +61,21 @@ org.apache.maven.plugins - maven-assembly-plugin - 3.6.0 - - - - com.mathgenerator.MainApplication - - - - jar-with-dependencies - - + maven-shade-plugin + 3.2.4 - make-assembly package - single + shade + + + + com.mathgenerator.Launcher + + + diff --git a/src/main/java/com/mathgenerator/Launcher.java b/src/main/java/com/mathgenerator/Launcher.java new file mode 100644 index 0000000..6773f2d --- /dev/null +++ b/src/main/java/com/mathgenerator/Launcher.java @@ -0,0 +1,22 @@ +package com.mathgenerator; + +/** + * 应用程序启动器类。 + *

+ * 这个类的唯一目的是为了解决在使用某些构建工具(如 Maven Shade Plugin)将 JavaFX 应用程序 + * 打包成一个可执行的 "fat JAR" 文件时可能出现的运行时问题。 + *

+ * 它通过一个标准的 {@code main} 方法来调用 {@link MainApplication#main(String[])} 方法, + * 充当了 JavaFX 应用程序的实际入口点。 + */ +public class Launcher { + + /** + * 程序的主入口点。 + * + * @param args 传递给应用程序的命令行参数。 + */ + public static void main(String[] args) { + MainApplication.main(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/MainApplication.java b/src/main/java/com/mathgenerator/MainApplication.java index cb6f216..a93e940 100644 --- a/src/main/java/com/mathgenerator/MainApplication.java +++ b/src/main/java/com/mathgenerator/MainApplication.java @@ -4,21 +4,56 @@ import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.image.Image; import javafx.stage.Stage; import java.io.IOException; +/** + * JavaFX 应用程序的主类。 + *

+ * 该类继承自 {@link Application},是整个 JavaFX 应用的生命周期入口。 + * 它负责初始化主舞台 (Stage),加载初始视图 (LoginView.fxml),设置窗口标题、 + * 图标和样式表,并最终显示应用程序窗口。 + */ public class MainApplication extends Application { + /** + * JavaFX 应用程序的启动方法。 + *

+ * 当应用启动时,JavaFX 平台会调用此方法。它负责配置和显示主窗口。 + * + * @param primaryStage 由 JavaFX 平台自动创建和传入的主舞台对象。 + * @throws IOException 如果在加载 FXML 文件时发生 I/O 错误。 + */ @Override public void start(Stage primaryStage) throws IOException { - // 启动时加载登录界面 Parent root = FXMLLoader.load(getClass().getResource("/com/mathgenerator/view/LoginView.fxml")); + + Scene scene = new Scene(root); + scene.getStylesheets().add(getClass().getResource("/com/mathgenerator/styles/styles.css").toExternalForm()); + primaryStage.setTitle("中小学数学学习软件"); - primaryStage.setScene(new Scene(root)); + + try { + Image appIcon = new Image(getClass().getResourceAsStream("/com/mathgenerator/images/icon.png")); + primaryStage.getIcons().add(appIcon); + } catch (Exception e) { + System.err.println("错误:无法加载应用程序图标!请检查 'icon.png' 是否在 images 文件夹中。"); + e.printStackTrace(); + } + + primaryStage.setScene(scene); primaryStage.setResizable(false); primaryStage.show(); } + /** + * 应用程序的静态 main 方法。 + *

+ * 这是 Java 程序的标准入口点,它通过调用 {@link #launch(String...)} 来启动 JavaFX 应用程序。 + * + * @param args 传递给应用程序的命令行参数。 + */ public static void main(String[] args) { launch(args); } diff --git a/src/main/java/com/mathgenerator/controller/ChangePasswordController.java b/src/main/java/com/mathgenerator/controller/ChangePasswordController.java index ceab172..6b959a6 100644 --- a/src/main/java/com/mathgenerator/controller/ChangePasswordController.java +++ b/src/main/java/com/mathgenerator/controller/ChangePasswordController.java @@ -13,6 +13,10 @@ import javafx.scene.control.PasswordField; import javafx.stage.Stage; import java.io.IOException; +/** + * “修改密码” 视图 (ChangePasswordView.fxml) 的 FXML 控制器类。 + * 该类负责处理已登录用户修改自己密码的逻辑。 + */ public class ChangePasswordController { private final UserService userService = new UserService(); @@ -26,51 +30,96 @@ public class ChangePasswordController { @FXML private Label statusLabel; /** - * 初始化控制器,接收当前用户信息 + * 初始化控制器,并从主菜单接收当前登录的用户信息。 + * 这个方法在 FXML 文件加载完成后被调用。 + * + * @param user 当前登录的 User 对象。 */ public void initData(User user) { this.currentUser = user; } + /** + * 处理“确认修改”按钮的点击事件。 + * 该方法会验证用户输入的密码,并调用用户服务来执行密码修改操作。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 + */ @FXML private void handleConfirmAction(ActionEvent event) { - // 1. 获取输入 String oldPassword = oldPasswordField.getText(); String newPassword = newPasswordField.getText(); String confirmNewPassword = confirmNewPasswordField.getText(); - // 2. 输入校验 if (oldPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) { - statusLabel.setText("所有密码字段都不能为空!"); + showStatusMessage("所有密码字段都不能为空!", true); return; } if (!newPassword.equals(confirmNewPassword)) { - statusLabel.setText("两次输入的新密码不匹配!"); + showStatusMessage("两次输入的新密码不匹配!", true); + return; + } + // 新增校验:新密码不能与当前密码相同 + if (oldPassword.equals(newPassword)) { + showStatusMessage("新密码不能与当前密码相同!", true); return; } if (!UserService.isPasswordValid(newPassword)) { - statusLabel.setText("新密码格式错误!必须为6-10位,且包含大小写字母和数字。"); + showStatusMessage("新密码格式错误!必须为6-10位,且包含大小写字母和数字。", true); return; } - // 3. 调用后端服务修改密码 boolean success = userService.changePassword( currentUser.username(), oldPassword, newPassword ); - // 4. 更新UI反馈 if (success) { - statusLabel.setText("密码修改成功!请返回主菜单。"); + showStatusMessage("密码修改成功!请返回主菜单。", true); confirmButton.setDisable(true); // 防止重复点击 } else { - statusLabel.setText("修改失败:当前密码错误。"); + showStatusMessage("修改失败:当前密码错误。", true); + } + } + + /** + * 在状态标签 (statusLabel) 中显示一条消息,并更新其样式。 + * + * @param message 要显示的消息文本。 + * @param hasContent 一个布尔值,指示消息是否为空,用于确定应用何种样式。 + */ + private void showStatusMessage(String message, boolean hasContent) { + statusLabel.setText(message); + updateStatusLabelStyle(hasContent); + } + + /** + * 根据状态标签是否有内容来更新其 CSS 样式。 + * + * @param hasContent 如果为 true,则应用包含文本内容的样式;否则,应用空标签的样式。 + */ + private void updateStatusLabelStyle(boolean hasContent) { + if (hasContent) { + // 移除空样式,添加有内容样式 + statusLabel.getStyleClass().removeAll("password-status-label"); + if (!statusLabel.getStyleClass().contains("password-status-label-with-text")) { + statusLabel.getStyleClass().add("password-status-label-with-text"); + } + } else { + // 移除有内容样式,添加空样式 + statusLabel.getStyleClass().removeAll("password-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("password-status-label")) { + statusLabel.getStyleClass().add("password-status-label"); + } } } /** - * 处理返回按钮事件,返回主菜单 + * 处理“返回主菜单”按钮的点击事件。 + * 导航用户返回到主菜单界面。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleBackAction(ActionEvent event) { diff --git a/src/main/java/com/mathgenerator/controller/LoginController.java b/src/main/java/com/mathgenerator/controller/LoginController.java index 264c0c0..0e65ce7 100644 --- a/src/main/java/com/mathgenerator/controller/LoginController.java +++ b/src/main/java/com/mathgenerator/controller/LoginController.java @@ -16,6 +16,11 @@ import javafx.stage.Stage; import java.io.IOException; import java.util.Optional; import com.mathgenerator.util.ValidationUtils; + +/** + * “登录”视图 (LoginView.fxml) 的 FXML 控制器类。 + * 负责处理用户身份验证,并导航到注册界面或主菜单。 + */ public class LoginController { // 依赖注入后端服务 @@ -38,8 +43,10 @@ public class LoginController { private Label statusLabel; /** - * 处理登录按钮点击事件。 - * @param event 事件对象 + * 处理“登录”按钮的点击事件。 + * 该方法会验证用户的输入,并尝试登录。如果成功,则导航到主菜单界面。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleLoginButtonAction(ActionEvent event) { @@ -69,8 +76,10 @@ public class LoginController { } /** - * 处理注册按钮点击事件,跳转到注册界面。 - * @param event 事件对象 + * 处理“注册”按钮的点击事件。 + * 导航用户到注册界面。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleRegisterButtonAction(ActionEvent event) { @@ -78,8 +87,10 @@ public class LoginController { } /** - * 加载主菜单界面,并传递用户信息。 - * @param user 登录成功的用户对象 + * 在用户成功登录后加载主菜单界面。 + * 此方法会将登录用户的完整信息传递给 MainMenuController。 + * + * @param user 成功登录的用户的 User 对象。 */ private void loadMainMenu(User user) { try { @@ -104,8 +115,9 @@ public class LoginController { } /** - * 切换到简单场景的辅助方法(如注册页)。 - * @param fxmlPath FXML文件的路径 + * 一个辅助工具方法,用于在当前窗口加载一个新的场景。 + * + * @param fxmlPath 要加载的场景的 FXML 文件路径。 */ private void loadScene(String fxmlPath) { try { diff --git a/src/main/java/com/mathgenerator/controller/MainMenuController.java b/src/main/java/com/mathgenerator/controller/MainMenuController.java index 60f7930..8af25e8 100644 --- a/src/main/java/com/mathgenerator/controller/MainMenuController.java +++ b/src/main/java/com/mathgenerator/controller/MainMenuController.java @@ -13,6 +13,11 @@ import javafx.scene.control.TextField; import javafx.stage.Stage; import java.io.IOException; + +/** + * “主菜单”视图 (MainMenuView.fxml) 的 FXML 控制器类。 + * 用户登录后进入此界面,可以选择题目难度、数量,并开始答题或进行其他操作。 + */ public class MainMenuController { private User currentUser; @@ -23,68 +28,109 @@ public class MainMenuController { @FXML private Button logoutButton; /** - * 初始化控制器,接收登录成功的用户信息。 - * @param user 当前登录的用户 + * 初始化控制器,并接收登录成功的用户信息。 + * + * @param user 当前登录的用户对象。 */ public void initData(User user) { this.currentUser = user; - welcomeLabel.setText("欢迎, " + currentUser.username() + "!"); + welcomeLabel.setText("🎉 欢迎, " + currentUser.username() + "!"); // 添加图标 + // 初始化状态标签 + clearStatus(); } + /** + * 处理选择“小学”难度按钮的事件。 + * + * @param event 事件对象。 + */ @FXML private void handlePrimaryAction(ActionEvent event) { startQuiz(Level.PRIMARY); } + /** + * 处理选择“初中”难度按钮的事件。 + * + * @param event 事件对象。 + */ @FXML private void handleJuniorHighAction(ActionEvent event) { startQuiz(Level.JUNIOR_HIGH); } + /** + * 处理选择“高中”难度按钮的事件。 + * + * @param event 事件对象。 + */ @FXML private void handleSeniorHighAction(ActionEvent event) { startQuiz(Level.SENIOR_HIGH); } + /** + * 处理“修改密码”按钮的点击事件,导航到修改密码界面。 + * + * @param event 事件对象。 + */ @FXML private void handleChangePasswordAction(ActionEvent event) { try { - // 1\. 加载 FXML 文件 + // 1. 加载 FXML 文件 FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ChangePasswordView.fxml")); Parent root = loader.load(); - // 2\. 获取新界面的控制器 + // 2. 获取新界面的控制器 ChangePasswordController controller = loader.getController(); - // 3\. 调用控制器的方法,传递当前用户信息 + // 3. 调用控制器的方法,传递当前用户信息 controller.initData(currentUser); - // 4\. 显示新场景 + // 4. 显示新场景 Stage stage = (Stage) logoutButton.getScene().getWindow(); stage.setScene(new Scene(root)); stage.setTitle("修改密码"); } catch (IOException e) { e.printStackTrace(); + setErrorStatus("加载修改密码界面失败!"); } } - @FXML + + /** + * 处理“退出登录”按钮的点击事件,返回到登录界面。 + * + * @param event 事件对象。 + */ + @FXML private void handleLogoutAction(ActionEvent event) { // 跳转回登录界面 loadScene("/com/mathgenerator/view/LoginView.fxml"); } /** - * 验证输入并准备开始答题。 - * @param level 选择的难度 + * 验证用户输入的题目数量,并准备开始答题。 + * 如果输入有效,则加载答题界面并传递相关数据。 + * + * @param level 用户选择的题目难度级别。 */ private void startQuiz(Level level) { try { - int count = Integer.parseInt(questionCountField.getText()); - if (count < 1 || count > 50) { - statusLabel.setText("题目数量必须在 1 到 50 之间!"); + String countText = questionCountField.getText().trim(); + + // 检查是否为空 + if (countText.isEmpty()) { + setErrorStatus("请输入题目数量!"); return; } - + int count = Integer.parseInt(countText); + // 修改范围检查:从1-50改为10-30 + if (count < 10 || count > 30) { + setErrorStatus("题目数量必须在 10 到 30 之间!"); + return; + } + // 清除状态信息 + clearStatus(); // 加载答题界面,并传递数据 FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/QuizView.fxml")); Parent root = loader.load(); @@ -97,12 +143,36 @@ public class MainMenuController { stage.setTitle(level.getChineseName() + " - 答题中"); } catch (NumberFormatException e) { - statusLabel.setText("请输入有效的题目数量!"); + setErrorStatus("请输入有效的数字!"); } catch (IOException e) { e.printStackTrace(); + setErrorStatus("加载答题界面失败,请重试!"); } } + /** + * 在状态标签中设置一条错误消息。 + * + * @param message 要显示的错误消息文本。 + */ + private void setErrorStatus(String message) { + statusLabel.setText(message); + statusLabel.getStyleClass().setAll("status-label"); + } + + /** + * 清除状态标签中的任何消息。 + */ + private void clearStatus() { + statusLabel.setText(""); + statusLabel.getStyleClass().clear(); + } + + /** + * 一个辅助工具方法,用于加载并切换到新的场景。 + * + * @param fxmlPath 要加载的 FXML 文件的路径。 + */ private void loadScene(String fxmlPath) { try { Stage stage = (Stage) logoutButton.getScene().getWindow(); diff --git a/src/main/java/com/mathgenerator/controller/QuizController.java b/src/main/java/com/mathgenerator/controller/QuizController.java index d2c7e0d..2b948bd 100644 --- a/src/main/java/com/mathgenerator/controller/QuizController.java +++ b/src/main/java/com/mathgenerator/controller/QuizController.java @@ -21,18 +21,18 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +/** + * “答题”视图 (QuizView.fxml) 的 FXML 控制器类。 + * 负责展示题目、接收用户答案、处理答题流程,并在结束后跳转到分数界面。 + */ public class QuizController { - // --- 后端服务 --- private final PaperService paperService; - - // --- 答题状态 --- private User currentUser; private List questions; private List userAnswers = new ArrayList<>(); private int currentQuestionIndex = 0; - // --- FXML 控件 --- @FXML private Label questionNumberLabel; @FXML private ProgressBar progressBar; @FXML private Label questionTextLabel; @@ -42,34 +42,32 @@ public class QuizController { @FXML private Label statusLabel; /** - * 构造函数,初始化后端服务 + * 构造函数。 + * 在这里初始化后端服务,如 PaperService。 */ public QuizController() { - // 这是依赖注入的一种简化形式,在真实项目中会使用框架管理 FileManager fileManager = new FileManager(); MixedDifficultyStrategy strategy = new MixedDifficultyStrategy(); this.paperService = new PaperService(fileManager, strategy); } /** - * 接收从主菜单传递过来的数据,并开始答题 + * 初始化控制器,从主菜单接收用户、难度和题目数量,并开始答题。 + * + * @param user 当前登录的用户。 + * @param level 选择的题目难度。 + * @param questionCount 生成的题目数量。 */ public void initData(User user, Level level, int questionCount) { this.currentUser = user; - - // 1. 调用后端服务生成题目 this.questions = paperService.createPaper(user, questionCount, level); - - // --- 核心修改在这里 --- - // 2. 立即调用后端服务,在后台自动保存生成的试卷 paperService.savePaper(user.username(), this.questions); - - // 3. 正常显示第一道题 displayCurrentQuestion(); } /** - * 显示当前的题目和选项 (已更新,增加ABCD前缀) + * 在界面上显示当前的题目和选项。 + * 同时更新进度条和题目编号。 */ private void displayCurrentQuestion() { ChoiceQuestion currentQuestion = questions.get(currentQuestionIndex); @@ -79,14 +77,13 @@ public class QuizController { questionTextLabel.setText(currentQuestion.questionText()); List radioButtons = List.of(option1, option2, option3, option4); - String[] prefixes = {"A. ", "B. ", "C. ", "D. "}; // 定义选项前缀 + String[] prefixes = {"A. ", "B. ", "C. ", "D. "}; for (int i = 0; i < radioButtons.size(); i++) { - // 将前缀和选项文本结合起来 radioButtons.get(i).setText(prefixes[i] + currentQuestion.options().get(i)); } - optionsGroup.selectToggle(null); // 清除上一次的选择 - statusLabel.setText(""); // 清除状态提示 + optionsGroup.selectToggle(null); + updateStatusLabelStyle(false); if (currentQuestionIndex == questions.size() - 1) { submitButton.setText("完成答题"); @@ -94,32 +91,54 @@ public class QuizController { } /** - * 处理提交按钮的点击事件 + * 处理“提交答案”按钮的点击事件。 + * 记录用户的选择,然后切换到下一题或结束答题。 + * + * @param event 事件对象。 */ @FXML private void handleSubmitButtonAction(ActionEvent event) { RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle(); if (selectedRadioButton == null) { statusLabel.setText("请选择一个答案!"); + updateStatusLabelStyle(true); return; } - // 记录用户答案的索引 + updateStatusLabelStyle(false); + List radioButtons = List.of(option1, option2, option3, option4); userAnswers.add(radioButtons.indexOf(selectedRadioButton)); - // 移动到下一题或结束答题 currentQuestionIndex++; if (currentQuestionIndex < questions.size()) { displayCurrentQuestion(); } else { - // 答题结束,计算分数并跳转到分数界面 calculateScoreAndShowResults(); } } /** - * 计算分数并准备跳转到结果页面 + * 根据状态标签是否有内容来更新其 CSS 样式。 + * + * @param hasContent 如果为 true,应用带文本的样式;否则,应用空标签的样式。 + */ + private void updateStatusLabelStyle(boolean hasContent) { + if (hasContent) { + statusLabel.getStyleClass().removeAll("quiz-status-label-empty"); + if (!statusLabel.getStyleClass().contains("quiz-status-label-with-text")) { + statusLabel.getStyleClass().add("quiz-status-label-with-text"); + } + } else { + statusLabel.getStyleClass().removeAll("quiz-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("quiz-status-label-empty")) { + statusLabel.getStyleClass().add("quiz-status-label-empty"); + } + } + } + + /** + * 在所有题目回答完毕后,计算最终得分并导航到分数显示界面。 */ private void calculateScoreAndShowResults() { int correctCount = 0; @@ -130,22 +149,19 @@ public class QuizController { } double score = (double) correctCount / questions.size() * 100; - // 禁用当前页面的按钮 submitButton.setDisable(true); statusLabel.setText("答题已完成,正在为您计算分数..."); + updateStatusLabelStyle(true); - // 加载分数界面并传递数据 try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/ScoreView.fxml")); Parent root = loader.load(); - ScoreController controller = loader.getController(); - controller.initData(currentUser, score); // 将用户和分数传递过去 + controller.initData(currentUser, score); Stage stage = (Stage) submitButton.getScene().getWindow(); stage.setScene(new Scene(root)); stage.setTitle("答题结果"); - } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/com/mathgenerator/controller/RegisterController.java b/src/main/java/com/mathgenerator/controller/RegisterController.java index d7fd31a..8f98a43 100644 --- a/src/main/java/com/mathgenerator/controller/RegisterController.java +++ b/src/main/java/com/mathgenerator/controller/RegisterController.java @@ -1,6 +1,9 @@ package com.mathgenerator.controller; import com.mathgenerator.service.UserService; +import com.mathgenerator.util.ValidationUtils; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; @@ -11,12 +14,20 @@ import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import javafx.stage.Stage; +import javafx.util.Duration; + import java.io.IOException; -import com.mathgenerator.util.ValidationUtils; + +/** + * “注册”视图 (RegisterView.fxml) 的 FXML 控制器类。 + * 负责处理新用户的注册流程,包括发送验证码和验证用户输入。 + */ public class RegisterController { private final UserService userService = new UserService(); - private String sentCode; // 用于存储已发送的验证码 + private String sentCode; + private Timeline countdownTimeline; + private int countdownSeconds = 60; @FXML private TextField usernameField; @FXML private TextField emailField; @@ -28,65 +39,123 @@ public class RegisterController { @FXML private Button backToLoginButton; @FXML private Label statusLabel; + /** + * 处理“发送验证码”按钮的点击事件。 + * 验证邮箱格式,并调用服务发送验证码邮件,然后启动一个60秒的冷却倒计时。 + * + * @param event 事件对象。 + */ @FXML private void handleSendCodeAction(ActionEvent event) { String email = emailField.getText(); - if (email.isEmpty() || !email.contains("@")) { - statusLabel.setText("请输入一个有效的邮箱地址!"); + if (!ValidationUtils.isEmailValid(email)) { + showStatusMessage("请输入一个有效的邮箱地址!", true); return; } - // 调用后端服务发送验证码 this.sentCode = userService.sendVerificationCode(email); - // 处理发送结果 if (this.sentCode != null) { - statusLabel.setText("验证码已成功发送,请查收您的邮箱。"); - sendCodeButton.setDisable(true); // 防止重复点击 + showStatusMessage("验证码已成功发送,请查收您的邮箱。", true); + startCountdown(); } else { - statusLabel.setText("验证码发送失败!请检查配置或联系管理员。"); + showStatusMessage("验证码发送失败!请检查配置或联系管理员。", true); } } + /** + * 启动发送验证码按钮的60秒冷却倒计时。 + * 在倒计时期间,按钮将被禁用。 + */ + private void startCountdown() { + sendCodeButton.setDisable(true); + countdownTimeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> { + countdownSeconds--; + sendCodeButton.setText(countdownSeconds + "s后重试"); + if (countdownSeconds <= 0) { + countdownTimeline.stop(); + sendCodeButton.setDisable(false); + sendCodeButton.setText("发送验证码"); + countdownSeconds = 60; + } + })); + countdownTimeline.setCycleCount(60); + countdownTimeline.play(); + } + + /** + * 处理“确认注册”按钮的点击事件。 + * 验证所有输入字段,如果有效,则调用服务注册新用户,并导航到设置密码界面。 + * + * @param event 事件对象。 + */ @FXML private void handleRegisterAction(ActionEvent event) { - // 1. 字段校验 (已简化,不再校验密码) String username = usernameField.getText(); String email = emailField.getText(); if (!ValidationUtils.isUsernameValid(username) || !ValidationUtils.isEmailValid(email) || verificationCodeField.getText().isEmpty()) { - statusLabel.setText("所有字段都不能为空且格式正确!"); + showStatusMessage("所有字段都不能为空且格式正确!", true); return; } + if (this.sentCode == null || !this.sentCode.equals(verificationCodeField.getText())) { - statusLabel.setText("验证码错误!"); + showStatusMessage("验证码错误!", true); return; } - // 2. 调用后端服务进行无密码注册 boolean success = userService.register(username, email); - // 3. 根据结果更新UI或跳转 if (success) { - statusLabel.setText("注册成功!请设置您的密码。"); - // 成功后,加载设置密码界面,并传递用户名 + showStatusMessage("注册成功!请设置您的密码。", true); loadSetPasswordScene(username); } else { - statusLabel.setText("注册失败:用户名或邮箱已被占用。"); + showStatusMessage("注册失败:用户名或邮箱已被占用。", true); } } /** - * (新增) 加载设置密码界面,并传递用户名。 + * 在状态标签中显示一条消息,并根据内容更新其样式。 + * + * @param message 要显示的消息。 + * @param hasContent 如果消息非空则为true。 + */ + private void showStatusMessage(String message, boolean hasContent) { + statusLabel.setText(message); + updateStatusLabelStyle(hasContent); + } + + /** + * 根据标签是否有内容来更新其CSS样式。 + * + * @param hasContent 如果有内容则为true。 + */ + private void updateStatusLabelStyle(boolean hasContent) { + if (hasContent) { + statusLabel.getStyleClass().removeAll("register-status-label"); + if (!statusLabel.getStyleClass().contains("register-status-label-with-text")) { + statusLabel.getStyleClass().add("register-status-label-with-text"); + } + } else { + statusLabel.getStyleClass().removeAll("register-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("register-status-label")) { + statusLabel.getStyleClass().add("register-status-label"); + } + } + } + + /** + * 加载“设置密码”场景,并将新注册的用户名传递过去。 + * + * @param username 新注册的用户名。 */ private void loadSetPasswordScene(String username) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/SetPasswordView.fxml")); Parent root = loader.load(); - SetPasswordController controller = loader.getController(); - controller.initData(username); // 将用户名传递给新界面的控制器 + controller.initData(username); Stage stage = (Stage) registerButton.getScene().getWindow(); stage.setScene(new Scene(root)); @@ -96,11 +165,21 @@ public class RegisterController { } } + /** + * 处理“返回登录”按钮的点击事件,导航回登录界面。 + * + * @param event 事件对象。 + */ @FXML private void handleBackToLoginAction(ActionEvent event) { loadScene("/com/mathgenerator/view/LoginView.fxml"); } + /** + * 辅助方法,用于加载并切换到指定的FXML场景。 + * + * @param fxmlPath FXML文件的路径。 + */ private void loadScene(String fxmlPath) { try { Parent root = FXMLLoader.load(getClass().getResource(fxmlPath)); @@ -110,4 +189,13 @@ public class RegisterController { e.printStackTrace(); } } + + /** + * 在控制器销毁前调用的清理方法,用于停止任何正在运行的后台任务,如倒计时。 + */ + public void cleanup() { + if (countdownTimeline != null) { + countdownTimeline.stop(); + } + } } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/controller/ScoreController.java b/src/main/java/com/mathgenerator/controller/ScoreController.java index dbcba69..584d1e2 100644 --- a/src/main/java/com/mathgenerator/controller/ScoreController.java +++ b/src/main/java/com/mathgenerator/controller/ScoreController.java @@ -11,6 +11,10 @@ import javafx.scene.control.Label; import javafx.stage.Stage; import java.io.IOException; +/** + * “分数”视图 (ScoreView.fxml) 的 FXML 控制器类。 + * 负责在用户完成答题后,显示最终得分和相应的鼓励消息。 + */ public class ScoreController { private User currentUser; @@ -21,15 +25,15 @@ public class ScoreController { @FXML private Button logoutButton; /** - * 初始化控制器,接收答题结果数据 - * @param user 当前用户 - * @param score 最终得分 + * 初始化控制器,并接收从答题界面传递过来的用户和分数数据。 + * + * @param user 当前登录的用户对象。 + * @param score 用户在上一轮答题中获得的最终分数。 */ public void initData(User user, double score) { this.currentUser = user; scoreLabel.setText(String.format("%.2f", score)); - // 根据分数显示不同的鼓励语 if (score == 100.0) { resultMessageLabel.setText("太棒了!你答对了所有题目!"); } else if (score >= 80) { @@ -42,7 +46,10 @@ public class ScoreController { } /** - * 处理“再做一组”按钮事件,返回主菜单 + * 处理“再做一组”按钮的点击事件。 + * 导航用户返回到主菜单,以便开始新一轮的练习。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleTryAgainAction(ActionEvent event) { @@ -50,7 +57,7 @@ public class ScoreController { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); Parent root = loader.load(); MainMenuController controller = loader.getController(); - controller.initData(currentUser); // 将用户信息传回主菜单 + controller.initData(currentUser); Stage stage = (Stage) tryAgainButton.getScene().getWindow(); stage.setScene(new Scene(root)); @@ -61,7 +68,10 @@ public class ScoreController { } /** - * 处理“退出登录”按钮事件,返回登录界面 + * 处理“退出登录”按钮的点击事件。 + * 导航用户返回到登录界面。 + * + * @param event 由按钮点击触发的 ActionEvent 事件。 */ @FXML private void handleLogoutAction(ActionEvent event) { diff --git a/src/main/java/com/mathgenerator/controller/SetPasswordController.java b/src/main/java/com/mathgenerator/controller/SetPasswordController.java index 278c80c..9d6a583 100644 --- a/src/main/java/com/mathgenerator/controller/SetPasswordController.java +++ b/src/main/java/com/mathgenerator/controller/SetPasswordController.java @@ -15,6 +15,10 @@ import javafx.stage.Stage; import java.io.IOException; +/** + * “设置密码”视图 (SetPasswordView.fxml) 的 FXML 控制器类。 + * 用户在注册成功后会进入此界面,以设置他们的初始密码。 + */ public class SetPasswordController { private final UserService userService = new UserService(); @@ -27,48 +31,88 @@ public class SetPasswordController { @FXML private Label statusLabel; /** - * 接收从注册界面传递过来的用户名 + * 初始化控制器,并接收从注册界面传递过来的用户名。 + * + * @param username 新注册的用户名。 */ public void initData(String username) { this.username = username; promptLabel.setText("为您的账户 " + username + " 设置密码"); } + /** + * 处理“确认”按钮的点击事件。 + * 该方法会验证两次输入的密码是否一致且符合格式要求,然后调用服务为用户设置密码, + * 成功后直接登录并跳转到主菜单。 + * + * @param event 事件对象。 + */ @FXML private void handleConfirmAction(ActionEvent event) { String newPassword = newPasswordField.getText(); String confirmPassword = confirmPasswordField.getText(); if (!newPassword.equals(confirmPassword)) { - statusLabel.setText("两次输入的密码不匹配!"); + showStatusMessage("两次输入的密码不匹配!", true); return; } + if (!ValidationUtils.isPasswordValid(newPassword)) { - statusLabel.setText("新密码格式错误!必须为6-10位,且包含大小写字母和数字。"); + showStatusMessage("新密码格式错误!必须为6-10位,且包含大小写字母和数字。", true); return; } - // 调用后端服务设置密码 boolean success = userService.setPassword(this.username, newPassword); if (success) { - statusLabel.setText("密码设置成功!正在进入主菜单..."); - // 密码设置成功后,获取完整的用户信息并直接跳转到主菜单 + showStatusMessage("密码设置成功!正在进入主菜单...", true); userService.findUserByUsername(this.username).ifPresent(this::loadMainMenu); } else { - statusLabel.setText("密码设置失败,请稍后重试或重新注册。"); + showStatusMessage("密码设置失败,请稍后重试或重新注册。", true); + } + } + + /** + * 在状态标签中显示一条消息,并根据内容更新其样式。 + * + * @param message 要显示的消息。 + * @param hasContent 如果消息非空则为true。 + */ + private void showStatusMessage(String message, boolean hasContent) { + statusLabel.setText(message); + updateStatusLabelStyle(hasContent); + } + + /** + * 根据标签是否有内容来更新其CSS样式。 + * + * @param hasContent 如果有内容则为true。 + */ + private void updateStatusLabelStyle(boolean hasContent) { + if (hasContent) { + statusLabel.getStyleClass().removeAll("setpassword-status-label"); + if (!statusLabel.getStyleClass().contains("setpassword-status-label-with-text")) { + statusLabel.getStyleClass().add("setpassword-status-label-with-text"); + } + } else { + statusLabel.getStyleClass().removeAll("setpassword-status-label-with-text"); + if (!statusLabel.getStyleClass().contains("setpassword-status-label")) { + statusLabel.getStyleClass().add("setpassword-status-label"); + } } } /** - * 加载主菜单界面,并传递用户信息 (实现自动登录) + * 加载主菜单界面,并传递完整的用户信息,以实现自动登录。 + * + * @param user 包含新设置密码的完整 User 对象。 */ private void loadMainMenu(User user) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/mathgenerator/view/MainMenuView.fxml")); Parent root = loader.load(); MainMenuController controller = loader.getController(); - controller.initData(user); // 将完整的User对象传递给主菜单 + controller.initData(user); Stage stage = (Stage) confirmButton.getScene().getWindow(); stage.setScene(new Scene(root)); diff --git a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java index add01a1..6d51dc5 100644 --- a/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/JuniorHighSchoolGenerator.java @@ -6,58 +6,56 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; /** - * 初中选择题生成器 (最终版 - 采用结构化插入)。 - * 通过直接在表达式结构中插入运算项,确保语法正确性和高性能。 + * 初中难度选择题的生成器。 + *

+ * 该类继承自 {@link PrimarySchoolGenerator},并在其基础上增加了平方和开平方根运算。 + * 它通过在基础表达式中结构化地插入这些新运算项,来确保生成的题目语法正确且性能高。 */ public class JuniorHighSchoolGenerator extends PrimarySchoolGenerator { private static final int[] PERFECT_SQUARES = {1, 4, 9, 16, 25, 36, 49, 64, 81, 100}; + /** + * 生成一道初中难度的数学选择题。 + *

+ * 此方法首先生成一个基础的四则运算表达式,然后随机地将其中一个操作数替换为 + * 平方或开方运算,最后计算结果并生成选项。如果过程中发生任何计算错误, + * 它会安全地回退到生成一道小学难度的题目。 + * + * @return 一个封装了初中难度题目的 {@link ChoiceQuestion} 对象。 + */ @Override public ChoiceQuestion generateSingleQuestion() { ThreadLocalRandom random = ThreadLocalRandom.current(); - int operandCount = random.nextInt(2, 5); // 2到4个操作数 + int operandCount = random.nextInt(2, 5); - // 1. 生成基础的表达式组件列表 List parts = new ArrayList<>(); - // 使用 getOperand() 和 getRandomOperator() 这些继承自父类的方法 parts.add(String.valueOf(getOperand())); for (int i = 1; i < operandCount; i++) { parts.add(getRandomOperator()); parts.add(String.valueOf(getOperand())); } - - // 2. 结构化地插入初中特色运算 - int modificationIndex = random.nextInt(operandCount) * 2; // 随机选择一个操作数的位置 + int modificationIndex = random.nextInt(operandCount) * 2; boolean useSquare = random.nextBoolean(); - if (useSquare) { - // 平方策略:直接在数字后附加平方符号 parts.set(modificationIndex, parts.get(modificationIndex) + "²"); } else { - // 开根号策略:用一个完美的开根号表达式替换整个数字 int perfectSquare = PERFECT_SQUARES[random.nextInt(PERFECT_SQUARES.length)]; parts.set(modificationIndex, "√" + perfectSquare); } - // 3. (可选)为增强后的表达式添加括号 if (operandCount > 2 && random.nextBoolean()) { - super.addParentheses(parts); // 调用父类的protected方法 + super.addParentheses(parts); } String finalQuestionText = String.join(" ", parts); - - // 4. 计算答案 double finalCorrectAnswer; try { Object result = evaluateExpression(finalQuestionText); finalCorrectAnswer = ((Number) result).doubleValue(); } catch (Exception e) { - // 发生意外,安全返回一个小学题 return super.generateSingleQuestion(); } - - // 5. 生成选项 List options = generateDecimalOptions(finalCorrectAnswer); int correctIndex = options.indexOf(formatNumber(finalCorrectAnswer)); diff --git a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java index 530e6dc..e896739 100644 --- a/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/PrimarySchoolGenerator.java @@ -1,6 +1,7 @@ package com.mathgenerator.generator; -import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.model.ChoiceQuestion; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -10,11 +11,14 @@ import java.util.concurrent.ThreadLocalRandom; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; -import java.text.DecimalFormat; /** - * 小学选择题生成器。 - * 生成包含 + - * / 和 () 的运算,并提供四个选项。 + * 小学难度选择题的生成器。 + *

+ * 该类负责生成包含加、减、乘、除和括号的四则运算题目。 + * 生成的题目会确保其计算结果为整数,并提供四个选项。 + * + * @see QuestionGenerator */ public class PrimarySchoolGenerator implements QuestionGenerator { @@ -22,7 +26,11 @@ public class PrimarySchoolGenerator implements QuestionGenerator { /** * 生成一道小学难度的数学选择题。 - * @return 一个包含题干、四个选项和正确答案索引的 ChoiceQuestion 对象。 + *

+ * 该方法会循环生成一个包含2到4个操作数的随机表达式,直到表达式的计算结果为整数。 + * 然后,它会围绕正确答案生成三个干扰项,并将它们随机排序后,封装成一个 {@link ChoiceQuestion} 对象。 + * + * @return 一个包含题干、四个选项和正确答案索引的 {@code ChoiceQuestion} 对象。 */ @Override public ChoiceQuestion generateSingleQuestion() { @@ -65,9 +73,13 @@ public class PrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成四个选项 (1个正确,3个干扰项) - * @param correctAnswer 正确答案 - * @return 包含四个选项的随机排序列表 + * 为给定的正确答案生成四个选项(一个正确,三个干扰项)。 + *

+ * 干扰项是通过在正确答案上加或减一个1到10之间的随机数生成的。 + * 所有选项(包括正确答案)会被放入一个列表中并随机打乱顺序。 + * + * @param correctAnswer 正确的整数答案。 + * @return 一个包含四个选项字符串的随机排序列表。 */ protected List generateOptions(int correctAnswer) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -88,28 +100,28 @@ public class PrimarySchoolGenerator implements QuestionGenerator { } /** - * 使用JVM的脚本引擎计算字符串表达式的值 (已优化兼容性)。 - * @param expression 数学表达式字符串 - * @return 计算结果 (可能是Integer或Double) - * @throws ScriptException 如果表达式有语法错误 + * 使用JVM的脚本引擎计算字符串表达式的值。 + *

+ * 此方法已优化以兼容 Rhino 引擎,并能处理平方、开方和三角函数等运算。 + * + * @param expression 数学表达式字符串,例如 " (3 + 5) * 2 "。 + * @return 计算结果,通常是 {@code Integer} 或 {@code Double} 类型。 + * @throws ScriptException 如果表达式包含语法错误。 + * @throws IllegalStateException 如果找不到 Rhino JavaScript 引擎。 */ protected Object evaluateExpression(String expression) throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); - // --- 核心修改在这里 --- - // 使用 "rhino" 作为引擎名称,这是Rhino引擎的官方名称 ScriptEngine engine = manager.getEngineByName("rhino"); if (engine == null) { - // 增加一个健壮性检查,如果引擎还是没找到,就给出清晰的错误提示 throw new IllegalStateException("错误:找不到Rhino JavaScript引擎。请检查pom.xml中是否已添加rhino-engine的依赖。"); } - // Rhino不需要预定义函数,可以直接计算 + // 预处理表达式以兼容Rhino引擎的数学函数 String script = expression.replaceAll("(\\d+(\\.\\d+)?)²", "Math.pow($1, 2)") .replaceAll("√(\\d+(\\.\\d+)?)", "Math.sqrt($1)") - .replaceAll("(\\d+)°", " * (Math.PI / 180)"); // Rhino对角度计算的语法要求更严格 + .replaceAll("(\\d+)°", " * (Math.PI / 180)"); - // 为了让sin/cos/tan能正确计算,需要特殊处理 script = script.replaceAll("sin\\(", "Math.sin(") .replaceAll("cos\\(", "Math.cos(") .replaceAll("tan\\(", "Math.tan("); @@ -118,24 +130,27 @@ public class PrimarySchoolGenerator implements QuestionGenerator { } /** - * 格式化数字,最多保留两位小数。 - * @param number 待格式化的数字 - * @return 格式化后的字符串 + * 将一个 double 类型的数字格式化为字符串,最多保留两位小数。 + *

+ * 如果数字是整数,则不显示小数位。 + * + * @param number 待格式化的数字。 + * @return 格式化后的字符串。 */ protected String formatNumber(double number) { if (number == (long) number) { - return String.format("%d", (long) number); // 如果是整数,不显示小数位 + return String.format("%d", (long) number); } else { - // 使用DecimalFormat来去除末尾多余的0 DecimalFormat df = new DecimalFormat("#.##"); return df.format(number); } } /** - * 为小数答案生成四个选项。 - * @param correctAnswer 正确答案 - * @return 包含四个选项的随机排序列表 + * 为给定的小数正确答案生成四个选项。 + * + * @param correctAnswer 正确的小数答案。 + * @return 一个包含四个格式化后选项字符串的随机排序列表。 */ protected List generateDecimalOptions(double correctAnswer) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -143,8 +158,7 @@ public class PrimarySchoolGenerator implements QuestionGenerator { options.add(formatNumber(correctAnswer)); while (options.size() < 4) { - double delta = random.nextDouble(1, 11); // 答案加减1-10之间的随机小数 - // 随机决定是加还是减 + double delta = random.nextDouble(1, 11); double distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; options.add(formatNumber(distractor)); } @@ -154,14 +168,29 @@ public class PrimarySchoolGenerator implements QuestionGenerator { return sortedOptions; } + /** + * 获取一个1到100之间的随机整数作为操作数。 + * + * @return 一个随机整数。 + */ protected int getOperand() { return ThreadLocalRandom.current().nextInt(1, 101); } + /** + * 从 {@code OPERATORS} 数组中随机选择一个运算符。 + * + * @return 一个随机的运算符字符串("+"、"-"、"*" 或 "/")。 + */ protected String getRandomOperator() { return OPERATORS[ThreadLocalRandom.current().nextInt(OPERATORS.length)]; } + /** + * 为给定的表达式组件列表随机添加一对括号。 + * + * @param parts 包含数字和运算符的字符串列表,将被原地修改。 + */ protected void addParentheses(List parts) { ThreadLocalRandom random = ThreadLocalRandom.current(); int startOperandIndex = random.nextInt(parts.size() / 2); @@ -171,7 +200,4 @@ public class PrimarySchoolGenerator implements QuestionGenerator { parts.add(endIndex + 1, ")"); parts.add(startIndex, "("); } - - - } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/QuestionGenerator.java b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java index e478038..c6e930b 100644 --- a/src/main/java/com/mathgenerator/generator/QuestionGenerator.java +++ b/src/main/java/com/mathgenerator/generator/QuestionGenerator.java @@ -1,14 +1,22 @@ package com.mathgenerator.generator; -import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.model.ChoiceQuestion; /** - * 题目生成器接口,定义了所有具体生成器必须实现的方法。 + * 题目生成器接口。 + *

+ * 该接口定义了所有具体题目生成器(如小学、初中、高中)必须实现的核心方法。 + * 任何实现此接口的类都应具备生成单个数学选择题的能力。 */ public interface QuestionGenerator { + /** * 生成一道符合特定难度的数学选择题。 - * @return 代表数学选择题的 ChoiceQuestion 对象 + *

+ * 此方法的实现应确保生成的题目包含题干、四个选项以及正确答案的索引, + * 并将这些信息封装在一个 {@link ChoiceQuestion} 对象中返回。 + * + * @return 一个代表新生成的数学选择题的 {@code ChoiceQuestion} 对象。 */ ChoiceQuestion generateSingleQuestion(); } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java index d38cee6..80f4b4d 100644 --- a/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/SafePrimarySchoolGenerator.java @@ -7,18 +7,24 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; /** - * “安全”的小学题目生成器,确保运算过程中不产生负数,并生成选择题。 + * “安全”的小学题目生成器。 + *

+ * 这个生成器专门用于创建小学难度的数学题,并确保在整个运算过程中不会产生任何负数结果。 + * 它通过一种递归下降的算法来构造表达式,从而在生成阶段就避免了负数的出现。 + * + * @see QuestionGenerator */ public class SafePrimarySchoolGenerator implements QuestionGenerator { /** - * 生成一道确保结果非负的小学难度的数学选择题。 - * @return 一个符合所有约束的 ChoiceQuestion 对象。 + * 生成一道确保结果非负的小学难度数学选择题。 + *

+ * 该方法首先确定一个操作数的总预算,然后调用递归方法 {@code generateSafeExpression} + * 来构造一个保证结果为正数的表达式,并最终封装成 {@link ChoiceQuestion} 对象。 + * + * @return 一个符合所有非负约束的 {@code ChoiceQuestion} 对象。 */ @Override public ChoiceQuestion generateSingleQuestion() { @@ -34,12 +40,21 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 内部记录类,用于在递归生成表达式时传递部分结果。 + * 内部记录 (Record),用于在递归生成表达式时传递部分结果。 + * + * @param parts 表达式的字符串组件列表。 + * @param value 该表达式的计算结果。 + * @param operandsUsed 生成此表达式已使用的操作数数量。 */ private record Term(List parts, int value, int operandsUsed) {} /** - * 根据操作数预算,生成一个确保结果非负的(子)表达式。 + * 根据给定的操作数预算,递归地生成一个确保结果非负的(子)表达式。 + *

+ * 算法的核心在于,当执行减法时,会检查确保被减数大于减数。 + * + * @param operandBudget 当前可用于生成表达式的操作数数量。 + * @return 一个包含表达式、其值和所用操作数数量的 {@code Term} 对象。 */ private Term generateSafeExpression(int operandBudget) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -72,7 +87,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成一个“项”(Term)。一个项可以是简单的乘除法序列,也可以是带括号的子表达式。 + * 生成一个“项”(Term)。 + *

+ * 一个项可以是一个简单的乘除法序列,或者是一个带括号的、更复杂的子表达式。 + * + * @param operandsRemaining 剩余可用的操作数数量。 + * @return 代表所生成项的 {@code Term} 对象。 */ private Term generateTerm(int operandsRemaining) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -91,7 +111,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成一个仅包含乘除法的简单项。 + * 生成一个仅包含乘法或除法的简单项。 + *

+ * 为了确保除法的结果是整数,除数会从被除数的所有因数中选取。 + * + * @param operandsRemaining 剩余可用的操作数数量。 + * @return 代表简单项的 {@code Term} 对象。 */ private Term generateSimpleTerm(int operandsRemaining) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -118,6 +143,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { return new Term(parts, termValue, operandsUsed); } + /** + * 获取一个正整数的所有因数。 + * + * @param number 要查找因数的数字。 + * @return 包含该数字所有因数的列表。 + */ private List getDivisors(int number) { List divisors = new ArrayList<>(); for (int i = 1; i <= number; i++) { @@ -127,7 +158,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { } /** - * 生成四个选项 (1个正确,3个干扰项) + * 为给定的正确答案生成四个选项(一个正确,三个干扰项)。 + *

+ * 此方法确保所有生成的干扰项也都是非负数。 + * + * @param correctAnswer 正确的整数答案。 + * @return 一个包含四个非负选项的随机排序列表。 */ private List generateOptions(int correctAnswer) { ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -137,8 +173,7 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator { while (options.size() < 4) { int delta = random.nextInt(1, 11); int distractor = random.nextBoolean() ? correctAnswer + delta : correctAnswer - delta; - // 确保干扰项非负 - if (distractor >= 0) { + if (distractor >= 0) { // 确保干扰项非负 options.add(String.valueOf(distractor)); } } diff --git a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java index 4dc6be4..c038127 100644 --- a/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java +++ b/src/main/java/com/mathgenerator/generator/SeniorHighSchoolGenerator.java @@ -6,8 +6,10 @@ import java.util.Map; import java.util.concurrent.ThreadLocalRandom; /** - * 高中选择题生成器 (最终版 - 采用构造式添加)。 - * 通过在已生成的初中题基础上添加预设的三角函数项来构造题目。 + * 高中难度选择题的生成器。 + *

+ * 该类继承自 {@link JuniorHighSchoolGenerator},通过在已生成的初中难度题目基础上, + * 构造性地添加一个预设的、计算结果简单的三角函数项来生成题目。 */ public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator { @@ -24,6 +26,14 @@ public class SeniorHighSchoolGenerator extends JuniorHighSchoolGenerator { // 将Map的键转换为数组,方便随机选取 private static final String[] TRIG_KEYS = TRIG_TERMS.keySet().toArray(new String[0]); + /** + * 生成一道高中难度的数学选择题。 + *

+ * 此方法首先调用父类方法生成一个初中难度的题目作为基础。然后,它随机选择一个 + * 预设的三角函数项,并将其与初中题目通过加法或减法结合,形成最终的高中题目。 + * + * @return 一个封装了高中难度题目的 {@link ChoiceQuestion} 对象。 + */ @Override public ChoiceQuestion generateSingleQuestion() { // 1. 先生成一个保证可计算的、高性能的初中选择题,作为基础 diff --git a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java index 485b2ec..7d18220 100644 --- a/src/main/java/com/mathgenerator/model/ChoiceQuestion.java +++ b/src/main/java/com/mathgenerator/model/ChoiceQuestion.java @@ -3,10 +3,14 @@ package com.mathgenerator.model; import java.util.List; /** - * 选择题数据模型。 - * @param questionText 题干 - * @param options 四个选项的列表 - * @param correctOptionIndex 正确答案在列表中的索引 (0-3) + * 选择题数据模型 (Record)。 + *

+ * 该记录 (Record) 用于以不可变的方式封装一道选择题的所有核心信息, + * 包括题干、选项列表以及正确答案的索引。 + * + * @param questionText 题目的文本内容,即题干。 + * @param options 一个包含四个选项字符串的列表 (List)。 + * @param correctOptionIndex 正确答案在 {@code options} 列表中的索引,范围从 0 到 3。 */ public record ChoiceQuestion(String questionText, List options, int correctOptionIndex) { } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/model/Level.java b/src/main/java/com/mathgenerator/model/Level.java index 8b27203..7f47178 100644 --- a/src/main/java/com/mathgenerator/model/Level.java +++ b/src/main/java/com/mathgenerator/model/Level.java @@ -1,14 +1,29 @@ package com.mathgenerator.model; /** - * 学段枚举,用于类型安全地表示小学、初中和高中。 + * 学段枚举 (Enum)。 + *

+ * 该枚举用于以类型安全的方式表示程序支持的不同题目难度级别, + * 包括小学、初中和高中。每个枚举常量都关联一个中文名称。 */ public enum Level { + /** + * 代表小学难度。 + */ PRIMARY("小学"), + + /** + * 代表初中难度。 + */ JUNIOR_HIGH("初中"), + + /** + * 代表高中难度。 + */ SENIOR_HIGH("高中"); private final String chineseName; + /** * 枚举的构造函数。 * @@ -17,10 +32,11 @@ public enum Level { Level(String chineseName) { this.chineseName = chineseName; } + /** - * 获取学段的中文名称。 + * 获取该学段对应的中文名称。 * - * @return 表示学段的中文名称字符串。 + * @return 表示学段的中文名称字符串 (例如, "小学")。 */ public String getChineseName() { return chineseName; diff --git a/src/main/java/com/mathgenerator/model/User.java b/src/main/java/com/mathgenerator/model/User.java index 66ecbd3..eccf84e 100644 --- a/src/main/java/com/mathgenerator/model/User.java +++ b/src/main/java/com/mathgenerator/model/User.java @@ -1,10 +1,14 @@ package com.mathgenerator.model; /** - * 用户数据记录 (Record),用于封装不可变的用户信息。 - * @param username 用户名 - * @param email 邮箱地址 (新增) - * @param password 密码 + * 用户数据记录 (Record)。 + *

+ * 该记录 (Record) 用于以不可变的方式封装一个用户的基本信息, + * 包括用户名、邮箱地址和密码。 + * + * @param username 用户的唯一标识符,即用户名。 + * @param email 用户的电子邮箱地址,用于接收验证码等。 + * @param password 用户账户的密码。在用户首次注册但还未设置密码时,此字段可能为 {@code null}。 */ public record User(String username, String email, String password) { } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/service/EmailConfig.java b/src/main/java/com/mathgenerator/service/EmailConfig.java index 44618f8..a28c5ce 100644 --- a/src/main/java/com/mathgenerator/service/EmailConfig.java +++ b/src/main/java/com/mathgenerator/service/EmailConfig.java @@ -5,6 +5,13 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; +/** + * 邮件配置加载类。 + *

+ * 该类负责从项目根目录下的 {@code config.properties} 文件中读取和提供 + * 发送邮件所需的 SMTP 服务器信息,如主机、端口、用户名和密码。 + * 所有配置项通过静态方法提供。 + */ public class EmailConfig { private static final Properties properties = new Properties(); @@ -17,18 +24,38 @@ public class EmailConfig { } } + /** + * 获取 SMTP 服务器的主机名。 + * + * @return SMTP 主机名字符串。 + */ public static String getHost() { return properties.getProperty("smtp.host"); } + /** + * 获取 SMTP 服务器的端口号。 + * + * @return SMTP 端口号。 + */ public static int getPort() { return Integer.parseInt(properties.getProperty("smtp.port")); } + /** + * 获取用于 SMTP 认证的用户名(通常是发件人邮箱地址)。 + * + * @return SMTP 用户名。 + */ public static String getUsername() { return properties.getProperty("smtp.username"); } + /** + * 获取用于 SMTP 认证的密码或授权码。 + * + * @return SMTP 密码或授权码。 + */ public static String getPassword() { return properties.getProperty("smtp.password"); } diff --git a/src/main/java/com/mathgenerator/service/PaperService.java b/src/main/java/com/mathgenerator/service/PaperService.java index 3df7045..a44437f 100644 --- a/src/main/java/com/mathgenerator/service/PaperService.java +++ b/src/main/java/com/mathgenerator/service/PaperService.java @@ -2,7 +2,7 @@ package com.mathgenerator.service; import com.mathgenerator.model.User; import com.mathgenerator.model.Level; -import com.mathgenerator.model.ChoiceQuestion; // 导入新模型 +import com.mathgenerator.model.ChoiceQuestion; import com.mathgenerator.service.strategy.PaperStrategy; import com.mathgenerator.storage.FileManager; import java.io.IOException; @@ -12,37 +12,48 @@ import java.util.List; import java.util.Set; /** - * 试卷服务,现在处理ChoiceQuestion对象。 + * 试卷服务类。 + *

+ * 该服务负责处理与试卷相关的核心业务逻辑,包括根据指定的策略生成试卷 + * (并处理题目查重),以及将生成的试卷保存到文件系统中。 */ public class PaperService { private final FileManager fileManager; private final PaperStrategy paperStrategy; + /** + * 构造一个新的 PaperService 实例。 + * + * @param fileManager 用于处理文件读写操作的 {@link FileManager} 实例。 + * @param paperStrategy 用于选择具体题目生成器策略的 {@link PaperStrategy} 实例。 + */ public PaperService(FileManager fileManager, PaperStrategy paperStrategy) { this.fileManager = fileManager; this.paperStrategy = paperStrategy; } /** - * 创建一份包含选择题的试卷。 - * @param user 当前用户 - * @param count 题目数量 - * @param currentLevel 当前难度 - * @return 生成的选择题列表 + * 创建一份包含指定数量选择题的试卷。 + *

+ * 该方法会首先加载用户历史题目以进行查重,然后根据传入的难度级别和策略 + * 循环生成新题目,直到满足指定的数量要求。所有在本轮生成过程中的题目也会 + * 进行内部查重。 + * + * @param user 当前请求生成试卷的用户对象。 + * @param count 需要生成的题目数量。 + * @param currentLevel 用户选择的主要难度级别。 + * @return 一个包含新生成的 {@link ChoiceQuestion} 对象的列表。 */ public List createPaper(User user, int count, Level currentLevel) { - // 查重集合现在存储题干字符串 Set existingQuestionTexts = fileManager.loadExistingQuestions(user.username()); List newPaper = new ArrayList<>(); Set generatedInSession = new HashSet<>(); System.out.println("正在根据策略生成选择题,请稍候..."); while (newPaper.size() < count) { - // 1. 生成的是ChoiceQuestion对象 ChoiceQuestion question = paperStrategy.selectGenerator(currentLevel).generateSingleQuestion(); - String questionText = question.questionText(); // 提取题干用于查重 + String questionText = question.questionText(); - // 2. 使用题干进行查重 if (!existingQuestionTexts.contains(questionText) && !generatedInSession.contains(questionText)) { newPaper.add(question); generatedInSession.add(questionText); @@ -52,9 +63,10 @@ public class PaperService { } /** - * 将生成的试卷保存到文件。 - * @param username 用户名 - * @param paper 试卷题目列表 + * 将一份生成的试卷保存到用户专属的文件夹中。 + * + * @param username 用户的用户名,用于确定文件夹路径。 + * @param paper 包含试卷所有 {@link ChoiceQuestion} 的列表。 */ public void savePaper(String username, List paper) { try { diff --git a/src/main/java/com/mathgenerator/service/UserService.java b/src/main/java/com/mathgenerator/service/UserService.java index 2c816a7..1a2eb64 100644 --- a/src/main/java/com/mathgenerator/service/UserService.java +++ b/src/main/java/com/mathgenerator/service/UserService.java @@ -1,10 +1,13 @@ package com.mathgenerator.service; import com.google.gson.Gson; -import java.util.Objects; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.mathgenerator.model.User; +import org.apache.commons.mail.Email; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.SimpleEmail; + import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -17,25 +20,35 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; -import org.apache.commons.mail.Email; -import org.apache.commons.mail.EmailException; -import org.apache.commons.mail.SimpleEmail; - +/** + * 用户服务类。 + *

+ * 负责处理所有与用户账户相关的业务逻辑,包括用户的注册、登录、密码修改、 + * 信息查询以及发送验证码等功能。该类通过读写 JSON 文件来持久化用户数据。 + */ public class UserService { private static final Path USER_FILE_PATH = Paths.get("users.json"); - // 密码策略: 6-10位, 必须包含大小写字母和数字 private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{6,10}$"); private Map userDatabase; private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + /** + * 构造一个新的 UserService 实例。 + * 在构造时会自动从 {@code users.json} 文件加载用户数据。 + */ public UserService() { this.userDatabase = loadUsersFromFile(); } + /** + * 从 {@code users.json} 文件加载用户数据到内存中的 Map。 + * 如果文件不存在或为空,则返回一个空的 Map。 + * + * @return 一个包含所有用户数据的 {@code ConcurrentHashMap}。 + */ private Map loadUsersFromFile() { - // 如果文件不存在,直接返回一个空的Map,不再创建默认用户 if (!Files.exists(USER_FILE_PATH)) { return new ConcurrentHashMap<>(); } @@ -43,7 +56,6 @@ public class UserService { try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) { Type type = new TypeToken>() {}.getType(); Map loadedUsers = gson.fromJson(reader, type); - // 如果文件为空或格式错误,也返回一个空的Map return loadedUsers != null ? new ConcurrentHashMap<>(loadedUsers) : new ConcurrentHashMap<>(); } catch (IOException e) { System.err.println("错误:加载用户文件失败 - " + e.getMessage()); @@ -51,6 +63,9 @@ public class UserService { } } + /** + * 将内存中的用户数据保存到 {@code users.json} 文件中。 + */ private void saveUsers() { try (FileWriter writer = new FileWriter(USER_FILE_PATH.toFile())) { gson.toJson(this.userDatabase, writer); @@ -59,70 +74,72 @@ public class UserService { } } + /** + * 根据用户名查找用户。 + * + * @param username 要查找的用户名。 + * @return 一个包含 {@link User} 对象的 {@code Optional},如果找不到则为空。 + */ public Optional findUserByUsername(String username) { return Optional.ofNullable(this.userDatabase.get(username)); } + /** + * 验证用户名和密码,执行登录操作。 + * + * @param username 用户的用户名。 + * @param password 用户的密码。 + * @return 如果登录成功,返回一个包含 {@link User} 对象的 {@code Optional};否则返回空的 {@code Optional}。 + */ public Optional login(String username, String password) { return findUserByUsername(username) .filter(user -> user.password().equals(password)); } /** - * (已更新) 发送真实的邮件验证码。 - * @param email 用户的邮箱 - * @return 成功发送则返回生成的6位验证码, 失败则返回null + * 向指定的邮箱地址发送一个6位数的随机验证码。 + * + * @param email 接收验证码的目标邮箱地址。 + * @return 如果邮件发送成功,则返回生成的6位验证码字符串;如果失败,则返回 {@code null}。 */ public String sendVerificationCode(String email) { String code = String.format("%06d", ThreadLocalRandom.current().nextInt(100000, 1000000)); try { Email mail = new SimpleEmail(); - - // 1. 设置SMTP服务器信息 mail.setHostName(EmailConfig.getHost()); mail.setSmtpPort(EmailConfig.getPort()); mail.setAuthentication(EmailConfig.getUsername(), EmailConfig.getPassword()); - mail.setSSLOnConnect(true); // 开启SSL加密 - - // 2. 设置邮件内容 - mail.setFrom(EmailConfig.getUsername()); // 发件人 - mail.setSubject("【数学学习软件】您的注册验证码"); // 邮件主题 - mail.setMsg("您好!\n\n感谢您注册数学学习软件。您的验证码是:" + code + "\n\n请在5分钟内使用。"); // 邮件正文 - mail.addTo(email); // 收件人 - - // 3. 发送邮件 + mail.setSSLOnConnect(true); + mail.setFrom(EmailConfig.getUsername()); + mail.setSubject("【数学学习软件】您的注册验证码"); + mail.setMsg("您好!\n\n感谢您注册数学学习软件。您的验证码是:" + code + "\n\n请在5分钟内使用。"); + mail.addTo(email); mail.send(); - System.out.println("验证码邮件已成功发送至: " + email); return code; - } catch (EmailException e) { System.err.println("错误:发送验证码邮件失败!请检查您的 config.properties 配置或网络连接。"); e.printStackTrace(); - return null; // 发送失败 + return null; } } /** - * (已修正) 注册一个没有初始密码的新用户。 - * @param username 新用户的用户名 - * @param email 新用户的邮箱 - * @return 注册成功返回true, 如果用户名或邮箱已存在则返回false。 + * 注册一个新用户,该用户初始时没有密码。 + * + * @param username 新用户的用户名。 + * @param email 新用户的邮箱地址。 + * @return 如果注册成功,返回 {@code true};如果用户名或邮箱已存在,则返回 {@code false}。 */ public boolean register(String username, String email) { if (userDatabase.containsKey(username)) { return false; // 用户名已存在 } - // 检查数据库中已存在的用户的email是否与新email相同 - // 使用 email.equals(u.email()) 可以安全地处理 u.email() 为 null 的情况 - if (userDatabase.values().stream() - .anyMatch(u -> email.equals(u.email()))) { + if (userDatabase.values().stream().anyMatch(u -> email.equals(u.email()))) { return false; // 邮箱已存在 } - // --- 核心修正在这里 --- - // 创建用户时,密码字段设为 null,表示该用户处于“待设置密码”状态 User newUser = new User(username, email, null); userDatabase.put(username, newUser); saveUsers(); @@ -130,36 +147,43 @@ public class UserService { } /** - * (新增) 为指定用户设置初始密码。 - * @param username 要设置密码的用户名 - * @param password 要设置的新密码 - * @return 成功设置返回 true, 如果用户不存在则返回 false + * 为指定用户设置其初始密码。 + * 此方法只在用户当前密码为 {@code null} 时才允许操作。 + * + * @param username 要设置密码的用户名。 + * @param password 要设置的新密码。 + * @return 如果密码设置成功,返回 {@code true};如果用户不存在或已有密码,则返回 {@code false}。 */ public boolean setPassword(String username, String password) { return findUserByUsername(username) .map(user -> { - // 只有当用户当前密码为 null 时才允许设置 if (user.password() == null) { User updatedUser = new User(user.username(), user.email(), password); userDatabase.put(username, updatedUser); saveUsers(); return true; } - return false; // 用户已经有密码,不能通过此方法设置 + return false; }).orElse(false); } + /** - * 验证密码是否符合复杂度要求。 - * @param password 待验证的密码 - * @return true如果符合要求 + * 验证给定的密码字符串是否符合预设的复杂度要求。 + * + * @param password 待验证的密码。 + * @return 如果密码有效,返回 {@code true};否则返回 {@code false}。 */ public static boolean isPasswordValid(String password) { return password != null && PASSWORD_PATTERN.matcher(password).matches(); } /** - * 修改密码。 - * @return 成功返回true + * 修改指定用户的密码。 + * + * @param username 要修改密码的用户名。 + * @param oldPassword 用户的当前密码,用于验证。 + * @param newPassword 用户的新密码。 + * @return 如果旧密码正确且新密码设置成功,返回 {@code true};否则返回 {@code false}。 */ public boolean changePassword(String username, String oldPassword, String newPassword) { return findUserByUsername(username) diff --git a/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java b/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java index cc7d78f..8954b05 100644 --- a/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java +++ b/src/main/java/com/mathgenerator/service/strategy/MixedDifficultyStrategy.java @@ -6,29 +6,39 @@ import java.util.concurrent.ThreadLocalRandom; /** * 混合难度策略的具体实现。 - * (已更新,会根据主难度选择不同的小学基础生成器) + *

+ * 该策略根据用户选择的主难度级别,按一定的概率混合使用不同难度的题目生成器, + * 以生成一张难度更多样化的试卷。 + * 例如,高中难度的试卷也可能包含一部分初中或小学的题目。 */ public class MixedDifficultyStrategy implements PaperStrategy { - // 持有所有可能的生成器 + // 持有所有可能的生成器实例 private final QuestionGenerator primaryGenerator = new PrimarySchoolGenerator(); private final QuestionGenerator safePrimaryGenerator = new SafePrimarySchoolGenerator(); // 新增 private final QuestionGenerator juniorHighGenerator = new JuniorHighSchoolGenerator(); private final QuestionGenerator seniorHighGenerator = new SeniorHighSchoolGenerator(); + /** + * 根据主难度级别选择一个题目生成器。 + *

+ * - **小学**: 100% 使用 {@link SafePrimarySchoolGenerator},确保题目不含负数。 + * - **初中**: 70% 概率使用 {@link JuniorHighSchoolGenerator},30% 使用 {@link PrimarySchoolGenerator}。 + * - **高中**: 60% 概率使用 {@link SeniorHighSchoolGenerator},30% 使用 {@link JuniorHighSchoolGenerator},10% 使用 {@link PrimarySchoolGenerator}。 + * + * @param mainLevel 用户选择的主难度级别。 + * @return 根据预设概率随机选择出的一个 {@link QuestionGenerator} 实例。 + */ @Override public QuestionGenerator selectGenerator(Level mainLevel) { double randomValue = ThreadLocalRandom.current().nextDouble(); return switch (mainLevel) { - // 当主难度是小学时,100%使用“安全”的生成器 case PRIMARY -> safePrimaryGenerator; case JUNIOR_HIGH -> { - // 初中试卷:70%初中难度,30%使用“不安全”的小学难度(允许负数) if (randomValue < 0.7) yield juniorHighGenerator; else yield primaryGenerator; } case SENIOR_HIGH -> { - // 高中试卷:60%高中,30%初中,10%使用“不安全”的小学难度 if (randomValue < 0.6) yield seniorHighGenerator; else if (randomValue < 0.9) yield juniorHighGenerator; else yield primaryGenerator; diff --git a/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java b/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java index 80d2ac4..5cc6b13 100644 --- a/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java +++ b/src/main/java/com/mathgenerator/service/strategy/PaperStrategy.java @@ -5,14 +5,17 @@ import com.mathgenerator.model.Level; /** * 试卷组合策略接口。 - * 封装了如何根据主难度来选择具体题目生成器的算法。 + *

+ * 该接口遵循策略模式,用于封装根据主难度级别选择不同题目生成器的算法。 + * 任何实现此接口的类都需要提供一个具体的生成器选择逻辑。 */ public interface PaperStrategy { + /** - * 根据用户选择的主难度,选择一个具体的题目生成器。 + * 根据用户选择的主要难度级别,选择一个具体的题目生成器。 * - * @param mainLevel 用户选择的主难度级别。 - * @return 一个根据策略选择出的QuestionGenerator实例。 + * @param mainLevel 用户选择的主难度级别 ({@link Level})。 + * @return 一个根据策略选择出的 {@link QuestionGenerator} 实例。 */ QuestionGenerator selectGenerator(Level mainLevel); } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/storage/FileManager.java b/src/main/java/com/mathgenerator/storage/FileManager.java index af3c47c..93f62ce 100644 --- a/src/main/java/com/mathgenerator/storage/FileManager.java +++ b/src/main/java/com/mathgenerator/storage/FileManager.java @@ -18,12 +18,30 @@ import java.util.stream.Collectors; import java.util.stream.Stream; /** - * 负责文件读写,现已修复Stream关闭异常。 + * 文件管理器类。 + *

+ * 该类负责处理所有与文件系统相关的读写操作。主要功能包括: + *

+ * 此版本已修复了先前版本中因未正确关闭文件流(Stream)而可能导致的资源泄漏问题。 */ public class FileManager { private static final Path BASE_PATH = Paths.get("generated_papers"); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + /** + * 将一份试卷的内容保存到指定用户的文件夹下。 + *

+ * 文件会以当前的时间戳命名,并保存为 {@code .txt} 格式。文件内容会进行格式化, + * 包含用户信息、生成时间、题目、选项和正确答案。 + * + * @param username 用户的用户名,用于确定保存文件的子目录。 + * @param paperContent 包含所有 {@link ChoiceQuestion} 对象的试卷列表。 + * @return 保存成功后返回文件的绝对路径字符串。 + * @throws IOException 如果在文件创建或写入过程中发生 I/O 错误。 + */ public String savePaper(String username, List paperContent) throws IOException { Path userDir = BASE_PATH.resolve(username); Files.createDirectories(userDir); @@ -55,6 +73,15 @@ public class FileManager { return filePath.toString(); } + /** + * 加载指定用户所有历史试卷中的题目题干。 + *

+ * 该方法会遍历用户文件夹下的所有 {@code .txt} 文件,解析出每道题的题干文本, + * 并将它们聚合到一个 Set 集合中返回,用于后续的题目查重。 + * + * @param username 要加载历史题目的用户名。 + * @return 一个包含该用户所有历史题干的 {@code Set} 集合。如果用户没有历史文件或读取失败,则返回空集合。 + */ public Set loadExistingQuestions(String username) { Path userDir = BASE_PATH.resolve(username); if (!Files.exists(userDir)) { @@ -64,8 +91,6 @@ public class FileManager { try (Stream stream = Files.walk(userDir)) { return stream .filter(file -> !Files.isDirectory(file) && file.toString().endsWith(".txt")) - // --- 核心修改在这里 (Part 1) --- - // flatMap现在操作的是一个由List生成的、全新的、开放的流 .flatMap(file -> readQuestionTextsFromTxtFile(file).stream()) .collect(Collectors.toSet()); } catch (IOException e) { @@ -75,13 +100,14 @@ public class FileManager { } /** - * (已修改) 从单个 .txt 文件中解析出所有题目的题干。 - * @param file 要读取的单个试卷文件的路径对象 (Path)。 - * @return 一个包含该文件中所有题干字符串的列表 (List)。 + * 从单个试卷文本文件中解析出所有题目的题干。 + *

+ * 使用正则表达式匹配以 "数字." 开头的行,并提取其后的文本作为题干。 + * + * @param file 要读取的单个试卷文件的 {@link Path} 对象。 + * @return 一个包含该文件中所有题干字符串的列表 (List)。如果读取失败,则返回空列表。 */ private List readQuestionTextsFromTxtFile(Path file) { - // --- 核心修改在这里 (Part 2) --- - // try-with-resources现在可以安全地关闭流,因为它在方法返回前已经被 .collect() 完全消耗了 try (Stream lines = Files.lines(file)) { Pattern questionPattern = Pattern.compile("^\\d+\\.\\s+(.*)"); return lines @@ -89,10 +115,10 @@ public class FileManager { .map(questionPattern::matcher) .filter(Matcher::matches) .map(matcher -> matcher.group(1).trim()) - .collect(Collectors.toList()); // 将流的结果收集到一个List中 + .collect(Collectors.toList()); } catch (IOException e) { System.err.println("错误:读取文件 " + file + " 失败 - " + e.getMessage()); - return Collections.emptyList(); // 发生错误时返回一个空列表 + return Collections.emptyList(); } } } \ No newline at end of file diff --git a/src/main/java/com/mathgenerator/util/ValidationUtils.java b/src/main/java/com/mathgenerator/util/ValidationUtils.java index 845a2f7..940168f 100644 --- a/src/main/java/com/mathgenerator/util/ValidationUtils.java +++ b/src/main/java/com/mathgenerator/util/ValidationUtils.java @@ -4,7 +4,10 @@ import java.util.regex.Pattern; /** * 一个包含静态校验方法的工具类。 - * 用于集中管理项目中所有的数据格式验证逻辑。 + *

+ * 该类用于集中管理项目中所有的数据格式验证逻辑,例如用户名、密码和邮箱的格式校验。 + * 通过提供统一的静态方法,确保了验证规则的一致性和代码的复用性。 + * 此类不可被实例化。 */ public final class ValidationUtils { @@ -16,14 +19,27 @@ public final class ValidationUtils { private static final Pattern USERNAME_NO_WHITESPACE_PATTERN = Pattern.compile("^\\S+$"); - // 私有构造函数,防止这个工具类被实例化 + // 邮箱策略:使用标准的正则表达式进行格式校验 + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$"); + + + /** + * 私有构造函数,防止这个工具类被实例化。 + */ private ValidationUtils() {} /** * 验证用户名格式是否有效。 - * 当前规则:不允许包含任何空格或空白字符。 - * @param username 待验证的用户名 - * @return 如果有效返回true, 否则返回false + *

+ * 当前规则为: + *

+ * + * @param username 待验证的用户名字符串。 + * @return 如果用户名格式有效,返回 {@code true};否则返回 {@code false}。 */ public static boolean isUsernameValid(String username) { if (username == null || username.isEmpty()) { @@ -33,20 +49,33 @@ public final class ValidationUtils { } /** - * 验证密码是否符合复杂度要求。 - * @param password 待验证的密码 - * @return 如果符合要求返回true + * 验证密码是否符合预设的复杂度要求。 + *

+ * 当前规则为: + *

+ * + * @param password 待验证的密码字符串。 + * @return 如果密码符合复杂度要求,返回 {@code true};否则返回 {@code false}。 */ public static boolean isPasswordValid(String password) { return password != null && PASSWORD_PATTERN.matcher(password).matches(); } /** - * 验证邮箱格式是否有效 (简单校验)。 - * @param email 待验证的邮箱 - * @return 如果格式基本正确返回true + * 验证邮箱地址格式是否有效。 + *

+ * 使用一个标准的正则表达式来检查邮箱地址是否符合常规格式 (例如, "user@example.com")。 + * + * @param email 待验证的邮箱地址字符串。 + * @return 如果邮箱格式基本正确,返回 {@code true};否则返回 {@code false}。 */ public static boolean isEmailValid(String email) { - return email != null && !email.isEmpty() && email.contains("@"); + if (email == null || email.isEmpty()) { + return false; + } + return EMAIL_PATTERN.matcher(email).matches(); } } \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/images/1.png b/src/main/resources/com/mathgenerator/images/1.png new file mode 100644 index 0000000..ac38781 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/1.png differ diff --git a/src/main/resources/com/mathgenerator/images/2.png b/src/main/resources/com/mathgenerator/images/2.png new file mode 100644 index 0000000..495a0dc Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/2.png differ diff --git a/src/main/resources/com/mathgenerator/images/3.png b/src/main/resources/com/mathgenerator/images/3.png new file mode 100644 index 0000000..7948648 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/3.png differ diff --git a/src/main/resources/com/mathgenerator/images/5.png b/src/main/resources/com/mathgenerator/images/5.png new file mode 100644 index 0000000..84f20bb Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/5.png differ diff --git a/src/main/resources/com/mathgenerator/images/6.png b/src/main/resources/com/mathgenerator/images/6.png new file mode 100644 index 0000000..8377cda Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/6.png differ diff --git a/src/main/resources/com/mathgenerator/images/7.png b/src/main/resources/com/mathgenerator/images/7.png new file mode 100644 index 0000000..3db09d5 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/7.png differ diff --git a/src/main/resources/com/mathgenerator/images/background.png b/src/main/resources/com/mathgenerator/images/background.png new file mode 100644 index 0000000..1c99681 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/background.png differ diff --git a/src/main/resources/com/mathgenerator/images/cute-dog-train.png b/src/main/resources/com/mathgenerator/images/cute-dog-train.png new file mode 100644 index 0000000..b6b6c4e Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/cute-dog-train.png differ diff --git a/src/main/resources/com/mathgenerator/images/icon.png b/src/main/resources/com/mathgenerator/images/icon.png new file mode 100644 index 0000000..b081c90 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/icon.png differ diff --git a/src/main/resources/com/mathgenerator/images/image.png b/src/main/resources/com/mathgenerator/images/image.png new file mode 100644 index 0000000..695347a Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/image.png differ diff --git a/src/main/resources/com/mathgenerator/images/mainmenu-bg.png b/src/main/resources/com/mathgenerator/images/mainmenu-bg.png new file mode 100644 index 0000000..4479bc0 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/mainmenu-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/password-bg.png b/src/main/resources/com/mathgenerator/images/password-bg.png new file mode 100644 index 0000000..4479bc0 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/password-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/quiz-bg.png b/src/main/resources/com/mathgenerator/images/quiz-bg.png new file mode 100644 index 0000000..d1fba93 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/quiz-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/register-bg.png b/src/main/resources/com/mathgenerator/images/register-bg.png new file mode 100644 index 0000000..31f6e39 Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/register-bg.png differ diff --git a/src/main/resources/com/mathgenerator/images/score-bg.png b/src/main/resources/com/mathgenerator/images/score-bg.png new file mode 100644 index 0000000..3136b1e Binary files /dev/null and b/src/main/resources/com/mathgenerator/images/score-bg.png differ diff --git a/src/main/resources/com/mathgenerator/styles/styles.css b/src/main/resources/com/mathgenerator/styles/styles.css new file mode 100644 index 0000000..d86f8bf --- /dev/null +++ b/src/main/resources/com/mathgenerator/styles/styles.css @@ -0,0 +1,955 @@ +/* ===== 统一界面尺寸 ===== */ +.root-container { + -fx-pref-width: 400px; + -fx-pref-height: 700px; + -fx-min-width: 400px; + -fx-min-height: 700px; + -fx-max-width: 400px; + -fx-max-height: 700px; +} + +/* ===== 登录界面专用样式 - 淡黄色和淡紫色主题 ===== */ +.login-background { + -fx-background-image: url('../images/background.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.login-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.85); + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.15), 20, 0.3, 0, 6); +} + +.login-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.2), 4, 0.5, 2, 2); +} + +.login-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.login-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.login-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 12px 24px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.login-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.login-secondary-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #fff176); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #fff176; + -fx-border-width: 1px; + -fx-padding: 10px 20px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.4), 6, 0.3, 2, 2); +} + +.login-secondary-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.6), 8, 0.4, 2, 2); +} + +.login-status-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 8px; + -fx-border-width: 1px; +} + +/* ===== 注册界面专用样式 - 修复版本 ===== */ +.register-background-new { + -fx-background-image: url('../images/register-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.register-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.85); + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.15), 20, 0.3, 0, 6); +} + +.register-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.2), 4, 0.5, 2, 2); +} + +.register-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.register-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.register-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 12px 24px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.register-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.register-secondary-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #fff176); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #fff176; + -fx-border-width: 1px; + -fx-padding: 10px 20px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.4), 6, 0.3, 2, 2); +} + +.register-secondary-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 245, 157, 0.6), 8, 0.4, 2, 2); +} + +.register-security-tip { + -fx-text-fill: #7b1fa2; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 8px; + -fx-border-width: 1px; +} + +.register-code-container { + -fx-background-color: rgba(255, 255, 255, 0.9); + -fx-background-radius: 15px; + -fx-border-radius: 15px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 1px; + -fx-padding: 8px 12px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +/* 修复验证码按钮大小 */ +.register-code-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-background-radius: 10px; + -fx-border-radius: 10px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 10px 16px; + -fx-min-width: 100px; + -fx-min-height: 40px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.3), 6, 0.3, 2, 2); +} + +.register-code-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.5), 8, 0.4, 2, 2); +} + +/* ===== 注册界面状态标签样式 - 修复长句子版本 ===== */ +.register-status-label { + -fx-text-fill: transparent; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 8px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 8px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 0px; + -fx-pref-height: 0px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; +} + +.register-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 10px; + -fx-padding: 12px 15px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 70px; + -fx-pref-height: 70px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.3, 2, 2); +} + +/* ===== 主菜单界面专用样式 - 浅紫色为主,浅黄色为辅 ===== */ +.mainmenu-background-new { + -fx-background-image: url('../images/mainmenu-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #f3e5f5, #e1bee7); +} + +.mainmenu-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.92); + -fx-background-radius: 30px; + -fx-border-radius: 30px; + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.25), 25, 0.5, 0, 8); +} + +/* ===== 主菜单欢迎标题样式 - 修复长用户名显示 ===== */ +.mainmenu-welcome-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 24px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.6, 2, 2); + -fx-wrap-text: true; + -fx-text-alignment: center; + -fx-alignment: center; + -fx-max-width: 360px; +} + +.mainmenu-difficulty-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 20px; + -fx-font-weight: bold; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.2), 2, 0.4, 1, 1); +} + +/* ===== 难度按钮样式 - 浅紫色质感 ===== */ +.mainmenu-difficulty-button { + -fx-background-color: linear-gradient(to bottom, #e1bee7, #ba68c8); + -fx-text-fill: white; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ba68c8; + -fx-border-width: 2px; + -fx-padding: 15px 30px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.5), 12, 0.5, 3, 3); +} + +.mainmenu-difficulty-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ab47bc); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.7), 15, 0.6, 4, 4); + -fx-scale-x: 1.08; + -fx-scale-y: 1.08; +} + +.mainmenu-difficulty-button:pressed { + -fx-background-color: linear-gradient(to bottom, #ab47bc, #8e24aa); +} + +/* ===== 数量标签样式 ===== */ +.mainmenu-count-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 16px; + -fx-font-weight: bold; +} + +/* ===== 数量输入框样式 ===== */ +.mainmenu-count-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.6); + -fx-border-radius: 10px; + -fx-background-radius: 10px; + -fx-padding: 8px 12px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.3), 6, 0.2, 2, 2); + -fx-alignment: center; +} + +.mainmenu-count-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.4), 8, 0.3, 2, 2); +} + +/* ===== 功能按钮样式 - 浅黄色辅助 ===== */ +.mainmenu-function-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #ffeb3b); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ffeb3b; + -fx-border-width: 1px; + -fx-padding: 10px 25px; + -fx-min-width: 140px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.4), 8, 0.3, 2, 2); +} + +.mainmenu-function-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.6), 10, 0.4, 3, 3); + -fx-text-fill: #4a148c; +} + +/* ===== 退出按钮样式 - 浅紫色 ===== */ +.mainmenu-logout-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ab47bc; + -fx-border-width: 1px; + -fx-padding: 10px 25px; + -fx-min-width: 140px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.mainmenu-logout-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); +} + +/* ===== 状态标签样式 ===== */ +.mainmenu-status-label { + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-alignment: center; +} + +/* ===== 答题界面专用样式 - 优化布局版本 ===== */ +.quiz-background-new { + -fx-background-image: url('../images/quiz-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.quiz-title-new { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.5, 2, 2); + -fx-alignment: center; +} + +/* ===== 进度条样式 ===== */ +.quiz-progress-bar-new { + -fx-accent: #ba68c8; + -fx-background-color: #f3e5f5; + -fx-background-radius: 8px; + -fx-border-radius: 8px; + -fx-padding: 2px; +} + +.quiz-progress-bar-new .track { + -fx-background-color: #f3e5f5; + -fx-background-radius: 8px; +} + +.quiz-progress-bar-new .bar { + -fx-background-color: linear-gradient(to right, #ba68c8, #ab47bc); + -fx-background-radius: 8px; +} + +/* ===== 题目内容样式 - 减小高度 ===== */ +.quiz-question-text { + -fx-text-fill: #4a148c; + -fx-font-weight: bold; + -fx-font-size: 18px; + -fx-wrap-text: true; + -fx-background-color: rgba(255, 255, 255, 0.92); + -fx-background-radius: 15px; + -fx-padding: 15px 20px; + -fx-border-color: rgba(186, 104, 200, 0.6); + -fx-border-radius: 15px; + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.3), 10, 0.4, 3, 3); + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 80px; + -fx-pref-height: 80px; +} + +/* ===== 选项容器样式 - 减小高度 ===== */ +.quiz-options-container { + -fx-background-color: rgba(255, 255, 255, 0.88); + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-padding: 15px 20px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.25), 12, 0.4, 3, 3); + -fx-min-height: 180px; + -fx-pref-height: 180px; +} + +/* ===== 单选按钮样式 - 减小间距 ===== */ +.quiz-radio-button-new { + -fx-text-fill: #4a148c; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-cursor: hand; + -fx-padding: 8px 0px; +} + +.quiz-radio-button-new .radio { + -fx-background-color: white; + -fx-border-color: #ba68c8; + -fx-border-radius: 12px; + -fx-background-radius: 12px; + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 4, 0.3, 1, 1); +} + +.quiz-radio-button-new:selected .radio { + -fx-background-color: #ba68c8; + -fx-border-color: #7b1fa2; + -fx-border-width: 3px; +} + +.quiz-radio-button-new:selected .dot { + -fx-background-color: white; +} + +.quiz-radio-button-new:hover .radio { + -fx-border-color: #7b1fa2; + -fx-border-width: 3px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.3), 6, 0.4, 2, 2); +} + +/* ===== 提交按钮样式 ===== */ +.quiz-submit-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 10px 25px; + -fx-min-width: 150px; + -fx-min-height: 45px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.4, 2, 2); +} + +.quiz-submit-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.5, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.quiz-submit-button:pressed { + -fx-background-color: linear-gradient(to bottom, #ab47bc, #8e24aa); +} + +/* ===== 答题状态标签样式 - 固定位置 ===== */ +.quiz-status-label-empty { + -fx-text-fill: transparent; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 8px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 8px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-min-height: 0px; + -fx-pref-height: 0px; + -fx-max-height: 0px; +} + +.quiz-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-min-height: 35px; + -fx-pref-height: 35px; + -fx-max-height: 35px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 4, 0.3, 1, 1); +} + +/* ===== 分数界面专用样式 - 修复文字换行 ===== */ +.score-background-new { + -fx-background-image: url('../images/score-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.score-glass-panel-new { + -fx-background-color: rgba(255, 255, 255, 0.9); + -fx-background-radius: 35px; + -fx-border-radius: 35px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.25), 30, 0.6, 0, 10); + -fx-padding: 30px 25px; +} + +.score-complete-title-new { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 32px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.3), 6, 0.6, 3, 3); + -fx-wrap-text: true; + -fx-text-alignment: center; + -fx-alignment: center; +} + +.score-label-new { + -fx-text-fill: linear-gradient(to bottom, #ba68c8, #7b1fa2); + -fx-font-weight: bold; + -fx-font-size: 60px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.4), 8, 0.7, 4, 4); + -fx-alignment: center; +} + +.score-description-new { + -fx-text-fill: #7b1fa2; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-alignment: center; +} + +/* ===== 结果消息样式 - 完全修复文字换行 ===== */ +.result-message-new { + -fx-text-fill: #7b1fa2; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(255, 245, 157, 0.3); + -fx-background-radius: 15px; + -fx-padding: 12px 20px; + -fx-border-color: rgba(255, 235, 59, 0.4); + -fx-border-radius: 15px; + -fx-border-width: 1px; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.2), 8, 0.3, 2, 2); + -fx-alignment: center; + -fx-text-alignment: center; + -fx-max-width: 300px; + -fx-min-height: 60px; + -fx-pref-height: 60px; +} + +/* ===== 庆祝按钮样式 ===== */ +.celebrate-button-new { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 12px 30px; + -fx-min-width: 160px; + -fx-min-height: 50px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 10, 0.4, 3, 3); +} + +.celebrate-button-new:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 12, 0.5, 4, 4); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.celebrate-button-new:pressed { + -fx-background-color: linear-gradient(to bottom, #ab47bc, #8e24aa); +} + +/* ===== 退出按钮样式 ===== */ +.score-logout-button-new { + -fx-background-color: linear-gradient(to bottom, #fff59d, #ffeb3b); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ffeb3b; + -fx-border-width: 1px; + -fx-padding: 10px 25px; + -fx-min-width: 140px; + -fx-min-height: 45px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.4), 8, 0.3, 2, 2); +} + +.score-logout-button-new:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.6), 10, 0.4, 3, 3); + -fx-text-fill: #4a148c; +} + +/* ===== 修改密码界面专用样式 - 淡黄色和淡紫色主题 ===== */ +.password-background-new { + -fx-background-image: url('../images/password-bg.png'); /* 修改 */ + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.password-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.88); + -fx-background-radius: 30px; + -fx-border-radius: 30px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.2), 25, 0.5, 0, 8); +} + +.password-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.6, 2, 2); +} + +.password-security-tip { + -fx-text-fill: #7b1fa2; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 10px; + -fx-padding: 10px 15px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; +} + +.password-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.password-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.password-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 12px 24px; + -fx-min-width: 150px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.password-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +.password-secondary-button { + -fx-background-color: linear-gradient(to bottom, #fff59d, #ffeb3b); + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-background-radius: 20px; + -fx-border-radius: 20px; + -fx-border-color: #ffeb3b; + -fx-border-width: 1px; + -fx-padding: 10px 20px; + -fx-min-width: 150px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.4), 6, 0.3, 2, 2); +} + +.password-secondary-button:hover { + -fx-background-color: linear-gradient(to bottom, #fff9c4, #fff59d); + -fx-effect: dropshadow(gaussian, rgba(255, 235, 59, 0.6), 8, 0.4, 2, 2); +} + +/* ===== 修改密码界面状态标签样式 - 修复换行版本 ===== */ +.password-status-label { + -fx-text-fill: transparent; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 10px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 10px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 0px; + -fx-pref-height: 0px; + -fx-max-width: 300px; +} + +.password-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 10px; + -fx-padding: 10px 15px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 60px; + -fx-pref-height: 60px; + -fx-max-width: 300px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.3, 2, 2); +} + +/* ===== 设置密码界面专用样式 - 淡黄色和淡紫色主题 ===== */ +.setpassword-background { + -fx-background-image: url('../images/background.png'); + -fx-background-size: cover; + -fx-background-position: center center; + -fx-background-repeat: no-repeat; + -fx-background-color: linear-gradient(to bottom right, #fff9c4, #f3e5f5); +} + +.setpassword-glass-panel { + -fx-background-color: rgba(255, 255, 255, 0.88); + -fx-background-radius: 30px; + -fx-border-radius: 30px; + -fx-border-color: rgba(186, 104, 200, 0.4); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.2), 25, 0.5, 0, 8); +} + +.setpassword-title { + -fx-text-fill: #7b1fa2; + -fx-font-weight: bold; + -fx-font-size: 26px; + -fx-effect: dropshadow(gaussian, rgba(123, 31, 162, 0.25), 4, 0.6, 2, 2); +} + +.setpassword-prompt { + -fx-text-fill: #7b1fa2; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.1); + -fx-background-radius: 10px; + -fx-padding: 10px 15px; + -fx-border-color: rgba(186, 104, 200, 0.2); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; +} + +.setpassword-textfield { + -fx-background-color: rgba(255, 255, 255, 0.95); + -fx-border-color: rgba(186, 104, 200, 0.5); + -fx-border-radius: 15px; + -fx-background-radius: 15px; + -fx-padding: 12px 16px; + -fx-font-size: 14px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.2, 2, 2); +} + +.setpassword-textfield:focused { + -fx-border-color: rgba(156, 39, 176, 0.8); + -fx-border-width: 2px; + -fx-effect: dropshadow(gaussian, rgba(156, 39, 176, 0.3), 8, 0.3, 2, 2); +} + +.setpassword-primary-button { + -fx-background-color: linear-gradient(to bottom, #ba68c8, #ab47bc); + -fx-text-fill: white; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 25px; + -fx-border-radius: 25px; + -fx-border-color: #ab47bc; + -fx-border-width: 2px; + -fx-padding: 12px 24px; + -fx-min-width: 150px; + -fx-cursor: hand; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.4), 8, 0.3, 2, 2); +} + +.setpassword-primary-button:hover { + -fx-background-color: linear-gradient(to bottom, #ce93d8, #ba68c8); + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.6), 10, 0.4, 3, 3); + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; +} + +/* ===== 设置密码界面状态标签样式 - 修复页面滑动 ===== */ +.setpassword-status-label { + -fx-text-fill: transparent; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: transparent; + -fx-background-radius: 10px; + -fx-padding: 0px; + -fx-border-color: transparent; + -fx-border-radius: 10px; + -fx-border-width: 0px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 70px; + -fx-pref-height: 70px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; +} + +.setpassword-status-label-with-text { + -fx-text-fill: #7b1fa2; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-wrap-text: true; + -fx-background-color: rgba(186, 104, 200, 0.15); + -fx-background-radius: 10px; + -fx-padding: 12px 15px; + -fx-border-color: rgba(186, 104, 200, 0.3); + -fx-border-radius: 10px; + -fx-border-width: 1px; + -fx-alignment: center; + -fx-text-alignment: center; + -fx-min-height: 70px; + -fx-pref-height: 70px; + -fx-max-width: 280px; + -fx-line-spacing: 2px; + -fx-effect: dropshadow(gaussian, rgba(186, 104, 200, 0.2), 6, 0.3, 2, 2); +} +/* ===== 表单字段标签 (新增) ===== */ +.form-label { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #7b1fa2; /* 使用与标题一致的紫色 */ + -fx-padding: 0 0 4px 8px; /* 在标签下方和左侧留出一点空间 */ +} \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml b/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml index 480d327..7e386ca 100644 --- a/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml +++ b/src/main/resources/com/mathgenerator/view/ChangePasswordView.fxml @@ -7,25 +7,85 @@ - + + - - - - - - \ No newline at end of file diff --git a/src/main/resources/com/mathgenerator/view/RegisterView.fxml b/src/main/resources/com/mathgenerator/view/RegisterView.fxml index 5012e91..657a541 100644 --- a/src/main/resources/com/mathgenerator/view/RegisterView.fxml +++ b/src/main/resources/com/mathgenerator/view/RegisterView.fxml @@ -3,36 +3,97 @@ - - + + - - - - + + - -