@ -0,0 +1,62 @@
|
||||
package com.personalproject.auth;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Interface for sending emails with registration codes.
|
||||
*/
|
||||
public final class EmailService {
|
||||
|
||||
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
private static final int CODE_LENGTH = 6;
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private EmailService() {
|
||||
// Prevent instantiation of utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random registration code.
|
||||
*
|
||||
* @return A randomly generated registration code
|
||||
*/
|
||||
public static String generateRegistrationCode() {
|
||||
StringBuilder code = new StringBuilder();
|
||||
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||
code.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
|
||||
}
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a registration code to the specified email address. In a real implementation, this would
|
||||
* connect to an email server.
|
||||
*
|
||||
* @param email The email address to send the code to
|
||||
* @param registrationCode The registration code to send
|
||||
* @return true if successfully sent (in this mock implementation, always true)
|
||||
*/
|
||||
public static boolean sendRegistrationCode(String email, String registrationCode) {
|
||||
// In a real implementation, this would connect to an email server
|
||||
// For the mock implementation, we'll just print to console
|
||||
System.out.println("Sending registration code " + registrationCode + " to " + email);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if an email address has a valid format.
|
||||
*
|
||||
* @param email The email address to validate
|
||||
* @return true if the email has valid format, false otherwise
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple email validation using regex
|
||||
String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@"
|
||||
+ "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
|
||||
return email.matches(emailRegex);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.personalproject.auth;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility class for password validation.
|
||||
*/
|
||||
public final class PasswordValidator {
|
||||
|
||||
private static final Pattern PASSWORD_PATTERN =
|
||||
Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$");
|
||||
|
||||
private PasswordValidator() {
|
||||
// Prevent instantiation of utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a password meets the requirements: - 6-10 characters - Contains at least one
|
||||
* uppercase letter - Contains at least one lowercase letter - Contains at least one digit.
|
||||
*
|
||||
* @param password The password to validate
|
||||
* @return true if password meets requirements, false otherwise
|
||||
*/
|
||||
public static boolean isValidPassword(String password) {
|
||||
if (password == null) {
|
||||
return false;
|
||||
}
|
||||
return PASSWORD_PATTERN.matcher(password).matches();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.personalproject.auth;
|
||||
|
||||
import com.personalproject.model.DifficultyLevel;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 不可变的账号定义.
|
||||
*/
|
||||
public record UserAccount(
|
||||
String username,
|
||||
String email,
|
||||
String password,
|
||||
DifficultyLevel difficultyLevel,
|
||||
LocalDateTime registrationDate,
|
||||
boolean isRegistered) {
|
||||
|
||||
/**
|
||||
* Creates a new user account with registration date set to now.
|
||||
*
|
||||
* @param username The username
|
||||
* @param email The email address
|
||||
* @param password The password
|
||||
* @param difficultyLevel The selected difficulty level
|
||||
* @param isRegistered Whether the user has completed registration
|
||||
*/
|
||||
public UserAccount {
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Username cannot be null or empty");
|
||||
}
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Email cannot be null or empty");
|
||||
}
|
||||
if (password == null) {
|
||||
throw new IllegalArgumentException("Password cannot be null");
|
||||
}
|
||||
if (difficultyLevel == null) {
|
||||
throw new IllegalArgumentException("Difficulty level cannot be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.personalproject.generator;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 生成至少包含一种三角函数的高中难度题目表达式.
|
||||
*/
|
||||
public final class HighSchoolQuestionGenerator implements QuestionGenerator {
|
||||
|
||||
private static final String[] OPERATORS = {"+", "-", "*", "/"};
|
||||
private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"};
|
||||
|
||||
@Override
|
||||
public String generateQuestion(Random random) {
|
||||
int operandCount = random.nextInt(5) + 1;
|
||||
String[] operands = new String[operandCount];
|
||||
for (int index = 0; index < operandCount; index++) {
|
||||
operands[index] = String.valueOf(random.nextInt(100) + 1);
|
||||
}
|
||||
int specialIndex = random.nextInt(operandCount);
|
||||
String function = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)];
|
||||
operands[specialIndex] = function + '(' + operands[specialIndex] + ')';
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < operandCount; index++) {
|
||||
if (index > 0) {
|
||||
String operator = OPERATORS[random.nextInt(OPERATORS.length)];
|
||||
builder.append(' ').append(operator).append(' ');
|
||||
}
|
||||
builder.append(operands[index]);
|
||||
}
|
||||
String expression = builder.toString();
|
||||
if (operandCount > 1 && random.nextBoolean()) {
|
||||
return '(' + expression + ')';
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.personalproject.generator;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 生成包含平方或开根号运算的初中难度题目表达式.
|
||||
*/
|
||||
public final class MiddleSchoolQuestionGenerator implements QuestionGenerator {
|
||||
|
||||
private static final String[] OPERATORS = {"+", "-", "*", "/"};
|
||||
|
||||
@Override
|
||||
public String generateQuestion(Random random) {
|
||||
int operandCount = random.nextInt(5) + 1;
|
||||
String[] operands = new String[operandCount];
|
||||
for (int index = 0; index < operandCount; index++) {
|
||||
operands[index] = String.valueOf(random.nextInt(100) + 1);
|
||||
}
|
||||
int specialIndex = random.nextInt(operandCount);
|
||||
if (random.nextBoolean()) {
|
||||
operands[specialIndex] = '(' + operands[specialIndex] + ")^2";
|
||||
} else {
|
||||
operands[specialIndex] = "sqrt(" + operands[specialIndex] + ')';
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < operandCount; index++) {
|
||||
if (index > 0) {
|
||||
String operator = OPERATORS[random.nextInt(OPERATORS.length)];
|
||||
builder.append(' ').append(operator).append(' ');
|
||||
}
|
||||
builder.append(operands[index]);
|
||||
}
|
||||
String expression = builder.toString();
|
||||
if (operandCount > 1 && random.nextBoolean()) {
|
||||
return '(' + expression + ')';
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package com.personalproject.generator;
|
||||
|
||||
import com.personalproject.model.QuizQuestion;
|
||||
import com.personalproject.service.MathExpressionEvaluator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 生成包含基础四则运算的小学难度题目表达式.
|
||||
*/
|
||||
public final class PrimaryQuestionGenerator implements QuestionGenerator, QuizQuestionGenerator {
|
||||
|
||||
private static final String[] OPERATORS = {"+", "-", "*", "/"};
|
||||
private static final int OPTIONS_COUNT = 4;
|
||||
|
||||
@Override
|
||||
public String generateQuestion(Random random) {
|
||||
// 至少生成两个操作数,避免题目退化成单个数字
|
||||
int operandCount = random.nextInt(4) + 2;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < operandCount; index++) {
|
||||
if (index > 0) {
|
||||
String operator = OPERATORS[random.nextInt(OPERATORS.length)];
|
||||
builder.append(' ').append(operator).append(' ');
|
||||
}
|
||||
int value = random.nextInt(100) + 1;
|
||||
builder.append(value);
|
||||
}
|
||||
String expression = builder.toString();
|
||||
if (operandCount > 1 && random.nextBoolean()) {
|
||||
return '(' + expression + ')';
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuizQuestion generateQuizQuestion(Random random) {
|
||||
// 生成数学表达式
|
||||
int operandCount = random.nextInt(4) + 2;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < operandCount; index++) {
|
||||
if (index > 0) {
|
||||
String operator = OPERATORS[random.nextInt(OPERATORS.length)];
|
||||
builder.append(' ').append(operator).append(' ');
|
||||
}
|
||||
int value = random.nextInt(100) + 1;
|
||||
builder.append(value);
|
||||
}
|
||||
String expression = builder.toString();
|
||||
if (operandCount > 1 && random.nextBoolean()) {
|
||||
expression = '(' + expression + ')';
|
||||
}
|
||||
|
||||
// 直接计算正确答案
|
||||
double correctAnswer;
|
||||
try {
|
||||
correctAnswer = MathExpressionEvaluator.evaluate(expression);
|
||||
} catch (Exception e) {
|
||||
// Fallback if evaluation fails
|
||||
correctAnswer = 0.0;
|
||||
}
|
||||
|
||||
// 生成选项
|
||||
List<String> options = generateOptions(correctAnswer, random);
|
||||
|
||||
// 随机选择正确答案索引
|
||||
int correctAnswerIndex = random.nextInt(options.size());
|
||||
|
||||
// 确保正确答案在选项中
|
||||
String correctOption = String.format("%.2f", correctAnswer);
|
||||
String oldOption = options.set(correctAnswerIndex, correctOption);
|
||||
|
||||
// Add the displaced option back to maintain 4 options
|
||||
options.add(oldOption);
|
||||
|
||||
return new QuizQuestion(expression, options, correctAnswerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成选择题选项
|
||||
*/
|
||||
private List<String> generateOptions(double correctAnswer, Random random) {
|
||||
List<String> options = new ArrayList<>();
|
||||
|
||||
// Add correct answer as one of the options
|
||||
options.add(String.format("%.2f", correctAnswer));
|
||||
|
||||
// Add incorrect options
|
||||
for (int i = 0; i < OPTIONS_COUNT - 1; i++) {
|
||||
double incorrectAnswer = correctAnswer + (random.nextGaussian() * 10); // Add random offset
|
||||
if (Math.abs(incorrectAnswer - correctAnswer) < 0.1) { // Ensure different
|
||||
incorrectAnswer += 1.5;
|
||||
}
|
||||
options.add(String.format("%.2f", incorrectAnswer));
|
||||
}
|
||||
|
||||
// Shuffle to randomize correct answer position
|
||||
Collections.shuffle(options, random);
|
||||
|
||||
// Find the correct answer index after shuffling
|
||||
String correctAnswerStr = String.format("%.2f", correctAnswer);
|
||||
int correctIndex = options.indexOf(correctAnswerStr);
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.personalproject.generator;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 负责生成单条数学题目的表达式.
|
||||
*/
|
||||
public interface QuestionGenerator {
|
||||
|
||||
/**
|
||||
* 基于提供的随机数生成器构造一道题目的表达式.
|
||||
*
|
||||
* @param random 用于生成随机数的实例.
|
||||
* @return 生成的题目表达式.
|
||||
*/
|
||||
String generateQuestion(Random random);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.personalproject.generator;
|
||||
|
||||
import com.personalproject.model.QuizQuestion;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 负责生成带答案的数学题目.
|
||||
*/
|
||||
public interface QuizQuestionGenerator {
|
||||
|
||||
/**
|
||||
* 基于提供的随机数生成器构造一道带答案的题目.
|
||||
*
|
||||
* @param random 用于生成随机数的实例.
|
||||
* @return 生成的带答案的题目.
|
||||
*/
|
||||
QuizQuestion generateQuizQuestion(Random random);
|
||||
}
|
||||
@ -0,0 +1,257 @@
|
||||
package com.personalproject.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents an exam session for a user.
|
||||
*/
|
||||
public final class ExamSession {
|
||||
|
||||
private final String username;
|
||||
private final DifficultyLevel difficultyLevel;
|
||||
private final List<QuizQuestion> questions;
|
||||
private final List<Integer> userAnswers;
|
||||
private final LocalDateTime startTime;
|
||||
private int currentQuestionIndex;
|
||||
|
||||
/**
|
||||
* Creates a new exam session.
|
||||
*
|
||||
* @param username The username of the test taker
|
||||
* @param difficultyLevel The difficulty level of the exam
|
||||
* @param questions The list of questions for the exam
|
||||
*/
|
||||
public ExamSession(String username, DifficultyLevel difficultyLevel,
|
||||
List<QuizQuestion> questions) {
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Username cannot be null or empty");
|
||||
}
|
||||
if (difficultyLevel == null) {
|
||||
throw new IllegalArgumentException("Difficulty level cannot be null");
|
||||
}
|
||||
if (questions == null || questions.isEmpty()) {
|
||||
throw new IllegalArgumentException("Questions list cannot be null or empty");
|
||||
}
|
||||
|
||||
this.username = username;
|
||||
this.difficultyLevel = difficultyLevel;
|
||||
this.questions = List.copyOf(questions); // Immutable copy of questions
|
||||
this.userAnswers = new ArrayList<>();
|
||||
// Initialize user answers with -1 (no answer selected)
|
||||
for (int i = 0; i < questions.size(); i++) {
|
||||
userAnswers.add(-1);
|
||||
}
|
||||
this.startTime = LocalDateTime.now();
|
||||
this.currentQuestionIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the username of the test taker.
|
||||
*
|
||||
* @return The username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the difficulty level of the exam.
|
||||
*
|
||||
* @return The difficulty level
|
||||
*/
|
||||
public DifficultyLevel getDifficultyLevel() {
|
||||
return difficultyLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of questions in the exam.
|
||||
*
|
||||
* @return An unmodifiable list of questions
|
||||
*/
|
||||
public List<QuizQuestion> getQuestions() {
|
||||
return questions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's answers to the questions.
|
||||
*
|
||||
* @return A list of answer indices (-1 means no answer selected)
|
||||
*/
|
||||
public List<Integer> getUserAnswers() {
|
||||
return List.copyOf(userAnswers); // Return a copy to prevent modification
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current question index.
|
||||
*
|
||||
* @return The current question index
|
||||
*/
|
||||
public int getCurrentQuestionIndex() {
|
||||
return currentQuestionIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's answer for the current question.
|
||||
*
|
||||
* @param answerIndex The index of the selected answer
|
||||
*/
|
||||
public void setAnswer(int answerIndex) {
|
||||
if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) {
|
||||
throw new IllegalStateException("No valid question at current index");
|
||||
}
|
||||
if (answerIndex < 0 || answerIndex > questions.get(currentQuestionIndex).getOptions().size()) {
|
||||
throw new IllegalArgumentException("Invalid answer index");
|
||||
}
|
||||
userAnswers.set(currentQuestionIndex, answerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to the next question.
|
||||
*
|
||||
* @return true if successfully moved to next question, false if already at the last question
|
||||
*/
|
||||
public boolean goToNextQuestion() {
|
||||
if (currentQuestionIndex < questions.size() - 1) {
|
||||
currentQuestionIndex++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to the previous question.
|
||||
*
|
||||
* @return true if successfully moved to previous question, false if already at the first question
|
||||
*/
|
||||
public boolean goToPreviousQuestion() {
|
||||
if (currentQuestionIndex > 0) {
|
||||
currentQuestionIndex--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the exam is complete (all questions answered or at the end).
|
||||
*
|
||||
* @return true if the exam is complete, false otherwise
|
||||
*/
|
||||
public boolean isComplete() {
|
||||
return currentQuestionIndex >= questions.size() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current question.
|
||||
*
|
||||
* @return The current quiz question
|
||||
*/
|
||||
public QuizQuestion getCurrentQuestion() {
|
||||
if (currentQuestionIndex < 0 || currentQuestionIndex >= questions.size()) {
|
||||
throw new IllegalStateException("No valid question at current index");
|
||||
}
|
||||
return questions.get(currentQuestionIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's answer for a specific question.
|
||||
*
|
||||
* @param questionIndex The index of the question
|
||||
* @return The index of the user's answer (or -1 if no answer selected)
|
||||
*/
|
||||
public int getUserAnswer(int questionIndex) {
|
||||
if (questionIndex < 0 || questionIndex >= questions.size()) {
|
||||
throw new IllegalArgumentException("Question index out of bounds");
|
||||
}
|
||||
return userAnswers.get(questionIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the score as a percentage.
|
||||
*
|
||||
* @return The score as a percentage (0-100)
|
||||
*/
|
||||
public double calculateScore() {
|
||||
int correctCount = 0;
|
||||
for (int i = 0; i < questions.size(); i++) {
|
||||
QuizQuestion question = questions.get(i);
|
||||
int userAnswer = userAnswers.get(i);
|
||||
if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) {
|
||||
correctCount++;
|
||||
}
|
||||
}
|
||||
return questions.isEmpty() ? 0.0 : (double) correctCount / questions.size() * 100.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of questions in the exam.
|
||||
*
|
||||
* @return The total number of questions
|
||||
*/
|
||||
public int getTotalQuestions() {
|
||||
return questions.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific question has been answered.
|
||||
*
|
||||
* @param questionIndex The index of the question
|
||||
* @return true if the question has been answered, false otherwise
|
||||
*/
|
||||
public boolean hasAnswered(int questionIndex) {
|
||||
if (questionIndex < 0 || questionIndex >= questions.size()) {
|
||||
throw new IllegalArgumentException("Question index out of bounds");
|
||||
}
|
||||
return userAnswers.get(questionIndex) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start time of the exam.
|
||||
*
|
||||
* @return The start time
|
||||
*/
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of correct answers.
|
||||
*
|
||||
* @return The count of correct answers
|
||||
*/
|
||||
public int getCorrectAnswersCount() {
|
||||
int correctCount = 0;
|
||||
for (int i = 0; i < questions.size(); i++) {
|
||||
QuizQuestion question = questions.get(i);
|
||||
int userAnswer = userAnswers.get(i);
|
||||
if (userAnswer != -1 && question.isAnswerCorrect(userAnswer)) {
|
||||
correctCount++;
|
||||
}
|
||||
}
|
||||
return correctCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of incorrect answers.
|
||||
*
|
||||
* @return The count of incorrect answers
|
||||
*/
|
||||
public int getIncorrectAnswersCount() {
|
||||
int totalAnswered = 0;
|
||||
int correctCount = 0;
|
||||
|
||||
for (int i = 0; i < questions.size(); i++) {
|
||||
int userAnswer = userAnswers.get(i);
|
||||
if (userAnswer != -1) {
|
||||
totalAnswered++;
|
||||
QuizQuestion question = questions.get(i);
|
||||
if (question.isAnswerCorrect(userAnswer)) {
|
||||
correctCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalAnswered - correctCount;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.personalproject.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a quiz question with multiple choice options.
|
||||
*/
|
||||
public final class QuizQuestion {
|
||||
|
||||
private final String questionText;
|
||||
private final List<String> options;
|
||||
private final int correctAnswerIndex;
|
||||
|
||||
/**
|
||||
* Creates a new quiz question.
|
||||
*
|
||||
* @param questionText The text of the question
|
||||
* @param options The list of answer options
|
||||
* @param correctAnswerIndex The index of the correct answer in the options list
|
||||
*/
|
||||
public QuizQuestion(String questionText, List<String> options, int correctAnswerIndex) {
|
||||
if (questionText == null || questionText.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Question text cannot be null or empty");
|
||||
}
|
||||
if (options == null || options.size() < 2) {
|
||||
throw new IllegalArgumentException("Options must contain at least 2 choices");
|
||||
}
|
||||
if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) {
|
||||
throw new IllegalArgumentException("Correct answer index out of bounds");
|
||||
}
|
||||
|
||||
this.questionText = questionText;
|
||||
this.options = List.copyOf(options); // Immutable copy
|
||||
this.correctAnswerIndex = correctAnswerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the question text.
|
||||
*
|
||||
* @return The question text
|
||||
*/
|
||||
public String getQuestionText() {
|
||||
return questionText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of answer options.
|
||||
*
|
||||
* @return An unmodifiable list of answer options
|
||||
*/
|
||||
public List<String> getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of the correct answer in the options list.
|
||||
*
|
||||
* @return The index of the correct answer
|
||||
*/
|
||||
public int getCorrectAnswerIndex() {
|
||||
return correctAnswerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given answer index matches the correct answer.
|
||||
*
|
||||
* @param answerIndex The index of the user's answer
|
||||
* @return true if the answer is correct, false otherwise
|
||||
*/
|
||||
public boolean isAnswerCorrect(int answerIndex) {
|
||||
return answerIndex == correctAnswerIndex;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,217 @@
|
||||
package com.personalproject.service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A mathematical expression evaluator that can handle basic arithmetic operations.
|
||||
*/
|
||||
public final class MathExpressionEvaluator {
|
||||
|
||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?");
|
||||
private static final Map<Character, Integer> PRECEDENCE = new HashMap<>();
|
||||
|
||||
static {
|
||||
PRECEDENCE.put('+', 1);
|
||||
PRECEDENCE.put('-', 1);
|
||||
PRECEDENCE.put('*', 2);
|
||||
PRECEDENCE.put('/', 2);
|
||||
PRECEDENCE.put('^', 3);
|
||||
}
|
||||
|
||||
private MathExpressionEvaluator() {
|
||||
// Prevent instantiation of utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a mathematical expression string.
|
||||
*
|
||||
* @param expression The mathematical expression to evaluate
|
||||
* @return The result of the evaluation
|
||||
* @throws IllegalArgumentException If the expression is invalid
|
||||
*/
|
||||
public static double evaluate(String expression) {
|
||||
if (expression == null) {
|
||||
throw new IllegalArgumentException("Expression cannot be null");
|
||||
}
|
||||
|
||||
expression = expression.replaceAll("\\s+", ""); // Remove whitespace
|
||||
if (expression.isEmpty()) {
|
||||
throw new IllegalArgumentException("Expression cannot be empty");
|
||||
}
|
||||
|
||||
// Tokenize the expression
|
||||
String[] tokens = tokenize(expression);
|
||||
|
||||
// Convert infix to postfix notation using Shunting Yard algorithm
|
||||
String[] postfix = infixToPostfix(tokens);
|
||||
|
||||
// Evaluate the postfix expression
|
||||
return evaluatePostfix(postfix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizes the expression into numbers and operators.
|
||||
*
|
||||
* @param expression The expression to tokenize
|
||||
* @return An array of tokens
|
||||
*/
|
||||
private static String[] tokenize(String expression) {
|
||||
java.util.List<String> tokens = new java.util.ArrayList<>();
|
||||
StringBuilder currentNumber = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < expression.length(); i++) {
|
||||
char c = expression.charAt(i);
|
||||
|
||||
if (Character.isDigit(c) || c == '.') {
|
||||
currentNumber.append(c);
|
||||
} else if (c == '(' || c == ')') {
|
||||
if (currentNumber.length() > 0) {
|
||||
tokens.add(currentNumber.toString());
|
||||
currentNumber.setLength(0);
|
||||
}
|
||||
tokens.add(String.valueOf(c));
|
||||
} else if (isOperator(c)) {
|
||||
if (currentNumber.length() > 0) {
|
||||
tokens.add(currentNumber.toString());
|
||||
currentNumber.setLength(0);
|
||||
}
|
||||
|
||||
// Handle unary minus
|
||||
if (c == '-' && (i == 0 || expression.charAt(i - 1) == '(')) {
|
||||
currentNumber.append(c);
|
||||
} else {
|
||||
tokens.add(String.valueOf(c));
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid character in expression: " + c);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNumber.length() > 0) {
|
||||
tokens.add(currentNumber.toString());
|
||||
}
|
||||
|
||||
return tokens.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the character is an operator.
|
||||
*
|
||||
* @param c The character to check
|
||||
* @return true if the character is an operator, false otherwise
|
||||
*/
|
||||
private static boolean isOperator(char c) {
|
||||
return c == '+' || c == '-' || c == '*' || c == '/' || c == '^';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts infix notation to postfix notation using the Shunting Yard algorithm.
|
||||
*
|
||||
* @param tokens The tokens in infix notation
|
||||
* @return An array of tokens in postfix notation
|
||||
*/
|
||||
private static String[] infixToPostfix(String[] tokens) {
|
||||
java.util.List<String> output = new java.util.ArrayList<>();
|
||||
Stack<String> operators = new Stack<>();
|
||||
|
||||
for (String token : tokens) {
|
||||
if (isNumber(token)) {
|
||||
output.add(token);
|
||||
} else if (token.equals("(")) {
|
||||
operators.push(token);
|
||||
} else if (token.equals(")")) {
|
||||
while (!operators.isEmpty() && !operators.peek().equals("(")) {
|
||||
output.add(operators.pop());
|
||||
}
|
||||
if (!operators.isEmpty()) {
|
||||
operators.pop(); // Remove the "("
|
||||
}
|
||||
} else if (isOperator(token.charAt(0))) {
|
||||
while (!operators.isEmpty()
|
||||
&& isOperator(operators.peek().charAt(0))
|
||||
&& PRECEDENCE.get(operators.peek().charAt(0)) >= PRECEDENCE.get(token.charAt(0))) {
|
||||
output.add(operators.pop());
|
||||
}
|
||||
operators.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
while (!operators.isEmpty()) {
|
||||
output.add(operators.pop());
|
||||
}
|
||||
|
||||
return output.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a postfix expression.
|
||||
*
|
||||
* @param postfix The tokens in postfix notation
|
||||
* @return The result of the evaluation
|
||||
*/
|
||||
private static double evaluatePostfix(String[] postfix) {
|
||||
Stack<Double> values = new Stack<>();
|
||||
|
||||
for (String token : postfix) {
|
||||
if (isNumber(token)) {
|
||||
values.push(Double.parseDouble(token));
|
||||
} else if (isOperator(token.charAt(0))) {
|
||||
if (values.size() < 2) {
|
||||
throw new IllegalArgumentException("Invalid expression: insufficient operands");
|
||||
}
|
||||
|
||||
double b = values.pop();
|
||||
double a = values.pop();
|
||||
double result = performOperation(a, b, token.charAt(0));
|
||||
values.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (values.size() != 1) {
|
||||
throw new IllegalArgumentException("Invalid expression: too many operands");
|
||||
}
|
||||
|
||||
return values.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the specified operation on the two operands.
|
||||
*
|
||||
* @param a The first operand
|
||||
* @param b The second operand
|
||||
* @param operator The operator to apply
|
||||
* @return The result of the operation
|
||||
*/
|
||||
private static double performOperation(double a, double b, char operator) {
|
||||
switch (operator) {
|
||||
case '+':
|
||||
return a + b;
|
||||
case '-':
|
||||
return a - b;
|
||||
case '*':
|
||||
return a * b;
|
||||
case '/':
|
||||
if (b == 0) {
|
||||
throw new ArithmeticException("Division by zero");
|
||||
}
|
||||
return a / b;
|
||||
case '^':
|
||||
return Math.pow(a, b);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown operator: " + operator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the token is a number.
|
||||
*
|
||||
* @param token The token to check
|
||||
* @return true if the token is a number, false otherwise
|
||||
*/
|
||||
private static boolean isNumber(String token) {
|
||||
return NUMBER_PATTERN.matcher(token).matches();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.personalproject.ui;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import com.personalproject.controller.MathLearningController;
|
||||
import com.personalproject.generator.HighSchoolQuestionGenerator;
|
||||
import com.personalproject.generator.MiddleSchoolQuestionGenerator;
|
||||
import com.personalproject.generator.PrimaryQuestionGenerator;
|
||||
import com.personalproject.generator.QuestionGenerator;
|
||||
import com.personalproject.model.DifficultyLevel;
|
||||
import com.personalproject.service.QuestionGenerationService;
|
||||
import com.personalproject.ui.scenes.LoginScene;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JavaFX GUI Application for the Math Learning Software.
|
||||
* This is the main entry point for the GUI application.
|
||||
*/
|
||||
public final class MathExamGUI extends Application {
|
||||
|
||||
private MathLearningController controller;
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
// Initialize the controller with generators
|
||||
Map<DifficultyLevel, QuestionGenerator> generatorMap = new EnumMap<>(DifficultyLevel.class);
|
||||
generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator());
|
||||
generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator());
|
||||
generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator());
|
||||
QuestionGenerationService questionGenerationService = new QuestionGenerationService(generatorMap);
|
||||
this.controller = new MathLearningController(generatorMap, questionGenerationService);
|
||||
|
||||
// Set up the primary stage
|
||||
primaryStage.setTitle("数学学习软件");
|
||||
|
||||
// Start with login scene
|
||||
LoginScene loginScene = new LoginScene(primaryStage, controller);
|
||||
Scene scene = new Scene(loginScene, 600, 400);
|
||||
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the JavaFX application.
|
||||
*
|
||||
* @param args Command-line arguments
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
package com.personalproject.ui.scenes;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.stage.Stage;
|
||||
import com.personalproject.controller.MathLearningController;
|
||||
import com.personalproject.ui.views.MainMenuView;
|
||||
import com.personalproject.ui.scenes.RegistrationScene;
|
||||
|
||||
/**
|
||||
* Scene for handling user login and registration.
|
||||
*/
|
||||
public class LoginScene extends BorderPane {
|
||||
|
||||
private final Stage primaryStage;
|
||||
private final MathLearningController controller;
|
||||
private TextField usernameField;
|
||||
private PasswordField passwordField;
|
||||
private Button loginButton;
|
||||
private Button registerButton;
|
||||
|
||||
/**
|
||||
* Constructor for LoginScene.
|
||||
*
|
||||
* @param primaryStage The main stage of the application
|
||||
* @param controller The math learning controller
|
||||
*/
|
||||
public LoginScene(Stage primaryStage, MathLearningController controller) {
|
||||
this.primaryStage = primaryStage;
|
||||
this.controller = controller;
|
||||
initializeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI components.
|
||||
*/
|
||||
private void initializeUI() {
|
||||
// Create the main layout
|
||||
VBox mainLayout = new VBox(15);
|
||||
mainLayout.setAlignment(Pos.CENTER);
|
||||
mainLayout.setPadding(new Insets(20));
|
||||
|
||||
// Title
|
||||
Label titleLabel = new Label("数学学习软件");
|
||||
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
|
||||
|
||||
// Login Form
|
||||
GridPane loginForm = new GridPane();
|
||||
loginForm.setHgap(10);
|
||||
loginForm.setVgap(10);
|
||||
loginForm.setAlignment(Pos.CENTER);
|
||||
|
||||
Label usernameLabel = new Label("用户名:");
|
||||
usernameField = new TextField();
|
||||
usernameField.setPrefWidth(200);
|
||||
|
||||
Label passwordLabel = new Label("密码:");
|
||||
passwordField = new PasswordField();
|
||||
passwordField.setPrefWidth(200);
|
||||
|
||||
loginForm.add(usernameLabel, 0, 0);
|
||||
loginForm.add(usernameField, 1, 0);
|
||||
loginForm.add(passwordLabel, 0, 1);
|
||||
loginForm.add(passwordField, 1, 1);
|
||||
|
||||
// Buttons
|
||||
HBox buttonBox = new HBox(10);
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
|
||||
loginButton = new Button("登录");
|
||||
registerButton = new Button("注册");
|
||||
|
||||
// Set button styles
|
||||
loginButton.setPrefWidth(100);
|
||||
registerButton.setPrefWidth(100);
|
||||
|
||||
buttonBox.getChildren().addAll(loginButton, registerButton);
|
||||
|
||||
// Add components to main layout
|
||||
mainLayout.getChildren().addAll(titleLabel, loginForm, buttonBox);
|
||||
|
||||
// Set the center of the border pane
|
||||
setCenter(mainLayout);
|
||||
|
||||
// Add event handlers
|
||||
addEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event handlers to UI components.
|
||||
*/
|
||||
private void addEventHandlers() {
|
||||
loginButton.setOnAction(e -> handleLogin());
|
||||
registerButton.setOnAction(e -> handleRegistration());
|
||||
|
||||
// Allow login with Enter key
|
||||
setOnKeyPressed(event -> {
|
||||
if (event.getCode().toString().equals("ENTER")) {
|
||||
handleLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the login process.
|
||||
*/
|
||||
private void handleLogin() {
|
||||
String username = usernameField.getText().trim();
|
||||
String password = passwordField.getText();
|
||||
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
showAlert(Alert.AlertType.WARNING, "警告", "请输入用户名和密码");
|
||||
return;
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
var userAccount = controller.authenticate(username, password);
|
||||
|
||||
if (userAccount.isPresent()) {
|
||||
// Login successful - navigate to main menu
|
||||
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount.get());
|
||||
primaryStage.getScene().setRoot(mainMenuView);
|
||||
} else {
|
||||
// Login failed
|
||||
showAlert(Alert.AlertType.ERROR, "登录失败", "用户名或密码错误");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the registration process.
|
||||
*/
|
||||
private void handleRegistration() {
|
||||
// Switch to registration scene
|
||||
RegistrationScene registrationScene = new RegistrationScene(primaryStage, controller);
|
||||
primaryStage.getScene().setRoot(registrationScene);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an alert dialog.
|
||||
*
|
||||
* @param alertType Type of alert
|
||||
* @param title Title of the alert
|
||||
* @param message Message to display
|
||||
*/
|
||||
private void showAlert(Alert.AlertType alertType, String title, String message) {
|
||||
Alert alert = new Alert(alertType);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
package com.personalproject.ui.views;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.stage.Stage;
|
||||
import com.personalproject.controller.MathLearningController;
|
||||
import com.personalproject.model.ExamSession;
|
||||
import com.personalproject.auth.UserAccount;
|
||||
import com.personalproject.ui.views.MainMenuView;
|
||||
|
||||
/**
|
||||
* View for displaying exam results.
|
||||
*/
|
||||
public class ExamResultsView extends BorderPane {
|
||||
|
||||
private final Stage primaryStage;
|
||||
private final MathLearningController controller;
|
||||
private final ExamSession examSession;
|
||||
private Button continueButton;
|
||||
private Button exitButton;
|
||||
|
||||
/**
|
||||
* Constructor for ExamResultsView.
|
||||
*
|
||||
* @param primaryStage The main stage of the application
|
||||
* @param controller The math learning controller
|
||||
* @param examSession The completed exam session
|
||||
*/
|
||||
public ExamResultsView(Stage primaryStage, MathLearningController controller, ExamSession examSession) {
|
||||
this.primaryStage = primaryStage;
|
||||
this.controller = controller;
|
||||
this.examSession = examSession;
|
||||
initializeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI components.
|
||||
*/
|
||||
private void initializeUI() {
|
||||
// Create the main layout
|
||||
VBox mainLayout = new VBox(20);
|
||||
mainLayout.setAlignment(Pos.CENTER);
|
||||
mainLayout.setPadding(new Insets(20));
|
||||
|
||||
// Results title
|
||||
Label titleLabel = new Label("考试结果");
|
||||
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
|
||||
|
||||
// Score display
|
||||
double score = examSession.calculateScore();
|
||||
Label scoreLabel = new Label(String.format("您的得分: %.2f%%", score));
|
||||
scoreLabel.setFont(Font.font("System", FontWeight.BOLD, 18));
|
||||
|
||||
// Performance breakdown
|
||||
VBox breakdownBox = new VBox(10);
|
||||
breakdownBox.setAlignment(Pos.CENTER);
|
||||
|
||||
Label totalQuestionsLabel = new Label("总题数: " + examSession.getTotalQuestions());
|
||||
Label correctAnswersLabel = new Label("答对题数: " + examSession.getCorrectAnswersCount());
|
||||
Label incorrectAnswersLabel = new Label("答错题数: " + examSession.getIncorrectAnswersCount());
|
||||
|
||||
breakdownBox.getChildren().addAll(totalQuestionsLabel, correctAnswersLabel, incorrectAnswersLabel);
|
||||
|
||||
// Buttons
|
||||
HBox buttonBox = new HBox(15);
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
|
||||
continueButton = new Button("继续考试");
|
||||
exitButton = new Button("退出");
|
||||
|
||||
// Set button sizes
|
||||
continueButton.setPrefSize(120, 40);
|
||||
exitButton.setPrefSize(120, 40);
|
||||
|
||||
buttonBox.getChildren().addAll(continueButton, exitButton);
|
||||
|
||||
// Add components to main layout
|
||||
mainLayout.getChildren().addAll(titleLabel, scoreLabel, breakdownBox, buttonBox);
|
||||
|
||||
// Set the center of the border pane
|
||||
setCenter(mainLayout);
|
||||
|
||||
// Add event handlers
|
||||
addEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event handlers to UI components.
|
||||
*/
|
||||
private void addEventHandlers() {
|
||||
continueButton.setOnAction(e -> handleContinue());
|
||||
exitButton.setOnAction(e -> handleExit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the continue button action.
|
||||
*/
|
||||
private void handleContinue() {
|
||||
// Go back to main menu to start a new exam
|
||||
controller.getUserAccount(examSession.getUsername())
|
||||
.ifPresentOrElse(
|
||||
userAccount -> {
|
||||
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount);
|
||||
primaryStage.getScene().setRoot(mainMenuView);
|
||||
},
|
||||
() -> {
|
||||
// If user account can't be found, show an error and go back to login
|
||||
showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录");
|
||||
// Go back to login scene
|
||||
com.personalproject.ui.scenes.LoginScene loginScene =
|
||||
new com.personalproject.ui.scenes.LoginScene(primaryStage, controller);
|
||||
primaryStage.getScene().setRoot(loginScene);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the exit button action.
|
||||
*/
|
||||
private void handleExit() {
|
||||
// Go back to main menu
|
||||
controller.getUserAccount(examSession.getUsername())
|
||||
.ifPresentOrElse(
|
||||
userAccount -> {
|
||||
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount);
|
||||
primaryStage.getScene().setRoot(mainMenuView);
|
||||
},
|
||||
() -> {
|
||||
// If user account can't be found, show an error and go back to login
|
||||
showAlert(Alert.AlertType.ERROR, "错误", "用户信息无法找到,请重新登录");
|
||||
// Go back to login scene
|
||||
com.personalproject.ui.scenes.LoginScene loginScene =
|
||||
new com.personalproject.ui.scenes.LoginScene(primaryStage, controller);
|
||||
primaryStage.getScene().setRoot(loginScene);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an alert dialog.
|
||||
*
|
||||
* @param alertType Type of alert
|
||||
* @param title Title of the alert
|
||||
* @param message Message to display
|
||||
*/
|
||||
private void showAlert(Alert.AlertType alertType, String title, String message) {
|
||||
Alert alert = new Alert(alertType);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
package com.personalproject.ui.views;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.stage.Stage;
|
||||
import com.personalproject.controller.MathLearningController;
|
||||
import com.personalproject.model.DifficultyLevel;
|
||||
import com.personalproject.auth.UserAccount;
|
||||
|
||||
/**
|
||||
* View for selecting exam difficulty and number of questions.
|
||||
*/
|
||||
public class ExamSelectionView extends BorderPane {
|
||||
|
||||
private final Stage primaryStage;
|
||||
private final MathLearningController controller;
|
||||
private final UserAccount userAccount;
|
||||
private ComboBox<DifficultyLevel> difficultyComboBox;
|
||||
private Spinner<Integer> questionCountSpinner;
|
||||
private Button startExamButton;
|
||||
private Button backButton;
|
||||
|
||||
/**
|
||||
* Constructor for ExamSelectionView.
|
||||
*
|
||||
* @param primaryStage The main stage of the application
|
||||
* @param controller The math learning controller
|
||||
* @param userAccount The current user account
|
||||
*/
|
||||
public ExamSelectionView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) {
|
||||
this.primaryStage = primaryStage;
|
||||
this.controller = controller;
|
||||
this.userAccount = userAccount;
|
||||
initializeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI components.
|
||||
*/
|
||||
private void initializeUI() {
|
||||
// Create the main layout
|
||||
VBox mainLayout = new VBox(20);
|
||||
mainLayout.setAlignment(Pos.CENTER);
|
||||
mainLayout.setPadding(new Insets(20));
|
||||
|
||||
// Title
|
||||
Label titleLabel = new Label("考试设置");
|
||||
titleLabel.setFont(Font.font("System", FontWeight.BOLD, 24));
|
||||
|
||||
// Form for exam settings
|
||||
GridPane examSettingsForm = new GridPane();
|
||||
examSettingsForm.setHgap(15);
|
||||
examSettingsForm.setVgap(15);
|
||||
examSettingsForm.setAlignment(Pos.CENTER);
|
||||
|
||||
Label difficultyLabel = new Label("选择难度:");
|
||||
difficultyComboBox = new ComboBox<>();
|
||||
difficultyComboBox.getItems().addAll(DifficultyLevel.PRIMARY, DifficultyLevel.MIDDLE, DifficultyLevel.HIGH);
|
||||
difficultyComboBox.setValue(userAccount.difficultyLevel()); // Default to user's difficulty
|
||||
difficultyComboBox.setPrefWidth(200);
|
||||
|
||||
Label questionCountLabel = new Label("题目数量 (10-30):");
|
||||
questionCountSpinner = new Spinner<>(10, 30, 10); // min, max, initial value
|
||||
questionCountSpinner.setPrefWidth(200);
|
||||
|
||||
examSettingsForm.add(difficultyLabel, 0, 0);
|
||||
examSettingsForm.add(difficultyComboBox, 1, 0);
|
||||
examSettingsForm.add(questionCountLabel, 0, 1);
|
||||
examSettingsForm.add(questionCountSpinner, 1, 1);
|
||||
|
||||
// Buttons
|
||||
HBox buttonBox = new HBox(15);
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
|
||||
startExamButton = new Button("开始考试");
|
||||
backButton = new Button("返回");
|
||||
|
||||
// Set button sizes
|
||||
startExamButton.setPrefSize(120, 40);
|
||||
backButton.setPrefSize(120, 40);
|
||||
|
||||
buttonBox.getChildren().addAll(startExamButton, backButton);
|
||||
|
||||
// Add components to main layout
|
||||
mainLayout.getChildren().addAll(titleLabel, examSettingsForm, buttonBox);
|
||||
|
||||
// Set the center of the border pane
|
||||
setCenter(mainLayout);
|
||||
|
||||
// Add event handlers
|
||||
addEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event handlers to UI components.
|
||||
*/
|
||||
private void addEventHandlers() {
|
||||
startExamButton.setOnAction(e -> handleStartExam());
|
||||
backButton.setOnAction(e -> handleBack());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the start exam button action.
|
||||
*/
|
||||
private void handleStartExam() {
|
||||
DifficultyLevel selectedDifficulty = difficultyComboBox.getValue();
|
||||
int questionCount = questionCountSpinner.getValue();
|
||||
|
||||
if (questionCount < 10 || questionCount > 30) {
|
||||
showAlert(Alert.AlertType.WARNING, "无效输入", "题目数量必须在10到30之间");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and start exam session
|
||||
com.personalproject.model.ExamSession examSession = controller.createExamSession(
|
||||
userAccount.username(), selectedDifficulty, questionCount);
|
||||
|
||||
ExamView examView = new ExamView(primaryStage, controller, examSession);
|
||||
primaryStage.getScene().setRoot(examView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the back button action.
|
||||
*/
|
||||
private void handleBack() {
|
||||
MainMenuView mainMenuView = new MainMenuView(primaryStage, controller, userAccount);
|
||||
primaryStage.getScene().setRoot(mainMenuView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an alert dialog.
|
||||
*
|
||||
* @param alertType Type of alert
|
||||
* @param title Title of the alert
|
||||
* @param message Message to display
|
||||
*/
|
||||
private void showAlert(Alert.AlertType alertType, String title, String message) {
|
||||
Alert alert = new Alert(alertType);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,261 @@
|
||||
package com.personalproject.ui.views;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.stage.Stage;
|
||||
import com.personalproject.controller.MathLearningController;
|
||||
import com.personalproject.model.ExamSession;
|
||||
import com.personalproject.model.QuizQuestion;
|
||||
import com.personalproject.ui.views.ExamResultsView;
|
||||
|
||||
/**
|
||||
* View for taking the exam with questions and answer options.
|
||||
*/
|
||||
public class ExamView extends BorderPane {
|
||||
|
||||
private final Stage primaryStage;
|
||||
private final MathLearningController controller;
|
||||
private final ExamSession examSession;
|
||||
private Label questionNumberLabel;
|
||||
private Label questionTextLabel;
|
||||
private ToggleGroup answerToggleGroup;
|
||||
private VBox optionsBox;
|
||||
private Button nextButton;
|
||||
private Button previousButton;
|
||||
private Button finishButton;
|
||||
private HBox buttonBox;
|
||||
|
||||
/**
|
||||
* Constructor for ExamView.
|
||||
*
|
||||
* @param primaryStage The main stage of the application
|
||||
* @param controller The math learning controller
|
||||
* @param examSession The current exam session
|
||||
*/
|
||||
public ExamView(Stage primaryStage, MathLearningController controller, ExamSession examSession) {
|
||||
this.primaryStage = primaryStage;
|
||||
this.controller = controller;
|
||||
this.examSession = examSession;
|
||||
initializeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI components.
|
||||
*/
|
||||
private void initializeUI() {
|
||||
// Create the main layout
|
||||
VBox mainLayout = new VBox(20);
|
||||
mainLayout.setAlignment(Pos.CENTER);
|
||||
mainLayout.setPadding(new Insets(20));
|
||||
|
||||
// Question number
|
||||
questionNumberLabel = new Label();
|
||||
questionNumberLabel.setFont(Font.font("System", FontWeight.BOLD, 16));
|
||||
|
||||
// Question text
|
||||
questionTextLabel = new Label();
|
||||
questionTextLabel.setWrapText(true);
|
||||
questionTextLabel.setFont(Font.font("System", FontWeight.NORMAL, 14));
|
||||
questionTextLabel.setMaxWidth(500);
|
||||
|
||||
// Options
|
||||
optionsBox = new VBox(10);
|
||||
optionsBox.setPadding(new Insets(10));
|
||||
|
||||
// Buttons
|
||||
buttonBox = new HBox(15);
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
|
||||
previousButton = new Button("上一题");
|
||||
nextButton = new Button("下一题");
|
||||
finishButton = new Button("完成考试");
|
||||
|
||||
// Set button sizes
|
||||
previousButton.setPrefSize(100, 35);
|
||||
nextButton.setPrefSize(100, 35);
|
||||
finishButton.setPrefSize(120, 35);
|
||||
|
||||
buttonBox.getChildren().addAll(previousButton, nextButton, finishButton);
|
||||
|
||||
// Add components to main layout
|
||||
mainLayout.getChildren().addAll(questionNumberLabel, questionTextLabel, optionsBox, buttonBox);
|
||||
|
||||
// Set the center of the border pane
|
||||
setCenter(mainLayout);
|
||||
|
||||
// Load the first question
|
||||
loadCurrentQuestion();
|
||||
|
||||
// Add event handlers
|
||||
addEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the current question into the UI.
|
||||
*/
|
||||
private void loadCurrentQuestion() {
|
||||
try {
|
||||
// Check if exam is complete before loading next question
|
||||
if (examSession.isComplete()) {
|
||||
// If exam is complete, the finish button should be enabled
|
||||
updateButtonStates();
|
||||
return;
|
||||
}
|
||||
|
||||
QuizQuestion currentQuestion = examSession.getCurrentQuestion();
|
||||
int currentIndex = examSession.getCurrentQuestionIndex();
|
||||
|
||||
if (currentQuestion == null) {
|
||||
showAlert(Alert.AlertType.ERROR, "错误", "当前题目为空,请重新开始考试");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update question number and text
|
||||
questionNumberLabel.setText("第 " + (currentIndex + 1) + " 题");
|
||||
questionTextLabel.setText(currentQuestion.getQuestionText());
|
||||
|
||||
// Clear previous options
|
||||
optionsBox.getChildren().clear();
|
||||
|
||||
// Create new options
|
||||
answerToggleGroup = new ToggleGroup();
|
||||
|
||||
for (int i = 0; i < currentQuestion.getOptions().size(); i++) {
|
||||
String option = currentQuestion.getOptions().get(i);
|
||||
RadioButton optionButton = new RadioButton((i + 1) + ". " + option);
|
||||
optionButton.setToggleGroup(answerToggleGroup);
|
||||
optionButton.setUserData(i); // Store option index
|
||||
|
||||
// If this question already has an answer, select it
|
||||
if (examSession.hasAnswered(currentIndex) &&
|
||||
examSession.getUserAnswer(currentIndex) == i) {
|
||||
optionButton.setSelected(true);
|
||||
}
|
||||
|
||||
optionsBox.getChildren().add(optionButton);
|
||||
}
|
||||
|
||||
// Update button states
|
||||
updateButtonStates();
|
||||
} catch (Exception e) {
|
||||
showAlert(Alert.AlertType.ERROR, "错误", "加载题目时发生错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of navigation buttons based on current position.
|
||||
*/
|
||||
private void updateButtonStates() {
|
||||
try {
|
||||
int currentIndex = examSession.getCurrentQuestionIndex();
|
||||
int totalQuestions = examSession.getTotalQuestions();
|
||||
|
||||
// Handle potential edge cases
|
||||
if (totalQuestions <= 0) {
|
||||
// If there are no questions, disable all navigation
|
||||
previousButton.setDisable(true);
|
||||
nextButton.setDisable(true);
|
||||
finishButton.setDisable(false); // Allow finishing exam
|
||||
return;
|
||||
}
|
||||
|
||||
// Previous button state
|
||||
previousButton.setDisable(currentIndex < 0 || currentIndex == 0);
|
||||
|
||||
// Next button state
|
||||
nextButton.setDisable(currentIndex < 0 || currentIndex >= totalQuestions - 1);
|
||||
|
||||
// Finish button state - enabled when exam is complete or at the last question
|
||||
boolean isExamComplete = examSession.isComplete();
|
||||
boolean isAtLastQuestion = (currentIndex >= totalQuestions - 1);
|
||||
finishButton.setDisable(!(isExamComplete || isAtLastQuestion));
|
||||
} catch (Exception e) {
|
||||
// In case of any error, disable navigation buttons to prevent further issues
|
||||
previousButton.setDisable(true);
|
||||
nextButton.setDisable(true);
|
||||
finishButton.setDisable(false); // Still allow finishing
|
||||
showAlert(Alert.AlertType.ERROR, "错误", "更新按钮状态时发生错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event handlers to UI components.
|
||||
*/
|
||||
private void addEventHandlers() {
|
||||
nextButton.setOnAction(e -> handleNextQuestion());
|
||||
previousButton.setOnAction(e -> handlePreviousQuestion());
|
||||
finishButton.setOnAction(e -> handleFinishExam());
|
||||
|
||||
// Add change listener to save answer when an option is selected
|
||||
answerToggleGroup.selectedToggleProperty().addListener((obs, oldSelection, newSelection) -> {
|
||||
if (newSelection != null) {
|
||||
int selectedIndex = (Integer) newSelection.getUserData();
|
||||
examSession.setAnswer(selectedIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the next question button action.
|
||||
*/
|
||||
private void handleNextQuestion() {
|
||||
try {
|
||||
if (examSession.goToNextQuestion()) {
|
||||
loadCurrentQuestion();
|
||||
} else {
|
||||
// If we can't go to next question, we might be at the end
|
||||
// Check if exam is complete and update button states accordingly
|
||||
updateButtonStates();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showAlert(Alert.AlertType.ERROR, "错误", "导航到下一题时发生错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the previous question button action.
|
||||
*/
|
||||
private void handlePreviousQuestion() {
|
||||
try {
|
||||
if (examSession.goToPreviousQuestion()) {
|
||||
loadCurrentQuestion();
|
||||
} else {
|
||||
// If we can't go to previous question, we might be at the beginning
|
||||
updateButtonStates();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showAlert(Alert.AlertType.ERROR, "错误", "导航到上一题时发生错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an alert dialog.
|
||||
*
|
||||
* @param alertType Type of alert
|
||||
* @param title Title of the alert
|
||||
* @param message Message to display
|
||||
*/
|
||||
private void showAlert(Alert.AlertType alertType, String title, String message) {
|
||||
Alert alert = new Alert(alertType);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the finish exam button action.
|
||||
*/
|
||||
private void handleFinishExam() {
|
||||
// Save exam results
|
||||
controller.saveExamResults(examSession);
|
||||
|
||||
// Show results
|
||||
ExamResultsView resultsView = new ExamResultsView(primaryStage, controller, examSession);
|
||||
primaryStage.getScene().setRoot(resultsView);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package com.personalproject.ui.views;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.stage.Stage;
|
||||
import com.personalproject.controller.MathLearningController;
|
||||
import com.personalproject.model.DifficultyLevel;
|
||||
import com.personalproject.model.ExamSession;
|
||||
import com.personalproject.auth.UserAccount;
|
||||
import com.personalproject.ui.scenes.LoginScene;
|
||||
|
||||
/**
|
||||
* View for the main menu where users can start exams or change settings.
|
||||
*/
|
||||
public class MainMenuView extends BorderPane {
|
||||
|
||||
private final Stage primaryStage;
|
||||
private final MathLearningController controller;
|
||||
private final UserAccount userAccount;
|
||||
private Button startExamButton;
|
||||
private Button changePasswordButton;
|
||||
private Button logoutButton;
|
||||
|
||||
/**
|
||||
* Constructor for MainMenuView.
|
||||
*
|
||||
* @param primaryStage The main stage of the application
|
||||
* @param controller The math learning controller
|
||||
* @param userAccount The current user account
|
||||
*/
|
||||
public MainMenuView(Stage primaryStage, MathLearningController controller, UserAccount userAccount) {
|
||||
this.primaryStage = primaryStage;
|
||||
this.controller = controller;
|
||||
this.userAccount = userAccount;
|
||||
initializeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI components.
|
||||
*/
|
||||
private void initializeUI() {
|
||||
// Create the main layout
|
||||
VBox mainLayout = new VBox(20);
|
||||
mainLayout.setAlignment(Pos.CENTER);
|
||||
mainLayout.setPadding(new Insets(20));
|
||||
|
||||
// Welcome message
|
||||
Label welcomeLabel = new Label("欢迎, " + userAccount.username());
|
||||
welcomeLabel.setFont(Font.font("System", FontWeight.BOLD, 18));
|
||||
|
||||
// Difficulty info
|
||||
Label difficultyLabel = new Label("当前难度: " + userAccount.difficultyLevel().getDisplayName());
|
||||
difficultyLabel.setFont(Font.font("System", FontWeight.NORMAL, 14));
|
||||
|
||||
// Buttons
|
||||
VBox buttonBox = new VBox(15);
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
|
||||
startExamButton = new Button("开始考试");
|
||||
changePasswordButton = new Button("修改密码");
|
||||
logoutButton = new Button("退出登录");
|
||||
|
||||
// Set button sizes
|
||||
startExamButton.setPrefSize(150, 40);
|
||||
changePasswordButton.setPrefSize(150, 40);
|
||||
logoutButton.setPrefSize(150, 40);
|
||||
|
||||
buttonBox.getChildren().addAll(startExamButton, changePasswordButton, logoutButton);
|
||||
|
||||
// Add components to main layout
|
||||
mainLayout.getChildren().addAll(welcomeLabel, difficultyLabel, buttonBox);
|
||||
|
||||
// Set the center of the border pane
|
||||
setCenter(mainLayout);
|
||||
|
||||
// Add event handlers
|
||||
addEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event handlers to UI components.
|
||||
*/
|
||||
private void addEventHandlers() {
|
||||
startExamButton.setOnAction(e -> handleStartExam());
|
||||
changePasswordButton.setOnAction(e -> handleChangePassword());
|
||||
logoutButton.setOnAction(e -> handleLogout());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the start exam button action.
|
||||
*/
|
||||
private void handleStartExam() {
|
||||
ExamSelectionView examSelectionView = new ExamSelectionView(primaryStage, controller, userAccount);
|
||||
primaryStage.getScene().setRoot(examSelectionView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the change password button action.
|
||||
*/
|
||||
private void handleChangePassword() {
|
||||
PasswordChangeView passwordChangeView = new PasswordChangeView(primaryStage, controller, userAccount);
|
||||
primaryStage.getScene().setRoot(passwordChangeView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the logout button action.
|
||||
*/
|
||||
private void handleLogout() {
|
||||
// Go back to login screen
|
||||
LoginScene loginScene = new LoginScene(primaryStage, controller);
|
||||
primaryStage.getScene().setRoot(loginScene);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue