合并前端内容 #7

Closed
pijtbanfl wants to merge 2 commits from wangqisheng_branch into main

@ -2,13 +2,13 @@
## 目录(所使用的代码规范地址在末尾)
### 📱 前端部分
- [项目概述](#前端项目概述) | [功能特性](#前端功能特性) | [技术栈](#前端技术栈) | [项目结构](#前端项目结构) | [快速开始](#前端快速开始)
- [功能验证流程](#前端功能验证流程) | [后端对接指南](#前端后端对接指南) | [前端核心模块说明](#前端核心模块说明) | [代码规范](#前端代码规范)
### 🖥️ 前端部分
- [前端项目概述](#前端项目概述) | [前端功能特性](#前端功能特性) | [前端技术栈](#前端技术栈) | [前端项目结构](#前端项目结构) | [前端快速开始](#前端快速开始)
- [前端使用指南](#前端使用指南) | [前端核心模块说明](#前端核心模块说明) | [前端代码规范](#前端代码规范)
### 🔧 后端部分
- [项目概述](#后端项目概述) | [功能特性](#后端功能特性) | [技术栈](#后端技术栈) | [项目结构](#后端项目结构) | [环境配置](#后端环境配置)
- [快速开始](#后端快速开始) | [API 接口文档](#api-接口文档) | [核心模块说明](#后端核心模块说明) | [数据存储](#数据存储) | [安全特性](#安全特性)
- [后端项目概述](#后端项目概述) | [后端功能特性](#后端功能特性) | [后端技术栈](#后端技术栈) | [后端项目结构](#后端项目结构) | [后端快速开始](#后端快速开始)
- [后端使用指南](#后端使用指南) | [后端核心模块说明](#后端核心模块说明) | [后端代码规范](#后端代码规范)
### 📋 部署与问题
- [完整部署指南](#完整部署指南)
@ -19,8 +19,7 @@
# 前端部分
## 前端项目概述
本前端基于 Electron提供从注册→密码设置→年级选择→输入题数→答题页→评分页的完整界面流程。**已内置测试功能**,无需后端即可验证所有基础功能。
本软件是一个基于 Electron 的桌面数学学习应用,采用前后端分离的模块化设计,最终打包为单一可执行程序。提供从用户注册→密码设置→年级选择→数学答题→成绩统计的完整学习流程。
## 前端功能特性
- ✅ 用户注册与邮箱验证码
@ -30,28 +29,32 @@
- ✅ 年级选择(小学/初中/高中)
- ✅ 数学题目生成与答题
- ✅ 成绩统计与显示
- ✅ 桌面应用打包
- ✅ 单一可执行程序(无需外部服务)
## 前端技术栈
- **框架**: Electron
- **桌面框架**: Electron
- **前端**: HTML5 + CSS3 + JavaScript (ES6+)
- **后端**: Node.js + Express集成到主进程
- **通信方式**: IPC进程间通信
- **数据存储**: JSON文件
- **样式**: 原生CSS符合Google CSS规范
- **代码规范**: Google JavaScript/CSS/HTML规范
## 前端项目结构
```
src/frontend/
├─ api/
│ ├─ httpApi.js # HTTP接口层
│ └─ mockApi.js # 模拟数据接口
├─ dist/ # 打包输出目录
├─ electron-main.js # Electron主进程
├─ preload.js # 预加载脚本
├─ index.html # 主页面
├─ renderer.js # 前端逻辑
├─ styles.css # 样式文件
├─ package.json # 项目配置
└─ package-lock.json # 依赖锁定
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 # 后端依赖配置
```
## 前端快速开始
@ -62,153 +65,101 @@ src/frontend/
### 2. 安装依赖
```bash
cd src/frontend
# 安装后端依赖(必需)
cd src/backend
npm install
# 安装前端依赖
cd ../frontend
npm install
```
### 3. 开发模式运行
```bash
npm run dev
npm start
```
**⚠️ 重要**: 必须先安装后端依赖,否则应用启动会报错 `Cannot find module 'nodemailer'`
### 4. 打包发布
```bash
npm run build
```
产物位于 `dist/` 目录,默认生成 NSIS 安装包。
## 前端功能验证流程
产物位于 `dist/` 目录,包含:
- `数学学习软件-7.0.0-Setup.exe` - 安装包
- `win-unpacked/` - 解压后的可执行程序目录
### 测试模式
当前为**测试模式**`TEST_MODE = true`),可以:
- 模拟邮箱验证码发送(界面直接显示验证码)
- 模拟用户注册和密码设置
- 自动生成小初高数学题目
- 完整的答题和评分流程
## 前端使用指南
### 1. 注册验证
### 1. 用户注册
- 输入用户名(必填,需唯一)
- 输入邮箱(`test@example.com`
- 点击"获取验证码"界面会显示验证码(如 `123456`
- 输入显示的验证码,点击"注册"
- 输入邮箱支持QQ邮箱和163邮箱
- 点击"获取验证码":系统会发送验证码到邮箱
- 输入收到的验证码,点击"注册"
### 2. 密码设置验证
### 2. 密码设置
- 设置密码6-10位含大小写字母和数字`Test123`
- 确认两次输入一致
- 点击"确认设置"
### 3. 用户信息管理
- 注册完成后,右上角显示"头像+用户名+修改按钮"
- 点击头像可选择本地图片更换
- 点击"修改用户名"可更改用户名(需唯一)
- 点击"修改密码"可更改密码
### 4. 答题功能验证
### 4. 数学学习
- 选择年级(小学/初中/高中)
- 输入题目数量10-30
- 点击"生成试卷"
- 逐题作答,查看最终分数
## 前端后端对接指南
### 1. 切换到真实后端
编辑 `api/httpApi.js`
```javascript
const BASE_URL = 'http://127.0.0.1:8080'; // 你的后端地址
const TEST_MODE = false; // 关闭测试模式
```
### 2. 后端接口要求
你的后端需要实现以下接口(返回 JSON 格式):
#### 发送验证码
- **接口**`POST /api/send-code`
- **请求体**`{ "email": "user@example.com", "username": "用户名" }`
- **返回**`{ "ok": true, "message": "验证码已发送" }`
#### 用户注册
- **接口**`POST /api/register`
- **请求体**`{ "email": "user@example.com", "username": "用户名", "code": "123456" }`
- **返回**`{ "ok": true, "message": "注册成功" }`
#### 设置密码
- **接口**`POST /api/set-password`
- **请求体**`{ "email": "user@example.com", "password": "密码" }`
- **返回**`{ "ok": true, "message": "密码设置成功" }`
#### 修改密码
- **接口**`POST /api/change-password`
- **请求体**`{ "email": "user@example.com", "oldPassword": "原密码", "newPassword": "新密码" }`
- **返回**`{ "ok": true, "message": "密码修改成功" }`
#### 修改用户名
- **接口**`POST /api/change-username`
- **请求体**`{ "email": "user@example.com", "username": "新用户名" }`
- **返回**`{ "ok": true, "message": "用户名修改成功" }`
#### 获取题目
- **接口**`GET /api/questions?grade=小学&count=10`
- **返回**`{ "ok": true, "data": [题目数组], "message": "题目生成成功" }`
### 3. 题目数据格式
```javascript
{
"ok": true,
"data": [
{
"id": "小学-1",
"stem": "计算2 + 3",
"options": [
{ "key": "A", "text": "5" },
{ "key": "B", "text": "6" },
{ "key": "C", "text": "4" },
{ "key": "D", "text": "7" }
],
"answer": "A"
}
]
}
```
## 前端核心模块说明
### 1. 主进程模块 (electron-main.js)
#### 功能概述
Electron主进程模块负责创建和管理应用程序窗口处理系统级事件,管理应用程序生命周期
Electron主进程模块负责创建和管理应用程序窗口同时集成了所有后端业务逻辑实现单一可执行程序。
#### 核心功能
- **窗口管理**: 创建和管理应用窗口
- **菜单配置**: 应用程序菜单设置
- **安全配置**: 安全策略和权限控制
- **后端逻辑集成**: 用户管理、邮箱验证、题目生成
- **IPC通信处理**: 处理渲染进程的API请求
- **数据管理**: JSON文件数据存储
- **生命周期管理**: 应用程序启动和关闭
#### 窗口配置
#### 集成架构
```javascript
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
// 动态路径处理
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)
#### 功能概述
渲染进程模块是前端逻辑的核心负责处理用户界面交互、数据管理、API调用等功能。
渲染进程模块是前端逻辑的核心,负责处理用户界面交互、数据管理、与主进程通信等功能。
#### 核心功能
- **界面管理**: 页面切换和状态管理
- **用户交互**: 表单处理、事件监听
- **数据管理**: 本地数据存储和同步
- **API调用**: 与后端服务通信
- **IPC通信**: 与主进程通信调用后端API
#### 主要功能模块
```javascript
@ -257,115 +208,33 @@ class UserManager {
### 3. 预加载脚本 (preload.js)
#### 功能概述
预加载脚本负责在渲染进程中安全地暴露Node.js API确保应用程序的安全性
预加载脚本负责在渲染进程中安全地暴露API接口实现与主进程的IPC通信
#### 预加载安全特性
- **API暴露**: 安全地暴露必要的Node.js API
#### 核心功能
- **API暴露**: 安全地暴露所有业务API
- **IPC通信**: 处理渲染进程与主进程的通信
- **权限控制**: 限制渲染进程的权限
- **数据验证**: 输入数据验证和清理
#### 暴露的API
```javascript
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作
selectFile: () => ipcRenderer.invoke('select-file'),
// 窗口操作
minimizeWindow: () => ipcRenderer.invoke('minimize-window'),
closeWindow: () => ipcRenderer.invoke('close-window')
});
```
### 4. API接口层 (api/)
#### HTTP接口模块 (httpApi.js)
负责与后端服务通信处理HTTP请求和响应。
#### 核心功能
- **请求封装**: 统一的HTTP请求处理
- **错误处理**: 网络错误和业务错误处理
- **数据转换**: 请求和响应数据格式转换
- **缓存管理**: 请求缓存和优化
#### 接口方法
```javascript
class HttpAPI {
// 发送验证码
async sendCode(email, username)
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 }),
// 用户注册
async register(userData)
// 用户管理
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 }),
// 设置密码
async setPassword(email, password)
// 获取题目
async getQuestions(grade, count)
// 用户信息管理
async changeUsername(email, username)
async changePassword(email, oldPassword, newPassword)
}
```
#### 模拟接口模块 (mockApi.js)
提供测试模式下的模拟数据,支持离线开发和测试。
#### 模拟功能
- **验证码模拟**: 界面显示验证码
- **用户数据模拟**: 本地用户数据管理
- **题目生成模拟**: 本地题目生成
- **成绩计算模拟**: 本地成绩计算
### 5. 样式系统 (styles.css)
#### 设计系统
- **颜色系统**: 统一的颜色变量和主题
- **字体系统**: 字体大小、行高、字重规范
- **间距系统**: 统一的间距和布局规范
- **组件样式**: 可复用的组件样式
#### CSS变量系统
```css
:root {
--bg: #0e1116;
--border: #202734;
--danger: #ef4444;
--muted: #9aa4b2;
--panel: #151a21;
--primary: #3b82f6;
--primary-2: #2563eb;
--success: #22c55e;
--text: #e8edf3;
}
// 题目管理
getQuestions: (grade, count) => ipcRenderer.invoke('api:get-questions', { grade, count })
});
```
#### 响应式设计
- **移动端适配**: 响应式布局设计
- **窗口适配**: 不同窗口尺寸适配
- **主题支持**: 深色主题设计
### 6. 用户界面组件
#### 页面组件
- **登录页面**: 用户认证界面
- **注册页面**: 用户注册界面
- **设置密码页面**: 密码设置界面
- **年级选择页面**: 年级选择界面
- **答题页面**: 题目答题界面
- **成绩页面**: 成绩显示界面
#### 交互组件
- **表单组件**: 输入框、按钮、选择器
- **模态框组件**: 确认对话框、提示框
- **导航组件**: 页面导航和状态指示
- **反馈组件**: 成功提示、错误提示
#### 用户体验
- **加载状态**: 加载动画和状态提示
- **错误处理**: 友好的错误提示
- **操作反馈**: 操作成功和失败反馈
- **数据验证**: 实时表单验证
## 前端代码规范
- 所有JavaScript文件符合Google JavaScript规范
@ -402,196 +271,213 @@ class HttpAPI {
## 后端项目结构
```
src/backend/
├─ data/
│ └─ users.json # 用户数据文件
├─ utils/
├─ data/
│ └─ users.json # 用户数据文件
├─ utils/
│ ├─ user-manager.js # 用户管理模块
│ ├─ multi-email-service.js # 多邮箱服务
│ └─ question-generator.js # 题目生成器
├─ email-config.js # 邮箱配置
├─ server.js # 主服务器文件
├─ package.json # 项目配置
└─ package-lock.json # 依赖锁定
└─ package.json # 项目配置
```
## 后端环境配置
### 1. 环境变量
`src/backend` 目录下创建 `.env` 文件:
```env
PORT=8080
NODE_ENV=development
SMTP_QQ_USER=your_qq@example.com
SMTP_QQ_PASS=your_smtp_password
SMTP_163_USER=your_163@example.com
SMTP_163_PASS=your_smtp_password
MAIL_FROM_NAME=数学学习软件
DATA_PATH=./data/users.json
```
### 2. 邮箱服务配置
支持多个邮箱服务商:
- QQ邮箱
- 163邮箱
- 其他SMTP服务还没有配置暂时只有这两个邮箱
## 后端快速开始
### 1. 安装依赖
```bash
cd src/backend
npm install
```
### 2. 启动服务
```bash
npm start
```
### 3. 验证服务
服务启动后会在终端显示:`Server listening on http://localhost:8080`
## API 接口文档
## 后端使用指南
### 基础信息
- **基础URL**: `http://localhost:8080`
- **数据格式**: JSON
- **请求方法**: GET/POST
### 1. API调用方式
- 所有API通过IPC通信调用无需HTTP请求
- 前端通过 `ipcRenderer.invoke` 调用主进程中的后端逻辑
### 1. 发送验证码
- **URL**: `POST /api/send-code`
- **请求体**:
```json
### 2. 用户管理流程
- 发送验证码 → 用户注册 → 设置密码
- 登录验证 → 修改信息 → 删除账户
{
"email": "user@example.com",
"username": "用户名"
}
```
### 3. 题目生成流程
- 选择年级 → 生成题目 → 返回题目数据
- 支持小学、初中、高中三个年级
- 自动生成选择题和计算题
- **响应**:
```json
## 后端核心模块说明
{
"ok": true,
"message": "验证码已发送"
}
```
### 1. 用户管理模块 (user-manager.js)
### 2. 用户注册
- **URL**: `POST /api/register`
- **请求体**:
```json
#### 功能概述
用户管理模块是整个后端系统的核心,负责处理所有用户相关的操作,包括注册、登录、密码管理、用户信息修改等。
{
"email": "user@example.com",
"username": "用户名",
"code": "123456"
}
```
- **响应**:
```json
#### 主要功能
- **用户注册**: 邮箱验证、用户名唯一性检查
- **用户登录**: 邮箱/用户名登录验证
- **密码管理**: 密码设置、修改、验证
- **用户信息**: 用户名修改、账户删除
- **数据存储**: JSON文件数据持久化
{
"ok": true,
"message": "注册成功"
}
```
#### 核心方法
- `createUser(email, username)`: 创建新用户
- `findUserByEmail(email)`: 根据邮箱查找用户
- `findUserByUsername(username)`: 根据用户名查找用户
- `updateUser(email, updates)`: 更新用户信息
- `deleteUser(email)`: 删除用户账户
- `verifyPassword(email, password)`: 验证密码
### 3. 设置密码
- **URL**: `POST /api/set-password`
- **请求体**:
```json
#### 用户管理安全特性
- **邮箱唯一性**: 每个邮箱只能注册一个账户
- **用户名唯一性**: 用户名必须唯一
- **密码安全**: 密码验证和加密存储
- **数据完整性**: 用户数据一致性检查
#### 数据存储格式
```json
{
"email": "user@example.com",
"password": "新密码"
"username": "用户名",
"registeredAt": "2024-01-01T00:00:00.000Z"
}
```
- **响应**:
```json
### 2. 邮箱服务模块 (multi-email-service.js)
{
"ok": true,
"message": "密码已设置"
}
```
#### 功能概述
邮箱服务模块负责发送验证码邮件,支持多个邮箱服务商,提供统一的邮件发送接口。
### 4. 修改密码
- **URL**: `POST /api/change-password`
- **请求体**:
```json
#### 支持的服务商
- **QQ邮箱**: 支持QQ邮箱SMTP服务
- **163邮箱**: 支持163邮箱SMTP服务
- **测试模式**: 开发环境下的模拟发送
{
"email": "user@example.com",
"oldPassword": "旧密码",
"newPassword": "新密码"
}
#### 核心功能
- **验证码生成**: 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'
}
};
```
- **响应**:
```json
{
"ok": true,
"message": "密码修改成功"
}
#### 邮件模板
```html
<h2>数学学习软件验证码</h2>
<p>您的验证码是:<strong>{{code}}</strong></p>
<p>验证码有效期为10分钟请及时使用。</p>
```
### 5. 修改用户名
- **URL**: `POST /api/change-username`
- **请求体**:
```json
### 3. 题目生成模块 (question-generator.js)
{
"email": "user@example.com",
"username": "新用户名"
}
```
#### 功能概述
题目生成模块负责根据年级和数量生成数学题目,支持小学、初中、高中三个年级的题目生成。
- **响应**:
```json
#### 生成逻辑(与个人项目逻辑类似)
- **年级适配**: 根据年级调整题目难度
- **题型多样**: 选择题、计算题、应用题
- **选项生成**: 智能生成干扰选项
- **答案验证**: 确保答案正确性
{
"ok": true,
"message": "用户名修改成功"
#### 题目类型
- **小学**: 基础四则运算、简单应用题
- **初中**: 代数运算、几何计算、函数基础
- **高中**: 复杂代数、三角函数、立体几何
#### 核心算法
```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
};
}
```
### 6. 获取题目
- **URL**: `GET /api/questions?grade=小学&count=10`
- **参数**:
- `grade`: 年级(小学/初中/高中)
- `count`: 题目数量10-30
- **响应**:
#### 题目质量控制
- **难度梯度**: 根据年级调整题目难度
- **选项合理性**: 干扰选项不能过于明显
- **计算准确性**: 确保所有计算正确
- **格式统一**: 题目格式标准化
#### 题目数据结构
```json
{
"ok": true,
"data": [
{
"id": "小学-1",
"stem": "计算2 + 3",
"options": [
{ "key": "A", "text": "5" },
{ "key": "B", "text": "6" },
{ "key": "C", "text": "4" },
{ "key": "D", "text": "7" }
],
"answer": "A"
}
"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}
],
"message": "题目生成成功"
"answer": "B"
}
```
## 后端核心模块说明
#### 智能特性
- **自适应难度**: 根据用户表现调整难度
- **题型平衡**: 确保各种题型均匀分布
- **错误分析**: 分析常见错误类型
## 后端代码规范
### JavaScript代码规范
- **命名规范**: 使用camelCase命名变量和函数
- **函数长度**: 单个函数不超过40行
- **注释规范**: 关键函数和复杂逻辑必须添加注释
- **错误处理**: 使用try-catch处理异步操作
- **代码风格**: 遵循Google JavaScript规范
### 模块化规范
- **单一职责**: 每个模块只负责一个功能
- **依赖注入**: 通过参数传递依赖关系
- **接口设计**: 统一的API响应格式
- **错误处理**: 统一的错误处理机制
### 数据安全规范
- **输入验证**: 所有用户输入必须验证
- **数据加密**: 敏感数据使用适当加密
- **权限控制**: 实现用户权限验证
- **日志记录**: 记录关键操作和错误
### 1. 用户管理模块 (user-manager.js)
@ -788,98 +674,7 @@ generateHighSchoolQuestion() {
- **答案验证**: 多重验证确保正确性
- **格式统一**: 标准化的题目格式
### 4. 服务器主模块 (server.js)
#### 功能概述
服务器主模块是整个后端系统的入口负责处理HTTP请求、路由分发、中间件配置等核心功能。
#### 核心功能
- **HTTP服务器**: 基于Express框架的Web服务器
- **路由管理**: RESTful API路由设计
- **中间件配置**: 跨域、解析、错误处理等
- **端口监听**: 可配置的端口监听
#### API路由设计
```javascript
// 用户相关路由
app.post('/api/send-code', sendCodeHandler);
app.post('/api/register', registerHandler);
app.post('/api/set-password', setPasswordHandler);
app.post('/api/change-password', changePasswordHandler);
app.post('/api/change-username', changeUsernameHandler);
// 题目相关路由
app.get('/api/questions', getQuestionsHandler);
```
#### 错误处理
- **统一错误格式**: 标准化的错误响应
- **日志记录**: 详细的错误日志
- **异常捕获**: 全局异常处理机制
#### 安全配置
- **CORS设置**: 跨域请求处理
- **请求验证**: 输入数据验证
- **错误信息**: 安全的错误信息返回
### 5. 邮箱配置模块 (email-config.js)
#### 功能概述
邮箱配置模块负责管理所有邮箱相关的配置,包括服务商配置、模板配置、发送参数等。
#### 配置管理
- **环境变量**: 从.env文件读取配置
- **服务商配置**: 多邮箱服务商支持
- **模板管理**: 邮件模板配置
- **参数验证**: 配置参数验证
#### 配置结构
```javascript
const emailConfig = {
services: {
qq: { /* QQ邮箱配置 */ },
netease: { /* 163邮箱配置 */ }
},
templates: {
verification: { /* 验证码模板 */ }
},
settings: {
timeout: 30000,
retryCount: 3
}
}
```
### 6. 数据管理
#### 数据存储
- **文件存储**: JSON文件存储用户数据
- **数据备份**: 自动备份机制
- **数据恢复**: 数据恢复功能
- **数据验证**: 数据完整性检查
#### 数据安全
- **加密存储**: 敏感数据加密
- **访问控制**: 数据访问权限控制
- **备份策略**: 定期数据备份
- **恢复机制**: 数据恢复流程
#### 性能优化
- **缓存机制**: 数据缓存策略
- **异步处理**: 异步数据操作
- **内存管理**: 内存使用优化
- **并发控制**: 并发请求处理
## 数据存储
- **用户数据**: `src/backend/data/users.json`
- **数据格式**: JSON数组
- **加密方式**: SHA256
## 安全特性
- 密码SHA256加密存储
- 邮箱验证码验证
- 用户名唯一性检查
- 输入数据验证
---
@ -889,46 +684,40 @@ const emailConfig = {
### 开发环境部署
#### 1. 后端部署
#### 1. 安装依赖
```bash
# 进入后端目录
# 安装后端依赖(必需,否则应用启动会报错)
cd src/backend
npm install
# 安装依赖
# 安装前端依赖
cd ../frontend
npm install
```
**重要说明**: 后端依赖是必需的因为前端通过IPC调用后端工具类如`nodemailer`),如果后端`node_modules`不存在,应用会报错 `Cannot find module 'nodemailer'`
# 配置环境变量(创建.env文件
# 启动服务
#### 2. 开发模式运行
```bash
# 启动应用(集成前后端)
npm start
```
#### 2. 前端部署
### 生产环境部署
#### 1. 生成安装包
```bash
# 进入前端目录
cd src/frontend
# 安装依赖
npm install
# 开发模式运行
npm run dev
# 或打包发布
npm run build
```
生成安装包,包含:
- `数学学习软件-7.0.0-Setup.exe` - 安装包
- `win-unpacked/` - 解压后的可执行程序目录
### 生产环境部署
#### 1. 后端部署
- 配置生产环境变量
- 设置邮箱服务商
- 配置数据备份
- 使用PM2等进程管理工具
#### 2. 前端部署
- 打包为桌面应用
- 生成安装包
- 分发安装程序
#### 2. 分发安装程序
- 将安装包分发给用户
- 用户双击安装即可使用
- 无需额外配置或服务
---
@ -992,29 +781,8 @@ npm install
---
## 项目特色
### 代码质量
- ✅ 完全符合Google代码规范
- ✅ 统一的代码风格
- ✅ 完整的错误处理
- ✅ 详细的注释说明
### 用户体验
- ✅ 直观的界面设计
- ✅ 流畅的操作体验
- ✅ 完善的错误提示
- ✅ 响应式布局
### 技术架构
- ✅ 前后端分离
- ✅ 模块化设计
- ✅ 可扩展架构
- ✅ 安全可靠
---
**项目状态**: 生产就绪 ✅
**最后更新**: 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)
**参考规范**: [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) | [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)

@ -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,7 +1,255 @@
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({
@ -25,7 +273,12 @@ function createWindow() {
});
}
app.whenReady().then(createWindow);
app.whenReady().then(() => {
// 初始化后端服务
initBackend();
// 创建窗口
createWindow();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
@ -39,7 +292,7 @@ app.on('activate', () => {
}
});
// 窗口关闭请求(用于“退出应用”
// 窗口关闭请求(用于"退出应用"
ipcMain.handle('app:close', () => {
if (mainWindow) {
mainWindow.close();

@ -218,7 +218,6 @@
</div>
</div>
<script src="./api/httpApi.js"></script>
<script src="./renderer.js"></script>
<noscript>需要启用 JavaScript</noscript>
</body>

File diff suppressed because it is too large Load Diff

@ -11,6 +11,12 @@
},
"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"
@ -24,7 +30,14 @@
"renderer.js",
"electron-main.js",
"preload.js",
"api/**/*"
"node_modules/**/*"
],
"extraResources": [
{
"from": "../backend/",
"to": "backend/",
"filter": ["**/*"]
}
],
"win": {
"target": ["nsis"],

@ -4,3 +4,86 @@ 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: '网络异常' };
}
}
});

Loading…
Cancel
Save