new version

This commit is contained in:
2026-03-02 00:17:33 +08:00
commit 0c220d3fe2
53 changed files with 20256 additions and 0 deletions

41
.gitignore vendored Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View 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
View File

0
public/craft.png Normal file
View File

0
public/find_diff_A.png Normal file
View File

0
public/find_diff_B.png Normal file
View File

0
public/habit.png Normal file
View File

0
public/home.png Normal file
View File

0
public/knowledge.png Normal file
View File

0
public/logic.png Normal file
View File

0
public/parent.png Normal file
View File

1
public/planet.png Normal file

File diff suppressed because one or more lines are too long

1
public/star.png Normal file

File diff suppressed because one or more lines are too long

BIN
public/五人一起.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

BIN
public/哪吒.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

BIN
public/唐僧.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

BIN
public/孙悟空.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

BIN
public/成长星球1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

BIN
public/沙僧.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

BIN
public/猪八戒.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

1922
src/App.vue Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

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

View 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
View 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')

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View 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
View 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,
},
})