Compare commits

...

20 Commits

@ -5,4 +5,4 @@
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
/dataSources.local.xml

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/MathEaxmApp.iml" filepath="$PROJECT_DIR$/MathEaxmApp.iml" />
<module fileurl="file://$PROJECT_DIR$/Y2.3.iml" filepath="$PROJECT_DIR$/Y2.3.iml" />
</modules>
</component>
</project>

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="f14627aa-6999-456f-900a-7580301e6797" name="更改" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="33sLYnV1Yrm0gvXoLXrKZburltC" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.ShowReadmeOnStart": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Administrator/Desktop/R/Y2.3/lib/javax.mail-1.6.2.jar",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"project.structure.last.edited": "模块",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.2",
"vue.rearranger.settings.migration": "true",
"应用程序.Main.executor": "Run"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9f38398b9061-39b83d9b5494-intellij.indexing.shared.core-IU-241.17011.79" />
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-IU-241.17011.79" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="应用程序级" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="f14627aa-6999-456f-900a-7580301e6797" name="更改" comment="" />
<created>1760100882302</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1760100882302</updated>
<workItem from="1760100883365" duration="17000" />
<workItem from="1760101061217" duration="763000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

@ -0,0 +1,227 @@
# 数学题目生成器 (图形化界面版) 项目说明文档
## 1. 项目概述
该项目是一个功能完善的、拥有现代化图形用户界面GUI的桌面应用。它旨在为不同年级小学、初中、高中的学生提供一个集用户管理、题目练习、自动评分和历史存档于一体的综合性数学学习平台。用户可以通过友好的界面进行注册、登录、选择难度、进行交互式答题并通过真实的电子邮件接收验证码确保了账户的安全性。
## 2. 项目功能
### 2.1 用户管理
- **用户注册**:提供邮箱和自定义用户名进行注册。系统会向用户提供的邮箱发送一封包含**真实6位验证码**的邮件,用户需凭此验证码完成注册。
- **密码安全**支持用户设置符合强度要求6-10位含大小写字母和数字的密码并在登录状态下安全地修改密码。
- **双模式登录**:系统支持用户使用**邮箱**或**用户名**两种方式配合密码进行登录,更加便捷。
### 2.2 题目与考试
- **智能题目生成**根据用户选择的年级动态生成不同复杂度的题目。题目包含2-5个操作数并能智能地添加有意义的单层或双重括号。
- **交互式答题**:题目以选择题形式在界面中逐题展示,用户可通过鼠标点击进行选择。
- **双向导航**:在答题过程中,用户可以使用“上一题”、“下一题”按钮在题目之间自由切换,检查并修改之前的答案。
- **提前交卷**:用户可在答题未完成时选择提前交卷,系统会进行确认提醒,并根据已完成的题目计分。
- **自动评分与存档**:答题结束后,系统会立即计算得分(百分制)并展示。同时,每一次生成的完整试卷(包含题目、选项、正确答案)都会自动保存到以该用户命名的专属文件夹中,便于后续复习。
### 2.3 用户界面
- **现代化图形界面**:基于 `Java Swing` 和 `FlatLaf` UI库构建提供了一个美观、清爽、响应流畅的桌面应用体验。
- **多视图切换**:拥有登录、注册、密码设置、主菜单、考试、分数等多个独立视图,流程清晰,操作直观。
- **人性化交互**:答题界面提供实时进度条,导航按钮状态会根据当前题号自动更新,为用户提供清晰的操作指引。
## 3. 系统架构
项目采用业界标准的 **MVC (Model-View-Controller)** 设计模式,确保了代码的高度解耦、可维护性和可扩展性。
### 3.1 模型 (Model)
负责处理数据和业务逻辑,完全独立于界面。
- **`model` 包**: 定义核心数据结构,如 `User`, `Question`。
- **`service` 包**: 实现所有后台服务逻辑,如 `UserManager` (用户管理)、`ExamManager` (考试管理) 和 `EmailService` (邮件发送服务)。
### 3.2 视图 (View)
负责渲染用户界面,是用户直接交互的层面。
- **`view` 包**: 包含了所有的UI界面类如 `LoginView`, `ExamView`, `MainMenuView` 等。这些类只负责“显示”,不包含任何业务逻辑。
### 3.3 控制器 (Controller)
作为模型和视图之间的桥梁,调度整个应用程序。
- **`controller` 包**: 核心类 `AppController` 在此包中。它接收视图层传来的用户操作,调用模型层处理,并根据结果更新视图层的显示。
## 4. 代码详解
### 4.1 题目生成规则
所有生成器都经过重构能够生成包含2-5个操作数及有意义括号的复杂表达式。
#### 4.1.1 `PrimaryQuestionGenerator`
为小学用户生成复杂的四则运算题目,智能地使用括号改变运算优先级。
```
((10 + 5) × 3) - 12
```
#### 4.1.2 `JuniorQuestionGenerator`
在复杂表达式中,确保至少包含一个平方 (`^2`) 或开根号 (`√`) 运算。
```
(8 + √ (49)) × 3^2
```
#### 4.1.3 `SeniorQuestionGenerator`
在复杂表达式中,确保至少包含一个三角函数 (`sin`, `cos`, `tan`),并已修复 `tan(90°)` 的bug。
```
5 × (sin(30°) + 4)
```
### 4.2 题目格式与计分
- 所有题目均为四选一的选择题。
- 系统会智能判断正确答案是整数还是小数,并生成逻辑相符的干扰选项。
- 计分方式为 `(答对题数 / 总题数) * 100`,结果四舍五入取整。
## 5. 运行说明
1. **启动程序** 运行 `Main.java` 类,启动图形化登录窗口。
2. **用户注册/登录**
- 新用户点击“注册”,按照“输入邮箱/用户名 -> 发送验证邮件 -> 查收邮件验证码 -> 设置密码”的流程完成注册。
- 老用户可使用邮箱或用户名直接登录。
3. **开始考试** 在主菜单选择题目难度和数量,点击“开始答题”进入考试界面。
4. **答题过程** 通过“上一题”、“下一题”进行导航,随时可通过“交卷并评分”按钮结束考试。
5. **查看分数** 交卷后,分数界面会显示本次得分和答题详情。用户可选择“再来一组”或“退出登录”。
## 6. 代码结构
```
src/
├── Main.java
├── config.properties
├── controller/
│ └── AppController.java
├── model/
│ ├── Question.java
│ ├── User.java
│ └── UserType.java
├── service/
│ ├── EmailService.java
│ ├── ExamManager.java
│ ├── ExpressionEvaluator.java
│ ├── UserManager.java
│ ├── ValidationService.java
│ └── generator/
│ └── ... (5个题目生成器相关文件)
└── view/
└── ... (8个界面视图相关文件)
```
## 7. 依赖与配置
### 软件依赖 (JAR文件)
本项目需要以下4个外部库文件以下依赖库以添加至总项目lib文件夹中
- **FlatLaf UI主题**: `flatlaf-3.4.1.jar`
- **Jakarta Mail API**: `jakarta.mail-api-2.1.2.jar`
- **Angus Mail (实现)**: `angus-mail-2.0.2.jar`
- **Angus Activation (依赖)**: `angus-activation-2.0.2.jar`
### Windows 系统下运行步骤
1. **配置 Java 22 编译环境**
确保系统已安装 Java 22 JDK 版本
2. **设置控制台编码**
打开 cmd 命令行工具,使用以下指令执行 .jar 文件:
```
java -jar .\MathExamApp.jar
```
### Linux 系统下运行步骤
1. **配置 Java 22 JDK 版本**
确保系统已安装 Java 22 JDK
2. **运行程序**
使用以下指令执行程序:
java -jar MathExamApp.jar
## 8. 注意事项
1. **数据文件夹**:程序第一次成功注册用户或生成试卷后,会在程序运行的根目录下自动创建以下文件夹:
- `data/`:此文件夹包含 `users.txt` 文件,用于存储所有用户的账号和密码信息。
- `exams/`:此文件夹下会以每个用户的邮箱/用户名创建子文件夹,用于存放该用户所有历史试卷的 `.txt` 文件。
2. **请勿删除**:上述两个文件夹是程序正常运行和数据持久化的关键。**请不要手动删除或修改这些文件夹及其中的内容**,否则将导致所有用户账号信息和历史试卷记录丢失,且无法恢复。

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

