Compare commits

..

24 Commits

Author SHA1 Message Date
pus7f45rn 36b416105d 添加 'doc/README.MD'
3 months ago
pus7f45rn 86ce644ba2 上传文件至 'lib'
3 months ago
pus7f45rn f0c4ce050f 添加 '.idea/workspace.xml'
3 months ago
pus7f45rn 5c3f3eb1d1 添加 '.idea/modules.xml'
3 months ago
pus7f45rn fa1bb984f0 添加 '.idea/misc.xml'
3 months ago
pus7f45rn a9f64cb59a 添加 '.idea/.gitignore'
3 months ago
pus7f45rn 4ece97b30e 删除 'src/.idea/vcs.xml'
3 months ago
pus7f45rn 6490167b22 删除 'src/.idea/modules.xml'
3 months ago
pus7f45rn a049f89694 删除 'src/.idea/misc.xml'
3 months ago
pus7f45rn a1d88aace3 删除 'src/.idea/.gitignore'
3 months ago
pus7f45rn b8b9ecfcc7 Merge pull request 'Mark as ready' (#2) from develop into main
3 months ago
pus7f45rn c8cd1a2590 添加 'src/controller/AppController.java'
3 months ago
pus7f45rn 97bc817e60 上传文件至 'src/view'
3 months ago
pus7f45rn 917ce6de87 上传文件至 'src/view'
3 months ago
pus7f45rn 27a4343c24 上传文件至 'src/view'
3 months ago
pus7f45rn 9bc8ace6cd 添加 'src/view/MainMenuView.java'
3 months ago
pus7f45rn d1f34e44d6 添加 'src/view/MainFrame.java'
3 months ago
pus7f45rn 0add72f15d 添加 'src/view/LoginView.java'
3 months ago
pus7f45rn ad4e1ce6b9 添加 'src/view/ExamView.java'
3 months ago
pus7f45rn bc74b63302 添加 'src/view/ChangePasswordView.java'
3 months ago
ymr 0a4ac1a5a2 项目依赖
3 months ago
ymr 549c74545e 项目依赖库文件
3 months ago
ymr 81b1185b67 添加 src 和 lib 核心代码
3 months ago
ymr fc8b0d9b65 Feat: 添加 src 和 lib 核心代码
3 months ago

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<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>

@ -1,2 +0,0 @@
# MathEaxmApp

@ -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.

@ -0,0 +1,20 @@
import com.formdev.flatlaf.FlatLightLaf;
import controller.AppController;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Main {
public static void main(String[] args) {
System.setProperty("awt.useSystemAAFontSettings", "on");
System.setProperty("swing.aatext", "true");
try {
UIManager.setLookAndFeel(new FlatLightLaf());
} catch (UnsupportedLookAndFeelException e) {
System.err.println("无法设置UI主题: " + e.getMessage());
}
SwingUtilities.invokeLater(AppController::new);
}
}

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,10 @@
smtp.host=smtp.qq.com
smtp.port=465
smtp.ssl.enable=true
smtp.auth=true
mail.sender.email=2631495488@qq.com
mail.sender.password=vdjvddhflexgdjfd

@ -0,0 +1,19 @@
package model;
import java.util.List;
public class Question {
private final String questionText;
private final List<String> options;
private final int correctOptionIndex;
public Question(String questionText, List<String> options, int correctOptionIndex) {
this.questionText = questionText;
this.options = options;
this.correctOptionIndex = correctOptionIndex;
}
public String getQuestionText() { return questionText; }
public List<String> getOptions() { return options; }
public int getCorrectOptionIndex() { return correctOptionIndex; }
}

@ -0,0 +1,56 @@
package model;
public class User {
private final String email;
private final String username; // 新增字段
private String hashedPassword;
private UserType userType;
public User(String email, String username, String hashedPassword, UserType userType) {
this.email = email;
this.username = username;
this.hashedPassword = hashedPassword;
this.userType = userType;
}
public String getEmail() { return email; }
public String getUsername() { return username; } // 新增Getter
public String getHashedPassword() { return hashedPassword; }
public UserType getUserType() { return userType; }
public void setUserType(UserType userType) { this.userType = userType; }
public void setHashedPassword(String plainPassword) {
this.hashedPassword = Integer.toString(plainPassword.hashCode());
}
private void setHashedPasswordDirectly(String hashedPassword) {
this.hashedPassword = hashedPassword;
}
@Override
public String toString() {
// 新的4字段格式用户名可以为空字符串
return String.join("|", email, username == null ? "" : username, hashedPassword, userType.name());
}
public static User fromString(String line) {
String[] parts = line.split("\\|");
User user = null;
if (parts.length == 4) { // 新格式: email|username|password|type
String email = parts[0];
String username = parts[1].isEmpty() ? null : parts[1];
String password = parts[2];
UserType type = UserType.valueOf(parts[3]);
user = new User(email, username, "", type);
user.setHashedPasswordDirectly(password);
} else if (parts.length == 3) { // 旧格式: email|password|type
String email = parts[0];
String password = parts[1];
UserType type = UserType.valueOf(parts[2]);
user = new User(email, null, "", type); // 旧用户没有用户名
user.setHashedPasswordDirectly(password);
}
return user;
}
}

@ -0,0 +1,17 @@
package model;
public enum UserType {
PRIMARY("小学"),
JUNIOR("初中"),
SENIOR("高中");
private final String displayName;
UserType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

@ -0,0 +1,74 @@
package service;
import javax.mail.*;
import javax.mail.internet.*;
import javax.activation.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class EmailService {
final Properties props = new Properties();
private final String senderEmail;
private final String senderPassword;
public EmailService() {
try (InputStream input = EmailService.class.getClassLoader().getResourceAsStream("config.properties")) {
if (input == null) {
System.err.println("致命错误:在类路径中找不到 config.properties 文件!");
System.err.println("请确认 'config.properties' 文件已放置在 'src' 文件夹根目录下。");
throw new RuntimeException("加载配置文件失败: config.properties not found in classpath");
}
props.load(input);
} catch (IOException e) {
System.err.println("错误:加载 config.properties 文件时发生IO错误");
throw new RuntimeException("加载配置文件失败", e);
}
this.senderEmail = props.getProperty("mail.sender.email");
this.senderPassword = props.getProperty("mail.sender.password");
}
public void sendVerificationCode(String recipientEmail, String code) throws MessagingException {
Properties mailProps = new Properties();
mailProps.put("mail.smtp.host", props.getProperty("smtp.host"));
mailProps.put("mail.smtp.port", props.getProperty("smtp.port"));
mailProps.put("mail.smtp.ssl.enable", props.getProperty("smtp.ssl.enable"));
mailProps.put("mail.smtp.auth", props.getProperty("smtp.auth"));
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(senderEmail, senderPassword);
}
};
Session session = Session.getInstance(mailProps, authenticator);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(senderEmail));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipientEmail));
message.setSubject("您的数学学习软件验证码", "UTF-8");
String content = "<h3>欢迎使用数学学习软件!</h3>" +
"<p>您的注册验证码是:<b style='color: #007bff; font-size: 18px;'>" + code + "</b></p>" +
"<p>该验证码5分钟内有效请勿泄露给他人。</p>";
message.setContent(content, "text/html; charset=UTF-8");
Transport.send(message);
System.out.println("验证码邮件已成功发送至 " + recipientEmail);
}
}

