合并前端内容 #2

Merged
pijtbanfl merged 2 commits from wangqisheng_branch into main 6 months ago

@ -1,2 +0,0 @@
# Pair

File diff suppressed because it is too large Load Diff

@ -0,0 +1,252 @@
// HTTP API接口封装支持测试模式和真实后端
(function() {
const BASE_URL = 'http://localhost:8080'; // 设置为后端地址,如 'http://127.0.0.1:8080'
const TEST_MODE = false; // 设为 false 使用真实后端
// 测试数据存储(注:这些数据仅在测试模式下使用,不会存储到后端)
const testData = {
codes: {}, // key: email, value: code
users: {}, // key: email, value: { email, username, password }
usernames: {}, // key: username(lowercased), value: email
};
// 生成6位数字验证码
function generateCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// 生成测试题目(测试模式使用)
function generateQuestions(grade, count) {
const questions = [];
const opsByGrade = {
'初中': ['+', '-', '×', '÷'],
'小学': ['+', '-'],
'高中': ['+', '-', '×', '÷'],
};
const ops = opsByGrade[grade] || ['+', '-'];
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function makeExpr() {
const operands = randInt(2, 4);
let expr = '';
for (let i = 0; i < operands; i++) {
const n = randInt(1, 100);
expr += (i === 0 ? '' : ` ${ops[randInt(0, ops.length - 1)]} `) + n;
}
return expr;
}
const used = new Set();
while (questions.length < count) {
const expr = makeExpr();
if (used.has(expr)) continue;
used.add(expr);
const evalExpr = expr.replace(/×/g, '*').replace(/÷/g, '/');
let answer = 0;
try {
answer = Math.round(eval(evalExpr));
} catch (e) {
continue;
}
const correct = answer;
const candidates = new Set([correct]);
while (candidates.size < 4) {
candidates.add(correct + randInt(-10, 10));
}
const options = Array.from(candidates).sort(() => Math.random() - 0.5).map((v, idx) => ({
key: String.fromCharCode(65 + idx),
text: String(v)
}));
const correctKey = options.find(o => Number(o.text) === correct)?.key;
questions.push({
id: `${grade}-${questions.length + 1}`,
stem: `计算:${expr}`,
options,
answer: correctKey
});
}
return questions;
}
async function post(path, body) {
if (TEST_MODE) {
// 测试模式:模拟延迟
await new Promise(resolve => setTimeout(resolve, 500));
return { ok: true, message: '测试模式' };
}
const res = await fetch(BASE_URL + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || {})
});
return await res.json();
}
async function get(path) {
if (TEST_MODE) {
await new Promise(resolve => setTimeout(resolve, 500));
return { ok: true, data: [], message: '测试模式' };
}
const res = await fetch(BASE_URL + path);
return await res.json();
}
// 统一的网络错误处理
function handleNetworkError(error) {
console.error('网络请求失败:', error);
return { ok: false, message: '网络异常' };
}
window.API = {
async sendRegisterCode(email, username) {
if (TEST_MODE) {
if (!username || !username.trim()) {
return { ok: false, message: '用户名不能为空' };
}
const unameKey = username.trim().toLowerCase();
if (testData.usernames[unameKey]) {
return { ok: false, message: '用户名已存在,请更换' };
}
const code = generateCode();
testData.codes[email] = code;
console.log(`[测试模式] 验证码发送到 ${email}: ${code}`);
return { ok: true, message: `验证码已发送(测试模式):${code}` };
}
try {
const data = await post('/api/send-code', { email, username });
return { ok: !!data.ok, message: data.message };
}
catch (e) { return handleNetworkError(e); }
},
async verifyCode(email, username, code) {
if (TEST_MODE) {
if (testData.codes[email] === code) {
if (!username || !username.trim()) {
return { ok: false, message: '用户名不能为空' };
}
const unameKey = username.trim().toLowerCase();
if (testData.usernames[unameKey]) {
return { ok: false, message: '用户名已存在,请更换' };
}
// 只验证,不创建用户
delete testData.codes[email];
return { ok: true, message: '验证成功,请设置密码' };
}
return { ok: false, message: '验证码错误' };
}
try {
const data = await post('/api/verify-code', { email, username, code });
return { ok: !!data.ok, message: data.message };
}
catch (e) { return handleNetworkError(e); }
},
async register(email, username, password) {
if (TEST_MODE) {
// 创建用户并设置密码
const unameKey = username.trim().toLowerCase();
testData.users[email] = { email, username, password };
testData.usernames[unameKey] = email;
return { ok: true, message: '注册成功' };
}
try {
const data = await post('/api/register', { email, username, password });
return { ok: !!data.ok, message: data.message };
}
catch (e) { return handleNetworkError(e); }
},
async setPassword(email, username, password) {
if (TEST_MODE) {
// 先创建用户,再设置密码(与后端逻辑一致)
if (!testData.users[email]) {
const unameKey = username.trim().toLowerCase();
testData.users[email] = { email, username, registered: true };
testData.usernames[unameKey] = email;
}
testData.users[email].password = password;
return { ok: true, message: '密码设置成功' };
}
try { const data = await post('/api/set-password', { email, username, password }); return { ok: !!data.ok, message: data.message }; }
catch (e) { return handleNetworkError(e); }
},
async changePassword(email, oldPassword, newPassword) {
if (TEST_MODE) {
if (testData.users[email] && testData.users[email].password === oldPassword) {
testData.users[email].password = newPassword;
return { ok: true, message: '密码修改成功' };
}
return { ok: false, message: '原密码错误' };
}
try { const data = await post('/api/change-password', { email, oldPassword, newPassword }); return { ok: !!data.ok, message: data.message }; }
catch (e) { return handleNetworkError(e); }
},
async changeUsername(email, newUsername) {
if (TEST_MODE) {
if (!testData.users[email]) return { ok: false, message: '用户未注册' };
if (!newUsername || !newUsername.trim()) return { ok: false, message: '用户名不能为空' };
const key = newUsername.trim().toLowerCase();
if (testData.usernames[key]) return { ok: false, message: '用户名已存在,请更换' };
// 删除旧映射并写入新映射
const old = testData.users[email].username;
if (old) delete testData.usernames[old.trim().toLowerCase()];
testData.users[email].username = newUsername;
testData.usernames[key] = email;
return { ok: true, message: '用户名修改成功' };
}
try { const data = await post('/api/change-username', { email, username: newUsername }); return { ok: !!data.ok, message: data.message }; }
catch (e) { return handleNetworkError(e); }
},
async deleteAccount(email, password) {
if (TEST_MODE) {
if (!testData.users[email]) return { ok: false, message: '用户不存在' };
if (testData.users[email].password !== password) return { ok: false, message: '密码不正确' };
// 先获取用户名,再删除用户
const username = testData.users[email].username;
delete testData.users[email];
// 删除用户名映射
if (username) delete testData.usernames[username.trim().toLowerCase()];
return { ok: true, message: '账号删除成功' };
}
try { const data = await post('/api/delete-account', { email, password }); return { ok: !!data.ok, message: data.message }; }
catch (e) { return handleNetworkError(e); }
},
async getQuestions(grade, count) {
if (TEST_MODE) {
const questions = generateQuestions(grade, count);
return { ok: true, data: questions, message: '题目生成成功' };
}
try { const data = await get(`/api/questions?grade=${encodeURIComponent(grade)}&count=${count}`); return { ok: !!data.ok, data: data.data, message: data.message }; }
catch (e) { return handleNetworkError(e); }
}
};
// 登录接口(测试模式 + HTTP
window.API.login = async function(account, password) {
if (TEST_MODE) {
if (!account || !password) return { ok: false, message: '请输入账号与密码' };
// 允许邮箱或用户名登录
let email = null;
if (testData.users[account]) {
email = account;
} else {
const key = account.trim().toLowerCase();
if (testData.usernames[key]) email = testData.usernames[key];
}
if (!email || !testData.users[email]) return { ok: false, message: '账号不存在' };
if (testData.users[email].password !== password) return { ok: false, message: '密码不正确' };
return { ok: true, data: { email, username: testData.users[email].username } };
}
try {
const data = await post('/api/login', { account, password });
return { ok: !!data.ok, data: data.data, message: data.message };
} catch(e) {
return handleNetworkError(e);
}
};
})();

@ -0,0 +1,153 @@
// 前端Mock API使用localStorage模拟后端接口
// 说明真实对接时可替换为基于fetch的HTTP调用
(function() {
const STORAGE_KEY = 'mock_users_v1';
// 从localStorage加载用户数据
function loadUsers() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch (e) {
return {};
}
}
// 保存用户数据到localStorage
function saveUsers(map) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
}
// 简单的字符串哈希函数(仅用于演示)
function hash(str) {
// 简化的哈希,仅演示,勿用于生产
let h = 0;
for (let i = 0; i < str.length; i++) {
h = ((h << 5) - h) + str.charCodeAt(i);
h |= 0;
}
return 'h' + Math.abs(h);
}
function generateCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
function ensureUser(email) {
const users = loadUsers();
if (!users[email]) {
users[email] = {code: null, email, passwordHash: null};
saveUsers(users);
}
}
const MockAPI = {
async sendRegisterCode(email) {
ensureUser(email);
const code = generateCode();
const users = loadUsers();
users[email].code = code;
saveUsers(users);
console.log('[Mock] 向邮箱发送验证码:', email, code);
return {ok: true};
},
async register(email, code) {
const users = loadUsers();
if (!users[email] || users[email].code !== code) {
return {ok: false, message: '验证码错误或邮箱未请求验证码'};
}
return {ok: true};
},
async setPassword(email, password) {
const users = loadUsers();
if (!users[email]) {
return {ok: false, message: '邮箱未注册'};
}
users[email].passwordHash = hash(password);
saveUsers(users);
return {ok: true};
},
async changePassword(email, oldPassword, newPassword) {
const users = loadUsers();
const u = users[email];
if (!u || !u.passwordHash) {
return {ok: false, message: '尚未设置密码'};
}
if (u.passwordHash !== hash(oldPassword)) {
return {ok: false, message: '原密码不正确'};
}
u.passwordHash = hash(newPassword);
saveUsers(users);
return {ok: true};
},
async getQuestions(grade, count) {
// 生成选择题,避免重复(同卷内)
const questions = [];
const used = new Set();
const opsByGrade = {
'小学': ['+', '-'],
'初中': ['+', '-', '×', '÷'],
'高中': ['+', '-', '×', '÷']
};
const ops = opsByGrade[grade] || ['+', '-'];
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function makeExpr() {
const operands = randInt(2, 4);
let expr = '';
for (let i = 0; i < operands; i++) {
const n = randInt(1, 100);
expr += (i === 0 ? '' : ` ${ops[randInt(0, ops.length - 1)]} `) + n;
}
return expr;
}
while (questions.length < count) {
const expr = makeExpr();
if (used.has(expr)) {
continue;
}
used.add(expr);
// 计算结果(仅演示,× ÷ 替换为 * /
const evalExpr = expr.replace(/×/g, '*').replace(/÷/g, '/');
let answer = 0;
try {
answer = Math.round(eval(evalExpr));
} catch (e) {
continue;
}
const correct = answer;
const candidates = new Set([correct]);
while (candidates.size < 4) {
candidates.add(correct + randInt(-10, 10));
}
const options = Array.from(candidates).sort(() => Math.random() - 0.5).map((v, idx) => ({
key: String.fromCharCode(65 + idx),
text: String(v)
}));
const correctKey = options.find(o => Number(o.text) === correct)?.key;
questions.push({
id: `${grade}-${questions.length + 1}`,
stem: `计算:${expr}`,
options,
answer: correctKey
});
}
return {ok: true, data: questions};
}
};
window.API = MockAPI;
})();

@ -0,0 +1,48 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1000,
height: 680,
minWidth: 860,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile(path.join(__dirname, 'index.html'));
// mainWindow.webContents.openDevTools();
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
// 窗口关闭请求(用于“退出应用”)
ipcMain.handle('app:close', () => {
if (mainWindow) {
mainWindow.close();
}
});

@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小初高数学学习软件</title>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div id="app">
<header class="app-header">
<div class="app-title">小初高数学学习软件</div>
<div class="app-actions">
<div id="user-box" class="user-box" style="display:none;">
<div class="avatar-container">
<img id="user-avatar" class="avatar" src="" alt="avatar">
<input type="file" id="avatar-input" accept="image/*" style="display:none;">
</div>
<span id="user-name" class="user-name"></span>
</div>
<div id="user-actions" class="user-actions" style="display:none;">
<button id="btn-change-username" class="link">修改用户名</button>
<button id="btn-change-password" class="link">修改密码</button>
<button id="btn-delete-account" class="link danger">删除账号</button>
</div>
</div>
</header>
<main class="view-container">
<!-- 登录页 -->
<section id="view-login" class="view active">
<h2>登录</h2>
<div class="form-group">
<label for="login-account">邮箱或用户名</label>
<input type="text" id="login-account" placeholder="请输入邮箱或用户名">
</div>
<div class="form-group">
<label for="login-password">密码</label>
<input type="password" id="login-password" placeholder="请输入密码">
</div>
<div class="form-row">
<button id="btn-login" class="primary">登录</button>
<button id="btn-goto-register" class="secondary">没有账号?去注册</button>
</div>
<div class="toast" id="login-toast"></div>
</section>
<!-- 注册页 -->
<section id="view-register" class="view">
<h2>注册</h2>
<div class="form-group">
<label for="reg-username">用户名(必填,需唯一)</label>
<input type="text" id="reg-username" placeholder="请输入用户名">
<div class="hint" id="reg-username-hint"></div>
</div>
<div class="form-group">
<label for="reg-email">邮箱</label>
<input type="email" id="reg-email" placeholder="请输入邮箱">
<div class="hint" id="reg-email-hint"></div>
</div>
<div class="form-row">
<div class="form-group flex-1" style="margin-bottom:0;">
<label for="reg-code">验证码</label>
<input type="text" id="reg-code" placeholder="请输入验证码">
</div>
<button id="btn-send-code" class="secondary">获取验证码</button>
</div>
<div class="form-row">
<button id="btn-register" class="primary">注册</button>
<button id="btn-back-to-login-from-register" class="secondary">返回登录</button>
</div>
<div class="toast" id="register-toast"></div>
</section>
<!-- 设置密码页 -->
<section id="view-set-password" class="view">
<h2>设置密码</h2>
<div class="form-group">
<label for="pwd-1">密码6-10位含大小写字母和数字</label>
<input type="password" id="pwd-1" placeholder="请输入密码">
</div>
<div class="form-group">
<label for="pwd-2">重复密码</label>
<input type="password" id="pwd-2" placeholder="请再次输入密码">
<div class="hint" id="pwd-hint"></div>
</div>
<div class="form-row">
<button id="btn-set-password" class="primary">确认设置</button>
<button id="btn-back-to-register-from-password" class="secondary">返回注册</button>
<button id="btn-back-to-login-from-password" class="secondary">返回登录</button>
</div>
<div class="toast" id="setpwd-toast"></div>
</section>
<!-- 年级选择页 -->
<section id="view-grade" class="view">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>选择年级</h2>
<button id="btn-back-to-login-from-grade" class="secondary">返回登录</button>
</div>
<div class="grade-grid">
<button class="grade-card" data-grade="小学">小学</button>
<button class="grade-card" data-grade="初中">初中</button>
<button class="grade-card" data-grade="高中">高中</button>
</div>
</section>
<!-- 输入题数页 -->
<section id="view-count" class="view">
<h2 id="count-title">请输入题目数量10-30</h2>
<div class="form-group small">
<input type="number" id="question-count" min="10" max="30" placeholder="10-30">
<div class="hint" id="count-hint"></div>
</div>
<div class="form-row">
<button id="btn-generate" class="primary">生成试卷</button>
<button id="btn-back-to-grade-from-count" class="secondary">返回选择年级</button>
</div>
</section>
<!-- 答题页 -->
<section id="view-quiz" class="view">
<div class="quiz-header">
<div id="quiz-progress">第 1/1 题</div>
<div id="quiz-grade"></div>
</div>
<div class="quiz-card">
<div class="question" id="quiz-question">题干加载中...</div>
<ul class="options" id="quiz-options"></ul>
<div class="quiz-actions">
<button id="btn-submit-next" class="primary">提交 / 下一题</button>
<button id="btn-prev" class="secondary">上一题</button>
</div>
</div>
</section>
<!-- 评分页 -->
<section id="view-result" class="view">
<h2>成绩</h2>
<div class="score" id="score-text">0 分</div>
<div class="result-actions">
<button id="btn-continue" class="primary">继续做题</button>
<button id="btn-exit" class="danger">退出</button>
</div>
</section>
</main>
<!-- 修改密码弹窗 -->
<div id="modal-mask" class="modal-mask" style="display:none;">
<div class="modal">
<h3>修改密码</h3>
<div class="form-group">
<label for="old-pwd">原密码</label>
<input type="password" id="old-pwd">
</div>
<div class="form-group">
<label for="new-pwd-1">新密码</label>
<input type="password" id="new-pwd-1" placeholder="6-10位含大小写字母和数字">
</div>
<div class="form-group">
<label for="new-pwd-2">重复新密码</label>
<input type="password" id="new-pwd-2">
<div class="hint" id="change-pwd-hint"></div>
</div>
<div class="modal-actions">
<button id="btn-confirm-change" class="primary">确认修改</button>
<button id="btn-cancel-change" class="secondary">取消</button>
</div>
</div>
</div>
<!-- 修改用户名弹窗 -->
<div id="modal-mask-username" class="modal-mask" style="display:none;">
<div class="modal">
<h3>修改用户名</h3>
<div class="form-group">
<label for="new-username">新用户名</label>
<input type="text" id="new-username" placeholder="请输入新用户名">
<div class="hint" id="change-username-hint"></div>
</div>
<div class="modal-actions">
<button id="btn-confirm-username" class="primary">确认修改</button>
<button id="btn-cancel-username" class="secondary">取消</button>
</div>
</div>
</div>
<!-- 删除账号确认弹窗 -->
<div id="modal-mask-delete" class="modal-mask" style="display:none;">
<div class="modal">
<h3>删除账号</h3>
<div class="form-group">
<p style="color: #e74c3c; margin-bottom: 15px;">
⚠️ 警告:此操作将永久删除您的账号和所有相关数据,无法恢复!
</p>
<p>请输入您的密码以确认删除:</p>
<input type="password" id="delete-password" placeholder="请输入密码确认">
<div class="hint" id="delete-hint"></div>
</div>
<div class="modal-actions">
<button id="btn-confirm-delete" class="danger">确认删除</button>
<button id="btn-cancel-delete" class="secondary">取消</button>
</div>
</div>
</div>
</div>
<!-- 自定义提示框 -->
<div id="custom-alert-mask" class="modal-mask" style="display:none;">
<div class="modal">
<h3 id="custom-alert-title">提示</h3>
<div class="form-group">
<p id="custom-alert-message"></p>
</div>
<div class="modal-actions">
<button id="custom-alert-ok" class="primary">确定</button>
</div>
</div>
</div>
<script src="./api/httpApi.js"></script>
<script src="./renderer.js"></script>
<noscript>需要启用 JavaScript</noscript>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,44 @@
{
"name": "math-learning-frontend",
"version": "7.0.0",
"private": true,
"description": "小初高数学学习软件 - 桌面前端Electron",
"main": "electron-main.js",
"scripts": {
"start": "electron .",
"dev": "electron .",
"build": "electron-builder --win --x64"
},
"author": "Pair Frontend",
"license": "MIT",
"devDependencies": {
"electron": "^31.2.1",
"electron-builder": "^24.13.3"
},
"build": {
"appId": "com.math.learning.app",
"productName": "数学学习软件",
"files": [
"index.html",
"styles.css",
"renderer.js",
"electron-main.js",
"preload.js",
"api/**/*"
],
"win": {
"target": ["nsis"],
"artifactName": "${productName}-${version}-Setup.${ext}",
"signAndEditExecutable": false
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"runAfterFinish": true,
"perMachine": false
}
}
}

@ -0,0 +1,6 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
closeApp: () => ipcRenderer.invoke('app:close')
});

@ -0,0 +1,719 @@
// 应用状态管理
const state = {
answers: [], // {id, chosen}
currentEmail: '',
currentIndex: 0,
currentUsername: '',
grade: '',
questions: [],
};
// 获取单个DOM元素
function $(sel) {
return document.querySelector(sel);
}
// 获取多个DOM元素
function $all(sel) {
return Array.from(document.querySelectorAll(sel));
}
// 显示指定的视图页面
function showView(id) {
$all('.view').forEach((v) => v.classList.remove('active'));
$(id).classList.add('active');
// 离开选择题目数量界面时清空输入框
if (id !== '#view-count') {
const input = $('#question-count');
const hint = $('#count-hint');
if (input) input.value = '';
if (hint) hint.textContent = '';
}
}
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);
}
// 自定义alert函数
function customAlert(message, title = '提示') {
return new Promise((resolve) => {
const mask = $('#custom-alert-mask');
const titleEl = $('#custom-alert-title');
const messageEl = $('#custom-alert-message');
const okBtn = $('#custom-alert-ok');
if (titleEl) titleEl.textContent = title;
if (messageEl) messageEl.textContent = message;
if (mask) mask.style.display = 'flex';
// 绑定确定按钮事件
const handleOk = () => {
if (mask) mask.style.display = 'none';
okBtn.removeEventListener('click', handleOk);
resolve();
};
if (okBtn) {
okBtn.addEventListener('click', handleOk);
}
});
}
// 自定义confirm函数
function customConfirm(message, title = '确认') {
return new Promise((resolve) => {
const mask = $('#custom-alert-mask');
const titleEl = $('#custom-alert-title');
const messageEl = $('#custom-alert-message');
const okBtn = $('#custom-alert-ok');
if (titleEl) titleEl.textContent = title;
if (messageEl) messageEl.textContent = message;
if (mask) mask.style.display = 'flex';
// 修改按钮文本和样式
if (okBtn) {
okBtn.textContent = '确定';
okBtn.className = 'primary';
}
// 添加取消按钮
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '取消';
cancelBtn.className = 'secondary';
cancelBtn.style.marginRight = '8px';
const modalActions = mask.querySelector('.modal-actions');
if (modalActions) {
modalActions.insertBefore(cancelBtn, okBtn);
}
// 绑定事件
const handleOk = () => {
cleanup();
resolve(true);
};
const handleCancel = () => {
cleanup();
resolve(false);
};
const cleanup = () => {
if (mask) mask.style.display = 'none';
okBtn.removeEventListener('click', handleOk);
cancelBtn.removeEventListener('click', handleCancel);
if (modalActions && cancelBtn.parentNode) {
modalActions.removeChild(cancelBtn);
}
};
if (okBtn) okBtn.addEventListener('click', handleOk);
cancelBtn.addEventListener('click', handleCancel);
});
}
// 清理用户状态
function clearUserState() {
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';
}
// 表单校验
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');
const sendCodeBtn = $('#btn-send-code');
// 验证码发送冷却时间管理
let cooldownTimer = null;
function startCooldown(seconds) {
sendCodeBtn.disabled = true;
sendCodeBtn.textContent = `重新发送 (${seconds}s)`;
cooldownTimer = setInterval(() => {
seconds--;
if (seconds <= 0) {
clearInterval(cooldownTimer);
cooldownTimer = null;
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = '获取验证码';
} else {
sendCodeBtn.textContent = `重新发送 (${seconds}s)`;
}
}, 1000);
}
$('#btn-send-code').addEventListener('click', async () => {
// 如果正在冷却中,直接返回
if (sendCodeBtn.disabled) {
return;
}
const email = emailInput.value.trim();
if (!validateEmail(email)) {
emailHint.textContent = '邮箱格式不正确';
return;
}
// 限制邮箱域名只允许QQ邮箱和163邮箱
if (!email.includes('@qq.com') && !email.includes('@163.com')) {
emailHint.textContent = '只支持QQ邮箱和163邮箱';
return;
}
emailHint.textContent = '';
const username = (usernameInput?.value || '').trim();
if (!username) { if (usernameHint) usernameHint.textContent = '请先填写用户名'; return; }
// 立即禁用按钮,防止重复点击
sendCodeBtn.disabled = true;
sendCodeBtn.textContent = '发送中...';
try {
const res = await window.API.sendRegisterCode(email, username);
if (res.ok) {
showToast('#register-toast', res.message || '验证码已发送', true);
// 开始60秒冷却
startCooldown(60);
} else {
showToast('#register-toast', res.message || '发送失败', false);
// 恢复按钮状态
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = '获取验证码';
}
} catch (error) {
// 网络错误等异常情况,恢复按钮状态
showToast('#register-toast', '网络错误,请重试', false);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = '获取验证码';
}
});
$('#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.verifyCode(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;
state.currentCode = code;
// 验证码验证成功时不立即清空注册页面内容,保持用户填写的信息
showView('#view-set-password');
});
// 添加返回登录页功能
$('#btn-back-to-login-from-register').addEventListener('click', () => {
// 清理状态和输入内容
clearUserState();
$('#reg-username').value = '';
$('#reg-email').value = '';
$('#reg-code').value = '';
$('#reg-username-hint').textContent = '';
$('#reg-email-hint').textContent = '';
showView('#view-login');
});
}
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.register(state.currentEmail, state.currentUsername, a);
if (res.ok) {
showToast('#setpwd-toast', '注册成功');
// 清空设置密码页面的输入内容
$('#pwd-1').value = '';
$('#pwd-2').value = '';
$('#pwd-hint').textContent = '';
// 密码设置成功后,清空注册页面内容(完成整个注册流程)
$('#reg-username').value = '';
$('#reg-email').value = '';
$('#reg-code').value = '';
$('#reg-username-hint').textContent = '';
$('#reg-email-hint').textContent = '';
// 显示用户信息和操作按钮
showUserInfo();
showView('#view-grade');
} else {
showToast('#setpwd-toast', res.message || '注册失败', false);
}
});
// 添加返回注册页功能
$('#btn-back-to-register-from-password').addEventListener('click', () => {
// 只清空设置密码页面的输入内容,不清空注册页面内容和用户状态
$('#pwd-1').value = '';
$('#pwd-2').value = '';
$('#pwd-hint').textContent = '';
showView('#view-register');
});
// 添加返回登录页功能
$('#btn-back-to-login-from-password').addEventListener('click', () => {
// 清理状态和输入内容
clearUserState();
$('#pwd-1').value = '';
$('#pwd-2').value = '';
$('#pwd-hint').textContent = '';
showView('#view-login');
});
}
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');
});
});
// 添加返回登录页功能
$('#btn-back-to-login-from-grade').addEventListener('click', () => {
// 清理状态
clearUserState();
showView('#view-login');
});
}
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();
});
// 添加返回选择年级功能
$('#btn-back-to-grade-from-count').addEventListener('click', () => {
// 清理年级状态
state.grade = '';
// 返回选择年级页面
showView('#view-grade');
});
}
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);
});
// 更新按钮状态
const prevBtn = $('#btn-prev');
const submitBtn = $('#btn-submit-next');
if (prevBtn) prevBtn.style.display = state.currentIndex > 0 ? 'inline-block' : 'none';
if (submitBtn) {
submitBtn.textContent = state.currentIndex < state.questions.length - 1 ? '提交 / 下一题' : '提交 / 完成';
}
// 考试过程中隐藏用户操作按钮
const userActions = document.getElementById('user-actions');
if (userActions) userActions.style.display = 'none';
// 考试过程中禁用头像上传功能
const avatarContainer = document.querySelector('.avatar-container');
if (avatarContainer) {
avatarContainer.style.pointerEvents = 'none';
}
// 恢复之前的选择(如果有的话)
const existingAnswer = state.answers.find(a => a.id === q.id);
if (existingAnswer) {
const radio = document.querySelector(`input[name="opt"][value="${existingAnswer.chosen}"]`);
if (radio) radio.checked = true;
}
}
function startQuiz() {
showView('#view-quiz');
renderCurrentQuestion();
}
function initQuiz() {
$('#btn-prev').addEventListener('click', () => {
if (state.currentIndex > 0) {
// 保存当前答案
const q = state.questions[state.currentIndex];
const chosen = document.querySelector('input[name="opt"]:checked');
if (chosen) {
// 更新或添加答案
const existingIndex = state.answers.findIndex(a => a.id === q.id);
if (existingIndex >= 0) {
state.answers[existingIndex] = { id: q.id, chosen: chosen.value, correct: q.answer };
} else {
state.answers.push({ id: q.id, chosen: chosen.value, correct: q.answer });
}
}
state.currentIndex--;
renderCurrentQuestion();
}
});
$('#btn-submit-next').addEventListener('click', () => {
const q = state.questions[state.currentIndex];
const chosen = document.querySelector('input[name="opt"]:checked');
if (!chosen) {
customAlert('请选择一个选项');
return;
}
// 更新或添加答案
const existingIndex = state.answers.findIndex(a => a.id === q.id);
if (existingIndex >= 0) {
state.answers[existingIndex] = { id: q.id, chosen: chosen.value, correct: q.answer };
} else {
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}`;
// 考试结束后重新显示用户操作按钮
const userActions = document.getElementById('user-actions');
if (userActions) userActions.style.display = 'flex';
// 考试结束后恢复头像上传功能
const avatarContainer = document.querySelector('.avatar-container');
if (avatarContainer) {
avatarContainer.style.pointerEvents = 'auto';
}
showView('#view-result');
}
function initResult() {
$('#btn-continue').addEventListener('click', () => {
showView('#view-grade');
});
$('#btn-exit').addEventListener('click', () => {
// 退出回登录页并清理状态
clearUserState();
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';
customAlert('修改成功,请重新登录');
// 清理状态并返回登录页,清空密码栏
clearUserState();
$('#login-password').value = '';
showView('#view-login');
});
}
// 修改用户名
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';
customAlert('用户名修改成功');
// 更新显示的用户名
state.currentUsername = newName;
const userName = document.getElementById('user-name');
if (userName) userName.textContent = newName;
});
}
function initDeleteAccountModal() {
const mask = $('#modal-mask-delete');
const btn = $('#btn-delete-account');
if (!btn) return;
btn.addEventListener('click', () => {
$('#delete-password').value = '';
$('#delete-hint').textContent = '';
mask.style.display = 'flex';
});
$('#btn-cancel-delete').addEventListener('click', () => {
mask.style.display = 'none';
});
$('#btn-confirm-delete').addEventListener('click', async () => {
const password = $('#delete-password').value;
const hint = $('#delete-hint');
if (!password) {
hint.textContent = '请输入密码确认删除';
return;
}
// 二次确认
const confirmed = await customConfirm('⚠️ 警告:此操作将永久删除您的账号和所有数据,无法恢复!\n\n确定要继续吗', '删除账号确认');
if (!confirmed) {
return;
}
const res = await window.API.deleteAccount(state.currentEmail, password);
if (!res.ok) {
hint.textContent = res.message || '删除失败';
return;
}
mask.style.display = 'none';
customAlert('账号删除成功');
// 清理状态并返回登录页,清空输入框
clearUserState();
$('#login-account').value = '';
$('#login-password').value = '';
showView('#view-login');
});
}
// 头像上传功能
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) {
// 检查文件类型
if (!file.type.startsWith('image/')) {
customAlert('请选择图片文件');
return;
}
// 检查文件大小限制为2MB
if (file.size > 2 * 1024 * 1024) {
customAlert('图片文件过大请选择小于2MB的图片');
return;
}
// 使用Canvas优化图片质量
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// 设置canvas尺寸为头像显示尺寸的2倍40px * 2 = 80px
canvas.width = 80; // 2倍尺寸提高清晰度
canvas.height = 80;
// 启用图像平滑
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// 绘制圆形头像
ctx.save();
ctx.beginPath();
ctx.arc(40, 40, 40, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
// 绘制图片
ctx.drawImage(img, 0, 0, 80, 80);
ctx.restore();
// 转换为高质量base64
const optimizedDataURL = canvas.toDataURL('image/jpeg', 0.9);
userAvatar.src = optimizedDataURL;
console.log('头像已优化并设置');
};
img.src = URL.createObjectURL(file);
}
});
}
}
// 启动
window.addEventListener('DOMContentLoaded', () => {
// 默认进入登录页
initRegister();
initSetPassword();
initGradeSelect();
initCount();
initQuiz();
initResult();
initChangePasswordModal();
initChangeUsernameModal();
initDeleteAccountModal();
initAvatarUpload();
initLogin();
});

@ -0,0 +1,483 @@
/* CSS变量定义 - 主题色彩配置 */
:root {
--bg: #0e1116;
--border: #202734;
--danger: #ef4444;
--muted: #9aa4b2;
--panel: #151a21;
--primary: #3b82f6;
--primary-2: #2563eb;
--success: #22c55e;
--text: #e8edf3;
}
/* 全局盒模型设置 */
* {
box-sizing: border-box;
}
/* 页面高度设置 */
html,
body,
#app {
height: 100%;
}
/* 页面基础样式 */
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
margin: 0;
}
/* 应用头部样式 */
.app-header {
align-items: center;
border-bottom: 1px solid var(--border);
display: flex;
height: 80px;
justify-content: space-between;
padding: 0 16px;
}
/* 应用标题样式 */
.app-title {
font-weight: 600;
}
/* 应用操作按钮区域 */
.app-actions {
align-items: center;
display: flex;
flex-wrap: nowrap;
gap: 16px;
}
/* 链接按钮样式 */
.app-actions .link {
background: transparent;
border: none;
color: var(--primary);
cursor: pointer;
font-size: 12px;
height: 40px;
line-height: 40px;
padding: 0 8px;
}
/* 用户信息框样式 */
.user-box {
align-items: center;
display: flex;
gap: 12px;
}
/* 头像容器样式 */
.avatar-container {
cursor: pointer;
position: relative;
}
/* 用户头像样式 */
.avatar {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
background: #233;
border: 1px solid var(--border);
border-radius: 50%;
height: 40px;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
image-rendering: high-quality;
image-rendering: pixelated;
object-fit: cover;
transform: translateZ(0);
width: 40px;
}
/* 用户名样式 */
.user-name {
color: #cbd5e1;
font-size: 16px;
font-weight: 500;
height: 42px;
line-height: 40px;
white-space: nowrap;
}
/* 用户操作按钮区域 */
.user-actions {
align-items: center;
display: flex;
gap: 12px;
}
/* 主视图容器样式 */
.view-container {
align-items: center;
display: flex;
height: calc(100% - 80px);
justify-content: center;
padding: 24px;
}
/* 页面视图样式 */
.view {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
display: none;
max-width: 100%;
min-height: 440px; /* 固定高度避免跳动 */
padding: 24px;
width: 860px;
}
.view.active {
display: block;
}
/* 注册页面样式 */
/* 注册页按钮与上方间距 */
#view-register .primary {
margin-top: 20px;
}
/* 登录页面样式 */
/* 登录页:增加表单项间距 */
#view-login .form-group {
margin-bottom: 20px;
}
/* 设置密码页面样式 */
/* 设置密码页:调大首个分组间距,调小按钮上边距 */
#view-set-password .form-group:first-of-type {
margin-bottom: 22px;
margin-top: 16px;
}
#view-set-password .form-group:first-of-type label {
margin-bottom: 10px;
}
#view-set-password .primary {
margin-top: 6px;
}
/* 年级选择页面样式 */
/* 年级选择:标题与卡片拉开距离并居中卡片 */
#view-grade h2 {
margin-bottom: 0;
}
#view-grade .grade-grid {
margin-top: 80px; /* 增加与标题的距离,减少下方空白 */
justify-content: center; /* 居中排列 */
grid-template-columns: repeat(3, 200px); /* 固定宽度列,利于视觉对齐 */
max-width: 680px; /* 容器最大宽度,便于居中 */
margin-left: auto;
margin-right: auto;
gap: 16px; /* 稍大空隙更美观 */
}
/* 生成题目页面样式 */
/* 生成题目:标题与输入区域拉开距离 */
#view-count h2 {
margin-bottom: 0;
}
#view-count .form-group.small {
margin-top: 24px;
}
/* 答题页面样式 */
/* 答题页:对齐选项单选与文本;增加内部间距,防止与按钮重叠的视觉拥挤 */
.quiz-card {
gap: 12px;
}
.question {
margin-bottom: 16px;
}
.options {
gap: 10px;
}
.options li label {
align-items: center;
display: flex;
gap: 8px;
}
.options input[type="radio"] {
margin-bottom: 1px;
vertical-align: middle;
}
/* 成绩页面样式 */
/* 成绩页居中+留白与间距,避免拥挤 */
#view-result.view.active {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 32px 24px; /* 增加内部留白 */
gap: 16px; /* 元素之间更舒展 */
}
/* 通用标题与表单控件样式 */
h2 {
font-size: 20px;
margin: 0 0 16px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group.small {
max-width: 320px;
}
label {
color: var(--muted);
font-size: 13px;
margin-bottom: 6px;
}
input[type="email"], input[type="text"], input[type="password"], input[type="number"] {
background: #0c1016;
border: 1px solid var(--border);
color: var(--text);
padding: 10px 12px;
border-radius: 8px;
}
.hint {
color: var(--muted);
font-size: 12px;
min-height: 16px;
}
.form-row {
align-items: flex-end;
display: flex;
flex-wrap: nowrap;
gap: 12px;
}
.flex-1 {
flex: 1;
}
button {
border: none;
border-radius: 8px;
cursor: pointer;
padding: 10px 14px;
}
button.primary {
background: var(--primary);
color: #fff;
}
button.primary:hover {
background: var(--primary-2);
}
button.secondary {
background: #2a3342;
color: #cbd5e1;
}
button.danger {
background: var(--danger);
color: #fff;
}
.toast {
color: var(--success);
margin-top: 8px;
min-height: 18px;
}
/* 年级选择栅格与卡片样式 */
.grade-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, 1fr);
}
.grade-card {
background: #0c1016;
border: 1px solid var(--border);
border-radius: 12px;
color: #e5e7eb;
font-size: 18px;
height: 128px;
}
.grade-card:hover {
border-color: var(--primary);
}
/* 答题页面题干与选项容器样式 */
.quiz-header {
color: var(--muted);
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.quiz-card {
background: #0c1016;
border: 1px solid var(--border);
border-radius: 12px;
display: flex;
flex-direction: column;
min-height: 300px;
padding: 16px;
}
.question {
font-size: 18px;
margin-bottom: 12px;
}
.options {
display: grid;
gap: 8px;
list-style: none;
margin: 0;
padding: 0;
}
.options li {
align-items: center;
background: #0f1420;
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
gap: 8px;
padding: 10px;
}
.quiz-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: auto;
}
.quiz-actions .primary {
order: 1;
}
.quiz-actions .secondary {
order: 2;
}
/* 成绩页面分数与操作按钮样式 */
.score {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background: linear-gradient(90deg, #60a5fa, #a78bfa);
background-clip: text;
font-size: 64px; /* 更醒目 */
font-weight: 700;
line-height: 1.05;
margin: 8px 0 30px;
text-align: center;
}
.result-actions {
display: flex;
gap: 16px; /* 按钮更疏朗 */
justify-content: center;
}
#view-result .result-actions button {
min-width: 128px;
height: 42px;
padding: 0 16px;
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
transition: transform .15s ease, box-shadow .15s ease;
}
#view-result .result-actions button:hover {
transform: translateY(-1px);
box-shadow: 0 10px 24px rgba(0,0,0,0.28);
}
/* 模态框样式(修改用户名/密码) */
.modal-mask {
align-items: center;
background: rgba(0,0,0,0.5);
display: flex;
inset: 0;
justify-content: center;
position: fixed;
}
.modal {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
width: 420px;
}
.modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
/* 自定义提示框样式 */
#custom-alert-mask .modal {
text-align: center;
width: 360px;
}
#custom-alert-mask h3 {
color: var(--text);
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
}
#custom-alert-mask p {
color: var(--muted);
font-size: 14px;
line-height: 1.5;
margin: 0;
}
#custom-alert-mask .modal-actions {
justify-content: center;
margin-top: 20px;
}
#custom-alert-mask .modal-actions button {
min-width: 80px;
}
/* 自定义确认对话框样式 */
#custom-alert-mask .modal-actions {
gap: 8px;
}
#custom-alert-mask .modal-actions .secondary {
background: var(--border);
color: var(--muted);
}
#custom-alert-mask .modal-actions .secondary:hover {
background: var(--muted);
color: var(--panel);
}
Loading…
Cancel
Save