正式版2.0.0 #13

Merged
hnu202326010319 merged 7 commits from develop into main 4 months ago

4
.gitignore vendored

@ -8,6 +8,7 @@ target/
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/workspace.xml
*.iws
*.iml
*.ipr
@ -35,4 +36,5 @@ build/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store.idea/workspace.xml
package.txt

@ -10,17 +10,18 @@
</component>
<component name="ChangeListManager">
<list default="true" id="ad9e49ad-e421-455c-8a89-0f35a7a15146" name="Changes" comment="发行版1.03">
<change afterPath="$PROJECT_DIR$/doc/测试文档/发行版1.04测试.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/pair/util/AsyncRegistrationHelper.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/StyleHelper.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/doc/测试文档/发行版1.03测试.md" beforeDir="false" afterPath="$PROJECT_DIR$/doc/测试文档/发行版1.03测试.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/model/QuizResult.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/model/QuizResult.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/service/UserService.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/service/UserService.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/InfGenPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/InfGenPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/LoginPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/LoginPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/MainWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/MainWindow.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/NavigablePanel.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/NavigablePanel.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/util/PasswordValidator.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/util/PasswordValidator.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/PasswordModifyPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/PasswordModifyPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/QuizPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/QuizPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/RegisterPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/RegisterPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/ResultPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/ResultPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/StartPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/StartPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/UIConstants.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/UIConstants.java" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -68,7 +69,7 @@
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"git-widget-placeholder": "LiangJunYaoBranch",
"git-widget-placeholder": "develop",
"kotlin-language-version-configured": "true",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
@ -112,8 +113,8 @@
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-28b599e66164-intellij.indexing.shared.core-IU-242.23726.103" />
<option value="bundled-js-predefined-d6986cc7102b-5c90d61e3bab-JavaScript-IU-242.23726.103" />
<option value="bundled-jdk-9823dce3aa75-fdfe4dae3a2d-intellij.indexing.shared.core-IU-243.21565.193" />
<option value="bundled-js-predefined-d6986cc7102b-e768b9ed790e-JavaScript-IU-243.21565.193" />
</set>
</attachedChunks>
</component>
@ -135,6 +136,7 @@
<workItem from="1759758921108" duration="6306000" />
<workItem from="1759815278314" duration="2288000" />
<workItem from="1759817587775" duration="661000" />
<workItem from="1759936802621" duration="762000" />
</task>
<task id="LOCAL-00001" summary="ui design v1">
<option name="closed" value="true" />

@ -1,10 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 新增Spring Boot父依赖必要用于插件版本管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.12</version>
<relativePath/>
</parent>
<groupId>com.mathquiz</groupId>
<artifactId>MathQuizApp</artifactId>
<version>1.04</version>
@ -13,22 +21,29 @@
<name>Math Quiz Application</name>
<description>小初高数学学习软件- JavaFX版本</description>
<!-- 新增 JitPack 仓库配置 -->
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url> <!-- 已移除多余空格 -->
<url>https://jitpack.io</url>
</repository>
</repositories>
<properties>
<!-- 其他原有配置 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<javafx.version>21.0.2</javafx.version>
<spring-boot.maven.plugin.version>3.1.12</spring-boot.maven.plugin.version>
<!-- 新增:覆盖漏洞依赖的安全版本 -->
<logback.version>1.5.13</logback.version> <!-- 修复logback漏洞 -->
<snakeyaml.version>2.2</snakeyaml.version> <!-- 修复snakeyaml高风险漏洞 -->
<spring.version>6.0.22</spring.version> <!-- 升级Spring核心依赖到安全版本 -->
</properties>
<dependencies>
<!-- 保留原有依赖 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
@ -56,25 +71,28 @@
<version>${javafx.version}</version>
</dependency>
<!-- JavaMail API -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<!-- 新增JavaMail 在 Java 9+ 必需的依赖 -->
<dependency>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<!-- 新增Spring Boot基础依赖必要否则插件可能无法工作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 保留编译器插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@ -86,6 +104,7 @@
</configuration>
</plugin>
<!-- 保留资源插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
@ -101,74 +120,39 @@
<version>3.6.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<id>copy-javafx-dependencies</id>
<phase>compile</phase> <!-- 编译阶段就复制,确保运行前可用 -->
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dependencies</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
<includeGroupIds>org.openjfx</includeGroupIds> <!-- 仅复制JavaFX相关依赖 -->
<outputDirectory>${project.build.directory}/javafx-libs</outputDirectory> <!-- 复制到target/javafx-libs -->
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven Shade插件用于创建可执行JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.maven.plugin.version}</version>
<configuration>
<mainClass>com.pair.Test</mainClass> <!-- 确保主类正确 -->
<jvmArguments>
<!-- 指向复制的JavaFX依赖目录 -->
--module-path ${project.build.directory}/javafx-libs --add-modules javafx.controls,javafx.fxml,javafx.graphics,javafx.base
--add-modules javafx.controls,javafx.fxml,javafx.graphics,javafx.base
</jvmArguments>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
<goal>repackage</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.pair.Test</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<finalName>MathQuizApp</finalName>
</configuration>
</execution>
</executions>
</plugin>
<!-- 支持 mvn javafx:run 启动的插件配置 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- 指定启动类全路径 -->
<mainClass>com.pair.Test</mainClass>
<!-- 传递JVM参数指定需要的JavaFX模块 -->
<arguments>
<argument>--add-modules</argument>
<argument>javafx.controls,javafx.fxml,javafx.graphics,javafx.base</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
</project>

@ -41,7 +41,6 @@ public class FileIOService {
public void initDataDirectory() throws IOException {
FileUtils.createDirectoryIfNotExists(DATA_DIR);
FileUtils.createDirectoryIfNotExists(USERS_DIR);
FileUtils.createDirectoryIfNotExists(HISTORY_DIR);
FileUtils.ensureFileExists(REGISTRATION_CODES_FILE);
@ -101,6 +100,10 @@ public class FileIOService {
return null;
}
public boolean existsUsername(String username) throws IOException {
return findUserByUsername(username) != null;
}
public User findUserByEmail(String email) throws IOException {
List<User> users = loadAllUsers();
@ -113,6 +116,10 @@ public class FileIOService {
return null;
}
public boolean existsEmail(String email) throws IOException {
return findUserByEmail(email) != null;
}
public boolean isUsernameExists(String username) throws IOException {
return findUserByUsername(username) != null;
}
@ -136,10 +143,68 @@ public class FileIOService {
FileUtils.deleteFile(CURRENT_USER_FILE);
}
// ==================== 答题历史操作 ====================
public void saveQuizHistory(QuizHistory history) throws IOException {
String filename = HISTORY_DIR + "/" +
sanitizeFilename(history.getUsername()) + "/" +
System.currentTimeMillis() + ".txt";
StringBuilder content = new StringBuilder();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
content.append("========== 答题记录 ==========\n");
content.append("用户:").append(history.getUsername()).append("\n");
content.append("时间:").append(dateFormat.format(history.getTimestamp())).append("\n");
content.append("总分:").append(history.getScore()).append(" 分\n");
// 调用 QuizService 的业务方法计算正确数和错误数
int correctCount = calculateCorrectCount(history);
int wrongCount = history.getQuestions().size() - correctCount;
content.append("正确:").append(correctCount).append(" 题 ");
content.append("错误:").append(wrongCount).append(" 题\n");
content.append("=============================\n\n");
List<ChoiceQuestion> questions = history.getQuestions();
List<Integer> userAnswers = history.getUserAnswers();
for (int i = 0; i < questions.size(); i++) {
ChoiceQuestion q = questions.get(i);
Integer userAnswer = userAnswers.get(i);
content.append("【题目 ").append(i + 1).append("】\n");
content.append(q.getQuestionText()).append("\n");
List<?> options = q.getOptions();
for (int j = 0; j < options.size(); j++) {
content.append((char)('A' + j)).append(". ")
.append(options.get(j)).append(" ");
}
content.append("\n");
int correctIndex = getCorrectAnswerIndex(q);
content.append("正确答案:").append((char)('A' + correctIndex)).append("\n");
content.append("用户答案:");
if (userAnswer != null) {
content.append((char)('A' + userAnswer));
} else {
content.append("未作答");
}
content.append("\n");
boolean isCorrect = (userAnswer != null && userAnswer == correctIndex);
content.append("结果:").append(isCorrect ? "✓ 正确" : "✗ 错误").append("\n\n");
}
FileUtils.writeStringToFile(filename, content.toString());
}
public List<String> getHistoryQuestions() throws IOException {
public List<String> getHistoryQuestions(String username) throws IOException {
List<String> historyQuestions = new ArrayList<>();
File[] files = FileUtils.listFiles(HISTORY_DIR);
File[] files = FileUtils.listFiles(HISTORY_DIR + "/" + username);
Arrays.sort(files, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));

@ -20,14 +20,6 @@ public class QuizService {
// ==================== 构造方法 ====================
public QuizService() throws IOException {
this.fileIOService = new FileIOService();
this.userService = new UserService(fileIOService);
this.currentQuestions = new ArrayList<>();
this.userAnswers = new ArrayList<>();
this.currentQuestionIndex = 0;
}
public QuizService(FileIOService fileIOService, UserService userService) {
this.fileIOService = fileIOService;
this.userService = userService;
@ -406,6 +398,32 @@ public class QuizService {
return sb.toString();
}
// ==================== 数据持久化 ====================
public void saveQuizHistory() {
QuizResult result = calculateResult();
QuizHistory history = new QuizHistory(
userService.getCurrentUser().getUsername(),
new Date(),
currentQuestions,
userAnswers,
result.getScore()
);
try {
fileIOService.saveQuizHistory(history);
} catch (IOException e) {
System.err.println(e);
}
try {
userService.updateUserStatistics(userService.getCurrentUser(), result.getScore());
} catch (IOException e) {
System.err.println(e);
}
System.out.println("✓ 答题记录已保存");
}
// ==================== Getters ====================

@ -45,7 +45,6 @@ public class UserService {
/**
*
*
* @param email
* @return
*/
@ -174,9 +173,8 @@ public class UserService {
/**
*
*
* @param email
* @param code
* @param code
* @return true
*/
public boolean verifyRegistrationCode(String email, String code) throws IOException {
@ -415,11 +413,17 @@ public class UserService {
// ==================== 用户信息管理 ====================
public boolean updateEmail(User user, String newEmail) throws IOException {
public void updateEmail(User user, String newEmail) throws IOException {
if (!validateEmail(newEmail)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
if (user.getEmail().equals(newEmail)) {
return ;
}
boolean exist = fileIOService.isEmailExists(newEmail);
if (exist) {
throw new IllegalArgumentException("邮箱已存在!");
}
user.setEmail(newEmail);
fileIOService.saveUser(user);
@ -428,14 +432,19 @@ public class UserService {
fileIOService.saveCurrentUser(user);
}
// System.out.println("✓ 邮箱更新成功");
return true;
}
public void updateUsername(User user, String newUsername) throws IOException {
if (newUsername.isEmpty()) {
throw new IllegalArgumentException("用户名不为空!");
}
if (user.getUsername().equals(newUsername)) {
return;
}
boolean exist = fileIOService.existsUsername(newUsername);
if (exist) {
throw new IllegalArgumentException("用户名已存在!");
}
user.setUsername(newUsername);
fileIOService.saveUser(user);
@ -481,6 +490,12 @@ public class UserService {
return fileIOService.findUserByUsername(username);
}
// ==================== 业务逻辑方法===================
/**
*
*/
public String getGradeDisplayName(User user) {
if (user == null || user.getGrade() == null) {
return "未知";
@ -514,4 +529,5 @@ public class UserService {
return sb.toString();
}
}

@ -1,14 +1,18 @@
// com/ui/InfGenPage.java
package com.pair.ui;
import com.pair.model.Grade;
import com.pair.service.UserService;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.layout.Priority;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.TextAlignment;
import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;
public class InfGenPage extends NavigablePanel {
@ -19,122 +23,336 @@ public class InfGenPage extends NavigablePanel {
private final Spinner<Integer> questionCountSpinner = new Spinner<>(10, 30, 10);
private final Button passwordModifyButton = new Button("修改密码");
private final Button generateButton = new Button("生成题目");
private final Button modifyUsernameButton = new Button("修改用户名"); // 新增
private final Button modifyEmailButton = new Button("修改邮箱"); // 新增
private final Button modifyUsernameButton = new Button("修改用户名");
private final Button modifyEmailButton = new Button("修改邮箱");
public InfGenPage(Runnable onBack, String currentUsername, String currentEmail, Grade currentGrade) {
// 用户统计信息标签
private final Label totalQuizzesLabel = new Label("0");
private final Label averageScoreLabel = new Label("0.0");
public InfGenPage(Runnable onBack, String currentUsername, String currentEmail, Grade currentGrade, int totalQuizzes, double averageScore) {
super(onBack);
initializeContent();
usernameField.setText(currentUsername);
emailField.setText(currentEmail);
gradeChoice.getSelectionModel().select(currentGrade.ordinal());
// 更新统计信息
updateStatistics(totalQuizzes, averageScore);
}
@Override
protected void buildContent() {
// 外层容器VBox 居中,带内边距和圆角阴影
VBox form = new VBox(UIConstants.DEFAULT_SPACING * 1.5);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
form.setMaxWidth(500);
// 标题居中
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
titleLabel.setTextAlignment(TextAlignment.CENTER);
titleLabel.setMaxWidth(Double.MAX_VALUE);
// ========== 创建表单项行(关键:标签左对齐 + 固定宽度)==========
HBox usernameRow = createFormRow("用户名:", usernameField, modifyUsernameButton);
HBox emailRow = createFormRow("邮箱:", emailField, modifyEmailButton);
HBox passwordRow = createFormRow("密码:", passwordLabel, passwordModifyButton);
HBox gradeRow = createFormRow("学段选择:", gradeChoice, null);
HBox countRow = createFormRow("题目数量:", questionCountSpinner, generateButton);
// ========== 配置控件样式 ==========
// 用户名输入框
usernameField.setStyle(UIConstants.INPUT_STYLE);
usernameField.setPrefWidth(200);
usernameField.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
// 密码标签
passwordLabel.setStyle("-fx-font-size: " + UIConstants.INPUT_FONT_SIZE + "px;");
// 邮箱输入框
emailField.setStyle(UIConstants.INPUT_STYLE);
emailField.setPrefWidth(200);
emailField.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
// 学段选择框
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE));
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);"
);
// 初始化控件样式
gradeChoice.getItems().addAll("小学", "初中", "高中");
gradeChoice.setValue("小学");
gradeChoice.setStyle(UIConstants.INPUT_STYLE);
gradeChoice.setPrefWidth(200);
// 题目数量Spinner
questionCountSpinner.setEditable(true);
questionCountSpinner.setPrefWidth(200);
questionCountSpinner.getEditor().setStyle(UIConstants.INPUT_STYLE);
questionCountSpinner.getEditor().setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
// 按钮样式统一
passwordModifyButton.setStyle(UIConstants.BUTTON_STYLE);
generateButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
generateButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
generateButton.setStyle(UIConstants.BUTTON_STYLE);
// 新增按钮样式
modifyUsernameButton.setStyle(UIConstants.BUTTON_STYLE);
modifyUsernameButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
modifyUsernameButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
modifyEmailButton.setStyle(UIConstants.BUTTON_STYLE);
modifyEmailButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
modifyEmailButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
// ========== 添加到表单 ==========
form.getChildren().addAll(
titleLabel,
usernameRow,
emailRow,
passwordRow,
gradeRow,
countRow
StyleHelper.styleTextField(usernameField, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleTextField(emailField, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleChoiceBox(gradeChoice, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleSpinner(questionCountSpinner);
passwordLabel.setStyle("-fx-font-size: " + UIConstants.BODY_FONT_SIZE + "px; -fx-pref-width: " + UIConstants.INPUT_MEDIUM_WIDTH + "px;");
// 按钮样式(注意:你类中已有 styleInfoButton / stylePrimaryButton直接调用
styleInfoButton(passwordModifyButton);
stylePrimaryButton(generateButton);
styleInfoButton(modifyUsernameButton);
styleInfoButton(modifyEmailButton);
VBox.setMargin(title, new Insets(0, 0, UIConstants.XLARGE_SPACING, 0));
// ===== 新布局:左侧表单 + 右侧统计 =====
HBox mainContent = new HBox(60); // 增加间距避免拥挤
mainContent.setAlignment(Pos.TOP_CENTER);
mainContent.setMinWidth(Region.USE_PREF_SIZE);
// ========== 左侧:用户信息 + 答题设置 ==========
VBox leftPanel = new VBox(UIConstants.LARGE_SPACING);
leftPanel.setAlignment(Pos.TOP_LEFT);
leftPanel.setMinWidth(450); // 增加最小宽度
leftPanel.setMaxWidth(500); // 增加最大宽度
leftPanel.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0; -fx-border-radius: 8; -fx-padding: 25;");
// --- 用户信息 ---
Label userInfoTitle = new Label("用户信息");
userInfoTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
userInfoTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
VBox userForm = new VBox(UIConstants.MEDIUM_SPACING);
userForm.getChildren().addAll(
createRow("用户名:", usernameField, modifyUsernameButton),
createRow("邮箱:", emailField, modifyEmailButton),
createRow("密码:", passwordLabel, passwordModifyButton)
);
this.setCenter(form);
// --- 答题设置 ---
Label quizSettingsTitle = new Label("答题设置");
quizSettingsTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
quizSettingsTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
VBox quizSettings = new VBox(UIConstants.MEDIUM_SPACING);
quizSettings.getChildren().addAll(
createRow("学段选择:", gradeChoice, null),
createRow("题目数量:", questionCountSpinner, generateButton)
);
// 组装左侧
leftPanel.getChildren().addAll(userInfoTitle, userForm, new Separator(), quizSettingsTitle, quizSettings);
// ========== 右侧:仅学习统计 ==========
VBox rightPanel = new VBox();
rightPanel.setAlignment(Pos.CENTER); // 统计区域垂直+水平居中
rightPanel.setMinWidth(340); // 增加最小宽度
rightPanel.setMaxWidth(400); // 增加最大宽度
VBox statsArea = createStatsArea();
rightPanel.getChildren().add(statsArea);
// 组装主内容
mainContent.getChildren().addAll(leftPanel, rightPanel);
// 组装完整卡片
card.getChildren().addAll(title, mainContent);
setCenter(card);
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
}
/**
*
*/
private HBox createFormRow(String labelText, Control content, Button rightButton) {
HBox row = new HBox(15); // 间距15
row.setAlignment(Pos.CENTER_LEFT); // ← 关键:让整行左对齐
row.setMaxWidth(Double.MAX_VALUE);
// 标签:左对齐,固定宽度,字体统一
Label label = new Label(labelText);
label.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.NORMAL, UIConstants.LABEL_ITEM_TITLE_SIZE));
label.setTextAlignment(TextAlignment.LEFT); // ← 关键:标签文字左对齐
label.setPrefWidth(120); // 固定宽度,确保所有标签对齐
row.getChildren().addAll(label, content);
if (rightButton != null) {
row.getChildren().add(rightButton);
private HBox createRow(String text, Control field, Button btn) {
HBox row = new HBox(20); // 间距适中
row.setAlignment(Pos.CENTER_LEFT);
row.setPadding(new Insets(8, 0, 8, 0));
// 左侧标签容器 - 确保宽度足够
VBox labelContainer = new VBox();
labelContainer.setPrefWidth(130); // 足够显示“用户名:”等标签
labelContainer.setAlignment(Pos.CENTER_RIGHT);
Label label = new Label(text);
label.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
label.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT) + ";");
labelContainer.getChildren().add(label);
// 中间字段容器 - 不再强制拉伸,避免挤压按钮
VBox fieldContainer = new VBox();
fieldContainer.setAlignment(Pos.CENTER_LEFT);
if (field != null) {
fieldContainer.getChildren().add(field);
if (field instanceof TextField || field instanceof PasswordField) {
field.setStyle(field.getStyle() + "-fx-padding: 8 12 8 12;");
} else if (field instanceof ChoiceBox) {
field.setStyle(field.getStyle() + "-fx-pref-width: 180;");
} else if (field instanceof Spinner) {
field.setStyle(field.getStyle() + "-fx-pref-width: 180;");
}
}
// 右侧按钮容器 - 关键:移除左边距,让按钮自然对齐
VBox buttonContainer = new VBox();
buttonContainer.setAlignment(Pos.CENTER_LEFT);
if (btn != null) {
buttonContainer.getChildren().add(btn);
// 👇 移除这个!它会导致按钮错位
// VBox.setMargin(btn, new Insets(0, 0, 0, 20)); ← 删除这行!
}
// ⚠️ 重要:不要对任何容器设置 Hgrow避免布局挤压
HBox.setHgrow(labelContainer, Priority.NEVER);
HBox.setHgrow(fieldContainer, Priority.NEVER);
HBox.setHgrow(buttonContainer, Priority.NEVER);
row.getChildren().addAll(labelContainer, fieldContainer, buttonContainer);
return row;
}
// Getters...
public TextField getUsernameField() { return usernameField; }
public TextField getEmailField() { return emailField; }
public ChoiceBox<String> getGradeChoice() { return gradeChoice; }
public Spinner<Integer> getQuestionCountSpinner() { return questionCountSpinner; }
public Button getGenerateButton() { return generateButton; }
public Button getPasswordModifyButton() { return passwordModifyButton; }
public Button getModifyUsernameButton() { return modifyUsernameButton; } // 新增
public Button getModifyEmailButton() { return modifyEmailButton; } // 新增
public TextField getUsernameField() {
return usernameField;
}
public TextField getEmailField() {
return emailField;
}
public ChoiceBox<String> getGradeChoice() {
return gradeChoice;
}
public Spinner<Integer> getQuestionCountSpinner() {
return questionCountSpinner;
}
public Button getGenerateButton() {
return generateButton;
}
public Button getPasswordModifyButton() {
return passwordModifyButton;
}
public Button getModifyUsernameButton() {
return modifyUsernameButton;
}
public Button getModifyEmailButton() {
return modifyEmailButton;
}
/**
* - 使
*/
private void styleInfoButton(Button btn) {
String baseStyle =
"-fx-background-color: linear-gradient(to bottom, #4CAF50, #45a049); " +
"-fx-text-fill: white; " +
"-fx-font-weight: bold; " +
"-fx-background-radius: 8; " +
"-fx-border-radius: 8; " +
"-fx-border-color: transparent; " +
"-fx-border-width: 2; " +
"-fx-font-family: '" + UIConstants.FONT_FAMILY + "'; " +
"-fx-font-size: " + UIConstants.BTN_FONT_SIZE + "px; " +
"-fx-cursor: hand; " +
"-fx-padding: 8 16 8 16; "; // 固定内边距,让文字有呼吸空间
String normalStyle = baseStyle +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
String hoverStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #45a049, #3d8b40); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
String pressedStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #3d8b40, #357a38); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 4, 0, 0, 1);";
btn.setStyle(normalStyle);
btn.setOnMouseEntered(e -> btn.setStyle(hoverStyle));
btn.setOnMouseExited(e -> btn.setStyle(normalStyle));
btn.setOnMousePressed(e -> btn.setStyle(pressedStyle));
btn.setOnMouseReleased(e -> btn.setStyle(normalStyle));
// ✅ 关键修复:不要硬编码宽度!让按钮自动适应内容
btn.setMinWidth(Region.USE_PREF_SIZE);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setPrefWidth(Region.USE_COMPUTED_SIZE);
}
/**
*
*/
private VBox createStatsArea() {
VBox statsBox = new VBox(20); // 增加间距
statsBox.setAlignment(Pos.CENTER);
statsBox.setPadding(new Insets(25)); // 增加内边距
statsBox.setStyle(
"-fx-background-color: linear-gradient(to bottom, " + UIConstants.toWeb(UIConstants.COLOR_LIGHT) + ", " + UIConstants.toWeb(UIConstants.COLOR_BG) + "); " +
"-fx-background-radius: 15; " +
"-fx-border-radius: 15; " +
"-fx-border-color: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-border-width: 2; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.15), 12, 0, 0, 6);"
);
statsBox.setMinWidth(250);
statsBox.setMaxWidth(300);
// 标题
Label statsTitle = new Label("📊 学习统计");
statsTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
statsTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 总答题次数
VBox quizzesBox = new VBox(8);
quizzesBox.setAlignment(Pos.CENTER);
quizzesBox.setStyle(
"-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_SUCCESS) + "20; " +
"-fx-background-radius: 10; " +
"-fx-padding: 15;"
);
Label quizzesTitle = new Label("总答题次数");
quizzesTitle.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
quizzesTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
totalQuizzesLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 28));
totalQuizzesLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_SUCCESS) + ";");
quizzesBox.getChildren().addAll(quizzesTitle, totalQuizzesLabel);
// 平均分
VBox scoreBox = new VBox(8);
scoreBox.setAlignment(Pos.CENTER);
scoreBox.setStyle(
"-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_WARNING) + "20; " +
"-fx-background-radius: 10; " +
"-fx-padding: 15;"
);
Label scoreTitle = new Label("平均分");
scoreTitle.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
scoreTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
averageScoreLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 28));
averageScoreLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_WARNING) + ";");
scoreBox.getChildren().addAll(scoreTitle, averageScoreLabel);
// 添加学习建议标签
Label suggestionLabel = new Label("继续加油,提升数学能力!");
suggestionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
suggestionLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + "; " +
"-fx-font-style: italic;");
statsBox.getChildren().addAll(statsTitle, quizzesBox, scoreBox, suggestionLabel);
return statsBox;
}
/**
*
*/
public void updateStatistics(int totalQuizzes, double averageScore) {
totalQuizzesLabel.setText(String.valueOf(totalQuizzes));
averageScoreLabel.setText(String.format("%.1f", averageScore));
}
/**
* - 使
*/
private void stylePrimaryButton(Button btn) {
String baseStyle =
"-fx-background-color: linear-gradient(to bottom, #FF9800, #F57C00); " +
"-fx-text-fill: white; " +
"-fx-font-weight: bold; " +
"-fx-background-radius: 8; " +
"-fx-border-radius: 8; " +
"-fx-border-color: transparent; " +
"-fx-border-width: 2; " +
"-fx-font-family: '" + UIConstants.FONT_FAMILY + "'; " +
"-fx-font-size: " + (UIConstants.BTN_FONT_SIZE + 1) + "px; " +
"-fx-cursor: hand; " +
"-fx-padding: 10 20 10 20; ";
String normalStyle = baseStyle +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
String hoverStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #F57C00, #EF6C00); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 10, 0, 0, 4);";
String pressedStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #EF6C00, #E65100); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
btn.setStyle(normalStyle);
btn.setOnMouseEntered(e -> btn.setStyle(hoverStyle));
btn.setOnMouseExited(e -> btn.setStyle(normalStyle));
btn.setOnMousePressed(e -> btn.setStyle(pressedStyle));
btn.setOnMouseReleased(e -> btn.setStyle(normalStyle));
// ✅ 关键修复:让“生成题目”按钮也自适应宽度
btn.setMinWidth(Region.USE_PREF_SIZE);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setPrefWidth(Region.USE_COMPUTED_SIZE);
}
}

@ -1,9 +1,7 @@
// com/ui/LoginPage.java
package com.pair.ui;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
@ -17,38 +15,35 @@ public class LoginPage extends NavigablePanel {
public LoginPage(Runnable onBack) {
super(onBack);
initializeContent(); // 字段初始化
initializeContent();
}
@Override
protected void buildContent() {
VBox form = new VBox(UIConstants.DEFAULT_SPACING);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
// 使用统一的输入框样式
usernameOrEmailField.setPromptText("邮箱/用户名");
usernameOrEmailField.setStyle(UIConstants.INPUT_STYLE);
StyleHelper.styleInputField(usernameOrEmailField);
passwordField.setPromptText("密码6-10位含大小写字母和数字");
passwordField.setStyle(UIConstants.INPUT_STYLE);
loginButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
loginButton.setStyle(UIConstants.BUTTON_STYLE);
loginButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
loginButton.setOnMouseEntered(e -> loginButton.setStyle(UIConstants.BUTTON_STYLE + UIConstants.BUTTON_HOVER_STYLE));
loginButton.setOnMouseExited(e -> loginButton.setStyle(UIConstants.BUTTON_STYLE));
registerLink.setStyle("-fx-text-fill: " + UIConstants.COLOR_ACCENT + ";");
StyleHelper.stylePasswordField(passwordField);
HBox linkBox = new HBox(registerLink);
linkBox.setAlignment(Pos.CENTER);
// 使用统一的按钮样式
StyleHelper.stylePrimaryButton(loginButton);
registerLink.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
form.getChildren().addAll(titleLabel, usernameOrEmailField, passwordField, loginButton, linkBox);
this.setCenter(form);
card.getChildren().addAll(title, usernameOrEmailField, passwordField, loginButton, registerLink);
setCenter(card);
}
public TextField getUsernameOrEmailField() { return usernameOrEmailField; }

@ -2,6 +2,7 @@
package com.pair.ui;
import com.pair.model.User;
import com.pair.service.FileIOService;
import com.pair.service.QuizService;
import com.pair.service.UserService;
import com.pair.util.AsyncRegistrationHelper;
@ -15,15 +16,19 @@ import javafx.stage.Stage;
import java.io.IOException;
public class MainWindow extends BorderPane {
private final UserService userService = new UserService();
private final QuizService quizService = new QuizService();
private final UserService userService;
private final QuizService quizService;
private User currentUser;
private final Stage primaryStage;
private Panel currentPanel;
public MainWindow(Stage primaryStage) throws IOException {
userService = new UserService();
quizService = new QuizService(new FileIOService(), userService);
this.primaryStage = primaryStage;
this.userService = new UserService();
this.quizService = new QuizService(new FileIOService(), userService);
showStartPage();
}
@ -139,8 +144,13 @@ public class MainWindow extends BorderPane {
}
private void initInfGenPage() {
InfGenPage infGenPage = new InfGenPage(() -> navigateTo(Panel.LOGIN), userService.getCurrentUser().getUsername(),
userService.getCurrentUser().getEmail(), userService.getCurrentUser().getGrade());
User currentUser = userService.getCurrentUser();
InfGenPage infGenPage = new InfGenPage(() -> navigateTo(Panel.LOGIN),
currentUser.getUsername(),
currentUser.getEmail(),
currentUser.getGrade(),
currentUser.getTotalQuizzes(),
currentUser.getAverageScore());
infGenPage.getGenerateButton().setOnAction(e -> {
int count = infGenPage.getQuestionCountSpinner().getValue();
try {
@ -165,7 +175,6 @@ public class MainWindow extends BorderPane {
infGenPage.getModifyEmailButton().setOnAction(e -> handleEmailModifyAction(infGenPage));
this.setCenter(infGenPage);
this.setStyle("-fx-background-color: " + UIConstants.COLOR_BACKGROUND + ";");
}
private void handleUsernameModifyAction(InfGenPage infGenPage) {
try {

@ -2,6 +2,7 @@
package com.pair.ui;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
@ -15,21 +16,32 @@ public abstract class NavigablePanel extends BorderPane {
public NavigablePanel(Runnable onBack) {
Button backButton = new Button("←");
backButton.setOnAction(e -> onBack.run());
backButton.setPrefSize(UIConstants.BACK_BUTTON_WIDTH, UIConstants.BACK_BUTTON_HEIGHT);
backButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
backButton.setPrefSize(50, 50); // 调整为正方形按钮
backButton.setFont(Font.font(UIConstants.FONT_FAMILY, 18)); // 增大字体
/* 创建悬浮按钮样式 - 半透明圆形按钮 */
backButton.setStyle(
"-fx-background-radius: 50; " +
"-fx-background-color: " + UIConstants.COLOR_ACCENT + "; " +
"-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "E6; " + // 添加透明度
"-fx-text-fill: white; " +
"-fx-font-weight: bold;"
"-fx-font-weight: bold; " +
"-fx-cursor: hand; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 2);"); // 添加阴影效果
/* 添加悬停效果 */
backButton.setOnMouseEntered(e ->
backButton.setStyle(backButton.getStyle().replace("E6;", "FF;")) // 鼠标悬停时不透明
);
backButton.setOnMouseExited(e ->
backButton.setStyle(backButton.getStyle().replace("FF;", "E6;")) // 鼠标离开时半透明
);
HBox topBar = new HBox(10);
topBar.setPadding(UIConstants.TOP_BAR_PADDING);
topBar.setAlignment(Pos.CENTER_LEFT);
topBar.getChildren().add(backButton);
/* 将返回按钮悬浮在左上角,带有边距 */
HBox leftBar = new HBox(10);
leftBar.setPadding(new Insets(20, 0, 0, 20)); // 顶部20px左侧20px边距
leftBar.setAlignment(Pos.TOP_LEFT);
leftBar.getChildren().add(backButton);
this.setTop(topBar);
this.setLeft(leftBar);
}
protected static void showErrorAlert(String title, String message) {

@ -1,4 +1,3 @@
// com/ui/PasswordModifyPage.java
package com.pair.ui;
import javafx.geometry.Pos;
@ -21,28 +20,32 @@ public class PasswordModifyPage extends NavigablePanel {
@Override
protected void buildContent() {
VBox form = new VBox(UIConstants.DEFAULT_SPACING);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("修改密码");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_WARNING) + "; " + // 使用警告色(橙色)突出显示
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
// 使用统一的输入框样式
oldPasswordField.setPromptText("旧密码");
oldPasswordField.setStyle(UIConstants.INPUT_STYLE);
StyleHelper.stylePasswordField(oldPasswordField);
newPasswordField.setPromptText("新密码6-10位");
newPasswordField.setStyle(UIConstants.INPUT_STYLE);
StyleHelper.stylePasswordField(newPasswordField);
confirmNewPasswordField.setPromptText("确认新密码");
confirmNewPasswordField.setStyle(UIConstants.INPUT_STYLE);
modifyButton.setStyle(UIConstants.BUTTON_STYLE);
modifyButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
modifyButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
StyleHelper.stylePasswordField(confirmNewPasswordField);
form.getChildren().addAll(
titleLabel, oldPasswordField, newPasswordField, confirmNewPasswordField, modifyButton
);
this.setCenter(form);
// 使用统一的按钮样式
StyleHelper.stylePrimaryButton(modifyButton);
card.getChildren().addAll(title, oldPasswordField, newPasswordField, confirmNewPasswordField, modifyButton);
setCenter(card);
}
public PasswordField getOldPasswordField() { return oldPasswordField; }

@ -1,4 +1,3 @@
// com/ui/QuizPage.java
package com.pair.ui;
import com.pair.model.ChoiceQuestion;
@ -26,10 +25,9 @@ public class QuizPage extends NavigablePanel {
private final Button nextButton = new Button("下一题");
private final Button submitButton = new Button("交卷");
// 题目导航矩阵容器
private final GridPane questionNavGrid = new GridPane();
private int totalQuestions = 10; // 默认10题由外部设置
private int currentQuestionIndex = 0; // 当前题号0-based
private int totalQuestions = 10;
private int currentQuestionIndex = 0;
public QuizPage(Runnable onBack, QuizService quizService) {
super(onBack);
@ -39,257 +37,273 @@ public class QuizPage extends NavigablePanel {
@Override
protected void buildContent() {
// 设置整体布局BorderPane
this.setPadding(new Insets(20));
this.setStyle("-fx-background-color: " + UIConstants.COLOR_BACKGROUND + ";");
// 顶部标题栏
HBox topBar = createTopBar();
this.setTop(topBar);
// 主体内容:左右分栏
HBox mainContent = new HBox(20);
mainContent.setAlignment(Pos.CENTER);
mainContent.setPadding(new Insets(10));
// 左侧:题目内容区
VBox leftPanel = createLeftPanel();
leftPanel.setMaxWidth(600);
// 右侧:题目导航矩阵
VBox rightPanel = createRightPanel();
rightPanel.setMaxWidth(300);
rightPanel.setMinWidth(280);
HBox.setHgrow(leftPanel, Priority.ALWAYS);
HBox.setHgrow(rightPanel, Priority.NEVER);
mainContent.getChildren().addAll(leftPanel, rightPanel);
this.setCenter(mainContent);
// 底部按钮
VBox bottomBarContainer = new VBox(10);
bottomBarContainer.setAlignment(Pos.CENTER);
bottomBarContainer.setPadding(new Insets(10));
nextButton.setStyle(UIConstants.BUTTON_STYLE);
submitButton.setStyle(UIConstants.BUTTON_STYLE + "-fx-background-color: " + UIConstants.COLOR_ERROR + ";");
prevButton.setStyle(UIConstants.BUTTON_STYLE);
nextButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
submitButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
prevButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
HBox buttonRow = new HBox(20);
buttonRow.setAlignment(Pos.CENTER);
buttonRow.getChildren().addAll(prevButton, nextButton, submitButton);
bottomBarContainer.getChildren().add(buttonRow);
this.setBottom(bottomBarContainer);
// 初始化按钮状态
updateButtonVisibility();
}
setPadding(new Insets(20));
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
/**
*
*/
private HBox createTopBar() {
HBox topBar = new HBox(20);
topBar.setAlignment(Pos.CENTER_LEFT);
topBar.setPadding(new Insets(10));
topBar.setStyle("-fx-background-color: white; -fx-border-width: 0 0 1 0; -fx-border-color: #bdc3c7;");
/* 顶部标题栏卡片 整体下移 20px */
VBox topWrap = new VBox(createTopBar());
topWrap.setPadding(new Insets(0, 0, 20, 0));
setTop(topWrap);
/* 中部:左右两卡片 调整布局创造参差美感 */
// 创建主内容区域使用BorderPane实现更灵活的布局
BorderPane mainContent = new BorderPane();
/* 左侧题目卡片 - 占据主要区域 */
Pane leftCard = createLeftCard();
mainContent.setCenter(leftCard);
/* 右侧导航区域 - 放置题目导航 */
VBox rightArea = new VBox(20);
rightArea.setAlignment(Pos.TOP_CENTER);
rightArea.setPadding(new Insets(0, 0, 100, 0)); // 增加底部内边距,让导航下移
// 创建题目导航标题
Label navTitle = new Label("题目导航");
navTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
// 创建题目导航网格
initQuestionNavGrid();
// 添加标题和导航到右侧区域
rightArea.getChildren().addAll(navTitle, questionNavGrid);
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
progressLabel.setStyle("-fx-font-size: " + UIConstants.HINT_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_HINT + ";");
mainContent.setRight(rightArea);
topBar.getChildren().addAll(titleLabel, progressLabel);
return topBar;
/* 将主内容区域添加到中心,减少上移间距 */
VBox centerWrap = new VBox(mainContent);
centerWrap.setPadding(new Insets(10, 0, 0, 0)); // 减少上移间距
setCenter(centerWrap);
/* 底部按钮栏卡片 位置上移,更接近导航 */
VBox bottomWrap = new VBox(createBottomCard());
bottomWrap.setPadding(new Insets(10, 0, 0, 0)); // 减少间距,让导航更接近底部
setBottom(bottomWrap);
}
/**
*
*/
private VBox createLeftPanel() {
// 直接使用带样式的 VBox 作为内容容器
VBox content = new VBox(UIConstants.DEFAULT_SPACING);
content.setPadding(new Insets(20));
content.setStyle(UIConstants.FORM_STYLE);
content.setPrefWidth(550);
content.setMinWidth(550);
content.setMaxWidth(550);
content.setPrefHeight(400);
content.setMinHeight(400);
content.setMaxHeight(400);
/* ---------------- 私有构造区域 ---------------- */
private HBox createTopBar() {
HBox bar = new HBox(20);
bar.setAlignment(Pos.CENTER_LEFT);
bar.setPadding(UIConstants.CARD_PADDING);
bar.setStyle(UIConstants.CARD_STYLE);
bar.setEffect(UIConstants.CARD_SHADOW);
// 增强标题视觉效果
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
titleLabel.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
/* 放大、加粗、主色显示进度 */
progressLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
progressLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
bar.getChildren().addAll(titleLabel, spacer, progressLabel);
return bar;
}
private Pane createLeftCard() {
VBox card = StyleHelper.createCard();
card.setAlignment(Pos.TOP_LEFT);
card.setMaxWidth(680); // 稍宽一点
card.setMinHeight(450); // 增加最小高度,让题目区域更大
card.setStyle(card.getStyle() + "-fx-padding: 30;"); // 增加内边距,让内容更舒展
questionLabel.setWrapText(true);
questionLabel.setPrefWidth(500);
questionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.QUIZ_TITLE_FONT_SIZE));
questionLabel.setStyle("-fx-font-size: " + UIConstants.QUIZ_TITLE_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_PRIMARY + ";");
questionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SUB_TITLE_FONT_SIZE));
questionLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT) + ";");
VBox optionsBox = new VBox(10);
VBox optionsBox = new VBox(15); // 增加选项间距
optionsBox.setAlignment(Pos.CENTER_LEFT);
optionsBox.setPadding(new Insets(20, 0, 0, 0)); // 增加选项区域的上边距
for (int i = 0; i < 4; i++) {
options[i] = new RadioButton("选项 " + (char)('A' + i));
options[i] = new RadioButton("选项 " + (char) ('A' + i));
options[i].setToggleGroup(optionGroup);
options[i].setStyle("-fx-font-size: " + UIConstants.LABEL_FONT_SIZE + "px;");
options[i].setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
// 添加选项变化监听器,实现实时保存
final int optionIndex = i;
options[i].selectedProperty().addListener((obs, wasSelected, isSelected) -> {
if (isSelected) {
saveCurrentAnswer(optionIndex);
}
});
optionsBox.getChildren().add(options[i]);
}
content.getChildren().addAll(questionLabel, optionsBox);
return content;
card.getChildren().addAll(questionLabel, optionsBox);
return card;
}
/**
*
*/
private VBox createRightPanel() {
VBox rightPanel = new VBox(10);
rightPanel.setAlignment(Pos.TOP_CENTER);
rightPanel.setPadding(new Insets(20));
rightPanel.setStyle(UIConstants.FORM_STYLE);
private Pane createRightCard() {
VBox card = StyleHelper.createCard();
card.setAlignment(Pos.TOP_CENTER);
card.setMaxWidth(300);
card.setMinHeight(350); // 设置比左侧稍小的高度,创造参差美感
card.setStyle(card.getStyle() + "-fx-padding: 20;"); // 增加内边距
Label navTitle = new Label("题目导航");
navTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUBTITLE_FONT_SIZE));
navTitle.setAlignment(Pos.CENTER);
navTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
navTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 初始化题目导航矩阵
initQuestionNavGrid();
rightPanel.getChildren().addAll(navTitle, questionNavGrid);
return rightPanel;
card.getChildren().addAll(navTitle, questionNavGrid);
return card;
}
/**
*
*/
private HBox createBottomCard() {
HBox card = new HBox(20);
card.setAlignment(Pos.CENTER);
card.setPadding(UIConstants.CARD_PADDING);
card.setStyle(UIConstants.CARD_STYLE);
card.setEffect(UIConstants.CARD_SHADOW);
// 使用统一的按钮样式
StyleHelper.styleButton(prevButton); // 使用默认样式
StyleHelper.styleButton(nextButton); // 使用默认样式
StyleHelper.styleErrorButton(submitButton); // 提交按钮使用错误样式(红色)突出显示
// 为上一题按钮添加保存逻辑
prevButton.setOnAction(e -> {
saveCurrentAnswer(); // 保存当前答案
if (currentQuestionIndex > 0) {
goToQuestion(currentQuestionIndex - 1);
}
});
// 为下一题按钮添加保存逻辑
nextButton.setOnAction(e -> {
saveCurrentAnswer(); // 保存当前答案
if (currentQuestionIndex < totalQuestions - 1) {
goToQuestion(currentQuestionIndex + 1);
}
});
// 提交按钮不需要保存,直接交卷
// submitButton的点击事件由外部处理
card.getChildren().addAll(prevButton, nextButton, submitButton);
return card;
}
/* ---------------- 导航网格 ---------------- */
private void initQuestionNavGrid() {
questionNavGrid.getChildren().clear();
questionNavGrid.setHgap(5);
questionNavGrid.setVgap(5);
questionNavGrid.setHgap(8); // 增加水平间距
questionNavGrid.setVgap(8); // 增加垂直间距
questionNavGrid.setAlignment(Pos.CENTER);
int cols = 5;
int rows = (totalQuestions + cols - 1) / cols;
for (int i = 0; i < totalQuestions; i++) {
int row = i / cols;
int col = i % cols;
String text = String.valueOf(i + 1);
Button btn = new Button(text);
btn.setPrefSize(50, 40);
btn.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.LABEL_FONT_SIZE));
btn.setStyle(getButtonStyleForStatus(i));
Button btn = new Button(String.valueOf(i + 1));
btn.setPrefSize(52, 44); // 稍大一点的按钮
btn.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
btn.setStyle(getNavBtnStyle(i));
final int index = i;
btn.setOnAction(e -> goToQuestion(index));
questionNavGrid.add(btn, col, row);
}
}
/**
*
* @param index 0-based
* @return CSS
*/
private String getButtonStyleForStatus(int index) {
if (index == currentQuestionIndex) {
// 当前题
return "-fx-background-color: " + UIConstants.COLOR_ACCENT + "; -fx-text-fill: white; -fx-font-weight: bold;";
} else if (quizService.isAnswered(index)) {
// 已作答
return "-fx-background-color: #2ecc71; -fx-text-fill: white;";
} else {
// 未作答
return "-fx-background-color: #ecf0f1; -fx-text-fill: #2c3e50;";
}
private String getNavBtnStyle(int index) {
if (index == currentQuestionIndex)
return "-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) +
"; -fx-text-fill: white; -fx-font-weight: bold;";
if (quizService.isAnswered(index))
return "-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_SECONDARY) +
"; -fx-text-fill: white;";
return "-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) +
"; -fx-text-fill: white;";
}
/* ---------------- 答案保存功能 ---------------- */
private void saveCurrentAnswer(int selectedOptionIndex) {
// 保存当前选择的答案
quizService.submitAnswer(currentQuestionIndex, selectedOptionIndex);
// 更新进度显示
updateProgress();
// 刷新导航按钮状态
refreshNavButtons();
}
/**
*
*/
private void updateButtonVisibility() {
if (currentQuestionIndex == totalQuestions - 1) {
nextButton.setVisible(false);
submitButton.setVisible(true);
private void saveCurrentAnswer() {
// 获取当前选中的选项
Toggle selectedToggle = optionGroup.getSelectedToggle();
if (selectedToggle != null) {
for (int i = 0; i < 4; i++) {
if (options[i] == selectedToggle) {
saveCurrentAnswer(i);
break;
}
}
} else {
nextButton.setVisible(true);
submitButton.setVisible(false);
// 如果没有选中任何选项,清除当前答案
// QuizService没有直接的清除方法我们保存一个无效索引来表示清除
// 或者使用submitCurrentAnswer(-1)来表示清除(如果支持)
// 暂时不处理清除逻辑,保持原有状态
updateProgress();
refreshNavButtons();
}
}
/**
*
* @param index 0-based
*/
/* ---------------- public 供外部调用 ---------------- */
public void goToQuestion(int index) {
// 在切换题目之前,先保存当前题目的答案
saveCurrentAnswer();
currentQuestionIndex = index;
quizService.goToQuestion(index);
updateProgressLabel();
updateQuestionNavButtons();
updateButtonVisibility();
loadQuestion(index);
updateProgress();
refreshNavButtons();
loadQuestion();
updateBottomBtnVisibility();
}
/**
*
*/
private void updateProgressLabel() {
int answeredCount = quizService.getAnsweredCount();
progressLabel.setText("完成 " + answeredCount + "/" + totalQuestions + " 题");
private void updateProgress() {
int answered = quizService.getAnsweredCount();
progressLabel.setText("完成 " + answered + "/" + totalQuestions + " 题");
}
/**
*
*/
private void updateQuestionNavButtons() {
private void refreshNavButtons() {
for (Node node : questionNavGrid.getChildren()) {
if (node instanceof Button) {
Button btn = (Button) node;
int index = Integer.parseInt(btn.getText()) - 1; // 转换为0-based
btn.setStyle(getButtonStyleForStatus(index));
int idx = Integer.parseInt(btn.getText()) - 1;
btn.setStyle(getNavBtnStyle(idx));
}
}
}
/**
* QuizService
* @param index
*/
private void loadQuestion(int index) {
System.out.println("🔄 加载题目 " + index + ", options[0]=" + options[0]);
if (options[0] == null) {
System.err.println("⚠️ RadioButton 未初始化,跳过题目加载");
return; // 防止 NPE
}
ChoiceQuestion question = quizService.getQuestion(index);
if (question == null) return;
private void updateBottomBtnVisibility() {
boolean isLast = currentQuestionIndex == totalQuestions - 1;
nextButton.setVisible(!isLast);
submitButton.setVisible(isLast);
}
// 显示题目
questionLabel.setText("第 " + (index + 1) + " 题:\n" + question.getQuestionText());
private void loadQuestion() {
ChoiceQuestion q = quizService.getQuestion(currentQuestionIndex);
if (q == null) return;
questionLabel.setText("第 " + (currentQuestionIndex + 1) + " 题:\n" + q.getQuestionText());
List<?> optionsList = question.getOptions();
List<?> opts = q.getOptions();
for (int i = 0; i < 4; i++) {
Object option = optionsList.get(i);
String optionText = option != null ? option.toString() : "未知";
options[i].setText((char)('A' + i) + ". " + optionText);
String txt = opts.get(i) == null ? "未知" : opts.get(i).toString();
options[i].setText((char) ('A' + i) + ". " + txt);
}
Integer userAnswer = quizService.getUserAnswer(index);
if (userAnswer != null && userAnswer >= 0 && userAnswer < 4) {
optionGroup.selectToggle(options[userAnswer]);
} else {
optionGroup.selectToggle(null);
}
// ✅ 强制刷新 UI可选通常不需要
// Platform.runLater(() -> {
// this.requestLayout(); // 触发重新布局
// });
Integer ans = quizService.getUserAnswer(currentQuestionIndex);
optionGroup.selectToggle(ans == null ? null : options[ans]);
}
// ========== Getter 方法 ==========
/* ---------------- Getters ---------------- */
public Label getProgressLabel() { return progressLabel; }
public Label getQuestionLabel() { return questionLabel; }
public RadioButton[] getOptions() { return options; }
@ -298,17 +312,9 @@ public class QuizPage extends NavigablePanel {
public Button getPrevButton() { return prevButton; }
public Button getSubmitButton() { return submitButton; }
// ========== 设置器方法 ==========
public void setTotalQuestions(int total) {
this.totalQuestions = total;
initQuestionNavGrid(); // 重新初始化导航矩阵
updateProgressLabel();
}
public void setCurrentQuestionIndex(int index) {
this.currentQuestionIndex = index;
updateQuestionNavButtons();
updateButtonVisibility();
loadQuestion(index);
initQuestionNavGrid();
updateProgress();
}
}

@ -1,4 +1,3 @@
// com/ui/RegisterPage.java
package com.pair.ui;
import javafx.geometry.Pos;
@ -23,37 +22,39 @@ public class RegisterPage extends NavigablePanel {
@Override
protected void buildContent() {
VBox form = new VBox(UIConstants.DEFAULT_SPACING);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
// 使用统一的输入框样式
emailField.setPromptText("邮箱");
emailField.setStyle(UIConstants.INPUT_STYLE);
StyleHelper.styleInputField(emailField);
codeField.setPromptText("注册码");
codeField.setStyle(UIConstants.INPUT_STYLE);
StyleHelper.styleInputField(codeField);
passwordField.setPromptText("密码6-10位");
passwordField.setStyle(UIConstants.INPUT_STYLE);
StyleHelper.stylePasswordField(passwordField);
confirmPasswordField.setPromptText("确认密码");
confirmPasswordField.setStyle(UIConstants.INPUT_STYLE);
sendCodeButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
sendCodeButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
sendCodeButton.setStyle(UIConstants.BUTTON_STYLE);
registerButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
registerButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
registerButton.setStyle(UIConstants.BUTTON_STYLE);
form.getChildren().addAll(
titleLabel, emailField, sendCodeButton, codeField,
passwordField, confirmPasswordField, registerButton
);
this.setCenter(form);
StyleHelper.stylePasswordField(confirmPasswordField);
// 使用统一的按钮样式
StyleHelper.styleSecondaryButton(sendCodeButton); // 次要按钮样式
StyleHelper.stylePrimaryButton(registerButton); // 主要按钮样式
card.getChildren().addAll(title, emailField, sendCodeButton, codeField,
passwordField, confirmPasswordField, registerButton);
setCenter(card);
}
// getters for controller
public TextField getEmailField() { return emailField; }
public TextField getCodeField() { return codeField; }
public PasswordField getPasswordField() { return passwordField; }

@ -52,6 +52,7 @@ public class ResultPage extends NavigablePanel {
public void updateResult() {
QuizResult result = quizService.calculateResult();
quizService.saveQuizHistory();
resultLabel.setText(result.toString());
gradeLabel.setText(quizService.getGrade(result));
}

@ -1,4 +1,3 @@
// com/ui/StartPage.java
package com.pair.ui;
import javafx.geometry.Pos;
@ -13,25 +12,27 @@ public class StartPage extends VBox {
private final Button startButton;
public StartPage(Runnable onStart) {
this.setAlignment(Pos.CENTER);
this.setSpacing(UIConstants.DEFAULT_SPACING);
this.setPadding(UIConstants.DEFAULT_PADDING);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
Label subtitleLabel = new Label("HNU@梁峻耀 吴佰轩");
subtitleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SUBTITLE_FONT_SIZE));
subtitleLabel.setStyle("-fx-text-fill: gray;");
setAlignment(Pos.CENTER);
setSpacing(UIConstants.DEFAULT_SPACING);
setPadding(UIConstants.PAGE_PADDING);
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
// 增强标题视觉效果 - StartPage使用超大字体作为主页面
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.XLARGE_TITLE_FONT_SIZE)); // 使用超大字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 4);" // 添加更强的阴影效果
);
Label sub = new Label("HNU@梁峻耀 吴佰轩");
sub.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
sub.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
startButton = new Button("开始");
startButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
startButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
StyleHelper.styleButton(startButton);
startButton.setOnAction(e -> onStart.run());
startButton.setStyle(UIConstants.BUTTON_STYLE);
startButton.setOnMouseEntered(e -> startButton.setStyle(UIConstants.BUTTON_STYLE + UIConstants.BUTTON_HOVER_STYLE));
startButton.setOnMouseExited(e -> startButton.setStyle(UIConstants.BUTTON_STYLE));
this.getChildren().addAll(titleLabel, subtitleLabel, startButton);
getChildren().addAll(title, sub, startButton);
}
}

@ -0,0 +1,153 @@
/* com/ui/StyleHelper.java */
package com.pair.ui;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.control.TextField;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Spinner;
public class StyleHelper {
/** 给按钮一次性加上通用样式、hover、pressed 动画 - 兼容方法 */
public static void styleButton(Button btn) {
btn.setStyle(UIConstants.BTN_NORMAL);
// 仅背景深浅变化,字体、圆角始终一致
btn.setOnMouseEntered(e -> btn.setStyle(UIConstants.BTN_HOVER));
btn.setOnMouseExited(e -> btn.setStyle(UIConstants.BTN_NORMAL));
btn.setOnMousePressed(e -> btn.setStyle(UIConstants.BTN_PRESSED));
btn.setOnMouseReleased(e -> btn.setStyle(UIConstants.BTN_NORMAL));
btn.setPrefSize(UIConstants.BTN_WIDTH, UIConstants.BTN_HEIGHT);
}
/** 新的统一按钮样式方法 - 支持不同类型按钮 */
public static void styleButton(Button btn, String type) {
btn.setStyle(UIConstants.getButtonNormalStyle(type));
btn.setOnMouseEntered(e -> btn.setStyle(UIConstants.getButtonHoverStyle(type)));
btn.setOnMouseExited(e -> btn.setStyle(UIConstants.getButtonNormalStyle(type)));
btn.setOnMousePressed(e -> btn.setStyle(UIConstants.getButtonPressedStyle(type)));
btn.setOnMouseReleased(e -> btn.setStyle(UIConstants.getButtonNormalStyle(type)));
// 根据按钮类型设置尺寸
switch (type) {
case UIConstants.BTN_TYPE_PRIMARY:
case UIConstants.BTN_TYPE_WARNING:
btn.setPrefSize(UIConstants.BTN_LARGE_WIDTH, UIConstants.BTN_LARGE_HEIGHT);
break;
case UIConstants.BTN_TYPE_SUCCESS:
case UIConstants.BTN_TYPE_ERROR:
case UIConstants.BTN_TYPE_INFO:
btn.setPrefSize(UIConstants.BTN_MEDIUM_WIDTH, UIConstants.BTN_MEDIUM_HEIGHT);
break;
default:
btn.setPrefSize(UIConstants.BTN_SMALL_WIDTH, UIConstants.BTN_SMALL_HEIGHT);
break;
}
}
/** 主要按钮 - 使用主色调 */
public static void stylePrimaryButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_PRIMARY);
}
/** 次要按钮 - 使用辅助色调 */
public static void styleSecondaryButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_SECONDARY);
}
/** 成功按钮 - 使用绿色调 */
public static void styleSuccessButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_SUCCESS);
}
/** 警告按钮 - 使用橙色调 */
public static void styleWarningButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_WARNING);
}
/** 错误按钮 - 使用红色调 */
public static void styleErrorButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_ERROR);
}
/** 信息按钮 - 使用蓝色调 */
public static void styleInfoButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_INFO);
}
/** 快速生成"白色卡片"VBox - 兼容方法 */
public static VBox createCard() {
VBox card = new VBox(UIConstants.DEFAULT_SPACING);
card.setPadding(UIConstants.CARD_PADDING);
card.setStyle(UIConstants.CARD_STYLE);
card.setEffect(UIConstants.CARD_SHADOW);
return card;
}
/** 新的卡片生成方法 - 支持不同尺寸 */
public static VBox createCard(String size) {
VBox card = new VBox(UIConstants.MEDIUM_SPACING);
card.setPadding(UIConstants.CARD_PADDING);
card.setAlignment(Pos.CENTER);
switch (size) {
case "SMALL":
card.setStyle(UIConstants.CARD_STYLE_SMALL);
break;
case "LARGE":
card.setStyle(UIConstants.CARD_STYLE_LARGE);
break;
default: // MEDIUM
card.setStyle(UIConstants.CARD_STYLE_MEDIUM);
break;
}
return card;
}
/** 创建小型卡片 */
public static VBox createSmallCard() {
return createCard("SMALL");
}
/** 创建中型卡片 */
public static VBox createMediumCard() {
return createCard("MEDIUM");
}
/** 创建大型卡片 */
public static VBox createLargeCard() {
return createCard("LARGE");
}
/** 统一输入框样式 - TextField */
public static void styleTextField(TextField textField, String width) {
textField.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - PasswordField */
public static void stylePasswordField(PasswordField passwordField, String width) {
passwordField.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - ChoiceBox */
public static void styleChoiceBox(ChoiceBox<?> choiceBox, String width) {
choiceBox.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - Spinner */
public static void styleSpinner(Spinner<?> spinner) {
spinner.getEditor().setStyle(UIConstants.getInputStyle(String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH)));
spinner.setStyle("-fx-background-radius: 8; -fx-border-radius: 8;");
}
/** 快速设置输入框为中等宽度 */
public static void styleInputField(TextField field) {
styleTextField(field, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
}
/** 快速设置密码框为中等宽度 */
public static void stylePasswordField(PasswordField field) {
stylePasswordField(field, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
}
}

@ -1,69 +1,217 @@
// UIConstants.java
package com.pair.ui;
import javafx.geometry.Insets;
import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;
public final class UIConstants {
private UIConstants() {}
public static final double LABEL_ITEM_TITLE_SIZE = 16.0;
/* ====== 间距 & 边距 ====== */
public static final double DEFAULT_SPACING = 16;
public static final Insets PAGE_PADDING = new Insets(40);
public static final Insets CARD_PADDING = new Insets(24);
public static final Insets TOP_BAR_PADDING = new Insets(12);
// 间距与边距
public static final double DEFAULT_SPACING = 15.0;
public static final Insets DEFAULT_PADDING = new Insets(40);
public static final Insets SMALL_PADDING = new Insets(20);
public static final Insets TOP_BAR_PADDING = new Insets(10);
// 新增:统一间距规范
public static final double SMALL_SPACING = 8;
public static final double MEDIUM_SPACING = 16;
public static final double LARGE_SPACING = 24;
public static final double XLARGE_SPACING = 32;
// 字体
// 新增:卡片尺寸规范
public static final double CARD_SMALL_WIDTH = 400;
public static final double CARD_MEDIUM_WIDTH = 600;
public static final double CARD_LARGE_WIDTH = 800;
public static final double CARD_MIN_HEIGHT = 500;
// 新增:输入框宽度规范
public static final double INPUT_SMALL_WIDTH = 200;
public static final double INPUT_MEDIUM_WIDTH = 300;
public static final double INPUT_LARGE_WIDTH = 400;
// 新增:按钮尺寸规范
public static final double BTN_SMALL_WIDTH = 100;
public static final double BTN_MEDIUM_WIDTH = 120;
public static final double BTN_LARGE_WIDTH = 140;
public static final double BTN_SMALL_HEIGHT = 32;
public static final double BTN_MEDIUM_HEIGHT = 36;
public static final double BTN_LARGE_HEIGHT = 40;
/* ====== 字体 ====== */
public static final String FONT_FAMILY = "Microsoft YaHei";
public static final double TITLE_FONT_SIZE = 26.0;
public static final double SUBTITLE_FONT_SIZE = 16.0;
public static final double BUTTON_FONT_SIZE = 15.0;
public static final double LABEL_FONT_SIZE = 14.0;
public static final double INPUT_FONT_SIZE = 14.0;
public static final double HINT_FONT_SIZE = 12.0;
public static final double ERROR_FONT_SIZE = 12.0;
public static final double QUIZ_TITLE_FONT_SIZE = 20.0;
public static final double SCORE_FONT_SIZE = 32.0;
// 按钮尺寸
public static final double BUTTON_WIDTH = 140.0;
public static final double BUTTON_HEIGHT = 40.0;
public static final double BACK_BUTTON_WIDTH = 80.0;
public static final double BACK_BUTTON_HEIGHT = 30.0;
// 颜色
public static final String COLOR_PRIMARY = "#2c3e50";
public static final String COLOR_ACCENT = "#3498db";
public static final String COLOR_ERROR = "#e74c3c";
public static final String COLOR_HINT = "#7f8c8d";
public static final String COLOR_BACKGROUND = "#ecf0f1";
// 按钮样式
public static final String BUTTON_STYLE =
"-fx-background-color: " + COLOR_ACCENT + "; " +
"-fx-text-fill: white; " +
"-fx-background-radius: 8; " +
"-fx-font-size: " + BUTTON_FONT_SIZE + "px; " +
"-fx-font-family: '" + FONT_FAMILY + "'; " +
"-fx-cursor: hand;";
public static final String BUTTON_HOVER_STYLE =
"-fx-background-color: #2980b9;";
// 输入框样式
public static final double TITLE_FONT_SIZE = 26;
public static final double SUB_TITLE_FONT_SIZE = 18;
public static final double BODY_FONT_SIZE = 14;
public static final double BTN_FONT_SIZE = 14;
public static final double SMALL_FONT_SIZE = 12;
// 新增:更大的标题字体
public static final double LARGE_TITLE_FONT_SIZE = 32;
public static final double XLARGE_TITLE_FONT_SIZE = 40;
/* ====== 圆角 & 阴影 ====== */
public static final double CARD_RADIUS = 12;
public static final DropShadow CARD_SHADOW = new DropShadow(15, 0, 4, Color.web("#00000020"));
/* ====== 主题色(一键换色只改这里) ====== */
public static final Color COLOR_PRIMARY = Color.web("#2563eb"); // 主色 - 蓝色
public static final Color COLOR_SECONDARY = Color.web("#10b981"); // 辅助 - 绿色
public static final Color COLOR_ERROR = Color.web("#ef4444"); // 错误 - 红色
public static final Color COLOR_BG = Color.web("#f3f4f6"); // 背景 - 浅灰
public static final Color COLOR_TEXT = Color.web("#1f2937"); // 文字 - 深灰
public static final Color COLOR_TEXT_SUB = Color.web("#6b7280"); // 副文字 - 中灰
// 新增:扩展配色方案
public static final Color COLOR_SUCCESS = Color.web("#22c55e"); // 成功 - 亮绿
public static final Color COLOR_WARNING = Color.web("#f59e0b"); // 警告 - 橙色
public static final Color COLOR_INFO = Color.web("#3b82f6"); // 信息 - 亮蓝
public static final Color COLOR_DARK = Color.web("#374151"); // 深色 - 深灰
public static final Color COLOR_LIGHT = Color.web("#ffffff"); // 浅色 - 白色
/* ====== 通用按钮 ====== */
public static final double BTN_WIDTH = 140;
public static final double BTN_HEIGHT = 40;
// 新增:按钮类型常量
public static final String BTN_TYPE_PRIMARY = "PRIMARY";
public static final String BTN_TYPE_SECONDARY = "SECONDARY";
public static final String BTN_TYPE_SUCCESS = "SUCCESS";
public static final String BTN_TYPE_WARNING = "WARNING";
public static final String BTN_TYPE_ERROR = "ERROR";
public static final String BTN_TYPE_INFO = "INFO";
// 新增:统一按钮样式生成方法
public static String getButtonNormalStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY: bgColor = COLOR_PRIMARY; break;
case BTN_TYPE_SECONDARY: bgColor = COLOR_SECONDARY; break;
case BTN_TYPE_SUCCESS: bgColor = COLOR_SUCCESS; break;
case BTN_TYPE_WARNING: bgColor = COLOR_WARNING; break;
case BTN_TYPE_ERROR: bgColor = COLOR_ERROR; break;
case BTN_TYPE_INFO: bgColor = COLOR_INFO; break;
default: bgColor = COLOR_PRIMARY; break;
}
return "-fx-background-color: " + toWeb(bgColor) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BTN_FONT_SIZE + "px;"
+ "-fx-cursor: hand;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
}
public static String getButtonHoverStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY: bgColor = COLOR_PRIMARY.darker(); break;
case BTN_TYPE_SECONDARY: bgColor = COLOR_SECONDARY.darker(); break;
case BTN_TYPE_SUCCESS: bgColor = COLOR_SUCCESS.darker(); break;
case BTN_TYPE_WARNING: bgColor = COLOR_WARNING.darker(); break;
case BTN_TYPE_ERROR: bgColor = COLOR_ERROR.darker(); break;
case BTN_TYPE_INFO: bgColor = COLOR_INFO.darker(); break;
default: bgColor = COLOR_PRIMARY.darker(); break;
}
return "-fx-background-color: " + toWeb(bgColor) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
}
public static String getButtonPressedStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY: bgColor = COLOR_PRIMARY.darker().darker(); break;
case BTN_TYPE_SECONDARY: bgColor = COLOR_SECONDARY.darker().darker(); break;
case BTN_TYPE_SUCCESS: bgColor = COLOR_SUCCESS.darker().darker(); break;
case BTN_TYPE_WARNING: bgColor = COLOR_WARNING.darker().darker(); break;
case BTN_TYPE_ERROR: bgColor = COLOR_ERROR.darker().darker(); break;
case BTN_TYPE_INFO: bgColor = COLOR_INFO.darker().darker(); break;
default: bgColor = COLOR_PRIMARY.darker().darker(); break;
}
return "-fx-background-color: " + toWeb(bgColor) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 4, 0, 0, 1);";
}
// 保留原有样式用于兼容
public static final String BTN_NORMAL =
"-fx-background-color: " + toWeb(COLOR_PRIMARY) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;" // 圆角
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BTN_FONT_SIZE + "px;"
+ "-fx-cursor: hand;";
public static final String BTN_HOVER =
"-fx-background-color: " + toWeb(COLOR_PRIMARY.darker()) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"; // 保持圆角
public static final String BTN_PRESSED =
"-fx-background-color: " + toWeb(COLOR_PRIMARY.darker().darker()) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;";
/* ====== 输入框 ====== */
// 新增:输入框样式生成方法
public static String getInputStyle(String width) {
return "-fx-background-radius: 8;"
+ "-fx-border-radius: 8;"
+ "-fx-border-color: " + toWeb(COLOR_TEXT_SUB) + ";"
+ "-fx-padding: 12;"
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BODY_FONT_SIZE + "px;"
+ "-fx-pref-width: " + width + ";"
+ "-fx-background-color: " + toWeb(COLOR_LIGHT) + ";"
+ "-fx-effect: innershadow(gaussian, rgba(0,0,0,0.05), 2, 0, 0, 1);";
}
// 新增:输入框宽度预设
public static final String INPUT_STYLE_SMALL = getInputStyle(String.valueOf(INPUT_SMALL_WIDTH));
public static final String INPUT_STYLE_MEDIUM = getInputStyle(String.valueOf(INPUT_MEDIUM_WIDTH));
public static final String INPUT_STYLE_LARGE = getInputStyle(String.valueOf(INPUT_LARGE_WIDTH));
// 保留原有样式用于兼容
public static final String INPUT_STYLE =
"-fx-background-radius: 8; " +
"-fx-border-radius: 8; " +
"-fx-border-color: #bdc3c7; " +
"-fx-padding: 8; " +
"-fx-font-size: " + INPUT_FONT_SIZE + "px; " +
"-fx-font-family: '" + FONT_FAMILY + "';";
// 表单容器样式
public static final String FORM_STYLE =
"-fx-background-color: white; " +
"-fx-background-radius: 12; " +
"-fx-border-radius: 12; " +
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.1), 10, 0, 0, 5);";
"-fx-background-radius: 8;"
+ "-fx-border-radius: 8;"
+ "-fx-border-color: " + toWeb(COLOR_TEXT_SUB) + ";"
+ "-fx-padding: 10;"
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BODY_FONT_SIZE + "px;"
+ "-fx-pref-width: 240;";
/* ====== 卡片容器 ====== */
// 新增:卡片容器样式生成方法
public static String getCardStyle(double width, double minHeight) {
return "-fx-background-color: " + toWeb(COLOR_LIGHT) + ";"
+ "-fx-background-radius: " + CARD_RADIUS + ";"
+ "-fx-border-radius: " + CARD_RADIUS + ";"
+ "-fx-pref-width: " + width + ";"
+ "-fx-min-height: " + minHeight + ";"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 15, 0, 4, 4);";
}
// 新增:卡片尺寸预设
public static final String CARD_STYLE_SMALL = getCardStyle(CARD_SMALL_WIDTH, CARD_MIN_HEIGHT);
public static final String CARD_STYLE_MEDIUM = getCardStyle(CARD_MEDIUM_WIDTH, CARD_MIN_HEIGHT);
public static final String CARD_STYLE_LARGE = getCardStyle(CARD_LARGE_WIDTH, CARD_MIN_HEIGHT);
// 保留原有样式用于兼容
public static final String CARD_STYLE =
"-fx-background-color: white;"
+ "-fx-background-radius: " + CARD_RADIUS + ";"
+ "-fx-border-radius: " + CARD_RADIUS + ";";
/* ====== 工具 ====== */
public static String toWeb(Color c) {
return String.format("#%02X%02X%02X", (int) (c.getRed() * 255),
(int) (c.getGreen() * 255), (int) (c.getBlue() * 255));
}
}

@ -4,12 +4,12 @@ package com.pair.util;
import com.pair.service.UserService;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import java.io.IOException;
import java.util.function.Consumer;
public class
AsyncRegistrationHelper {
public class AsyncRegistrationHelper {
private static final int COUNTDOWN_SECONDS = 60;
@ -52,13 +52,16 @@ AsyncRegistrationHelper {
}).start();
});
task.setOnFailed(e -> {
task.setOnFailed((WorkerStateEvent e) -> {
Throwable ex = task.getException();
String msg = switch (ex) {
case IllegalArgumentException iae -> iae.getMessage();
case IOException ioe -> "系统错误:" + ioe.getMessage();
default -> "发送失败,请检查网络或稍后重试";
};
String msg;
if (ex instanceof IllegalArgumentException iae) {
msg = iae.getMessage();
} else if (ex instanceof IOException ioe) {
msg = "系统错误:" + ioe.getMessage();
} else {
msg = "发送失败,请检查网络或稍后重试";
}
onFailure.accept(msg);
Platform.runLater(onComplete);
});

@ -81,23 +81,4 @@ public class EmailUtil {
}
/**
*
*
* @param toEmail
* @param newPassword
* @return true
*/
public static boolean sendPasswordReset(String toEmail, String newPassword) {
// TODO: 将来如果需要"找回密码"功能,可以在这里实现
System.out.println("【模拟】发送密码重置邮件");
System.out.println("收件人: " + toEmail);
System.out.println("新密码: " + newPassword);
return true;
}
}

@ -143,4 +143,5 @@ public class PasswordValidator {
// 打乱字符顺序
return RandomUtils.shuffleString(code.toString());
}
}
Loading…
Cancel
Save