@ -0,0 +1,169 @@
package service;
import model.Question;
import model.User;
import model.UserType;
import service.generator.*;
import java.io.PrintWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;
public class ExamManager {
private final Map<UserType, QuestionGenerator> generators = new HashMap<>();
public ExamManager() {
generators.put(UserType.PRIMARY, new PrimaryQuestionGenerator());
generators.put(UserType.JUNIOR, new JuniorQuestionGenerator());
generators.put(UserType.SENIOR, new SeniorQuestionGenerator());
}
public List<Question> generateExam(User user, UserType userType, int count) {
QuestionGenerator generator = generators.get(userType);
if (generator == null) throw new IllegalArgumentException("不支持的用户类型");
List<Question> questions = new ArrayList<>();
Set<String> generatedTexts = new HashSet<>();
int maxAttempts = count * 20;
int attempts = 0;
while (questions.size() < count && attempts < maxAttempts) {
String qText = generator.generateQuestion();
if (generatedTexts.contains(qText)) {
attempts++;
continue;
}
try {
double answer = ExpressionEvaluator.evaluate(qText);
if (Double.isInfinite(answer) || Double.isNaN(answer)) continue;
List<String> options = generateOptions(answer); // 调用已优化的方法
int correctIndex = options.indexOf(formatAnswer(answer));
questions.add(new Question(qText, options, correctIndex));
generatedTexts.add(qText);
} catch (Exception e) {
} finally {
attempts++;
}
}
if (!questions.isEmpty()) {
saveExamToFile(user, questions);
}
return questions;
}
private List<String> generateOptions(double correctAnswer) {
Set<String> options = new LinkedHashSet<>();
options.add(formatAnswer(correctAnswer));
Random rand = new Random();
// 判断答案是否为整数
boolean isIntegerAnswer = Math.abs(correctAnswer - Math.round(correctAnswer)) < 1e-9;
while (options.size() < 4) {
String distractorStr;
if (isIntegerAnswer) {
int intAnswer = (int) Math.round(correctAnswer);
int distractor;
int type = rand.nextInt(3); // 三种整数干扰策略
switch(type) {
case 0: // 策略一:在答案附近加减一个较小的随机数
distractor = intAnswer + (rand.nextInt(8) + 1) * (rand.nextBoolean() ? 1 : -1);
break;
case 1: // 策略二模拟看错位的错误加减10
distractor = intAnswer + (rand.nextBoolean() ? 10 : -10);
break;
default: // 策略三:颠倒个位和十位(如果答案是两位数)
if (intAnswer >= 10 && intAnswer <= 99) {
distractor = (intAnswer % 10) * 10 + (intAnswer / 10);
} else {
// 如果不是两位数,则使用策略一
distractor = intAnswer + (rand.nextInt(8) + 1) * (rand.nextBoolean() ? 1 : -1);
}
break;
}
// 确保干扰项不等于正确答案
if (distractor == intAnswer) {
distractor++;
}
distractorStr = String.valueOf(distractor);
} else {
double distractor;
if (rand.nextBoolean()) { // 乘法干扰
distractor = correctAnswer * (rand.nextDouble() * 1.5 + 0.5);
} else { // 临近值干扰
distractor = correctAnswer + (rand.nextDouble() - 0.5) * Math.max(1, Math.abs(correctAnswer * 0.2));
}
distractorStr = formatAnswer(distractor);
}
// 确保选项不重复
if (!options.contains(distractorStr)) {
options.add(distractorStr);
}
}
List<String> shuffledOptions = new ArrayList<>(options);
Collections.shuffle(shuffledOptions);
return shuffledOptions;
}
private void saveExamToFile(User user, List<Question> questions) {
try {
Path userExamDir = Paths.get("exams", user.getEmail());
Files.createDirectories(userExamDir);
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
Path examFile = userExamDir.resolve("exam_" + timestamp + ".txt");
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(examFile, StandardCharsets.UTF_8))) {
writer.println("用户: " + user.getEmail());
writer.println("时间: " + new Date());
writer.println("题目类型: " + user.getUserType().getDisplayName());
writer.println("题目数量: " + questions.size());
writer.println("========================================");
writer.println();
for (int i = 0; i < questions.size(); i++) {
Question q = questions.get(i);
writer.println((i + 1) + ". " + q.getQuestionText());
List<String> options = q.getOptions();
char optionLabel = 'A';
for (int j = 0; j < options.size(); j++) {
writer.println(" " + optionLabel + ". " + options.get(j));
optionLabel++;
}
writer.println(" 正确答案: " + (char)('A' + q.getCorrectOptionIndex()));
writer.println();
}
}
System.out.println("试卷已成功保存至: " + examFile.toAbsolutePath());
} catch (IOException e) {
System.err.println("保存试卷文件时出错: " + e.getMessage());
}
}
private String formatAnswer(double number) {
if (Math.abs(number - Math.round(number)) < 1e-9) {
return String.valueOf((long) Math.round(number));
} else {
return String.format("%.2f", number);
}
}
}

@ -0,0 +1,120 @@
package service;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ExpressionEvaluator {
public static double evaluate(String expression) {
// 1. 预处理,将特殊运算(开方、平方、三角函数)直接计算并替换为数值
String processedExpr = preprocessSpecialOperations(expression);
// 2. 使用双栈算法计算最终的四则运算表达式
return calculateStandardExpression(processedExpr);
}
private static String preprocessSpecialOperations(String expr) {
// 替换中文运算符
expr = expr.replace('×', '*').replace('÷', '/');
// 匹配并计算三角函数,例如 sin(30°)
Pattern trigPattern = Pattern.compile("(sin|cos|tan)\\((\\d+)°\\)");
Matcher trigMatcher = trigPattern.matcher(expr);
while (trigMatcher.find()) {
String func = trigMatcher.group(1);
double angle = Double.parseDouble(trigMatcher.group(2));
double value = 0.0;
switch (func) {
case "sin": value = Math.sin(Math.toRadians(angle)); break;
case "cos": value = Math.cos(Math.toRadians(angle)); break;
case "tan": value = Math.tan(Math.toRadians(angle)); break;
}
expr = trigMatcher.replaceFirst(String.format("%.4f", value));
trigMatcher = trigPattern.matcher(expr);
}
// 匹配并计算平方,例如 5^2
Pattern squarePattern = Pattern.compile("(\\d+)\\^2");
Matcher squareMatcher = squarePattern.matcher(expr);
while (squareMatcher.find()) {
double base = Double.parseDouble(squareMatcher.group(1));
expr = squareMatcher.replaceFirst(String.valueOf(base * base));
squareMatcher = squarePattern.matcher(expr);
}
// 匹配并计算开方,例如 √(16)
Pattern sqrtPattern = Pattern.compile("√\\((\\d+)\\)");
Matcher sqrtMatcher = sqrtPattern.matcher(expr);
while (sqrtMatcher.find()) {
double num = Double.parseDouble(sqrtMatcher.group(1));
expr = sqrtMatcher.replaceFirst(String.valueOf(Math.sqrt(num)));
sqrtMatcher = sqrtPattern.matcher(expr);
}
return expr;
}
private static double calculateStandardExpression(String expression) {
Stack<Double> values = new Stack<>();
Stack<Character> ops = new Stack<>();
char[] tokens = expression.toCharArray();
for (int i = 0; i < tokens.length; i++) {
if (tokens[i] == ' ') continue;
if ((tokens[i] >= '0' && tokens[i] <= '9') || tokens[i] == '.') {
StringBuilder sbuf = new StringBuilder();
while (i < tokens.length && ((tokens[i] >= '0' && tokens[i] <= '9') || tokens[i] == '.')) {
sbuf.append(tokens[i++]);
}
i--;
values.push(Double.parseDouble(sbuf.toString()));
} else if (tokens[i] == '(') {
ops.push(tokens[i]);
} else if (tokens[i] == ')') {
while (ops.peek() != '(') {
values.push(applyOp(ops.pop(), values.pop(), values.pop()));
}
ops.pop();
} else if (isOperator(tokens[i])) {
while (!ops.empty() && hasPrecedence(tokens[i], ops.peek())) {
values.push(applyOp(ops.pop(), values.pop(), values.pop()));
}
ops.push(tokens[i]);
}
}
while (!ops.empty()) {
values.push(applyOp(ops.pop(), values.pop(), values.pop()));
}
return values.pop();
}
private static boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
private static boolean hasPrecedence(char op1, char op2) {
if (op2 == '(' || op2 == ')') return false;
return (op1 != '*' && op1 != '/') || (op2 != '+' && op2 != '-');
}
private static double applyOp(char op, double b, double a) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/':
if (b == 0) throw new UnsupportedOperationException("不能除以零");
return a / b;
}
return 0;
}
}

@ -0,0 +1,97 @@
package service;
import model.User;
import model.UserType;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
public class UserManager {
private final Path usersFile = Paths.get("data", "users.txt");
private final Map<String, User> usersByEmail = new HashMap<>(); // 使用邮箱作为主键
public UserManager() {
try {
Files.createDirectories(usersFile.getParent());
if (!Files.exists(usersFile)) Files.createFile(usersFile);
loadUsers();
} catch (IOException e) {
System.err.println("初始化用户管理器时出错: " + e.getMessage());
}
}
private void loadUsers() throws IOException {
List<String> lines = Files.readAllLines(usersFile, StandardCharsets.UTF_8);
for (String line : lines) {
User user = User.fromString(line);
if (user != null) usersByEmail.put(user.getEmail(), user);
}
}
private void saveUsers() {
try (BufferedWriter writer = Files.newBufferedWriter(usersFile, StandardCharsets.UTF_8)) {
for (User user : usersByEmail.values()) writer.write(user.toString() + "\n");
} catch (IOException e) {
System.err.println("保存用户时出错: " + e.getMessage());
}
}
public User authenticate(String identifier, String password) {
String hashedInput = hashPassword(password);
// 策略一:尝试将 identifier 作为邮箱进行快速查找
User userByEmail = usersByEmail.get(identifier);
if (userByEmail != null && userByEmail.getHashedPassword().equals(hashedInput)) {
return userByEmail; // 邮箱匹配成功
}
// 策略二:如果邮箱查找失败,则遍历所有用户,尝试匹配用户名
for (User user : usersByEmail.values()) {
if (user.getUsername() != null && user.getUsername().equals(identifier)) {
if (user.getHashedPassword().equals(hashedInput)) {
return user; // 用户名匹配成功
}
break; // 用户名是唯一的,找到后无需继续遍历
}
}
return null; // 认证失败
}
public boolean emailExists(String email) {
return usersByEmail.containsKey(email);
}
public boolean usernameExists(String username) {
if (username == null || username.trim().isEmpty()) return false;
for (User user : usersByEmail.values()) {
if (username.equals(user.getUsername())) {
return true;
}
}
return false;
}
public void registerUser(String email, String username, String password, UserType type) {
User newUser = new User(email, username, "", type);
newUser.setHashedPassword(password);
usersByEmail.put(email, newUser);
saveUsers();
}
public void updateUser(User user) {
if (usersByEmail.containsKey(user.getEmail())) {
usersByEmail.put(user.getEmail(), user);
saveUsers();
}
}
private String hashPassword(String password) {
return Integer.toString(password.hashCode());
}
}

@ -0,0 +1,25 @@
package service;
public class ValidationService {
public static boolean isValidEmail(String email) {
if (email == null || email.trim().isEmpty()) {
return false;
}
String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
return email.matches(emailRegex);
}
public static boolean isPasswordStrong(String password) {
if (password == null || password.length() < 6 || password.length() > 10) {
return false;
}
boolean hasUpperCase = password.matches(".*[A-Z].*");
boolean hasLowerCase = password.matches(".*[a-z].*");
boolean hasDigit = password.matches(".*[0-9].*");
return hasUpperCase && hasLowerCase && hasDigit;
}
}

@ -0,0 +1,61 @@
package service.generator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public abstract class AbstractQuestionGenerator implements QuestionGenerator {
protected final Random rand = new Random();
private static final char[] OPS = {'+', '-', '×', '÷'};
protected String buildComplexExpression(List<String> operands) {
int numOperands = operands.size();
if (numOperands < 2) return operands.get(0); // 如果只有一个操作数,直接返回
List<Character> operators = new ArrayList<>();
for (int i = 0; i < numOperands - 1; i++) {
operators.add(OPS[rand.nextInt(OPS.length)]);
}
int parensType = rand.nextInt(5); // 增加不加括号的概率
switch (parensType) {
case 1: // 单括号 (a op b) op c ...
if (numOperands >= 3) {
Collections.swap(operands, 0, rand.nextInt(numOperands));
Collections.swap(operands, 1, rand.nextInt(numOperands));
return String.format("(%s %c %s) %c %s", operands.get(0), operators.get(0), operands.get(1), operators.get(1), buildRest(operands, operators, 2));
}
break;
case 2: // a op (b op c) op d ...
if (numOperands >= 3) {
return String.format("%s %c (%s %c %s)", operands.get(0), operators.get(0), operands.get(1), operators.get(1), buildRest(operands, operators, 2));
}
break;
case 3: // 双重括号 ((a op b) op c) op d ...
if (numOperands >= 4) {
return String.format("((%s %c %s) %c %s) %c %s", operands.get(0), operators.get(0), operands.get(1), operators.get(1), operands.get(2), operators.get(2), buildRest(operands, operators, 3));
}
break;
case 4: // 双重括号 a op (b op (c op d)) ...
if (numOperands >= 4) {
return String.format("%s %c (%s %c (%s %c %s))", operands.get(0), operators.get(0), operands.get(1), operators.get(1), operands.get(2), operators.get(2), buildRest(operands, operators, 3));
}
break;
}
// 默认情况:无括号或无法应用括号
return buildRest(operands, operators, 0);
}
private String buildRest(List<String> operands, List<Character> operators, int startIndex) {
StringBuilder sb = new StringBuilder();
sb.append(operands.get(startIndex));
for (int i = startIndex; i < operators.size(); i++) {
sb.append(" ").append(operators.get(i)).append(" ");
sb.append(operands.get(i + 1));
}
return sb.toString();
}
}

