new version
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
237
DEPLOY.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# 成长星球项目部署步骤
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
成长星球是一个儿童成长规划Web应用,使用Vue 3 + TypeScript + Vite + Pinia + Three.js + Element Plus技术栈开发。
|
||||||
|
|
||||||
|
## 部署前准备
|
||||||
|
|
||||||
|
### 1. 环境要求
|
||||||
|
|
||||||
|
- Node.js 16.x 或更高版本
|
||||||
|
- npm 7.x 或更高版本
|
||||||
|
- 服务器环境(推荐使用Nginx + PM2)
|
||||||
|
- 域名(可选)
|
||||||
|
|
||||||
|
### 2. 项目构建
|
||||||
|
|
||||||
|
1. **克隆项目**
|
||||||
|
```bash
|
||||||
|
git clone <项目仓库地址>
|
||||||
|
cd 儿童成长规划项目
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **安装依赖**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **构建生产版本**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建完成后,生成的静态文件将位于 `dist` 目录中。
|
||||||
|
|
||||||
|
## 服务器部署
|
||||||
|
|
||||||
|
### 1. 服务器准备
|
||||||
|
|
||||||
|
- **安装Node.js**
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
|
||||||
|
# CentOS/RHEL
|
||||||
|
curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
|
||||||
|
sudo yum install -y nodejs
|
||||||
|
```
|
||||||
|
|
||||||
|
- **安装Nginx**
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y nginx
|
||||||
|
|
||||||
|
# CentOS/RHEL
|
||||||
|
sudo yum install -y nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
- **安装PM2(可选,用于管理Node.js应用)**
|
||||||
|
```bash
|
||||||
|
npm install -g pm2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 部署静态文件
|
||||||
|
|
||||||
|
1. **复制构建文件到服务器**
|
||||||
|
```bash
|
||||||
|
# 使用scp命令
|
||||||
|
scp -r dist/* user@server_ip:/var/www/chengzhangxingqiu
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **配置Nginx**
|
||||||
|
|
||||||
|
创建Nginx配置文件:
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/nginx/sites-available/chengzhangxingqiu
|
||||||
|
```
|
||||||
|
|
||||||
|
配置内容:
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
root /var/www/chengzhangxingqiu;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 静态文件缓存
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **启用站点**
|
||||||
|
```bash
|
||||||
|
sudo ln -s /etc/nginx/sites-available/chengzhangxingqiu /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. HTTPS配置(可选)
|
||||||
|
|
||||||
|
使用Let's Encrypt获取免费SSL证书:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装certbot
|
||||||
|
sudo apt-get install certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# 获取证书
|
||||||
|
sudo certbot --nginx -d your-domain.com
|
||||||
|
|
||||||
|
# 自动更新证书
|
||||||
|
sudo systemctl enable certbot.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署后检查
|
||||||
|
|
||||||
|
1. **访问应用**
|
||||||
|
打开浏览器访问 `http://your-domain.com` 或 `https://your-domain.com`
|
||||||
|
|
||||||
|
2. **功能测试**
|
||||||
|
- 测试用户登录功能
|
||||||
|
- 测试各个模块的功能
|
||||||
|
- 测试图片加载是否正常
|
||||||
|
- 测试英语发音功能
|
||||||
|
|
||||||
|
3. **性能监控**
|
||||||
|
- 监控服务器资源使用情况
|
||||||
|
- 检查页面加载速度
|
||||||
|
- 测试响应时间
|
||||||
|
|
||||||
|
## 维护与更新
|
||||||
|
|
||||||
|
### 1. 代码更新
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 拉取最新代码
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# 安装新依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 重新构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 复制新文件到服务器
|
||||||
|
scp -r dist/* user@server_ip:/var/www/chengzhangxingqiu
|
||||||
|
|
||||||
|
# 重启Nginx(如果需要)
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 日志查看
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nginx访问日志
|
||||||
|
sudo tail -f /var/log/nginx/access.log
|
||||||
|
|
||||||
|
# Nginx错误日志
|
||||||
|
sudo tail -f /var/log/nginx/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 常见问题排查
|
||||||
|
|
||||||
|
- **页面空白**:检查Nginx配置和文件路径
|
||||||
|
- **图片不显示**:检查图片路径和文件权限
|
||||||
|
- **功能异常**:检查浏览器控制台错误信息
|
||||||
|
- **服务器错误**:检查Nginx错误日志
|
||||||
|
|
||||||
|
## 安全建议
|
||||||
|
|
||||||
|
1. **定期更新依赖**
|
||||||
|
```bash
|
||||||
|
npm update
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **设置文件权限**
|
||||||
|
```bash
|
||||||
|
sudo chown -R www-data:www-data /var/www/chengzhangxingqiu
|
||||||
|
sudo chmod -R 755 /var/www/chengzhangxingqiu
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **启用防火墙**
|
||||||
|
```bash
|
||||||
|
sudo ufw enable
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **定期备份**
|
||||||
|
```bash
|
||||||
|
# 备份静态文件
|
||||||
|
tar -czf chengzhangxingqiu-backup-$(date +%Y%m%d).tar.gz /var/www/chengzhangxingqiu
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署流程图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ 本地开发环境 │────>│ 构建生产版本 │────>│ 部署到服务器 │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ 静态文件生成 │────>│ Nginx配置 │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ HTTPS配置 │
|
||||||
|
└─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 功能测试 │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保服务器有足够的磁盘空间和内存
|
||||||
|
2. 定期更新服务器系统和软件包
|
||||||
|
3. 监控服务器状态,及时处理异常
|
||||||
|
4. 备份重要数据,防止数据丢失
|
||||||
|
5. 遵循安全最佳实践,保护用户数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**部署完成后,您的成长星球应用将可以在互联网上访问,为孩子们提供一个有趣、安全的学习环境。**
|
||||||
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>成长星球</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2178
package-lock.json
generated
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "growth-planet",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"element-plus": "^2.13.3",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"three": "^0.183.2",
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.5",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
public/badge.png
Normal file
0
public/craft.png
Normal file
0
public/find_diff_A.png
Normal file
0
public/find_diff_B.png
Normal file
0
public/habit.png
Normal file
0
public/home.png
Normal file
0
public/knowledge.png
Normal file
0
public/logic.png
Normal file
0
public/parent.png
Normal file
1
public/planet.png
Normal file
1
public/star.png
Normal file
BIN
public/五人一起.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
public/儿童成长星球.png
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
public/哪吒.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
public/唐僧.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
public/孙悟空.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
public/成长星球1.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
public/沙僧.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
public/猪八戒.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
public/生成卡通图片 (10).png
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
BIN
public/生成卡通图片 (11).png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
public/生成卡通图片 (12).png
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
public/生成卡通图片 (13).png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
public/生成卡通图片 (4).png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
public/生成卡通图片 (5).png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
public/生成卡通图片 (6).png
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
public/生成卡通图片 (7).png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
public/生成卡通图片 (8).png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
public/生成卡通图片 (9).png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
1922
src/App.vue
Normal file
1176
src/components/AdventureArea.vue
Normal file
4662
src/components/AdventureMap.vue
Normal file
692
src/components/CalendarArea.vue
Normal file
@@ -0,0 +1,692 @@
|
|||||||
|
<template>
|
||||||
|
<div class="calendar-area">
|
||||||
|
<div class="calendar-header">
|
||||||
|
<h2 class="calendar-title">📅 我的成长日历</h2>
|
||||||
|
<p class="calendar-subtitle">每一天都是成长的好日子!✨</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日历控制 -->
|
||||||
|
<div class="calendar-controls">
|
||||||
|
<button class="control-btn" @click="previousMonth">
|
||||||
|
<span class="btn-arrow">◀</span>
|
||||||
|
</button>
|
||||||
|
<div class="current-month">
|
||||||
|
<span class="month-emoji">🌸</span>
|
||||||
|
<span class="month-text">{{ currentYear }}年 {{ currentMonth + 1 }}月</span>
|
||||||
|
<span class="month-emoji">🌸</span>
|
||||||
|
</div>
|
||||||
|
<button class="control-btn" @click="nextMonth">
|
||||||
|
<span class="btn-arrow">▶</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日历表格 -->
|
||||||
|
<div class="calendar-grid">
|
||||||
|
<div class="calendar-weekdays">
|
||||||
|
<div class="weekday" v-for="day in weekdays" :key="day">
|
||||||
|
<span class="weekday-emoji">{{ getWeekdayEmoji(day) }}</span>
|
||||||
|
<span class="weekday-text">{{ day }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-days">
|
||||||
|
<div
|
||||||
|
v-for="(day, index) in calendarDays"
|
||||||
|
:key="index"
|
||||||
|
class="calendar-day"
|
||||||
|
:class="{
|
||||||
|
'today': isToday(day),
|
||||||
|
'checked': isChecked(day),
|
||||||
|
'future': isFuture(day)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="day-content">
|
||||||
|
<div class="day-number">{{ day || '' }}</div>
|
||||||
|
<div class="day-emoji" v-if="day && isChecked(day)">
|
||||||
|
{{ getCheckEmoji(day) }}
|
||||||
|
</div>
|
||||||
|
<div class="day-stats" v-if="day && getDayStats(day).count > 0">
|
||||||
|
<div class="stats-badge">
|
||||||
|
<span class="stars">⭐{{ getDayStats(day).stars }}</span>
|
||||||
|
<span class="habits">{{ getDayStats(day).count }}个习惯</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 月度统计 -->
|
||||||
|
<div class="month-stats">
|
||||||
|
<h3 class="stats-title">📊 本月统计</h3>
|
||||||
|
<div class="stats-cards">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🎯</div>
|
||||||
|
<div class="stat-value">{{ monthlyStats.totalDays }}</div>
|
||||||
|
<div class="stat-label">打卡天数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">⭐</div>
|
||||||
|
<div class="stat-value">{{ monthlyStats.totalStars }}</div>
|
||||||
|
<div class="stat-label">获得星星</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🔥</div>
|
||||||
|
<div class="stat-value">{{ monthlyStats.longestStreak }}</div>
|
||||||
|
<div class="stat-label">最长连续</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">💪</div>
|
||||||
|
<div class="stat-value">{{ monthlyStats.totalHabits }}</div>
|
||||||
|
<div class="stat-label">习惯次数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 习惯完成度 -->
|
||||||
|
<div class="habit-progress">
|
||||||
|
<h3 class="progress-title">📈 习惯完成度</h3>
|
||||||
|
<div class="progress-items">
|
||||||
|
<div v-for="(habit, key) in habits" :key="key" class="progress-item">
|
||||||
|
<div class="progress-header">
|
||||||
|
<span class="habit-icon">{{ habit.icon }}</span>
|
||||||
|
<span class="habit-name">{{ habit.name }}</span>
|
||||||
|
<span class="habit-count">{{ habitData[key as keyof typeof habitData]?.count || 0 }}次</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: `${getProgress(habitData[key as keyof typeof habitData]?.count || 0)}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||||
|
|
||||||
|
const starEnergyStore = useStarEnergyStore();
|
||||||
|
|
||||||
|
// 当前日期
|
||||||
|
const currentDate = new Date();
|
||||||
|
const currentYear = ref(currentDate.getFullYear());
|
||||||
|
const currentMonth = ref(currentDate.getMonth());
|
||||||
|
|
||||||
|
// 星期标题
|
||||||
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
|
|
||||||
|
// 习惯数据
|
||||||
|
const habits = {
|
||||||
|
brushTeeth: { name: '刷牙', icon: '🦷' },
|
||||||
|
noBedLate: { name: '不赖床', icon: '⏰' },
|
||||||
|
noPickyEating: { name: '不挑食', icon: '🥦' },
|
||||||
|
washHands: { name: '勤洗手', icon: '🧼' },
|
||||||
|
homeworkFast: { name: '写作业快', icon: '📝' },
|
||||||
|
earlySleep: { name: '早睡', icon: '🌙' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从store获取习惯数据
|
||||||
|
const habitData = computed(() => starEnergyStore.habits);
|
||||||
|
|
||||||
|
// 获取日历天数
|
||||||
|
const calendarDays = computed(() => {
|
||||||
|
const days = [];
|
||||||
|
const firstDay = new Date(currentYear.value, currentMonth.value, 1);
|
||||||
|
const lastDay = new Date(currentYear.value, currentMonth.value + 1, 0);
|
||||||
|
const startDay = firstDay.getDay();
|
||||||
|
const totalDays = lastDay.getDate();
|
||||||
|
|
||||||
|
// 填充空白
|
||||||
|
for (let i = 0; i < startDay; i++) {
|
||||||
|
days.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充日期
|
||||||
|
for (let i = 1; i <= totalDays; i++) {
|
||||||
|
days.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取月份统计
|
||||||
|
const monthlyStats = computed(() => {
|
||||||
|
const stats = {
|
||||||
|
totalDays: 0,
|
||||||
|
totalStars: 0,
|
||||||
|
longestStreak: 0,
|
||||||
|
totalHabits: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const habits = starEnergyStore.habits;
|
||||||
|
Object.values(habits).forEach(habit => {
|
||||||
|
stats.totalHabits += habit.count;
|
||||||
|
if (habit.streak > stats.longestStreak) {
|
||||||
|
stats.longestStreak = habit.streak;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算打卡天数和星星数
|
||||||
|
const year = currentYear.value;
|
||||||
|
const month = currentMonth.value + 1;
|
||||||
|
const daysInMonth = new Date(year, month, 0).getDate();
|
||||||
|
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
let hasCheckIn = false;
|
||||||
|
|
||||||
|
Object.values(habits).forEach(habit => {
|
||||||
|
if (habit.lastChecked === dateStr) {
|
||||||
|
hasCheckIn = true;
|
||||||
|
stats.totalStars += 10; // 每次打卡10星
|
||||||
|
if (habit.streak % 3 === 0) {
|
||||||
|
stats.totalStars += 5; // 连续3天额外5星
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasCheckIn) {
|
||||||
|
stats.totalDays++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否是今天
|
||||||
|
const isToday = (day: number | null) => {
|
||||||
|
if (!day) return false;
|
||||||
|
const today = new Date();
|
||||||
|
return (
|
||||||
|
day === today.getDate() &&
|
||||||
|
currentMonth.value === today.getMonth() &&
|
||||||
|
currentYear.value === today.getFullYear()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断是否是未来日期
|
||||||
|
const isFuture = (day: number | null) => {
|
||||||
|
if (!day) return false;
|
||||||
|
const today = new Date();
|
||||||
|
const date = new Date(currentYear.value, currentMonth.value, day);
|
||||||
|
return date > today;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断是否已打卡
|
||||||
|
const isChecked = (day: number | null) => {
|
||||||
|
if (!day) return false;
|
||||||
|
const year = currentYear.value;
|
||||||
|
const month = currentMonth.value + 1;
|
||||||
|
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
return Object.values(starEnergyStore.habits).some(
|
||||||
|
habit => habit.lastChecked === dateStr
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取打卡表情
|
||||||
|
const getCheckEmoji = (day: number | null) => {
|
||||||
|
if (!day) return '';
|
||||||
|
const stats = getDayStats(day);
|
||||||
|
if (stats.count >= 6) return '🎉';
|
||||||
|
if (stats.count >= 4) return '🌟';
|
||||||
|
if (stats.count >= 2) return '😊';
|
||||||
|
return '✅';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取每日统计
|
||||||
|
const getDayStats = (day: number | null) => {
|
||||||
|
if (!day) return { count: 0, stars: 0 };
|
||||||
|
|
||||||
|
const year = currentYear.value;
|
||||||
|
const month = currentMonth.value + 1;
|
||||||
|
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
Object.values(starEnergyStore.habits).forEach(habit => {
|
||||||
|
if (habit.lastChecked === dateStr) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
stars: count * 10
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取星期表情
|
||||||
|
const getWeekdayEmoji = (day: string) => {
|
||||||
|
const emojis = ['😴', '😊', '😄', '😃', '😁', '🎉', '🥳'];
|
||||||
|
return emojis[weekdays.indexOf(day)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取进度百分比
|
||||||
|
const getProgress = (count: number) => {
|
||||||
|
const max = 100;
|
||||||
|
return Math.min((count / max) * 100, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上个月
|
||||||
|
const previousMonth = () => {
|
||||||
|
if (currentMonth.value === 0) {
|
||||||
|
currentMonth.value = 11;
|
||||||
|
currentYear.value--;
|
||||||
|
} else {
|
||||||
|
currentMonth.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下个月
|
||||||
|
const nextMonth = () => {
|
||||||
|
if (currentMonth.value === 11) {
|
||||||
|
currentMonth.value = 0;
|
||||||
|
currentYear.value++;
|
||||||
|
} else {
|
||||||
|
currentMonth.value++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-area {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FF69B4;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日历控制 */
|
||||||
|
.calendar-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 10px rgba(255, 182, 193, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 15px rgba(255, 182, 193, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-arrow {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-month {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 3px solid #FFB6C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-emoji {
|
||||||
|
font-size: 24px;
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日历表格 */
|
||||||
|
.calendar-grid {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 4px solid #FFB6C1;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-weekdays {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
background: linear-gradient(135deg, #E6E6FA, #D8BFD8);
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-emoji {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-days {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day:hover:not(.future) {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today {
|
||||||
|
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||||
|
border-color: #FF69B4;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 105, 180, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.checked {
|
||||||
|
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||||
|
border-color: #32CD32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.future {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-emoji {
|
||||||
|
font-size: 20px;
|
||||||
|
animation: bounce 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-badge {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
color: #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.habits {
|
||||||
|
color: #32CD32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 月度统计 */
|
||||||
|
.month-stats {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 4px solid #87CEEB;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4169E1;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #E6E6FA, #D8BFD8);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 3px solid #DDA0DD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 习惯完成度 */
|
||||||
|
.habit-progress {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 4px solid #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FF8C00;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #FFE4B5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.habit-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.habit-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.habit-count {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FF8C00;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #98FB98, #90EE90, #32CD32);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(5deg); }
|
||||||
|
75% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.calendar-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-emoji {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday-text {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-emoji {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
579
src/components/CraftArea.vue
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
<template>
|
||||||
|
<div class="craft-area">
|
||||||
|
<div class="area-header">
|
||||||
|
<div class="header-with-audio">
|
||||||
|
<h2 class="area-title">🎨 手工工坊 🎨</h2>
|
||||||
|
<button class="audio-btn" @click="playAudio('欢迎来到手工工坊!动动手,变厉害!')">🔊</button>
|
||||||
|
</div>
|
||||||
|
<p class="area-desc">动动手,变厉害!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="craft-types">
|
||||||
|
<!-- DIY小达人 -->
|
||||||
|
<div class="craft-card" @click="selectCraft('diy')">
|
||||||
|
<div class="craft-icon diy-icon">🎨</div>
|
||||||
|
<h3 class="craft-title">DIY小达人</h3>
|
||||||
|
<p class="craft-desc">跟着 "手工兔" 学做手工</p>
|
||||||
|
<button class="card-audio-btn" @click.stop="playAudio('跟着手工兔,学做卡纸花朵、瓶盖拼图、纸盘手偶等有趣的手工!')">🔊</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 家务小帮手 -->
|
||||||
|
<div class="craft-card" @click="selectCraft('chore')">
|
||||||
|
<div class="craft-icon chore-icon">🧹</div>
|
||||||
|
<h3 class="craft-title">家务小帮手</h3>
|
||||||
|
<p class="craft-desc">帮星球清理 "杂物角"</p>
|
||||||
|
<button class="card-audio-btn" @click.stop="playAudio('帮家人做家务,比如叠袜子、摆碗筷、整理书包,成为家务小能手!')">🔊</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自然探索家 -->
|
||||||
|
<div class="craft-card" @click="selectCraft('nature')">
|
||||||
|
<div class="craft-icon nature-icon">🔍</div>
|
||||||
|
<h3 class="craft-title">自然探索家</h3>
|
||||||
|
<p class="craft-desc">带着 APP 去户外探索</p>
|
||||||
|
<button class="card-audio-btn" @click.stop="playAudio('去户外探索大自然吧!观察蚂蚁搬家、树叶脉络,用画笔记录花朵的样子!')">🔊</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手工内容 -->
|
||||||
|
<div class="craft-content" v-if="currentCraft">
|
||||||
|
<div class="content-header">
|
||||||
|
<h3 class="content-title">{{ craftTitles[currentCraft] }}</h3>
|
||||||
|
<button class="back-btn" @click="currentCraft = null">返回</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tasks">
|
||||||
|
<div v-for="(task, index) in tasks[currentCraft]" :key="index" class="task-card">
|
||||||
|
<div class="task-icon">{{ task.icon }}</div>
|
||||||
|
<div class="task-info">
|
||||||
|
<h4 class="task-title">{{ task.title }}</h4>
|
||||||
|
<p class="task-desc">{{ task.description }}</p>
|
||||||
|
<div class="task-reward">
|
||||||
|
<span class="reward-icon">⭐</span>
|
||||||
|
<span>{{ task.reward }} 颗星星能量</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-actions">
|
||||||
|
<button class="task-audio-btn" @click="playTaskAudio(task)">🔊</button>
|
||||||
|
<button class="task-btn" @click="completeTask(currentCraft, index)">
|
||||||
|
{{ task.completed ? '已完成' : '开始' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片上传区域 -->
|
||||||
|
<div v-if="currentCraft === 'diy' || currentCraft === 'nature'" class="upload-section">
|
||||||
|
<h4>完成后上传照片</h4>
|
||||||
|
<input type="file" accept="image/*" @change="handleFileUpload" class="file-input">
|
||||||
|
<div class="preview" v-if="imagePreview">
|
||||||
|
<img :src="imagePreview" alt="预览" class="preview-image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||||
|
|
||||||
|
const starEnergyStore = useStarEnergyStore();
|
||||||
|
const currentCraft = ref<string | null>(null);
|
||||||
|
const imagePreview = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 初始化时加载语音合成API的voices
|
||||||
|
onMounted(() => {
|
||||||
|
// 触发语音合成API的加载
|
||||||
|
window.speechSynthesis.getVoices();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放音频 - 库洛米声音风格
|
||||||
|
const playAudio = (text: string) => {
|
||||||
|
try {
|
||||||
|
// 使用Web Speech API进行文本转语音
|
||||||
|
const speech = new SpeechSynthesisUtterance(text);
|
||||||
|
// 设置语言
|
||||||
|
speech.lang = 'zh-CN';
|
||||||
|
// 设置音量、语速和语调 - 库洛米风格(可爱、活泼、略带淘气)
|
||||||
|
speech.volume = 1; // 音量
|
||||||
|
speech.rate = 1.1; // 语速稍快,更有活力
|
||||||
|
speech.pitch = 1.7; // 更高的语调,更可爱
|
||||||
|
|
||||||
|
// 尝试选择更适合儿童的语音
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
if (voices.length > 0) {
|
||||||
|
// 优先选择女性或儿童语音
|
||||||
|
let selectedVoice = voices.find(voice =>
|
||||||
|
voice.lang === 'zh-CN' &&
|
||||||
|
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女'))
|
||||||
|
) || voices.find(voice => voice.lang === 'zh-CN');
|
||||||
|
|
||||||
|
if (selectedVoice) {
|
||||||
|
speech.voice = selectedVoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放
|
||||||
|
window.speechSynthesis.speak(speech);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('音频播放失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 播放任务说明
|
||||||
|
const playTaskAudio = (task: any) => {
|
||||||
|
const audioTexts: Record<string, string> = {
|
||||||
|
'卡纸花朵': '用卡纸制作花朵,先剪出花瓣形状,然后粘贴在花心周围,可以做出五颜六色的花朵哦!',
|
||||||
|
'瓶盖拼图': '收集不同颜色的瓶盖,发挥想象力,拼出各种有趣的图案,比如动物、房子或者汽车!',
|
||||||
|
'纸盘手偶': '用纸盘做底,画上可爱的表情,可以做成小猫、小狗或者小兔子,然后给它们编个小故事吧!',
|
||||||
|
'叠袜子': '学习把袜子配对,然后把它们整齐地叠好,这样可以帮爸爸妈妈节省时间哦!',
|
||||||
|
'摆碗筷': '在吃饭前帮忙摆放碗筷,要确保每个人的碗筷都齐全,摆放整齐,你真是个好帮手!',
|
||||||
|
'整理书包': '每天晚上整理书包,把第二天需要的课本和文具都准备好,这样早上就不会手忙脚乱啦!',
|
||||||
|
'蚂蚁搬家': '仔细观察蚂蚁是怎么搬食物的,它们会排队,还会互相帮助,拍下照片记录下来吧!',
|
||||||
|
'树叶脉络': '收集不同形状的树叶,观察它们的脉络,可以用拓印的方式记录下来,画出美丽的图案!',
|
||||||
|
'花朵写生': '在户外找到漂亮的花朵,用画笔仔细观察,画出它的颜色和形状,成为小画家吧!'
|
||||||
|
};
|
||||||
|
|
||||||
|
playAudio(audioTexts[task.title] || task.description);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手工类型标题
|
||||||
|
const craftTitles = {
|
||||||
|
diy: 'DIY小达人',
|
||||||
|
chore: '家务小帮手',
|
||||||
|
nature: '自然探索家'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 任务数据
|
||||||
|
const tasks = ref({
|
||||||
|
diy: [
|
||||||
|
{
|
||||||
|
title: '卡纸花朵',
|
||||||
|
description: '跟着 "手工兔" 学做卡纸花朵,步骤有图片 + 语音讲解',
|
||||||
|
reward: 8,
|
||||||
|
completed: false,
|
||||||
|
icon: '🌸'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '瓶盖拼图',
|
||||||
|
description: '用瓶盖制作创意拼图,发挥想象力',
|
||||||
|
reward: 8,
|
||||||
|
completed: false,
|
||||||
|
icon: '🧩'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '纸盘手偶',
|
||||||
|
description: '用纸盘制作可爱的手偶,自己设计表情',
|
||||||
|
reward: 8,
|
||||||
|
completed: false,
|
||||||
|
icon: '🤹'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
chore: [
|
||||||
|
{
|
||||||
|
title: '叠袜子',
|
||||||
|
description: '3-4 岁:学习叠袜子,整齐摆放',
|
||||||
|
reward: 10,
|
||||||
|
completed: false,
|
||||||
|
icon: '🧦'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '摆碗筷',
|
||||||
|
description: '5-6 岁:帮助摆放碗筷,准备吃饭',
|
||||||
|
reward: 10,
|
||||||
|
completed: false,
|
||||||
|
icon: '🍽️'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '整理书包',
|
||||||
|
description: '7-8 岁:自己整理书包,准备第二天的学习用品',
|
||||||
|
reward: 10,
|
||||||
|
completed: false,
|
||||||
|
icon: '🎒'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nature: [
|
||||||
|
{
|
||||||
|
title: '蚂蚁搬家',
|
||||||
|
description: '拍蚂蚁搬家的照片,观察它们的行为',
|
||||||
|
reward: 8,
|
||||||
|
completed: false,
|
||||||
|
icon: '🐜'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '树叶脉络',
|
||||||
|
description: '收集不同形状的树叶,观察它们的脉络',
|
||||||
|
reward: 8,
|
||||||
|
completed: false,
|
||||||
|
icon: '🍃'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '花朵写生',
|
||||||
|
description: '在户外观察花朵,用画笔记录它们的样子',
|
||||||
|
reward: 8,
|
||||||
|
completed: false,
|
||||||
|
icon: '🖌️'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选择手工类型
|
||||||
|
const selectCraft = (craft: string) => {
|
||||||
|
currentCraft.value = craft;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 完成任务
|
||||||
|
const completeTask = (craft: string, index: number) => {
|
||||||
|
const task = tasks.value[craft as keyof typeof tasks.value][index];
|
||||||
|
if (!task.completed) {
|
||||||
|
task.completed = true;
|
||||||
|
// 奖励星星能量
|
||||||
|
starEnergyStore.addEnergy(task.reward, 'craft');
|
||||||
|
// 播放完成语音
|
||||||
|
playAudio(`太棒了!完成了${task.title}!获得了${task.reward}颗星星能量!`);
|
||||||
|
// 显示完成动画或提示
|
||||||
|
alert(`任务完成!获得 ${task.reward} 颗星星能量!`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理文件上传
|
||||||
|
const handleFileUpload = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const file = input.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
imagePreview.value = e.target?.result as string;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
// 这里可以添加上传到服务器的逻辑
|
||||||
|
playAudio('哇!你的作品颜色搭配超棒,创意满分!你真是个小艺术家!');
|
||||||
|
alert('照片上传成功!星宝说:"哇!颜色搭配超棒,创意满分💯"');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.craft-area {
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-with-audio {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FF69B4;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-desc {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-types {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-icon {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 35px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diy-icon {
|
||||||
|
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chore-icon {
|
||||||
|
background: linear-gradient(135deg, #87CEEB, #B0E0E6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nature-icon {
|
||||||
|
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-audio-btn {
|
||||||
|
background: linear-gradient(135deg, #FFD166, #06D6A0);
|
||||||
|
border: none;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-audio-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-content {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.85));
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 3px solid #FFB6C1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
animation: shine 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% { transform: translateX(-100%) rotate(45deg); }
|
||||||
|
100% { transform: translateX(100%) rotate(45deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-reward {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-audio-btn {
|
||||||
|
background: linear-gradient(135deg, #FFD166, #06D6A0);
|
||||||
|
border: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-audio-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-btn {
|
||||||
|
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||||
|
color: #006400;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
background: linear-gradient(135deg, #90EE90, #32CD32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(5deg); }
|
||||||
|
75% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1310
src/components/HabitArea.vue
Normal file
3604
src/components/KnowledgeArea.vue
Normal file
574
src/components/LogicArea.vue
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
<template>
|
||||||
|
<div class="logic-area">
|
||||||
|
<div class="area-header">
|
||||||
|
<h2 class="area-title">逻辑迷宫</h2>
|
||||||
|
<p class="area-desc">练思维,变聪明!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logic-games">
|
||||||
|
<!-- 迷宫大冒险 -->
|
||||||
|
<div class="game-card" @click="selectGame('maze')">
|
||||||
|
<div class="game-icon maze-icon"></div>
|
||||||
|
<h3 class="game-title">迷宫大冒险</h3>
|
||||||
|
<p class="game-desc">按线索走迷宫,挑战不同难度</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 找不同挑战 -->
|
||||||
|
<div class="game-card" @click="selectGame('findDiff')">
|
||||||
|
<div class="game-icon findDiff-icon"></div>
|
||||||
|
<h3 class="game-title">找不同挑战</h3>
|
||||||
|
<p class="game-desc">对比两张图片,找出不一样的地方</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 涂鸦变变变 -->
|
||||||
|
<div class="game-card" @click="selectGame('doodle')">
|
||||||
|
<div class="game-icon doodle-icon"></div>
|
||||||
|
<h3 class="game-title">涂鸦变变变</h3>
|
||||||
|
<p class="game-desc">画出创意,星宝帮你变成动画</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 游戏内容 -->
|
||||||
|
<div class="game-content" v-if="currentGame">
|
||||||
|
<div class="content-header">
|
||||||
|
<h3 class="content-title">{{ gameTitles[currentGame] }}</h3>
|
||||||
|
<button class="back-btn" @click="currentGame = null">返回</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 迷宫大冒险 -->
|
||||||
|
<div v-if="currentGame === 'maze'" class="maze-game">
|
||||||
|
<div class="difficulty-select">
|
||||||
|
<h4>选择难度</h4>
|
||||||
|
<div class="difficulty-buttons">
|
||||||
|
<button
|
||||||
|
v-for="level in difficultyLevels"
|
||||||
|
:key="level.level"
|
||||||
|
class="difficulty-btn"
|
||||||
|
:class="{ active: selectedDifficulty === level.level }"
|
||||||
|
@click="selectedDifficulty = level.level"
|
||||||
|
>
|
||||||
|
{{ level.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="maze-container">
|
||||||
|
<div class="maze-placeholder">
|
||||||
|
<p>迷宫游戏区域</p>
|
||||||
|
<p>难度:{{ difficultyLevels.find(l => l.level === selectedDifficulty)?.name }}</p>
|
||||||
|
<button class="start-maze-btn" @click="completeMaze">开始挑战</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 找不同挑战 -->
|
||||||
|
<div v-if="currentGame === 'findDiff'" class="findDiff-game">
|
||||||
|
<div class="image-pair">
|
||||||
|
<div class="image-container">
|
||||||
|
<h5>图片 A</h5>
|
||||||
|
<div class="game-image-placeholder game-image-a">
|
||||||
|
<span>🦁</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-container">
|
||||||
|
<h5>图片 B</h5>
|
||||||
|
<div class="game-image-placeholder game-image-b">
|
||||||
|
<span>🐯</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="diff-finder">
|
||||||
|
<h4>找出 3 个不同之处</h4>
|
||||||
|
<div class="diff-inputs">
|
||||||
|
<input type="text" v-model="diffAnswers[0]" placeholder="第一个不同..." class="diff-input">
|
||||||
|
<input type="text" v-model="diffAnswers[1]" placeholder="第二个不同..." class="diff-input">
|
||||||
|
<input type="text" v-model="diffAnswers[2]" placeholder="第三个不同..." class="diff-input">
|
||||||
|
</div>
|
||||||
|
<button class="check-btn" @click="checkDiff">检查答案</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 涂鸦变变变 -->
|
||||||
|
<div v-if="currentGame === 'doodle'" class="doodle-game">
|
||||||
|
<div class="doodle-area">
|
||||||
|
<h4>画出你的创意</h4>
|
||||||
|
<div class="canvas-container">
|
||||||
|
<canvas ref="doodleCanvas" width="300" height="300" class="doodle-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="doodle-controls">
|
||||||
|
<button class="control-btn" @click="clearCanvas">清除</button>
|
||||||
|
<button class="control-btn" @click="completeDoodle">完成</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="story-creator">
|
||||||
|
<h4>故事创编机</h4>
|
||||||
|
<p>用 "兔子、雨伞、森林" 编一个故事</p>
|
||||||
|
<textarea v-model="storyText" placeholder="开始编写你的故事..." class="story-textarea"></textarea>
|
||||||
|
<button class="create-story-btn" @click="createStory">生成动画</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||||
|
|
||||||
|
const starEnergyStore = useStarEnergyStore();
|
||||||
|
const currentGame = ref<string | null>(null);
|
||||||
|
const selectedDifficulty = ref(1);
|
||||||
|
const diffAnswers = ref(['', '', '']);
|
||||||
|
const storyText = ref('');
|
||||||
|
const doodleCanvas = ref<HTMLCanvasElement | null>(null);
|
||||||
|
let ctx: CanvasRenderingContext2D | null = null;
|
||||||
|
let isDrawing = ref(false);
|
||||||
|
|
||||||
|
// 游戏标题
|
||||||
|
const gameTitles = {
|
||||||
|
maze: '迷宫大冒险',
|
||||||
|
findDiff: '找不同挑战',
|
||||||
|
doodle: '涂鸦变变变'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 难度级别
|
||||||
|
const difficultyLevels = [
|
||||||
|
{ level: 1, name: '简单(3岁)' },
|
||||||
|
{ level: 2, name: '中等(5岁)' },
|
||||||
|
{ level: 3, name: '复杂(8岁)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 选择游戏
|
||||||
|
const selectGame = (game: string) => {
|
||||||
|
currentGame.value = game;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 完成迷宫
|
||||||
|
const completeMaze = () => {
|
||||||
|
starEnergyStore.addEnergy(5, 'logic');
|
||||||
|
alert('迷宫挑战成功!获得 5 颗星星能量!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查找不同答案
|
||||||
|
const checkDiff = () => {
|
||||||
|
// 这里可以添加答案检查逻辑
|
||||||
|
starEnergyStore.addEnergy(5, 'logic');
|
||||||
|
alert('找不同挑战成功!获得 5 颗星星能量!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除画布
|
||||||
|
const clearCanvas = () => {
|
||||||
|
if (ctx && doodleCanvas.value) {
|
||||||
|
ctx.clearRect(0, 0, doodleCanvas.value.width, doodleCanvas.value.height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 完成涂鸦
|
||||||
|
const completeDoodle = () => {
|
||||||
|
starEnergyStore.addEnergy(8, 'logic');
|
||||||
|
alert('涂鸦完成!星宝把你的画变成了动画!获得 8 颗星星能量!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建故事
|
||||||
|
const createStory = () => {
|
||||||
|
if (storyText.value) {
|
||||||
|
starEnergyStore.addEnergy(8, 'logic');
|
||||||
|
alert('故事创编成功!星宝把你的故事做成了动画!获得 8 颗星星能量!');
|
||||||
|
} else {
|
||||||
|
alert('请先编写故事内容!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化画布
|
||||||
|
onMounted(() => {
|
||||||
|
if (doodleCanvas.value) {
|
||||||
|
ctx = doodleCanvas.value.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
|
||||||
|
// 画布事件监听
|
||||||
|
doodleCanvas.value.addEventListener('mousedown', (e) => {
|
||||||
|
isDrawing.value = true;
|
||||||
|
const rect = doodleCanvas.value?.getBoundingClientRect();
|
||||||
|
if (rect && ctx) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
doodleCanvas.value.addEventListener('mousemove', (e) => {
|
||||||
|
if (isDrawing.value && ctx && doodleCanvas.value) {
|
||||||
|
const rect = doodleCanvas.value.getBoundingClientRect();
|
||||||
|
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
doodleCanvas.value.addEventListener('mouseup', () => {
|
||||||
|
isDrawing.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
doodleCanvas.value.addEventListener('mouseout', () => {
|
||||||
|
isDrawing.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logic-area {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-desc {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logic-games {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maze-icon {
|
||||||
|
background-image: url('/maze.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.findDiff-icon {
|
||||||
|
background-image: url('/find_diff.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.doodle-icon {
|
||||||
|
background-image: url('/doodle.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-content {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background: #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 迷宫游戏样式 */
|
||||||
|
.maze-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-select h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-btn {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-btn.active {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maze-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maze-placeholder {
|
||||||
|
height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-maze-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 找不同游戏样式 */
|
||||||
|
.findDiff-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-pair {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image-placeholder {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 64px;
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
animation: imagePulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes imagePulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image-placeholder.game-image-a {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image-placeholder.game-image-b {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-finder h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-inputs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-input {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 涂鸦游戏样式 */
|
||||||
|
.doodle-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doodle-area h4,
|
||||||
|
.story-creator h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doodle-canvas {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doodle-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-creator p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-story-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
790
src/components/ParentArea.vue
Normal file
@@ -0,0 +1,790 @@
|
|||||||
|
<template>
|
||||||
|
<div class="parent-area">
|
||||||
|
<div class="area-header">
|
||||||
|
<h2 class="area-title">亲子守护站</h2>
|
||||||
|
<p class="area-desc">和爸爸妈妈一起冒险!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 亲子评价 -->
|
||||||
|
<div class="parent-feedback-section">
|
||||||
|
<h3 class="section-title">亲子评价</h3>
|
||||||
|
<div class="feedback-card">
|
||||||
|
<div class="feedback-icon">💬</div>
|
||||||
|
<div class="feedback-info">
|
||||||
|
<h4 class="feedback-title">基于表现的自动表扬</h4>
|
||||||
|
<p class="feedback-desc">系统会根据孩子的能量值和习惯打卡情况,自动生成表扬的话,点击朗读按钮读给孩子听!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="feedback-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>孩子当前表现</label>
|
||||||
|
<div class="performance-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">总能量值:</span>
|
||||||
|
<span class="stat-value">{{ totalStars }} 颗星星</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">习惯打卡:</span>
|
||||||
|
<span class="stat-value">{{ totalCheckins }} 次</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">击败怪兽:</span>
|
||||||
|
<span class="stat-value">{{ defeatedMonstersCount }} 只</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>自动生成的表扬语</label>
|
||||||
|
<div class="generated-feedback">
|
||||||
|
<p>{{ generatedPraise }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="generate-feedback-btn" @click="generatePraise">重新生成</button>
|
||||||
|
<button class="read-feedback-btn" @click="readFeedback" :disabled="!generatedPraise">读给孩子听</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="savedFeedbacks.length > 0" class="saved-feedbacks">
|
||||||
|
<h4>历史表扬</h4>
|
||||||
|
<div v-for="(feedback, index) in savedFeedbacks" :key="index" class="saved-feedback-item">
|
||||||
|
<div class="feedback-meta">
|
||||||
|
<span class="feedback-type">表扬</span>
|
||||||
|
<span class="feedback-date">{{ feedback.date }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="feedback-text">{{ feedback.content }}</p>
|
||||||
|
<button class="read-saved-btn" @click="readFeedback(feedback.content)">再次朗读</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成长报告 -->
|
||||||
|
<div class="growth-report-section">
|
||||||
|
<h3 class="section-title">成长报告</h3>
|
||||||
|
<div class="report-card">
|
||||||
|
<div class="report-header">
|
||||||
|
<h4>本周成长报告</h4>
|
||||||
|
<span class="report-date">{{ currentWeek }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="report-content">
|
||||||
|
<div class="report-item">
|
||||||
|
<span class="item-label">已击败怪兽:</span>
|
||||||
|
<span class="item-value">{{ defeatedMonstersCount }} 只</span>
|
||||||
|
</div>
|
||||||
|
<div class="report-item">
|
||||||
|
<span class="item-label">习惯打卡:</span>
|
||||||
|
<span class="item-value">{{ totalCheckins }} 次</span>
|
||||||
|
</div>
|
||||||
|
<div class="report-item">
|
||||||
|
<span class="item-label">数学游戏:</span>
|
||||||
|
<span class="item-value">{{ completedMathTasks }} 个</span>
|
||||||
|
</div>
|
||||||
|
<div class="report-item">
|
||||||
|
<span class="item-label">获得星星:</span>
|
||||||
|
<span class="item-value">{{ totalStars }} 颗</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="report-tips">
|
||||||
|
<h5>引导建议</h5>
|
||||||
|
<p>继续鼓励孩子坚持好习惯,多参与手工活动,培养动手能力和创造力。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 安全守护 -->
|
||||||
|
<div class="safety-section">
|
||||||
|
<h3 class="section-title">安全守护</h3>
|
||||||
|
<div class="safety-card">
|
||||||
|
<div class="safety-item">
|
||||||
|
<h4>每日冒险时间</h4>
|
||||||
|
<div class="time-setting">
|
||||||
|
<input type="range" v-model="dailyTime" min="10" max="60" step="5" class="time-slider">
|
||||||
|
<span class="time-value">{{ dailyTime }} 分钟</span>
|
||||||
|
</div>
|
||||||
|
<p class="safety-desc">建议每天最多 30 分钟,保护孩子视力。</p>
|
||||||
|
</div>
|
||||||
|
<div class="safety-item">
|
||||||
|
<h4>内容屏蔽</h4>
|
||||||
|
<div class="shield-settings">
|
||||||
|
<label class="shield-option">
|
||||||
|
<input type="checkbox" v-model="shieldSettings.violence">
|
||||||
|
<span>暴力内容</span>
|
||||||
|
</label>
|
||||||
|
<label class="shield-option">
|
||||||
|
<input type="checkbox" v-model="shieldSettings.inappropriate">
|
||||||
|
<span>不当内容</span>
|
||||||
|
</label>
|
||||||
|
<label class="shield-option">
|
||||||
|
<input type="checkbox" v-model="shieldSettings.ads">
|
||||||
|
<span>广告内容</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="safety-item">
|
||||||
|
<h4>好友管理</h4>
|
||||||
|
<p class="safety-desc">爸爸妈妈可以帮你添加好友,不用担心遇到坏人。</p>
|
||||||
|
<button class="add-friend-btn">添加好友</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||||
|
|
||||||
|
const starEnergyStore = useStarEnergyStore();
|
||||||
|
|
||||||
|
// 亲子评价相关
|
||||||
|
const generatedPraise = ref('');
|
||||||
|
const savedFeedbacks = ref<Array<{type: string, content: string, date: string}>>([]);
|
||||||
|
|
||||||
|
// 生成表扬语
|
||||||
|
const generatePraise = () => {
|
||||||
|
const energy = starEnergyStore.getTotalEnergy;
|
||||||
|
const checkins = Object.values(starEnergyStore.habits).reduce((sum, habit) => sum + habit.count, 0);
|
||||||
|
const defeatedMonsters = Object.values(starEnergyStore.monsters).filter(m => m.defeated).length;
|
||||||
|
|
||||||
|
// 根据表现生成不同的表扬语
|
||||||
|
const praiseTemplates = [
|
||||||
|
// 高能量值
|
||||||
|
energy >= 500 ? `太棒了!你已经收集了 ${energy} 颗星星,真是个超级小英雄!` : '',
|
||||||
|
energy >= 300 ? `你太厉害了!已经有 ${energy} 颗星星了,继续保持哦!` : '',
|
||||||
|
energy >= 100 ? `不错哦!你已经有 ${energy} 颗星星了,要继续努力!` : '',
|
||||||
|
|
||||||
|
// 习惯打卡
|
||||||
|
checkins >= 20 ? `你坚持打卡了 ${checkins} 次,真是个有毅力的好孩子!` : '',
|
||||||
|
checkins >= 10 ? `你已经打卡了 ${checkins} 次,好习惯正在慢慢养成!` : '',
|
||||||
|
checkins >= 5 ? `你开始养成好习惯了,已经打卡 ${checkins} 次,继续加油!` : '',
|
||||||
|
|
||||||
|
// 击败怪兽
|
||||||
|
defeatedMonsters >= 3 ? `你已经击败了 ${defeatedMonsters} 只怪兽,真是个勇敢的小战士!` : '',
|
||||||
|
defeatedMonsters >= 1 ? `你已经击败了 ${defeatedMonsters} 只怪兽,继续保护成长星球!` : '',
|
||||||
|
|
||||||
|
// 通用表扬
|
||||||
|
'你是最棒的!爸爸妈妈为你感到骄傲!',
|
||||||
|
'继续努力,你会变得越来越优秀!',
|
||||||
|
'你的进步真大,继续保持哦!',
|
||||||
|
'你真是个好孩子,爸爸妈妈爱你!',
|
||||||
|
'看到你的成长,我们真的很开心!'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 过滤掉空字符串,然后随机选择一个
|
||||||
|
const validPraise = praiseTemplates.filter(p => p);
|
||||||
|
generatedPraise.value = validPraise[Math.floor(Math.random() * validPraise.length)];
|
||||||
|
|
||||||
|
// 保存到历史记录,只保留最近5条
|
||||||
|
const newFeedback = {
|
||||||
|
type: 'praise',
|
||||||
|
content: generatedPraise.value,
|
||||||
|
date: new Date().toLocaleString()
|
||||||
|
};
|
||||||
|
|
||||||
|
savedFeedbacks.value.unshift(newFeedback); // 添加到开头
|
||||||
|
if (savedFeedbacks.value.length > 5) {
|
||||||
|
savedFeedbacks.value = savedFeedbacks.value.slice(0, 5); // 只保留前5条
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
const user = localStorage.getItem('currentUser') || 'default';
|
||||||
|
localStorage.setItem(`parentFeedback_${user}`, JSON.stringify(savedFeedbacks.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 朗读评价
|
||||||
|
const readFeedback = (content?: string) => {
|
||||||
|
let text = content || generatedPraise.value;
|
||||||
|
|
||||||
|
// 确保text是字符串且不为空
|
||||||
|
if (!text || typeof text !== 'string' || text.trim() === '') {
|
||||||
|
// 如果没有表扬语,先生成一个
|
||||||
|
generatePraise();
|
||||||
|
text = generatedPraise.value;
|
||||||
|
|
||||||
|
// 如果仍然没有表扬语,提示用户
|
||||||
|
if (!text || typeof text !== 'string' || text.trim() === '') {
|
||||||
|
alert('请先生成表扬语!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const speech = new SpeechSynthesisUtterance(text);
|
||||||
|
speech.lang = 'zh-CN';
|
||||||
|
speech.volume = 1;
|
||||||
|
speech.rate = 1;
|
||||||
|
speech.pitch = 1.2;
|
||||||
|
|
||||||
|
// 尝试选择更适合儿童的中文语音
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
|
||||||
|
if (voices.length > 0) {
|
||||||
|
// 优先选择女性或儿童语音
|
||||||
|
const selectedVoice = voices.find(voice =>
|
||||||
|
voice.lang === 'zh-CN' &&
|
||||||
|
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女') || voice.name.includes('Microsoft Yaoyao') || voice.name.includes('Microsoft Huihui'))
|
||||||
|
) || voices.find(voice => voice.lang === 'zh-CN');
|
||||||
|
|
||||||
|
if (selectedVoice) {
|
||||||
|
speech.voice = selectedVoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即播放
|
||||||
|
window.speechSynthesis.speak(speech);
|
||||||
|
} else {
|
||||||
|
// 语音列表尚未加载完成,等待加载完成后再播放
|
||||||
|
const handleVoicesChanged = () => {
|
||||||
|
const updatedVoices = window.speechSynthesis.getVoices();
|
||||||
|
const selectedVoice = updatedVoices.find(voice =>
|
||||||
|
voice.lang === 'zh-CN' &&
|
||||||
|
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女') || voice.name.includes('Microsoft Yaoyao') || voice.name.includes('Microsoft Huihui'))
|
||||||
|
) || updatedVoices.find(voice => voice.lang === 'zh-CN');
|
||||||
|
|
||||||
|
if (selectedVoice) {
|
||||||
|
speech.voice = selectedVoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.speechSynthesis.speak(speech);
|
||||||
|
|
||||||
|
// 移除事件监听器
|
||||||
|
window.speechSynthesis.removeEventListener('voiceschanged', handleVoicesChanged);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加事件监听器
|
||||||
|
window.speechSynthesis.addEventListener('voiceschanged', handleVoicesChanged);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('音频播放失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载历史评价
|
||||||
|
const loadSavedFeedbacks = () => {
|
||||||
|
const user = localStorage.getItem('currentUser') || 'default';
|
||||||
|
const saved = localStorage.getItem(`parentFeedback_${user}`);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
savedFeedbacks.value = JSON.parse(saved);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载评价失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化时加载语音合成API的voices和生成表扬语
|
||||||
|
onMounted(() => {
|
||||||
|
// 触发语音合成API的加载
|
||||||
|
window.speechSynthesis.getVoices();
|
||||||
|
// 加载历史评价并生成第一条表扬语
|
||||||
|
loadSavedFeedbacks();
|
||||||
|
generatePraise();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 成长报告相关
|
||||||
|
const currentWeek = computed(() => {
|
||||||
|
const today = new Date();
|
||||||
|
const startOfWeek = new Date(today);
|
||||||
|
startOfWeek.setDate(today.getDate() - today.getDay() + 1);
|
||||||
|
const endOfWeek = new Date(startOfWeek);
|
||||||
|
endOfWeek.setDate(startOfWeek.getDate() + 6);
|
||||||
|
return `${startOfWeek.getMonth() + 1}/${startOfWeek.getDate()} - ${endOfWeek.getMonth() + 1}/${endOfWeek.getDate()}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const defeatedMonstersCount = computed(() => {
|
||||||
|
return Object.values(starEnergyStore.monsters).filter(m => m.defeated).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCheckins = computed(() => {
|
||||||
|
return Object.values(starEnergyStore.habits).reduce((sum, habit) => sum + habit.count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const completedMathTasks = ref(5); // 模拟数据
|
||||||
|
const totalStars = computed(() => starEnergyStore.getTotalEnergy);
|
||||||
|
|
||||||
|
// 安全设置
|
||||||
|
const dailyTime = ref(30);
|
||||||
|
const shieldSettings = ref({
|
||||||
|
violence: true,
|
||||||
|
inappropriate: true,
|
||||||
|
ads: true
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.parent-area {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-desc {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #667eea;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 亲子评价 */
|
||||||
|
.parent-feedback-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-form {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-type-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn {
|
||||||
|
flex: 1;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-feedback {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-feedback p {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-feedback-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-feedback-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-feedback-btn {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-feedback-btn:hover:not(:disabled) {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-feedback-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-feedbacks {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-feedbacks h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #667eea;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-feedback-item {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-type {
|
||||||
|
font-weight: bold;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-saved-btn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-saved-btn:hover {
|
||||||
|
background: #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成长报告 */
|
||||||
|
.growth-report-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-header h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-date {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-content {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-tips {
|
||||||
|
background: #f0f8ff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-tips h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-tips p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 安全守护 */
|
||||||
|
.safety-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safety-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safety-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safety-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safety-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safety-item h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slider {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #ddd;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #667eea;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slider::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #667eea;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safety-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shield-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shield-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shield-option input {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-friend-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-friend-btn:hover {
|
||||||
|
background: #764ba2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
813
src/components/RewardArea.vue
Normal file
@@ -0,0 +1,813 @@
|
|||||||
|
<template>
|
||||||
|
<div class="reward-area">
|
||||||
|
<div class="reward-header">
|
||||||
|
<h2 class="reward-title">🎁 奖励商店</h2>
|
||||||
|
<p class="reward-subtitle">用星星兑换你喜欢的奖励吧!✨</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 星星余额显示 -->
|
||||||
|
<div class="star-balance">
|
||||||
|
<div class="balance-card">
|
||||||
|
<div class="balance-icon">⭐</div>
|
||||||
|
<div class="balance-info">
|
||||||
|
<div class="balance-label">我的星星</div>
|
||||||
|
<div class="balance-amount">{{ starEnergyStore.getTotalEnergy }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 奖励分类 -->
|
||||||
|
<div class="reward-categories">
|
||||||
|
<button
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
class="category-btn"
|
||||||
|
:class="{ active: currentCategory === category.id }"
|
||||||
|
@click="currentCategory = category.id"
|
||||||
|
>
|
||||||
|
<span class="category-icon">{{ category.icon }}</span>
|
||||||
|
<span class="category-name">{{ category.name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 奖励列表 -->
|
||||||
|
<div class="reward-list">
|
||||||
|
<div
|
||||||
|
v-for="reward in filteredRewards"
|
||||||
|
:key="reward.id"
|
||||||
|
class="reward-card"
|
||||||
|
:class="{
|
||||||
|
disabled: (reward.type === 'boss_defeat' ? starEnergyStore.getBossDefeats < reward.cost : starEnergyStore.getTotalEnergy < reward.cost),
|
||||||
|
claimed: claimedRewards.includes(reward.id)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="reward-emoji">{{ reward.emoji }}</div>
|
||||||
|
<div class="reward-info">
|
||||||
|
<h3 class="reward-name">{{ reward.name }}</h3>
|
||||||
|
<p class="reward-desc">{{ reward.description }}</p>
|
||||||
|
<div class="reward-cost">
|
||||||
|
<span class="cost-icon">{{ reward.type === 'boss_defeat' ? '👾' : '⭐' }}</span>
|
||||||
|
<span class="cost-value">{{ reward.cost }}</span>
|
||||||
|
<span class="cost-unit">{{ reward.type === 'boss_defeat' ? '次击败' : '星' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="redeem-btn"
|
||||||
|
:class="{ disabled: ((reward.type === 'boss_defeat' ? starEnergyStore.getBossDefeats < reward.cost : starEnergyStore.getTotalEnergy < reward.cost) || claimedRewards.includes(reward.id)) }"
|
||||||
|
@click="redeemReward(reward)"
|
||||||
|
:disabled="(reward.type === 'boss_defeat' ? starEnergyStore.getBossDefeats < reward.cost : starEnergyStore.getTotalEnergy < reward.cost) || claimedRewards.includes(reward.id)"
|
||||||
|
>
|
||||||
|
{{ getButtonText(reward) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已兑换奖励 -->
|
||||||
|
<div class="claimed-rewards" v-if="claimedRewards.length > 0">
|
||||||
|
<h3 class="claimed-title">🎉 已兑换的奖励</h3>
|
||||||
|
<div class="claimed-grid">
|
||||||
|
<div
|
||||||
|
v-for="id in claimedRewards"
|
||||||
|
:key="id"
|
||||||
|
class="claimed-card"
|
||||||
|
>
|
||||||
|
{{ getRewardById(id)?.emoji }} {{ getRewardById(id)?.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 兑换成功弹窗 -->
|
||||||
|
<div v-if="showSuccessModal" class="modal-overlay" @click="closeModal">
|
||||||
|
<div class="modal-content" @click.stop>
|
||||||
|
<div class="modal-emoji">🎉</div>
|
||||||
|
<h3 class="modal-title">兑换成功!</h3>
|
||||||
|
<p class="modal-message">恭喜你获得了 {{ selectedReward?.name }}!</p>
|
||||||
|
<button class="modal-btn" @click="closeModal">太棒了!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 星星不足提示 -->
|
||||||
|
<div v-if="showInsufficientModal" class="modal-overlay" @click="closeModal">
|
||||||
|
<div class="modal-content insufficient" @click.stop>
|
||||||
|
<div class="modal-emoji">😢</div>
|
||||||
|
<h3 class="modal-title">星星不足</h3>
|
||||||
|
<p class="modal-message">你还需要 {{ insufficientStars }} 颗星星才能兑换这个奖励哦!</p>
|
||||||
|
<button class="modal-btn" @click="closeModal">继续努力!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||||
|
|
||||||
|
const starEnergyStore = useStarEnergyStore();
|
||||||
|
|
||||||
|
// 当前分类
|
||||||
|
const currentCategory = ref('all');
|
||||||
|
|
||||||
|
// 已兑换奖励ID列表
|
||||||
|
const claimedRewards = computed(() => starEnergyStore.claimedRewards);
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
const showSuccessModal = ref(false);
|
||||||
|
const showInsufficientModal = ref(false);
|
||||||
|
const insufficientStars = ref(0);
|
||||||
|
const selectedReward = ref<any>(null);
|
||||||
|
|
||||||
|
// 奖励分类
|
||||||
|
const categories = [
|
||||||
|
{ id: 'all', name: '全部', icon: '🎯' },
|
||||||
|
{ id: 'toy', name: '玩具', icon: '🎮' },
|
||||||
|
{ id: 'entertainment', name: '娱乐', icon: '🎬' },
|
||||||
|
{ id: 'food', name: '美食', icon: '🍔' },
|
||||||
|
{ id: 'activity', name: '活动', icon: '🎪' },
|
||||||
|
{ id: 'boss', name: 'BOSS奖励', icon: '👾' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 奖励列表
|
||||||
|
const rewards = [
|
||||||
|
// 小奖励(20星)
|
||||||
|
{
|
||||||
|
id: 'cartoon_20',
|
||||||
|
name: '看20分钟动画片',
|
||||||
|
description: '可以看20分钟喜欢的动画片哦!',
|
||||||
|
cost: 20,
|
||||||
|
emoji: '📺',
|
||||||
|
category: 'entertainment'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'snack_20',
|
||||||
|
name: '小零食一份',
|
||||||
|
description: '可以吃一份健康的小零食!',
|
||||||
|
cost: 20,
|
||||||
|
emoji: '🍪',
|
||||||
|
category: 'food'
|
||||||
|
},
|
||||||
|
// 中等奖励(50星)
|
||||||
|
{
|
||||||
|
id: 'movie_50',
|
||||||
|
name: '看一部电影',
|
||||||
|
description: '可以看一部完整的电影!',
|
||||||
|
cost: 50,
|
||||||
|
emoji: '🎬',
|
||||||
|
category: 'entertainment'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'icecream_50',
|
||||||
|
name: '冰淇淋一个',
|
||||||
|
description: '可以吃一个美味的冰淇淋!',
|
||||||
|
cost: 50,
|
||||||
|
emoji: '🍦',
|
||||||
|
category: 'food'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'park_50',
|
||||||
|
name: '去公园玩',
|
||||||
|
description: '可以和爸爸妈妈去公园玩!',
|
||||||
|
cost: 50,
|
||||||
|
emoji: '🎡',
|
||||||
|
category: 'activity'
|
||||||
|
},
|
||||||
|
// 大奖励(100星)
|
||||||
|
{
|
||||||
|
id: 'toy_100',
|
||||||
|
name: '一个小玩具',
|
||||||
|
description: '可以获得一个心仪的小玩具!',
|
||||||
|
cost: 100,
|
||||||
|
emoji: '🧸',
|
||||||
|
category: 'toy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'restaurant_100',
|
||||||
|
name: '去餐厅吃饭',
|
||||||
|
description: '可以去喜欢的餐厅吃顿好的!',
|
||||||
|
cost: 100,
|
||||||
|
emoji: '🍽️',
|
||||||
|
category: 'food'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'zoo_100',
|
||||||
|
name: '去动物园',
|
||||||
|
description: '可以去动物园看小动物!',
|
||||||
|
cost: 100,
|
||||||
|
emoji: '🦁',
|
||||||
|
category: 'activity'
|
||||||
|
},
|
||||||
|
// 超级奖励(200星)
|
||||||
|
{
|
||||||
|
id: 'lego_200',
|
||||||
|
name: '乐高积木套装',
|
||||||
|
description: '可以获得一套乐高积木!',
|
||||||
|
cost: 200,
|
||||||
|
emoji: '🧱',
|
||||||
|
category: 'toy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sticker_book_200',
|
||||||
|
name: '贴纸书',
|
||||||
|
description: '女孩喜欢的精美贴纸书!',
|
||||||
|
cost: 200,
|
||||||
|
emoji: '🖼️',
|
||||||
|
category: 'toy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quiet_book_200',
|
||||||
|
name: '安静书',
|
||||||
|
description: '女孩喜欢的手工安静书!',
|
||||||
|
cost: 200,
|
||||||
|
emoji: '📚',
|
||||||
|
category: 'toy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'amusement_200',
|
||||||
|
name: '游乐园一日游',
|
||||||
|
description: '可以和爸爸妈妈去游乐园玩一整天!',
|
||||||
|
cost: 200,
|
||||||
|
emoji: '🎢',
|
||||||
|
category: 'activity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bike_200',
|
||||||
|
name: '自行车一辆',
|
||||||
|
description: '可以获得一辆全新的自行车!',
|
||||||
|
cost: 200,
|
||||||
|
emoji: '🚲',
|
||||||
|
category: 'toy'
|
||||||
|
},
|
||||||
|
// 高级奖励(400星)
|
||||||
|
{
|
||||||
|
id: 'special_toy_400',
|
||||||
|
name: '指定玩具',
|
||||||
|
description: '可以获得一个指定的心仪玩具!',
|
||||||
|
cost: 400,
|
||||||
|
emoji: '🎁',
|
||||||
|
category: 'toy'
|
||||||
|
},
|
||||||
|
// 终极奖励(500星)
|
||||||
|
{
|
||||||
|
id: 'family_trip_500',
|
||||||
|
name: '家庭旅行',
|
||||||
|
description: '可以和家人一起去旅行!',
|
||||||
|
cost: 500,
|
||||||
|
emoji: '✈️',
|
||||||
|
category: 'activity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'game_console_500',
|
||||||
|
name: '游戏机',
|
||||||
|
description: '可以获得一台游戏机!',
|
||||||
|
cost: 500,
|
||||||
|
emoji: '🎮',
|
||||||
|
category: 'toy'
|
||||||
|
},
|
||||||
|
// BOSS奖励(需要击败BOSS次数)
|
||||||
|
{
|
||||||
|
id: 'boss_reward_1',
|
||||||
|
name: 'BOSS击败者勋章',
|
||||||
|
description: '击败BOSS一次的荣誉勋章!',
|
||||||
|
cost: 1,
|
||||||
|
emoji: '🏆',
|
||||||
|
category: 'boss',
|
||||||
|
type: 'boss_defeat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'boss_reward_3',
|
||||||
|
name: 'BOSS克星称号',
|
||||||
|
description: '击败BOSS三次的尊贵称号!',
|
||||||
|
cost: 3,
|
||||||
|
emoji: '👑',
|
||||||
|
category: 'boss',
|
||||||
|
type: 'boss_defeat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'boss_reward_5',
|
||||||
|
name: '终极BOSS猎手',
|
||||||
|
description: '击败BOSS五次的终极荣誉!',
|
||||||
|
cost: 5,
|
||||||
|
emoji: '🌟',
|
||||||
|
category: 'boss',
|
||||||
|
type: 'boss_defeat'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 筛选奖励
|
||||||
|
const filteredRewards = computed(() => {
|
||||||
|
if (currentCategory.value === 'all') {
|
||||||
|
return rewards;
|
||||||
|
}
|
||||||
|
return rewards.filter(reward => reward.category === currentCategory.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取按钮文本
|
||||||
|
const getButtonText = (reward: any) => {
|
||||||
|
if (claimedRewards.value.includes(reward.id)) {
|
||||||
|
return '已兑换 ✓';
|
||||||
|
}
|
||||||
|
if (reward.type === 'boss_defeat') {
|
||||||
|
if (starEnergyStore.getBossDefeats < reward.cost) {
|
||||||
|
return `还差${reward.cost - starEnergyStore.getBossDefeats}次击败`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (starEnergyStore.getTotalEnergy < reward.cost) {
|
||||||
|
return `还差${reward.cost - starEnergyStore.getTotalEnergy}星`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '立即兑换';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 兑换奖励
|
||||||
|
const redeemReward = (reward: any) => {
|
||||||
|
// 检查是否是BOSS奖励
|
||||||
|
if (reward.type === 'boss_defeat') {
|
||||||
|
if (starEnergyStore.getBossDefeats < reward.cost) {
|
||||||
|
insufficientStars.value = reward.cost - starEnergyStore.getBossDefeats;
|
||||||
|
selectedReward.value = reward;
|
||||||
|
showInsufficientModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通奖励使用星星能量
|
||||||
|
if (starEnergyStore.getTotalEnergy < reward.cost) {
|
||||||
|
insufficientStars.value = reward.cost - starEnergyStore.getTotalEnergy;
|
||||||
|
selectedReward.value = reward;
|
||||||
|
showInsufficientModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimedRewards.value.includes(reward.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedReward.value = reward;
|
||||||
|
let success = false;
|
||||||
|
|
||||||
|
if (reward.type === 'boss_defeat') {
|
||||||
|
// BOSS奖励使用击败次数兑换
|
||||||
|
// 这里可以添加专门的BOSS奖励兑换逻辑
|
||||||
|
success = starEnergyStore.redeemReward(reward.id, 0); // 不消耗星星
|
||||||
|
} else {
|
||||||
|
// 普通奖励使用星星能量
|
||||||
|
success = starEnergyStore.redeemReward(reward.id, reward.cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showSuccessModal.value = true;
|
||||||
|
|
||||||
|
// 播放音效
|
||||||
|
try {
|
||||||
|
const speech = new SpeechSynthesisUtterance('恭喜你,兑换成功!');
|
||||||
|
speech.lang = 'zh-CN';
|
||||||
|
speech.volume = 1;
|
||||||
|
speech.rate = 1;
|
||||||
|
speech.pitch = 1.2;
|
||||||
|
window.speechSynthesis.speak(speech);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('音频播放失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据ID获取奖励
|
||||||
|
const getRewardById = (id: string) => {
|
||||||
|
return rewards.find(reward => reward.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const closeModal = () => {
|
||||||
|
showSuccessModal.value = false;
|
||||||
|
showInsufficientModal.value = false;
|
||||||
|
selectedReward.value = null;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reward-area {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FF69B4;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 星星余额 */
|
||||||
|
.star-balance {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-card {
|
||||||
|
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
box-shadow: 0 8px 25px rgba(255, 215, 0, 0.4);
|
||||||
|
border: 4px solid #FF8C00;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-icon {
|
||||||
|
font-size: 50px;
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-label {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8B4513;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-amount {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #8B4513;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 奖励分类 */
|
||||||
|
.reward-categories {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 3px solid #FFB6C1;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-btn.active {
|
||||||
|
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||||
|
border-color: #FF69B4;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-btn.active .category-name {
|
||||||
|
color: white;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 奖励列表 */
|
||||||
|
.reward-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 4px solid #98FB98;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
animation: shine 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% { transform: translateX(-100%) rotate(45deg); }
|
||||||
|
100% { transform: translateX(100%) rotate(45deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-card:hover:not(.disabled):not(.claimed) {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
border-color: #32CD32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-card.disabled {
|
||||||
|
border-color: #ccc;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-card.claimed {
|
||||||
|
border-color: #FFD700;
|
||||||
|
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-emoji {
|
||||||
|
font-size: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-cost {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||||
|
border-radius: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #8B4513;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8B4513;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-btn {
|
||||||
|
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||||
|
color: #006400;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-btn:hover:not(.disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
background: linear-gradient(135deg, #90EE90, #32CD32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-btn.disabled {
|
||||||
|
background: #ccc;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已兑换奖励 */
|
||||||
|
.claimed-rewards {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 4px solid #FFD700;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimed-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FF8C00;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimed-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimed-card {
|
||||||
|
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 10px rgba(255, 215, 0, 0.4);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗 */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 5px solid #98FB98;
|
||||||
|
animation: modalAppear 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.insufficient {
|
||||||
|
border-color: #FFB6C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8) translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-emoji {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: bounce 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-message {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||||
|
color: #006400;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 40px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(5deg); }
|
||||||
|
75% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.reward-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-card {
|
||||||
|
padding: 15px;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-emoji {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimed-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
744
src/components/WelcomeArea.vue
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome-area">
|
||||||
|
<!-- 漂浮的星星装饰 -->
|
||||||
|
<div class="star-decoration star-1">⭐</div>
|
||||||
|
<div class="star-decoration star-2">✨</div>
|
||||||
|
<div class="star-decoration star-3">⭐</div>
|
||||||
|
<div class="star-decoration star-4">✨</div>
|
||||||
|
<div class="star-decoration star-5">⭐</div>
|
||||||
|
<!-- 漂浮的成长星球图片 -->
|
||||||
|
<div class="planet-decoration planet-1">
|
||||||
|
<img :src="'./儿童成长星球.png'" alt="儿童成长星球" class="floating-planet">
|
||||||
|
</div>
|
||||||
|
<!-- 漂浮的卡通图片装饰 -->
|
||||||
|
<div class="floating-image image-1">
|
||||||
|
<img :src="'./生成卡通图片 (8).png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
<div class="floating-image image-2">
|
||||||
|
<img :src="'./生成卡通图片 (9).png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
<div class="floating-image image-3">
|
||||||
|
<img :src="'./生成卡通图片 (10).png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
<div class="floating-image image-4">
|
||||||
|
<img :src="'./生成卡通图片 (11).png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
<div class="floating-image image-5">
|
||||||
|
<img :src="'./生成卡通图片 (12).png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
<div class="floating-image image-6">
|
||||||
|
<img :src="'./生成卡通图片 (13).png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
<div class="floating-image image-7">
|
||||||
|
<img :src="'./哪吒.png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
<div class="floating-image image-8">
|
||||||
|
<img :src="'./孙悟空.png'" alt="卡通装饰" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 星宝角色 -->
|
||||||
|
<div class="xingbao-guide">
|
||||||
|
<div class="xingbao-avatar">🌟</div>
|
||||||
|
<div class="xingbao-speech">
|
||||||
|
<div class="speech-bubble">
|
||||||
|
<p>小勇士,欢迎来到成长星球!我是你的向导星宝~</p>
|
||||||
|
</div>
|
||||||
|
<button class="audio-btn mini" @click="playAudio('小勇士,欢迎来到成长星球!我是你的向导星宝!')">🔊</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-header">
|
||||||
|
<div class="header-with-audio">
|
||||||
|
<h2 class="welcome-title">✨ 欢迎来到成长星球! ✨</h2>
|
||||||
|
<button class="audio-btn" @click="playAudio('欢迎来到成长星球!你准备好成为成长小勇士了吗?')">🔊</button>
|
||||||
|
</div>
|
||||||
|
<p class="welcome-subtitle">你准备好成为成长小勇士了吗?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-content">
|
||||||
|
<div class="welcome-image">
|
||||||
|
<div class="planet-container">
|
||||||
|
<img :src="'./成长星球1.png'" alt="成长星球" class="planet-image">
|
||||||
|
<div class="planet-glow"></div>
|
||||||
|
<div class="planet-orbit">
|
||||||
|
<div class="orbit-star">⭐</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-story">
|
||||||
|
<div class="story-card">
|
||||||
|
<div class="story-icon">🌍</div>
|
||||||
|
<div class="story-text">
|
||||||
|
<p>在遥远的宇宙里,有一颗超可爱的成长星球~ 这里长满了知识树、习惯花,还有会说话的小精灵!</p>
|
||||||
|
<button class="story-audio-btn" @click="playAudio('在遥远的宇宙里,有一颗超可爱的成长星球。这里长满了知识树、习惯花,还有会说话的小精灵!')">🔊</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="story-card warning">
|
||||||
|
<div class="story-icon">😟</div>
|
||||||
|
<div class="story-text">
|
||||||
|
<p>但最近,3 只 "坏习惯怪兽" 偷偷入侵了星球,把知识树的叶子弄蔫了,习惯花也不开了😟</p>
|
||||||
|
<button class="story-audio-btn" @click="playAudio('但是最近,3只坏习惯怪兽偷偷入侵了星球,把知识树的叶子弄蔫了,习惯花也不开了。')">🔊</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="story-card mission">
|
||||||
|
<div class="story-icon">🦸</div>
|
||||||
|
<div class="story-text">
|
||||||
|
<p>你愿意化身 "成长小勇士",和星球向导 "星宝" 一起,通过学习、动手、养成好习惯,收集 "星星能量",打败怪兽、守护星球,成为全能小英雄吗?</p>
|
||||||
|
<button class="story-audio-btn" @click="playAudio('你愿意化身成长小勇士,和星球向导星宝一起,通过学习、动手、养成好习惯,收集星星能量,打败怪兽、守护星球,成为全能小英雄吗?')">🔊</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-start">
|
||||||
|
<button class="start-btn" @click="startAdventure">
|
||||||
|
<span class="btn-icon">🚀</span>
|
||||||
|
<span class="btn-text">开始冒险</span>
|
||||||
|
</button>
|
||||||
|
<div class="start-hint">💡 点击按钮开启你的冒险之旅!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡通人物 -->
|
||||||
|
<div class="cartoon-characters">
|
||||||
|
<div class="character kuromi">
|
||||||
|
<div class="character-bubble">加油哦!💪</div>
|
||||||
|
<div class="character-emoji">💜</div>
|
||||||
|
<div class="character-name">库洛米</div>
|
||||||
|
</div>
|
||||||
|
<div class="character nailong">
|
||||||
|
<div class="character-bubble">一起玩吧!🎉</div>
|
||||||
|
<div class="character-emoji">🐲</div>
|
||||||
|
<div class="character-name">奶龙</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'navigate', area: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 初始化时加载语音合成API的voices
|
||||||
|
onMounted(() => {
|
||||||
|
// 触发语音合成API的加载
|
||||||
|
window.speechSynthesis.getVoices();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放音频 - 库洛米声音风格
|
||||||
|
const playAudio = (text: string) => {
|
||||||
|
try {
|
||||||
|
// 使用Web Speech API进行文本转语音
|
||||||
|
const speech = new SpeechSynthesisUtterance(text);
|
||||||
|
// 设置语言
|
||||||
|
speech.lang = 'zh-CN';
|
||||||
|
// 设置音量、语速和语调 - 库洛米风格(可爱、活泼、略带淘气)
|
||||||
|
speech.volume = 1; // 音量
|
||||||
|
speech.rate = 1.1; // 语速稍快,更有活力
|
||||||
|
speech.pitch = 1.7; // 更高的语调,更可爱
|
||||||
|
|
||||||
|
// 尝试选择更适合儿童的语音
|
||||||
|
const voices = window.speechSynthesis.getVoices();
|
||||||
|
if (voices.length > 0) {
|
||||||
|
// 优先选择女性或儿童语音
|
||||||
|
let selectedVoice = voices.find(voice =>
|
||||||
|
voice.lang === 'zh-CN' &&
|
||||||
|
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女'))
|
||||||
|
) || voices.find(voice => voice.lang === 'zh-CN');
|
||||||
|
|
||||||
|
if (selectedVoice) {
|
||||||
|
speech.voice = selectedVoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放
|
||||||
|
window.speechSynthesis.speak(speech);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('音频播放失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAdventure = () => {
|
||||||
|
// 播放开始冒险的语音
|
||||||
|
playAudio('让我们开始冒险吧!选择你想去的场景吧!');
|
||||||
|
// 触发导航事件,显示冒险地图
|
||||||
|
emit('navigate', 'adventure');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.welcome-area {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
position: relative;
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
background: linear-gradient(135deg, #FFF5E6 0%, #FFE4E1 50%, #E0F7FA 100%);
|
||||||
|
border-radius: 30px;
|
||||||
|
margin: 10px;
|
||||||
|
box-shadow: 0 10px 30px rgba(255, 182, 193, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 星星装饰 */
|
||||||
|
.star-decoration {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 24px;
|
||||||
|
animation: twinkle 2s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-1 { top: 50px; left: 30px; animation-delay: 0s; }
|
||||||
|
.star-2 { top: 80px; right: 50px; animation-delay: 0.5s; }
|
||||||
|
.star-3 { top: 200px; left: 80px; animation-delay: 1s; }
|
||||||
|
.star-4 { bottom: 150px; right: 100px; animation-delay: 1.5s; }
|
||||||
|
.star-5 { bottom: 100px; left: 150px; animation-delay: 2s; }
|
||||||
|
|
||||||
|
/* 漂浮的成长星球图片 */
|
||||||
|
.planet-decoration {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-1 {
|
||||||
|
top: 100px;
|
||||||
|
right: 150px;
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-planet {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 40px rgba(255, 182, 193, 0.5);
|
||||||
|
animation: pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 漂浮的卡通图片装饰 */
|
||||||
|
.floating-image {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: float 15s ease-in-out infinite;
|
||||||
|
filter: drop-shadow(0 8px 20px rgba(255, 255, 255, 0.5));
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-image img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-image.image-1 { top: 20%; left: 10%; animation-delay: 0s; animation-duration: 18s; }
|
||||||
|
.floating-image.image-2 { top: 30%; right: 10%; animation-delay: 3s; animation-duration: 20s; }
|
||||||
|
.floating-image.image-3 { bottom: 20%; left: 10%; animation-delay: 6s; animation-duration: 19s; }
|
||||||
|
.floating-image.image-4 { bottom: 30%; right: 10%; animation-delay: 9s; animation-duration: 21s; }
|
||||||
|
.floating-image.image-5 { top: 50%; left: 5%; animation-delay: 12s; animation-duration: 17s; }
|
||||||
|
.floating-image.image-6 { top: 15%; right: 15%; animation-delay: 1s; animation-duration: 16s; }
|
||||||
|
.floating-image.image-7 { bottom: 15%; left: 15%; animation-delay: 4s; animation-duration: 22s; }
|
||||||
|
.floating-image.image-8 { top: 60%; right: 5%; animation-delay: 7s; animation-duration: 15s; }
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.floating-image img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-image.image-1 { top: 15%; left: 5%; }
|
||||||
|
.floating-image.image-2 { top: 25%; right: 5%; }
|
||||||
|
.floating-image.image-3 { bottom: 15%; left: 5%; }
|
||||||
|
.floating-image.image-4 { bottom: 25%; right: 5%; }
|
||||||
|
.floating-image.image-5 { top: 45%; left: 5%; }
|
||||||
|
.floating-image.image-6 { top: 10%; right: 10%; }
|
||||||
|
.floating-image.image-7 { bottom: 10%; left: 10%; }
|
||||||
|
.floating-image.image-8 { top: 60%; right: 5%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 星宝角色 */
|
||||||
|
.xingbao-guide {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xingbao-avatar {
|
||||||
|
font-size: 50px;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.xingbao-speech {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-bubble {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
position: relative;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-bubble::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 20px;
|
||||||
|
border-width: 10px 10px 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: white transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-bubble p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-with-audio {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(135deg, #FF69B4, #FFB6C1);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-subtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
background: white;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 25px;
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
max-width: 550px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 星球容器 */
|
||||||
|
.welcome-image {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-image {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 40px rgba(255, 182, 193, 0.5), 0 0 80px rgba(255, 215, 0, 0.3);
|
||||||
|
animation: pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
background: radial-gradient(circle, rgba(255, 215, 0, 0.2) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: glow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-orbit {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 260px;
|
||||||
|
height: 260px;
|
||||||
|
border: 2px dashed rgba(255, 215, 0, 0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: rotate 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orbit-star {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 24px;
|
||||||
|
animation: twinkle 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 故事卡片 */
|
||||||
|
.welcome-story {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
border: 3px solid #FFB6C1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card.warning {
|
||||||
|
border-color: #FFD700;
|
||||||
|
background: linear-gradient(135deg, #FFFACD, #FFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card.mission {
|
||||||
|
border-color: #98FB98;
|
||||||
|
background: linear-gradient(135deg, #F0FFF0, #FFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-text {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-text p {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-start {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
background: linear-gradient(135deg, #98FB98, #32CD32, #90EE90);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
color: #006400;
|
||||||
|
border: none;
|
||||||
|
padding: 18px 50px;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 6px 20px rgba(50, 205, 50, 0.4);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: shine 3s linear infinite;
|
||||||
|
border: 3px solid #006400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn:hover {
|
||||||
|
transform: translateY(-8px) scale(1.05);
|
||||||
|
box-shadow: 0 12px 30px rgba(50, 205, 50, 0.6);
|
||||||
|
background-position: right center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn:active {
|
||||||
|
transform: translateY(-4px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
animation: wiggle 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-hint {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
animation: twinkle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡通人物 */
|
||||||
|
.cartoon-characters {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 0 40px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character:nth-child(2) {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-bubble {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-bubble::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 8px 8px 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: white transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-emoji {
|
||||||
|
font-size: 60px;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
margin-top: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 音频按钮样式 */
|
||||||
|
.audio-btn {
|
||||||
|
background: linear-gradient(135deg, #FFD166, #06D6A0);
|
||||||
|
border: none;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
border: 2px solid #06D6A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-btn:hover {
|
||||||
|
transform: scale(1.15) rotate(15deg);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-btn.mini {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-audio-btn {
|
||||||
|
background: linear-gradient(135deg, #FFB6C1, #FF69B4);
|
||||||
|
border: none;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 3px 6px rgba(255, 105, 180, 0.3);
|
||||||
|
animation: twinkle 2s ease-in-out infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-audio-btn:hover {
|
||||||
|
transform: scale(1.2) rotate(10deg);
|
||||||
|
box-shadow: 0 5px 10px rgba(255, 105, 180, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画定义 */
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(5deg); }
|
||||||
|
75% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.5; transform: scale(0.9); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-15px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||||
|
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% { background-position: left center; }
|
||||||
|
100% { background-position: right center; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.welcome-area {
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-image {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-glow {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planet-orbit {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card {
|
||||||
|
padding: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-icon {
|
||||||
|
font-size: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xingbao-guide {
|
||||||
|
display: none; /* 小屏幕隐藏星宝 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartoon-characters {
|
||||||
|
padding: 0 20px;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-emoji {
|
||||||
|
font-size: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
padding: 14px 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
src/main.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.mount('#app')
|
||||||
264
src/stores/adventureProgress.ts
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
// 从 localStorage 加载数据
|
||||||
|
const loadFromLocalStorage = () => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('adventureProgressData');
|
||||||
|
return saved ? JSON.parse(saved) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载冒险进度失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
const saveToLocalStorage = (data: any) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('adventureProgressData', JSON.stringify(data));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存冒险进度失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 冒险进度存储
|
||||||
|
export const useAdventureProgressStore = defineStore('adventureProgress', {
|
||||||
|
state: () => {
|
||||||
|
// 尝试从 localStorage 加载保存的数据
|
||||||
|
const savedData = loadFromLocalStorage();
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 各游戏完成次数
|
||||||
|
gameCompletions: savedData?.gameCompletions || {
|
||||||
|
// 太空游戏
|
||||||
|
'planet-explorer': 0,
|
||||||
|
'star-collector': 0,
|
||||||
|
'space-puzzle': 0,
|
||||||
|
'moon-walk': 0,
|
||||||
|
'constellation': 0,
|
||||||
|
// 天空游戏
|
||||||
|
'cloud-maze': 0,
|
||||||
|
'bird-flight': 0,
|
||||||
|
'rainbow-paint': 0,
|
||||||
|
'cloud-puzzle': 0,
|
||||||
|
// 海洋游戏
|
||||||
|
'treasure-hunt': 0,
|
||||||
|
'fish-match': 0,
|
||||||
|
'coral-color': 0,
|
||||||
|
'ocean-puzzle': 0,
|
||||||
|
// 地下游戏
|
||||||
|
'mining': 0,
|
||||||
|
'mole-whack': 0,
|
||||||
|
'cave-explore': 0,
|
||||||
|
// 森林游戏
|
||||||
|
'animal-friends': 0,
|
||||||
|
'plant-grow': 0,
|
||||||
|
'leaf-collect': 0,
|
||||||
|
'forest-puzzle': 0
|
||||||
|
},
|
||||||
|
// 各游戏最高分数
|
||||||
|
highScores: savedData?.highScores || {
|
||||||
|
'planet-explorer': 0,
|
||||||
|
'star-collector': 0,
|
||||||
|
'space-puzzle': 0,
|
||||||
|
'moon-walk': 0,
|
||||||
|
'constellation': 0,
|
||||||
|
'cloud-maze': 0,
|
||||||
|
'bird-flight': 0,
|
||||||
|
'rainbow-paint': 0,
|
||||||
|
'cloud-puzzle': 0,
|
||||||
|
'treasure-hunt': 0,
|
||||||
|
'fish-match': 0,
|
||||||
|
'coral-color': 0,
|
||||||
|
'ocean-puzzle': 0,
|
||||||
|
'mining': 0,
|
||||||
|
'mole-whack': 0,
|
||||||
|
'cave-explore': 0,
|
||||||
|
'animal-friends': 0,
|
||||||
|
'plant-grow': 0,
|
||||||
|
'leaf-collect': 0,
|
||||||
|
'forest-puzzle': 0
|
||||||
|
},
|
||||||
|
// 解锁的场景
|
||||||
|
unlockedScenes: savedData?.unlockedScenes || ['space', 'sky'],
|
||||||
|
// 游戏总游玩时间(分钟)
|
||||||
|
totalPlayTime: savedData?.totalPlayTime || 0,
|
||||||
|
// 首次访问时间
|
||||||
|
firstVisit: savedData?.firstVisit || new Date().toISOString(),
|
||||||
|
// 最后访问时间
|
||||||
|
lastVisit: savedData?.lastVisit || new Date().toISOString()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
// 获取游戏完成次数
|
||||||
|
getGameCompletions: (state) => (gameId: string) => state.gameCompletions[gameId] || 0,
|
||||||
|
|
||||||
|
// 获取游戏最高分
|
||||||
|
getHighScore: (state) => (gameId: string) => state.highScores[gameId] || 0,
|
||||||
|
|
||||||
|
// 获取场景是否解锁
|
||||||
|
isSceneUnlocked: (state) => (scene: string) => state.unlockedScenes.includes(scene),
|
||||||
|
|
||||||
|
// 获取总完成次数
|
||||||
|
getTotalCompletions: (state) => Object.values(state.gameCompletions).reduce((a, b) => a + b, 0),
|
||||||
|
|
||||||
|
// 获取解锁场景数
|
||||||
|
getUnlockedScenesCount: (state) => state.unlockedScenes.length
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 保存状态到 localStorage
|
||||||
|
$persist() {
|
||||||
|
const data = {
|
||||||
|
gameCompletions: this.gameCompletions,
|
||||||
|
highScores: this.highScores,
|
||||||
|
unlockedScenes: this.unlockedScenes,
|
||||||
|
totalPlayTime: this.totalPlayTime,
|
||||||
|
firstVisit: this.firstVisit,
|
||||||
|
lastVisit: this.lastVisit
|
||||||
|
};
|
||||||
|
saveToLocalStorage(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 记录游戏完成
|
||||||
|
recordGameCompletion(gameId: string, score: number) {
|
||||||
|
this.gameCompletions[gameId]++;
|
||||||
|
|
||||||
|
// 更新最高分
|
||||||
|
if (score > (this.highScores[gameId] || 0)) {
|
||||||
|
this.highScores[gameId] = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否解锁新场景
|
||||||
|
this.checkSceneUnlocks();
|
||||||
|
|
||||||
|
// 更新最后访问时间
|
||||||
|
this.lastVisit = new Date().toISOString();
|
||||||
|
|
||||||
|
this.$persist();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查场景解锁条件
|
||||||
|
checkSceneUnlocks() {
|
||||||
|
const totalCompletions = this.getTotalCompletions;
|
||||||
|
|
||||||
|
// 完成太空场景的所有游戏后解锁天空
|
||||||
|
const spaceCompleted = ['planet-explorer', 'star-collector', 'space-puzzle', 'moon-walk', 'constellation']
|
||||||
|
.every(game => this.gameCompletions[game] > 0);
|
||||||
|
if (spaceCompleted && !this.unlockedScenes.includes('sky')) {
|
||||||
|
this.unlockedScenes.push('sky');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成天空场景的所有游戏后解锁海洋
|
||||||
|
const skyCompleted = ['cloud-maze', 'bird-flight', 'rainbow-paint', 'cloud-puzzle']
|
||||||
|
.every(game => this.gameCompletions[game] > 0);
|
||||||
|
if (skyCompleted && !this.unlockedScenes.includes('ocean')) {
|
||||||
|
this.unlockedScenes.push('ocean');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成海洋场景的所有游戏后解锁地下
|
||||||
|
const oceanCompleted = ['treasure-hunt', 'fish-match', 'coral-color', 'ocean-puzzle']
|
||||||
|
.every(game => this.gameCompletions[game] > 0);
|
||||||
|
if (oceanCompleted && !this.unlockedScenes.includes('underground')) {
|
||||||
|
this.unlockedScenes.push('underground');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成地下场景的所有游戏后解锁森林
|
||||||
|
const undergroundCompleted = ['mining', 'mole-whack', 'cave-explore']
|
||||||
|
.every(game => this.gameCompletions[game] > 0);
|
||||||
|
if (undergroundCompleted && !this.unlockedScenes.includes('forest')) {
|
||||||
|
this.unlockedScenes.push('forest');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 增加游玩时间
|
||||||
|
addPlayTime(minutes: number) {
|
||||||
|
this.totalPlayTime += minutes;
|
||||||
|
this.lastVisit = new Date().toISOString();
|
||||||
|
this.$persist();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取游戏进度百分比
|
||||||
|
getGameProgress(gameId: string, targetCompletions: number = 5): number {
|
||||||
|
const completions = this.gameCompletions[gameId] || 0;
|
||||||
|
return Math.min(100, Math.round((completions / targetCompletions) * 100));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取场景完成进度
|
||||||
|
getSceneProgress(scene: string): number {
|
||||||
|
const sceneGames: Record<string, string[]> = {
|
||||||
|
space: ['planet-explorer', 'star-collector', 'space-puzzle', 'moon-walk', 'constellation'],
|
||||||
|
sky: ['cloud-maze', 'bird-flight', 'rainbow-paint', 'cloud-puzzle'],
|
||||||
|
ocean: ['treasure-hunt', 'fish-match', 'coral-color', 'ocean-puzzle'],
|
||||||
|
underground: ['mining', 'mole-whack', 'cave-explore'],
|
||||||
|
forest: ['animal-friends', 'plant-grow', 'leaf-collect', 'forest-puzzle']
|
||||||
|
};
|
||||||
|
|
||||||
|
const games = sceneGames[scene] || [];
|
||||||
|
if (games.length === 0) return 0;
|
||||||
|
|
||||||
|
const completedGames = games.filter(game => this.gameCompletions[game] > 0).length;
|
||||||
|
return Math.round((completedGames / games.length) * 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取总进度百分比
|
||||||
|
getTotalProgress(): number {
|
||||||
|
const totalGames = Object.keys(this.gameCompletions).length;
|
||||||
|
const playedGames = Object.values(this.gameCompletions).filter(count => count > 0).length;
|
||||||
|
return Math.round((playedGames / totalGames) * 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置所有进度
|
||||||
|
resetAll() {
|
||||||
|
this.gameCompletions = {
|
||||||
|
'planet-explorer': 0,
|
||||||
|
'star-collector': 0,
|
||||||
|
'space-puzzle': 0,
|
||||||
|
'moon-walk': 0,
|
||||||
|
'constellation': 0,
|
||||||
|
'cloud-maze': 0,
|
||||||
|
'bird-flight': 0,
|
||||||
|
'rainbow-paint': 0,
|
||||||
|
'cloud-puzzle': 0,
|
||||||
|
'treasure-hunt': 0,
|
||||||
|
'fish-match': 0,
|
||||||
|
'coral-color': 0,
|
||||||
|
'ocean-puzzle': 0,
|
||||||
|
'mining': 0,
|
||||||
|
'mole-whack': 0,
|
||||||
|
'cave-explore': 0,
|
||||||
|
'animal-friends': 0,
|
||||||
|
'plant-grow': 0,
|
||||||
|
'leaf-collect': 0,
|
||||||
|
'forest-puzzle': 0
|
||||||
|
};
|
||||||
|
this.highScores = {
|
||||||
|
'planet-explorer': 0,
|
||||||
|
'star-collector': 0,
|
||||||
|
'space-puzzle': 0,
|
||||||
|
'moon-walk': 0,
|
||||||
|
'constellation': 0,
|
||||||
|
'cloud-maze': 0,
|
||||||
|
'bird-flight': 0,
|
||||||
|
'rainbow-paint': 0,
|
||||||
|
'cloud-puzzle': 0,
|
||||||
|
'treasure-hunt': 0,
|
||||||
|
'fish-match': 0,
|
||||||
|
'coral-color': 0,
|
||||||
|
'ocean-puzzle': 0,
|
||||||
|
'mining': 0,
|
||||||
|
'mole-whack': 0,
|
||||||
|
'cave-explore': 0,
|
||||||
|
'animal-friends': 0,
|
||||||
|
'plant-grow': 0,
|
||||||
|
'leaf-collect': 0,
|
||||||
|
'forest-puzzle': 0
|
||||||
|
};
|
||||||
|
this.unlockedScenes = ['space', 'sky'];
|
||||||
|
this.totalPlayTime = 0;
|
||||||
|
this.firstVisit = new Date().toISOString();
|
||||||
|
this.lastVisit = new Date().toISOString();
|
||||||
|
localStorage.removeItem('adventureProgressData');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
325
src/stores/starEnergy.ts
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
// 获取当前用户
|
||||||
|
const getCurrentUser = () => {
|
||||||
|
return localStorage.getItem('currentUser') || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从 localStorage 加载数据
|
||||||
|
const loadFromLocalStorage = () => {
|
||||||
|
try {
|
||||||
|
const user = getCurrentUser();
|
||||||
|
const saved = localStorage.getItem(`starEnergyData_${user}`);
|
||||||
|
return saved ? JSON.parse(saved) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载数据失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
const saveToLocalStorage = (data: any) => {
|
||||||
|
try {
|
||||||
|
const user = getCurrentUser();
|
||||||
|
localStorage.setItem(`starEnergyData_${user}`, JSON.stringify(data));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 星星能量存储
|
||||||
|
export const useStarEnergyStore = defineStore('starEnergy', {
|
||||||
|
state: () => {
|
||||||
|
// 尝试从 localStorage 加载保存的数据
|
||||||
|
const savedData = loadFromLocalStorage();
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 星星能量总数
|
||||||
|
total: savedData?.total || 20,
|
||||||
|
// 各区域的星星能量
|
||||||
|
areas: savedData?.areas || {
|
||||||
|
knowledge: 0,
|
||||||
|
craft: 0,
|
||||||
|
logic: 0,
|
||||||
|
habit: 0,
|
||||||
|
parent: 0
|
||||||
|
},
|
||||||
|
// 习惯打卡记录
|
||||||
|
habits: savedData?.habits || {
|
||||||
|
brushTeeth: {
|
||||||
|
name: '刷牙',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null as string | null
|
||||||
|
},
|
||||||
|
noBedLate: {
|
||||||
|
name: '不赖床',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null as string | null
|
||||||
|
},
|
||||||
|
noPickyEating: {
|
||||||
|
name: '不挑食',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null as string | null
|
||||||
|
},
|
||||||
|
washHands: {
|
||||||
|
name: '勤洗手',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null as string | null
|
||||||
|
},
|
||||||
|
homeworkFast: {
|
||||||
|
name: '写作业快',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null as string | null
|
||||||
|
},
|
||||||
|
earlySleep: {
|
||||||
|
name: '早睡',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null as string | null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 怪兽状态
|
||||||
|
monsters: {
|
||||||
|
boss: {
|
||||||
|
name: '坏习惯大魔王',
|
||||||
|
health: (savedData?.monsters && 'boss' in savedData.monsters && !savedData.monsters.boss.defeated) ? savedData.monsters.boss.health : 500,
|
||||||
|
defeated: (savedData?.monsters && 'boss' in savedData.monsters) ? savedData.monsters.boss.defeated : false,
|
||||||
|
habits: ['brushTeeth', 'noBedLate', 'noPickyEating', 'washHands', 'homeworkFast', 'earlySleep']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 勋章
|
||||||
|
badges: savedData?.badges || [] as string[],
|
||||||
|
// 已兑换奖励
|
||||||
|
claimedRewards: savedData?.claimedRewards || [] as string[],
|
||||||
|
// 击败BOSS次数
|
||||||
|
bossDefeats: savedData?.bossDefeats || 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
// 获取总星星能量
|
||||||
|
getTotalEnergy: (state) => state.total,
|
||||||
|
|
||||||
|
// 获取各区域能量
|
||||||
|
getAreaEnergy: (state) => (area: keyof typeof state.areas) => state.areas[area],
|
||||||
|
|
||||||
|
// 获取习惯打卡情况
|
||||||
|
getHabitStatus: (state) => (habit: keyof typeof state.habits) => state.habits[habit],
|
||||||
|
|
||||||
|
// 获取怪兽状态
|
||||||
|
getMonsterStatus: (state) => (monster: keyof typeof state.monsters) => state.monsters[monster],
|
||||||
|
|
||||||
|
// 获取所有勋章
|
||||||
|
getAllBadges: (state) => state.badges,
|
||||||
|
|
||||||
|
// 获取已兑换奖励
|
||||||
|
getClaimedRewards: (state) => state.claimedRewards,
|
||||||
|
|
||||||
|
// 获取击败BOSS次数
|
||||||
|
getBossDefeats: (state) => state.bossDefeats
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 保存状态到 localStorage
|
||||||
|
$persist() {
|
||||||
|
const data = {
|
||||||
|
total: this.total,
|
||||||
|
areas: this.areas,
|
||||||
|
habits: this.habits,
|
||||||
|
monsters: this.monsters,
|
||||||
|
badges: this.badges,
|
||||||
|
claimedRewards: this.claimedRewards
|
||||||
|
};
|
||||||
|
saveToLocalStorage(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 增加星星能量
|
||||||
|
addEnergy(amount: number, area: keyof typeof this.areas) {
|
||||||
|
this.total += amount;
|
||||||
|
this.areas[area] += amount;
|
||||||
|
this.$persist();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 减少星星能量
|
||||||
|
removeEnergy(amount: number) {
|
||||||
|
if (this.total >= amount) {
|
||||||
|
this.total -= amount;
|
||||||
|
this.$persist();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打卡习惯
|
||||||
|
checkHabit(habit: keyof typeof this.habits) {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const habitData = this.habits[habit];
|
||||||
|
|
||||||
|
// 防止重复打卡
|
||||||
|
if (habitData.lastChecked === today) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新打卡记录
|
||||||
|
habitData.count++;
|
||||||
|
habitData.lastChecked = today;
|
||||||
|
|
||||||
|
// 更新连续打卡天数
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
if (habitData.lastChecked === yesterdayStr) {
|
||||||
|
habitData.streak++;
|
||||||
|
} else {
|
||||||
|
habitData.streak = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 奖励星星能量
|
||||||
|
this.addEnergy(10, 'habit');
|
||||||
|
|
||||||
|
// 连续3天打卡额外奖励
|
||||||
|
if (habitData.streak % 3 === 0) {
|
||||||
|
this.addEnergy(5, 'habit');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 攻击怪兽
|
||||||
|
attackMonster(monster: keyof typeof this.monsters, energy: number) {
|
||||||
|
const monsterData = this.monsters[monster];
|
||||||
|
if (monsterData.defeated) return;
|
||||||
|
|
||||||
|
// 每次攻击消耗10颗星星,固定掉10滴血
|
||||||
|
if (this.total >= 10) {
|
||||||
|
monsterData.health = Math.max(0, monsterData.health - 10);
|
||||||
|
|
||||||
|
// 扣除星星能量
|
||||||
|
this.total -= 10;
|
||||||
|
|
||||||
|
// 检查是否击败
|
||||||
|
if (monsterData.health === 0) {
|
||||||
|
monsterData.defeated = true;
|
||||||
|
// 奖励星星能量
|
||||||
|
this.addEnergy(200, 'habit');
|
||||||
|
|
||||||
|
// 增加击败BOSS次数
|
||||||
|
this.bossDefeats++;
|
||||||
|
|
||||||
|
// 解锁勋章
|
||||||
|
const badge = '习惯守护者';
|
||||||
|
if (!this.badges.includes(badge)) {
|
||||||
|
this.badges.push(badge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$persist();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 亲子合力攻击
|
||||||
|
parentAttack(monster: keyof typeof this.monsters) {
|
||||||
|
const monsterData = this.monsters[monster];
|
||||||
|
if (monsterData.defeated) return;
|
||||||
|
|
||||||
|
// 亲子攻击固定掉10滴血
|
||||||
|
monsterData.health = Math.max(0, monsterData.health - 10);
|
||||||
|
|
||||||
|
// 检查是否击败
|
||||||
|
if (monsterData.health === 0) {
|
||||||
|
monsterData.defeated = true;
|
||||||
|
// 奖励星星能量
|
||||||
|
this.addEnergy(200, 'habit');
|
||||||
|
|
||||||
|
// 增加击败BOSS次数
|
||||||
|
this.bossDefeats++;
|
||||||
|
|
||||||
|
// 解锁勋章
|
||||||
|
const badge = '习惯守护者';
|
||||||
|
if (!this.badges.includes(badge)) {
|
||||||
|
this.badges.push(badge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$persist();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 兑换奖励
|
||||||
|
redeemReward(rewardId: string, cost: number) {
|
||||||
|
if (this.total < cost) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.total -= cost;
|
||||||
|
if (!this.claimedRewards.includes(rewardId)) {
|
||||||
|
this.claimedRewards.push(rewardId);
|
||||||
|
}
|
||||||
|
this.$persist();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置所有数据(清除游戏进度)
|
||||||
|
resetAll() {
|
||||||
|
this.total = 0;
|
||||||
|
this.areas = {
|
||||||
|
knowledge: 0,
|
||||||
|
craft: 0,
|
||||||
|
logic: 0,
|
||||||
|
habit: 0,
|
||||||
|
parent: 0
|
||||||
|
};
|
||||||
|
this.habits = {
|
||||||
|
brushTeeth: {
|
||||||
|
name: '刷牙',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null
|
||||||
|
},
|
||||||
|
noBedLate: {
|
||||||
|
name: '不赖床',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null
|
||||||
|
},
|
||||||
|
noPickyEating: {
|
||||||
|
name: '不挑食',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null
|
||||||
|
},
|
||||||
|
washHands: {
|
||||||
|
name: '勤洗手',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null
|
||||||
|
},
|
||||||
|
homeworkFast: {
|
||||||
|
name: '写作业快',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null
|
||||||
|
},
|
||||||
|
earlySleep: {
|
||||||
|
name: '早睡',
|
||||||
|
count: 0,
|
||||||
|
streak: 0,
|
||||||
|
lastChecked: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.monsters = {
|
||||||
|
boss: {
|
||||||
|
name: '坏习惯大魔王',
|
||||||
|
health: 500,
|
||||||
|
defeated: false,
|
||||||
|
habits: ['brushTeeth', 'noBedLate', 'noPickyEating', 'washHands', 'homeworkFast', 'earlySleep']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.badges = [];
|
||||||
|
this.claimedRewards = [];
|
||||||
|
this.bossDefeats = 0;
|
||||||
|
const user = getCurrentUser();
|
||||||
|
localStorage.removeItem(`starEnergyData_${user}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
227
src/style.css
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
/* 全局样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局变量 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #FF6B6B;
|
||||||
|
--secondary-color: #4ECDC4;
|
||||||
|
--accent-color: #FFD166;
|
||||||
|
--light-accent: #06D6A0;
|
||||||
|
--text-color: #292F36;
|
||||||
|
--light-text: #6C757D;
|
||||||
|
--white: #ffffff;
|
||||||
|
--light-bg: rgba(255, 255, 255, 0.95);
|
||||||
|
--shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
--hover-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||||
|
--border-radius: 20px;
|
||||||
|
--transition: all 0.3s ease;
|
||||||
|
--cartoon-font: 'Comic Sans MS', 'Arial', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
background-color: #F7FFF7;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 10% 20%, rgba(255, 107, 107, 0.1) 0%, transparent 20%),
|
||||||
|
radial-gradient(circle at 90% 30%, rgba(78, 205, 196, 0.1) 0%, transparent 20%),
|
||||||
|
radial-gradient(circle at 50% 80%, rgba(255, 209, 102, 0.1) 0%, transparent 20%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡通按钮样式 */
|
||||||
|
.cartoon-btn {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||||
|
color: var(--white);
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartoon-btn:hover {
|
||||||
|
transform: translateY(-3px) scale(1.05);
|
||||||
|
box-shadow: var(--hover-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartoon-btn:active {
|
||||||
|
transform: translateY(0) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡通卡片样式 */
|
||||||
|
.cartoon-card {
|
||||||
|
background: var(--light-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: var(--transition);
|
||||||
|
border: 3px solid var(--accent-color);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartoon-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
animation: shine 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% { transform: translateX(-100%) rotate(45deg); }
|
||||||
|
100% { transform: translateX(100%) rotate(45deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartoon-card:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02);
|
||||||
|
box-shadow: var(--hover-shadow);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡通标题样式 */
|
||||||
|
.cartoon-title {
|
||||||
|
font-family: var(--cartoon-font);
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
animation: bounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-5px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡通图标样式 */
|
||||||
|
.cartoon-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
animation: wiggle 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(5deg); }
|
||||||
|
75% { transform: rotate(-5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡通徽章样式 */
|
||||||
|
.cartoon-badge {
|
||||||
|
background: linear-gradient(135deg, var(--accent-color), var(--light-accent));
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cartoon-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartoon-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用按钮样式 */
|
||||||
|
.btn {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||||
|
color: var(--white);
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: var(--hover-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.card {
|
||||||
|
background: var(--light-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: var(--hover-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.6s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.map-areas {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
tsconfig.app.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
18
vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
})
|
||||||