功能测试正常切可用的第一版 #1

Merged
Coldin04 merged 9 commits from develop into main 2025-04-25 13:36:24 +00:00
17 changed files with 1688 additions and 39 deletions

61
.pnp.cjs generated
View file

@ -37,6 +37,7 @@ const RAW_RUNTIME_STATE =
["eslint-plugin-vue", "virtual:7d6457be6b857bdb44066d1c6f9d2471ca71cf43c0df847ead5ae9289c5e9f98de3a67668fdd588c95fcb212da2ba4f12a34babd1f52380566b5d7bf44a88ca5#npm:10.0.0"],\
["jiti", "npm:2.4.2"],\
["npm-run-all2", "npm:7.0.2"],\
["peerjs", "npm:1.5.4"],\
["pinia", "virtual:7d6457be6b857bdb44066d1c6f9d2471ca71cf43c0df847ead5ae9289c5e9f98de3a67668fdd588c95fcb212da2ba4f12a34babd1f52380566b5d7bf44a88ca5#npm:3.0.2"],\
["prettier", "npm:3.5.3"],\
["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>::version=5.8.3&hash=5786d5"],\
@ -1001,6 +1002,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@msgpack/msgpack", [\
["npm:2.8.0", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/@msgpack-msgpack-npm-2.8.0-c4768c71d4-10c0.zip/node_modules/@msgpack/msgpack/",\
"packageDependencies": [\
["@msgpack/msgpack", "npm:2.8.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@nodelib/fs.scandir", [\
["npm:2.1.5", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-10c0.zip/node_modules/@nodelib/fs.scandir/",\
@ -2107,6 +2117,7 @@ const RAW_RUNTIME_STATE =
["eslint-plugin-vue", "virtual:7d6457be6b857bdb44066d1c6f9d2471ca71cf43c0df847ead5ae9289c5e9f98de3a67668fdd588c95fcb212da2ba4f12a34babd1f52380566b5d7bf44a88ca5#npm:10.0.0"],\
["jiti", "npm:2.4.2"],\
["npm-run-all2", "npm:7.0.2"],\
["peerjs", "npm:1.5.4"],\
["pinia", "virtual:7d6457be6b857bdb44066d1c6f9d2471ca71cf43c0df847ead5ae9289c5e9f98de3a67668fdd588c95fcb212da2ba4f12a34babd1f52380566b5d7bf44a88ca5#npm:3.0.2"],\
["prettier", "npm:3.5.3"],\
["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>::version=5.8.3&hash=5786d5"],\
@ -2765,6 +2776,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["eventemitter3", [\
["npm:4.0.7", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/eventemitter3-npm-4.0.7-7afcdd74ae-10c0.zip/node_modules/eventemitter3/",\
"packageDependencies": [\
["eventemitter3", "npm:4.0.7"]\
],\
"linkType": "HARD"\
}]\
]],\
["execa", [\
["npm:9.5.2", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/execa-npm-9.5.2-f74ad877bc-10c0.zip/node_modules/execa/",\
@ -3951,6 +3971,28 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["peerjs", [\
["npm:1.5.4", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/peerjs-npm-1.5.4-b1f39eb912-10c0.zip/node_modules/peerjs/",\
"packageDependencies": [\
["peerjs", "npm:1.5.4"],\
["@msgpack/msgpack", "npm:2.8.0"],\
["eventemitter3", "npm:4.0.7"],\
["peerjs-js-binarypack", "npm:2.1.0"],\
["webrtc-adapter", "npm:9.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["peerjs-js-binarypack", [\
["npm:2.1.0", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/peerjs-js-binarypack-npm-2.1.0-59ed32add7-10c0.zip/node_modules/peerjs-js-binarypack/",\
"packageDependencies": [\
["peerjs-js-binarypack", "npm:2.1.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["perfect-debounce", [\
["npm:1.0.0", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/perfect-debounce-npm-1.0.0-0968810009-10c0.zip/node_modules/perfect-debounce/",\
@ -4226,6 +4268,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["sdp", [\
["npm:3.2.0", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/sdp-npm-3.2.0-b1a5f85233-10c0.zip/node_modules/sdp/",\
"packageDependencies": [\
["sdp", "npm:3.2.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["semver", [\
["npm:6.3.1", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/semver-npm-6.3.1-bcba31fdbe-10c0.zip/node_modules/semver/",\
@ -4959,6 +5010,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["webrtc-adapter", [\
["npm:9.0.3", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/webrtc-adapter-npm-9.0.3-9d13b6cc5b-10c0.zip/node_modules/webrtc-adapter/",\
"packageDependencies": [\
["webrtc-adapter", "npm:9.0.3"],\
["sdp", "npm:3.2.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["which", [\
["npm:2.0.2", {\
"packageLocation": "../../../../AppData/Local/Yarn/Berry/cache/which-npm-2.0.2-320ddf72f7-10c0.zip/node_modules/which/",\

Binary file not shown.

160
README.md
View file

@ -1,39 +1,161 @@
# .
<p align="center">
<img src="./src/assets/logo.svg" alt="Arithma Battle Logo" width="200">
</p>
This template should help get you started developing with Vue 3 in Vite.
# Arithma Battle
## Recommended IDE Setup
基于Vue.js的心算竞技场 | 实时对战PK × 渐进式训练
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
✓ 成绩追踪
## Type Support for `.vue` Imports in TS
📊 使用Vue.js构建的网页端数学训练平台融合游戏化设计支持多难度级别、排行榜、响应式布局适合教育与娱乐场景。
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
🚀 通过组件化开发实现高效交互,后续计划扩展社交分享与成就系统。
## Customize configuration
## 项目概述
See [Vite Configuration Reference](https://vite.dev/config/).
ArithmaBattle 是一款基于网页的交互式心算训练与竞技平台旨在通过趣味PK模式和单机练习模式提升用户的计算速度和数学能力。
## Project Setup
### 核心功能:
```sh
yarn
```
- 实时对战模式(支持加减乘除运算)
- 多难度级别适应不同年龄段用户
- 成绩追踪系统记录进步轨迹
- 响应式设计适配各种设备
### Compile and Hot-Reload for Development
### 项目特色:
- ✓ 将数学练习与游戏对战完美结合
- ✓ 单人训练与多人PK双模式
- ✓ 竞技机制激发学习动力
- ✓ 简洁直观的用户界面
- ✓ 适合课堂教学或家庭娱乐
```sh
无论是想提升计算能力的学生,还是喜欢数字游戏的爱好者,都能在这里找到挑战乐趣。
## 在线演示
[![Demo](https://img.shields.io/badge/demo-online-green)](https://ab.co1d.in)
上述链接为演示部署版本。
## 技术栈
- 前端框架Vue.js
- UI组件自定义组件
- 状态管理Vuex
- 路由管理Vue Router
- 构建工具Vite
- CSS预处理SCSS/LESS
- 响应式设计Flexbox/Grid
## 安装与运行
### 前提条件
- Node.js (v14.0+)
- npm 或 yarn
### 安装步骤
```bash
# 克隆仓库
git clone https://github.com/Coldin04/ArithmaBattle.git
# 进入项目目录
cd ArithmaBattle
# 安装依赖
yarn install
# 启动开发服务器
yarn dev
```
### Type-Check, Compile and Minify for Production
### 构建生产版本
```bash
```sh
yarn build
```
### Lint with [ESLint](https://eslint.org/)
## 项目结构
```sh
yarn lint
```
ArithmaBattle/
├── public/ # 静态资源目录
│ ├── favicon.ico # 网站图标
│ └── index.html # HTML入口文件
├── src/ # 源代码目录
│ ├── assets/ # 资源文件(图片、音效等)
│ ├── components/ # 组件目录
│ │ ├── common/ # 通用组件
│ │ │ ├── Button.vue
│ │ │ └── Modal.vue
│ │ ├── game/ # 游戏相关组件
│ │ │ ├── ProblemDisplay.vue # 数学题目显示
│ │ │ ├── Timer.vue # 计时器组件
│ │ │ ├── ScoreBoard.vue # 得分板组件
│ │ │ └── AnswerInput.vue # 答案输入组件
│ │ └── layout/ # 布局组件
│ │ ├── Header.vue
│ │ └── Footer.vue
│ ├── views/ # 页面视图组件
│ │ ├── Home.vue # 首页
│ │ ├── Practice.vue # 练习模式
│ │ ├── Battle.vue # 对战模式
│ │ ├── Profile.vue # 用户资料
│ │ └── Leaderboard.vue # 排行榜
│ ├── router/ # 路由配置
│ │ └── index.js
│ ├── store/ # Vuex状态管理
│ │ ├── index.js
│ │ └── modules/ # 状态模块
│ │ ├── game.js
│ │ └── user.js
│ ├── styles/ # 全局样式文件
│ │ ├── variables.scss
│ │ └── global.scss
│ ├── utils/ # 工具函数
│ │ ├── arithmetic.js # 数学运算生成
│ │ └── storage.js # 本地存储处理
│ ├── App.vue # 根组件
│ └── main.js # 应用入口
├── .gitignore # Git忽略配置
├── package.json # 项目依赖与脚本
├── vite.config.js # Vite配置
└── README.md # 项目说明文档
```
## 功能展示
![练习模式](./screenshots/practice-mode.png)
![对战模式](./screenshots/battle-mode.png)
## 贡献指南
欢迎对ArithmaBattle项目做出贡献请按照以下步骤参与
1. Fork本仓库
2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启一个Pull Request
## 开发计划
- [ ] 添加更多数学运算类型(如指数、开方等)
- [ ] 实现多人在线对战房间
- [ ] 添加成就系统
- [ ] 开发移动应用版本
- [ ] 支持自定义题目难度
## 许可证
本项目采用MIT许可证 - 查看[LICENSE](LICENSE)文件了解详情
## 联系方式
项目维护者: Coldin04 - https://co1d.in
项目链接: [https://github.com/yourusername/ArithmaBattle](https://github.com/Coldin04/ArithmaBattle)

View file

@ -13,6 +13,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"peerjs": "^1.5.4",
"pinia": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
@ -28,7 +29,7 @@
"eslint-plugin-vue": "~10.0.0",
"jiti": "^2.4.2",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.3",
"prettier": "^3.5.3",
"typescript": "~5.8.0",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

BIN
screenshots/battle-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

View file

@ -5,16 +5,9 @@ import HelloWorld from './components/HelloWorld.vue'
<template>
<header>
<RouterLink to="/">
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</RouterLink>
</header>
<RouterView />

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745571679148" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4929" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M430.08 20.48H86.016c-40.448 0-57.344 24.064-57.344 57.344 0 98.304 0.512 260.096 0.512 336.896 0 40.448 20.992 65.024 56.832 65.024h344.576c40.448 0 57.344-15.36 57.344-57.344V77.824A56.448 56.448 0 0 0 430.08 20.48z m-58.368 239.616c-3.584 12.288-11.776 18.432-24.576 18.432h-179.2c-18.432 0-29.696-18.944-23.552-38.912 3.584-11.776 11.776-18.432 24.064-18.432h178.176c19.456 0.512 31.232 18.432 25.088 38.912zM946.688 20.48H602.112c-33.792 0-57.344 20.992-57.344 57.344V422.4c0 31.232 25.088 57.344 57.344 57.344h344.576c30.208 0 57.344-23.04 57.344-57.344V77.824c0-36.352-23.552-57.344-57.344-57.344z m-86.016 258.048h-57.344v57.344c0 11.264-13.824 28.672-28.672 28.672-12.8 0-28.672-12.8-28.672-28.672-0.512-34.816 0-57.344 0-57.344s-22.528 0.512-57.344 0c-11.264 0-28.672-13.312-28.672-28.672 0-12.288 13.824-28.672 28.672-28.672h57.344s-0.512-23.04 0-57.344c0-16.384 13.312-29.184 28.672-29.184 12.8 0 28.672 11.264 28.672 29.184v57.344s23.552-0.512 57.344 0c16.896 0 29.184 16.384 29.184 28.672 0 13.312-18.432 28.672-29.184 28.672zM430.08 537.088H85.504c-43.008 0-57.344 12.8-57.344 57.344v344.576c0 35.328 27.136 57.344 57.344 57.344H430.08c40.448 0 57.344-15.36 57.344-57.344V594.432a55.68 55.68 0 0 0-57.344-57.344z m-68.096 333.824c-13.312 12.8-31.744 13.312-45.056 0.512a2462.272 2462.272 0 0 1-51.712-51.712c-2.048-2.048-3.584-5.12-5.632-8.192-21.504 21.504-40.448 40.96-59.392 59.904-11.264 11.264-24.064 12.8-37.376 5.632-13.312-7.168-20.992-22.016-17.408-35.84a36.864 36.864 0 0 1 10.752-17.408c18.432-18.944 37.376-37.376 56.32-56.832a84.992 84.992 0 0 1-6.656-7.168c-17.408-17.92-35.328-35.328-52.736-53.248a30.208 30.208 0 0 1-3.072-38.4c8.704-13.312 24.576-19.456 37.888-14.336 5.12 2.048 10.24 6.144 14.336 9.728 18.944 18.432 37.376 37.376 57.344 57.856 5.12-6.144 9.728-11.776 14.848-16.896 12.8-13.312 26.112-26.112 39.424-39.424 16.384-16.384 34.816-17.408 49.152-2.56 14.336 14.336 13.312 32.768-2.56 48.64-16.384 16.384-32.256 32.768-48.64 49.152-2.048 2.048-3.584 4.096-6.144 6.656 10.24 10.24 20.48 20.48 30.72 30.208 9.216 9.216 18.944 18.432 28.16 28.16 11.776 13.312 10.752 32.256-2.56 45.568z m642.048-276.48c0-28.672-30.72-57.344-57.344-57.344H602.112c-29.696 0-57.344 30.208-57.344 57.344v344.576c0 25.088 28.672 57.344 57.344 57.344h344.576c40.448 0 57.344-27.136 57.344-69.12 0.512-99.84 0.512-233.472 0-332.8z m-143.36 258.048h-169.984c-22.528 0-30.72-13.312-30.72-28.672 0-16.384 13.824-28.672 28.672-28.672h172.032c14.336 0 28.672 11.776 28.672 28.672 0 15.872-17.408 28.672-28.672 28.672z m0-114.688c-64.512 0.512-85.504 0-172.032 0-11.264 0-28.672-11.776-28.672-28.672 0-15.872 13.824-28.672 24.576-28.672h176.128c13.824 0.512 28.672 14.848 28.672 28.672 0 14.336-14.848 28.672-28.672 28.672z" p-id="4930"></path></svg>

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

View file

@ -0,0 +1,394 @@
.network-battle-page {
background: linear-gradient(135deg, #f7f9fc, #e2e7ed);
max-width: 800px;
margin: 2rem auto;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
text-align: center;
padding: 2rem;
position: relative;
}
.network-battle-page h1 {
font-size: 2.2rem;
color: #333;
margin-bottom: 1.5rem;
}
.connection-section {
background-color: rgba(255, 255, 255, 0.7);
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.connection-type {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.connection-type button {
background: rgba(255, 255, 255, 0.8);
color: #555;
border: 2px solid #ddd;
border-radius: 30px;
padding: 0.7rem 1.5rem;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.connection-type button.active {
background: linear-gradient(90deg, #84b9ff, #32a8ff, #0faeff);
color: white;
border-color: transparent;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.host-section, .join-section {
margin-top: 1rem;
}
.local-info {
background-color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
}
.peer-id {
font-weight: bold;
color: #0077cc;
background: rgba(0, 119, 204, 0.1);
padding: 0.3rem 0.6rem;
border-radius: 4px;
margin: 0 0.3rem;
}
.copy-button {
background: rgba(0, 119, 204, 0.1);
color: #0077cc;
border: 1px solid #0077cc;
border-radius: 20px;
padding: 0.3rem 0.8rem;
font-size: 0.9rem;
margin-left: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
}
.copy-button:hover {
background: rgba(0, 119, 204, 0.2);
}
.input-group {
margin-bottom: 1rem;
}
.input-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: #555;
}
.input-group input {
padding: 0.8rem;
font-size: 1rem;
border-radius: 20px;
border: 2px solid #ddd;
width: 80%;
max-width: 400px;
text-align: center;
outline: none;
transition: all 0.3s ease;
}
.input-group input:focus {
border-color: #007ACC;
box-shadow: 0 0 0 2px rgba(0,122,204,0.2);
}
.game-settings {
margin: 1.5rem 0;
}
label {
margin-left: 1rem;
font-weight: bold;
color: #555;
}
select {
padding: 0.6rem 1rem;
font-size: 1rem;
border-radius: 20px;
border: 2px solid #ddd;
background-color: #f8f9fa;
color: #555;
cursor: pointer;
outline: none;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
select:focus {
border-color: #007ACC;
box-shadow: 0 0 0 2px rgba(0,122,204,0.2);
}
button {
background: linear-gradient(90deg, #84b9ff, #32a8ff, #0faeff);
color: white;
border: none;
border-radius: 30px;
padding: 0.7rem 1.5rem;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin: 0.5rem;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0,0,0,0.15);
}
button:disabled {
background: #cccccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.players-info {
display: flex;
justify-content: space-around;
margin-bottom: 1.5rem;
}
.player {
background-color: rgba(255, 255, 255, 0.7);
border-radius: 10px;
padding: 1rem;
width: 45%;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.player-turn {
border-color: #32a8ff;
background-color: rgba(50, 168, 255, 0.1);
box-shadow: 0 0 8px rgba(50, 168, 255, 0.3);
}
.player h3 {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
color: #333;
}
.score {
font-size: 1.4rem;
font-weight: bold;
margin: 0.5rem 0;
color: #42b883;
}
.question-section p {
font-size: 1.8rem;
font-weight: bold;
margin: 1.5rem 0;
color: #333;
}
.question-section input {
margin: 1rem 0;
padding: 0.8rem;
font-size: 1.1rem;
border-radius: 20px;
border: 2px solid #ddd;
width: 200px;
text-align: center;
outline: none;
transition: all 0.3s ease;
}
.question-section input:focus {
border-color: #007ACC;
box-shadow: 0 0 0 2px rgba(0,122,204,0.2);
}
.question-section input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.timer-section p {
font-size: 1.4rem;
font-weight: bold;
color: #e74c3c;
}
.feedback-container {
height: 30px;
margin: 10px 0;
position: relative;
}
.feedback {
padding: 6px 12px;
border-radius: 20px;
display: inline-block;
font-weight: bold;
position: absolute;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.feedback.correct {
background-color: rgba(66, 184, 131, 0.2);
color: #2a8c5f;
border: 1px solid #42b883;
}
.feedback.wrong {
background-color: rgba(231, 76, 60, 0.2);
color: #c0392b;
border: 1px solid #e74c3c;
}
.correct-answer {
animation: pulse-green 0.5s;
border-color: #42b883 !important;
}
.wrong-answer {
animation: pulse-red 0.5s;
border-color: #e74c3c !important;
}
.game-messages {
height: 40px;
margin: 1rem 0;
display: flex;
justify-content: center;
align-items: center;
}
.game-message {
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: bold;
animation: fade-in 0.5s;
}
.game-message.success {
background-color: rgba(66, 184, 131, 0.2);
color: #2a8c5f;
}
.game-message.error {
background-color: rgba(231, 76, 60, 0.2);
color: #c0392b;
}
.game-message.info {
background-color: rgba(52, 152, 219, 0.2);
color: #2980b9;
}
.game-result-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
animation: fade-in 0.5s;
}
.game-result {
background-color: white;
border-radius: 12px;
padding: 2rem;
width: 90%;
max-width: 500px;
text-align: center;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.game-result h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: #333;
}
.final-scores {
display: flex;
justify-content: space-around;
margin-bottom: 1.5rem;
}
.final-scores p {
font-size: 1.2rem;
font-weight: bold;
}
.result-message {
font-size: 2rem;
margin: 1.5rem 0;
}
.result-message.win {
color: #42b883;
}
.result-message.lose {
color: #e74c3c;
}
.result-message.draw {
color: #f39c12;
}
/* 动画效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s, transform 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-10px) translateX(-50%);
}
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(66, 184, 131, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(66, 184, 131, 0); }
100% { box-shadow: 0 0 0 0 rgba(66, 184, 131, 0); }
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(231, 76, 60, 0); }
100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}

220
src/assets/practice.css Normal file
View file

@ -0,0 +1,220 @@
.practice-page {
background: linear-gradient(135deg, #f7f9fc, #e2e7ed);
max-width: 800px;
margin: 2rem auto;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
text-align: center;
padding: 2rem;
}
.practice-page h1 {
font-size: 2.2rem;
color: #333;
margin-bottom: 1.5rem;
}
header label {
margin-left: 1rem;
font-weight: bold;
color: #555;
}
header select {
padding: 0.6rem 1rem;
font-size: 1rem;
border-radius: 20px;
border: 2px solid #ddd;
background-color: #f8f9fa;
color: #555;
cursor: pointer;
outline: none;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
header select:focus {
border-color: #007ACC;
box-shadow: 0 0 0 2px rgba(0,122,204,0.2);
}
button {
background: linear-gradient(90deg, #84b9ff, #32a8ff, #0faeff);
color: white;
border: none;
border-radius: 30px;
padding: 0.7rem 1.5rem;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin: 0.5rem;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0,0,0,0.15);
background: linear-gradient(90deg, #84b9ff, #32a8ff, #0faeff);
}
header button {
margin-top: 1.5rem;
padding: 0.7rem 1.8rem;
}
.question-section p {
font-size: 1.8rem;
font-weight: bold;
margin: 1.5rem 0;
color: #333;
}
.question-section input {
margin: 1rem 0;
padding: 0.8rem;
font-size: 1.1rem;
border-radius: 20px;
border: 2px solid #ddd;
width: 200px;
text-align: center;
outline: none;
transition: all 0.3s ease;
}
.question-section input:focus {
border-color: #007ACC;
box-shadow: 0 0 0 2px rgba(0,122,204,0.2);
}
.timer-section p {
font-size: 1.4rem;
font-weight: bold;
color: #e74c3c;
}
.score-section p {
font-size: 1.4rem;
font-weight: bold;
color: #42b883;
}
footer {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #ddd;
}
footer p {
font-size: 1.2rem;
font-weight: bold;
color: #555;
margin-bottom: 1rem;
}
footer ul {
list-style-type: none;
padding: 0;
margin: 1rem 0;
max-height: 300px;
overflow-y: auto;
/* 移除背景色和内边距,使其更扁平 */
background-color: transparent;
border-radius: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
footer li {
margin: 0;
padding: 0.8rem 1rem;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
transition: all 0.2s ease;
border-left: 3px solid #32a8ff;
}
footer li:hover {
background-color: white;
transform: translateX(2px);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
footer button {
background: linear-gradient(90deg, #ff7675, #d63031);
margin-top: 1.5rem;
padding: 0.6rem 1.4rem;
}
footer button:hover {
background: linear-gradient(90deg, #ff6b6b, #c0392b);
}
/* 答题反馈动画 */
.feedback-container {
height: 30px;
margin: 10px 0;
position: relative;
}
.feedback {
padding: 6px 12px;
border-radius: 20px;
display: inline-block;
font-weight: bold;
position: absolute;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.feedback.correct {
background-color: rgba(66, 184, 131, 0.2);
color: #2a8c5f;
border: 1px solid #42b883;
}
.feedback.wrong {
background-color: rgba(231, 76, 60, 0.2);
color: #c0392b;
border: 1px solid #e74c3c;
}
/* 输入框的反馈效果 */
.correct-answer {
animation: pulse-green 0.5s;
border-color: #42b883 !important;
}
.wrong-answer {
animation: pulse-red 0.5s;
border-color: #e74c3c !important;
}
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s, transform 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-10px) translateX(-50%);
}
/* 脉动动画效果 */
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(66, 184, 131, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(66, 184, 131, 0); }
100% { box-shadow: 0 0 0 0 rgba(66, 184, 131, 0); }
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(231, 76, 60, 0); }
100% { box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); }
}

View file

@ -17,6 +17,16 @@ const router = createRouter({
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
{
path: '/practice',
name: 'Practice',
component: () => import('../views/Practice.vue'),
},
{
path: '/network-battle',
name: 'NetworkBattle',
component: () => import('../views/NetworkBattle.vue'),
},
],
})

View file

@ -0,0 +1,81 @@
export function generateQuestion(difficulty: number): { question: string, answer: number } {
const ops = ['+', '-', '×', '÷'];
const generateEasyAddition = (): { question: string, answer: number } => {
const a = Math.floor(Math.random() * 10);
const b = Math.floor(Math.random() * 10);
return {
question: `${a} + ${b}`,
answer: a + b,
};
};
const generateEasySubtraction = (): { question: string, answer: number } => {
const a = Math.floor(Math.random() * 10);
const b = Math.floor(Math.random() * a); // 保证 a > b
return {
question: `${a} - ${b}`,
answer: a - b,
};
};
const generateMultiplication = (): { question: string, answer: number } => {
const a = Math.floor(Math.random() * 10);
const b = Math.floor(Math.random() * 10);
return {
question: `${a} × ${b}`,
answer: a * b,
};
};
const generateMediumAddition = (): { question: string, answer: number } => {
const a = Math.floor(Math.random() * 100);
const b = Math.floor(Math.random() * 100);
return {
question: `${a} + ${b}`,
answer: a + b,
};
};
const generateMediumSubtraction = (): { question: string, answer: number } => {
const a = Math.floor(Math.random() * 100);
const b = Math.floor(Math.random() * a); // 保证 a > b
return {
question: `${a} - ${b}`,
answer: a - b,
};
};
const generateMultiplicationDivision = (): { question: string, answer: number } => {
const table = [1, 2, 3, 4, 5, 6, 7, 8, 9]; // 乘法表
const a = table[Math.floor(Math.random() * table.length)];
const b = table[Math.floor(Math.random() * table.length)];
const result = a * b;
return {
question: `${result} ÷ ${a}`,
answer: b,
};
};
switch (difficulty) {
case 1: // 10以内加减法
return Math.random() < 0.5
? generateEasyAddition()
: generateEasySubtraction();
case 2: // 10以内乘法
return generateMultiplication();
case 3: // 100以内加减法
return Math.random() < 0.5
? generateMediumAddition()
: generateMediumSubtraction();
case 4: // 乘法表除法
return generateMultiplicationDivision();
default:
throw new Error('Invalid difficulty level');
}
}

View file

@ -1,9 +1,73 @@
<template>
<transition name="fade">
<div class="home">
<h1>欢迎来到 Arithma Battle</h1>
<p>这里是心算竞技平台快来挑战你的数学能力吧</p>
<p>准备好练习你的数学计算能力或者与他人一决高下了吗</p>
<div class="actions">
<router-link to="/practice" class="btn btn-dopamine">单人练习</router-link>
<router-link to="/network-battle" class="btn btn-dopamine">在线比赛</router-link>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
//
</script>
<template>
<main>
<TheWelcome />
</main>
</template>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.home {
text-align: center;
padding: 2rem;
position: relative;
}
h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
color: #666;
margin-bottom: 0.6rem;
}
.actions {
margin-top: 2rem;
display: flex;
justify-content: center;
gap: 1rem; /* 控制按钮之间的间距 */
}
.btn {
display: inline-block;
padding: 0.8rem 1.5rem; /* 统一按钮大小 */
font-size: 1rem;
color: #fff;
border-radius: 30px; /* 圆弧形 */
text-decoration: none;
transition: background-color 0.3s, transform 0.3s;
}
/* 多巴胺蓝色系按钮 */
.btn-dopamine {
background: linear-gradient(90deg, #84b9ff, #32a8ff, #0faeff); /* 霓虹蓝色渐变 */
}
.btn-dopamine:hover {
transform: scale(1.05); /* 按钮放大效果 */
box-shadow: 0 10px 20px rgba(0, 123, 255, 0.4); /* 增加阴影效果 */
}
</style>

503
src/views/NetworkBattle.vue Normal file
View file

@ -0,0 +1,503 @@
<template>
<div class="network-battle-page">
<header>
<h1>局域网对战</h1>
<div v-if="!isConnected && !isGameActive" class="connection-section">
<div class="connection-type">
<button @click="mode = 'host'" :class="{ active: mode === 'host' }">创建房间</button>
<button @click="mode = 'join'" :class="{ active: mode === 'join' }">加入房间</button>
</div>
<div v-if="mode === 'host'" class="host-section">
<div v-if="localPeerId" class="local-info">
<p>你的房间ID: <span class="peer-id">{{ localPeerId }}</span></p>
<button @click="copyToClipboard(localPeerId)" class="copy-button">复制房间ID</button>
</div>
<p v-if="connectionStatus">{{ connectionStatus }}</p>
</div>
<div v-if="mode === 'join'" class="join-section">
<div class="input-group">
<label for="remotePeerId">输入房间ID:</label>
<input id="remotePeerId" v-model="remotePeerId" placeholder="输入对方的房间ID" />
</div>
<button @click="connectToPeer" :disabled="!remotePeerId">连接</button>
<p v-if="connectionStatus">{{ connectionStatus }}</p>
</div>
</div>
<div v-if="isConnected && !isGameActive" class="game-settings">
<label for="difficulty">选择难度:</label>
<select id="difficulty" v-model="selectedDifficulty">
<option value="1">10以内加减法</option>
<option value="2">10以内乘法</option>
<option value="3">100以内加减法</option>
<option value="4">乘法表除法</option>
</select>
<button @click="startGame">开始游戏</button>
</div>
</header>
<main v-if="isGameActive">
<div class="players-info">
<div class="player local">
<h3>我方</h3>
<p class="score">得分: {{ localScore }}</p>
</div>
<div class="player remote">
<h3>对方</h3>
<p class="score">得分: {{ remoteScore }}</p>
<p v-if="remoteIsAnswering" class="status">对方正在答题...</p>
</div>
</div>
<section class="question-section">
<p>{{ currentQuestion.question }}</p>
<input type="text" v-model="userAnswer" @keyup.enter="submitAnswer"
:class="{'correct-answer': answerFeedback === 'correct',
'wrong-answer': answerFeedback === 'wrong'}"
:disabled="!isGameActive || waitingForNextQuestion" />
<button @click="submitAnswer" :disabled="!isGameActive || waitingForNextQuestion">提交答案</button>
</section>
<div class="feedback-container">
<transition name="fade">
<div v-if="answerFeedback === 'correct'" class="feedback correct">
<span> 回答正确!</span>
</div>
</transition>
<transition name="fade">
<div v-if="answerFeedback === 'wrong'" class="feedback wrong">
<span> 正确答案: {{ lastCorrectAnswer }}</span>
</div>
</transition>
</div>
<section class="timer-section">
<p>剩余时间: {{ timeLeft }} </p>
</section>
<div class="game-messages">
<div v-if="gameMessage" class="game-message" :class="gameMessageType">
{{ gameMessage }}
</div>
</div>
</main>
<div v-if="gameEnded" class="game-result-overlay">
<div class="game-result">
<h2>游戏结束</h2>
<div class="final-scores">
<p>我方得分: {{ localScore }}</p>
<p>对方得分: {{ remoteScore }}</p>
</div>
<h3 class="result-message" :class="{ 'win': localScore > remoteScore, 'lose': localScore < remoteScore, 'draw': localScore === remoteScore }">
{{ resultMessage }}
</h3>
<button @click="resetGame">返回</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import { generateQuestion } from '@/utils/questionGenerator';
import Peer, { DataConnection } from 'peerjs';
// WebRTC
const mode = ref<'host' | 'join'>('host');
const peer = ref<Peer | null>(null);
const connection = ref<DataConnection | null>(null);
const localPeerId = ref<string>('');
const remotePeerId = ref<string>('');
const isConnected = ref<boolean>(false);
const connectionStatus = ref<string>('');
//
const isGameActive = ref<boolean>(false);
const gameEnded = ref<boolean>(false);
const selectedDifficulty = ref<string>('1');
const currentQuestion = ref<{ question: string, answer: number }>({ question: '', answer: 0 });
const userAnswer = ref<string>('');
const timeLeft = ref<number>(30);
const localScore = ref<number>(0);
const remoteScore = ref<number>(0);
const answerFeedback = ref<'none' | 'correct' | 'wrong'>('none');
const lastCorrectAnswer = ref<string>('');
const timerInterval = ref<number | null>(null);
const gameMessage = ref<string>('');
const gameMessageType = ref<string>('');
const waitingForNextQuestion = ref<boolean>(false);
const questionNumber = ref<number>(0);
const remoteIsAnswering = ref<boolean>(false);
const waitingForSettingsAck = ref<boolean>(false);
//
const resultMessage = computed(() => {
if (localScore.value > remoteScore.value) {
return '你赢了!';
} else if (localScore.value < remoteScore.value) {
return '你输了!';
} else {
return '平局!';
}
});
// PeerJS
onMounted(() => {
initPeer();
});
//
onUnmounted(() => {
if (connection.value) {
connection.value.close();
}
if (peer.value) {
peer.value.destroy();
}
clearTimerInterval();
});
//
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text)
.then(() => {
showGameMessage('已复制到剪贴板', 'success');
})
.catch(err => {
console.error('复制失败:', err);
showGameMessage('复制失败,请手动复制', 'error');
});
}
// PeerJS
function initPeer() {
peer.value = new Peer({
debug: 2,
config: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:global.stun.twilio.com:3478' }
]
}
});
peer.value.on('open', (id) => {
localPeerId.value = id;
connectionStatus.value = '准备就绪,等待连接...';
});
peer.value.on('connection', (conn) => {
handleConnection(conn);
});
peer.value.on('error', (err) => {
console.error('PeerJS错误:', err);
connectionStatus.value = `连接错误: ${err}`;
showGameMessage('连接错误,请重试', 'error');
});
}
//
function connectToPeer() {
if (!peer.value || !remotePeerId.value) return;
connectionStatus.value = '正在连接...';
try {
const conn = peer.value.connect(remotePeerId.value, {
reliable: true
});
handleConnection(conn);
} catch (error) {
console.error('连接失败:', error);
connectionStatus.value = '连接失败,请重试';
}
}
//
function handleConnection(conn: DataConnection) {
connection.value = conn;
conn.on('open', () => {
isConnected.value = true;
connectionStatus.value = '连接成功!';
showGameMessage('连接成功!', 'success');
});
conn.on('data', (data: any) => {
handleReceivedData(data);
});
conn.on('close', () => {
isConnected.value = false;
connectionStatus.value = '连接已关闭';
if (isGameActive.value) {
showGameMessage('对方已断开连接', 'error');
endGame();
}
});
conn.on('error', (err) => {
console.error('连接错误:', err);
connectionStatus.value = `连接错误: ${err}`;
showGameMessage('连接错误', 'error');
});
}
//
function startGame() {
if (!isConnected.value || !connection.value) return;
//
timeLeft.value = 30;
localScore.value = 0;
remoteScore.value = 0;
gameEnded.value = false;
waitingForNextQuestion.value = false;
questionNumber.value = 0;
remoteIsAnswering.value = false;
//
waitingForSettingsAck.value = true;
//
showGameMessage('正在等待对方准备...', 'info');
//
connection.value.send({
type: 'game-settings',
difficulty: selectedDifficulty.value
});
}
//
function actuallyStartGame() {
//
isGameActive.value = true;
waitingForSettingsAck.value = false;
//
startTimer();
//
if (connection.value) {
connection.value.send({
type: 'game-started'
});
}
showGameMessage('游戏开始!', 'success');
//
if (mode.value === 'host') {
generateNewQuestion();
}
}
//
function generateNewQuestion() {
try {
currentQuestion.value = generateQuestion(parseInt(selectedDifficulty.value));
questionNumber.value++;
waitingForNextQuestion.value = false;
userAnswer.value = '';
answerFeedback.value = 'none';
remoteIsAnswering.value = false;
//
if (mode.value === 'host' && connection.value) {
connection.value.send({
type: 'new-question',
question: currentQuestion.value.question,
answer: currentQuestion.value.answer,
questionNumber: questionNumber.value
});
}
} catch (error) {
console.error(error);
showGameMessage('生成题目出错', 'error');
}
}
//
function submitAnswer() {
if (!isGameActive.value || waitingForNextQuestion.value) return;
const isCorrect = currentQuestion.value.answer.toString() === userAnswer.value;
lastCorrectAnswer.value = currentQuestion.value.answer.toString();
if (isCorrect) {
localScore.value += 1;
answerFeedback.value = 'correct';
} else {
answerFeedback.value = 'wrong';
}
//
waitingForNextQuestion.value = true;
//
if (connection.value) {
connection.value.send({
type: 'answer-result',
isCorrect,
score: localScore.value,
questionNumber: questionNumber.value
});
}
//
if (mode.value === 'host') {
setTimeout(() => {
generateNewQuestion();
}, 1500);
}
//
setTimeout(() => {
answerFeedback.value = 'none';
}, 1500);
}
//
function startTimer() {
clearTimerInterval();
//
if (timeLeft.value <= 0) {
timeLeft.value = 30;
}
timerInterval.value = window.setInterval(() => {
if (timeLeft.value > 0) {
timeLeft.value--;
}
if (timeLeft.value <= 0) {
clearTimerInterval();
endGame();
//
if (connection.value) {
connection.value.send({
type: 'game-ended'
});
}
}
}, 1000);
}
//
function clearTimerInterval() {
if (timerInterval.value !== null) {
clearInterval(timerInterval.value);
timerInterval.value = null;
}
}
//
function endGame() {
isGameActive.value = false;
gameEnded.value = true;
clearTimerInterval();
}
//
function resetGame() {
localScore.value = 0;
remoteScore.value = 0;
timeLeft.value = 30;
isGameActive.value = false;
gameEnded.value = false;
userAnswer.value = '';
answerFeedback.value = 'none';
waitingForNextQuestion.value = false;
questionNumber.value = 0;
remoteIsAnswering.value = false;
waitingForSettingsAck.value = false;
if (timerInterval.value !== null) {
clearInterval(timerInterval.value);
timerInterval.value = null;
}
}
//
function handleReceivedData(data: any) {
console.log('收到数据:', data);
switch (data.type) {
case 'game-settings':
//
selectedDifficulty.value = data.difficulty;
//
if (connection.value) {
connection.value.send({
type: 'settings-ack'
});
}
break;
case 'settings-ack':
//
if (waitingForSettingsAck.value) {
actuallyStartGame();
}
break;
case 'game-started':
//
isGameActive.value = true;
gameEnded.value = false;
timeLeft.value = 30;
startTimer();
break;
case 'new-question':
//
currentQuestion.value = {
question: data.question,
answer: data.answer
};
questionNumber.value = data.questionNumber;
waitingForNextQuestion.value = false;
userAnswer.value = '';
answerFeedback.value = 'none';
remoteIsAnswering.value = false;
break;
case 'answer-result':
//
remoteScore.value = data.score;
remoteIsAnswering.value = true;
if (data.isCorrect) {
showGameMessage('对方答对了!', 'info');
} else {
showGameMessage('对方答错了!', 'info');
}
break;
case 'game-ended':
//
endGame();
break;
}
}
//
function showGameMessage(message: string, type: 'success' | 'error' | 'info' = 'info') {
gameMessage.value = message;
gameMessageType.value = type;
setTimeout(() => {
gameMessage.value = '';
}, 3000);
}
</script>
<style scoped src="../assets/network-bettle.css"></style>

150
src/views/Practice.vue Normal file
View file

@ -0,0 +1,150 @@
<template>
<div class="practice-page">
<header>
<h1>单人练习</h1>
<label for="difficulty">选择难度:</label>
<select id="difficulty" v-model="selectedDifficulty">
<option value="1">10以内加减法</option>
<option value="2">10以内乘法</option>
<option value="3">100以内加减法</option>
<option value="4">乘法表除法</option>
</select>
<button @click="startGame">开始游戏</button>
</header>
<main v-if="isGameActive">
<section class="question-section">
<p>{{ currentQuestion.question }}</p>
<input type="text" v-model="userAnswer" @keyup.enter="submitAnswer"
:class="{'correct-answer': answerFeedback === 'correct',
'wrong-answer': answerFeedback === 'wrong'}" />
<button @click="submitAnswer">提交答案</button>
</section>
<div class="feedback-container">
<transition name="fade">
<div v-if="answerFeedback === 'correct'" class="feedback correct">
<span> 回答正确!</span>
</div>
</transition>
<transition name="fade">
<div v-if="answerFeedback === 'wrong'" class="feedback wrong">
<span> 正确答案: {{ lastCorrectAnswer }}</span>
</div>
</transition>
</div>
<section class="timer-section">
<p>剩余时间: {{ timeLeft }} </p>
</section>
<section class="score-section">
<p>得分: {{ score }}</p>
</section>
</main>
<footer v-if="!isGameActive">
<p>历史成绩:</p>
<ul>
<li v-for="(record, index) in historyRecords" :key="index">{{ record }}</li>
</ul>
<button @click="resetGame">重置历史记录</button>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { generateQuestion } from '@/utils/questionGenerator';
//
const selectedDifficulty = ref('1'); //
const isGameActive = ref(false);
const currentQuestion = ref<{ question: string, answer: number }>({ question: '', answer: 0 });
const userAnswer = ref('');
const timeLeft = ref(30); // 30
const score = ref(0);
const historyRecords = ref<string[]>([]);
const questionsGenerated = ref<number>(0); //
const answerFeedback = ref('none'); // : 'none', 'correct', 'wrong'
const lastCorrectAnswer = ref(''); //
//
function startGame() {
isGameActive.value = true;
timeLeft.value = 30; //
score.value = 0;
generateNewQuestion();
startTimer();
}
//
function generateNewQuestion() {
try {
currentQuestion.value = generateQuestion(parseInt(selectedDifficulty.value));
} catch (error) {
console.error(error);
}
}
//
function submitAnswer() {
const isCorrect = currentQuestion.value.answer.toString() === userAnswer.value;
lastCorrectAnswer.value = currentQuestion.value.answer.toString();
if (isCorrect) {
score.value += 1;
answerFeedback.value = 'correct';
} else {
answerFeedback.value = 'wrong';
}
questionsGenerated.value++;
generateNewQuestion();
userAnswer.value = '';
//
setTimeout(() => {
answerFeedback.value = 'none';
}, 1500);
}
//
function startTimer() {
const intervalId = setInterval(() => {
timeLeft.value--;
if (timeLeft.value <= 0) {
clearInterval(intervalId);
endGame();
}
}, 1000);
}
//
function endGame() {
isGameActive.value = false;
historyRecords.value.push(`得分: ${score.value}, 生成题目数: ${questionsGenerated.value}`);
saveHistoryToLocalStorage();
}
// localStorage
function saveHistoryToLocalStorage() {
const records = JSON.parse(localStorage.getItem('history') || '[]');
records.push(`得分: ${score.value}, 生成题目数: ${questionsGenerated.value}`);
localStorage.setItem('history', JSON.stringify(records));
}
//
function resetGame() {
historyRecords.value = [];
localStorage.removeItem('history');
questionsGenerated.value = 0;
}
//
onMounted(() => {
historyRecords.value = JSON.parse(localStorage.getItem('history') || '[]');
});
//
onUnmounted(() => {
timeLeft.value = 0;
});
</script>
<style scoped src="../assets/practice.css"></style>

View file

@ -712,6 +712,13 @@ __metadata:
languageName: node
linkType: hard
"@msgpack/msgpack@npm:^2.8.0":
version: 2.8.0
resolution: "@msgpack/msgpack@npm:2.8.0"
checksum: 10c0/5964ed3daad9ccf314238da81c91152dc693bca167b2469445c1d3ce0495443612e543d052281061a91ec48ed44a6a49dd3a334b5d0dbe2dc2db6ea6143e5787
languageName: node
linkType: hard
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@ -1511,8 +1518,9 @@ __metadata:
eslint-plugin-vue: "npm:~10.0.0"
jiti: "npm:^2.4.2"
npm-run-all2: "npm:^7.0.2"
peerjs: "npm:^1.5.4"
pinia: "npm:^3.0.1"
prettier: "npm:3.5.3"
prettier: "npm:^3.5.3"
typescript: "npm:~5.8.0"
vite: "npm:^6.2.4"
vite-plugin-vue-devtools: "npm:^7.7.2"
@ -2098,6 +2106,13 @@ __metadata:
languageName: node
linkType: hard
"eventemitter3@npm:^4.0.7":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"
checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b
languageName: node
linkType: hard
"execa@npm:^9.5.2":
version: 9.5.2
resolution: "execa@npm:9.5.2"
@ -3160,6 +3175,25 @@ __metadata:
languageName: node
linkType: hard
"peerjs-js-binarypack@npm:^2.1.0":
version: 2.1.0
resolution: "peerjs-js-binarypack@npm:2.1.0"
checksum: 10c0/562b6ef27b0de6b152dcab3e13684d38decfff8b51967e12ae14707b1292a78c162ce6befc9ae63ffefec81555eb37372a0e8a530aeabdc69bea3348e4a43790
languageName: node
linkType: hard
"peerjs@npm:^1.5.4":
version: 1.5.4
resolution: "peerjs@npm:1.5.4"
dependencies:
"@msgpack/msgpack": "npm:^2.8.0"
eventemitter3: "npm:^4.0.7"
peerjs-js-binarypack: "npm:^2.1.0"
webrtc-adapter: "npm:^9.0.0"
checksum: 10c0/f52d95693059917ae701c829f766b8c8c6ad13041f16e294d3f82a9f49357e769800895cba445139c3586c89a59f6d818b58c4f1251612139fa6a891d951ace7
languageName: node
linkType: hard
"perfect-debounce@npm:^1.0.0":
version: 1.0.0
resolution: "perfect-debounce@npm:1.0.0"
@ -3249,7 +3283,7 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:3.5.3":
"prettier@npm:^3.5.3":
version: 3.5.3
resolution: "prettier@npm:3.5.3"
bin:
@ -3434,6 +3468,13 @@ __metadata:
languageName: node
linkType: hard
"sdp@npm:^3.2.0":
version: 3.2.0
resolution: "sdp@npm:3.2.0"
checksum: 10c0/fa0146132b4c9185f276b80e09f52259b103e609565ac40c560250dbe7fc47723d30530c0db9cac6217c83153944a71af81fa70dc0367f195aabcf110f8185fd
languageName: node
linkType: hard
"semver@npm:^6.3.1":
version: 6.3.1
resolution: "semver@npm:6.3.1"
@ -3990,6 +4031,15 @@ __metadata:
languageName: node
linkType: hard
"webrtc-adapter@npm:^9.0.0":
version: 9.0.3
resolution: "webrtc-adapter@npm:9.0.3"
dependencies:
sdp: "npm:^3.2.0"
checksum: 10c0/bb8c1626cc0e18bd34c03074fc6a541666dbe473c8f063c5d0ea2a3a3a4887f498212a131c936777f660382185f44941476609a50df5f1543015240623375725
languageName: node
linkType: hard
"which@npm:^2.0.1":
version: 2.0.2
resolution: "which@npm:2.0.2"