结对项目

main
Rumia 6 months ago
parent da187bd9be
commit 99f4c034b4

File diff suppressed because it is too large Load Diff

@ -1,16 +0,0 @@
[
{
"email": "3454934335@qq.com",
"username": "wwb",
"registeredAt": "2025-10-11T05:26:22.038Z",
"password": "cc43f52af644042612d98dea06cf5b64d8e2f8af1de8065fe83b208f4a6d8875",
"updatedAt": "2025-10-11T05:26:22.051Z"
},
{
"email": "3063485007@qq.com",
"username": "wqs",
"registeredAt": "2025-10-11T19:08:09.303Z",
"password": "be0c664d19564b568c4dd76d00a26a08ee89214877e52225eee5ffefab36c15f",
"updatedAt": "2025-10-11T19:08:26.339Z"
}
]

@ -1,31 +0,0 @@
// =============================================
// 数学学习软件 - 邮箱配置文件
// =============================================
// ==================== 选择1: QQ邮箱服务 ====================
module.exports = {
service: 'qq',
auth: {
user: '3454934335@qq.com', // 替换为你的QQ邮箱
pass: 'vxhsswmmqiyvchhh' // 替换为QQ邮箱授权码16位
}
};
// ==================== 选择2: 163邮箱服务 ====================
module.exports = {
service: '163',
auth: {
user: '18950579895@163.com',// 替换为你的163邮箱
pass: 'UCgJrE7yzzd4Uz3g' // 替换为163邮箱授权码
}
};
// ==================== 选择3: 测试模式 ====================
// 不发送实际邮件,验证码在控制台显示
// 适用于开发和测试环境
/*
module.exports = {};
*/

File diff suppressed because it is too large Load Diff

@ -1,35 +0,0 @@
{
"name": "math-learning-backend",
"version": "1.0.0",
"description": "数学学习软件后端API - 支持QQ邮箱和163邮箱验证",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"后端服务测试模式\" && node server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"body-parser": "^1.20.2",
"nodemailer": "^6.9.7"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": [
"education",
"math",
"learning",
"email",
"qq",
"163"
],
"author": "数学学习软件团队",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/your-repo/math-learning-software"
}
}

@ -1,310 +0,0 @@
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// 导入工具类
const UserManager = require('./utils/user-manager');
const MultiEmailService = require('./utils/multi-email-service');
const MathQuestionGenerator = require('./utils/question-generator');
// 创建Express应用实例
const app = express();
const PORT = 8080;
// 配置中间件
app.use(cors());
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, '../project4')));
// 实例化管理器
const userManager = new UserManager();
const emailService = new MultiEmailService();
// 存储验证码(内存存储,重启后失效)
const verificationCodes = new Map();
// 生成6位数字验证码
function generateVerificationCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// 验证邮箱格式是否正确
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// API路由
// 检查服务器健康状态
app.get('/api/health', (req, res) => {
res.json({
ok: true,
message: '服务正常运行',
emailService: emailService.transporter ? '已配置' : '测试模式'
});
});
// 发送邮箱验证码
app.post('/api/send-code', async (req, res) => {
try {
const {email, username} = req.body;
console.log('收到发送验证码请求:', {email, username});
if (!email || !username) {
return res.json({ok: false, message: '邮箱和用户名不能为空'});
}
// 验证邮箱格式
if (!validateEmail(email)) {
return res.json({ok: false, message: '邮箱格式不正确'});
}
// 限制邮箱域名只允许QQ邮箱和163邮箱
if (!email.includes('@qq.com') && !email.includes('@163.com')) {
return res.json({ok: false, message: '只支持QQ邮箱和163邮箱'});
}
// 检查用户名是否已存在
if (userManager.findUserByUsername(username)) {
return res.json({ok: false, message: '用户名已存在'});
}
// 检查邮箱是否已注册
if (userManager.findUserByEmail(email)) {
return res.json({ok: false, message: '邮箱已被注册'});
}
const code = generateVerificationCode();
verificationCodes.set(email, {
code,
username,
timestamp: Date.now(),
});
console.log(`为邮箱 ${email} 生成验证码: ${code}`);
// 发送邮件
const emailResult = await emailService.sendVerificationCode(email, code, username);
if (emailResult.success) {
res.json({ok: true, message: emailResult.message});
} else {
// 如果邮件发送失败,但在开发模式下提供了验证码,仍然算成功
if (emailResult.debug) {
res.json({ok: true, message: emailResult.message + ' ' + emailResult.debug});
} else {
res.json({ok: false, message: emailResult.message});
}
}
} catch (error) {
console.error('发送验证码错误:', error);
res.json({ok: false, message: '服务器内部错误'});
}
});
// 验证码验证
app.post('/api/verify-code', (req, res) => {
try {
const {email, username, code} = req.body;
console.log('收到验证码验证请求:', {email, username, code});
if (!email || !username || !code) {
return res.json({ok: false, message: '请填写完整信息'});
}
const storedData = verificationCodes.get(email);
if (!storedData) {
return res.json({ok: false, message: '请先获取验证码'});
}
// 验证码10分钟过期
if (Date.now() - storedData.timestamp > 10 * 60 * 1000) {
verificationCodes.delete(email);
return res.json({ok: false, message: '验证码已过期,请重新获取'});
}
if (storedData.code !== code) {
return res.json({ok: false, message: '验证码不正确'});
}
if (storedData.username !== username) {
return res.json({ok: false, message: '用户名与验证时不一致'});
}
// 清除验证码
verificationCodes.delete(email);
res.json({ok: true, message: '验证成功,请设置密码'});
} catch (error) {
console.error('验证码验证错误:', error);
res.json({ok: false, message: error.message});
}
});
// 注册(创建用户 + 设置密码)
app.post('/api/register', (req, res) => {
try {
const {email, username, password} = req.body;
console.log('收到注册请求:', {email, username});
if (!email || !username || !password) {
return res.json({ok: false, message: '请填写完整信息'});
}
// 创建用户并设置密码
userManager.createUser(email, username);
userManager.setPassword(email, password);
res.json({ok: true, message: '注册成功'});
} catch (error) {
console.error('注册错误:', error);
res.json({ok: false, message: error.message});
}
});
// 登录
app.post('/api/login', (req, res) => {
try {
const {account, password} = req.body;
if (!account || !password) {
return res.json({ok: false, message: '请填写账号和密码'});
}
// 通过邮箱或用户名查找用户
let user = userManager.findUserByEmail(account);
if (!user) {
user = userManager.findUserByUsername(account);
}
if (!user || !user.password) {
return res.json({ok: false, message: '用户不存在'});
}
if (!userManager.verifyPassword(user.email, password)) {
return res.json({ok: false, message: '密码不正确'});
}
res.json({
ok: true,
message: '登录成功',
data: {
email: user.email,
username: user.username,
},
});
} catch (error) {
console.error('登录错误:', error);
res.json({ok: false, message: error.message});
}
});
// 修改密码
app.post('/api/change-password', (req, res) => {
try {
const {email, oldPassword, newPassword} = req.body;
if (!email || !oldPassword || !newPassword) {
return res.json({ok: false, message: '请填写完整信息'});
}
userManager.changePassword(email, oldPassword, newPassword);
res.json({ok: true, message: '密码修改成功'});
} catch (error) {
console.error('修改密码错误:', error);
res.json({ok: false, message: error.message});
}
});
// 修改用户名
app.post('/api/change-username', (req, res) => {
try {
const {email, username} = req.body;
if (!email || !username) {
return res.json({ok: false, message: '请填写完整信息'});
}
userManager.changeUsername(email, username);
res.json({ok: true, message: '用户名修改成功'});
} catch (error) {
console.error('修改用户名错误:', error);
res.json({ok: false, message: error.message});
}
});
// 删除账号
app.post('/api/delete-account', (req, res) => {
try {
const {email, password} = req.body;
if (!email || !password) {
return res.json({ok: false, message: '请填写完整信息'});
}
// 验证密码
if (!userManager.verifyPassword(email, password)) {
return res.json({ok: false, message: '密码不正确'});
}
userManager.deleteUser(email);
res.json({ok: true, message: '账号删除成功'});
} catch (error) {
console.error('删除账号错误:', error);
res.json({ok: false, message: error.message});
}
});
// 获取题目
app.get('/api/questions', (req, res) => {
try {
const {grade, count} = req.query;
if (!grade || !count) {
return res.json({ok: false, message: '请选择年级和题目数量'});
}
const countNum = parseInt(count);
if (isNaN(countNum) || countNum < 10 || countNum > 30) {
return res.json({ok: false, message: '题目数量需在10-30之间'});
}
const generator = new MathQuestionGenerator();
const questions = generator.generateQuestions(grade, countNum);
res.json({
ok: true,
data: questions,
message: '题目生成成功',
});
} catch (error) {
console.error('生成题目失败:', error);
res.json({ok: false, message: '生成题目失败'});
}
});
// 提供前端页面
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../project4/index.html'));
});
// 启动服务器
app.listen(PORT, () => {
console.log(`========================================`);
console.log(`🎓 数学学习软件后端服务已启动`);
console.log(`📍 服务地址: http://localhost:${PORT}`);
console.log(`🌐 前端页面: http://localhost:${PORT}/`);
console.log(`🔧 API文档: http://localhost:${PORT}/api/health`);
console.log(`📧 邮箱服务: ${emailService.transporter ? '已配置' : '测试模式'}`);
console.log(`========================================`);
});
module.exports = app;

@ -1,104 +0,0 @@
const nodemailer = require('nodemailer');
// 多邮箱服务类,支持多种邮箱服务商
class MultiEmailService {
constructor() {
this.transporters = new Map();
this.initTransporters();
}
// 初始化各种邮箱服务商的传输器
initTransporters() {
// QQ邮箱配置
try {
// 修改
const qqTransporter = nodemailer.createTransport({
auth: {
pass: 'vxhsswmmqiyvchhh',
user: '3454934335@qq.com',
},
service: 'qq',
});
this.transporters.set('qq', qqTransporter);
console.log('✅ QQ邮箱服务已初始化');
} catch (error) {
console.error('❌ QQ邮箱初始化失败:', error);
}
// 163邮箱配置
try {
// 修改
const mail163Transporter = nodemailer.createTransport({
auth: {
pass: 'UCgJrE7yzzd4Uz3g',
user: '18950579895@163.com',
},
service: '163',
});
this.transporters.set('163', mail163Transporter);
console.log('✅ 163邮箱服务已初始化');
} catch (error) {
console.error('❌ 163邮箱初始化失败:', error);
}
}
// 发送验证码邮件
async sendVerificationCode(email, code, username) {
// 根据邮箱域名选择发件箱
let transporter = email.includes('@qq.com') ? this.transporters.get('qq') :
email.includes('@163.com') ? this.transporters.get('163') :
this.transporters.get('qq');
if (!transporter) {
console.log(`🔧 测试模式 - 邮箱: ${email}, 验证码: ${code}`);
return {success: true, message: '验证码已生成(测试模式)', debug: `验证码: ${code}`};
}
const mailOptions = {
from: transporter.options.auth.user,
to: email,
subject: '数学学习软件 - 注册验证码',
html: this.generateEmailTemplate(code, username)
};
try {
await transporter.sendMail(mailOptions);
console.log(`✅ 验证码邮件已发送到: ${email}`);
return {success: true, message: '验证码已发送到您的邮箱'};
} catch (error) {
console.error('❌ 邮件发送失败:', error);
return {success: false, message: '邮件发送失败,请稍后重试', debug: `验证码: ${code} (请使用此验证码完成注册)`};
}
}
// 生成邮件HTML模板
generateEmailTemplate(code, username) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
.container { background: white; border-radius: 10px; padding: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.header { color: #3b82f6; text-align: center; margin-bottom: 30px; border-bottom: 2px solid #3b82f6; padding-bottom: 15px; }
.code { text-align: center; margin: 30px 0; font-size: 32px; font-weight: bold; color: #3b82f6; letter-spacing: 5px; padding: 20px; background: #f0f7ff; border-radius: 8px; border: 2px dashed #3b82f6; }
</style>
</head>
<body>
<div class="container">
<div class="header"><h2>🎓 数学学习软件</h2></div>
<p>亲爱的 <strong>${username}</strong></p>
<p>您正在注册数学学习软件验证码为</p>
<div class="code">${code}</div>
<p>验证码有效期为10分钟请尽快完成注册</p>
<p>如果这不是您本人的操作请忽略此邮件</p>
</div>
</body>
</html>
`;
}
}
module.exports = MultiEmailService;

@ -1,441 +0,0 @@
const fs = require('fs');
const path = require('path');
class MathQuestionGenerator {
constructor() {
this.currentSessionQuestions = new Set();
}
// 生成指定年级和数量的题目
generateQuestions(grade, count) {
console.log(`正在生成${grade} ${count}道题目...`);
const questions = [];
this.currentSessionQuestions.clear();
const maxAttempts = count * 20;
let attempts = 0;
while (questions.length < count && attempts < maxAttempts) {
attempts++;
const question = this.generateQuestion(grade);
if (!question) {
continue;
}
const questionKey = `${grade}-${question.stem}`;
if (!this.currentSessionQuestions.has(questionKey)) {
questions.push(question);
this.currentSessionQuestions.add(questionKey);
console.log(`✅ 生成第${questions.length}题: ${question.stem}`);
}
}
if (questions.length < count) {
console.warn(`⚠️ 只生成了${questions.length}道题目,未能达到要求的${count}`);
}
return questions;
}
// 根据年级生成单个题目
generateQuestion(grade) {
try {
switch (grade) {
case '小学':
return this.generatePrimaryQuestion();
case '初中':
return this.generateMiddleSchoolQuestion();
case '高中':
return this.generateHighSchoolQuestion();
default:
return this.generatePrimaryQuestion();
}
} catch (error) {
console.error(`生成${grade}题目时出错:`, error);
return null;
}
}
// 生成小学题目2-5个操作数基础四则运算
generatePrimaryQuestion() {
const numOperands = Math.floor(Math.random() * 4) + 2;
const operations = ['+', '-', '×', '÷'];
let expression = '';
let correctAnswer = 0;
for (let i = 0; i < numOperands; i++) {
const num = Math.floor(Math.random() * 100) + 1;
if (i === 0) {
expression = num.toString();
correctAnswer = num;
} else {
const op = operations[Math.floor(Math.random() * operations.length)];
expression += ` ${op} ${num}`;
correctAnswer = this.applyOperation(correctAnswer, num, op);
}
}
correctAnswer = this.calculateWithPriority(expression);
if (numOperands >= 3 && Math.random() < 0.3) {
const result = this.addParentheses(expression, correctAnswer);
if (result) {
expression = result.expression;
correctAnswer = result.answer;
}
}
const options = this.generateOptions(correctAnswer, 4);
return {
id: `primary-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
stem: `计算:${expression} = ?`,
options: options,
answer: options.find(opt => opt.isCorrect).key
};
}
// 生成初中题目1-5个操作数包含平方和开方
generateMiddleSchoolQuestion() {
const numOperands = Math.floor(Math.random() * 5) + 1;
const operations = ['+', '-', '×', '÷'];
const specialPosition = Math.floor(Math.random() * numOperands);
const isSquare = Math.random() < 0.5;
let expression = '';
let correctAnswer = 0;
for (let i = 0; i < numOperands; i++) {
const {term, value} = i === specialPosition ?
this.generateSpecialTerm(isSquare) : this.generateRandomTerm();
if (i === 0) {
expression = term;
correctAnswer = value;
} else {
const op = operations[Math.floor(Math.random() * operations.length)];
expression += ` ${op} ${term}`;
correctAnswer = this.applyOperation(correctAnswer, value, op);
}
}
correctAnswer = this.calculateWithPriority(expression);
if (numOperands >= 3 && Math.random() < 0.4) {
const result = this.addParentheses(expression, correctAnswer);
if (result) {expression = result.expression; correctAnswer = result.answer;}
}
const options = this.generateOptions(correctAnswer, 4);
return {
id: `middle-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
stem: `计算:${expression} = ?`,
options: options,
answer: options.find(opt => opt.isCorrect).key
};
}
// 生成特殊项(平方或开方)
generateSpecialTerm(isSquare) {
if (isSquare) {
const base = Math.floor(Math.random() * 15) + 1;
return { term: `${base}²`, value: base * base };
} else {
const perfectSquares = [4, 9, 16, 25, 36, 49, 64, 81, 100];
const num = perfectSquares[Math.floor(Math.random() * perfectSquares.length)];
return { term: `${num}`, value: Math.sqrt(num) };
}
}
// 生成随机项(数字、平方或开方)
generateRandomTerm() {
const termType = Math.random();
if (termType < 0.3) {
const base = Math.floor(Math.random() * 15) + 1;
return { term: `${base}²`, value: base * base };
} else if (termType < 0.6) {
const perfectSquares = [4, 9, 16, 25, 36, 49, 64, 81, 100];
const num = perfectSquares[Math.floor(Math.random() * perfectSquares.length)];
return { term: `${num}`, value: Math.sqrt(num) };
} else {
const num = Math.floor(Math.random() * 100) + 1;
return { term: num.toString(), value: num };
}
}
// 生成高中题目1-5个操作数包含三角函数
generateHighSchoolQuestion() {
const numOperands = Math.floor(Math.random() * 5) + 1;
const operations = ['+', '-', '×', '÷'];
const specialPosition = Math.floor(Math.random() * numOperands);
let expression = '', correctAnswer = 0;
for (let i = 0; i < numOperands; i++) {
const {term, value} = i === specialPosition ?
this.generateTrigTerm() : (Math.random() < 0.4 ? this.generateTrigTerm() : this.generateNumberTerm());
if (i === 0) { expression = term; correctAnswer = value; }
else {
const op = operations[Math.floor(Math.random() * operations.length)];
expression += ` ${op} ${term}`;
correctAnswer = this.applyOperation(correctAnswer, value, op);
}
}
correctAnswer = this.calculateWithPriority(expression);
if (numOperands >= 3 && Math.random() < 0.4) {
const result = this.addParentheses(expression, correctAnswer);
if (result) { expression = result.expression; correctAnswer = result.answer; }
}
const options = this.generateOptions(correctAnswer, 4);
return {
id: `high-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
stem: `计算:${expression} = ?`,
options: options,
answer: options.find(opt => opt.isCorrect).key
};
}
// 生成三角函数项
generateTrigTerm() {
const functions = ['sin', 'cos', 'tan'];
const func = functions[Math.floor(Math.random() * functions.length)];
const angle = Math.floor(Math.random() * 100) + 1;
const value = Math.round(Math[func](angle * Math.PI / 180) * 100) / 100;
return { term: `${func}${angle}`, value };
}
// 生成数字项
generateNumberTerm() {
const num = Math.floor(Math.random() * 100) + 1;
return { term: num.toString(), value: num };
}
// 生成选择题选项
generateOptions(correctAnswer, count) {
const options = [];
const keys = ['A', 'B', 'C', 'D'];
const correctIndex = Math.floor(Math.random() * count);
const isInteger = typeof correctAnswer === 'number' && Number.isInteger(correctAnswer);
for (let i = 0; i < count; i++) {
const value = i === correctIndex ? correctAnswer : this.generateWrongOption(correctAnswer, isInteger, i);
options.push({key: keys[i], text: value.toString(), isCorrect: i === correctIndex});
}
return options;
}
// 生成错误选项
generateWrongOption(correctAnswer, isInteger, index) {
if (typeof correctAnswer !== 'number') {
return Math.random().toString(36).substring(2, 6);
}
let attempts = 0;
let value;
do {
const deviation = (Math.random() - 0.5) * 4;
value = isInteger ?
correctAnswer + Math.floor(Math.random() * 10) - 5 :
Math.round((correctAnswer + deviation) * 100) / 100;
if (++attempts > 10) {
value = correctAnswer + (index + 1);
if (isInteger) {
value = Math.round(value);
}
break;
}
} while (value === correctAnswer);
return value;
}
// 获取数字的所有因数
getDivisors(n) {
const divisors = [];
for (let i = 2; i <= Math.min(n, 100); i++) { // 除数也限制在1-100范围内
if (n % i === 0) divisors.push(i);
}
return divisors;
}
// 小学题目专用的括号添加函数,确保括号内不会产生负数
addParenthesesForPrimary(expression, originalAnswer) {
const parts = expression.split(' ');
// 如果表达式太短,不需要加括号
if (parts.length < 5) return null;
// 找到所有可以加括号的位置(运算符位置)
const operatorPositions = [];
for (let i = 1; i < parts.length - 1; i += 2) {
// 只考虑加法和乘法,避免减法导致负数
if (parts[i] === '+' || parts[i] === '×') {
operatorPositions.push(i);
}
}
if (operatorPositions.length === 0) return null;
// 随机选择一个运算符位置
const operatorIndex = operatorPositions[Math.floor(Math.random() * operatorPositions.length)];
// 确定括号的范围(从运算符前一个操作数到运算符后一个操作数)
const startPos = operatorIndex - 1;
const endPos = operatorIndex + 1;
// 构建带括号的表达式
let result = '';
for (let i = 0; i < parts.length; i++) {
if (i === startPos) {
result += '(';
}
result += parts[i];
if (i === endPos) {
result += ')';
}
if (i < parts.length - 1) {
result += ' ';
}
}
// 计算带括号的答案
let newAnswer = this.calculateWithPriority(result);
// 确保答案是非负整数
if (newAnswer < 0 || !Number.isInteger(newAnswer)) {
return null;
}
return {
answer: newAnswer,
expression: result,
};
}
// 通用的括号添加函数
// 为表达式添加括号
addParentheses(expression, originalAnswer) {
const parts = expression.split(' ');
// 如果表达式太短,不需要加括号
if (parts.length < 5) return null;
// 找到所有可以加括号的位置(运算符位置)
const operatorPositions = [];
for (let i = 1; i < parts.length - 1; i += 2) {
operatorPositions.push(i);
}
if (operatorPositions.length === 0) return null;
// 随机选择一个运算符位置
const operatorIndex = operatorPositions[Math.floor(Math.random() * operatorPositions.length)];
// 确定括号的范围(从运算符前一个操作数到运算符后一个操作数)
const startPos = operatorIndex - 1;
const endPos = operatorIndex + 1;
// 构建带括号的表达式
let result = '';
for (let i = 0; i < parts.length; i++) {
if (i === startPos) {
result += '(';
}
result += parts[i];
if (i === endPos) {
result += ')';
}
if (i < parts.length - 1) {
result += ' ';
}
}
// 计算带括号的答案
let newAnswer = this.calculateWithPriority(result);
return {
answer: newAnswer,
expression: result,
};
}
// 执行四则运算操作
applyOperation(current, value, op) {
switch (op) {
case '+': return current + value;
case '-': return current - value;
case '×': return current * value;
case '÷': return value !== 0 ? current / value : current;
default: return current;
}
}
// 使用正确优先级计算表达式的答案
calculateWithPriority(expression) {
// 替换运算符为JavaScript可识别的
let jsExpression = expression
.replace(/×/g, '*')
.replace(/÷/g, '/')
.replace(/²/g, '**2')
.replace(/√(\d+)/g, 'Math.sqrt($1)')
.replace(/sin(\d+)/g, 'Math.sin($1 * Math.PI / 180)')
.replace(/cos(\d+)/g, 'Math.cos($1 * Math.PI / 180)')
.replace(/tan(\d+)/g, 'Math.tan($1 * Math.PI / 180)');
try {
// 使用eval计算表达式
let result = eval(jsExpression);
// 处理特殊情况
if (typeof result === 'number') {
// 如果是整数,返回整数
if (Number.isInteger(result)) {
return result;
}
// 否则保留两位小数
return Math.round(result * 100) / 100;
}
return result;
} catch (error) {
console.error('计算表达式时出错:', expression, error);
// 如果计算失败,返回原始表达式的估算值
return this.estimateExpression(expression);
}
}
// 估算表达式的值当eval失败时使用
estimateExpression(expression) {
// 简单的估算逻辑,按顺序计算
const parts = expression.split(' ');
let result = parseFloat(parts[0]);
for (let i = 1; i < parts.length; i += 2) {
const operator = parts[i];
const num = parseFloat(parts[i + 1]);
switch (operator) {
case '+':
result += num;
break;
case '-':
result -= num;
break;
case '×':
result *= num;
break;
case '÷':
if (num !== 0) result /= num;
break;
}
}
return Math.round(result * 100) / 100;
}
}
module.exports = MathQuestionGenerator;

@ -1,183 +0,0 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const USERS_FILE = path.join(__dirname, '../data/users.json');
// 用户管理类,处理用户注册、登录等操作
class UserManager {
constructor() {
this.ensureDataFile();
}
// 确保数据文件存在
ensureDataFile() {
const dataDir = path.dirname(USERS_FILE);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, {recursive: true});
}
if (!fs.existsSync(USERS_FILE)) {
fs.writeFileSync(USERS_FILE, JSON.stringify([]));
}
}
// 读取所有用户数据
readUsers() {
try {
return JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
} catch (error) {
console.error('读取用户数据失败:', error);
return [];
}
}
// 保存用户数据到文件
writeUsers(users) {
try {
fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2));
return true;
} catch (error) {
console.error('写入用户数据失败:', error);
return false;
}
}
// 对密码进行SHA256哈希加密
hashPassword(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
// 验证密码格式6-10位
validatePassword(password) {
if (!password || password.length < 6 || password.length > 10) {
return false;
}
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasDigit = /\d/.test(password);
return hasUpper && hasLower && hasDigit;
}
findUserByEmail(email) {
const users = this.readUsers();
return users.find((user) => user.email === email);
}
findUserByUsername(username) {
const users = this.readUsers();
return users.find((user) => user.username === username);
}
createUser(email, username) {
const users = this.readUsers();
// 严格检查邮箱是否已存在
if (users.some((user) => user.email === email)) {
throw new Error('邮箱已被注册,每个邮箱只能注册一个账户');
}
// 检查用户名是否已存在
if (users.some((user) => user.username === username)) {
throw new Error('用户名已存在');
}
const newUser = {
email,
registeredAt: new Date().toISOString(),
username,
};
users.push(newUser);
if (this.writeUsers(users)) {
console.log(`✅ 新用户注册: ${username} (${email})`);
return newUser;
} else {
throw new Error('用户创建失败');
}
}
setPassword(email, password) {
const users = this.readUsers();
const userIndex = users.findIndex((user) => user.email === email);
if (userIndex === -1) {
throw new Error('用户不存在');
}
if (!this.validatePassword(password)) {
throw new Error('密码需6-10位且包含大小写字母和数字');
}
users[userIndex].password = this.hashPassword(password);
users[userIndex].updatedAt = new Date().toISOString();
if (this.writeUsers(users)) {
console.log(`✅ 用户设置密码: ${email}`);
return true;
} else {
throw new Error('密码设置失败');
}
}
verifyPassword(email, password) {
const user = this.findUserByEmail(email);
if (!user || !user.password) {
return false;
}
return user.password === this.hashPassword(password);
}
changePassword(email, oldPassword, newPassword) {
if (!this.verifyPassword(email, oldPassword)) {
throw new Error('原密码不正确');
}
return this.setPassword(email, newPassword);
}
changeUsername(email, newUsername) {
const users = this.readUsers();
const userIndex = users.findIndex((user) => user.email === email);
if (userIndex === -1) {
throw new Error('用户不存在');
}
// 检查新用户名是否已存在(排除当前用户)
if (users.some((user) => user.username === newUsername && user.email !== email)) {
throw new Error('用户名已存在');
}
const oldUsername = users[userIndex].username;
users[userIndex].username = newUsername;
users[userIndex].updatedAt = new Date().toISOString();
if (this.writeUsers(users)) {
console.log(`✅ 用户修改用户名: ${oldUsername} -> ${newUsername}`);
return true;
} else {
throw new Error('用户名修改失败');
}
}
deleteUser(email) {
const users = this.readUsers();
const userIndex = users.findIndex((user) => user.email === email);
if (userIndex === -1) {
throw new Error('用户不存在');
}
const deletedUser = users[userIndex];
users.splice(userIndex, 1);
if (this.writeUsers(users)) {
console.log(`✅ 用户账号已删除: ${deletedUser.username} (${email})`);
return true;
} else {
throw new Error('账号删除失败');
}
}
}
module.exports = UserManager;

@ -1,252 +0,0 @@
// 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);
}
};
})();

@ -1,153 +0,0 @@
// 前端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;
})();

@ -1,48 +0,0 @@
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();
}
});

@ -1,225 +0,0 @@
<!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

@ -1,44 +0,0 @@
{
"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
}
}
}

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

@ -1,719 +0,0 @@
// 应用状态管理
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();
});

@ -1,483 +0,0 @@
/* 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