完结版本 #19

Merged
hnu202326010328 merged 24 commits from develop into main 3 months ago

@ -1,80 +1,98 @@
# 中小学数学卷子自动生成程序
# MathGenerator - 中小学数学学习软件
本项目是一个基于 Java 开发的命令行应用CLI旨在为小学、初中和高中的数学老师提供一个便捷的工具用于自动生成符合教学要求的数学练习卷
`MathGenerator` 是一个使用 JavaFX 构建的桌面应用程序,旨在为中小学生提供一个有趣且富有挑战性的数学练习平台。用户可以注册登录,选择不同难度(小学、初中、高中),生成定制数量的数学测验卷,并在完成后立即获得分数反馈
程序设计遵循了现代软件工程原则通过模块化和面向对象的设计实现了UI与逻辑分离、高内聚、低耦合以及良好的可扩展性。
## 核心功能
## 主要功能
* **多学段支持**:可为 **小学**、**初中**、**高中** 三个不同学段生成题目
* **分级与混合难度**
* 题目难度逐级递增,高年级题目会自动包含低年级知识点(例如,高中题可能包含平方、开根号等)
* 生成的试卷会自动混合不同难度的题目(例如,一份高中试卷会包含一定比例的初中和小学基础题),更贴近真实考试
* **历史题目查重**:确保为同一位老师生成的题目不会与历史试卷中的题目重复,保证每次练习都是全新的
* **菜单式交互界面**:提供清晰、易用的数字菜单进行操作,无需记忆复杂命令
* **自动保存与归档**:生成的试卷会自动以“年-月-日-时-分-秒.txt”的格式命名并保存在每位教师专属的文件夹下方便管理和追溯
- **完整的用户系统**: 支持新用户注册、邮箱验证、登录以及密码修改功能。
- **三级难度递进**:
- **小学**: “安全”的四则运算,确保运算过程和结果不为负数
- **初中**: 在小学基础上,增加了平方和开方运算。
- **高中**: 在初中基础上,增加了基础的三角函数运算 (sin, cos, tan)
- **动态试卷生成**: 用户可自定义每套试卷的题目数量10-30题程序会根据所选难度动态生成不重复的题目
- **即时反馈与评分**: 完成答题后,系统会立刻计算分数,并根据分数高低给出不同的鼓励性评语
- **试卷存档**: 每次生成的试卷都会自动以 `.txt` 格式保存在本地,方便用户随时回顾和复习
- **精美的用户界面**: 使用 JavaFX 和 CSS精心设计了所有界面包含自定义的背景图片和玻璃拟态效果提升了用户体验
## 技术栈
* **开发语言**Java 21
* **构建工具**Apache Maven
- **核心框架**: Java 17, JavaFX 17
- **JSON 处理**: Google Gson (用于持久化用户数据)
- **邮件服务**: Apache Commons Mail (用于发送注册验证码)
- **表达式计算**: Mozilla Rhino (用于动态计算生成的数学表达式字符串)
- **构建工具**: Maven
## 如何启动
## 如何运行
配置 **JDK 21 (或更高版本)****Apache Maven**
#### 1\. 先决条件
### 1\. 环境要求
- JDK 17 或更高版本
- Apache Maven
* `JAVA_HOME` 环境变量已正确设置。
* **全平台通用**
#### 2\. 配置邮件服务
### 2\. 构建项目
为了使“邮箱验证码”功能正常工作,您需要在项目根目录下创建一个 `config.properties` 文件,并填入您的 SMTP 服务信息。
首先,需要使用 Maven 将项目打包成一个可执行的 `.jar` 文件。
同时,在自己的邮箱设置中开通相应服务
在项目的根目录(即 `pom.xml` 文件所在的目录)下,打开终端并执行以下命令:
**`config.properties` 文件模板:**
```bash
mvn clean package
```properties
# QQ邮箱 SMTP 服务器配置示例
smtp.host=smtp.qq.com
smtp.port=465
# 您的发件人邮箱地址
smtp.username=your-email@qq.com
# 您的邮箱SMTP授权码 (注意:不是邮箱登录密码)
smtp.password=your_email_authorization_code
```
该命令会编译所有代码,并 T 在 `target/` 目录下生成一个名为 `mathgenerator.jar` 的文件。
#### 3\. 构建和运行
### 3\. 运行程序
构建成功后,使用以下命令来启动程序:
在项目根目录下,执行以下 Maven 命令:
```bash
java -jar mathgenerator.jar
# 清理并构建项目,生成一个可执行的 fat-jar
mvn clean package
# 运行生成的 JAR 文件
java -jar MathGenerator-1.0.0.jar
```
如果一切顺利,您将在终端看到欢迎界面和用户登录提示。
*(注意: JAR 文件名可能因 `pom.xml` 配置而异)*
## 项目结构
项目采用标准的 Maven 目录结构,核心逻辑清晰分离:
```
.
├── pom.xml # Maven 核心配置文件
├── users.json # 用户数据文件
└── src
└── main
└── java
└── com
└── mathgenerator
├── Application.java # 程序主入口:负责组装和启动
├── model/ # 数据模型包
├── ui/ # 用户界面包
|
├── storage/ # 存储包
├── generator/ # 题目生成器包
└── service/ # 核心业务服务包
└── strategy/ # 试卷组合策略子包
```
src
├── main
│ ├── java
│ │ └── com
│ │ └── mathgenerator
│ │ ├── controller # FXML视图的控制器类
│ │ ├── generator # 不同难度题目的生成器
│ │ ├── model # 数据模型 (User, Level, ChoiceQuestion)
│ │ ├── service # 核心业务逻辑 (用户服务, 试卷服务)
│ │ ├── storage # 文件读写管理
│ │ ├── util # 工具类 (验证逻辑)
│ │ ├── Launcher.java # 解决 fat-jar 问题的启动器
│ │ └── MainApplication.java # JavaFX主程序入口
│ └── resources
│ └── com
│ └── mathgenerator
│ ├── images # UI图片资源
│ ├── styles # CSS样式表
│ │
│ └── view # FXML界面布局文件
│── users.json # (程序首次运行后生成) 用户数据文件
└── config.properties # 邮箱配置文件
```
## 界面概览
- **登录与注册**: 简洁的登录界面,提供注册新用户的入口。注册流程包含邮箱验证,确保用户真实性。
- **主菜单**: 用户登录后,可以看到欢迎信息,并选择“小学”、“初中”、“高中”三种难度。同时可以自定义题目数量。
- **答题界面**: 清晰地展示题目、选项和进度条。用户通过单选按钮作答,并提交答案进入下一题。
- **分数界面**: 所有题目完成后,展示最终得分和鼓励评语,并提供“再做一组”或“退出登录”的选项。
-----

@ -1,84 +0,0 @@
# 使用 `record` 定义 `User` 类的好处
这是对 `User` 类的说明:[User.java](../src/main/java/com/mathgenerator/model/User.java)
在项目中,`User` 类被定义为一个 `record`
```java
public record User(String username, String password, Level level) {}
```
这个看似简单的单行代码,实际上是 Java 14 引入的一项强大特性。它不仅是“少写代码”的捷径,更代表了一种更现代化、更安全、更清晰的编程范式。下面,我们将详细阐述使用 `record` 的核心优势。
## 1\. 代码的极致简洁与可读性
`record` 出现之前,要创建一个简单的数据载体类,我们需要编写大量冗长的模板代码。
#### 传统写法(对比)
```java
public final class OldUser {
private final String username;
private final String password;
private final Level level;
public OldUser(String username, String password, Level level) {
this.username = username;
this.password = password;
this.level = level;
}
public String getUsername() { return username; }
public String getPassword() { return password; }
public Level getLevel() { return level; }
@Override
public boolean equals(Object o) { }
@Override
public int hashCode() { }
@Override
public String toString() { }
}
```
可以看到,一个简单的 `User` 类需要 **30-40 行** 代码。而使用 `record`,我们用 **1 行** 就完成了同样的功能。这极大地减少了视觉噪音,让代码库更整洁,开发者可以一眼看清 `User` 类的意图——它仅仅是一个数据容器。
## 2\. 默认实现的不可变性 (Immutability)
`record` 声明的类具有一个至关重要的特性:**默认不可变**。
当编译器看到 `record User(...)` 时,它会自动执行以下操作:
* 为 `username`, `password`, `level` 创建 **`private final`** 字段。
* `final` 关键字意味着这些字段一旦在对象创建时被赋值,就**永远不能再被修改**。
**不可变性的好处:**
* **线程安全**:不可变对象可以在多个线程之间自由共享,无需担心数据被意外篡改,从而简化了并发编程。
* **可预测性**:当您将 `User` 对象传递给一个方法时,您完全不用担心这个方法会改变对象内部的状态。这使得程序的行为更容易推理和调试。
* **减少 Bug**:许多难以追踪的 Bug 都源于对象状态的意外变化。不可变性从根本上杜绝了这类问题。
## 3\. 自动生成高质量的数据处理方法
`record` 不仅创建了字段和构造函数,还自动为我们生成了所有数据类都应该具备的核心方法,且这些方法的实现是标准和可靠的。
* **`equals()``hashCode()`**
* 编译器会生成一个基于所有字段内容的 `equals()` 方法。如果两个 `User` 对象的用户名、密码和等级都相同,`equals()` 就会返回 `true`
* 同时生成的 `hashCode()` 方法与 `equals()` 保持一致,这对于将 `User` 对象用作 `HashMap` 的键或存入 `HashSet` 至关重要。手动编写这两个方法既繁琐又容易出错,而 `record` 则完美地解决了这个问题。
* **`toString()`**
* 自动生成的 `toString()` 方法提供了清晰、可读的输出,非常便于日志记录和调试。
* 例如,打印一个 `User` 对象会得到类似 `User[username=张三1, password=123, level=PRIMARY]` 的输出,而不是无意义的类名和哈希码。
* **简洁的访问器方法**
* `record` 会为每个字段生成一个同名的、不带 `get` 前缀的公共访问器方法,如 `user.username()` `user.password()`。这种命名方式更加简洁。
## 4\. 明确的语义和意图
当项目中的一个类被声明为 `record` 时,它向所有阅读代码的开发者传递了一个清晰的信号:**“我是一个简单、透明、不可变的数据载体”**。
这排除了该类包含复杂业务逻辑的可能性,使得代码的结构和意图更加清晰,降低了团队协作的沟通成本。

@ -1,120 +1,145 @@
# 功能拆分文档
# MathGenerator 项目详解
项目是一个基于Java开发的命令行应用程序旨在为小学、初-中和高中三个学段的教师自动生成符合特定难度要求的数学练习卷。项目采用模块化和面向对象的思想进行设计,并引入了策略模式等设计模式,确保了代码的高内聚、低耦合以及良好的可扩展性
文档提供了 `MathGenerator` 项目的深入介绍,旨在帮助开发者快速理解项目的设计、功能实现和部署方法
## 项目结构
## 一、项目结构 (Maven)
项目采用标准的 Maven 目录结构,并遵循了职责分离的设计原则,将不同功能的模块划分到独立的包中,最终结构如下:
项目采用标准的 Maven 目录结构,并通过分包将不同职责的类清晰地隔离开来,遵循了关注点分离 (SoC) 的设计原则。
```
.
├── pom.xml # Maven 核心配置文件
├── users.json # 用户数据文件
└── src
└── main
└── java
└── com
└── mathgenerator
├── Application.java # 程序主入口:负责组装和启动
├── model/ # 数据模型包
│ ├── Level.java # 学段枚举
│ └── User.java # 用户数据模型
├── ui/ # 用户界面包
│ └── ConsoleUI.java # 负责所有命令行交互
├── storage/ # 存储包
│ └── FileManager.java # 负责所有文件的读写
├── generator/ # 题目生成器包
│ ├── QuestionGenerator.java
│ ├── PrimarySchoolGenerator.java
│ ├── JuniorHighSchoolGenerator.java
│ └── SeniorHighSchoolGenerator.java
└── service/ # 核心业务服务包
├── UserService.java # 负责用户注册、登录和持久化
├── PaperService.java # 负责试卷的创建流程
└── strategy/ # 试卷组合策略子包
├── PaperStrategy.java
└── MixedDifficultyStrategy.java
├── pom.xml # Maven 项目配置文件
├── config.properties # (需手动创建) 邮件服务配置文件
├── generated_papers/ # (程序运行后生成) 保存试卷的目录
├── users.json # (程序运行后生成) 用户数据库文件
└── src/
├── main/
│ ├── java/
│ │ └── com/mathgenerator/
│ │ ├── controller/ # MVC - 控制器层
│ │ │ ├── LoginController.java
│ │ │ ├── RegisterController.java
│ │ │ ├── MainMenuController.java
│ │ │ └── ... (其他界面控制器)
│ │ ├── generator/ # 题目生成器 (工厂)
│ │ │ ├── QuestionGenerator.java (接口)
│ │ │ ├── PrimarySchoolGenerator.java
│ │ │ └── ... (其他难度生成器)
│ │ ├── model/ # MVC - 模型层
│ │ │ ├── User.java
│ │ │ ├── Level.java
│ │ │ └── ChoiceQuestion.java
│ │ ├── service/ # 业务逻辑服务层
│ │ │ ├── strategy/ # 策略模式包
│ │ │ │ ├── PaperStrategy.java (接口)
│ │ │ │ └── MixedDifficultyStrategy.java
│ │ │ ├── UserService.java
│ │ │ ├── PaperService.java
│ │ │ └── EmailConfig.java
│ │ ├── storage/ # 文件持久化层
│ │ │ └── FileManager.java
│ │ ├── util/ # 工具类
│ │ │ └── ValidationUtils.java
│ │ ├── Launcher.java # 主启动器 (解决 fat-jar 问题)
│ │ └── MainApplication.java # JavaFX 应用主入口
│ └── resources/
│ └── com/mathgenerator/
│ ├── images/ # UI 图片资源
│ ├── styles/ # CSS 样式表
│ │ └── styles.css
│ └── view/ # MVC - 视图层 (FXML)
│ ├── LoginView.fxml
│ ├── RegisterView.fxml
│ └── ... (其他 FXML 视图文件)
└── test/
└── ... (测试代码目录)
```
## 核心功能拆解
项目的核心功能可以划分为以下几个主要模块:
---
## 二、核心功能拆分
### 1\. 用户账户模块
项目整体功能被划分为多个核心模块每个模块由专门的Java包Package进行管理职责清晰。
- **职责**: 管理用户的整个生命周期,包括注册、登录、认证和信息修改。
- **实现**:
- **数据持久化**: 用户信息(用户名、邮箱、加密密码)被序列化为 JSON 格式,并存储在根目录的 `users.json` 文件中。数据的读写由 `UserService` 通过 `Gson` 库完成。
- **注册流程**:
1. `RegisterController` 收集用户输入(用户名、邮箱)。
2. 调用 `ValidationUtils` 对输入格式进行校验。
3. 调用 `UserService``sendVerificationCode` 方法,该方法通过 `EmailConfig` 读取配置,并使用 `Apache Commons Mail` 发送验证码邮件。
4. 验证码校验通过后,`UserService` 会创建一个密码字段为 `null` 的新用户,并导航至 `SetPasswordController` 设置初始密码。
- **登录与授权**: `LoginController` 调用 `UserService``login` 方法,验证用户名和密码的匹配性。成功后,将完整的 `User` 对象传递给 `MainMenuController`,完成授权。
### 1\. 数据模型模块 (`model`包)
### 2\. 试卷生成与管理模块
该模块定义了项目中使用的核心数据结构,确保数据的类型安全和一致性。
- **职责**: 动态创建数学试卷,并将其持久化存储。
- **实现**:
- **题目生成**:
- 采用**工厂方法**和**策略模式**。`QuestionGenerator` 作为工厂接口,定义了 `generateSingleQuestion` 方法。
- `PrimarySchoolGenerator`、`JuniorHighSchoolGenerator` 等是具体工厂,分别负责生产不同难度的题目(产品)。
- `PaperService` 作为上下文,利用 `MixedDifficultyStrategy` (具体策略) 来决定在特定难度下应调用哪个 `QuestionGenerator` 工厂。
- **题目查重**: 在 `PaperService``createPaper` 方法中,首先会调用 `FileManager``loadExistingQuestions` 方法,加载用户以往做过的所有题目的题干。在生成新题目时,会确保新题目的题干既不与历史题目重复,也不与本轮已生成的题目重复。
- **试卷存档**: `QuizController` 在生成试卷后,会立即调用 `PaperService``savePaper` 方法。该方法再委托 `FileManager` 将试卷内容格式化成一个用户友好的 `.txt` 文件,并存放在 `generated_papers/<username>/` 目录下。
* **`Level` 枚举 (Enum)**:
* 定义了三个常量:`PRIMARY` (小学), `JUNIOR_HIGH` (初中), `SENIOR_HIGH` (高中)。
* 使用枚举代替纯字符串有效防止了因拼写错误导致的bug。
* **`User` 类**: 一个数据类(采用 `Record` 实现),用于封装用户的核心属性:用户名 (`username`)、密码 (`password`) 和所属学段 (`level`)。
### 3\. UI 与交互模块
### 2\. 题目生成模块 (`generator`包)
- **职责**: 提供用户友好的图形界面,并处理用户的交互事件。
- **实现**:
- **视图定义**: 所有界面布局均由 FXML 文件在 `resources/com/mathgenerator/view` 目录中定义。这种方式将界面设计与业务逻辑完全分离。
- **样式**: 统一的视觉风格由 `resources/com/mathgenerator/styles/styles.css` 文件定义,实现了包括玻璃拟态、按钮悬停效果和动态状态标签在内的丰富样式。
- **事件处理**: 每个 FXML 文件都绑定了一个 `Controller` 类。`@FXML` 注解将 FXML 中的控件(如 Button, TextField注入到控制器中。控件的 `onAction` 属性(如 `onAction="#handleLoginButtonAction"`) 将用户操作(如点击)直接关联到控制器中的相应处理方法。
- **场景导航**: 界面间的跳转是通过在控制器中加载新的 FXML 文件,获取其控制器实例,(如有需要)传递数据,然后将新场景设置到主舞台 (Stage) 上来实现的。
这是实现项目核心业务逻辑的模块,采用接口驱动和继承链设计。
## MAVEN 构建与运行
* **`QuestionGenerator` 接口**: 定义了所有题目生成器必须遵守的统一规范,包含一个核心方法 `String generateSingleQuestion()`
* **三个实现类**:
* `PrimarySchoolGenerator`: 生成包含 `+`, `-`, `*`, `/` 和 `()` 的小学难度题目。
* `JuniorHighSchoolGenerator`: **继承自 `PrimarySchoolGenerator`**,在小学题目基础上,确保至少包含一个平方或开根号运算。
* `SeniorHighSchoolGenerator`: **继承自 `JuniorHighSchoolGenerator`**,在初中题目基础上,再确保至少包含一个 `sin`, `cos``tan` 三角函数运算。
* 所有生成器都遵守“操作数在1-5个之间取值范围1-100”的规则。
要成功构建并运行此项目,请遵循以下步骤:
### 3\. 文件存储模块 (`storage`包)
#### 1\. 前置要求
该模块封装了所有与**试卷文件**系统交互的操作。
- **JDK 17** (或更高版本): 确保已安装并配置好环境变量。
- **Apache Maven**: 确保已安装并配置好环境变量。
- **SMTP 服务**: 您需要一个支持 SMTP 的邮箱账户,用于发送注册验证码。
* **`FileManager` 类**:
* **保存试卷**: 提供 `savePaper` 方法,以“年-月-日-时-分-秒.txt”的格式保存试卷到用户专属的文件夹。
* **读取历史题目 (查重)**: 提供 `loadExistingQuestions` 方法,扫描用户文件夹下的所有历史题目,返回一个 `Set<String>` 集合用于查重。
#### 2\. 配置邮件服务
### 4\. 业务服务模块 (`service`包)
在项目的根目录下(与 `pom.xml` 同级),手动创建一个名为 `config.properties` 的文本文件。然后,根据您的邮箱服务商信息,填入以下内容。
该模块是业务逻辑的协调者,包含了用户管理和试卷生成两大核心服务。
**以 QQ 邮箱为例:**
* **`UserService` 类**:
* 是用户管理的**核心**。
* 负责用户的**注册**、**登录**以及**持久化存储**。
* 程序启动时从 `users.json` 文件加载用户数据,在用户注册时将新数据写回文件。
* **`PaperService` 类**:
* 负责处理“生成并保存一份完整试卷”的**流程**。
* 它持有一个 `PaperStrategy` 对象的引用,将具体的题目难度选择**委托**给策略对象来执行。
* **`service.strategy` 子包**:
* **`PaperStrategy` 接口**: 定义了“试卷组合策略”的抽象规范。
* **`MixedDifficultyStrategy` 类**: `PaperStrategy` 的一个具体实现,负责生成包含不同难度梯度的混合试卷。
```properties
# SMTP 服务器地址
smtp.host=smtp.qq.com
# SSL 加密端口
smtp.port=465
# 您的发件人邮箱地址
smtp.username=your-email@qq.com
# 您的邮箱 SMTP 授权码 (注意:这不是邮箱的登录密码,需要在邮箱设置中生成)
smtp.password=your_email_authorization_code
```
### 5\. 主程序与UI模块 (`Application.java` 和 `ui`包)
#### 3\. 使用 Maven 构建
为遵循单一职责原则程序的启动和UI交互被拆分到独立的单元中。
打开终端或命令行,导航到项目的根目录,然后执行以下 Maven 命令:
* **`Application.java`**: 程序的唯一入口。其 `main` 方法非常简洁,仅负责创建和组装所有核心组件(如 `UserService`, `PaperService`, `ConsoleUI` 等),然后启动应用。
* **`ui.ConsoleUI` 类**: 一个专门负责所有控制台用户界面显示和交互的类。它实现了**菜单驱动**的用户界面,处理用户的**登录**、**注册**、**难度切换**和**题目生成**等所有交互操作。
```bash
# 1. 清理旧的构建产物
mvn clean
## 三、如何使用 Maven 构建与运行
# 2. 编译代码、运行测试并打包成一个可执行的 "fat-jar"
mvn package
```
项目采用 Maven 进行自动化构建和打包。
执行成功后,您会在 `target/` 目录下找到一个名为 `math-generator-1.0-SNAPSHOT.jar` (文件名可能略有不同) 的文件。这个 JAR 文件已经包含了所有依赖,可以直接运行
1. **构建 (打包)**: 在项目根目录(`pom.xml` 所在位置)打开终端,执行以下命令:
#### 4\. 运行应用程序
```bash
mvn clean package
```
继续在终端中,执行以下命令来启动应用程序:
该命令会自动完成编译、测试和打包,并在 `target/` 目录下生成一个可执行的 `mathgenerator.jar` 文件。
```bash
java -jar target/math-generator-1.0-SNAPSHOT.jar
```
2. **运行**: 继续在终端中执行以下命令来启动程序。
片刻之后,您将看到应用程序的登录窗口
```bash
java -jar mathgenerator.jar
```
-----

@ -7,7 +7,7 @@ set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8
:: 3. 运行你的 JAR 包
echo loading...
java -jar mathgenerator.jar
java -jar MathGenerator-1.0.0.jar
:: 4. 清理环境变量并暂停,以便查看程序输出
set JAVA_TOOL_OPTIONS=

@ -1,18 +1,98 @@
# 程序运行说明
# MathGenerator - 程序运行指南
1. 环境配置java21及以上不推荐使用windows要设置更改字符否则中文字符会乱码建议使用ubuntumacos等系统
本文档将详细引导您完成 `MathGenerator` 数学学习软件的下载、配置、构建和运行全过程。请遵循以下步骤以确保程序能够顺利启动。
2. 在控制台输入
## 1\. 系统环境要求
```bash
java -jar mathgenerator.jar
```
在开始之前,请确保您的计算机上已安装并正确配置了以下软件:
可以开始使用程序
- **Java Development Kit (JDK) 17**: 本项目基于 Java 17 开发。您需要安装 JDK 17 或更高版本。
- *验证方法*: 打开终端或命令行,输入 `java -version`,确保显示的版本号是 17 或以上。
- **Apache Maven**: 本项目使用 Maven 进行依赖管理和项目构建。
- *验证方法*: 在终端或命令行中,输入 `mvn -version`,确保能够看到 Maven 的版本信息。
3. 程序提供在windows运行的bat脚本需要可以双击运行
## 2\. 关键配置步骤:邮件服务
4. 第一次运行时在运行目录下会生成默认用户的json文件生成的试卷放在generated文件夹下
本程序的“注册”功能需要通过发送邮件验证码来验证用户邮箱的真实性。因此,您**必须**在运行程序前配置好 SMTP 邮件服务。
5. 注册功能的说明:注册功能是自己添加,由于项目要求登录时将用户名和密码同行隔空格输入,因此注册时的特殊情况,如密码含空格,会无法登录,但是登录不是课程考核之一,希望不要引起不必要的误会,特此说明
#### 第 1 步:创建配置文件
在项目的根目录下 (与 `pom.xml` 文件位于同一级),手动创建一个名为 `config.properties` 的文本文件。
#### 第 2 步:填写配置信息
使用文本编辑器打开 `config.properties` 文件,并根据您自己的邮箱服务商信息,填入以下内容。
**我们强烈推荐使用 QQ 邮箱作为示例,其配置如下:**
```properties
# 邮件服务器主机名 (QQ邮箱固定为此值)
smtp.host=smtp.qq.com
# SSL 加密端口 (QQ邮箱固定为此值)
smtp.port=465
# 您的发件人邮箱地址 (例如: 12345678@qq.com)
smtp.username=your-email@qq.com
# 您的邮箱SMTP授权码 (!!! 注意:这不是邮箱的登录密码)
smtp.password=your_email_authorization_code
```
#### 第 3 步:获取 SMTP 授权码
“授权码”是用于登录第三方客户端的专用密码,以保证您主登录密码的安全。
**获取 QQ 邮箱授权码的步骤:**
1. 登录您的 QQ 邮箱网页版。
2. 点击顶部的 **“设置”** -\> **“账户”**。
3. 向下滚动,找到 **“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”** 区域。
4. 确保 **“POP3/SMTP服务”** 和 **“IMAP/SMTP服务”** 至少有一个是开启状态。
5. 点击 **“生成授权码”**,根据弹出的窗口提示,使用您的密保手机发送一条短信。
6. 发送成功后窗口会显示一串16位的字符串这就是您的**授权码**。
7. 将这串授权码复制并粘贴到 `config.properties` 文件的 `smtp.password` 字段中。
> **重要提示**: 请妥善保管您的授权码,不要泄露给他人。
## 3\. 使用 Maven 构建项目
配置完成后,您需要使用 Maven 将项目源代码打包成一个可执行的 JAR 文件。
1. 打开您的终端或命令行工具 (例如 CMD, PowerShell, Terminal)。
2. 使用 `cd` 命令导航到 `MathGenerator` 项目的根目录。
3. 执行以下 Maven 命令来构建项目:
```bash
mvn clean package
```
4. Maven 会自动下载所有必需的依赖库,并编译您的代码。如果一切顺利,您会在命令行的输出日志中看到 `[INFO] BUILD SUCCESS`
5. 构建成功后,在项目目录下会生成一个新的 `target` 文件夹。其中,`math-generator-1.0-SNAPSHOT.jar` (文件名可能因 `pom.xml` 配置而略有不同) 就是我们需要的可执行文件。
## 4\. 运行应用程序
确保您仍然在项目的根目录的终端中,执行以下命令来启动应用程序:
```bash
java -jar target/math-generator-1.0-SNAPSHOT.jar
```
执行命令后,程序的登录窗口将会启动。
## 5\. 首次使用说明
- **注册新用户**: 首次使用时,您需要点击 **“注册新用户”** 按钮。
- **验证邮箱**: 在注册界面,请填写真实有效的邮箱地址,以便接收验证码。
- **开始练习**: 注册并设置密码成功后,您将自动登录并进入主菜单,可以选择不同难度开始您的数学练习之旅。
## 6\. 程序生成的文件
当您运行程序并与之交互后,项目根目录下会自动生成以下文件和文件夹:
- `users.json`: 这是一个 JSON 文件,用于存储所有已注册用户的信息。请不要手动修改或删除它。
- `generated_papers/`: 这是一个文件夹,您生成的每一份试卷都会以 `.txt` 格式保存在这里,并以您的用户名进行归类。
-----
如果您在运行过程中遇到任何问题,请首先检查 **JDK 和 Maven 的版本**是否正确,以及 `config.properties` 文件中的**邮箱配置和授权码**是否填写无误。

@ -1,48 +1,62 @@
# 设计模式
# MathGenerator - 设计模式分析
项目在开发过程中,为了实现代码的高内聚、低耦合以及未来的高可扩展性,采纳了多种业界标准的设计模式和原则。本文档将详细解析其中最核心的几种设计模式
文档旨在分析 `MathGenerator` 项目中所采用的核心软件设计模式。通过合理运用这些模式,项目在结构清晰度、可扩展性和可维护性方面都得到了显著提升
## 1. 策略模式 (Strategy Pattern)
## 1. 模型-视图-控制器 (Model-View-Controller, MVC)
策略模式是本项目架构中最为核心和体现拓展性的设计模式
MVC 是本项目最核心的架构模式,它将整个应用程序划分为三个紧密协作但相互独立的组件,有效地实现了业务逻辑与用户界面的解耦
* **意图**:定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。此模式让算法的变化独立于使用算法的客户。
- **模型 (Model)**:
- **描述**: 模型的职责是管理应用程序的数据和业务规则。它不关心数据如何被展示。
- **实现**: 项目中的 `com.mathgenerator.model` 包完美地扮演了模型的角色。
- `User.java`: 定义了用户的数据结构。
- `Level.java`: 使用枚举定义了难度级别,是业务规则的一部分。
- `ChoiceQuestion.java`: 封装了单道题目的数据结构。
* **在项目中的体现**
* **`PaperStrategy` 接口**:这是策略的抽象。它定义了一个 `selectGenerator(Level mainLevel)` 方法,所有具体的试卷组合算法都必须实现这个接口。
* **`MixedDifficultyStrategy` 类**这是一个具体的策略实现。它封装了我们设计的“混合难度”算法例如高中试卷包含60%高中题、30%初中题、10%小学题)。
* **`PaperService` 类**这是使用策略的上下文Context。`PaperService` 不再关心试卷的具体组合逻辑,它只持有一个 `PaperStrategy` 对象的引用,并在生成试卷时调用其 `selectGenerator` 方法。
- **视图 (View)**:
- **描述**: 视图负责渲染用户界面,将模型中的数据显示给用户。在 JavaFX 中,这通常由 FXML 文件来定义。
- **实现**: 项目 `resources/com/mathgenerator/view` 目录下的所有 `.fxml` 文件共同构成了视图层。每个 FXML 文件都精确地描述了一个界面的布局和组件,而具体的样式则由 CSS 文件 (`styles.css`) 提供。
* **带来的好处**
* **极高的可扩展性**:如果我们想新增一种“只出难题”的试卷模式,只需新增一个 `HardModeStrategy` 类实现 `PaperStrategy` 接口,然后在程序入口处注入这个新策略即可,**完全不需要修改 `PaperService` 的代码**。这完美遵循了“开闭原则”
* **职责分离**`PaperService` 的职责被简化为控制试卷生成的整体流程,而具体的题目组合算法则被分离到各个策略类中,使得代码结构更清晰
- **控制器 (Controller)**:
- **描述**: 控制器是模型和视图之间的协调者。它接收来自视图的用户输入,处理这些输入(可能需要更新模型),并选择合适的视图来响应用户
- **实现**: `com.mathgenerator.controller` 包下的所有 Java 类都是控制器。例如,`LoginController.java` 负责处理登录按钮的点击事件,调用 `UserService` (业务逻辑) 来验证用户信息 (模型),并根据结果决定是停留在登录页还是跳转到主菜单 (更新视图)
## 2. 工厂方法模式 (Factory Method Pattern) 的简化应用
## 2. 策略模式 (Strategy Pattern)
虽然没有严格地实现一个完整的工厂方法模式,但其核心思想在项目中得到了应用
策略模式在项目的试卷生成模块中得到了巧妙的应用,它允许在运行时动态地选择题目的生成算法
* **意图**:定义一个用于创建对象的接口,让子类决定实例化哪一个类
- **描述**: 该模式定义了一系列算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户
* **在项目中的体现**
* 我们的 `MixedDifficultyStrategy` 内部的 `selectGenerator` 方法实际上扮演了一个**简单工厂Simple Factory**的角色。它根据输入的 `Level` 和内部的随机逻辑,负责“生产”出具体的 `QuestionGenerator` 实例(`PrimarySchoolGenerator`, `JuniorHighSchoolGenerator` 等)。
- **实现**:
- **策略接口 (`PaperStrategy.java`)**: 定义了一个所有具体策略都必须实现的公共接口,其中包含一个核心方法 `selectGenerator(Level mainLevel)`
- **具体策略 (`MixedDifficultyStrategy.java`)**: 这是策略接口的一个具体实现。它封装了“混合难度”这一算法:根据用户选择的主难度,按不同的概率混合使用小学、初中、高中的题目生成器。
- **上下文 (`PaperService.java`)**: `PaperService` 类持有一个 `PaperStrategy` 对象的引用。当 `createPaper` 方法被调用时,它不关心具体的生成器是如何被选择的,而是将这个决策委托给当前的策略对象去完成。
* **带来的好处**
* **集中创建逻辑**:所有关于“如何根据难度创建对应题目生成器”的逻辑都被集中在一个地方,而不是散落在代码各处。当需要修改创建逻辑时,我们只需要改动这个“工厂”即可。
这种设计极大地增强了灵活性。未来如果需要增加新的试卷组合策略(例如“只考错题”策略),只需增加一个新的 `PaperStrategy` 实现类,而无需修改 `PaperService` 的代码。
## 3. 模板方法模式 (Template Method Pattern)
## 3. 单例模式 (Singleton-like Service Layer)
此模式在我们的题目生成器继承链中得到了巧妙的运用
尽管项目中没有严格按照“私有构造函数 + 静态 `getInstance()` 方法”来实现单例,但其 `service` 层的设计思想与单例模式非常相似
* **意图**:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
- **描述**: 确保一个类只有一个实例,并提供一个全局访问点
* **在项目中的体现**
* **`PrimarySchoolGenerator`** 提供了最基础的题目生成逻辑。
* **`JuniorHighSchoolGenerator`** 继承前者,它的 `generateSingleQuestion` 方法首先通过 `super.generateSingleQuestion()` 调用父类的“模板”来获得一个基础题目,然后在此基础上增加自己的特定步骤(添加平方或开根号)。
* **`SeniorHighSchoolGenerator`** 同理,它调用父类(初中生成器)的方法,在这个“半成品”上增加自己的步骤(添加三角函数)
- **实现**:
- `UserService.java``PaperService.java` 这两个类封装了核心的业务逻辑。
- 在各个 `Controller` 中,这些服务类都通过 `new` 关键字被实例化。然而,从整个应用程序的生命周期来看,每个服务在逻辑上都代表了一个单一、集中的功能单元(例如,管理所有用户或处理所有试卷生成请求)。
- 这种“服务层”的架构方式,使得业务逻辑被集中管理,避免了代码的重复,并为将来引入依赖注入框架(如 Spring 或 Guice打下了良好的基础
* **带来的好处**
* **代码复用**:避免了在每个生成器中都重复编写基础表达式的生成逻辑。
* **结构清晰**:清晰地定义了不同难度题目之间的层级递进关系。
## 4. 工厂方法模式 (Factory Method Pattern) - 隐式应用
在题目生成器的设计中,隐式地运用了工厂方法模式的思想。
通过综合运用这些设计模式和原则,我们的项目不仅实现了所有功能需求,更拥有了一个专业、健壮且易于扩展的软件架构。
- **描述**: 定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
- **实现**:
- **产品接口 (`QuestionGenerator.java`)**: 定义了所有生成器(工厂)必须创建的对象类型,即 `ChoiceQuestion`
- **具体产品**: `ChoiceQuestion` 对象本身。
- **具体工厂**: `PrimarySchoolGenerator.java`, `JuniorHighSchoolGenerator.java`, `SeniorHighSchoolGenerator.java` 等类,每个类都是一个具体的“工厂”,负责生产特定类型的“产品”(即不同难度的题目)。
- **决策者**: `MixedDifficultyStrategy` 在这里扮演了决策者的角色,它根据输入(难度级别)来决定调用哪一个具体的“工厂”来生产题目。
虽然没有一个显式的 `QuestionFactory` 类,但将“创建题目”的职责委托给一系列可替换的生成器类,这正是工厂方法模式的核心思想。
---

@ -21,25 +21,21 @@
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>org.mozilla</groupId>
<artifactId>rhino-engine</artifactId>
@ -54,32 +50,27 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<release>17</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.mathgenerator.MainApplication</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.mathgenerator.Launcher</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

@ -0,0 +1,13 @@
package com.mathgenerator;
/**
* JavaFX
* JavaFX fat JAR "unnamed module"
* JavaFX main
* JavaFX Application
*/
public class Launcher {
public static void main(String[] args) {
MainApplication.main(args);
}
}

@ -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
* <p>
* {@link Application} JavaFX
* (Stage) (LoginView.fxml)
*
*/
public class MainApplication extends Application {
/**
* JavaFX
* <p>
* 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
* <p>
* Java {@link #launch(String...)} JavaFX
*
* @param args
*/
public static void main(String[] args) {
launch(args);
}

@ -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) {

@ -43,44 +43,39 @@ public class LoginController {
*/
@FXML
private void handleLoginButtonAction(ActionEvent event) {
String username = usernameField.getText();
// 1. 获取输入,现在它可能是用户名或邮箱
String identifier = usernameField.getText();
String password = passwordField.getText();
// --- 2. 使用工具类进行校验 ---
if (!ValidationUtils.isUsernameValid(username)) {
statusLabel.setText("登录失败:用户名不能为空且不能包含空格。");
// 2. 基本的非空校验
if (identifier.isEmpty()) {
statusLabel.setText("用户名或邮箱不能为空!");
return;
}
if (password.isEmpty()) {
statusLabel.setText("用户名和密码不能为空!");
statusLabel.setText("密码不能为空!");
return;
}
Optional<User> userOptional = userService.login(username, password);
// 3. 调用更新后的 login 方法
Optional<User> userOptional = userService.login(identifier, password);
if (userOptional.isPresent()) {
statusLabel.setText("登录成功!");
// 登录成功,调用新方法跳转到主菜单
loadMainMenu(userOptional.get());
} else {
statusLabel.setText("登录失败:用户名或密码错误。");
// 4. (已修改) 更新错误提示
statusLabel.setText("登录失败:用户名/邮箱或密码错误。");
}
}
/**
*
* @param event
*/
// ... (该文件中的其他方法无需修改)
@FXML
private void handleRegisterButtonAction(ActionEvent event) {
loadScene("/com/mathgenerator/view/RegisterView.fxml");
}
/**
*
* @param user
*/
private void loadMainMenu(User user) {
try {
// 1. 加载 FXML 文件
@ -103,10 +98,6 @@ public class LoginController {
}
}
/**
*
* @param fxmlPath FXML
*/
private void loadScene(String fxmlPath) {
try {
Parent root = FXMLLoader.load(getClass().getResource(fxmlPath));

@ -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();

@ -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<ChoiceQuestion> questions;
private List<Integer> userAnswers = new ArrayList<>();
private int currentQuestionIndex = 0;
// --- FXML 控件 ---
@FXML private Label questionNumberLabel;
@FXML private ProgressBar progressBar;
@FXML private Label questionTextLabel;
@ -42,27 +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;
// 调用后端服务生成题目
this.questions = paperService.createPaper(user, questionCount, level);
paperService.savePaper(user.username(), this.questions);
displayCurrentQuestion();
}
/**
* (ABCD)
*
*
*/
private void displayCurrentQuestion() {
ChoiceQuestion currentQuestion = questions.get(currentQuestionIndex);
@ -72,47 +77,71 @@ public class QuizController {
questionTextLabel.setText(currentQuestion.questionText());
List<RadioButton> 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("完成答题");
} else {
submitButton.setText("下一题");
}
}
/**
*
*
*
*
* @param event
*/
@FXML
private void handleSubmitButtonAction(ActionEvent event) {
RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle();
if (selectedRadioButton == null) {
statusLabel.setText("请选择一个答案!");
updateStatusLabelStyle(true);
return;
}
// 记录用户答案的索引
updateStatusLabelStyle(false);
List<RadioButton> 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;
@ -123,22 +152,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();
}

@ -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,129 @@ 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);
// --- 核心修改:不再调用 register而是检查用户名和邮箱是否已存在 ---
if (userService.isUsernameOrEmailTaken(username, email)) {
showStatusMessage("注册失败:用户名或邮箱已被占用。", true);
return;
}
// 验证通过,导航到设置密码界面,并传递用户名和邮箱
showStatusMessage("验证成功!请设置您的密码。", true);
loadSetPasswordScene(username, email);
}
// 3. 根据结果更新UI或跳转
if (success) {
statusLabel.setText("注册成功!请设置您的密码。");
// 成功后,加载设置密码界面,并传递用户名
loadSetPasswordScene(username);
/**
*
*
* @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.setText("注册失败:用户名或邮箱已被占用。");
statusLabel.getStyleClass().removeAll("register-status-label-with-text");
if (!statusLabel.getStyleClass().contains("register-status-label")) {
statusLabel.getStyleClass().add("register-status-label");
}
}
}
/**
* ()
* ()
*
* @param username
* @param email
*/
private void loadSetPasswordScene(String username) {
private void loadSetPasswordScene(String username, String email) {
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, email);
Stage stage = (Stage) registerButton.getScene().getWindow();
stage.setScene(new Scene(root));
@ -96,11 +171,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 +195,13 @@ public class RegisterController {
e.printStackTrace();
}
}
/**
*
*/
public void cleanup() {
if (countdownTimeline != null) {
countdownTimeline.stop();
}
}
}

@ -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) {

@ -19,6 +19,7 @@ public class SetPasswordController {
private final UserService userService = new UserService();
private String username;
private String email; // 新增一个字段来存储邮箱
@FXML private Label promptLabel;
@FXML private PasswordField newPasswordField;
@ -27,48 +28,78 @@ public class SetPasswordController {
@FXML private Label statusLabel;
/**
*
* ()
*
* @param username
* @param email
*/
public void initData(String username) {
public void initData(String username, String email) {
this.username = username;
this.email = email; // 保存邮箱
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);
// --- 核心修改:调用新的服务方法,完成最终的注册和密码设置 ---
boolean success = userService.createUserWithPassword(this.username, this.email, newPassword);
if (success) {
statusLabel.setText("密码设置成功!正在进入主菜单...");
// 密码设置成功后,获取完整的用户信息并直接跳转到主菜单
showStatusMessage("注册成功!正在进入主菜单...", true);
// 注册成功后,直接查找这个新创建的用户并登录
userService.findUserByUsername(this.username).ifPresent(this::loadMainMenu);
} else {
statusLabel.setText("密码设置失败,请稍后重试或重新注册。");
// 这个分支理论上很难进入,除非在极短时间内有其他人注册了相同的用户名/邮箱
showStatusMessage("注册失败,用户名或邮箱可能刚刚被占用,请重新注册。", true);
}
}
// ... (文件中的 showStatusMessage, updateStatusLabelStyle, loadMainMenu 方法保持不变) ...
private void showStatusMessage(String message, boolean hasContent) {
statusLabel.setText(message);
updateStatusLabelStyle(hasContent);
}
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");
}
}
}
/**
* ()
*/
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));

@ -6,58 +6,56 @@ import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* ( - )
*
*
* <p>
* {@link PrimarySchoolGenerator}
*
*/
public class JuniorHighSchoolGenerator extends PrimarySchoolGenerator {
private static final int[] PERFECT_SQUARES = {1, 4, 9, 16, 25, 36, 49, 64, 81, 100};
/**
*
* <p>
*
*
* 退
*
* @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<String> 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<String> options = generateDecimalOptions(finalCorrectAnswer);
int correctIndex = options.indexOf(formatNumber(finalCorrectAnswer));

@ -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;
/**
*
* + - * / ()
*
* <p>
*
*
*
* @see QuestionGenerator
*/
public class PrimarySchoolGenerator implements QuestionGenerator {
@ -22,7 +26,11 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
/**
*
* @return ChoiceQuestion
* <p>
* 24
* {@link ChoiceQuestion}
*
* @return {@code ChoiceQuestion}
*/
@Override
public ChoiceQuestion generateSingleQuestion() {
@ -65,9 +73,13 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
}
/**
* (13)
* @param correctAnswer
* @return
*
* <p>
* 110
*
*
* @param correctAnswer
* @return
*/
protected List<String> generateOptions(int correctAnswer) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@ -88,28 +100,28 @@ public class PrimarySchoolGenerator implements QuestionGenerator {
}
/**
* 使JVM ()
* @param expression
* @return (IntegerDouble)
* @throws ScriptException
* 使JVM
* <p>
* 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
* <p>
*
*
* @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<String> 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;
}
/**
* 1100
*
* @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<String> 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, "(");
}
}

@ -1,14 +1,22 @@
package com.mathgenerator.generator;
import com.mathgenerator.model.ChoiceQuestion; // 导入新模型
import com.mathgenerator.model.ChoiceQuestion;
/**
*
*
* <p>
*
*
*/
public interface QuestionGenerator {
/**
*
* @return ChoiceQuestion
* <p>
*
* {@link ChoiceQuestion}
*
* @return {@code ChoiceQuestion}
*/
ChoiceQuestion generateSingleQuestion();
}

@ -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;
/**
*
*
* <p>
*
*
*
* @see QuestionGenerator
*/
public class SafePrimarySchoolGenerator implements QuestionGenerator {
/**
*
* @return ChoiceQuestion
*
* <p>
* {@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<String> parts, int value, int operandsUsed) {}
/**
*
*
* <p>
*
*
* @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)
* <p>
*
*
* @param operandsRemaining
* @return {@code Term}
*/
private Term generateTerm(int operandsRemaining) {
ThreadLocalRandom random = ThreadLocalRandom.current();
@ -91,7 +111,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
}
/**
*
*
* <p>
*
*
* @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<Integer> getDivisors(int number) {
List<Integer> divisors = new ArrayList<>();
for (int i = 1; i <= number; i++) {
@ -127,7 +158,12 @@ public class SafePrimarySchoolGenerator implements QuestionGenerator {
}
/**
* (13)
*
* <p>
*
*
* @param correctAnswer
* @return
*/
private List<String> 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));
}
}

