diff --git a/src/controller/AppController.java b/src/controller/AppController.java new file mode 100644 index 0000000..ee55db9 --- /dev/null +++ b/src/controller/AppController.java @@ -0,0 +1,316 @@ +package controller; + +import model.*; +import service.*; +import view.*; + +import javax.swing.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + + +public class AppController { + // 模型和服务 + private final UserManager userManager; + private final ExamManager examManager; + private final EmailService emailService; + + // 视图 + private final MainFrame mainFrame; + private final LoginView loginView; + private final RegisterView registerView; + private final PasswordSetView passwordSetView; + private final MainMenuView mainMenuView; + private final ExamView examView; + private final ScoreView scoreView; + + // 应用状态 + private User currentUser; + private String emailForRegistration; + private String usernameForRegistration; + private final Map registrationCodes = new HashMap<>(); + private List currentExam; + private int currentQuestionIndex; + private int score; + private int[] userAnswers; // 新增:用于存储用户每一题的答案 + + public AppController() { + this.mainFrame = new MainFrame(); + this.userManager = new UserManager(); + this.examManager = new ExamManager(); + this.emailService = new EmailService(); + + this.loginView = new LoginView(); + this.registerView = new RegisterView(); + this.passwordSetView = new PasswordSetView(); + this.mainMenuView = new MainMenuView(); + this.examView = new ExamView(); + this.scoreView = new ScoreView(); + + mainFrame.addPanel(loginView, "LOGIN"); + mainFrame.addPanel(registerView, "REGISTER"); + mainFrame.addPanel(passwordSetView, "PASSWORD_SET"); + mainFrame.addPanel(mainMenuView, "MAIN_MENU"); + mainFrame.addPanel(examView, "EXAM"); + mainFrame.addPanel(scoreView, "SCORE"); + + attachListeners(); + mainFrame.showPanel("LOGIN"); + } + + /** + * 附加所有UI组件的事件监听器。 + */ + private void attachListeners() { + // --- 登录逻辑 --- + loginView.getLoginButton().addActionListener(e -> handleLogin()); + loginView.getRegisterButton().addActionListener(e -> mainFrame.showPanel("REGISTER")); + + // --- 注册逻辑 --- + registerView.getGetCodeButton().addActionListener(e -> handleGetCode()); + registerView.getRegisterButton().addActionListener(e -> handleVerifyCode()); + registerView.getBackButton().addActionListener(e -> mainFrame.showPanel("LOGIN")); + + // --- 密码设置逻辑 --- + passwordSetView.getSubmitButton().addActionListener(e -> handleSetPassword()); + + // --- 主菜单逻辑 --- + mainMenuView.getStartExamButton().addActionListener(e -> handleStartExam()); + mainMenuView.getChangePasswordButton().addActionListener(e -> handleChangePassword()); + mainMenuView.getLogoutButton().addActionListener(e -> handleLogout()); + + // --- 考试逻辑 (已重构) --- + examView.getPrevButton().addActionListener(e -> handlePreviousQuestion()); + examView.getNextButton().addActionListener(e -> handleNextQuestion()); + examView.getFinishButton().addActionListener(e -> handleFinishExam()); + + // --- 分数界面逻辑 --- + scoreView.getContinueButton().addActionListener(e -> { + String welcomeName = currentUser.getUsername() != null && !currentUser.getUsername().isEmpty() ? currentUser.getUsername() : currentUser.getEmail(); + mainMenuView.setWelcomeMessage("欢迎, " + welcomeName); + mainFrame.showPanel("MAIN_MENU"); + }); + scoreView.getExitButton().addActionListener(e -> handleLogout()); + } + + // --- 核心处理方法 --- + + private void handleLogin() { + User user = userManager.authenticate(loginView.getIdentifier(), loginView.getPassword()); + if (user != null) { + currentUser = user; + String welcomeName = user.getUsername() != null && !user.getUsername().isEmpty() ? user.getUsername() : user.getEmail(); + mainMenuView.setWelcomeMessage("欢迎, " + welcomeName); + mainFrame.showPanel("MAIN_MENU"); + } else { + JOptionPane.showMessageDialog(mainFrame, "邮箱/用户名或密码错误", "登录失败", JOptionPane.ERROR_MESSAGE); + } + loginView.clearFields(); + } + + private void handleGetCode() { + String email = registerView.getEmail(); + String username = registerView.getUsername(); + + if (!ValidationService.isValidEmail(email)) { + JOptionPane.showMessageDialog(mainFrame, "请输入有效的邮箱格式!", "格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (username.trim().isEmpty() || username.contains("|") || username.contains(" ")) { + JOptionPane.showMessageDialog(mainFrame, "用户名不能为空,且不能包含'|'或空格!", "格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (userManager.emailExists(email)) { + JOptionPane.showMessageDialog(mainFrame, "该邮箱已被注册!", "注册失败", JOptionPane.ERROR_MESSAGE); + return; + } + if (userManager.usernameExists(username)) { + JOptionPane.showMessageDialog(mainFrame, "该用户名已被使用!", "注册失败", JOptionPane.ERROR_MESSAGE); + return; + } + + emailForRegistration = email; + usernameForRegistration = username; + String code = String.format("%06d", new Random().nextInt(1000000)); + registrationCodes.put(emailForRegistration, code); + + final JButton getCodeButton = registerView.getGetCodeButton(); + getCodeButton.setEnabled(false); + getCodeButton.setText("发送中..."); + + SwingWorker worker = new SwingWorker<>() { + private Exception mailException = null; + + @Override + protected Void doInBackground() { + try { + emailService.sendVerificationCode(emailForRegistration, code); + } catch (Exception e) { + this.mailException = e; + } + return null; + } + + @Override + protected void done() { + getCodeButton.setEnabled(true); + getCodeButton.setText("发送验证邮件"); + + if (mailException == null) { + JOptionPane.showMessageDialog(mainFrame, "一封包含验证码的【电子邮件】已发送至您的邮箱,请登录邮箱查收。", "发送成功", JOptionPane.INFORMATION_MESSAGE); + } else { + String errorMessage = "邮件发送失败,请检查:\n1. 网络连接是否正常。\n2. config.properties 中的邮箱和授权码是否正确。\n\n错误详情: " + mailException.getMessage(); + JOptionPane.showMessageDialog(mainFrame, errorMessage, "发送失败", JOptionPane.ERROR_MESSAGE); + mailException.printStackTrace(); + } + } + }; + worker.execute(); + } + + private void handleVerifyCode() { + String code = registerView.getCode(); + if (code.equals(registrationCodes.get(emailForRegistration))) { + mainFrame.showPanel("PASSWORD_SET"); + } else { + JOptionPane.showMessageDialog(mainFrame, "注册码错误!", "验证失败", JOptionPane.ERROR_MESSAGE); + } + } + + private void handleSetPassword() { + String pass1 = passwordSetView.getPassword(); + String pass2 = passwordSetView.getConfirmPassword(); + + if (!pass1.equals(pass2)) { + JOptionPane.showMessageDialog(mainFrame, "两次输入的密码不一致!", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (!ValidationService.isPasswordStrong(pass1)) { + JOptionPane.showMessageDialog(mainFrame, "密码必须为6-10位,且包含大小写字母和数字。", "密码格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + + userManager.registerUser(emailForRegistration, usernameForRegistration, pass1, UserType.PRIMARY); + JOptionPane.showMessageDialog(mainFrame, "注册成功!请返回登录。", "成功", JOptionPane.INFORMATION_MESSAGE); + mainFrame.showPanel("LOGIN"); + } + + private void handleStartExam() { + UserType type = mainMenuView.getSelectedType(); + int count = mainMenuView.getQuestionCount(); + + if(currentUser.getUserType() != type) { + currentUser.setUserType(type); + userManager.updateUser(currentUser); + } + + currentExam = examManager.generateExam(currentUser, type, count); + if (currentExam.size() < count) { + JOptionPane.showMessageDialog(mainFrame, "抱歉,未能生成足够数量的不重复题目,实际生成 " + currentExam.size() + " 道。", "提示", JOptionPane.WARNING_MESSAGE); + } + if (currentExam.isEmpty()) return; + + currentQuestionIndex = 0; + score = 0; + userAnswers = new int[currentExam.size()]; + Arrays.fill(userAnswers, -1); // -1 代表未作答 + + examView.setupForExam(currentExam.size()); + displayCurrentQuestion(); + mainFrame.showPanel("EXAM"); + } + + private void displayCurrentQuestion() { + Question q = currentExam.get(currentQuestionIndex); + int savedAnswer = userAnswers[currentQuestionIndex]; + examView.displayQuestion(q, currentQuestionIndex + 1, currentExam.size(), savedAnswer); + examView.updateNavigationButtons(currentQuestionIndex, currentExam.size()); + } + + private void handlePreviousQuestion() { + saveCurrentAnswer(); + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + displayCurrentQuestion(); + } + } + + private void handleNextQuestion() { + saveCurrentAnswer(); + if (currentQuestionIndex < currentExam.size() - 1) { + currentQuestionIndex++; + displayCurrentQuestion(); + } + } + + private void saveCurrentAnswer() { + int selectedIndex = examView.getSelectedIndex(); + userAnswers[currentQuestionIndex] = selectedIndex; + } + + private void handleFinishExam() { + saveCurrentAnswer(); + + int choice = JOptionPane.showConfirmDialog(mainFrame, + "您确定要交卷吗?\n未完成的题目将按 0 分计算。", + "确认交卷", + JOptionPane.YES_NO_OPTION); + + if (choice == JOptionPane.YES_OPTION) { + calculateScore(); + int finalScore = (int) Math.round((double) score / currentExam.size() * 100); + scoreView.setScore(finalScore, score, currentExam.size()); + mainFrame.showPanel("SCORE"); + } + } + + private void calculateScore() { + score = 0; + for (int i = 0; i < currentExam.size(); i++) { + if (userAnswers[i] != -1 && userAnswers[i] == currentExam.get(i).getCorrectOptionIndex()) { + score++; + } + } + } + + private void handleChangePassword() { + ChangePasswordView dialog = new ChangePasswordView(mainFrame); + + dialog.getConfirmButton().addActionListener(e -> { + String oldPass = dialog.getOldPassword(); + String newPass = dialog.getNewPassword(); + String confirmPass = dialog.getConfirmPassword(); + + if (userManager.authenticate(currentUser.getEmail(), oldPass) == null) { + JOptionPane.showMessageDialog(dialog, "原密码错误!", "验证失败", JOptionPane.ERROR_MESSAGE); + return; + } + if (!newPass.equals(confirmPass)) { + JOptionPane.showMessageDialog(dialog, "两次输入的新密码不一致!", "错误", JOptionPane.ERROR_MESSAGE); + return; + } + if (!ValidationService.isPasswordStrong(newPass)) { + JOptionPane.showMessageDialog(dialog, "新密码必须为6-10位,且包含大小写字母和数字。", "密码格式错误", JOptionPane.ERROR_MESSAGE); + return; + } + + currentUser.setHashedPassword(newPass); + userManager.updateUser(currentUser); + + JOptionPane.showMessageDialog(dialog, "密码修改成功!", "成功", JOptionPane.INFORMATION_MESSAGE); + dialog.dispose(); + }); + + dialog.getCancelButton().addActionListener(e -> dialog.dispose()); + dialog.setVisible(true); + } + + private void handleLogout() { + currentUser = null; + loginView.clearFields(); + mainFrame.showPanel("LOGIN"); + } +} \ No newline at end of file diff --git a/src/view/ChangePasswordView.java b/src/view/ChangePasswordView.java new file mode 100644 index 0000000..50cf96b --- /dev/null +++ b/src/view/ChangePasswordView.java @@ -0,0 +1,50 @@ +package view; + +import javax.swing.*; +import java.awt.*; + +public class ChangePasswordView extends JDialog { + private final JPasswordField oldPasswordField = new JPasswordField(20); + private final JPasswordField newPasswordField = new JPasswordField(20); + private final JPasswordField confirmPasswordField = new JPasswordField(20); + private final JButton confirmButton = new JButton("确认修改"); + private final JButton cancelButton = new JButton("取消"); + + public ChangePasswordView(Frame owner) { + super(owner, "修改密码", true); + setSize(450, 300); + setLocationRelativeTo(owner); + setLayout(new BorderLayout(10, 10)); + + JPanel fieldsPanel = new JPanel(new GridBagLayout()); + fieldsPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + + JLabel hintLabel = new JLabel("新密码要求: 6-10位,且必须包含大小写字母和数字。"); + hintLabel.setForeground(Color.GRAY); + gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; fieldsPanel.add(hintLabel, gbc); + + gbc.gridwidth = 1; + gbc.gridx = 0; gbc.gridy = 1; fieldsPanel.add(new JLabel("原密码:"), gbc); + gbc.gridx = 1; gbc.gridy = 1; fieldsPanel.add(oldPasswordField, gbc); + gbc.gridx = 0; gbc.gridy = 2; fieldsPanel.add(new JLabel("新密码:"), gbc); + gbc.gridx = 1; gbc.gridy = 2; fieldsPanel.add(newPasswordField, gbc); + gbc.gridx = 0; gbc.gridy = 3; fieldsPanel.add(new JLabel("确认新密码:"), gbc); + gbc.gridx = 1; gbc.gridy = 3; fieldsPanel.add(confirmPasswordField, gbc); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0)); + buttonPanel.add(confirmButton); + buttonPanel.add(cancelButton); + + add(fieldsPanel, BorderLayout.CENTER); + add(buttonPanel, BorderLayout.SOUTH); + } + + public String getOldPassword() { return new String(oldPasswordField.getPassword()); } + public String getNewPassword() { return new String(newPasswordField.getPassword()); } + public String getConfirmPassword() { return new String(confirmPasswordField.getPassword()); } + public JButton getConfirmButton() { return confirmButton; } + public JButton getCancelButton() { return cancelButton; } +} \ No newline at end of file diff --git a/src/view/ExamView.java b/src/view/ExamView.java new file mode 100644 index 0000000..3789205 --- /dev/null +++ b/src/view/ExamView.java @@ -0,0 +1,140 @@ +package view; + +import model.Question; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; +import java.awt.*; +import java.util.List; + + +public class ExamView extends JPanel { + private final JLabel progressLabel = new JLabel(" ", SwingConstants.CENTER); + private final JProgressBar progressBar = new JProgressBar(); + private final JTextPane questionPane = new JTextPane(); + private final JRadioButton[] optionButtons = new JRadioButton[4]; + private final ButtonGroup optionsGroup = new ButtonGroup(); + + private final JButton prevButton = new JButton("上一题"); + private final JButton nextButton = new JButton("下一题"); + private final JButton finishButton = new JButton("交卷并评分"); + + public ExamView() { + setLayout(new BorderLayout(20, 20)); + setBorder(new EmptyBorder(20, 40, 40, 40)); + + // --- 顶部进度区域 --- + JPanel topPanel = new JPanel(new BorderLayout(0, 10)); + progressLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 20)); + topPanel.add(progressLabel, BorderLayout.NORTH); + + progressBar.setPreferredSize(new Dimension(1, 15)); + topPanel.add(progressBar, BorderLayout.CENTER); + add(topPanel, BorderLayout.NORTH); + + // --- 中间题目区域 --- + questionPane.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 36)); + questionPane.setEditable(false); + questionPane.setFocusable(false); + questionPane.setOpaque(false); + StyledDocument doc = questionPane.getStyledDocument(); + SimpleAttributeSet center = new SimpleAttributeSet(); + StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER); + doc.setParagraphAttributes(0, doc.getLength(), center, false); + + JPanel questionWrapper = new JPanel(new BorderLayout()); + questionWrapper.setOpaque(false); + questionWrapper.add(questionPane, BorderLayout.CENTER); + add(questionWrapper, BorderLayout.CENTER); + + // --- 底部选项和导航 --- + JPanel southPanel = new JPanel(new BorderLayout(20, 30)); + + // --- 选项面板 --- + JPanel optionsPanel = new JPanel(new GridBagLayout()); + optionsPanel.setOpaque(false); + GridBagConstraints gbcOpt = new GridBagConstraints(); + gbcOpt.insets = new Insets(15, 80, 15, 80); // 调整选项的内外边距 + gbcOpt.anchor = GridBagConstraints.WEST; + + for (int i = 0; i < 4; i++) { + optionButtons[i] = new JRadioButton(); + optionButtons[i].setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20)); + optionButtons[i].setOpaque(false); + optionsGroup.add(optionButtons[i]); + } + + + gbcOpt.gridx = 0; gbcOpt.gridy = 0; optionsPanel.add(optionButtons[0], gbcOpt); // 位置 A + gbcOpt.gridx = 1; gbcOpt.gridy = 0; optionsPanel.add(optionButtons[1], gbcOpt); // 位置 B + gbcOpt.gridx = 0; gbcOpt.gridy = 1; optionsPanel.add(optionButtons[2], gbcOpt); // 位置 C + gbcOpt.gridx = 1; gbcOpt.gridy = 1; optionsPanel.add(optionButtons[3], gbcOpt); // 位置 D + + JPanel optionsWrapper = new JPanel(new GridBagLayout()); + optionsWrapper.setOpaque(false); + optionsWrapper.add(optionsPanel); + southPanel.add(optionsWrapper, BorderLayout.CENTER); + + // --- 导航按钮面板 --- + JPanel navigationPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 30, 0)); + prevButton.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + nextButton.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + finishButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + finishButton.putClientProperty("JButton.buttonType", "roundRect"); + + navigationPanel.add(prevButton); + navigationPanel.add(finishButton); + navigationPanel.add(nextButton); + southPanel.add(navigationPanel, BorderLayout.SOUTH); + + add(southPanel, BorderLayout.SOUTH); + } + + public void displayQuestion(Question q, int qNum, int total, int savedAnswerIndex) { + progressLabel.setText(String.format("第 %d 题 / 共 %d 题", qNum, total)); + progressBar.setValue(qNum); + questionPane.setText(q.getQuestionText()); + + StyledDocument doc = questionPane.getStyledDocument(); + SimpleAttributeSet center = new SimpleAttributeSet(); + StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER); + doc.setParagraphAttributes(0, doc.getLength(), center, false); + + optionsGroup.clearSelection(); + List options = q.getOptions(); + + char optionLabel = 'A'; + for (int i = 0; i < 4; i++) { + optionButtons[i].setText(optionLabel + ". " + options.get(i)); + optionLabel++; + } + + if (savedAnswerIndex != -1) { + optionButtons[savedAnswerIndex].setSelected(true); + } + } + + public void setupForExam(int totalQuestions) { + progressBar.setMinimum(1); + progressBar.setMaximum(totalQuestions); + } + + public void updateNavigationButtons(int qIndex, int total) { + prevButton.setEnabled(qIndex > 0); + nextButton.setEnabled(qIndex < total - 1); + nextButton.setText(qIndex == total - 2 ? "最后一题" : "下一题"); + } + + public int getSelectedIndex() { + for (int i = 0; i < 4; i++) { + if (optionButtons[i].isSelected()) { return i; } + } + return -1; + } + + public JButton getPrevButton() { return prevButton; } + public JButton getNextButton() { return nextButton; } + public JButton getFinishButton() { return finishButton; } +} \ No newline at end of file diff --git a/src/view/LoginView.java b/src/view/LoginView.java new file mode 100644 index 0000000..1b9ef88 --- /dev/null +++ b/src/view/LoginView.java @@ -0,0 +1,64 @@ +package view; + +import javax.swing.*; +import java.awt.*; + +public class LoginView extends JPanel { + private final JTextField identifierField = new JTextField(20); + private final JPasswordField passwordField = new JPasswordField(20); + private final JButton loginButton = new JButton("登 录"); + private final JButton registerButton = new JButton("还没有账号?立即注册"); + + public LoginView() { + setLayout(new GridBagLayout()); + + JPanel formPanel = new JPanel(new GridBagLayout()); + formPanel.setBorder(BorderFactory.createEmptyBorder(30, 40, 30, 40)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.fill = GridBagConstraints.HORIZONTAL; + + JLabel titleLabel = new JLabel("欢迎回来", SwingConstants.CENTER); + titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28)); + gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; + formPanel.add(titleLabel, gbc); + + gbc.gridwidth = 1; + gbc.gridx = 0; gbc.gridy = 1; addLabel(formPanel, "邮箱 / 用户名:", gbc); + gbc.gridx = 1; gbc.gridy = 1; addComponent(formPanel, identifierField, gbc); + + gbc.gridx = 0; gbc.gridy = 2; addLabel(formPanel, "密码:", gbc); + gbc.gridx = 1; gbc.gridy = 2; addComponent(formPanel, passwordField, gbc); + + gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2; + loginButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + addComponent(formPanel, loginButton, gbc); + + gbc.gridy = 4; + + registerButton.putClientProperty("FlatLaf.style", "buttonType: borderless"); + addComponent(formPanel, registerButton, gbc); + + add(formPanel); + } + + private void addLabel(JPanel panel, String text, GridBagConstraints gbc) { + JLabel label = new JLabel(text); + label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + panel.add(label, gbc); + } + + private void addComponent(JPanel panel, JComponent comp, GridBagConstraints gbc) { + comp.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + panel.add(comp, gbc); + } + + public String getIdentifier() { return identifierField.getText().trim(); } + public String getPassword() { return new String(passwordField.getPassword()); } + public JButton getLoginButton() { return loginButton; } + public JButton getRegisterButton() { return registerButton; } + public void clearFields() { + identifierField.setText(""); + passwordField.setText(""); + } +} \ No newline at end of file diff --git a/src/view/MainFrame.java b/src/view/MainFrame.java new file mode 100644 index 0000000..af39906 --- /dev/null +++ b/src/view/MainFrame.java @@ -0,0 +1,27 @@ +package view; + +import javax.swing.*; +import java.awt.*; + +public class MainFrame extends JFrame { + private final CardLayout cardLayout = new CardLayout(); + private final JPanel mainPanel = new JPanel(cardLayout); + + public MainFrame() { + super("数学学习软件"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(800, 600); + setLocationRelativeTo(null); + setResizable(false); + add(mainPanel); + } + + public void addPanel(JPanel panel, String name) { + mainPanel.add(panel, name); + } + + public void showPanel(String name) { + cardLayout.show(mainPanel, name); + setVisible(true); + } +} \ No newline at end of file diff --git a/src/view/MainMenuView.java b/src/view/MainMenuView.java new file mode 100644 index 0000000..40c0cf8 --- /dev/null +++ b/src/view/MainMenuView.java @@ -0,0 +1,72 @@ +package view; + +import model.UserType; +import javax.swing.*; +import java.awt.*; + +public class MainMenuView extends JPanel { + private final JLabel welcomeLabel = new JLabel("", SwingConstants.CENTER); + private final JComboBox typeComboBox = new JComboBox<>(); + private final JSpinner countSpinner = new JSpinner(new SpinnerNumberModel(10, 10, 30, 1)); + private final JButton startExamButton = new JButton("开始答题"); + private final JButton changePasswordButton = new JButton("修改密码"); + private final JButton logoutButton = new JButton("退出登录"); + + public MainMenuView() { + setLayout(new BorderLayout(20, 20)); + setBorder(BorderFactory.createEmptyBorder(40, 40, 40, 40)); + + welcomeLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28)); + add(welcomeLabel, BorderLayout.NORTH); + + JPanel centerPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(15, 10, 15, 10); + + gbc.gridx = 0; gbc.gridy = 0; addLabel(centerPanel, "选择题目难度:", gbc); + gbc.gridx = 1; gbc.gridy = 0; addComponent(centerPanel, typeComboBox, gbc); + + gbc.gridx = 0; gbc.gridy = 1; addLabel(centerPanel, "选择题目数量:", gbc); + gbc.gridx = 1; gbc.gridy = 1; addComponent(centerPanel, countSpinner, gbc); + + startExamButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 20)); + + startExamButton.putClientProperty("JButton.buttonType", "roundRect"); + gbc.gridy = 2; gbc.gridwidth = 2; gbc.insets = new Insets(30, 10, 15, 10); + addComponent(centerPanel, startExamButton, gbc); + add(centerPanel, BorderLayout.CENTER); + + JPanel southPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 20, 0)); + southPanel.add(changePasswordButton); + southPanel.add(logoutButton); + add(southPanel, BorderLayout.SOUTH); + + for (UserType type : UserType.values()) { + typeComboBox.addItem(type.getDisplayName()); + } + } + + private void addLabel(JPanel panel, String text, GridBagConstraints gbc) { + JLabel label = new JLabel(text); + label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + panel.add(label, gbc); + } + + private void addComponent(JPanel panel, JComponent comp, GridBagConstraints gbc) { + comp.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + panel.add(comp, gbc); + } + + public void setWelcomeMessage(String message) { welcomeLabel.setText(message); } + public UserType getSelectedType() { + String displayName = (String) typeComboBox.getSelectedItem(); + for (UserType type : UserType.values()) { + if (type.getDisplayName().equals(displayName)) return type; + } + return UserType.PRIMARY; + } + public int getQuestionCount() { return (int) countSpinner.getValue(); } + public JButton getStartExamButton() { return startExamButton; } + public JButton getChangePasswordButton() { return changePasswordButton; } + public JButton getLogoutButton() { return logoutButton; } +} \ No newline at end of file diff --git a/src/view/PasswordSetView.java b/src/view/PasswordSetView.java new file mode 100644 index 0000000..3b5347b --- /dev/null +++ b/src/view/PasswordSetView.java @@ -0,0 +1,54 @@ +package view; + +import javax.swing.*; +import java.awt.*; + +public class PasswordSetView extends JPanel { + private final JPasswordField passwordField = new JPasswordField(20); + private final JPasswordField confirmPasswordField = new JPasswordField(20); + private final JButton submitButton = new JButton("完成注册"); + + public PasswordSetView() { + setLayout(new GridBagLayout()); + JPanel formPanel = new JPanel(new GridBagLayout()); + formPanel.setBorder(BorderFactory.createEmptyBorder(30, 40, 30, 40)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.fill = GridBagConstraints.HORIZONTAL; + + JLabel titleLabel = new JLabel("设置您的密码", SwingConstants.CENTER); + titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28)); + gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; formPanel.add(titleLabel, gbc); + + JLabel hintLabel = new JLabel("密码要求: 6-10位,且必须包含大小写字母和数字。", SwingConstants.CENTER); + hintLabel.setForeground(Color.GRAY); + gbc.gridy = 1; formPanel.add(hintLabel, gbc); + + gbc.gridwidth = 1; + gbc.gridx = 0; gbc.gridy = 2; addLabel(formPanel, "输入密码:", gbc); + gbc.gridx = 1; gbc.gridy = 2; addComponent(formPanel, passwordField, gbc); + + gbc.gridx = 0; gbc.gridy = 3; addLabel(formPanel, "确认密码:", gbc); + gbc.gridx = 1; gbc.gridy = 3; addComponent(formPanel, confirmPasswordField, gbc); + + gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 2; gbc.insets = new Insets(20, 10, 10, 10); + submitButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + addComponent(formPanel, submitButton, gbc); + + add(formPanel); + } + + private void addLabel(JPanel panel, String text, GridBagConstraints gbc) { + JLabel label = new JLabel(text); + label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + panel.add(label, gbc); + } + private void addComponent(JPanel panel, JComponent comp, GridBagConstraints gbc) { + comp.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + panel.add(comp, gbc); + } + + public String getPassword() { return new String(passwordField.getPassword()); } + public String getConfirmPassword() { return new String(confirmPasswordField.getPassword()); } + public JButton getSubmitButton() { return submitButton; } +} \ No newline at end of file diff --git a/src/view/RegisterView.java b/src/view/RegisterView.java new file mode 100644 index 0000000..323de7d --- /dev/null +++ b/src/view/RegisterView.java @@ -0,0 +1,72 @@ +package view; + +import javax.swing.*; +import java.awt.*; + +public class RegisterView extends JPanel { + private final JTextField emailField = new JTextField(20); + private final JTextField usernameField = new JTextField(20); + private final JTextField codeField = new JTextField(10); + private final JButton getCodeButton = new JButton("发送验证邮件"); + private final JButton registerButton = new JButton("下一步:设置密码"); + private final JButton backButton = new JButton("返回登录"); + + public RegisterView() { + setLayout(new GridBagLayout()); + JPanel formPanel = new JPanel(new GridBagLayout()); + formPanel.setBorder(BorderFactory.createEmptyBorder(30, 40, 30, 40)); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.fill = GridBagConstraints.HORIZONTAL; + + JLabel titleLabel = new JLabel("创建您的账号", SwingConstants.CENTER); + titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28)); + gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; formPanel.add(titleLabel, gbc); + + gbc.gridwidth = 1; + gbc.gridx = 0; gbc.gridy = 1; addLabel(formPanel, "邮箱地址:", gbc); + gbc.gridx = 1; gbc.gridy = 1; addComponent(formPanel, emailField, gbc); + + gbc.gridx = 0; gbc.gridy = 2; addLabel(formPanel, "设置用户名:", gbc); + gbc.gridx = 1; gbc.gridy = 2; addComponent(formPanel, usernameField, gbc); + + JPanel codePanel = new JPanel(new BorderLayout(10, 0)); + addComponent(codePanel, codeField, BorderLayout.CENTER); + codePanel.add(getCodeButton, BorderLayout.EAST); + + gbc.gridx = 0; gbc.gridy = 3; addLabel(formPanel, "邮箱验证码:", gbc); + gbc.gridx = 1; gbc.gridy = 3; formPanel.add(codePanel, gbc); + + JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 10, 0)); + registerButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + backButton.putClientProperty("FlatLaf.style", "buttonType: borderless"); + buttonPanel.add(registerButton); + buttonPanel.add(backButton); + gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 2; gbc.insets = new Insets(20, 10, 10, 10); + formPanel.add(buttonPanel, gbc); + + add(formPanel); + } + + private void addLabel(JPanel p, String s, GridBagConstraints g) { + JLabel label = new JLabel(s); + label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + p.add(label, g); + } + private void addComponent(JPanel p, JComponent c, GridBagConstraints g) { + c.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + p.add(c, g); + } + private void addComponent(JPanel p, JComponent c, String pos) { + c.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); + p.add(c, pos); + } + + public String getEmail() { return emailField.getText().trim(); } + public String getUsername() { return usernameField.getText().trim(); } + public String getCode() { return codeField.getText().trim(); } + public JButton getGetCodeButton() { return getCodeButton; } + public JButton getRegisterButton() { return registerButton; } + public JButton getBackButton() { return backButton; } +} \ No newline at end of file diff --git a/src/view/ScoreView.java b/src/view/ScoreView.java new file mode 100644 index 0000000..f389431 --- /dev/null +++ b/src/view/ScoreView.java @@ -0,0 +1,52 @@ +package view; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; + +public class ScoreView extends JPanel { + private final JLabel scoreLabel = new JLabel("0", SwingConstants.CENTER); + private final JLabel scoreUnitLabel = new JLabel("分", SwingConstants.CENTER); + private final JLabel detailLabel = new JLabel(" ", SwingConstants.CENTER); + private final JButton continueButton = new JButton("再来一组"); + private final JButton exitButton = new JButton("退出登录"); + + public ScoreView() { + setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(10, 10, 10, 10); + + JLabel titleLabel = new JLabel("答题结束!", SwingConstants.CENTER); + titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 32)); + add(titleLabel, gbc); + + JPanel scorePanel = new JPanel(); + scoreLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 100)); + scoreUnitLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 30)); + scoreUnitLabel.setBorder(new EmptyBorder(0,0, -50, 0)); + scorePanel.add(scoreLabel); + scorePanel.add(scoreUnitLabel); + add(scorePanel, gbc); + + detailLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20)); + add(detailLabel, gbc); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0)); + continueButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + // 【修复】将 "=" 修改为 ":" + continueButton.putClientProperty("JButton.buttonType", "roundRect"); + buttonPanel.add(continueButton); + buttonPanel.add(exitButton); + gbc.insets = new Insets(30, 10, 10, 10); + add(buttonPanel, gbc); + } + + public void setScore(int score, int correct, int total) { + scoreLabel.setText(String.valueOf(score)); + detailLabel.setText(String.format("答对 %d 题,共 %d 题", correct, total)); + } + + public JButton getContinueButton() { return continueButton; } + public JButton getExitButton() { return exitButton; } +} \ No newline at end of file