结对项目

main
Rumia 7 months ago
parent 5d10886ded
commit d9b26088eb

@ -0,0 +1,788 @@
# 小初高数学学习软件 - 完整项目文档
## 目录(所使用的代码规范地址在末尾)
### 🖥️ 前端部分
- [前端项目概述](#前端项目概述) | [前端功能特性](#前端功能特性) | [前端技术栈](#前端技术栈) | [前端项目结构](#前端项目结构) | [前端快速开始](#前端快速开始)
- [前端使用指南](#前端使用指南) | [前端核心模块说明](#前端核心模块说明) | [前端代码规范](#前端代码规范)
### 🔧 后端部分
- [后端项目概述](#后端项目概述) | [后端功能特性](#后端功能特性) | [后端技术栈](#后端技术栈) | [后端项目结构](#后端项目结构) | [后端快速开始](#后端快速开始)
- [后端使用指南](#后端使用指南) | [后端核心模块说明](#后端核心模块说明) | [后端代码规范](#后端代码规范)
### 📋 部署与问题
- [完整部署指南](#完整部署指南)
- [常见问题解决](#常见问题解决)
---
# 前端部分
## 前端项目概述
本软件是一个基于 Electron 的桌面数学学习应用,采用前后端分离的模块化设计,最终打包为单一可执行程序。提供从用户注册→密码设置→年级选择→数学答题→成绩统计的完整学习流程。
## 前端功能特性
- ✅ 用户注册与邮箱验证码
- ✅ 密码设置与修改
- ✅ 用户头像上传
- ✅ 用户名修改
- ✅ 年级选择(小学/初中/高中)
- ✅ 数学题目生成与答题
- ✅ 成绩统计与显示
- ✅ 单一可执行程序(无需外部服务)
## 前端技术栈
- **桌面框架**: Electron
- **前端**: HTML5 + CSS3 + JavaScript (ES6+)
- **后端**: Node.js + Express集成到主进程
- **通信方式**: IPC进程间通信
- **数据存储**: JSON文件
- **样式**: 原生CSS符合Google CSS规范
- **代码规范**: Google JavaScript/CSS/HTML规范
## 前端项目结构
```
src/
├─ frontend/ # 前端模块GUI界面
│ ├─ electron-main.js # 主进程(集成后端逻辑)
│ ├─ preload.js # IPC通信接口
│ ├─ renderer.js # 渲染进程(前端逻辑)
│ ├─ index.html # 主页面
│ ├─ styles.css # 样式文件
│ └─ package.json # 前端配置
└─ backend/ # 后端模块(业务逻辑+数据)
├─ utils/ # 工具类
├─ data/ # 数据存储
├─ email-config.js # 邮箱配置文件
└─ package.json # 后端依赖配置
```
## 前端快速开始
### 1. 环境要求
- Node.js 18+
- npm 或 yarn
### 2. 安装依赖
```bash
# 安装后端依赖(必需)
cd src/backend
npm install
# 安装前端依赖
cd ../frontend
npm install
```
### 3. 开发模式运行
```bash
npm start
```
**⚠️ 重要**: 必须先安装后端依赖,否则应用启动会报错 `Cannot find module 'nodemailer'`
### 4. 打包发布
```bash
npm run build
```
产物位于 `dist/` 目录,包含:
- `数学学习软件-7.0.0-Setup.exe` - 安装包
- `win-unpacked/` - 解压后的可执行程序目录
## 前端使用指南
### 1. 用户注册
- 输入用户名(必填,需唯一)
- 输入邮箱支持QQ邮箱和163邮箱
- 点击"获取验证码":系统会发送验证码到邮箱
- 输入收到的验证码,点击"注册"
### 2. 密码设置
- 设置密码6-10位含大小写字母和数字`Test123`
- 确认两次输入一致
- 点击"确认设置"
### 3. 用户信息管理
- 注册完成后,右上角显示"头像+用户名+修改按钮"
- 点击头像可选择本地图片更换
- 点击"修改用户名"可更改用户名(需唯一)
- 点击"修改密码"可更改密码
### 4. 数学学习
- 选择年级(小学/初中/高中)
- 输入题目数量10-30
- 点击"生成试卷"
- 逐题作答,查看最终分数
## 前端核心模块说明
### 1. 主进程模块 (electron-main.js)
#### 功能概述
Electron主进程模块负责创建和管理应用程序窗口同时集成了所有后端业务逻辑实现单一可执行程序。
#### 核心功能
- **窗口管理**: 创建和管理应用窗口
- **后端逻辑集成**: 用户管理、邮箱验证、题目生成
- **IPC通信处理**: 处理渲染进程的API请求
- **数据管理**: JSON文件数据存储
- **生命周期管理**: 应用程序启动和关闭
#### 集成架构
```javascript
// 动态路径处理
const getBackendPath = (relativePath) => {
if (app.isPackaged) {
return path.join(process.resourcesPath, 'backend', relativePath);
} else {
return path.join(__dirname, '..', 'backend', relativePath);
}
};
// IPC处理器
ipcMain.handle('api:send-code', async (event, { email, username }) => {
// 发送验证码逻辑
});
ipcMain.handle('api:login', async (event, { account, password }) => {
// 用户登录逻辑
});
```
### 2. 渲染进程模块 (renderer.js)
#### 功能概述
渲染进程模块是前端逻辑的核心,负责处理用户界面交互、数据管理、与主进程通信等功能。
#### 核心功能
- **界面管理**: 页面切换和状态管理
- **用户交互**: 表单处理、事件监听
- **数据管理**: 本地数据存储和同步
- **IPC通信**: 与主进程通信调用后端API
#### 主要功能模块
```javascript
// 用户认证模块
class AuthManager {
// 用户登录
async login(account, password)
// 用户注册
async register(userData)
// 密码设置
async setPassword(password)
}
// 题目管理模块
class QuestionManager {
// 获取题目
async getQuestions(grade, count)
// 提交答案
async submitAnswer(questionId, answer)
// 获取成绩
async getScore()
}
// 用户信息管理模块
class UserManager {
// 修改用户名
async changeUsername(newUsername)
// 修改密码
async changePassword(oldPassword, newPassword)
// 上传头像
async uploadAvatar(file)
}
```
#### 界面状态管理
- **页面切换**: 登录页、注册页、答题页等
- **状态同步**: 用户信息、答题进度
- **错误处理**: 网络错误、验证错误等
### 3. 预加载脚本 (preload.js)
#### 功能概述
预加载脚本负责在渲染进程中安全地暴露API接口实现与主进程的IPC通信。
#### 核心功能
- **API暴露**: 安全地暴露所有业务API
- **IPC通信**: 处理渲染进程与主进程的通信
- **权限控制**: 限制渲染进程的权限
- **数据验证**: 输入数据验证和清理
#### 暴露的API
```javascript
contextBridge.exposeInMainWorld('API', {
// 用户认证
sendRegisterCode: (email, username) => ipcRenderer.invoke('api:send-code', { email, username }),
verifyCode: (email, username, code) => ipcRenderer.invoke('api:verify-code', { email, username, code }),
register: (email, username, password) => ipcRenderer.invoke('api:register', { email, username, password }),
login: (account, password) => ipcRenderer.invoke('api:login', { account, password }),
// 用户管理
changePassword: (email, oldPassword, newPassword) => ipcRenderer.invoke('api:change-password', { email, oldPassword, newPassword }),
changeUsername: (email, username) => ipcRenderer.invoke('api:change-username', { email, username }),
deleteAccount: (email, password) => ipcRenderer.invoke('api:delete-account', { email, password }),
// 题目管理
getQuestions: (grade, count) => ipcRenderer.invoke('api:get-questions', { grade, count })
});
```
## 前端代码规范
- 所有JavaScript文件符合Google JavaScript规范
- CSS文件符合Google CSS规范
- HTML文件符合Google HTML规范
- 使用2空格缩进
- 属性按字母顺序排列
- 统一的命名规范
- 完整的注释说明
---
# 后端部分
## 后端项目概述
基于 Node.js 的后端服务,提供用户管理、邮箱验证、题目生成等核心功能。支持多邮箱服务商,具备完整的用户生命周期管理。
## 后端功能特性
- ✅ 用户注册与邮箱验证码发送
- ✅ 密码加密存储SHA256
- ✅ 用户信息管理(用户名、密码修改)
- ✅ 数学题目智能生成
- ✅ 多邮箱服务商支持
- ✅ 数据持久化存储
## 后端技术栈
- **运行环境**: Node.js
- **框架**: 原生 Node.js + Express
- **数据存储**: JSON文件
- **邮箱服务**: Nodemailer
- **加密**: crypto (SHA256)
- **代码规范**: Google JavaScript规范
## 后端项目结构
```
src/backend/
├─ data/
│ └─ users.json # 用户数据文件
├─ utils/
│ ├─ user-manager.js # 用户管理模块
│ ├─ multi-email-service.js # 多邮箱服务
│ └─ question-generator.js # 题目生成器
├─ email-config.js # 邮箱配置
└─ package.json # 项目配置
```
## 后端快速开始
### 1. 安装依赖
```bash
cd src/backend
npm install
```
### 2. 启动服务
```bash
npm start
```
### 3. 验证服务
服务启动后会在终端显示:`Server listening on http://localhost:8080`
## 后端使用指南
### 1. API调用方式
- 所有API通过IPC通信调用无需HTTP请求
- 前端通过 `ipcRenderer.invoke` 调用主进程中的后端逻辑
### 2. 用户管理流程
- 发送验证码 → 用户注册 → 设置密码
- 登录验证 → 修改信息 → 删除账户
### 3. 题目生成流程
- 选择年级 → 生成题目 → 返回题目数据
- 支持小学、初中、高中三个年级
- 自动生成选择题和计算题
## 后端核心模块说明
### 1. 用户管理模块 (user-manager.js)
#### 功能概述
用户管理模块是整个后端系统的核心,负责处理所有用户相关的操作,包括注册、登录、密码管理、用户信息修改等。
#### 主要功能
- **用户注册**: 邮箱验证、用户名唯一性检查
- **用户登录**: 邮箱/用户名登录验证
- **密码管理**: 密码设置、修改、验证
- **用户信息**: 用户名修改、账户删除
- **数据存储**: JSON文件数据持久化
#### 核心方法
- `createUser(email, username)`: 创建新用户
- `findUserByEmail(email)`: 根据邮箱查找用户
- `findUserByUsername(username)`: 根据用户名查找用户
- `updateUser(email, updates)`: 更新用户信息
- `deleteUser(email)`: 删除用户账户
- `verifyPassword(email, password)`: 验证密码
#### 用户管理安全特性
- **邮箱唯一性**: 每个邮箱只能注册一个账户
- **用户名唯一性**: 用户名必须唯一
- **密码安全**: 密码验证和加密存储
- **数据完整性**: 用户数据一致性检查
#### 数据存储格式
```json
{
"email": "user@example.com",
"username": "用户名",
"registeredAt": "2024-01-01T00:00:00.000Z"
}
```
### 2. 邮箱服务模块 (multi-email-service.js)
#### 功能概述
邮箱服务模块负责发送验证码邮件,支持多个邮箱服务商,提供统一的邮件发送接口。
#### 支持的服务商
- **QQ邮箱**: 支持QQ邮箱SMTP服务
- **163邮箱**: 支持163邮箱SMTP服务
- **测试模式**: 开发环境下的模拟发送
#### 核心功能
- **验证码生成**: 6位数字验证码
- **邮件发送**: 通过SMTP发送验证码
- **模板渲染**: 使用HTML邮件模板
- **错误处理**: 发送失败时的降级处理
#### 配置示例
```javascript
// QQ邮箱配置
const qqConfig = {
host: 'smtp.qq.com',
port: 587,
secure: false,
auth: {
user: 'your_qq@qq.com',
pass: 'your_smtp_password'
}
};
// 163邮箱配置
const config163 = {
host: 'smtp.163.com',
port: 587,
secure: false,
auth: {
user: 'your_163@163.com',
pass: 'your_smtp_password'
}
};
```
#### 邮件模板
```html
<h2>数学学习软件验证码</h2>
<p>您的验证码是:<strong>{{code}}</strong></p>
<p>验证码有效期为10分钟请及时使用。</p>
```
### 3. 题目生成模块 (question-generator.js)
#### 功能概述
题目生成模块负责根据年级和数量生成数学题目,支持小学、初中、高中三个年级的题目生成。
#### 生成逻辑(与个人项目逻辑类似)
- **年级适配**: 根据年级调整题目难度
- **题型多样**: 选择题、计算题、应用题
- **选项生成**: 智能生成干扰选项
- **答案验证**: 确保答案正确性
#### 题目类型
- **小学**: 基础四则运算、简单应用题
- **初中**: 代数运算、几何计算、函数基础
- **高中**: 复杂代数、三角函数、立体几何
#### 核心算法
```javascript
// 题目生成算法
generateQuestion(grade, type) {
const difficulty = this.getDifficultyByGrade(grade);
const operation = this.selectOperation(type);
const numbers = this.generateNumbers(difficulty);
const expression = this.buildExpression(numbers, operation);
const answer = this.calculateAnswer(expression);
const options = this.generateOptions(answer);
return {
stem: expression,
options: options,
answer: answer
};
}
```
#### 题目质量控制
- **难度梯度**: 根据年级调整题目难度
- **选项合理性**: 干扰选项不能过于明显
- **计算准确性**: 确保所有计算正确
- **格式统一**: 题目格式标准化
#### 题目数据结构
```json
{
"id": "小学-1",
"stem": "计算2 + 3 = ?",
"options": [
{"key": "A", "text": "4", "isCorrect": false},
{"key": "B", "text": "5", "isCorrect": true},
{"key": "C", "text": "6", "isCorrect": false},
{"key": "D", "text": "7", "isCorrect": false}
],
"answer": "B"
}
```
#### 智能特性
- **自适应难度**: 根据用户表现调整难度
- **题型平衡**: 确保各种题型均匀分布
- **错误分析**: 分析常见错误类型
## 后端代码规范
### JavaScript代码规范
- **命名规范**: 使用camelCase命名变量和函数
- **函数长度**: 单个函数不超过40行
- **注释规范**: 关键函数和复杂逻辑必须添加注释
- **错误处理**: 使用try-catch处理异步操作
- **代码风格**: 遵循Google JavaScript规范
### 模块化规范
- **单一职责**: 每个模块只负责一个功能
- **依赖注入**: 通过参数传递依赖关系
- **接口设计**: 统一的API响应格式
- **错误处理**: 统一的错误处理机制
### 数据安全规范
- **输入验证**: 所有用户输入必须验证
- **数据加密**: 敏感数据使用适当加密
- **权限控制**: 实现用户权限验证
- **日志记录**: 记录关键操作和错误
### 1. 用户管理模块 (user-manager.js)
#### 功能概述
用户管理模块是整个后端系统的核心,负责处理所有用户相关的操作,包括注册、登录、密码管理、用户信息修改等。
#### 主要功能
- **用户注册**: 支持邮箱和用户名唯一性验证
- **密码管理**: SHA256加密存储支持密码设置和修改
- **用户信息管理**: 用户名修改、用户删除
- **数据持久化**: JSON文件存储支持数据备份和恢复
#### 核心方法
```javascript
// 用户注册
createUser(email, username)
// 密码设置
setPassword(email, password)
// 密码验证
verifyPassword(email, password)
// 密码修改
changePassword(email, oldPassword, newPassword)
// 用户名修改
changeUsername(email, newUsername)
// 用户删除
deleteUser(email)
```
#### 用户管理安全特性
- **密码加密**: 使用SHA256哈希算法加密存储
- **数据验证**: 严格的输入验证和格式检查
- **唯一性检查**: 邮箱和用户名的唯一性保证
- **错误处理**: 完善的异常处理和错误信息返回
#### 数据存储格式
```json
{
"email": "user@example.com",
"username": "用户名",
"password": "sha256加密后的密码",
"registeredAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
```
### 2. 邮箱服务模块 (multi-email-service.js)
#### 功能概述
邮箱服务模块负责处理所有邮件发送功能,支持多个邮箱服务商,提供验证码发送、邮件模板管理等功能。
#### 支持的服务商
- **QQ邮箱**: 支持SMTP协议需要授权码
- **163邮箱**: 支持SMTP协议需要授权码
- **其他SMTP服务**: 可扩展支持更多邮箱服务商
#### 核心功能
- **多服务商支持**: 自动选择可用的邮箱服务商
- **验证码发送**: 6位数字验证码生成和发送
- **邮件模板**: 统一的邮件格式和样式
- **错误处理**: 服务商切换和错误重试机制
#### 配置示例
```javascript
// 邮箱服务配置
const emailServices = {
qq: {
host: 'smtp.qq.com',
port: 587,
secure: false,
auth: {
user: process.env.SMTP_QQ_USER,
pass: process.env.SMTP_QQ_PASS
}
},
netease: {
host: 'smtp.163.com',
port: 587,
secure: false,
auth: {
user: process.env.SMTP_163_USER,
pass: process.env.SMTP_163_PASS
}
}
}
```
#### 邮件模板
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>验证码</title>
</head>
<body>
<h2>数学学习软件验证码</h2>
<p>您的验证码是:<strong>{code}</strong></p>
<p>验证码有效期为5分钟请及时使用。</p>
</body>
</html>
```
### 3. 题目生成模块 (question-generator.js)
#### 功能概述
题目生成模块是系统的智能核心,能够根据年级和数量要求自动生成数学题目,支持小学、初中、高中三个年级的题目类型。
#### 生成逻辑(与个人项目逻辑类似)
- **小学题目**: 基础四则运算、简单应用题
- **初中题目**: 代数运算、几何计算、函数基础
- **高中题目**: 复杂函数、三角函数、立体几何
#### 题目类型
1. **四则运算题**
- 基础加减乘除
- 带括号的复合运算
- 小数和分数运算
2. **应用题**
- 生活场景问题
- 逻辑推理题
- 图形计算题
3. **几何题**
- 面积和周长计算
- 角度计算
- 立体几何
#### 核心算法
```javascript
// 题目生成主流程
generateQuestions(grade, count) {
// 1. 根据年级选择题目类型
// 2. 生成题目内容
// 3. 计算正确答案
// 4. 生成干扰选项
// 5. 验证题目质量
// 6. 返回题目数组
}
// 小学题目生成
generatePrimaryQuestion() {
// 基础四则运算
// 简单应用题
// 图形计算
}
// 初中题目生成
generateMiddleSchoolQuestion() {
// 代数运算
// 几何计算
// 函数基础
}
// 高中题目生成
generateHighSchoolQuestion() {
// 复杂函数
// 三角函数
// 立体几何
}
```
#### 题目质量控制
- **难度梯度**: 根据年级调整题目难度
- **答案验证**: 确保答案正确性
- **选项设计**: 合理的干扰选项
- **重复检查**: 避免重复题目
#### 题目数据结构
```javascript
{
"id": "小学-1",
"stem": "计算2 + 3",
"options": [
{ "key": "A", "text": "5" },
{ "key": "B", "text": "6" },
{ "key": "C", "text": "4" },
{ "key": "D", "text": "7" }
],
"answer": "A",
"difficulty": "简单",
"type": "四则运算"
}
```
#### 智能特性
- **自适应难度**: 根据年级自动调整
- **题目去重**: 避免重复题目
- **答案验证**: 多重验证确保正确性
- **格式统一**: 标准化的题目格式
---
# 部署与问题
## 完整部署指南
### 开发环境部署
#### 1. 安装依赖
```bash
# 安装后端依赖(必需,否则应用启动会报错)
cd src/backend
npm install
# 安装前端依赖
cd ../frontend
npm install
```
**重要说明**: 后端依赖是必需的因为前端通过IPC调用后端工具类如`nodemailer`),如果后端`node_modules`不存在,应用会报错 `Cannot find module 'nodemailer'`
#### 2. 开发模式运行
```bash
# 启动应用(集成前后端)
npm start
```
### 生产环境部署
#### 1. 生成安装包
```bash
cd src/frontend
npm run build
```
生成安装包,包含:
- `数学学习软件-7.0.0-Setup.exe` - 安装包
- `win-unpacked/` - 解压后的可执行程序目录
#### 2. 分发安装程序
- 将安装包分发给用户
- 用户双击安装即可使用
- 无需额外配置或服务
---
## 常见问题解决
### npm 安装问题
#### 1. 清理环境
```bash
# 清理npm缓存
npm cache clean --force
# 删除node_modules和package-lock.json
Remove-Item -Recurse -Force node_modules -ErrorAction SilentlyContinue
Remove-Item -Force package-lock.json -ErrorAction SilentlyContinue
```
#### 2. 配置镜像源
```bash
# 设置国内镜像源
npm config set registry https://registry.npmmirror.com
# 设置Electron镜像
$env:ELECTRON_MIRROR = "https://npmmirror.com/mirrors/electron/"
```
#### 3. 优化网络配置(并非必需,网络良好的话不进行该配置也能正常安装)
```bash
# 设置下载超时时间
npm config set fetch-timeout 300000
# 设置重试机制
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
```
#### 4. 重新安装
```bash
npm install
```
### 邮箱服务问题
- 检查邮箱服务商SMTP设置
- 确认邮箱密码(使用授权码)
- 检查网络连接
- 验证邮箱服务商限制
### 端口冲突问题
- 修改环境变量中的PORT设置
- 检查端口占用情况
- 重启服务
### 数据文件问题
- 检查data目录权限
- 确认文件路径配置
- 备份重要数据
---
**项目状态**: 生产就绪 ✅
**最后更新**: 2025年10月12日
**架构**: 前后端分离 + 单一可执行程序 + 纯IPC通信
**参考规范**: [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) | [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)

@ -0,0 +1,16 @@
[
{
"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",
"registeredAt": "2025-10-12T08:56:13.598Z",
"username": "wqs",
"password": "be0c664d19564b568c4dd76d00a26a08ee89214877e52225eee5ffefab36c15f",
"updatedAt": "2025-10-12T08:56:13.606Z"
}
]

@ -0,0 +1,31 @@
// =============================================
// 数学学习软件 - 邮箱配置文件
// =============================================
// ==================== 选择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 = {};
*/

@ -0,0 +1,14 @@
{
"name": "math-learning-backend",
"version": "1.0.0",
"description": "小初高数学学习软件 - 后端业务逻辑",
"main": "utils/",
"scripts": {
"install-deps": "npm install"
},
"dependencies": {
"nodemailer": "^6.9.7"
},
"author": "Pair Backend",
"license": "MIT"
}

@ -0,0 +1,104 @@
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;

@ -0,0 +1,441 @@
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;

@ -0,0 +1,183 @@
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;

@ -0,0 +1,301 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const fs = require('fs');
// 获取后端文件路径
const getBackendPath = (relativePath) => {
if (app.isPackaged) {
// 打包后的路径
return path.join(process.resourcesPath, 'backend', relativePath);
} else {
// 开发环境路径
return path.join(__dirname, '..', 'backend', relativePath);
}
};
const UserManager = require(getBackendPath('utils/user-manager'));
const MultiEmailService = require(getBackendPath('utils/multi-email-service'));
const MathQuestionGenerator = require(getBackendPath('utils/question-generator'));
let mainWindow;
let backendApp;
let userManager;
let emailService;
// 初始化后端服务
function initBackend() {
// 创建Express应用
backendApp = express();
// 配置中间件
backendApp.use(cors());
backendApp.use(bodyParser.json());
// 实例化管理器
userManager = new UserManager();
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);
}
// 注册IPC处理器
ipcMain.handle('api:send-code', async (event, { email, username }) => {
try {
if (!email || !username) {
return { ok: false, message: '邮箱和用户名不能为空' };
}
if (!validateEmail(email)) {
return { ok: false, message: '邮箱格式不正确' };
}
if (!email.includes('@qq.com') && !email.includes('@163.com')) {
return { ok: false, message: '只支持QQ邮箱和163邮箱' };
}
if (userManager.findUserByUsername(username)) {
return { ok: false, message: '用户名已存在' };
}
if (userManager.findUserByEmail(email)) {
return { ok: false, message: '邮箱已被注册' };
}
const code = generateVerificationCode();
verificationCodes.set(email, {
code,
username,
timestamp: Date.now(),
});
const emailResult = await emailService.sendVerificationCode(email, code, username);
if (emailResult.success) {
return { ok: true, message: emailResult.message };
} else {
if (emailResult.debug) {
return { ok: true, message: emailResult.message + ' ' + emailResult.debug };
} else {
return { ok: false, message: emailResult.message };
}
}
} catch (error) {
console.error('发送验证码错误:', error);
return { ok: false, message: '服务器内部错误' };
}
});
ipcMain.handle('api:verify-code', async (event, { email, username, code }) => {
try {
if (!email || !username || !code) {
return { ok: false, message: '请填写完整信息' };
}
const storedData = verificationCodes.get(email);
if (!storedData) {
return { ok: false, message: '请先获取验证码' };
}
if (Date.now() - storedData.timestamp > 10 * 60 * 1000) {
verificationCodes.delete(email);
return { ok: false, message: '验证码已过期,请重新获取' };
}
if (storedData.code !== code) {
return { ok: false, message: '验证码不正确' };
}
if (storedData.username !== username) {
return { ok: false, message: '用户名与验证时不一致' };
}
verificationCodes.delete(email);
return { ok: true, message: '验证成功,请设置密码' };
} catch (error) {
console.error('验证码验证错误:', error);
return { ok: false, message: error.message };
}
});
ipcMain.handle('api:register', async (event, { email, username, password }) => {
try {
if (!email || !username || !password) {
return { ok: false, message: '请填写完整信息' };
}
userManager.createUser(email, username);
userManager.setPassword(email, password);
return { ok: true, message: '注册成功' };
} catch (error) {
console.error('注册错误:', error);
return { ok: false, message: error.message };
}
});
ipcMain.handle('api:login', async (event, { account, password }) => {
try {
if (!account || !password) {
return { ok: false, message: '请填写账号和密码' };
}
let user = userManager.findUserByEmail(account);
if (!user) {
user = userManager.findUserByUsername(account);
}
if (!user || !user.password) {
return { ok: false, message: '用户不存在' };
}
if (!userManager.verifyPassword(user.email, password)) {
return { ok: false, message: '密码不正确' };
}
return {
ok: true,
message: '登录成功',
data: {
email: user.email,
username: user.username,
},
};
} catch (error) {
console.error('登录错误:', error);
return { ok: false, message: error.message };
}
});
ipcMain.handle('api:change-password', async (event, { email, oldPassword, newPassword }) => {
try {
if (!email || !oldPassword || !newPassword) {
return { ok: false, message: '请填写完整信息' };
}
userManager.changePassword(email, oldPassword, newPassword);
return { ok: true, message: '密码修改成功' };
} catch (error) {
console.error('修改密码错误:', error);
return { ok: false, message: error.message };
}
});
ipcMain.handle('api:change-username', async (event, { email, username }) => {
try {
if (!email || !username) {
return { ok: false, message: '请填写完整信息' };
}
userManager.changeUsername(email, username);
return { ok: true, message: '用户名修改成功' };
} catch (error) {
console.error('修改用户名错误:', error);
return { ok: false, message: error.message };
}
});
ipcMain.handle('api:delete-account', async (event, { email, password }) => {
try {
if (!email || !password) {
return { ok: false, message: '请填写完整信息' };
}
if (!userManager.verifyPassword(email, password)) {
return { ok: false, message: '密码不正确' };
}
userManager.deleteUser(email);
return { ok: true, message: '账号删除成功' };
} catch (error) {
console.error('删除账号错误:', error);
return { ok: false, message: error.message };
}
});
ipcMain.handle('api:get-questions', async (event, { grade, count }) => {
try {
if (!grade || !count) {
return { ok: false, message: '请选择年级和题目数量' };
}
const countNum = parseInt(count);
if (isNaN(countNum) || countNum < 10 || countNum > 30) {
return { ok: false, message: '题目数量需在10-30之间' };
}
const generator = new MathQuestionGenerator();
const questions = generator.generateQuestions(grade, countNum);
return {
ok: true,
data: questions,
message: '题目生成成功',
};
} catch (error) {
console.error('生成题目失败:', error);
return { ok: false, message: '生成题目失败' };
}
});
}
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(() => {
// 初始化后端服务
initBackend();
// 创建窗口
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,224 @@
<!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="./renderer.js"></script>
<noscript>需要启用 JavaScript</noscript>
</body>
</html>

@ -0,0 +1,57 @@
{
"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",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"body-parser": "^1.20.2",
"nodemailer": "^6.9.7"
},
"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",
"node_modules/**/*"
],
"extraResources": [
{
"from": "../backend/",
"to": "backend/",
"filter": ["**/*"]
}
],
"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,89 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
closeApp: () => ipcRenderer.invoke('app:close')
});
// 暴露API接口使用IPC通信替代HTTP
contextBridge.exposeInMainWorld('API', {
async sendRegisterCode(email, username) {
try {
const result = await ipcRenderer.invoke('api:send-code', { email, username });
return result;
} catch (error) {
console.error('发送验证码失败:', error);
return { ok: false, message: '网络异常' };
}
},
async verifyCode(email, username, code) {
try {
const result = await ipcRenderer.invoke('api:verify-code', { email, username, code });
return result;
} catch (error) {
console.error('验证码验证失败:', error);
return { ok: false, message: '网络异常' };
}
},
async register(email, username, password) {
try {
const result = await ipcRenderer.invoke('api:register', { email, username, password });
return result;
} catch (error) {
console.error('注册失败:', error);
return { ok: false, message: '网络异常' };
}
},
async login(account, password) {
try {
const result = await ipcRenderer.invoke('api:login', { account, password });
return result;
} catch (error) {
console.error('登录失败:', error);
return { ok: false, message: '网络异常' };
}
},
async changePassword(email, oldPassword, newPassword) {
try {
const result = await ipcRenderer.invoke('api:change-password', { email, oldPassword, newPassword });
return result;
} catch (error) {
console.error('修改密码失败:', error);
return { ok: false, message: '网络异常' };
}
},
async changeUsername(email, username) {
try {
const result = await ipcRenderer.invoke('api:change-username', { email, username });
return result;
} catch (error) {
console.error('修改用户名失败:', error);
return { ok: false, message: '网络异常' };
}
},
async deleteAccount(email, password) {
try {
const result = await ipcRenderer.invoke('api:delete-account', { email, password });
return result;
} catch (error) {
console.error('删除账号失败:', error);
return { ok: false, message: '网络异常' };
}
},
async getQuestions(grade, count) {
try {
const result = await ipcRenderer.invoke('api:get-questions', { grade, count });
return result;
} catch (error) {
console.error('获取题目失败:', error);
return { ok: false, message: '网络异常' };
}
}
});

@ -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