@ -6,8 +6,10 @@ import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
/**
* ( - )
*
*
* <p>
* {@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]);
/**
*
* <p>
*
*
*
* @return {@link ChoiceQuestion}
*/
@Override
public ChoiceQuestion generateSingleQuestion() {
// 1. 先生成一个保证可计算的、高性能的初中选择题,作为基础

@ -3,10 +3,14 @@ package com.mathgenerator.model;
import java.util.List;
/**
*
* @param questionText
* @param options
* @param correctOptionIndex (0-3)
* (Record)
* <p>
* (Record)
*
*
* @param questionText
* @param options (List)
* @param correctOptionIndex {@code options} 0 3
*/
public record ChoiceQuestion(String questionText, List<String> options, int correctOptionIndex) {
}

@ -1,14 +1,29 @@
package com.mathgenerator.model;
/**
*
* (Enum)
* <p>
*
*
*/
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;

@ -1,10 +1,14 @@
package com.mathgenerator.model;
/**
* (Record)
* @param username
* @param email ()
* @param password
* (Record)
* <p>
* (Record)
*
*
* @param username
* @param email
* @param password {@code null}
*/
public record User(String username, String email, String password) {
}

@ -5,6 +5,13 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
*
* <p>
* {@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");
}

@ -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
*
* <p>
*
*
*/
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
*
* <p>
*
*
*
*
* @param user
* @param count
* @param currentLevel
* @return {@link ChoiceQuestion}
*/
public List<ChoiceQuestion> createPaper(User user, int count, Level currentLevel) {
// 查重集合现在存储题干字符串
Set<String> existingQuestionTexts = fileManager.loadExistingQuestions(user.username());
List<ChoiceQuestion> newPaper = new ArrayList<>();
Set<String> generatedInSession = new HashSet<>();
System.out.println("正在根据策略生成选择题,请稍候...");
//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,15 +63,16 @@ public class PaperService {
}
/**
*
* @param username
* @param paper
*
*
* @param username
* @param paper {@link ChoiceQuestion}
*/
public void savePaper(String username, List<ChoiceQuestion> paper) {
try {
String filePath = fileManager.savePaper(username, paper);
System.out.println("成功!" + paper.size() + "道数学题目已生成。");
System.out.println("文件已保存至: " + filePath);
//System.out.println("成功!" + paper.size() + "道数学题目已生成。");
//System.out.println("文件已保存至: " + filePath);
} catch (IOException e) {
System.err.println("错误:保存文件失败 - " + e.getMessage());
}

@ -1,10 +1,14 @@
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 com.mathgenerator.util.ValidationUtils;
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,14 +21,9 @@ 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;
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<String, User> userDatabase;
@ -34,16 +33,51 @@ public class UserService {
this.userDatabase = loadUsersFromFile();
}
// --- 新增方法:检查用户名或邮箱是否已被占用 ---
/**
*
* @param username
* @param email
* @return true
*/
public boolean isUsernameOrEmailTaken(String username, String email) {
if (userDatabase.containsKey(username)) {
return true; // 用户名已存在
}
return userDatabase.values().stream()
.anyMatch(u -> email.equals(u.email())); // 邮箱已存在
}
// --- 核心修改:创建一个全新的、原子化的注册方法 ---
/**
*
* @param username
* @param email
* @param password
* @return true
*/
public boolean createUserWithPassword(String username, String email, String password) {
// 双重检查,确保在并发场景下数据的一致性
if (isUsernameOrEmailTaken(username, email)) {
return false;
}
User newUser = new User(username, email, password);
userDatabase.put(username, newUser);
saveUsers();
return true;
}
// --- 旧的 register 和 setPassword 方法已被上面的新方法取代,可以删除 ---
// ... (文件中的其他方法,如 login, sendVerificationCode, changePassword 等保持不变) ...
private Map<String, User> loadUsersFromFile() {
// 如果文件不存在直接返回一个空的Map不再创建默认用户
if (!Files.exists(USER_FILE_PATH)) {
return new ConcurrentHashMap<>();
}
try (FileReader reader = new FileReader(USER_FILE_PATH.toFile())) {
Type type = new TypeToken<Map<String, User>>() {}.getType();
Map<String, User> loadedUsers = gson.fromJson(reader, type);
// 如果文件为空或格式错误也返回一个空的Map
return loadedUsers != null ? new ConcurrentHashMap<>(loadedUsers) : new ConcurrentHashMap<>();
} catch (IOException e) {
System.err.println("错误:加载用户文件失败 - " + e.getMessage());
@ -63,104 +97,44 @@ public class UserService {
return Optional.ofNullable(this.userDatabase.get(username));
}
public Optional<User> login(String username, String password) {
return findUserByUsername(username)
.filter(user -> user.password().equals(password));
public Optional<User> login(String identifier, String password) {
Optional<User> userOptional;
if (identifier.contains("@") && ValidationUtils.isEmailValid(identifier)) {
userOptional = this.userDatabase.values().stream()
.filter(user -> identifier.equals(user.email()))
.findFirst();
} else {
userOptional = findUserByUsername(identifier);
}
return userOptional.filter(user -> user.password() != null && user.password().equals(password));
}
/**
* ()
* @param email
* @return 6, 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; // 发送失败
}
}
/**
* ()
* @param username
* @param email
* @return true, 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()))) {
return false; // 邮箱已存在
return null;
}
// --- 核心修正在这里 ---
// 创建用户时,密码字段设为 null表示该用户处于“待设置密码”状态
User newUser = new User(username, email, null);
userDatabase.put(username, newUser);
saveUsers();
return true;
}
/**
* ()
* @param username
* @param password
* @return true, 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; // 用户已经有密码,不能通过此方法设置
}).orElse(false);
}
/**
*
* @param password
* @return true
*/
public static boolean isPasswordValid(String password) {
return password != null && PASSWORD_PATTERN.matcher(password).matches();
}
/**
*
* @return true
*/
public boolean changePassword(String username, String oldPassword, String newPassword) {
return findUserByUsername(username)
.filter(user -> user.password().equals(oldPassword))

@ -6,29 +6,39 @@ import java.util.concurrent.ThreadLocalRandom;
/**
*
* ()
* <p>
* 使
*
*
*/
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();
/**
*
* <p>
* - ****: 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;

@ -5,14 +5,17 @@ import com.mathgenerator.model.Level;
/**
*
*
* <p>
*
*
*/
public interface PaperStrategy {
/**
*
*
*
* @param mainLevel
* @return QuestionGenerator
* @param mainLevel ({@link Level})
* @return {@link QuestionGenerator}
*/
QuestionGenerator selectGenerator(Level mainLevel);
}

@ -1,57 +1,86 @@
package com.mathgenerator.storage;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.mathgenerator.model.ChoiceQuestion; // 导入新模型
import java.io.FileReader;
import java.io.FileWriter;
import com.mathgenerator.model.ChoiceQuestion;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* JSONChoiceQuestion
*
* <p>
*
* <ul>
* <li></li>
* <li></li>
* </ul>
* 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");
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
/**
* JSON
* @param username
* @param paperContent ChoiceQuestion
* @return
*
* <p>
* {@code .txt}
*
*
* @param username
* @param paperContent {@link ChoiceQuestion}
* @return
* @throws IOException I/O
*/
public String savePaper(String username, List<ChoiceQuestion> paperContent) throws IOException {
Path userDir = BASE_PATH.resolve(username);
Files.createDirectories(userDir);
String timestamp = LocalDateTime.now().format(FORMATTER);
String fileName = timestamp + ".json"; // 文件后缀改为 .json
String fileName = timestamp + ".txt";
Path filePath = userDir.resolve(fileName);
try (FileWriter writer = new FileWriter(filePath.toFile())) {
gson.toJson(paperContent, writer);
StringBuilder formattedContent = new StringBuilder();
formattedContent.append("数学试卷\n");
formattedContent.append("用户: ").append(username).append("\n");
formattedContent.append("生成时间: ").append(timestamp).append("\n");
formattedContent.append("========================================\n\n");
for (int i = 0; i < paperContent.size(); i++) {
ChoiceQuestion question = paperContent.get(i);
formattedContent.append(i + 1).append(". ").append(question.questionText()).append("\n");
char optionChar = 'A';
for (String option : question.options()) {
formattedContent.append(" ").append(optionChar++).append(". ").append(option).append("\n");
}
String correctAnswer = question.options().get(question.correctOptionIndex());
formattedContent.append(" [正确答案: ").append(correctAnswer).append("]\n\n");
}
Files.writeString(filePath, formattedContent.toString(), StandardOpenOption.CREATE);
return filePath.toString();
}
/**
*
* @param username
* @return Set
*
* <p>
* {@code .txt}
* Set
*
* @param username
* @return {@code Set<String>}
*/
public Set<String> loadExistingQuestions(String username) {
Path userDir = BASE_PATH.resolve(username);
@ -61,9 +90,8 @@ public class FileManager {
try (Stream<Path> stream = Files.walk(userDir)) {
return stream
.filter(file -> !Files.isDirectory(file) && file.toString().endsWith(".json")) // 只读取 .json 文件
.flatMap(this::readQuestionsFromFile) // 使用方法引用
.map(ChoiceQuestion::questionText) // 提取每个对象的题干文本
.filter(file -> !Files.isDirectory(file) && file.toString().endsWith(".txt"))
.flatMap(file -> readQuestionTextsFromTxtFile(file).stream())
.collect(Collectors.toSet());
} catch (IOException e) {
System.err.println("错误:读取历史文件失败 - " + e.getMessage());
@ -72,18 +100,25 @@ public class FileManager {
}
/**
* JSONChoiceQuestion
* @param file (Path)
* @return ChoiceQuestion (Stream)
*
* <p>
* 使 "数字."
*
* @param file {@link Path}
* @return (List)
*/
private Stream<ChoiceQuestion> readQuestionsFromFile(Path file) {
try (FileReader reader = new FileReader(file.toFile())) {
Type listType = new TypeToken<List<ChoiceQuestion>>() {}.getType();
List<ChoiceQuestion> questions = gson.fromJson(reader, listType);
return questions != null ? questions.stream() : Stream.empty();
private List<String> readQuestionTextsFromTxtFile(Path file) {
try (Stream<String> lines = Files.lines(file)) {
Pattern questionPattern = Pattern.compile("^\\d+\\.\\s+(.*)");
return lines
.map(String::trim)
.map(questionPattern::matcher)
.filter(Matcher::matches)
.map(matcher -> matcher.group(1).trim())
.collect(Collectors.toList());
} catch (IOException e) {
System.err.println("错误:读取或解析文件 " + file + " 失败 - " + e.getMessage());
return Stream.empty();
System.err.println("错误:读取文件 " + file + " 失败 - " + e.getMessage());
return Collections.emptyList();
}
}
}

@ -4,7 +4,10 @@ import java.util.regex.Pattern;
/**
*
*
* <p>
*
*
*
*/
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
* <p>
*
* <ul>
* <li></li>
* <li></li>
* </ul>
*
* @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
*
* <p>
*
* <ul>
* <li> 6 10 </li>
* <li></li>
* </ul>
*
* @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
*
* <p>
* 使 (, "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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

@ -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; /* 在标签下方和左侧留出一点空间 */
}

@ -7,25 +7,85 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="450.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.ChangePasswordController">
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0"
prefWidth="400.0" spacing="18.0" styleClass="password-background-new, root-container"
xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.mathgenerator.controller.ChangePasswordController"
stylesheets="@/com/mathgenerator/styles/styles.css">
<children>
<Label text="修改密码">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<PasswordField fx:id="oldPasswordField" maxWidth="300.0" promptText="当前密码" />
<PasswordField fx:id="newPasswordField" maxWidth="300.0" promptText="新密码 (6-10位, 含大小写字母和数字)" />
<PasswordField fx:id="confirmNewPasswordField" maxWidth="300.0" promptText="确认新密码" />
<Button fx:id="confirmButton" mnemonicParsing="false" onAction="#handleConfirmAction" prefWidth="120.0" text="确认修改" />
<Button fx:id="backButton" mnemonicParsing="false" onAction="#handleBackAction" style="-fx-background-color: #6c757d;" text="返回主菜单" textFill="WHITE" />
<Label fx:id="statusLabel" textFill="RED" wrapText="true">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
<VBox alignment="CENTER" spacing="12.0" styleClass="password-glass-panel"
prefWidth="360.0" prefHeight="500.0">
<children>
<Label text="修改密码" styleClass="password-title">
<font>
<Font name="System Bold" size="26.0" />
</font>
<VBox.margin>
<Insets bottom="5.0" top="10.0" />
</VBox.margin>
</Label>
<Label text="请确保密码安全,包含大小写字母和数字" styleClass="password-security-tip"
maxWidth="300.0" alignment="CENTER" wrapText="true" />
<VBox maxWidth="300.0" spacing="4.0">
<children>
<Label text="当前密码" styleClass="form-label"/>
<PasswordField fx:id="oldPasswordField" styleClass="password-textfield"/>
</children>
</VBox>
<VBox maxWidth="300.0" spacing="4.0">
<children>
<Label text="新密码 (6-10位)" styleClass="form-label"/>
<PasswordField fx:id="newPasswordField" styleClass="password-textfield"/>
</children>
</VBox>
<VBox maxWidth="300.0" spacing="4.0">
<children>
<Label text="确认新密码" styleClass="form-label"/>
<PasswordField fx:id="confirmNewPasswordField" styleClass="password-textfield"/>
</children>
</VBox>
<VBox alignment="CENTER" spacing="10.0">
<children>
<Button fx:id="confirmButton" mnemonicParsing="false"
onAction="#handleConfirmAction"
text="确认修改" styleClass="password-primary-button" />
<Button fx:id="backButton" mnemonicParsing="false"
onAction="#handleBackAction"
text="返回主菜单" styleClass="password-secondary-button" />
</children>
<VBox.margin>
<Insets top="12.0" />
</VBox.margin>
</VBox>
<Label fx:id="statusLabel" styleClass="password-status-label"
maxWidth="300.0" alignment="CENTER" text=""
wrapText="true"
minHeight="60.0" prefHeight="60.0">
<VBox.margin>
<Insets top="8.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="30.0" left="30.0" right="30.0" top="25.0" />
</padding>
</VBox>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
<Insets bottom="25.0" left="25.0" right="25.0" top="25.0" />
</padding>
</VBox>

@ -8,24 +8,70 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="400.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.LoginController">
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0"
prefWidth="400.0" spacing="20.0" styleClass="login-background, root-container"
xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.mathgenerator.controller.LoginController"
stylesheets="@/com/mathgenerator/styles/styles.css">
<children>
<Label text="用户登录">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<TextField fx:id="usernameField" maxWidth="250.0" promptText="用户名" />
<PasswordField fx:id="passwordField" maxWidth="250.0" promptText="密码" />
<Button fx:id="loginButton" mnemonicParsing="false" onAction="#handleLoginButtonAction" prefWidth="100.0" text="登录" />
<Button fx:id="registerButton" mnemonicParsing="false" onAction="#handleRegisterButtonAction" style="-fx-background-color: #6c757d;" text="注册新用户" textFill="WHITE" />
<Label fx:id="statusLabel" textFill="RED">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
<VBox alignment="CENTER" spacing="15.0" styleClass="login-glass-panel">
<children>
<Label text="用户登录" styleClass="login-title">
<font>
<Font name="System Bold" size="26.0" />
</font>
<VBox.margin>
<Insets bottom="10.0" />
</VBox.margin>
</Label>
<VBox maxWidth="280.0" spacing="4.0">
<children>
<Label text="用户名或邮箱" styleClass="form-label" />
<TextField fx:id="usernameField" styleClass="login-textfield" />
</children>
</VBox>
<VBox maxWidth="280.0" spacing="4.0">
<children>
<Label text="密码" styleClass="form-label" />
<PasswordField fx:id="passwordField" styleClass="login-textfield" />
</children>
</VBox>
<VBox alignment="CENTER" spacing="12.0">
<children>
<Button fx:id="loginButton" mnemonicParsing="false"
onAction="#handleLoginButtonAction" prefWidth="140.0"
text="登录" styleClass="login-primary-button" />
<Button fx:id="registerButton" mnemonicParsing="false"
onAction="#handleRegisterButtonAction" prefWidth="140.0"
text="注册新用户" styleClass="login-secondary-button" />
</children>
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</VBox>
<Label fx:id="statusLabel" styleClass="login-status-label" text="欢迎使用数学学习软件">
<VBox.margin>
<Insets top="15.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="35.0" left="35.0" right="35.0" top="35.0" />
</padding>
</VBox>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
<Insets bottom="30.0" left="30.0" right="30.0" top="30.0" />
</padding>
</VBox>

@ -8,43 +8,98 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="500.0" spacing="20.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.MainMenuController">
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0"
prefWidth="400.0" spacing="20.0" styleClass="mainmenu-background-new, root-container"
xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.mathgenerator.controller.MainMenuController"
stylesheets="@/com/mathgenerator/styles/styles.css">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
<Insets bottom="25.0" left="25.0" right="25.0" top="25.0" />
</padding>
<children>
<Label fx:id="welcomeLabel" text="欢迎, [用户名]!">
<!-- 欢迎标题 - 修复长用户名显示 -->
<Label fx:id="welcomeLabel" text="欢迎, [用户名]!" styleClass="mainmenu-welcome-title"
maxWidth="360.0" alignment="CENTER" wrapText="true">
<font>
<Font name="System Bold" size="28.0" />
<Font name="System Bold" size="24.0" />
</font>
<VBox.margin>
<Insets bottom="15.0" />
</VBox.margin>
</Label>
<VBox alignment="CENTER" spacing="15.0" VBox.vgrow="ALWAYS">
<!-- 主要内容区域 -->
<VBox alignment="CENTER" spacing="20.0" VBox.vgrow="ALWAYS"
styleClass="mainmenu-glass-panel" prefWidth="340.0">
<children>
<Label text="请选择题目难度">
<!-- 难度选择标签 -->
<Label text="请选择题目难度" styleClass="mainmenu-difficulty-label">
<font>
<Font size="18.0" />
<Font size="20.0" />
</font>
<VBox.margin>
<Insets bottom="12.0" />
</VBox.margin>
</Label>
<Button fx:id="primaryButton" mnemonicParsing="false" onAction="#handlePrimaryAction" prefHeight="50.0" prefWidth="200.0" text="小学" />
<Button fx:id="juniorHighButton" mnemonicParsing="false" onAction="#handleJuniorHighAction" prefHeight="50.0" prefWidth="200.0" text="初中" />
<Button fx:id="seniorHighButton" mnemonicParsing="false" onAction="#handleSeniorHighAction" prefHeight="50.0" prefWidth="200.0" text="高中" />
<HBox alignment="CENTER" spacing="10.0">
<!-- 题目数量设置 -->
<VBox alignment="CENTER" spacing="8.0">
<children>
<Label text="题目数量:" />
<TextField fx:id="questionCountField" prefWidth="80.0" text="10" />
<HBox alignment="CENTER" spacing="10.0">
<children>
<Label text="题目数量:" styleClass="mainmenu-count-label" />
<TextField fx:id="questionCountField" prefWidth="80.0"
text="10" styleClass="mainmenu-count-textfield" />
</children>
</HBox>
</children>
<VBox.margin>
<Insets top="20.0" />
<Insets bottom="12.0" />
</VBox.margin>
</HBox>
<Label fx:id="statusLabel" textFill="RED" />
</VBox>
<!-- 难度选择按钮 -->
<VBox alignment="CENTER" spacing="15.0">
<children>
<Button fx:id="primaryButton" mnemonicParsing="false"
onAction="#handlePrimaryAction" prefHeight="55.0"
prefWidth="200.0" text="小学" styleClass="mainmenu-difficulty-button" />
<Button fx:id="juniorHighButton" mnemonicParsing="false"
onAction="#handleJuniorHighAction" prefHeight="55.0"
prefWidth="200.0" text="初中" styleClass="mainmenu-difficulty-button" />
<Button fx:id="seniorHighButton" mnemonicParsing="false"
onAction="#handleSeniorHighAction" prefHeight="55.0"
prefWidth="200.0" text="高中" styleClass="mainmenu-difficulty-button" />
</children>
</VBox>
<!-- 状态标签 -->
<Label fx:id="statusLabel" styleClass="mainmenu-status-label"
maxWidth="280.0" alignment="CENTER" text="" />
</children>
<padding>
<Insets bottom="25.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>
<HBox alignment="CENTER_RIGHT" spacing="10.0">
<!-- 底部功能按钮 -->
<HBox alignment="CENTER" spacing="12.0" prefHeight="60.0">
<children>
<Button fx:id="changePasswordButton" mnemonicParsing="false" onAction="#handleChangePasswordAction" text="修改密码" />
<Button fx:id="logoutButton" mnemonicParsing="false" onAction="#handleLogoutAction" style="-fx-background-color: #dc3545;" text="退出登录" textFill="WHITE" />
<Button fx:id="changePasswordButton" mnemonicParsing="false"
onAction="#handleChangePasswordAction"
text="修改密码" styleClass="mainmenu-function-button" />
<Button fx:id="logoutButton" mnemonicParsing="false"
onAction="#handleLogoutAction"
text="退出登录" styleClass="mainmenu-logout-button" />
</children>
<VBox.margin>
<Insets top="20.0" bottom="5.0" />
</VBox.margin>
</HBox>
</children>
</VBox>

@ -9,60 +9,96 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="500.0" prefWidth="600.0" spacing="20.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.QuizController">
<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0"
prefWidth="400.0" spacing="12.0" styleClass="quiz-background-new, root-container"
xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.mathgenerator.controller.QuizController"
stylesheets="@/com/mathgenerator/styles/styles.css">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<children>
<Label fx:id="questionNumberLabel" text="第 1 / 10 题">
<!-- 题目进度 -->
<Label fx:id="questionNumberLabel" text="第 1 / 10 题" styleClass="quiz-title-new"
maxWidth="360.0" alignment="CENTER">
<font>
<Font size="18.0" />
<Font size="16.0" />
</font>
</Label>
<ProgressBar fx:id="progressBar" prefWidth="560.0" progress="0.1" />
<Label fx:id="questionTextLabel" style="-fx-font-weight: bold;" text="题目内容: 1 + 1 = ?" wrapText="true">
<!-- 进度条 -->
<ProgressBar fx:id="progressBar" prefWidth="360.0" progress="0.1"
styleClass="quiz-progress-bar-new" />
<!-- 题目内容 -->
<Label fx:id="questionTextLabel" text="题目内容: 1 + 1 = ?"
styleClass="quiz-question-text" wrapText="true"
maxWidth="360.0" alignment="CENTER">
<font>
<Font size="24.0" />
<Font size="18.0" />
</font>
<VBox.margin>
<Insets top="20.0" />
<Insets top="15.0" />
</VBox.margin>
</Label>
<VBox fx:id="optionsVBox" alignment="CENTER_LEFT" spacing="15.0">
<!-- 选项容器 -->
<VBox fx:id="optionsVBox" alignment="CENTER_LEFT" spacing="10.0"
styleClass="quiz-options-container" prefWidth="360.0">
<children>
<RadioButton fx:id="option1" mnemonicParsing="false" text="选项1">
<RadioButton fx:id="option1" mnemonicParsing="false" text="A. 选项1"
styleClass="quiz-radio-button-new">
<toggleGroup>
<ToggleGroup fx:id="optionsGroup" />
</toggleGroup>
<font>
<Font size="16.0" />
<Font size="14.0" />
</font>
</RadioButton>
<RadioButton fx:id="option2" mnemonicParsing="false" text="选项2" toggleGroup="$optionsGroup">
<RadioButton fx:id="option2" mnemonicParsing="false" text="B. 选项2"
toggleGroup="$optionsGroup" styleClass="quiz-radio-button-new">
<font>
<Font size="16.0" />
<Font size="14.0" />
</font>
</RadioButton>
<RadioButton fx:id="option3" mnemonicParsing="false" text="选项3" toggleGroup="$optionsGroup">
<RadioButton fx:id="option3" mnemonicParsing="false" text="C. 选项3"
toggleGroup="$optionsGroup" styleClass="quiz-radio-button-new">
<font>
<Font size="16.0" />
<Font size="14.0" />
</font>
</RadioButton>
<RadioButton fx:id="option4" mnemonicParsing="false" text="选项4" toggleGroup="$optionsGroup">
<RadioButton fx:id="option4" mnemonicParsing="false" text="D. 选项4"
toggleGroup="$optionsGroup" styleClass="quiz-radio-button-new">
<font>
<Font size="16.0" />
<Font size="14.0" />
</font>
</RadioButton>
</children>
<VBox.margin>
<Insets left="50.0" top="20.0" />
<Insets top="15.0" />
</VBox.margin>
</VBox>
<Button fx:id="submitButton" mnemonicParsing="false" onAction="#handleSubmitButtonAction" prefHeight="40.0" prefWidth="150.0" text="提交答案">
<!-- 提交按钮 - 改为下一题 -->
<Button fx:id="submitButton" mnemonicParsing="false"
onAction="#handleSubmitButtonAction"
text="下一题" styleClass="quiz-submit-button">
<VBox.margin>
<Insets top="30.0" />
<Insets top="20.0" />
</VBox.margin>
</Button>
<Label fx:id="statusLabel" textFill="RED" />
<!-- 状态标签 - 预留固定空间 -->
<Label fx:id="statusLabel" styleClass="quiz-status-label-empty"
maxWidth="360.0" alignment="CENTER" text=""
minHeight="35.0" prefHeight="35.0" maxHeight="35.0">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
</children>
</VBox>

@ -3,36 +3,98 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="500.0" prefWidth="450.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.RegisterController">
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0"
prefWidth="400.0" spacing="20.0" styleClass="register-background-new, root-container"
xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.mathgenerator.controller.RegisterController"
stylesheets="@/com/mathgenerator/styles/styles.css">
<children>
<Label text="新用户注册">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<TextField fx:id="usernameField" maxWidth="300.0" promptText="用户名" />
<TextField fx:id="emailField" maxWidth="300.0" promptText="邮箱地址" />
<HBox alignment="CENTER" maxWidth="300.0" spacing="10.0">
<VBox alignment="CENTER" spacing="15.0" styleClass="register-glass-panel"
prefWidth="360.0" prefHeight="500.0">
<children>
<TextField fx:id="verificationCodeField" promptText="邮箱验证码" HBox.hgrow="ALWAYS" />
<Button fx:id="sendCodeButton" mnemonicParsing="false" onAction="#handleSendCodeAction" text="发送验证码" />
<Label text="新用户注册" styleClass="register-title">
<font>
<Font name="System Bold" size="26.0" />
</font>
<VBox.margin>
<Insets bottom="10.0" />
</VBox.margin>
</Label>
<VBox maxWidth="320.0" spacing="4.0">
<children>
<Label text="用户名" styleClass="form-label"/>
<TextField fx:id="usernameField" styleClass="register-textfield" />
</children>
</VBox>
<VBox maxWidth="320.0" spacing="4.0">
<children>
<Label text="邮箱地址" styleClass="form-label"/>
<TextField fx:id="emailField" styleClass="register-textfield" />
</children>
</VBox>
<VBox spacing="8.0" maxWidth="320.0">
<children>
<Label text="请输入邮箱验证码:" styleClass="register-security-tip" />
<HBox alignment="CENTER" spacing="10.0" styleClass="register-code-container">
<children>
<TextField fx:id="verificationCodeField"
promptText="验证码"
styleClass="register-textfield"
HBox.hgrow="ALWAYS" />
<Button fx:id="sendCodeButton" mnemonicParsing="false"
onAction="#handleSendCodeAction"
text="发送验证码"
styleClass="register-code-button" />
</children>
</HBox>
</children>
</VBox>
<VBox alignment="CENTER" spacing="12.0">
<children>
<!-- 修改为"前往设置密码" -->
<Button fx:id="registerButton" mnemonicParsing="false"
onAction="#handleRegisterAction" prefWidth="150.0"
text="前往设置密码" styleClass="register-primary-button" />
<Button fx:id="backToLoginButton" mnemonicParsing="false"
onAction="#handleBackToLoginAction" prefWidth="150.0"
text="返回登录" styleClass="register-secondary-button" />
</children>
<VBox.margin>
<Insets top="15.0" />
</VBox.margin>
</VBox>
<Label fx:id="statusLabel" styleClass="register-status-label"
maxWidth="280.0" alignment="CENTER" text=""
wrapText="true"
minHeight="70.0" prefHeight="70.0">
<VBox.margin>
<Insets top="8.0" />
</VBox.margin>
</Label>
</children>
</HBox>
<Button fx:id="registerButton" mnemonicParsing="false" onAction="#handleRegisterAction" prefWidth="120.0" text="确认注册" />
<Button fx:id="backToLoginButton" mnemonicParsing="false" onAction="#handleBackToLoginAction" style="-fx-background-color: #6c757d;" text="返回登录" textFill="WHITE" />
<Label fx:id="statusLabel" textFill="RED" wrapText="true">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
<padding>
<Insets bottom="30.0" left="40.0" right="40.0" top="30.0" />
</padding>
</VBox>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
<Insets bottom="40.0" left="40.0" right="40.0" top="40.0" />
</padding>
</VBox>

@ -6,38 +6,80 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="500.0" spacing="25.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.ScoreController">
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0"
prefWidth="400.0" spacing="20.0" styleClass="score-background-new, root-container"
xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.mathgenerator.controller.ScoreController"
stylesheets="@/com/mathgenerator/styles/styles.css">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
<Insets bottom="25.0" left="25.0" right="25.0" top="25.0" />
</padding>
<children>
<Label text="答题完成!">
<font>
<Font name="System Bold" size="36.0" />
</font>
</Label>
<VBox alignment="CENTER" spacing="10.0">
<!-- 主要内容区域 -->
<VBox alignment="CENTER" spacing="20.0" styleClass="score-glass-panel-new"
prefWidth="350.0" prefHeight="500.0">
<children>
<Label text="您的最终得分是:">
<!-- 完成标题 -->
<Label text="答题完成!" styleClass="score-complete-title-new"
wrapText="true" maxWidth="300.0">
<font>
<Font size="18.0" />
<Font name="System Bold" size="32.0" />
</font>
<VBox.margin>
<Insets bottom="5.0" />
</VBox.margin>
</Label>
<Label fx:id="scoreLabel" text="100.00" textFill="#007bff">
<font>
<Font name="System Bold" size="64.0" />
</font>
<!-- 得分显示区域 -->
<VBox alignment="CENTER" spacing="10.0">
<children>
<Label text="您的最终得分是:" styleClass="score-description-new"
maxWidth="300.0" alignment="CENTER">
<font>
<Font size="18.0" />
</font>
</Label>
<Label fx:id="scoreLabel" text="100.00"
styleClass="score-label-new" maxWidth="300.0"
alignment="CENTER">
<font>
<Font name="System Bold" size="60.0" />
</font>
</Label>
</children>
</VBox>
<!-- 结果消息 - 完全修复文字换行 -->
<Label fx:id="resultMessageLabel"
styleClass="result-message-new"
wrapText="true"
maxWidth="300.0"
alignment="CENTER"
text="太棒了!你答对了所有题目!">
<VBox.margin>
<Insets top="5.0" />
</VBox.margin>
</Label>
</children>
</VBox>
<Label fx:id="resultMessageLabel" text="太棒了!你答对了所有题目!" />
<VBox alignment="CENTER" spacing="15.0">
<VBox.margin>
<Insets top="20.0" />
</VBox.margin>
<children>
<Button fx:id="tryAgainButton" mnemonicParsing="false" onAction="#handleTryAgainAction" prefWidth="150.0" text="再做一组" />
<Button fx:id="logoutButton" mnemonicParsing="false" onAction="#handleLogoutAction" style="-fx-background-color: #6c757d;" text="退出登录" textFill="WHITE" />
<!-- 按钮区域 -->
<VBox alignment="CENTER" spacing="12.0">
<VBox.margin>
<Insets top="15.0" />
</VBox.margin>
<children>
<Button fx:id="tryAgainButton" mnemonicParsing="false"
onAction="#handleTryAgainAction"
text="再做一组" styleClass="celebrate-button-new" />
<Button fx:id="logoutButton" mnemonicParsing="false"
onAction="#handleLogoutAction"
text="退出登录" styleClass="score-logout-button-new" />
</children>
</VBox>
</children>
</VBox>
</children>

@ -7,23 +7,66 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="450.0" spacing="15.0" style="-fx-background-color: #f4f4f4;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.mathgenerator.controller.SetPasswordController">
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0"
prefWidth="400.0" spacing="15.0" styleClass="setpassword-background, root-container"
xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.mathgenerator.controller.SetPasswordController"
stylesheets="@/com/mathgenerator/styles/styles.css">
<children>
<Label text="设置您的初始密码">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
<Label fx:id="promptLabel" text="为您的账户 [用户名] 设置密码" />
<PasswordField fx:id="newPasswordField" maxWidth="300.0" promptText="新密码 (6-10位, 含大小写字母和数字)" />
<PasswordField fx:id="confirmPasswordField" maxWidth="300.0" promptText="确认新密码" />
<Button fx:id="confirmButton" mnemonicParsing="false" onAction="#handleConfirmAction" prefWidth="120.0" text="确认并进入" />
<Label fx:id="statusLabel" textFill="RED" wrapText="true">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Label>
<VBox alignment="CENTER" spacing="15.0" styleClass="setpassword-glass-panel"
prefWidth="360.0" prefHeight="480.0">
<children>
<Label text="设置初始密码" styleClass="setpassword-title">
<font>
<Font name="System Bold" size="26.0" />
</font>
<VBox.margin>
<Insets bottom="5.0" />
</VBox.margin>
</Label>
<Label fx:id="promptLabel" text="为您的账户 [用户名] 设置密码"
styleClass="setpassword-prompt" maxWidth="280.0" alignment="CENTER" wrapText="true" />
<VBox maxWidth="280.0" spacing="4.0">
<children>
<Label text="新密码 (6-10位, 含大小写字母和数字)" styleClass="form-label" wrapText="true"/>
<PasswordField fx:id="newPasswordField" styleClass="setpassword-textfield" />
</children>
</VBox>
<VBox maxWidth="280.0" spacing="4.0">
<children>
<Label text="确认新密码" styleClass="form-label" />
<PasswordField fx:id="confirmPasswordField" styleClass="setpassword-textfield" />
</children>
</VBox>
<Button fx:id="confirmButton" mnemonicParsing="false"
onAction="#handleConfirmAction"
text="确认并进入" styleClass="setpassword-primary-button" />
<Label fx:id="statusLabel" styleClass="setpassword-status-label"
maxWidth="280.0" alignment="CENTER" text=""
wrapText="true"
minHeight="70.0" prefHeight="70.0" maxHeight="70.0">
<VBox.margin>
<Insets top="8.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="25.0" left="30.0" right="30.0" top="25.0" />
</padding>
</VBox>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>

Loading…
Cancel
Save