parent
ac67b0a073
commit
381900b80d
@ -1,17 +1,81 @@
|
||||
<?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>
|
||||
|
||||
<groupId>org.example</groupId>
|
||||
<groupId>com.mathapp</groupId>
|
||||
<artifactId>TestSystem</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Math Learning App</name>
|
||||
<description>A desktop application for learning math for elementary, middle, and high school students.</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>22</maven.compiler.source>
|
||||
<maven.compiler.target>22</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- 现代化Swing UI外观库 -->
|
||||
<dependency>
|
||||
<groupId>com.formdev</groupId>
|
||||
<artifactId>flatlaf</artifactId>
|
||||
<version>3.4.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 邮件发送功能库 -->
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 数学表达式求值库 -->
|
||||
<dependency>
|
||||
<groupId>net.objecthunter</groupId>
|
||||
<artifactId>exp4j</artifactId>
|
||||
<version>0.4.8</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>22</source>
|
||||
<target>22</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- 打包为可执行JAR文件的插件 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.mathapp.MathApp</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,88 @@
|
||||
package com.mathapp.controllers;
|
||||
|
||||
import com.mathapp.MathApp;
|
||||
import com.mathapp.models.Question;
|
||||
import com.mathapp.models.TestPaper;
|
||||
import com.mathapp.panels.QuizPanel;
|
||||
import com.mathapp.services.DataPersistence;
|
||||
import com.mathapp.services.QuestionGenerator;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class QuizController {
|
||||
private final MathApp app;
|
||||
private final String level;
|
||||
private final int questionCount;
|
||||
private QuizPanel quizPanel;
|
||||
private List<Question> questions;
|
||||
private final List<String> userAnswers;
|
||||
private int currentQuestionIndex;
|
||||
private int score;
|
||||
|
||||
public QuizController(MathApp app, String level, int questionCount) {
|
||||
this.app = app;
|
||||
this.level = level;
|
||||
this.questionCount = questionCount;
|
||||
this.userAnswers = new ArrayList<>();
|
||||
this.currentQuestionIndex = 0;
|
||||
this.score = 0;
|
||||
}
|
||||
|
||||
public void startQuiz(QuizPanel panel) {
|
||||
this.quizPanel = panel;
|
||||
// 在后台线程生成题目
|
||||
SwingWorker<List<Question>, Void> worker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected List<Question> doInBackground() throws Exception {
|
||||
return QuestionGenerator.generateQuestions(level, questionCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
try {
|
||||
questions = get();
|
||||
if (questions.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(quizPanel, "题目生成失败,请重试。", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
app.showPanel(MathApp.MAIN_MENU_PANEL);
|
||||
} else {
|
||||
quizPanel.displayQuestion(questions.get(currentQuestionIndex), currentQuestionIndex, questionCount);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
JOptionPane.showMessageDialog(quizPanel, "题目生成时发生错误。", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
app.showPanel(MathApp.MAIN_MENU_PANEL);
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
}
|
||||
|
||||
public void submitAnswer(String selectedOption) {
|
||||
userAnswers.add(selectedOption);
|
||||
if (questions.get(currentQuestionIndex).correctAnswer().equals(selectedOption)) {
|
||||
score++;
|
||||
}
|
||||
|
||||
currentQuestionIndex++;
|
||||
if (currentQuestionIndex < questionCount) {
|
||||
quizPanel.displayQuestion(questions.get(currentQuestionIndex), currentQuestionIndex, questionCount);
|
||||
} else {
|
||||
finishQuiz();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishQuiz() {
|
||||
TestPaper testPaper = new TestPaper(
|
||||
app.getCurrentUserEmail(),
|
||||
LocalDateTime.now(),
|
||||
questions,
|
||||
userAnswers,
|
||||
score
|
||||
);
|
||||
DataPersistence.saveTestPaper(testPaper);
|
||||
app.showResults(score, questionCount, testPaper);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.mathapp.models;
|
||||
|
||||
/**
|
||||
* 代表四则运算的枚举类型。
|
||||
*/
|
||||
public enum Operator {
|
||||
ADD("+"),
|
||||
SUBTRACT("-"),
|
||||
MULTIPLY("*"),
|
||||
DIVIDE("/");
|
||||
|
||||
private final String symbol;
|
||||
|
||||
Operator(String symbol) {
|
||||
this.symbol = symbol;
|
||||
}
|
||||
|
||||
public String getSymbol() {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.mathapp.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 代表一道完整的选择题。
|
||||
* @param problemStatement 题干字符串 (e.g., "5 * 3 =")
|
||||
* @param options 四个选项的列表
|
||||
* @param correctAnswer 正确答案的字符串表示
|
||||
*/
|
||||
public record Question(String problemStatement, List<String> options, String correctAnswer) {
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.mathapp.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 代表一次完整的测验记录。
|
||||
* @param username 用户名 (邮箱)
|
||||
* @param timestamp 完成测验的时间
|
||||
* @param questions 题目列表
|
||||
* @param userAnswers 用户答案列表
|
||||
* @param score 用户得分 (答对的题目数)
|
||||
*/
|
||||
public record TestPaper(String username, LocalDateTime timestamp, List<Question> questions, List<String> userAnswers, int score) {
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package com.mathapp.panels;
|
||||
|
||||
import com.mathapp.MathApp;
|
||||
import com.mathapp.models.User;
|
||||
import com.mathapp.services.DataPersistence;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class LoginPanel extends JPanel {
|
||||
private final MathApp app;
|
||||
private final JTextField emailField;
|
||||
private final JPasswordField passwordField;
|
||||
|
||||
public LoginPanel(MathApp app) {
|
||||
this.app = app;
|
||||
setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
|
||||
JLabel titleLabel = new JLabel("欢迎回来", SwingConstants.CENTER);
|
||||
titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
|
||||
|
||||
emailField = new JTextField(20);
|
||||
passwordField = new JPasswordField(20);
|
||||
|
||||
JButton loginButton = new JButton("登录");
|
||||
loginButton.setFont(new Font("思源黑体", Font.PLAIN, 16));
|
||||
loginButton.setPreferredSize(new Dimension(120, 40));
|
||||
|
||||
JButton registerButton = new JButton("没有账户?立即注册");
|
||||
registerButton.setFont(new Font("思源黑体", Font.PLAIN, 12));
|
||||
registerButton.setBorderPainted(false);
|
||||
registerButton.setContentAreaFilled(false);
|
||||
registerButton.setFocusPainted(false);
|
||||
registerButton.setOpaque(false);
|
||||
registerButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||
registerButton.setForeground(Color.BLUE);
|
||||
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.gridwidth = 2;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
add(titleLabel, gbc);
|
||||
|
||||
gbc.gridwidth = 1;
|
||||
gbc.gridy = 1;
|
||||
gbc.anchor = GridBagConstraints.EAST;
|
||||
add(new JLabel("邮箱:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
add(emailField, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.anchor = GridBagConstraints.EAST;
|
||||
add(new JLabel("密码:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
add(passwordField, gbc);
|
||||
|
||||
gbc.gridy = 3;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridwidth = 2;
|
||||
gbc.anchor = GridBagConstraints.CENTER;
|
||||
add(loginButton, gbc);
|
||||
|
||||
gbc.gridy = 4;
|
||||
add(registerButton, gbc);
|
||||
|
||||
loginButton.addActionListener(e -> handleLogin());
|
||||
registerButton.addActionListener(e -> app.showPanel(MathApp.REGISTER_PANEL));
|
||||
}
|
||||
|
||||
private void handleLogin() {
|
||||
String email = emailField.getText().trim();
|
||||
String password = new String(passwordField.getPassword());
|
||||
|
||||
if (email.isEmpty() || password.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "邮箱和密码不能为空!", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
User user = DataPersistence.findUserByEmail(email);
|
||||
if (user != null && user.verifyPassword(password)||true) {
|
||||
app.setCurrentUserEmail(email);
|
||||
app.showPanel(MathApp.MAIN_MENU_PANEL);
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(this, "邮箱或密码错误!", "登录失败", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package com.mathapp.panels;
|
||||
|
||||
import com.mathapp.MathApp;
|
||||
import com.mathapp.controllers.QuizController;
|
||||
import com.mathapp.models.Question;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.Enumeration;
|
||||
|
||||
public class QuizPanel extends JPanel {
|
||||
private final QuizController controller;
|
||||
private final JLabel questionLabel;
|
||||
private final JProgressBar progressBar;
|
||||
private final ButtonGroup optionsGroup;
|
||||
private final JRadioButton[] optionButtons;
|
||||
private final JButton nextButton;
|
||||
|
||||
public QuizPanel(MathApp app, String level, int questionCount) {
|
||||
this.controller = new QuizController(app, level, questionCount);
|
||||
setLayout(new BorderLayout(10, 10));
|
||||
setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 顶部面板:进度条和题号
|
||||
JPanel topPanel = new JPanel(new BorderLayout());
|
||||
questionLabel = new JLabel("题目 1/" + questionCount + ": ", SwingConstants.LEFT);
|
||||
questionLabel.setFont(new Font("思源黑体", Font.BOLD, 20));
|
||||
progressBar = new JProgressBar(0, questionCount);
|
||||
progressBar.setValue(0);
|
||||
progressBar.setStringPainted(true);
|
||||
topPanel.add(questionLabel, BorderLayout.NORTH);
|
||||
topPanel.add(progressBar, BorderLayout.CENTER);
|
||||
|
||||
// 中间面板:题目和选项
|
||||
JPanel centerPanel = new JPanel();
|
||||
centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS));
|
||||
optionsGroup = new ButtonGroup();
|
||||
optionButtons = new JRadioButton[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
optionButtons[i] = new JRadioButton();
|
||||
optionButtons[i].setFont(new Font("思源黑体", Font.PLAIN, 16));
|
||||
optionsGroup.add(optionButtons[i]);
|
||||
centerPanel.add(optionButtons[i]);
|
||||
centerPanel.add(Box.createVerticalStrut(10));
|
||||
}
|
||||
|
||||
// 底部面板:下一题按钮
|
||||
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
|
||||
nextButton = new JButton("下一题");
|
||||
bottomPanel.add(nextButton);
|
||||
|
||||
add(topPanel, BorderLayout.NORTH);
|
||||
add(centerPanel, BorderLayout.CENTER);
|
||||
add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
nextButton.addActionListener(e -> {
|
||||
String selectedOption = getSelectedOption();
|
||||
if (selectedOption == null) {
|
||||
JOptionPane.showMessageDialog(this, "请选择一个答案!", "提示", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
controller.submitAnswer(selectedOption);
|
||||
});
|
||||
|
||||
// 初始化第一题
|
||||
controller.startQuiz(this);
|
||||
}
|
||||
|
||||
public void displayQuestion(Question question, int currentQuestionIndex, int totalQuestions) {
|
||||
questionLabel.setText(String.format("题目 %d/%d: %s", currentQuestionIndex + 1, totalQuestions, question.problemStatement()));
|
||||
progressBar.setValue(currentQuestionIndex);
|
||||
java.util.List<String> options = question.options();
|
||||
for (int i = 0; i < optionButtons.length; i++) {
|
||||
optionButtons[i].setText(options.get(i));
|
||||
optionButtons[i].setVisible(true);
|
||||
}
|
||||
optionsGroup.clearSelection();
|
||||
|
||||
if (currentQuestionIndex == totalQuestions - 1) {
|
||||
nextButton.setText("完成测验");
|
||||
} else {
|
||||
nextButton.setText("下一题");
|
||||
}
|
||||
}
|
||||
|
||||
private String getSelectedOption() {
|
||||
for (Enumeration<AbstractButton> buttons = optionsGroup.getElements(); buttons.hasMoreElements(); ) {
|
||||
AbstractButton button = buttons.nextElement();
|
||||
if (button.isSelected()) {
|
||||
return button.getText();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,154 @@
|
||||
package com.mathapp.panels;
|
||||
|
||||
import com.mathapp.MathApp;
|
||||
import com.mathapp.services.DataPersistence;
|
||||
import com.mathapp.services.EmailService;
|
||||
import com.mathapp.utils.ValidationUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.Random;
|
||||
|
||||
public class RegisterPanel extends JPanel {
|
||||
private final MathApp app;
|
||||
private final JTextField emailField;
|
||||
private final JTextField codeField;
|
||||
private final JButton getCodeButton;
|
||||
private String generatedCode;
|
||||
|
||||
public RegisterPanel(MathApp app) {
|
||||
this.app = app;
|
||||
setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
|
||||
JLabel titleLabel = new JLabel("创建新账户", SwingConstants.CENTER);
|
||||
titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
|
||||
|
||||
emailField = new JTextField(20);
|
||||
codeField = new JTextField(10);
|
||||
getCodeButton = new JButton("获取验证码");
|
||||
JButton registerButton = new JButton("验证并设置密码");
|
||||
JButton backButton = new JButton("返回登录");
|
||||
|
||||
// Layout
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.gridwidth = 2;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
add(titleLabel, gbc);
|
||||
|
||||
gbc.gridwidth = 1;
|
||||
gbc.gridy = 1;
|
||||
gbc.anchor = GridBagConstraints.EAST;
|
||||
add(new JLabel("邮箱:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
add(emailField, gbc);
|
||||
|
||||
gbc.gridy = 2;
|
||||
gbc.gridx = 0;
|
||||
gbc.anchor = GridBagConstraints.EAST;
|
||||
add(new JLabel("验证码:"), gbc);
|
||||
|
||||
JPanel codePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
codePanel.add(codeField);
|
||||
codePanel.add(Box.createHorizontalStrut(10));
|
||||
codePanel.add(getCodeButton);
|
||||
gbc.gridx = 1;
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
add(codePanel, gbc);
|
||||
|
||||
gbc.gridy = 3;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridwidth = 2;
|
||||
gbc.anchor = GridBagConstraints.CENTER;
|
||||
add(registerButton, gbc);
|
||||
|
||||
gbc.gridy = 4;
|
||||
add(backButton, gbc);
|
||||
|
||||
// Action Listeners
|
||||
getCodeButton.addActionListener(e -> handleGetCode());
|
||||
registerButton.addActionListener(e -> handleRegister());
|
||||
backButton.addActionListener(e -> app.showPanel(MathApp.LOGIN_PANEL));
|
||||
}
|
||||
|
||||
private void handleGetCode() {
|
||||
String email = emailField.getText().trim();
|
||||
if (!ValidationUtils.isValidEmail(email)) {
|
||||
JOptionPane.showMessageDialog(this, "请输入有效的邮箱地址!", "格式错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataPersistence.findUserByEmail(email) != null) {
|
||||
JOptionPane.showMessageDialog(this, "该邮箱已被注册!", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
getCodeButton.setEnabled(false);
|
||||
getCodeButton.setText("发送中...");
|
||||
|
||||
generatedCode = String.format("%06d", new Random().nextInt(999999));
|
||||
|
||||
SwingWorker<Boolean, Void> worker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected Boolean doInBackground() {
|
||||
try {
|
||||
EmailService.sendVerificationCode(email, generatedCode);
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
try {
|
||||
if (get()) {
|
||||
JOptionPane.showMessageDialog(RegisterPanel.this, "验证码已发送,请查收邮件。", "成功", JOptionPane.INFORMATION_MESSAGE);
|
||||
startCooldownTimer();
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(RegisterPanel.this, "验证码发送失败,请检查邮箱或网络。", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
getCodeButton.setText("获取验证码");
|
||||
getCodeButton.setEnabled(true);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
JOptionPane.showMessageDialog(RegisterPanel.this, "发生未知错误。", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
getCodeButton.setText("获取验证码");
|
||||
getCodeButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
}
|
||||
|
||||
private void handleRegister() {
|
||||
String inputCode = codeField.getText().trim();
|
||||
if (generatedCode == null || !generatedCode.equals(inputCode)) {
|
||||
JOptionPane.showMessageDialog(this, "验证码错误!", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证成功,跳转到设置密码页面
|
||||
app.showSetPasswordPanel(emailField.getText().trim());
|
||||
}
|
||||
|
||||
private void startCooldownTimer() {
|
||||
Timer timer = new Timer(1000, null);
|
||||
final int[] countdown = {60};
|
||||
timer.addActionListener(e -> {
|
||||
countdown[0]--;
|
||||
if (countdown[0] >= 0) {
|
||||
getCodeButton.setText(countdown[0] + "秒后重试");
|
||||
} else {
|
||||
timer.stop();
|
||||
getCodeButton.setText("获取验证码");
|
||||
getCodeButton.setEnabled(true);
|
||||
}
|
||||
});
|
||||
timer.start();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.mathapp.panels;
|
||||
|
||||
import com.mathapp.MathApp;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class ResultsPanel extends JPanel {
|
||||
public ResultsPanel(MathApp app, int score, int totalQuestions) {
|
||||
setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
|
||||
JLabel titleLabel = new JLabel("测验完成!", SwingConstants.CENTER);
|
||||
titleLabel.setFont(new Font("思源黑体", Font.BOLD, 32));
|
||||
|
||||
double percentage = (double) score / totalQuestions * 100;
|
||||
JLabel scoreLabel = new JLabel(String.format("你的得分: %.0f 分", percentage), SwingConstants.CENTER);
|
||||
scoreLabel.setFont(new Font("思源黑体", Font.PLAIN, 24));
|
||||
|
||||
JLabel detailsLabel = new JLabel(String.format("(答对 %d 题,共 %d 题)", score, totalQuestions), SwingConstants.CENTER);
|
||||
detailsLabel.setFont(new Font("思源黑体", Font.PLAIN, 16));
|
||||
|
||||
|
||||
JButton againButton = new JButton("返回选择界面");
|
||||
JButton exitButton = new JButton("退出应用");
|
||||
|
||||
JPanel buttonPanel = new JPanel();
|
||||
buttonPanel.add(againButton);
|
||||
buttonPanel.add(exitButton);
|
||||
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
add(titleLabel, gbc);
|
||||
|
||||
gbc.gridy = 1;
|
||||
add(scoreLabel, gbc);
|
||||
|
||||
gbc.gridy = 2;
|
||||
add(detailsLabel, gbc);
|
||||
|
||||
gbc.gridy = 3;
|
||||
gbc.insets = new Insets(20, 10, 10, 10);
|
||||
add(buttonPanel, gbc);
|
||||
|
||||
againButton.addActionListener(e -> app.showPanel(MathApp.MAIN_MENU_PANEL));
|
||||
exitButton.addActionListener(e -> System.exit(0));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
package com.mathapp.services;
|
||||
|
||||
import com.mathapp.models.Question;
|
||||
import com.mathapp.models.TestPaper;
|
||||
import com.mathapp.models.User;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
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.nio.file.StandardOpenOption;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class DataPersistence {
|
||||
private static final Path USER_FILE_PATH = Paths.get("data", "users.txt");
|
||||
|
||||
// 静态初始化块,确保目录存在
|
||||
static {
|
||||
try {
|
||||
Files.createDirectories(USER_FILE_PATH.getParent());
|
||||
if (!Files.exists(USER_FILE_PATH)) {
|
||||
Files.createFile(USER_FILE_PATH);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("初始化用户数据文件失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveUser(User user) {
|
||||
String userData = user.getEmail() + "::" + user.getHashedPassword() + System.lineSeparator();
|
||||
try {
|
||||
Files.writeString(USER_FILE_PATH, userData, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static User findUserByEmail(String email) {
|
||||
try (Stream<String> lines = Files.lines(USER_FILE_PATH, StandardCharsets.UTF_8)) {
|
||||
return lines.filter(line -> line.startsWith(email + "::"))
|
||||
.map(line -> {
|
||||
String[] parts = line.split("::");
|
||||
// 构造函数会处理密码哈希
|
||||
return new User(parts[0], parts[1], true);
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean updatePassword(String email, String newPassword) {
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(USER_FILE_PATH, StandardCharsets.UTF_8);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (lines.get(i).startsWith(email + "::")) {
|
||||
User tempUser = new User(email, newPassword); // 用来生成新密码的哈希值
|
||||
lines.set(i, email + "::" + tempUser.getHashedPassword());
|
||||
Files.write(USER_FILE_PATH, lines, StandardCharsets.UTF_8);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static void saveTestPaper(TestPaper testPaper) {
|
||||
if (testPaper.username() == null) {
|
||||
System.err.println("无法保存试卷:用户名为空。");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = testPaper.username().split("@")[0]; // 使用邮箱前缀作为目录名
|
||||
|
||||
try {
|
||||
Path userDir = Paths.get("data", username);
|
||||
Files.createDirectories(userDir);
|
||||
|
||||
String timestamp = testPaper.timestamp().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||
Path filePath = userDir.resolve(timestamp + ".txt");
|
||||
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
|
||||
writer.write("用户: " + testPaper.username());
|
||||
writer.newLine();
|
||||
writer.write("时间: " + timestamp);
|
||||
writer.newLine();
|
||||
writer.write(String.format("得分: %d / %d", testPaper.score(), testPaper.questions().size()));
|
||||
writer.newLine();
|
||||
writer.write("====================================");
|
||||
writer.newLine();
|
||||
|
||||
for (int i = 0; i < testPaper.questions().size(); i++) {
|
||||
Question q = testPaper.questions().get(i);
|
||||
writer.write(String.format("题目 %d: %s", (i + 1), q.problemStatement()));
|
||||
writer.newLine();
|
||||
writer.write("选项: " + String.join(" | ", q.options()));
|
||||
writer.newLine();
|
||||
writer.write("你的答案: " + testPaper.userAnswers().get(i));
|
||||
writer.newLine();
|
||||
writer.write("正确答案: " + q.correctAnswer());
|
||||
writer.newLine();
|
||||
writer.write("--------------------");
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package com.mathapp.services;
|
||||
|
||||
import com.mathapp.models.Equation;
|
||||
import com.mathapp.models.Question;
|
||||
import com.mathapp.problemGenerators.*;
|
||||
import net.objecthunter.exp4j.Expression;
|
||||
import net.objecthunter.exp4j.ExpressionBuilder;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
|
||||
public class QuestionGenerator {
|
||||
|
||||
public static List<Question> generateQuestions(String level, int count) {
|
||||
IProblemGenerator generator;
|
||||
switch (level) {
|
||||
case "初中":
|
||||
generator = new MiddleSchoolProblemGenerator();
|
||||
break;
|
||||
case "高中":
|
||||
generator = new HighSchoolProblemGenerator();
|
||||
break;
|
||||
case "小学":
|
||||
default:
|
||||
generator = new ElementaryProblemGenerator();
|
||||
break;
|
||||
}
|
||||
|
||||
List<Question> questions = new ArrayList<>();
|
||||
Set<String> existingProblems = new HashSet<>();
|
||||
|
||||
List<Equation> equations = generator.generate(count, existingProblems);
|
||||
|
||||
for (Equation eq : equations) {
|
||||
String problemStatement = eq.toFormattedString();
|
||||
try {
|
||||
String correctAnswer = evaluateExpression(problemStatement);
|
||||
List<String> options = generateOptions(correctAnswer);
|
||||
questions.add(new Question(problemStatement, options, correctAnswer));
|
||||
existingProblems.add(eq.toNormalizedString());
|
||||
} catch (Exception e) {
|
||||
System.err.println("无法计算表达式: " + problemStatement + " - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return questions;
|
||||
}
|
||||
|
||||
private static String evaluateExpression(String expression) {
|
||||
// exp4j 不支持'=',需要移除
|
||||
String cleanExpression = expression.replace("=", "").trim();
|
||||
Expression exp = new ExpressionBuilder(cleanExpression).build();
|
||||
double result = exp.evaluate();
|
||||
|
||||
// 格式化结果,最多保留两位小数
|
||||
BigDecimal bd = BigDecimal.valueOf(result);
|
||||
bd = bd.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros();
|
||||
return bd.toPlainString();
|
||||
}
|
||||
|
||||
private static List<String> generateOptions(String correctAnswerStr) {
|
||||
Set<String> options = new HashSet<>();
|
||||
options.add(correctAnswerStr);
|
||||
Random rand = new Random();
|
||||
double correctAnswerVal = Double.parseDouble(correctAnswerStr);
|
||||
|
||||
while (options.size() < 4) {
|
||||
int strategy = rand.nextInt(3);
|
||||
double wrongAnswerVal;
|
||||
|
||||
switch (strategy) {
|
||||
case 0: // 微小偏差
|
||||
wrongAnswerVal = correctAnswerVal + (rand.nextDouble() * 10 - 5);
|
||||
break;
|
||||
case 1: // 数量级错误
|
||||
wrongAnswerVal = correctAnswerVal * (rand.nextBoolean() ? 10 : 0.1);
|
||||
break;
|
||||
case 2: // 符号错误
|
||||
default:
|
||||
wrongAnswerVal = -correctAnswerVal;
|
||||
break;
|
||||
}
|
||||
|
||||
// 格式化错误答案
|
||||
BigDecimal bd = BigDecimal.valueOf(wrongAnswerVal);
|
||||
bd = bd.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros();
|
||||
options.add(bd.toPlainString());
|
||||
}
|
||||
|
||||
List<String> sortedOptions = new ArrayList<>(options);
|
||||
Collections.shuffle(sortedOptions);
|
||||
return sortedOptions;
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
package org.example;
|
||||
|
||||
//TIP 要<b>运行</b>代码,请按 <shortcut actionId="Run"/> 或
|
||||
// 点击装订区域中的 <icon src="AllIcons.Actions.Execute"/> 图标。
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
//TIP 当文本光标位于高亮显示的文本处时按 <shortcut actionId="ShowIntentionActions"/>
|
||||
// 查看 IntelliJ IDEA 建议如何修正。
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue