|
|
|
|
@ -0,0 +1,346 @@
|
|
|
|
|
// 简单路由与状态管理
|
|
|
|
|
const state = {
|
|
|
|
|
currentEmail: '',
|
|
|
|
|
currentUsername: '',
|
|
|
|
|
grade: '',
|
|
|
|
|
questions: [],
|
|
|
|
|
currentIndex: 0,
|
|
|
|
|
answers: [] // {id, chosen}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function $(sel) { return document.querySelector(sel); }
|
|
|
|
|
function $all(sel) { return Array.from(document.querySelectorAll(sel)); }
|
|
|
|
|
|
|
|
|
|
function showView(id) {
|
|
|
|
|
$all('.view').forEach(v => v.classList.remove('active'));
|
|
|
|
|
$(id).classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
function initLogin() {
|
|
|
|
|
const acc = $('#login-account');
|
|
|
|
|
const pwd = $('#login-password');
|
|
|
|
|
const btn = $('#btn-login');
|
|
|
|
|
const toReg = $('#btn-goto-register');
|
|
|
|
|
if (toReg) toReg.addEventListener('click', () => showView('#view-register'));
|
|
|
|
|
if (btn) btn.addEventListener('click', async () => {
|
|
|
|
|
const account = (acc?.value || '').trim();
|
|
|
|
|
const password = (pwd?.value || '').trim();
|
|
|
|
|
if (!account || !password) { showToast('#login-toast', '请输入账号与密码', false); return; }
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
const res = await window.API.login(account, password);
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
if (!res.ok) { showToast('#login-toast', res.message || '登录失败', false); return; }
|
|
|
|
|
// 登录成功,设置当前信息并显示用户栏
|
|
|
|
|
state.currentEmail = res.data.email;
|
|
|
|
|
state.currentUsername = res.data.username;
|
|
|
|
|
showUserInfo();
|
|
|
|
|
showView('#view-grade');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showToast(id, text, ok = true) {
|
|
|
|
|
const el = $(id);
|
|
|
|
|
el.style.color = ok ? 'var(--success)' : 'var(--danger)';
|
|
|
|
|
el.textContent = text;
|
|
|
|
|
setTimeout(() => { el.textContent = ''; }, 2000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表单校验
|
|
|
|
|
function validateEmail(email) {
|
|
|
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validatePassword(pwd) {
|
|
|
|
|
if (!pwd || pwd.length < 6 || pwd.length > 10) return false;
|
|
|
|
|
const hasUpper = /[A-Z]/.test(pwd);
|
|
|
|
|
const hasLower = /[a-z]/.test(pwd);
|
|
|
|
|
const hasDigit = /\d/.test(pwd);
|
|
|
|
|
return hasUpper && hasLower && hasDigit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initRegister() {
|
|
|
|
|
const emailInput = $('#reg-email');
|
|
|
|
|
const usernameInput = $('#reg-username');
|
|
|
|
|
const codeInput = $('#reg-code');
|
|
|
|
|
const emailHint = $('#reg-email-hint');
|
|
|
|
|
const usernameHint = $('#reg-username-hint');
|
|
|
|
|
|
|
|
|
|
$('#btn-send-code').addEventListener('click', async () => {
|
|
|
|
|
const email = emailInput.value.trim();
|
|
|
|
|
if (!validateEmail(email)) {
|
|
|
|
|
emailHint.textContent = '邮箱格式不正确';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
emailHint.textContent = '';
|
|
|
|
|
const username = (usernameInput?.value || '').trim();
|
|
|
|
|
if (!username) { if (usernameHint) usernameHint.textContent = '请先填写用户名'; return; }
|
|
|
|
|
const res = await window.API.sendRegisterCode(email, username);
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
showToast('#register-toast', res.message || '验证码已发送', true);
|
|
|
|
|
} else {
|
|
|
|
|
showToast('#register-toast', res.message || '发送失败', false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$('#btn-register').addEventListener('click', async () => {
|
|
|
|
|
const email = emailInput.value.trim();
|
|
|
|
|
const username = (usernameInput?.value || '').trim();
|
|
|
|
|
const code = codeInput.value.trim();
|
|
|
|
|
if (!validateEmail(email)) {
|
|
|
|
|
emailHint.textContent = '邮箱格式不正确';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!username) { if (usernameHint) usernameHint.textContent = '用户名不能为空'; return; }
|
|
|
|
|
if (!code) {
|
|
|
|
|
showToast('#register-toast', '请输入验证码', false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log('[UI] 注册提交:', { email, username, code });
|
|
|
|
|
const res = await window.API.register(email, username, code);
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
console.warn('[UI] 注册失败:', res);
|
|
|
|
|
showToast('#register-toast', res.message || '注册失败', false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log('[UI] 注册成功, 进入设置密码');
|
|
|
|
|
state.currentEmail = email;
|
|
|
|
|
state.currentUsername = username;
|
|
|
|
|
showView('#view-set-password');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initSetPassword() {
|
|
|
|
|
const p1 = $('#pwd-1');
|
|
|
|
|
const p2 = $('#pwd-2');
|
|
|
|
|
const hint = $('#pwd-hint');
|
|
|
|
|
$('#btn-set-password').addEventListener('click', async () => {
|
|
|
|
|
const a = p1.value;
|
|
|
|
|
const b = p2.value;
|
|
|
|
|
if (!validatePassword(a)) {
|
|
|
|
|
hint.textContent = '密码需6-10位且含大小写字母与数字';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (a !== b) {
|
|
|
|
|
hint.textContent = '两次输入不一致';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
hint.textContent = '';
|
|
|
|
|
const res = await window.API.setPassword(state.currentEmail, a);
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
showToast('#setpwd-toast', '设置成功');
|
|
|
|
|
// 密码设置成功后,显示用户信息和操作按钮
|
|
|
|
|
showUserInfo();
|
|
|
|
|
showView('#view-grade');
|
|
|
|
|
} else {
|
|
|
|
|
showToast('#setpwd-toast', res.message || '设置失败', false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initGradeSelect() {
|
|
|
|
|
$all('.grade-card').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
state.grade = btn.getAttribute('data-grade');
|
|
|
|
|
$('#count-title').textContent = `准备生成${state.grade}数学题目,请输入题目数量(10-30)`;
|
|
|
|
|
showView('#view-count');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initCount() {
|
|
|
|
|
const input = $('#question-count');
|
|
|
|
|
const hint = $('#count-hint');
|
|
|
|
|
$('#btn-generate').addEventListener('click', async () => {
|
|
|
|
|
const n = Number(input.value);
|
|
|
|
|
if (!Number.isInteger(n) || n < 10 || n > 30) {
|
|
|
|
|
hint.textContent = '题目数量需在10-30之间';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
hint.textContent = '';
|
|
|
|
|
const res = await window.API.getQuestions(state.grade, n);
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
showToast('#count-hint', '生成题目失败', false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
state.questions = res.data;
|
|
|
|
|
state.currentIndex = 0;
|
|
|
|
|
state.answers = [];
|
|
|
|
|
startQuiz();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderCurrentQuestion() {
|
|
|
|
|
const q = state.questions[state.currentIndex];
|
|
|
|
|
$('#quiz-progress').textContent = `第 ${state.currentIndex + 1}/${state.questions.length} 题`;
|
|
|
|
|
$('#quiz-grade').textContent = state.grade;
|
|
|
|
|
$('#quiz-question').textContent = q.stem;
|
|
|
|
|
const list = $('#quiz-options');
|
|
|
|
|
list.innerHTML = '';
|
|
|
|
|
q.options.forEach(opt => {
|
|
|
|
|
const li = document.createElement('li');
|
|
|
|
|
li.innerHTML = `<label><input type="radio" name="opt" value="${opt.key}" /> <strong>${opt.key}.</strong> ${opt.text}</label>`;
|
|
|
|
|
list.appendChild(li);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startQuiz() {
|
|
|
|
|
showView('#view-quiz');
|
|
|
|
|
renderCurrentQuestion();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initQuiz() {
|
|
|
|
|
$('#btn-submit-next').addEventListener('click', () => {
|
|
|
|
|
const q = state.questions[state.currentIndex];
|
|
|
|
|
const chosen = document.querySelector('input[name="opt"]:checked');
|
|
|
|
|
if (!chosen) {
|
|
|
|
|
alert('请选择一个选项');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
state.answers.push({ id: q.id, chosen: chosen.value, correct: q.answer });
|
|
|
|
|
if (state.currentIndex < state.questions.length - 1) {
|
|
|
|
|
state.currentIndex++;
|
|
|
|
|
renderCurrentQuestion();
|
|
|
|
|
} else {
|
|
|
|
|
finishQuiz();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function finishQuiz() {
|
|
|
|
|
const total = state.questions.length;
|
|
|
|
|
const right = state.answers.filter(a => a.chosen === a.correct).length;
|
|
|
|
|
const score = Math.round((right / total) * 100);
|
|
|
|
|
$('#score-text').textContent = `${score} 分`;
|
|
|
|
|
showView('#view-result');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initResult() {
|
|
|
|
|
$('#btn-continue').addEventListener('click', () => {
|
|
|
|
|
showView('#view-grade');
|
|
|
|
|
});
|
|
|
|
|
$('#btn-exit').addEventListener('click', () => {
|
|
|
|
|
// 退出回登录页并清理状态
|
|
|
|
|
state.currentEmail = '';
|
|
|
|
|
state.currentUsername = '';
|
|
|
|
|
state.grade = '';
|
|
|
|
|
state.questions = [];
|
|
|
|
|
state.currentIndex = 0;
|
|
|
|
|
state.answers = [];
|
|
|
|
|
const userBox = document.getElementById('user-box');
|
|
|
|
|
const userActions = document.getElementById('user-actions');
|
|
|
|
|
if (userBox) userBox.style.display = 'none';
|
|
|
|
|
if (userActions) userActions.style.display = 'none';
|
|
|
|
|
showView('#view-login');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initChangePasswordModal() {
|
|
|
|
|
const mask = $('#modal-mask');
|
|
|
|
|
$('#btn-change-password').addEventListener('click', () => {
|
|
|
|
|
$('#old-pwd').value = '';
|
|
|
|
|
$('#new-pwd-1').value = '';
|
|
|
|
|
$('#new-pwd-2').value = '';
|
|
|
|
|
$('#change-pwd-hint').textContent = '';
|
|
|
|
|
mask.style.display = 'flex';
|
|
|
|
|
});
|
|
|
|
|
$('#btn-cancel-change').addEventListener('click', () => {
|
|
|
|
|
mask.style.display = 'none';
|
|
|
|
|
});
|
|
|
|
|
$('#btn-confirm-change').addEventListener('click', async () => {
|
|
|
|
|
const oldPwd = $('#old-pwd').value;
|
|
|
|
|
const p1 = $('#new-pwd-1').value;
|
|
|
|
|
const p2 = $('#new-pwd-2').value;
|
|
|
|
|
const hint = $('#change-pwd-hint');
|
|
|
|
|
if (!validatePassword(p1)) { hint.textContent = '新密码不符合规则'; return; }
|
|
|
|
|
if (p1 !== p2) { hint.textContent = '两次新密码不一致'; return; }
|
|
|
|
|
const res = await window.API.changePassword(state.currentEmail, oldPwd, p1);
|
|
|
|
|
if (!res.ok) { hint.textContent = res.message || '修改失败'; return; }
|
|
|
|
|
mask.style.display = 'none';
|
|
|
|
|
alert('修改成功');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 修改用户名
|
|
|
|
|
function showUserInfo() {
|
|
|
|
|
// 显示用户信息(头像+用户名)
|
|
|
|
|
const userBox = document.getElementById('user-box');
|
|
|
|
|
const userActions = document.getElementById('user-actions');
|
|
|
|
|
const userName = document.getElementById('user-name');
|
|
|
|
|
const userAvatar = document.getElementById('user-avatar');
|
|
|
|
|
|
|
|
|
|
if (userBox && userActions && userName && userAvatar) {
|
|
|
|
|
userBox.style.display = 'flex';
|
|
|
|
|
userActions.style.display = 'flex';
|
|
|
|
|
userName.textContent = state.currentUsername || '用户';
|
|
|
|
|
// 默认头像
|
|
|
|
|
userAvatar.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="%23222"/><circle cx="32" cy="24" r="14" fill="%23555"/><rect x="14" y="40" width="36" height="16" rx="8" fill="%23555"/></svg>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initChangeUsernameModal() {
|
|
|
|
|
const mask = $('#modal-mask-username');
|
|
|
|
|
const btn = $('#btn-change-username');
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
$('#new-username').value = '';
|
|
|
|
|
$('#change-username-hint').textContent = '';
|
|
|
|
|
mask.style.display = 'flex';
|
|
|
|
|
});
|
|
|
|
|
$('#btn-cancel-username').addEventListener('click', () => {
|
|
|
|
|
mask.style.display = 'none';
|
|
|
|
|
});
|
|
|
|
|
$('#btn-confirm-username').addEventListener('click', async () => {
|
|
|
|
|
const newName = ($('#new-username')?.value || '').trim();
|
|
|
|
|
const hint = $('#change-username-hint');
|
|
|
|
|
if (!newName) { hint.textContent = '新用户名不能为空'; return; }
|
|
|
|
|
const res = await window.API.changeUsername(state.currentEmail, newName);
|
|
|
|
|
if (!res.ok) { hint.textContent = res.message || '修改失败'; return; }
|
|
|
|
|
mask.style.display = 'none';
|
|
|
|
|
alert('用户名修改成功');
|
|
|
|
|
// 更新显示的用户名
|
|
|
|
|
state.currentUsername = newName;
|
|
|
|
|
const userName = document.getElementById('user-name');
|
|
|
|
|
if (userName) userName.textContent = newName;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 头像上传功能
|
|
|
|
|
function initAvatarUpload() {
|
|
|
|
|
const avatarContainer = document.querySelector('.avatar-container');
|
|
|
|
|
const avatarInput = document.getElementById('avatar-input');
|
|
|
|
|
const userAvatar = document.getElementById('user-avatar');
|
|
|
|
|
|
|
|
|
|
if (avatarContainer && avatarInput && userAvatar) {
|
|
|
|
|
avatarContainer.addEventListener('click', () => {
|
|
|
|
|
avatarInput.click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
avatarInput.addEventListener('change', (e) => {
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
userAvatar.src = e.target.result;
|
|
|
|
|
// 这里可以调用后端接口上传头像
|
|
|
|
|
console.log('头像已选择,可上传到后端');
|
|
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 启动
|
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
// 默认进入登录页
|
|
|
|
|
initRegister();
|
|
|
|
|
initSetPassword();
|
|
|
|
|
initGradeSelect();
|
|
|
|
|
initCount();
|
|
|
|
|
initQuiz();
|
|
|
|
|
initResult();
|
|
|
|
|
initChangePasswordModal();
|
|
|
|
|
initChangeUsernameModal();
|
|
|
|
|
initAvatarUpload();
|
|
|
|
|
initLogin();
|
|
|
|
|
});
|
|
|
|
|
|