@ -0,0 +1,49 @@
package service.generator;
import java.util.Random;
public class JuniorQuestionGenerator implements QuestionGenerator {
private final Random rand = new Random();
@Override
public String generateQuestion() {
int numOperands = rand.nextInt(4) + 2; // 2-5个操作数
String[] operands = new String[numOperands];
// 先填充普通数字
for (int i = 0; i < numOperands; i++) {
operands[i] = String.valueOf(num(15));
}
// 随机替换一个为特殊运算
int specialIndex = rand.nextInt(numOperands);
operands[specialIndex] = specialOp();
// 根据操作数数量选择模板
switch (numOperands) {
case 2:
return String.format("%s %c %s", operands[0], op(), operands[1]);
case 3:
return String.format("(%s %c %s) %c %s", operands[0], opLow(), operands[1], opHigh(), operands[2]);
case 4:
return String.format("(%s %c %s) %c (%s %c %s)", operands[0], opHigh(), operands[1], opLow(), operands[2], opLow(), operands[3]);
case 5:
default:
return String.format("((%s %c %s) %c %s) %c (%s %c %s)", operands[0], opLow(), operands[1], opHigh(), operands[2], opLow(), operands[3], opLow(), operands[4]);
}
}
// --- 辅助方法 ---
private int num(int bound) { return rand.nextInt(bound) + 1; }
private char op() { return "+-×".charAt(rand.nextInt(3)); }
private char opLow() { return "+-".charAt(rand.nextInt(2)); }
private char opHigh() { return '×'; }
private String specialOp() {
if (rand.nextBoolean()) {
return (rand.nextInt(9) + 2) + "^2"; // 2^2 to 10^2
} else {
int base = rand.nextInt(10) + 1;
return "√(" + (base * base) + ")";
}
}
}

@ -0,0 +1,69 @@
package service.generator;
import java.util.Random;
public class PrimaryQuestionGenerator implements QuestionGenerator {
private final Random rand = new Random();
@Override
public String generateQuestion() {
int numOperands = rand.nextInt(4) + 2; // 随机生成2-5个操作数
switch (numOperands) {
case 2:
return simpleExpression(num(50), op(), num(50));
case 3:
return generateThreeOperandExpression();
case 4:
return generateFourOperandExpression();
case 5:
default:
return generateFiveOperandExpression();
}
}
private String generateThreeOperandExpression() {
if (rand.nextBoolean()) {
// 模板: (a + b) * c
return String.format("(%d %c %d) %c %d", num(20), opLow(), num(20), opHigh(), num(10));
} else {
// 模板: a * (b - c)
int b = num(29) + 1; // b 的范围是 2 到 30
int c = num(b - 1); // c 的范围是 1 到 b-1确保 c < b
return String.format("%d %c (%d %c %d)", num(10), opHigh(), b, opLow(), c);
}
}
private String generateFourOperandExpression() {
if (rand.nextBoolean()) {
// 模板: ((a + b) * c) - d
return String.format("((%d %c %d) %c %d) %c %d", num(10), opLow(), num(10), opHigh(), num(5), opLow(), num(20));
} else {
// 模板: a * (b - (c + d))
return String.format("%d %c (%d %c (%d %c %d))", num(5), opHigh(), num(50), opLow(), num(10), opLow(), num(10));
}
}
private String generateFiveOperandExpression() {
// 模板: (a * b) + (c - d) * e
int c = num(29) + 1; // c 的范围是 2 到 30
int d = num(c - 1); // d 的范围是 1 到 c-1确保 d < c
return String.format("(%d %c %d) %c (%d %c %d) %c %d", num(10), opHigh(), num(10), opLow(), c, opLow(), d, opHigh(), num(5));
}
// --- 辅助方法 ---
private int num(int bound) {
// 健壮性检查防止外部调用传入无效的bound
if (bound <= 0) return 1;
return rand.nextInt(bound) + 1;
}
private char op() { return "+-×÷".charAt(rand.nextInt(4)); }
private char opLow() { return "+-".charAt(rand.nextInt(2)); } // 低优先级
private char opHigh() { return "×÷".charAt(rand.nextInt(2)); } // 高优先级
private String simpleExpression(int a, char op, int b) {
if (op == '÷') a = b * num(10);
if (op == '-') a = Math.max(a, b + 1);
return String.format("%d %c %d", a, op, b);
}
}

@ -0,0 +1,5 @@
package service.generator;
public interface QuestionGenerator {
String generateQuestion();
}

@ -0,0 +1,51 @@
package service.generator;
import java.util.Random;
public class SeniorQuestionGenerator implements QuestionGenerator {
private final Random rand = new Random();
private static final String[] TRIG_FUNCS = {"sin", "cos", "tan"};
private static final int[] COMMON_ANGLES = {0, 30, 45, 60, 90,135,180};
private static final int[] TAN_SAFE_ANGLES = {0, 30, 45, 60,135};
@Override
public String generateQuestion() {
int numOperands = rand.nextInt(3) + 2; // 2-4个操作数以控制复杂度
String[] operands = new String[numOperands];
for (int i = 0; i < numOperands; i++) {
operands[i] = String.valueOf(num(10));
}
int specialIndex = rand.nextInt(numOperands);
operands[specialIndex] = specialOp();
switch (numOperands) {
case 2:
return String.format("%s %c %s", operands[0], op(), operands[1]);
case 3:
return String.format("(%s %c %s) %c %s", operands[0], opLow(), operands[1], opHigh(), operands[2]);
case 4:
default:
return String.format("%s %c (%s %c (%s %c %s))", operands[0], opHigh(), operands[1], opLow(), operands[2], opLow(), operands[3]);
}
}
// --- 辅助方法 ---
private int num(int bound) { return rand.nextInt(bound) + 1; }
private char op() { return "+-×".charAt(rand.nextInt(3)); }
private char opLow() { return "+-".charAt(rand.nextInt(2)); }
private char opHigh() { return '×'; }
private String specialOp() {
String func = TRIG_FUNCS[rand.nextInt(TRIG_FUNCS.length)];
int angle;
if (func.equals("tan")) {
angle = TAN_SAFE_ANGLES[rand.nextInt(TAN_SAFE_ANGLES.length)];
} else {
angle = COMMON_ANGLES[rand.nextInt(COMMON_ANGLES.length)];
}
return String.format("%s(%d°)", func, angle);
}
}

@ -1,140 +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; }
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; }
}

@ -1,64 +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("");
}
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("");
}
}

@ -1,27 +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);
}
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);
}
}

@ -1,72 +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; }
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; }
}
Loading…
Cancel
Save