diff --git a/src/com/personalproject/ui/DesktopApp.java b/src/com/personalproject/ui/DesktopApp.java new file mode 100644 index 0000000..61e1e26 --- /dev/null +++ b/src/com/personalproject/ui/DesktopApp.java @@ -0,0 +1,658 @@ +package com.personalproject.ui; + +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.model.ExamSession; +import com.personalproject.model.QuizQuestion; +import com.personalproject.service.QuestionGenerationService; +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SpinnerNumberModel; + +/** + * A Swing desktop application that drives the math learning backend. + */ +public final class DesktopApp extends JFrame { + + private static final String CARD_LOGIN = "login"; + private static final String CARD_REGISTER = "register"; + private static final String CARD_VERIFY = "verify"; + private static final String CARD_SET_PASSWORD = "set_password"; + private static final String CARD_CHANGE_PASSWORD = "change_password"; + private static final String CARD_SELECT = "select"; + private static final String CARD_EXAM = "exam"; + private static final String CARD_SCORE = "score"; + + private final MathLearningController controller; + private final CardLayout cardLayout; + private final JPanel cards; + + private String currentUsername = ""; + private ExamSession currentExamSession; + private int currentSelectedAnswerIndex = -1; + + public DesktopApp() { + super("数学学习桌面应用"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setMinimumSize(new Dimension(820, 640)); + + Map 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); + + this.cardLayout = new CardLayout(); + this.cards = new JPanel(cardLayout); + + cards.add(buildLoginPanel(), CARD_LOGIN); + cards.add(buildRegisterPanel(), CARD_REGISTER); + cards.add(buildVerifyPanel(), CARD_VERIFY); + cards.add(buildSetPasswordPanel(), CARD_SET_PASSWORD); + cards.add(buildChangePasswordPanel(), CARD_CHANGE_PASSWORD); + cards.add(buildSelectPanel(), CARD_SELECT); + cards.add(buildExamPanel(), CARD_EXAM); + cards.add(buildScorePanel(), CARD_SCORE); + + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(cards, BorderLayout.CENTER); + cardLayout.show(cards, CARD_LOGIN); + } + + private JPanel buildLoginPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("登录"); + title.setFont(title.getFont().deriveFont(20f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("密码:"), gc); + JPasswordField passwordField = new JPasswordField(18); + gc.gridx = 1; + panel.add(passwordField, gc); + + gc.gridy++; + gc.gridx = 0; + JButton loginBtn = new JButton("登录"); + panel.add(loginBtn, gc); + gc.gridx = 1; + JButton toRegisterBtn = new JButton("注册"); + panel.add(toRegisterBtn, gc); + + gc.gridy++; + gc.gridx = 0; + JButton toChangePwd = new JButton("修改密码"); + panel.add(toChangePwd, gc); + + loginBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String password = String.valueOf(passwordField.getPassword()); + if (username.isEmpty() || password.isEmpty()) { + showError("请输入用户名和密码"); + return; + } + Optional account = controller.authenticate(username, + password); + if (account.isPresent()) { + currentUsername = username; + cardLayout.show(cards, CARD_SELECT); + } else { + showError("登录失败:用户名或密码错误,或未完成注册"); + } + }); + + toRegisterBtn.addActionListener(e -> cardLayout.show(cards, CARD_REGISTER)); + toChangePwd.addActionListener(e -> cardLayout.show(cards, CARD_CHANGE_PASSWORD)); + + return panel; + } + + private JPanel buildRegisterPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("用户注册"); + title.setFont(title.getFont().deriveFont(20f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("邮箱:"), gc); + JTextField emailField = new JTextField(18); + gc.gridx = 1; + panel.add(emailField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("难度:"), gc); + JComboBox levelBox = new JComboBox<>(DifficultyLevel.values()); + gc.gridx = 1; + panel.add(levelBox, gc); + + gc.gridy++; + gc.gridx = 0; + JButton sendCodeBtn = new JButton("发送注册码"); + panel.add(sendCodeBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回登录"); + panel.add(backBtn, gc); + + sendCodeBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + DifficultyLevel level = (DifficultyLevel) levelBox.getSelectedItem(); + if (username.isEmpty() || email.isEmpty() || level == null) { + showError("请填写完整信息"); + return; + } + if (!controller.isValidEmail(email)) { + showError("邮箱格式不正确"); + return; + } + boolean ok = controller.initiateRegistration(username, email, level); + if (ok) { + currentUsername = username; + JOptionPane.showMessageDialog(this, "注册码已发送到邮箱(示例中输出到控制台)"); + cardLayout.show(cards, CARD_VERIFY); + } else { + showError("注册启动失败:用户名已存在或邮箱已注册"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_LOGIN)); + return panel; + } + + private JPanel buildVerifyPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("输入邮箱收到的注册码"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("注册码:"), gc); + JTextField codeField = new JTextField(18); + gc.gridx = 1; + panel.add(codeField, gc); + + gc.gridy++; + gc.gridx = 0; + JButton verifyBtn = new JButton("验证"); + panel.add(verifyBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回"); + panel.add(backBtn, gc); + + verifyBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String code = codeField.getText().trim(); + if (username.isEmpty() || code.isEmpty()) { + showError("请输入用户名与注册码"); + return; + } + boolean ok = controller.verifyRegistrationCode(username, code); + if (ok) { + currentUsername = username; + JOptionPane.showMessageDialog(this, "验证成功,请设置密码"); + cardLayout.show(cards, CARD_SET_PASSWORD); + } else { + showError("验证失败:注册码错误或超出尝试次数"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_REGISTER)); + return panel; + } + + private JPanel buildSetPasswordPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("设置密码(6-10位,含大小写字母和数字)"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("密码:"), gc); + JPasswordField pwd1 = new JPasswordField(18); + gc.gridx = 1; + panel.add(pwd1, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("确认密码:"), gc); + JPasswordField pwd2 = new JPasswordField(18); + gc.gridx = 1; + panel.add(pwd2, gc); + + gc.gridy++; + gc.gridx = 0; + JButton setBtn = new JButton("设置密码"); + panel.add(setBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回"); + panel.add(backBtn, gc); + + setBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String p1 = String.valueOf(pwd1.getPassword()); + String p2 = String.valueOf(pwd2.getPassword()); + if (username.isEmpty() || p1.isEmpty() || p2.isEmpty()) { + showError("请填写完整信息"); + return; + } + if (!p1.equals(p2)) { + showError("两次密码不一致"); + return; + } + if (!controller.isValidPassword(p1)) { + showError("密码不符合要求:6-10位,含大小写字母和数字"); + return; + } + boolean ok = controller.setPassword(username, p1); + if (ok) { + currentUsername = username; + JOptionPane.showMessageDialog(this, "密码设置成功,请登录"); + cardLayout.show(cards, CARD_LOGIN); + } else { + showError("设置失败:请确认已完成注册或用户存在"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_VERIFY)); + return panel; + } + + private JPanel buildChangePasswordPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("修改密码"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("用户名:"), gc); + JTextField usernameField = new JTextField(18); + gc.gridx = 1; + panel.add(usernameField, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("原密码:"), gc); + JPasswordField oldPwd = new JPasswordField(18); + gc.gridx = 1; + panel.add(oldPwd, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("新密码:"), gc); + JPasswordField newPwd1 = new JPasswordField(18); + gc.gridx = 1; + panel.add(newPwd1, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("确认新密码:"), gc); + JPasswordField newPwd2 = new JPasswordField(18); + gc.gridx = 1; + panel.add(newPwd2, gc); + + gc.gridy++; + gc.gridx = 0; + JButton changeBtn = new JButton("修改"); + panel.add(changeBtn, gc); + gc.gridx = 1; + JButton backBtn = new JButton("返回登录"); + panel.add(backBtn, gc); + + changeBtn.addActionListener(e -> { + String username = usernameField.getText().trim(); + String old = String.valueOf(oldPwd.getPassword()); + String n1 = String.valueOf(newPwd1.getPassword()); + String n2 = String.valueOf(newPwd2.getPassword()); + if (username.isEmpty() || old.isEmpty() || n1.isEmpty() || n2.isEmpty()) { + showError("请填写完整信息"); + return; + } + if (!n1.equals(n2)) { + showError("两次新密码不一致"); + return; + } + if (!controller.isValidPassword(n1)) { + showError("新密码不符合要求:6-10位,含大小写字母和数字"); + return; + } + boolean ok = controller.changePassword(username, old, n1); + if (ok) { + JOptionPane.showMessageDialog(this, "密码修改成功,请使用新密码登录"); + cardLayout.show(cards, CARD_LOGIN); + } else { + showError("修改失败:原密码错误或未完成注册"); + } + }); + + backBtn.addActionListener(e -> cardLayout.show(cards, CARD_LOGIN)); + return panel; + } + + private JPanel buildSelectPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("选择难度与题目数量"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 1; + panel.add(new JLabel("难度:"), gc); + JComboBox levelBox = new JComboBox<>(DifficultyLevel.values()); + gc.gridx = 1; + panel.add(levelBox, gc); + + gc.gridy++; + gc.gridx = 0; + panel.add(new JLabel("题目数量 (10-30):"), gc); + JSpinner countSpinner = new JSpinner(new SpinnerNumberModel(10, 10, 30, 1)); + gc.gridx = 1; + panel.add(countSpinner, gc); + + gc.gridy++; + gc.gridx = 0; + JButton startBtn = new JButton("开始做题"); + panel.add(startBtn, gc); + gc.gridx = 1; + JButton logoutBtn = new JButton("退出登录"); + panel.add(logoutBtn, gc); + + startBtn.addActionListener(e -> { + if (currentUsername == null || currentUsername.isEmpty()) { + showError("请先登录"); + cardLayout.show(cards, CARD_LOGIN); + return; + } + DifficultyLevel level = (DifficultyLevel) levelBox.getSelectedItem(); + int count = (Integer) countSpinner.getValue(); + try { + currentExamSession = controller.createExamSession(currentUsername, level, count); + currentSelectedAnswerIndex = -1; + loadExamQuestionIntoUI(); + cardLayout.show(cards, CARD_EXAM); + } catch (IllegalArgumentException ex) { + showError("无法创建考试:" + ex.getMessage()); + } + }); + + logoutBtn.addActionListener(e -> { + currentUsername = ""; + cardLayout.show(cards, CARD_LOGIN); + }); + return panel; + } + + private JPanel buildExamPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("答题"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 2; + JTextArea questionArea = new JTextArea(5, 50); + questionArea.setEditable(false); + questionArea.setLineWrap(true); + questionArea.setWrapStyleWord(true); + panel.add(new JScrollPane(questionArea), gc); + + gc.gridy++; + gc.gridwidth = 2; + JPanel optionsPanel = new JPanel(new GridBagLayout()); + optionsPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + panel.add(optionsPanel, gc); + + ButtonGroup group = new ButtonGroup(); + JRadioButton[] optionButtons = new JRadioButton[4]; + for (int i = 0; i < optionButtons.length; i++) { + optionButtons[i] = new JRadioButton(); + group.add(optionButtons[i]); + } + + // place options + for (int i = 0; i < optionButtons.length; i++) { + GridBagConstraints og = new GridBagConstraints(); + og.gridx = 0; + og.gridy = i; + og.anchor = GridBagConstraints.WEST; + og.insets = new Insets(6, 6, 6, 6); + optionsPanel.add(optionButtons[i], og); + } + + gc.gridy++; + gc.gridwidth = 1; + JButton submitBtn = new JButton("提交/下一题"); + panel.add(submitBtn, gc); + gc.gridx = 1; + JButton backToSelect = new JButton("中止并返回"); + panel.add(backToSelect, gc); + + submitBtn.addActionListener(e -> { + if (currentExamSession == null) { + showError("没有正在进行的考试"); + return; + } + int selected = currentSelectedAnswerIndex; + if (selected < 0) { + showError("请先选择一个选项"); + return; + } + try { + currentExamSession.setAnswer(selected); + } catch (RuntimeException ex) { + showError("设置答案失败:" + ex.getMessage()); + return; + } + boolean hasNext = currentExamSession.goToNextQuestion(); + if (hasNext) { + currentSelectedAnswerIndex = -1; + group.clearSelection(); + loadExamQuestionIntoUI(questionArea, optionButtons); + } else { + showScoreAndSave(); + cardLayout.show(cards, CARD_SCORE); + } + }); + + backToSelect.addActionListener(e -> cardLayout.show(cards, CARD_SELECT)); + + // initial load hookup via helper method when starting exam + panel.putClientProperty("questionArea", questionArea); + panel.putClientProperty("optionButtons", optionButtons); + return panel; + } + + private JPanel buildScorePanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + GridBagConstraints gc = baseGbc(); + + JLabel title = new JLabel("成绩"); + title.setFont(title.getFont().deriveFont(18f)); + gc.gridwidth = 2; + panel.add(title, gc); + + gc.gridy++; + gc.gridwidth = 2; + JLabel scoreLabel = new JLabel("得分: 0.00%"); + panel.add(scoreLabel, gc); + + gc.gridy++; + gc.gridwidth = 1; + JButton continueBtn = new JButton("继续做题"); + panel.add(continueBtn, gc); + gc.gridx = 1; + JButton exitBtn = new JButton("退出到登录"); + panel.add(exitBtn, gc); + + continueBtn.addActionListener(e -> cardLayout.show(cards, CARD_SELECT)); + exitBtn.addActionListener(e -> { + currentUsername = ""; + cardLayout.show(cards, CARD_LOGIN); + }); + + panel.putClientProperty("scoreLabel", scoreLabel); + return panel; + } + + private void loadExamQuestionIntoUI() { + JPanel examPanel = (JPanel) cards.getComponent(6); // CARD_EXAM position in add order + JTextArea questionArea = (JTextArea) examPanel.getClientProperty("questionArea"); + @SuppressWarnings("unchecked") + JRadioButton[] optionButtons = (JRadioButton[]) examPanel.getClientProperty("optionButtons"); + loadExamQuestionIntoUI(questionArea, optionButtons); + } + + private void loadExamQuestionIntoUI(JTextArea questionArea, JRadioButton[] optionButtons) { + if (currentExamSession == null) { + return; + } + QuizQuestion question = currentExamSession.getCurrentQuestion(); + questionArea.setText(question.getQuestionText()); + List options = question.getOptions(); + for (int i = 0; i < optionButtons.length; i++) { + if (i < options.size()) { + optionButtons[i].setText(options.get(i)); + int idx = i; + for (var al : optionButtons[i].getActionListeners()) { + optionButtons[i].removeActionListener(al); + } + optionButtons[i].addActionListener(e -> currentSelectedAnswerIndex = idx); + optionButtons[i].setVisible(true); + } else { + optionButtons[i].setVisible(false); + } + } + } + + private void showScoreAndSave() { + if (currentExamSession == null) { + return; + } + double score = currentExamSession.calculateScore(); + try { + controller.saveExamResults(currentExamSession); + JOptionPane.showMessageDialog(this, + String.format("本次得分:%.2f%%\n结果已保存。", score)); + } catch (RuntimeException ex) { + JOptionPane.showMessageDialog(this, + String.format("本次得分:%.2f%%\n保存结果失败:%s", score, ex.getMessage())); + } + JPanel scorePanel = (JPanel) cards.getComponent(7); // CARD_SCORE + JLabel scoreLabel = (JLabel) scorePanel.getClientProperty("scoreLabel"); + scoreLabel.setText(String.format("得分: %.2f%%", score)); + } + + private static GridBagConstraints baseGbc() { + GridBagConstraints gc = new GridBagConstraints(); + gc.gridx = 0; + gc.gridy = 0; + gc.insets = new Insets(8, 8, 8, 8); + gc.anchor = GridBagConstraints.WEST; + return gc; + } + + private void showError(String message) { + JOptionPane.showMessageDialog(this, message, "错误", JOptionPane.ERROR_MESSAGE); + } + + public static void main(String[] args) { + EventQueue.invokeLater(() -> { + DesktopApp app = new DesktopApp(); + app.setLocationRelativeTo(null); + app.setVisible(true); + }); + } +} + +