@ -0,0 +1,211 @@
|
||||
package com.example.mathsystemtogether;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 选择题生成器
|
||||
*/
|
||||
public class ChoiceQuestionGenerator {
|
||||
private final Random random = new Random();
|
||||
private final int MAX_OPERAND_VALUE = 50; // 降低数值范围,便于计算
|
||||
private final int MIN_OPERAND_VALUE = 1;
|
||||
|
||||
/**
|
||||
* 生成指定数量的选择题
|
||||
*/
|
||||
public List<Question> generateQuestions(Level level, int count) {
|
||||
List<Question> questions = new ArrayList<>();
|
||||
Set<String> usedQuestions = new HashSet<>();
|
||||
|
||||
int attempts = 0;
|
||||
int maxAttempts = count * 100; // 最大尝试次数
|
||||
|
||||
while (questions.size() < count && attempts < maxAttempts) {
|
||||
Question question = generateSingleQuestion(level, questions.size() + 1);
|
||||
String questionKey = question.getQuestionText();
|
||||
|
||||
if (!usedQuestions.contains(questionKey)) {
|
||||
usedQuestions.add(questionKey);
|
||||
questions.add(question);
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
|
||||
return questions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个选择题
|
||||
*/
|
||||
private Question generateSingleQuestion(Level level, int questionNumber) {
|
||||
switch (level) {
|
||||
case 小学:
|
||||
return generatePrimaryQuestion(questionNumber);
|
||||
case 初中:
|
||||
return generateJuniorQuestion(questionNumber);
|
||||
case 高中:
|
||||
return generateSeniorQuestion(questionNumber);
|
||||
default:
|
||||
return generatePrimaryQuestion(questionNumber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成小学题目
|
||||
*/
|
||||
private Question generatePrimaryQuestion(int questionNumber) {
|
||||
int a = MIN_OPERAND_VALUE + random.nextInt(MAX_OPERAND_VALUE);
|
||||
int b = MIN_OPERAND_VALUE + random.nextInt(MAX_OPERAND_VALUE);
|
||||
|
||||
String operation = getRandomOperation("+*-");
|
||||
String questionText = String.format("%d %s %d = ?", a, operation, b);
|
||||
int correctAnswer = calculate(a, b, operation);
|
||||
|
||||
// 生成错误选项
|
||||
List<Integer> options = generateWrongOptions(correctAnswer);
|
||||
options.add(correctAnswer);
|
||||
Collections.shuffle(options);
|
||||
|
||||
return new Question(questionText,
|
||||
String.valueOf(options.get(0)),
|
||||
String.valueOf(options.get(1)),
|
||||
String.valueOf(options.get(2)),
|
||||
String.valueOf(options.get(3)),
|
||||
String.valueOf(correctAnswer),
|
||||
questionNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成初中题目
|
||||
*/
|
||||
private Question generateJuniorQuestion(int questionNumber) {
|
||||
int a = MIN_OPERAND_VALUE + random.nextInt(MAX_OPERAND_VALUE);
|
||||
|
||||
// 50%概率生成平方或开方题目
|
||||
if (random.nextBoolean()) {
|
||||
// 平方题目
|
||||
String questionText = String.format("%d² = ?", a);
|
||||
int correctAnswer = a * a;
|
||||
List<Integer> options = generateWrongOptions(correctAnswer);
|
||||
options.add(correctAnswer);
|
||||
Collections.shuffle(options);
|
||||
|
||||
return new Question(questionText,
|
||||
String.valueOf(options.get(0)),
|
||||
String.valueOf(options.get(1)),
|
||||
String.valueOf(options.get(2)),
|
||||
String.valueOf(options.get(3)),
|
||||
String.valueOf(correctAnswer),
|
||||
questionNumber);
|
||||
} else {
|
||||
// 开方题目
|
||||
int perfectSquare = (int) Math.pow(random.nextInt(10) + 1, 2);
|
||||
String questionText = String.format("√%d = ?", perfectSquare);
|
||||
int correctAnswer = (int) Math.sqrt(perfectSquare);
|
||||
List<Integer> options = generateWrongOptions(correctAnswer);
|
||||
options.add(correctAnswer);
|
||||
Collections.shuffle(options);
|
||||
|
||||
return new Question(questionText,
|
||||
String.valueOf(options.get(0)),
|
||||
String.valueOf(options.get(1)),
|
||||
String.valueOf(options.get(2)),
|
||||
String.valueOf(options.get(3)),
|
||||
String.valueOf(correctAnswer),
|
||||
questionNumber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成高中题目
|
||||
*/
|
||||
private Question generateSeniorQuestion(int questionNumber) {
|
||||
int angle = random.nextInt(360);
|
||||
String[] functions = {"sin", "cos", "tan"};
|
||||
String function = functions[random.nextInt(functions.length)];
|
||||
|
||||
String questionText = String.format("%s(%d°) = ?", function, angle);
|
||||
double result = calculateTrigFunction(function, angle);
|
||||
int correctAnswer = (int) Math.round(result * 100) / 100; // 保留两位小数
|
||||
|
||||
List<Integer> options = generateWrongOptions(correctAnswer);
|
||||
options.add(correctAnswer);
|
||||
Collections.shuffle(options);
|
||||
|
||||
return new Question(questionText,
|
||||
String.valueOf(options.get(0)),
|
||||
String.valueOf(options.get(1)),
|
||||
String.valueOf(options.get(2)),
|
||||
String.valueOf(options.get(3)),
|
||||
String.valueOf(correctAnswer),
|
||||
questionNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成错误选项
|
||||
*/
|
||||
private List<Integer> generateWrongOptions(int correctAnswer) {
|
||||
List<Integer> options = new ArrayList<>();
|
||||
Set<Integer> usedOptions = new HashSet<>();
|
||||
usedOptions.add(correctAnswer);
|
||||
|
||||
while (options.size() < 3) {
|
||||
int wrongAnswer;
|
||||
if (correctAnswer == 0) {
|
||||
wrongAnswer = random.nextInt(20) - 10;
|
||||
} else {
|
||||
// 生成接近正确答案的错误选项
|
||||
int variation = random.nextInt(Math.max(1, Math.abs(correctAnswer) / 2)) + 1;
|
||||
wrongAnswer = correctAnswer + (random.nextBoolean() ? variation : -variation);
|
||||
}
|
||||
|
||||
if (!usedOptions.contains(wrongAnswer)) {
|
||||
options.add(wrongAnswer);
|
||||
usedOptions.add(wrongAnswer);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算基本运算
|
||||
*/
|
||||
private int calculate(int a, int b, String operation) {
|
||||
switch (operation) {
|
||||
case "+": return a + b;
|
||||
case "-": return a - b;
|
||||
case "*": return a * b;
|
||||
case "/": return b != 0 ? a / b : 0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算三角函数
|
||||
*/
|
||||
private double calculateTrigFunction(String function, int angle) {
|
||||
double radians = Math.toRadians(angle);
|
||||
switch (function) {
|
||||
case "sin": return Math.sin(radians);
|
||||
case "cos": return Math.cos(radians);
|
||||
case "tan": return Math.tan(radians);
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机运算符
|
||||
*/
|
||||
private String getRandomOperation(String operations) {
|
||||
int index = random.nextInt(operations.length());
|
||||
return String.valueOf(operations.charAt(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* 难度级别枚举
|
||||
*/
|
||||
public enum Level {
|
||||
小学, 初中, 高中
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,335 @@
|
||||
package com.example.mathsystemtogether;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 考试系统控制器
|
||||
*/
|
||||
public class ExamController {
|
||||
|
||||
// 登录界面控件
|
||||
@FXML private TextField usernameField;
|
||||
@FXML private PasswordField passwordField;
|
||||
@FXML private Button loginButton;
|
||||
@FXML private Label loginStatusLabel;
|
||||
|
||||
// 考试设置界面控件
|
||||
@FXML private VBox examSetupPanel;
|
||||
@FXML private Label welcomeLabel;
|
||||
@FXML private ComboBox<String> levelComboBox;
|
||||
@FXML private TextField questionCountField;
|
||||
@FXML private Button startExamButton;
|
||||
@FXML private Button logoutButton;
|
||||
@FXML private Label statusLabel;
|
||||
|
||||
// 考试界面控件
|
||||
@FXML private VBox examPanel;
|
||||
@FXML private Label questionNumberLabel;
|
||||
@FXML private Label questionTextLabel;
|
||||
@FXML private RadioButton optionA;
|
||||
@FXML private RadioButton optionB;
|
||||
@FXML private RadioButton optionC;
|
||||
@FXML private RadioButton optionD;
|
||||
@FXML private ToggleGroup answerGroup;
|
||||
@FXML private Button submitButton;
|
||||
@FXML private Button nextButton;
|
||||
@FXML private Label progressLabel;
|
||||
@FXML private Button exitExamButton;
|
||||
|
||||
// 结果界面控件
|
||||
@FXML private VBox resultPanel;
|
||||
@FXML private Label resultTitleLabel;
|
||||
@FXML private Label scoreLabel;
|
||||
@FXML private Label correctCountLabel;
|
||||
@FXML private Label totalCountLabel;
|
||||
@FXML private TextArea resultDetailsArea;
|
||||
@FXML private Button restartButton;
|
||||
@FXML private Button backToLoginButton;
|
||||
|
||||
// 数据成员
|
||||
private Account currentAccount;
|
||||
private List<Question> examQuestions;
|
||||
private int currentQuestionIndex = 0;
|
||||
private Map<Integer, String> userAnswers = new HashMap<>();
|
||||
private ChoiceQuestionGenerator questionGenerator;
|
||||
private final Map<String, Account> userMap = new HashMap<>();
|
||||
|
||||
// 常量定义
|
||||
private static final int MIN_QUESTIONS = 5;
|
||||
private static final int MAX_QUESTIONS = 20;
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
initAccounts();
|
||||
setupLevelComboBox();
|
||||
setupAnswerGroup();
|
||||
examSetupPanel.setVisible(false);
|
||||
examPanel.setVisible(false);
|
||||
resultPanel.setVisible(false);
|
||||
questionCountField.setText("10");
|
||||
}
|
||||
|
||||
private void initAccounts() {
|
||||
// 小学三个账号
|
||||
userMap.put("张三1", new Account("张三1", "123", Level.小学));
|
||||
userMap.put("张三2", new Account("张三2", "123", Level.小学));
|
||||
userMap.put("张三3", new Account("张三3", "123", Level.小学));
|
||||
// 初中三个账号
|
||||
userMap.put("李四1", new Account("李四1", "123", Level.初中));
|
||||
userMap.put("李四2", new Account("李四2", "123", Level.初中));
|
||||
userMap.put("李四3", new Account("李四3", "123", Level.初中));
|
||||
// 高中三个账号
|
||||
userMap.put("王五1", new Account("王五1", "123", Level.高中));
|
||||
userMap.put("王五2", new Account("王五2", "123", Level.高中));
|
||||
userMap.put("王五3", new Account("王五3", "123", Level.高中));
|
||||
}
|
||||
|
||||
private void setupLevelComboBox() {
|
||||
ObservableList<String> levels = FXCollections.observableArrayList("小学", "初中", "高中");
|
||||
levelComboBox.setItems(levels);
|
||||
levelComboBox.setValue("小学");
|
||||
}
|
||||
|
||||
private void setupAnswerGroup() {
|
||||
answerGroup = new ToggleGroup();
|
||||
optionA.setToggleGroup(answerGroup);
|
||||
optionB.setToggleGroup(answerGroup);
|
||||
optionC.setToggleGroup(answerGroup);
|
||||
optionD.setToggleGroup(answerGroup);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleLogin() {
|
||||
String username = usernameField.getText().trim();
|
||||
String password = passwordField.getText();
|
||||
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
loginStatusLabel.setText("请输入用户名和密码");
|
||||
loginStatusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
Account account = userMap.get(username);
|
||||
if (account != null && account.password.equals(password)) {
|
||||
currentAccount = account;
|
||||
welcomeLabel.setText("欢迎," + username + "!当前级别:" + account.level);
|
||||
levelComboBox.setValue(account.level.toString());
|
||||
examSetupPanel.setVisible(true);
|
||||
loginStatusLabel.setText("登录成功!");
|
||||
loginStatusLabel.setStyle("-fx-text-fill: green;");
|
||||
} else {
|
||||
loginStatusLabel.setText("用户名或密码错误");
|
||||
loginStatusLabel.setStyle("-fx-text-fill: red;");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleLogout() {
|
||||
currentAccount = null;
|
||||
examQuestions = null;
|
||||
currentQuestionIndex = 0;
|
||||
userAnswers.clear();
|
||||
examSetupPanel.setVisible(false);
|
||||
examPanel.setVisible(false);
|
||||
resultPanel.setVisible(false);
|
||||
usernameField.clear();
|
||||
passwordField.clear();
|
||||
loginStatusLabel.setText("");
|
||||
statusLabel.setText("");
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleStartExam() {
|
||||
if (currentAccount == null) {
|
||||
statusLabel.setText("请先登录");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int count = Integer.parseInt(questionCountField.getText());
|
||||
if (count < MIN_QUESTIONS || count > MAX_QUESTIONS) {
|
||||
statusLabel.setText("题目数量应在" + MIN_QUESTIONS + "-" + MAX_QUESTIONS + "之间");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取选择的级别
|
||||
String selectedLevel = levelComboBox.getValue();
|
||||
ChoiceQuestionGenerator.Level level = ChoiceQuestionGenerator.Level.valueOf(selectedLevel);
|
||||
|
||||
// 生成考试题目
|
||||
questionGenerator = new ChoiceQuestionGenerator();
|
||||
examQuestions = questionGenerator.generateQuestions(level, count);
|
||||
|
||||
if (examQuestions.isEmpty()) {
|
||||
statusLabel.setText("无法生成题目,请重试");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始考试
|
||||
startExam();
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
statusLabel.setText("请输入有效的数字");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
}
|
||||
}
|
||||
|
||||
private void startExam() {
|
||||
currentQuestionIndex = 0;
|
||||
userAnswers.clear();
|
||||
examSetupPanel.setVisible(false);
|
||||
examPanel.setVisible(true);
|
||||
resultPanel.setVisible(false);
|
||||
displayCurrentQuestion();
|
||||
}
|
||||
|
||||
private void displayCurrentQuestion() {
|
||||
if (currentQuestionIndex >= examQuestions.size()) {
|
||||
showResults();
|
||||
return;
|
||||
}
|
||||
|
||||
Question question = examQuestions.get(currentQuestionIndex);
|
||||
questionNumberLabel.setText("第 " + (currentQuestionIndex + 1) + " 题 / 共 " + examQuestions.size() + " 题");
|
||||
questionTextLabel.setText(question.getQuestionText());
|
||||
|
||||
optionA.setText("A. " + question.getOptionA());
|
||||
optionB.setText("B. " + question.getOptionB());
|
||||
optionC.setText("C. " + question.getOptionC());
|
||||
optionD.setText("D. " + question.getOptionD());
|
||||
|
||||
// 清除之前的选择
|
||||
answerGroup.selectToggle(null);
|
||||
|
||||
// 更新按钮状态
|
||||
if (currentQuestionIndex == examQuestions.size() - 1) {
|
||||
submitButton.setText("提交并完成考试");
|
||||
nextButton.setVisible(false);
|
||||
} else {
|
||||
submitButton.setText("提交答案");
|
||||
nextButton.setVisible(true);
|
||||
}
|
||||
|
||||
progressLabel.setText("进度: " + (currentQuestionIndex + 1) + "/" + examQuestions.size());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleSubmitAnswer() {
|
||||
RadioButton selectedOption = (RadioButton) answerGroup.getSelectedToggle();
|
||||
if (selectedOption == null) {
|
||||
statusLabel.setText("请选择一个答案");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
String answer = selectedOption.getText().substring(0, 1); // 获取A、B、C、D
|
||||
userAnswers.put(currentQuestionIndex, answer);
|
||||
|
||||
if (currentQuestionIndex == examQuestions.size() - 1) {
|
||||
// 最后一题,显示结果
|
||||
showResults();
|
||||
} else {
|
||||
// 下一题
|
||||
currentQuestionIndex++;
|
||||
displayCurrentQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleNextQuestion() {
|
||||
currentQuestionIndex++;
|
||||
displayCurrentQuestion();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleExitExam() {
|
||||
if (showExitConfirmation()) {
|
||||
examPanel.setVisible(false);
|
||||
examSetupPanel.setVisible(true);
|
||||
currentQuestionIndex = 0;
|
||||
userAnswers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean showExitConfirmation() {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("确认退出");
|
||||
alert.setHeaderText("确定要退出考试吗?");
|
||||
alert.setContentText("退出后当前进度将丢失。");
|
||||
return alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK;
|
||||
}
|
||||
|
||||
private void showResults() {
|
||||
examPanel.setVisible(false);
|
||||
resultPanel.setVisible(true);
|
||||
|
||||
int correctCount = 0;
|
||||
StringBuilder resultDetails = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < examQuestions.size(); i++) {
|
||||
Question question = examQuestions.get(i);
|
||||
String userAnswer = userAnswers.get(i);
|
||||
boolean isCorrect = question.isCorrect(userAnswer);
|
||||
|
||||
if (isCorrect) {
|
||||
correctCount++;
|
||||
}
|
||||
|
||||
resultDetails.append("第").append(i + 1).append("题: ");
|
||||
resultDetails.append(question.getQuestionText()).append("\n");
|
||||
resultDetails.append("你的答案: ").append(userAnswer != null ? userAnswer : "未作答").append(" ");
|
||||
resultDetails.append("正确答案: ").append(question.getCorrectAnswer()).append(" ");
|
||||
resultDetails.append(isCorrect ? "✓" : "✗").append("\n\n");
|
||||
}
|
||||
|
||||
int score = (int) Math.round((double) correctCount / examQuestions.size() * 100);
|
||||
|
||||
resultTitleLabel.setText("考试完成!");
|
||||
scoreLabel.setText("得分: " + score + "分");
|
||||
correctCountLabel.setText("正确: " + correctCount + "题");
|
||||
totalCountLabel.setText("总计: " + examQuestions.size() + "题");
|
||||
resultDetailsArea.setText(resultDetails.toString());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleRestart() {
|
||||
resultPanel.setVisible(false);
|
||||
examSetupPanel.setVisible(true);
|
||||
currentQuestionIndex = 0;
|
||||
userAnswers.clear();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleBackToLogin() {
|
||||
resultPanel.setVisible(false);
|
||||
examSetupPanel.setVisible(false);
|
||||
examPanel.setVisible(false);
|
||||
handleLogout();
|
||||
}
|
||||
|
||||
// 内部类
|
||||
static class Account {
|
||||
final String username;
|
||||
final String password;
|
||||
final Level level;
|
||||
|
||||
Account(String username, String password, Level level) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
enum Level {
|
||||
小学, 初中, 高中
|
||||
}
|
||||
}
|
||||
@ -1,458 +0,0 @@
|
||||
package com.example.mathsystemtogether;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
public class MathSystemController {
|
||||
|
||||
// 登录界面控件
|
||||
@FXML private TextField usernameField;
|
||||
@FXML private PasswordField passwordField;
|
||||
@FXML private Button loginButton;
|
||||
@FXML private Label loginStatusLabel;
|
||||
|
||||
// 主界面控件
|
||||
@FXML private VBox mainPanel;
|
||||
@FXML private Label welcomeLabel;
|
||||
@FXML private ComboBox<String> levelComboBox;
|
||||
@FXML private TextField questionCountField;
|
||||
@FXML private Button generateButton;
|
||||
@FXML private Button logoutButton;
|
||||
@FXML private Button viewHistoryButton;
|
||||
@FXML private TextArea questionDisplayArea;
|
||||
@FXML private Button saveButton;
|
||||
@FXML private Label statusLabel;
|
||||
|
||||
// 数据成员
|
||||
private Account currentAccount;
|
||||
private List<String> currentQuestions;
|
||||
private final Map<String, Account> userMap = new HashMap<>();
|
||||
private final String OUTPUT_ROOT = "papers";
|
||||
|
||||
// 常量定义
|
||||
private static final int MIN_QUESTIONS = 10;
|
||||
private static final int MAX_QUESTIONS = 30;
|
||||
private static final int MAX_ATTEMPTS_MULTIPLIER = 200;
|
||||
private static final int MAX_OPERAND_VALUE = 100;
|
||||
private static final int MIN_OPERAND_VALUE = 1;
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
initAccounts();
|
||||
setupLevelComboBox();
|
||||
mainPanel.setVisible(false);
|
||||
questionCountField.setText("10");
|
||||
}
|
||||
|
||||
private void initAccounts() {
|
||||
// 小学三个账号
|
||||
userMap.put("张三1", new Account("张三1", "123", Level.小学));
|
||||
userMap.put("张三2", new Account("张三2", "123", Level.小学));
|
||||
userMap.put("张三3", new Account("张三3", "123", Level.小学));
|
||||
// 初中三个账号
|
||||
userMap.put("李四1", new Account("李四1", "123", Level.初中));
|
||||
userMap.put("李四2", new Account("李四2", "123", Level.初中));
|
||||
userMap.put("李四3", new Account("李四3", "123", Level.初中));
|
||||
// 高中三个账号
|
||||
userMap.put("王五1", new Account("王五1", "123", Level.高中));
|
||||
userMap.put("王五2", new Account("王五2", "123", Level.高中));
|
||||
userMap.put("王五3", new Account("王五3", "123", Level.高中));
|
||||
}
|
||||
|
||||
private void setupLevelComboBox() {
|
||||
ObservableList<String> levels = FXCollections.observableArrayList("小学", "初中", "高中");
|
||||
levelComboBox.setItems(levels);
|
||||
levelComboBox.setValue("小学");
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleLogin() {
|
||||
String username = usernameField.getText().trim();
|
||||
String password = passwordField.getText();
|
||||
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
loginStatusLabel.setText("请输入用户名和密码");
|
||||
loginStatusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
Account account = userMap.get(username);
|
||||
if (account != null && account.password.equals(password)) {
|
||||
currentAccount = account;
|
||||
welcomeLabel.setText("欢迎," + username + "!当前级别:" + account.level);
|
||||
levelComboBox.setValue(account.level.toString());
|
||||
mainPanel.setVisible(true);
|
||||
loginStatusLabel.setText("登录成功!");
|
||||
loginStatusLabel.setStyle("-fx-text-fill: green;");
|
||||
} else {
|
||||
loginStatusLabel.setText("用户名或密码错误");
|
||||
loginStatusLabel.setStyle("-fx-text-fill: red;");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleLogout() {
|
||||
currentAccount = null;
|
||||
currentQuestions = null;
|
||||
mainPanel.setVisible(false);
|
||||
usernameField.clear();
|
||||
passwordField.clear();
|
||||
questionDisplayArea.clear();
|
||||
loginStatusLabel.setText("");
|
||||
statusLabel.setText("");
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleGenerateQuestions() {
|
||||
if (currentAccount == null) {
|
||||
statusLabel.setText("请先登录");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int count = Integer.parseInt(questionCountField.getText());
|
||||
if (count < MIN_QUESTIONS || count > MAX_QUESTIONS) {
|
||||
statusLabel.setText("题目数量应在" + MIN_QUESTIONS + "-" + MAX_QUESTIONS + "之间");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取选择的级别
|
||||
String selectedLevel = levelComboBox.getValue();
|
||||
Level level = Level.valueOf(selectedLevel);
|
||||
|
||||
// 生成题目
|
||||
Set<String> history = loadHistoryQuestions(currentAccount.username);
|
||||
currentQuestions = generateUniquePaper(level, count, history);
|
||||
|
||||
// 显示题目
|
||||
displayQuestions(currentQuestions);
|
||||
|
||||
statusLabel.setText("成功生成 " + currentQuestions.size() + " 道题目");
|
||||
statusLabel.setStyle("-fx-text-fill: green;");
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
statusLabel.setText("请输入有效的数字");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleSaveQuestions() {
|
||||
if (currentQuestions == null || currentQuestions.isEmpty()) {
|
||||
statusLabel.setText("没有题目可保存");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File savedFile = savePaper(currentAccount.username, currentQuestions);
|
||||
statusLabel.setText("题目已保存到:" + savedFile.getAbsolutePath());
|
||||
statusLabel.setStyle("-fx-text-fill: green;");
|
||||
} catch (IOException e) {
|
||||
statusLabel.setText("保存失败:" + e.getMessage());
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleViewHistory() {
|
||||
if (currentAccount == null) {
|
||||
statusLabel.setText("请先登录");
|
||||
statusLabel.setStyle("-fx-text-fill: red;");
|
||||
return;
|
||||
}
|
||||
|
||||
File folder = new File(OUTPUT_ROOT, currentAccount.username);
|
||||
if (!folder.exists() || !folder.isDirectory()) {
|
||||
statusLabel.setText("没有历史记录");
|
||||
statusLabel.setStyle("-fx-text-fill: orange;");
|
||||
return;
|
||||
}
|
||||
|
||||
File[] files = folder.listFiles((dir, name) -> name.endsWith(".txt"));
|
||||
if (files == null || files.length == 0) {
|
||||
statusLabel.setText("没有历史记录");
|
||||
statusLabel.setStyle("-fx-text-fill: orange;");
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示历史文件列表
|
||||
StringBuilder history = new StringBuilder("历史题目文件:\n\n");
|
||||
for (File file : files) {
|
||||
history.append("📄 ").append(file.getName()).append("\n");
|
||||
history.append(" 创建时间:").append(new Date(file.lastModified())).append("\n");
|
||||
history.append(" 文件大小:").append(file.length()).append(" 字节\n\n");
|
||||
}
|
||||
|
||||
questionDisplayArea.setText(history.toString());
|
||||
statusLabel.setText("显示历史记录");
|
||||
statusLabel.setStyle("-fx-text-fill: blue;");
|
||||
}
|
||||
|
||||
private void displayQuestions(List<String> questions) {
|
||||
StringBuilder display = new StringBuilder();
|
||||
for (int i = 0; i < questions.size(); i++) {
|
||||
display.append((i + 1)).append(". ").append(questions.get(i)).append("\n\n");
|
||||
}
|
||||
questionDisplayArea.setText(display.toString());
|
||||
}
|
||||
|
||||
// 从Main.java移植的核心逻辑
|
||||
private List<String> generateUniquePaper(Level level, int count, Set<String> history) {
|
||||
Set<String> set = new LinkedHashSet<>();
|
||||
int attempts = 0;
|
||||
int maxAttempts = count * MAX_ATTEMPTS_MULTIPLIER;
|
||||
while (set.size() < count && attempts < maxAttempts) {
|
||||
String q = generateQuestion(level);
|
||||
attempts++;
|
||||
if (!history.contains(q) && !set.contains(q)) {
|
||||
set.add(q);
|
||||
}
|
||||
}
|
||||
if (set.size() < count) {
|
||||
statusLabel.setText("注意:因去重限制,仅生成" + set.size() + "题");
|
||||
statusLabel.setStyle("-fx-text-fill: orange;");
|
||||
}
|
||||
return new ArrayList<>(set);
|
||||
}
|
||||
|
||||
private String generateQuestion(Level level) {
|
||||
QuestionGenerator generator = getGenerator(level);
|
||||
return generator.generate();
|
||||
}
|
||||
|
||||
private QuestionGenerator getGenerator(Level level) {
|
||||
switch (level) {
|
||||
case 小学: return new PrimaryQuestionGenerator();
|
||||
case 初中: return new JuniorQuestionGenerator();
|
||||
case 高中: return new SeniorQuestionGenerator();
|
||||
default: return new PrimaryQuestionGenerator();
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> loadHistoryQuestions(String username) {
|
||||
Set<String> set = new HashSet<>();
|
||||
File folder = new File(OUTPUT_ROOT, username);
|
||||
if (!folder.exists() || !folder.isDirectory()) return set;
|
||||
File[] files = folder.listFiles((dir, name) -> name.endsWith(".txt"));
|
||||
if (files == null) return set;
|
||||
for (File f : files) {
|
||||
try (BufferedReader br = new BufferedReader(
|
||||
new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
int dot = line.indexOf('.');
|
||||
if (dot > 0) {
|
||||
String maybe = line.substring(dot + 1).trim();
|
||||
if (!maybe.isEmpty()) set.add(maybe);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private File savePaper(String username, List<String> questions) throws IOException {
|
||||
File folder = new File(OUTPUT_ROOT, username);
|
||||
if (!folder.exists()) {
|
||||
if (!folder.mkdirs()) throw new IOException("无法创建目录:" + folder.getAbsolutePath());
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-d-H-m-s");
|
||||
String filename = now.format(formatter) + ".txt";
|
||||
File out = new File(folder, filename);
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(out, StandardCharsets.UTF_8))) {
|
||||
for (int i = 0; i < questions.size(); i++) {
|
||||
bw.write((i + 1) + ". " + questions.get(i));
|
||||
bw.newLine();
|
||||
bw.newLine();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 内部类和接口
|
||||
enum Level {
|
||||
小学, 初中, 高中
|
||||
}
|
||||
|
||||
static class Account {
|
||||
final String username;
|
||||
final String password;
|
||||
final Level level;
|
||||
|
||||
Account(String username, String password, Level level) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
interface QuestionGenerator {
|
||||
String generate();
|
||||
}
|
||||
|
||||
static class PrimaryQuestionGenerator implements QuestionGenerator {
|
||||
@Override
|
||||
public String generate() {
|
||||
List<String> tokens = generateBaseExpression(2, 5, "+*/");
|
||||
maybeInsertParentheses(tokens);
|
||||
return buildExpression(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
static class JuniorQuestionGenerator implements QuestionGenerator {
|
||||
@Override
|
||||
public String generate() {
|
||||
List<String> tokens = generateBaseExpression(2, 5, "+-*/");
|
||||
ensureJuniorFeature(tokens);
|
||||
maybeInsertParentheses(tokens);
|
||||
return buildExpression(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
static class SeniorQuestionGenerator implements QuestionGenerator {
|
||||
@Override
|
||||
public String generate() {
|
||||
List<String> tokens = generateBaseExpression(3, 5, "+-*/");
|
||||
ensureSeniorFeature(tokens);
|
||||
maybeInsertParentheses(tokens);
|
||||
return buildExpression(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private static String buildExpression(List<String> tokens) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String tk : tokens) sb.append(tk);
|
||||
sb.append(" =");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static List<String> generateBaseExpression(int minOperands, int maxOperands, String operators) {
|
||||
Random RANDOM = new Random();
|
||||
int operands = minOperands + RANDOM.nextInt(maxOperands - minOperands + 1);
|
||||
List<String> tokens = new ArrayList<>();
|
||||
int first = MIN_OPERAND_VALUE + RANDOM.nextInt(MAX_OPERAND_VALUE);
|
||||
tokens.add(String.valueOf(first));
|
||||
for (int i = 1; i < operands; i++) {
|
||||
char op = pickOperator(operators);
|
||||
int n = MIN_OPERAND_VALUE + RANDOM.nextInt(MAX_OPERAND_VALUE);
|
||||
if (op == '/') {
|
||||
int prevNumber = extractLastPureNumber(tokens);
|
||||
if (prevNumber > 0) {
|
||||
n = pickDivisor(prevNumber);
|
||||
}
|
||||
}
|
||||
tokens.add(" " + op + " ");
|
||||
tokens.add(String.valueOf(n));
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static char pickOperator(String candidates) {
|
||||
Random RANDOM = new Random();
|
||||
int idx = RANDOM.nextInt(candidates.length());
|
||||
return candidates.charAt(idx);
|
||||
}
|
||||
|
||||
private static int pickDivisor(int dividend) {
|
||||
Random RANDOM = new Random();
|
||||
dividend = Math.abs(dividend);
|
||||
if (dividend == 0) return 1;
|
||||
List<Integer> divisors = new ArrayList<>();
|
||||
for (int d = 1; d <= Math.min(dividend, MAX_OPERAND_VALUE); d++) {
|
||||
if (dividend % d == 0) {
|
||||
divisors.add(d);
|
||||
}
|
||||
}
|
||||
return divisors.get(RANDOM.nextInt(divisors.size()));
|
||||
}
|
||||
|
||||
private static void maybeInsertParentheses(List<String> tokens) {
|
||||
Random RANDOM = new Random();
|
||||
int parenCount = RANDOM.nextInt(3);
|
||||
for (int k = 0; k < parenCount; k++) {
|
||||
int terms = (tokens.size() + 1) / 2;
|
||||
if (terms < 3) break;
|
||||
int leftIndex = 0;
|
||||
int rightIndex = 0;
|
||||
for (int t = 0; t < 10; t++) {
|
||||
int li = RANDOM.nextInt(terms - 1);
|
||||
int ri = li + 1 + RANDOM.nextInt(terms - li - 1);
|
||||
leftIndex = li * 2;
|
||||
rightIndex = ri * 2;
|
||||
if (rightIndex - leftIndex >= 4) break;
|
||||
}
|
||||
tokens.add(leftIndex, "(");
|
||||
tokens.add(rightIndex + 2, ")");
|
||||
}
|
||||
}
|
||||
|
||||
private static int extractLastPureNumber(List<String> tokens) {
|
||||
for (int i = tokens.size() - 1; i >= 0; i--) {
|
||||
String tk = tokens.get(i).trim();
|
||||
if (tk.matches("\\d+")) {
|
||||
return Integer.parseInt(tk);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static void ensureJuniorFeature(List<String> tokens) {
|
||||
Random RANDOM = new Random();
|
||||
boolean hasSquareOrSqrt = false;
|
||||
for (String tk : tokens) {
|
||||
if (tk.contains("^2") || tk.contains("√")) { hasSquareOrSqrt = true; break; }
|
||||
}
|
||||
if (!hasSquareOrSqrt) {
|
||||
List<Integer> operandIndexes = new ArrayList<>();
|
||||
for (int i = 0; i < tokens.size(); i += 2) operandIndexes.add(i);
|
||||
if (operandIndexes.isEmpty()) return;
|
||||
int idxToken = operandIndexes.get(RANDOM.nextInt(operandIndexes.size()));
|
||||
String val = tokens.get(idxToken).trim();
|
||||
if (RANDOM.nextBoolean()) {
|
||||
tokens.set(idxToken, val + "^2");
|
||||
} else {
|
||||
tokens.set(idxToken, "√(" + val + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureSeniorFeature(List<String> tokens) {
|
||||
Random RANDOM = new Random();
|
||||
boolean hasTrig = false;
|
||||
for (String tk : tokens) {
|
||||
if (tk.contains("sin") || tk.contains("cos") || tk.contains("tan")) { hasTrig = true; break; }
|
||||
}
|
||||
if (!hasTrig) {
|
||||
List<Integer> operandIndexes = new ArrayList<>();
|
||||
for (int i = 0; i < tokens.size(); i += 2) operandIndexes.add(i);
|
||||
if (operandIndexes.isEmpty()) return;
|
||||
int idxToken = operandIndexes.get(RANDOM.nextInt(operandIndexes.size()));
|
||||
String val = tokens.get(idxToken).trim();
|
||||
String func;
|
||||
switch (RANDOM.nextInt(3)) {
|
||||
case 0: func = "sin"; break;
|
||||
case 1: func = "cos"; break;
|
||||
default: func = "tan"; break;
|
||||
}
|
||||
tokens.set(idxToken, func + "(" + val + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.example.mathsystemtogether;
|
||||
|
||||
/**
|
||||
* 选择题数据模型
|
||||
*/
|
||||
public class Question {
|
||||
private String questionText; // 题目内容
|
||||
private String optionA; // 选项A
|
||||
private String optionB; // 选项B
|
||||
private String optionC; // 选项C
|
||||
private String optionD; // 选项D
|
||||
private String correctAnswer; // 正确答案
|
||||
private int questionNumber; // 题目编号
|
||||
|
||||
public Question(String questionText, String optionA, String optionB,
|
||||
String optionC, String optionD, String correctAnswer, int questionNumber) {
|
||||
this.questionText = questionText;
|
||||
this.optionA = optionA;
|
||||
this.optionB = optionB;
|
||||
this.optionC = optionC;
|
||||
this.optionD = optionD;
|
||||
this.correctAnswer = correctAnswer;
|
||||
this.questionNumber = questionNumber;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getQuestionText() { return questionText; }
|
||||
public String getOptionA() { return optionA; }
|
||||
public String getOptionB() { return optionB; }
|
||||
public String getOptionC() { return optionC; }
|
||||
public String getOptionD() { return optionD; }
|
||||
public String getCorrectAnswer() { return correctAnswer; }
|
||||
public int getQuestionNumber() { return questionNumber; }
|
||||
|
||||
// 检查答案是否正确
|
||||
public boolean isCorrect(String userAnswer) {
|
||||
return correctAnswer.equals(userAnswer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("第%d题: %s\nA. %s\nB. %s\nC. %s\nD. %s\n正确答案: %s",
|
||||
questionNumber, questionText, optionA, optionB, optionC, optionD, correctAnswer);
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<?import javafx.scene.control.Button?>
|
||||
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="com.example.mathsystemtogether.HelloController">
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
|
||||
</padding>
|
||||
|
||||
<Label fx:id="welcomeText"/>
|
||||
<Button text="Hello!" onAction="#onHelloButtonClick"/>
|
||||
</VBox>
|
||||
Loading…
Reference in new issue