@ -0,0 +1,316 @@
package controller;
import model.*;
import service.*;
import view.*;
import javax.swing.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class AppController {
// 模型和服务
private final UserManager userManager;
private final ExamManager examManager;
private final EmailService emailService;
// 视图
private final MainFrame mainFrame;
private final LoginView loginView;
private final RegisterView registerView;
private final PasswordSetView passwordSetView;
private final MainMenuView mainMenuView;
private final ExamView examView;
private final ScoreView scoreView;
// 应用状态
private User currentUser;
private String emailForRegistration;
private String usernameForRegistration;
private final Map<String, String> registrationCodes = new HashMap<>();
private List<Question> currentExam;
private int currentQuestionIndex;
private int score;
private int[] userAnswers; // 新增:用于存储用户每一题的答案
public AppController() {
this.mainFrame = new MainFrame();
this.userManager = new UserManager();
this.examManager = new ExamManager();
this.emailService = new EmailService();
this.loginView = new LoginView();
this.registerView = new RegisterView();
this.passwordSetView = new PasswordSetView();
this.mainMenuView = new MainMenuView();
this.examView = new ExamView();
this.scoreView = new ScoreView();
mainFrame.addPanel(loginView, "LOGIN");
mainFrame.addPanel(registerView, "REGISTER");
mainFrame.addPanel(passwordSetView, "PASSWORD_SET");
mainFrame.addPanel(mainMenuView, "MAIN_MENU");
mainFrame.addPanel(examView, "EXAM");
mainFrame.addPanel(scoreView, "SCORE");
attachListeners();
mainFrame.showPanel("LOGIN");
}
/**
* UI
*/
private void attachListeners() {
// --- 登录逻辑 ---
loginView.getLoginButton().addActionListener(e -> handleLogin());
loginView.getRegisterButton().addActionListener(e -> mainFrame.showPanel("REGISTER"));
// --- 注册逻辑 ---
registerView.getGetCodeButton().addActionListener(e -> handleGetCode());
registerView.getRegisterButton().addActionListener(e -> handleVerifyCode());
registerView.getBackButton().addActionListener(e -> mainFrame.showPanel("LOGIN"));
// --- 密码设置逻辑 ---
passwordSetView.getSubmitButton().addActionListener(e -> handleSetPassword());
// --- 主菜单逻辑 ---
mainMenuView.getStartExamButton().addActionListener(e -> handleStartExam());
mainMenuView.getChangePasswordButton().addActionListener(e -> handleChangePassword());
mainMenuView.getLogoutButton().addActionListener(e -> handleLogout());
// --- 考试逻辑 (已重构) ---
examView.getPrevButton().addActionListener(e -> handlePreviousQuestion());
examView.getNextButton().addActionListener(e -> handleNextQuestion());
examView.getFinishButton().addActionListener(e -> handleFinishExam());
// --- 分数界面逻辑 ---
scoreView.getContinueButton().addActionListener(e -> {
String welcomeName = currentUser.getUsername() != null && !currentUser.getUsername().isEmpty() ? currentUser.getUsername() : currentUser.getEmail();
mainMenuView.setWelcomeMessage("欢迎, " + welcomeName);
mainFrame.showPanel("MAIN_MENU");
});
scoreView.getExitButton().addActionListener(e -> handleLogout());
}
// --- 核心处理方法 ---
private void handleLogin() {
User user = userManager.authenticate(loginView.getIdentifier(), loginView.getPassword());
if (user != null) {
currentUser = user;
String welcomeName = user.getUsername() != null && !user.getUsername().isEmpty() ? user.getUsername() : user.getEmail();
mainMenuView.setWelcomeMessage("欢迎, " + welcomeName);
mainFrame.showPanel("MAIN_MENU");
} else {
JOptionPane.showMessageDialog(mainFrame, "邮箱/用户名或密码错误", "登录失败", JOptionPane.ERROR_MESSAGE);
}
loginView.clearFields();
}
private void handleGetCode() {
String email = registerView.getEmail();
String username = registerView.getUsername();
if (!ValidationService.isValidEmail(email)) {
JOptionPane.showMessageDialog(mainFrame, "请输入有效的邮箱格式!", "格式错误", JOptionPane.ERROR_MESSAGE);
return;
}
if (username.trim().isEmpty() || username.contains("|") || username.contains(" ")) {
JOptionPane.showMessageDialog(mainFrame, "用户名不能为空,且不能包含'|'或空格!", "格式错误", JOptionPane.ERROR_MESSAGE);
return;
}
if (userManager.emailExists(email)) {
JOptionPane.showMessageDialog(mainFrame, "该邮箱已被注册!", "注册失败", JOptionPane.ERROR_MESSAGE);
return;
}
if (userManager.usernameExists(username)) {
JOptionPane.showMessageDialog(mainFrame, "该用户名已被使用!", "注册失败", JOptionPane.ERROR_MESSAGE);
return;
}
emailForRegistration = email;
usernameForRegistration = username;
String code = String.format("%06d", new Random().nextInt(1000000));
registrationCodes.put(emailForRegistration, code);
final JButton getCodeButton = registerView.getGetCodeButton();
getCodeButton.setEnabled(false);
getCodeButton.setText("发送中...");
SwingWorker<Void, Void> worker = new SwingWorker<>() {
private Exception mailException = null;
@Override
protected Void doInBackground() {
try {
emailService.sendVerificationCode(emailForRegistration, code);
} catch (Exception e) {
this.mailException = e;
}
return null;
}
@Override
protected void done() {
getCodeButton.setEnabled(true);
getCodeButton.setText("发送验证邮件");
if (mailException == null) {
JOptionPane.showMessageDialog(mainFrame, "一封包含验证码的【电子邮件】已发送至您的邮箱,请登录邮箱查收。", "发送成功", JOptionPane.INFORMATION_MESSAGE);
} else {
String errorMessage = "邮件发送失败,请检查:\n1. 网络连接是否正常。\n2. config.properties 中的邮箱和授权码是否正确。\n\n错误详情: " + mailException.getMessage();
JOptionPane.showMessageDialog(mainFrame, errorMessage, "发送失败", JOptionPane.ERROR_MESSAGE);
mailException.printStackTrace();
}
}
};
worker.execute();
}
private void handleVerifyCode() {
String code = registerView.getCode();
if (code.equals(registrationCodes.get(emailForRegistration))) {
mainFrame.showPanel("PASSWORD_SET");
} else {
JOptionPane.showMessageDialog(mainFrame, "注册码错误!", "验证失败", JOptionPane.ERROR_MESSAGE);
}
}
private void handleSetPassword() {
String pass1 = passwordSetView.getPassword();
String pass2 = passwordSetView.getConfirmPassword();
if (!pass1.equals(pass2)) {
JOptionPane.showMessageDialog(mainFrame, "两次输入的密码不一致!", "错误", JOptionPane.ERROR_MESSAGE);
return;
}
if (!ValidationService.isPasswordStrong(pass1)) {
JOptionPane.showMessageDialog(mainFrame, "密码必须为6-10位且包含大小写字母和数字。", "密码格式错误", JOptionPane.ERROR_MESSAGE);
return;
}
userManager.registerUser(emailForRegistration, usernameForRegistration, pass1, UserType.PRIMARY);
JOptionPane.showMessageDialog(mainFrame, "注册成功!请返回登录。", "成功", JOptionPane.INFORMATION_MESSAGE);
mainFrame.showPanel("LOGIN");
}
private void handleStartExam() {
UserType type = mainMenuView.getSelectedType();
int count = mainMenuView.getQuestionCount();
if(currentUser.getUserType() != type) {
currentUser.setUserType(type);
userManager.updateUser(currentUser);
}
currentExam = examManager.generateExam(currentUser, type, count);
if (currentExam.size() < count) {
JOptionPane.showMessageDialog(mainFrame, "抱歉,未能生成足够数量的不重复题目,实际生成 " + currentExam.size() + " 道。", "提示", JOptionPane.WARNING_MESSAGE);
}
if (currentExam.isEmpty()) return;
currentQuestionIndex = 0;
score = 0;
userAnswers = new int[currentExam.size()];
Arrays.fill(userAnswers, -1); // -1 代表未作答
examView.setupForExam(currentExam.size());
displayCurrentQuestion();
mainFrame.showPanel("EXAM");
}
private void displayCurrentQuestion() {
Question q = currentExam.get(currentQuestionIndex);
int savedAnswer = userAnswers[currentQuestionIndex];
examView.displayQuestion(q, currentQuestionIndex + 1, currentExam.size(), savedAnswer);
examView.updateNavigationButtons(currentQuestionIndex, currentExam.size());
}
private void handlePreviousQuestion() {
saveCurrentAnswer();
if (currentQuestionIndex > 0) {
currentQuestionIndex--;
displayCurrentQuestion();
}
}
private void handleNextQuestion() {
saveCurrentAnswer();
if (currentQuestionIndex < currentExam.size() - 1) {
currentQuestionIndex++;
displayCurrentQuestion();
}
}
private void saveCurrentAnswer() {
int selectedIndex = examView.getSelectedIndex();
userAnswers[currentQuestionIndex] = selectedIndex;
}
private void handleFinishExam() {
saveCurrentAnswer();
int choice = JOptionPane.showConfirmDialog(mainFrame,
"您确定要交卷吗?\n未完成的题目将按 0 分计算。",
"确认交卷",
JOptionPane.YES_NO_OPTION);
if (choice == JOptionPane.YES_OPTION) {
calculateScore();
int finalScore = (int) Math.round((double) score / currentExam.size() * 100);
scoreView.setScore(finalScore, score, currentExam.size());
mainFrame.showPanel("SCORE");
}
}
private void calculateScore() {
score = 0;
for (int i = 0; i < currentExam.size(); i++) {
if (userAnswers[i] != -1 && userAnswers[i] == currentExam.get(i).getCorrectOptionIndex()) {
score++;
}
}
}
private void handleChangePassword() {
ChangePasswordView dialog = new ChangePasswordView(mainFrame);
dialog.getConfirmButton().addActionListener(e -> {
String oldPass = dialog.getOldPassword();
String newPass = dialog.getNewPassword();
String confirmPass = dialog.getConfirmPassword();
if (userManager.authenticate(currentUser.getEmail(), oldPass) == null) {
JOptionPane.showMessageDialog(dialog, "原密码错误!", "验证失败", JOptionPane.ERROR_MESSAGE);
return;
}
if (!newPass.equals(confirmPass)) {
JOptionPane.showMessageDialog(dialog, "两次输入的新密码不一致!", "错误", JOptionPane.ERROR_MESSAGE);
return;
}
if (!ValidationService.isPasswordStrong(newPass)) {
JOptionPane.showMessageDialog(dialog, "新密码必须为6-10位且包含大小写字母和数字。", "密码格式错误", JOptionPane.ERROR_MESSAGE);
return;
}
currentUser.setHashedPassword(newPass);
userManager.updateUser(currentUser);
JOptionPane.showMessageDialog(dialog, "密码修改成功!", "成功", JOptionPane.INFORMATION_MESSAGE);
dialog.dispose();
});
dialog.getCancelButton().addActionListener(e -> dialog.dispose());
dialog.setVisible(true);
}
private void handleLogout() {
currentUser = null;
loginView.clearFields();
mainFrame.showPanel("LOGIN");
}
}

@ -0,0 +1,50 @@
package view;
import javax.swing.*;
import java.awt.*;
public class ChangePasswordView extends JDialog {
private final JPasswordField oldPasswordField = new JPasswordField(20);
private final JPasswordField newPasswordField = new JPasswordField(20);
private final JPasswordField confirmPasswordField = new JPasswordField(20);
private final JButton confirmButton = new JButton("确认修改");
private final JButton cancelButton = new JButton("取消");
public ChangePasswordView(Frame owner) {
super(owner, "修改密码", true);
setSize(450, 300);
setLocationRelativeTo(owner);
setLayout(new BorderLayout(10, 10));
JPanel fieldsPanel = new JPanel(new GridBagLayout());
fieldsPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel hintLabel = new JLabel("新密码要求: 6-10位且必须包含大小写字母和数字。");
hintLabel.setForeground(Color.GRAY);
gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; fieldsPanel.add(hintLabel, gbc);
gbc.gridwidth = 1;
gbc.gridx = 0; gbc.gridy = 1; fieldsPanel.add(new JLabel("原密码:"), gbc);
gbc.gridx = 1; gbc.gridy = 1; fieldsPanel.add(oldPasswordField, gbc);
gbc.gridx = 0; gbc.gridy = 2; fieldsPanel.add(new JLabel("新密码:"), gbc);
gbc.gridx = 1; gbc.gridy = 2; fieldsPanel.add(newPasswordField, gbc);
gbc.gridx = 0; gbc.gridy = 3; fieldsPanel.add(new JLabel("确认新密码:"), gbc);
gbc.gridx = 1; gbc.gridy = 3; fieldsPanel.add(confirmPasswordField, gbc);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0));
buttonPanel.add(confirmButton);
buttonPanel.add(cancelButton);
add(fieldsPanel, BorderLayout.CENTER);
add(buttonPanel, BorderLayout.SOUTH);
}
public String getOldPassword() { return new String(oldPasswordField.getPassword()); }
public String getNewPassword() { return new String(newPasswordField.getPassword()); }
public String getConfirmPassword() { return new String(confirmPasswordField.getPassword()); }
public JButton getConfirmButton() { return confirmButton; }
public JButton getCancelButton() { return cancelButton; }
}

@ -0,0 +1,140 @@
package view;
import model.Question;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.*;
import java.util.List;
public class ExamView extends JPanel {
private final JLabel progressLabel = new JLabel(" ", SwingConstants.CENTER);
private final JProgressBar progressBar = new JProgressBar();
private final JTextPane questionPane = new JTextPane();
private final JRadioButton[] optionButtons = new JRadioButton[4];
private final ButtonGroup optionsGroup = new ButtonGroup();
private final JButton prevButton = new JButton("上一题");
private final JButton nextButton = new JButton("下一题");
private final JButton finishButton = new JButton("交卷并评分");
public ExamView() {
setLayout(new BorderLayout(20, 20));
setBorder(new EmptyBorder(20, 40, 40, 40));
// --- 顶部进度区域 ---
JPanel topPanel = new JPanel(new BorderLayout(0, 10));
progressLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 20));
topPanel.add(progressLabel, BorderLayout.NORTH);
progressBar.setPreferredSize(new Dimension(1, 15));
topPanel.add(progressBar, BorderLayout.CENTER);
add(topPanel, BorderLayout.NORTH);
// --- 中间题目区域 ---
questionPane.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 36));
questionPane.setEditable(false);
questionPane.setFocusable(false);
questionPane.setOpaque(false);
StyledDocument doc = questionPane.getStyledDocument();
SimpleAttributeSet center = new SimpleAttributeSet();
StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
doc.setParagraphAttributes(0, doc.getLength(), center, false);
JPanel questionWrapper = new JPanel(new BorderLayout());
questionWrapper.setOpaque(false);
questionWrapper.add(questionPane, BorderLayout.CENTER);
add(questionWrapper, BorderLayout.CENTER);
// --- 底部选项和导航 ---
JPanel southPanel = new JPanel(new BorderLayout(20, 30));
// --- 选项面板 ---
JPanel optionsPanel = new JPanel(new GridBagLayout());
optionsPanel.setOpaque(false);
GridBagConstraints gbcOpt = new GridBagConstraints();
gbcOpt.insets = new Insets(15, 80, 15, 80); // 调整选项的内外边距
gbcOpt.anchor = GridBagConstraints.WEST;
for (int i = 0; i < 4; i++) {
optionButtons[i] = new JRadioButton();
optionButtons[i].setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20));
optionButtons[i].setOpaque(false);
optionsGroup.add(optionButtons[i]);
}
gbcOpt.gridx = 0; gbcOpt.gridy = 0; optionsPanel.add(optionButtons[0], gbcOpt); // 位置 A
gbcOpt.gridx = 1; gbcOpt.gridy = 0; optionsPanel.add(optionButtons[1], gbcOpt); // 位置 B
gbcOpt.gridx = 0; gbcOpt.gridy = 1; optionsPanel.add(optionButtons[2], gbcOpt); // 位置 C
gbcOpt.gridx = 1; gbcOpt.gridy = 1; optionsPanel.add(optionButtons[3], gbcOpt); // 位置 D
JPanel optionsWrapper = new JPanel(new GridBagLayout());
optionsWrapper.setOpaque(false);
optionsWrapper.add(optionsPanel);
southPanel.add(optionsWrapper, BorderLayout.CENTER);
// --- 导航按钮面板 ---
JPanel navigationPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 30, 0));
prevButton.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
nextButton.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
finishButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
finishButton.putClientProperty("JButton.buttonType", "roundRect");
navigationPanel.add(prevButton);
navigationPanel.add(finishButton);
navigationPanel.add(nextButton);
southPanel.add(navigationPanel, BorderLayout.SOUTH);
add(southPanel, BorderLayout.SOUTH);
}
public void displayQuestion(Question q, int qNum, int total, int savedAnswerIndex) {
progressLabel.setText(String.format("第 %d 题 / 共 %d 题", qNum, total));
progressBar.setValue(qNum);
questionPane.setText(q.getQuestionText());
StyledDocument doc = questionPane.getStyledDocument();
SimpleAttributeSet center = new SimpleAttributeSet();
StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
doc.setParagraphAttributes(0, doc.getLength(), center, false);
optionsGroup.clearSelection();
List<String> options = q.getOptions();
char optionLabel = 'A';
for (int i = 0; i < 4; i++) {
optionButtons[i].setText(optionLabel + ". " + options.get(i));
optionLabel++;
}
if (savedAnswerIndex != -1) {
optionButtons[savedAnswerIndex].setSelected(true);
}
}
public void setupForExam(int totalQuestions) {
progressBar.setMinimum(1);
progressBar.setMaximum(totalQuestions);
}
public void updateNavigationButtons(int qIndex, int total) {
prevButton.setEnabled(qIndex > 0);
nextButton.setEnabled(qIndex < total - 1);
nextButton.setText(qIndex == total - 2 ? "最后一题" : "下一题");
}
public int getSelectedIndex() {
for (int i = 0; i < 4; i++) {
if (optionButtons[i].isSelected()) { return i; }
}
return -1;
}
public JButton getPrevButton() { return prevButton; }
public JButton getNextButton() { return nextButton; }
public JButton getFinishButton() { return finishButton; }
}

@ -0,0 +1,64 @@
package view;
import javax.swing.*;
import java.awt.*;
public class LoginView extends JPanel {
private final JTextField identifierField = new JTextField(20);
private final JPasswordField passwordField = new JPasswordField(20);
private final JButton loginButton = new JButton("登 录");
private final JButton registerButton = new JButton("还没有账号?立即注册");
public LoginView() {
setLayout(new GridBagLayout());
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setBorder(BorderFactory.createEmptyBorder(30, 40, 30, 40));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel titleLabel = new JLabel("欢迎回来", SwingConstants.CENTER);
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28));
gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2;
formPanel.add(titleLabel, gbc);
gbc.gridwidth = 1;
gbc.gridx = 0; gbc.gridy = 1; addLabel(formPanel, "邮箱 / 用户名:", gbc);
gbc.gridx = 1; gbc.gridy = 1; addComponent(formPanel, identifierField, gbc);
gbc.gridx = 0; gbc.gridy = 2; addLabel(formPanel, "密码:", gbc);
gbc.gridx = 1; gbc.gridy = 2; addComponent(formPanel, passwordField, gbc);
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2;
loginButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
addComponent(formPanel, loginButton, gbc);
gbc.gridy = 4;
registerButton.putClientProperty("FlatLaf.style", "buttonType: borderless");
addComponent(formPanel, registerButton, gbc);
add(formPanel);
}
private void addLabel(JPanel panel, String text, GridBagConstraints gbc) {
JLabel label = new JLabel(text);
label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
panel.add(label, gbc);
}
private void addComponent(JPanel panel, JComponent comp, GridBagConstraints gbc) {
comp.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
panel.add(comp, gbc);
}
public String getIdentifier() { return identifierField.getText().trim(); }
public String getPassword() { return new String(passwordField.getPassword()); }
public JButton getLoginButton() { return loginButton; }
public JButton getRegisterButton() { return registerButton; }
public void clearFields() {
identifierField.setText("");
passwordField.setText("");
}
}

@ -0,0 +1,27 @@
package view;
import javax.swing.*;
import java.awt.*;
public class MainFrame extends JFrame {
private final CardLayout cardLayout = new CardLayout();
private final JPanel mainPanel = new JPanel(cardLayout);
public MainFrame() {
super("数学学习软件");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(800, 600);
setLocationRelativeTo(null);
setResizable(false);
add(mainPanel);
}
public void addPanel(JPanel panel, String name) {
mainPanel.add(panel, name);
}
public void showPanel(String name) {
cardLayout.show(mainPanel, name);
setVisible(true);
}
}

@ -0,0 +1,72 @@
package view;
import model.UserType;
import javax.swing.*;
import java.awt.*;
public class MainMenuView extends JPanel {
private final JLabel welcomeLabel = new JLabel("", SwingConstants.CENTER);
private final JComboBox<String> typeComboBox = new JComboBox<>();
private final JSpinner countSpinner = new JSpinner(new SpinnerNumberModel(10, 10, 30, 1));
private final JButton startExamButton = new JButton("开始答题");
private final JButton changePasswordButton = new JButton("修改密码");
private final JButton logoutButton = new JButton("退出登录");
public MainMenuView() {
setLayout(new BorderLayout(20, 20));
setBorder(BorderFactory.createEmptyBorder(40, 40, 40, 40));
welcomeLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28));
add(welcomeLabel, BorderLayout.NORTH);
JPanel centerPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(15, 10, 15, 10);
gbc.gridx = 0; gbc.gridy = 0; addLabel(centerPanel, "选择题目难度:", gbc);
gbc.gridx = 1; gbc.gridy = 0; addComponent(centerPanel, typeComboBox, gbc);
gbc.gridx = 0; gbc.gridy = 1; addLabel(centerPanel, "选择题目数量:", gbc);
gbc.gridx = 1; gbc.gridy = 1; addComponent(centerPanel, countSpinner, gbc);
startExamButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 20));
startExamButton.putClientProperty("JButton.buttonType", "roundRect");
gbc.gridy = 2; gbc.gridwidth = 2; gbc.insets = new Insets(30, 10, 15, 10);
addComponent(centerPanel, startExamButton, gbc);
add(centerPanel, BorderLayout.CENTER);
JPanel southPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 20, 0));
southPanel.add(changePasswordButton);
southPanel.add(logoutButton);
add(southPanel, BorderLayout.SOUTH);
for (UserType type : UserType.values()) {
typeComboBox.addItem(type.getDisplayName());
}
}
private void addLabel(JPanel panel, String text, GridBagConstraints gbc) {
JLabel label = new JLabel(text);
label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
panel.add(label, gbc);
}
private void addComponent(JPanel panel, JComponent comp, GridBagConstraints gbc) {
comp.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
panel.add(comp, gbc);
}
public void setWelcomeMessage(String message) { welcomeLabel.setText(message); }
public UserType getSelectedType() {
String displayName = (String) typeComboBox.getSelectedItem();
for (UserType type : UserType.values()) {
if (type.getDisplayName().equals(displayName)) return type;
}
return UserType.PRIMARY;
}
public int getQuestionCount() { return (int) countSpinner.getValue(); }
public JButton getStartExamButton() { return startExamButton; }
public JButton getChangePasswordButton() { return changePasswordButton; }
public JButton getLogoutButton() { return logoutButton; }
}

@ -0,0 +1,54 @@
package view;
import javax.swing.*;
import java.awt.*;
public class PasswordSetView extends JPanel {
private final JPasswordField passwordField = new JPasswordField(20);
private final JPasswordField confirmPasswordField = new JPasswordField(20);
private final JButton submitButton = new JButton("完成注册");
public PasswordSetView() {
setLayout(new GridBagLayout());
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setBorder(BorderFactory.createEmptyBorder(30, 40, 30, 40));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel titleLabel = new JLabel("设置您的密码", SwingConstants.CENTER);
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28));
gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; formPanel.add(titleLabel, gbc);
JLabel hintLabel = new JLabel("密码要求: 6-10位且必须包含大小写字母和数字。", SwingConstants.CENTER);
hintLabel.setForeground(Color.GRAY);
gbc.gridy = 1; formPanel.add(hintLabel, gbc);
gbc.gridwidth = 1;
gbc.gridx = 0; gbc.gridy = 2; addLabel(formPanel, "输入密码:", gbc);
gbc.gridx = 1; gbc.gridy = 2; addComponent(formPanel, passwordField, gbc);
gbc.gridx = 0; gbc.gridy = 3; addLabel(formPanel, "确认密码:", gbc);
gbc.gridx = 1; gbc.gridy = 3; addComponent(formPanel, confirmPasswordField, gbc);
gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 2; gbc.insets = new Insets(20, 10, 10, 10);
submitButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
addComponent(formPanel, submitButton, gbc);
add(formPanel);
}
private void addLabel(JPanel panel, String text, GridBagConstraints gbc) {
JLabel label = new JLabel(text);
label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
panel.add(label, gbc);
}
private void addComponent(JPanel panel, JComponent comp, GridBagConstraints gbc) {
comp.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
panel.add(comp, gbc);
}
public String getPassword() { return new String(passwordField.getPassword()); }
public String getConfirmPassword() { return new String(confirmPasswordField.getPassword()); }
public JButton getSubmitButton() { return submitButton; }
}

@ -0,0 +1,72 @@
package view;
import javax.swing.*;
import java.awt.*;
public class RegisterView extends JPanel {
private final JTextField emailField = new JTextField(20);
private final JTextField usernameField = new JTextField(20);
private final JTextField codeField = new JTextField(10);
private final JButton getCodeButton = new JButton("发送验证邮件");
private final JButton registerButton = new JButton("下一步:设置密码");
private final JButton backButton = new JButton("返回登录");
public RegisterView() {
setLayout(new GridBagLayout());
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setBorder(BorderFactory.createEmptyBorder(30, 40, 30, 40));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel titleLabel = new JLabel("创建您的账号", SwingConstants.CENTER);
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28));
gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; formPanel.add(titleLabel, gbc);
gbc.gridwidth = 1;
gbc.gridx = 0; gbc.gridy = 1; addLabel(formPanel, "邮箱地址:", gbc);
gbc.gridx = 1; gbc.gridy = 1; addComponent(formPanel, emailField, gbc);
gbc.gridx = 0; gbc.gridy = 2; addLabel(formPanel, "设置用户名:", gbc);
gbc.gridx = 1; gbc.gridy = 2; addComponent(formPanel, usernameField, gbc);
JPanel codePanel = new JPanel(new BorderLayout(10, 0));
addComponent(codePanel, codeField, BorderLayout.CENTER);
codePanel.add(getCodeButton, BorderLayout.EAST);
gbc.gridx = 0; gbc.gridy = 3; addLabel(formPanel, "邮箱验证码:", gbc);
gbc.gridx = 1; gbc.gridy = 3; formPanel.add(codePanel, gbc);
JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 10, 0));
registerButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
backButton.putClientProperty("FlatLaf.style", "buttonType: borderless");
buttonPanel.add(registerButton);
buttonPanel.add(backButton);
gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 2; gbc.insets = new Insets(20, 10, 10, 10);
formPanel.add(buttonPanel, gbc);
add(formPanel);
}
private void addLabel(JPanel p, String s, GridBagConstraints g) {
JLabel label = new JLabel(s);
label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
p.add(label, g);
}
private void addComponent(JPanel p, JComponent c, GridBagConstraints g) {
c.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
p.add(c, g);
}
private void addComponent(JPanel p, JComponent c, String pos) {
c.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16));
p.add(c, pos);
}
public String getEmail() { return emailField.getText().trim(); }
public String getUsername() { return usernameField.getText().trim(); }
public String getCode() { return codeField.getText().trim(); }
public JButton getGetCodeButton() { return getCodeButton; }
public JButton getRegisterButton() { return registerButton; }
public JButton getBackButton() { return backButton; }
}

@ -0,0 +1,52 @@
package view;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public class ScoreView extends JPanel {
private final JLabel scoreLabel = new JLabel("0", SwingConstants.CENTER);
private final JLabel scoreUnitLabel = new JLabel("分", SwingConstants.CENTER);
private final JLabel detailLabel = new JLabel(" ", SwingConstants.CENTER);
private final JButton continueButton = new JButton("再来一组");
private final JButton exitButton = new JButton("退出登录");
public ScoreView() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(10, 10, 10, 10);
JLabel titleLabel = new JLabel("答题结束!", SwingConstants.CENTER);
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 32));
add(titleLabel, gbc);
JPanel scorePanel = new JPanel();
scoreLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 100));
scoreUnitLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 30));
scoreUnitLabel.setBorder(new EmptyBorder(0,0, -50, 0));
scorePanel.add(scoreLabel);
scorePanel.add(scoreUnitLabel);
add(scorePanel, gbc);
detailLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20));
add(detailLabel, gbc);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0));
continueButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
// 【修复】将 "=" 修改为 ":"
continueButton.putClientProperty("JButton.buttonType", "roundRect");
buttonPanel.add(continueButton);
buttonPanel.add(exitButton);
gbc.insets = new Insets(30, 10, 10, 10);
add(buttonPanel, gbc);
}
public void setScore(int score, int correct, int total) {
scoreLabel.setText(String.valueOf(score));
detailLabel.setText(String.format("答对 %d 题,共 %d 题", correct, total));
}
public JButton getContinueButton() { return continueButton; }
public JButton getExitButton() { return exitButton; }
}
Loading…
Cancel
Save