new version
This commit is contained in:
1176
src/components/AdventureArea.vue
Normal file
1176
src/components/AdventureArea.vue
Normal file
File diff suppressed because it is too large
Load Diff
4662
src/components/AdventureMap.vue
Normal file
4662
src/components/AdventureMap.vue
Normal file
File diff suppressed because it is too large
Load Diff
692
src/components/CalendarArea.vue
Normal file
692
src/components/CalendarArea.vue
Normal file
@@ -0,0 +1,692 @@
|
||||
<template>
|
||||
<div class="calendar-area">
|
||||
<div class="calendar-header">
|
||||
<h2 class="calendar-title">📅 我的成长日历</h2>
|
||||
<p class="calendar-subtitle">每一天都是成长的好日子!✨</p>
|
||||
</div>
|
||||
|
||||
<!-- 日历控制 -->
|
||||
<div class="calendar-controls">
|
||||
<button class="control-btn" @click="previousMonth">
|
||||
<span class="btn-arrow">◀</span>
|
||||
</button>
|
||||
<div class="current-month">
|
||||
<span class="month-emoji">🌸</span>
|
||||
<span class="month-text">{{ currentYear }}年 {{ currentMonth + 1 }}月</span>
|
||||
<span class="month-emoji">🌸</span>
|
||||
</div>
|
||||
<button class="control-btn" @click="nextMonth">
|
||||
<span class="btn-arrow">▶</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 日历表格 -->
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-weekdays">
|
||||
<div class="weekday" v-for="day in weekdays" :key="day">
|
||||
<span class="weekday-emoji">{{ getWeekdayEmoji(day) }}</span>
|
||||
<span class="weekday-text">{{ day }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-days">
|
||||
<div
|
||||
v-for="(day, index) in calendarDays"
|
||||
:key="index"
|
||||
class="calendar-day"
|
||||
:class="{
|
||||
'today': isToday(day),
|
||||
'checked': isChecked(day),
|
||||
'future': isFuture(day)
|
||||
}"
|
||||
>
|
||||
<div class="day-content">
|
||||
<div class="day-number">{{ day || '' }}</div>
|
||||
<div class="day-emoji" v-if="day && isChecked(day)">
|
||||
{{ getCheckEmoji(day) }}
|
||||
</div>
|
||||
<div class="day-stats" v-if="day && getDayStats(day).count > 0">
|
||||
<div class="stats-badge">
|
||||
<span class="stars">⭐{{ getDayStats(day).stars }}</span>
|
||||
<span class="habits">{{ getDayStats(day).count }}个习惯</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 月度统计 -->
|
||||
<div class="month-stats">
|
||||
<h3 class="stats-title">📊 本月统计</h3>
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🎯</div>
|
||||
<div class="stat-value">{{ monthlyStats.totalDays }}</div>
|
||||
<div class="stat-label">打卡天数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⭐</div>
|
||||
<div class="stat-value">{{ monthlyStats.totalStars }}</div>
|
||||
<div class="stat-label">获得星星</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🔥</div>
|
||||
<div class="stat-value">{{ monthlyStats.longestStreak }}</div>
|
||||
<div class="stat-label">最长连续</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">💪</div>
|
||||
<div class="stat-value">{{ monthlyStats.totalHabits }}</div>
|
||||
<div class="stat-label">习惯次数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 习惯完成度 -->
|
||||
<div class="habit-progress">
|
||||
<h3 class="progress-title">📈 习惯完成度</h3>
|
||||
<div class="progress-items">
|
||||
<div v-for="(habit, key) in habits" :key="key" class="progress-item">
|
||||
<div class="progress-header">
|
||||
<span class="habit-icon">{{ habit.icon }}</span>
|
||||
<span class="habit-name">{{ habit.name }}</span>
|
||||
<span class="habit-count">{{ habitData[key as keyof typeof habitData]?.count || 0 }}次</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${getProgress(habitData[key as keyof typeof habitData]?.count || 0)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||
|
||||
const starEnergyStore = useStarEnergyStore();
|
||||
|
||||
// 当前日期
|
||||
const currentDate = new Date();
|
||||
const currentYear = ref(currentDate.getFullYear());
|
||||
const currentMonth = ref(currentDate.getMonth());
|
||||
|
||||
// 星期标题
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
|
||||
// 习惯数据
|
||||
const habits = {
|
||||
brushTeeth: { name: '刷牙', icon: '🦷' },
|
||||
noBedLate: { name: '不赖床', icon: '⏰' },
|
||||
noPickyEating: { name: '不挑食', icon: '🥦' },
|
||||
washHands: { name: '勤洗手', icon: '🧼' },
|
||||
homeworkFast: { name: '写作业快', icon: '📝' },
|
||||
earlySleep: { name: '早睡', icon: '🌙' }
|
||||
};
|
||||
|
||||
// 从store获取习惯数据
|
||||
const habitData = computed(() => starEnergyStore.habits);
|
||||
|
||||
// 获取日历天数
|
||||
const calendarDays = computed(() => {
|
||||
const days = [];
|
||||
const firstDay = new Date(currentYear.value, currentMonth.value, 1);
|
||||
const lastDay = new Date(currentYear.value, currentMonth.value + 1, 0);
|
||||
const startDay = firstDay.getDay();
|
||||
const totalDays = lastDay.getDate();
|
||||
|
||||
// 填充空白
|
||||
for (let i = 0; i < startDay; i++) {
|
||||
days.push(null);
|
||||
}
|
||||
|
||||
// 填充日期
|
||||
for (let i = 1; i <= totalDays; i++) {
|
||||
days.push(i);
|
||||
}
|
||||
|
||||
return days;
|
||||
});
|
||||
|
||||
// 获取月份统计
|
||||
const monthlyStats = computed(() => {
|
||||
const stats = {
|
||||
totalDays: 0,
|
||||
totalStars: 0,
|
||||
longestStreak: 0,
|
||||
totalHabits: 0
|
||||
};
|
||||
|
||||
const habits = starEnergyStore.habits;
|
||||
Object.values(habits).forEach(habit => {
|
||||
stats.totalHabits += habit.count;
|
||||
if (habit.streak > stats.longestStreak) {
|
||||
stats.longestStreak = habit.streak;
|
||||
}
|
||||
});
|
||||
|
||||
// 计算打卡天数和星星数
|
||||
const year = currentYear.value;
|
||||
const month = currentMonth.value + 1;
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
let hasCheckIn = false;
|
||||
|
||||
Object.values(habits).forEach(habit => {
|
||||
if (habit.lastChecked === dateStr) {
|
||||
hasCheckIn = true;
|
||||
stats.totalStars += 10; // 每次打卡10星
|
||||
if (habit.streak % 3 === 0) {
|
||||
stats.totalStars += 5; // 连续3天额外5星
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasCheckIn) {
|
||||
stats.totalDays++;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
});
|
||||
|
||||
// 判断是否是今天
|
||||
const isToday = (day: number | null) => {
|
||||
if (!day) return false;
|
||||
const today = new Date();
|
||||
return (
|
||||
day === today.getDate() &&
|
||||
currentMonth.value === today.getMonth() &&
|
||||
currentYear.value === today.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
// 判断是否是未来日期
|
||||
const isFuture = (day: number | null) => {
|
||||
if (!day) return false;
|
||||
const today = new Date();
|
||||
const date = new Date(currentYear.value, currentMonth.value, day);
|
||||
return date > today;
|
||||
};
|
||||
|
||||
// 判断是否已打卡
|
||||
const isChecked = (day: number | null) => {
|
||||
if (!day) return false;
|
||||
const year = currentYear.value;
|
||||
const month = currentMonth.value + 1;
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
|
||||
return Object.values(starEnergyStore.habits).some(
|
||||
habit => habit.lastChecked === dateStr
|
||||
);
|
||||
};
|
||||
|
||||
// 获取打卡表情
|
||||
const getCheckEmoji = (day: number | null) => {
|
||||
if (!day) return '';
|
||||
const stats = getDayStats(day);
|
||||
if (stats.count >= 6) return '🎉';
|
||||
if (stats.count >= 4) return '🌟';
|
||||
if (stats.count >= 2) return '😊';
|
||||
return '✅';
|
||||
};
|
||||
|
||||
// 获取每日统计
|
||||
const getDayStats = (day: number | null) => {
|
||||
if (!day) return { count: 0, stars: 0 };
|
||||
|
||||
const year = currentYear.value;
|
||||
const month = currentMonth.value + 1;
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
|
||||
let count = 0;
|
||||
Object.values(starEnergyStore.habits).forEach(habit => {
|
||||
if (habit.lastChecked === dateStr) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
count,
|
||||
stars: count * 10
|
||||
};
|
||||
};
|
||||
|
||||
// 获取星期表情
|
||||
const getWeekdayEmoji = (day: string) => {
|
||||
const emojis = ['😴', '😊', '😄', '😃', '😁', '🎉', '🥳'];
|
||||
return emojis[weekdays.indexOf(day)];
|
||||
};
|
||||
|
||||
// 获取进度百分比
|
||||
const getProgress = (count: number) => {
|
||||
const max = 100;
|
||||
return Math.min((count / max) * 100, 100);
|
||||
};
|
||||
|
||||
// 上个月
|
||||
const previousMonth = () => {
|
||||
if (currentMonth.value === 0) {
|
||||
currentMonth.value = 11;
|
||||
currentYear.value--;
|
||||
} else {
|
||||
currentMonth.value--;
|
||||
}
|
||||
};
|
||||
|
||||
// 下个月
|
||||
const nextMonth = () => {
|
||||
if (currentMonth.value === 11) {
|
||||
currentMonth.value = 0;
|
||||
currentYear.value++;
|
||||
} else {
|
||||
currentMonth.value++;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calendar-area {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.calendar-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #FF69B4;
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.calendar-subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
/* 日历控制 */
|
||||
.calendar-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||
color: white;
|
||||
border: none;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 10px rgba(255, 182, 193, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 15px rgba(255, 182, 193, 0.6);
|
||||
}
|
||||
|
||||
.btn-arrow {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.current-month {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 15px 25px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
border: 3px solid #FFB6C1;
|
||||
}
|
||||
|
||||
.month-emoji {
|
||||
font-size: 24px;
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.month-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
/* 日历表格 */
|
||||
.calendar-grid {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 25px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||
border: 4px solid #FFB6C1;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.calendar-weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 10px;
|
||||
background: linear-gradient(135deg, #E6E6FA, #D8BFD8);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.weekday-emoji {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.weekday-text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.calendar-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.calendar-day:hover:not(.future) {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.calendar-day.today {
|
||||
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||
border-color: #FF69B4;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 20px rgba(255, 105, 180, 0.4);
|
||||
}
|
||||
|
||||
.calendar-day.checked {
|
||||
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||
border-color: #32CD32;
|
||||
}
|
||||
|
||||
.calendar-day.future {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.day-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.day-emoji {
|
||||
font-size: 20px;
|
||||
animation: bounce 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.day-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stats-badge {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stars {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.habits {
|
||||
color: #32CD32;
|
||||
}
|
||||
|
||||
/* 月度统计 */
|
||||
.month-stats {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 25px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||
border: 4px solid #87CEEB;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #4169E1;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #E6E6FA, #D8BFD8);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 3px solid #DDA0DD;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
/* 习惯完成度 */
|
||||
.habit-progress {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 25px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||
border: 4px solid #FFD700;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #FF8C00;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.progress-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
background: #f9f9f9;
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
border: 2px solid #FFE4B5;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.habit-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.habit-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.habit-count {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #FF8C00;
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #98FB98, #90EE90, #32CD32);
|
||||
border-radius: 6px;
|
||||
transition: width 0.5s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(5deg); }
|
||||
75% { transform: rotate(-5deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.calendar-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.weekday-emoji {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.weekday-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.day-emoji {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
579
src/components/CraftArea.vue
Normal file
579
src/components/CraftArea.vue
Normal file
@@ -0,0 +1,579 @@
|
||||
<template>
|
||||
<div class="craft-area">
|
||||
<div class="area-header">
|
||||
<div class="header-with-audio">
|
||||
<h2 class="area-title">🎨 手工工坊 🎨</h2>
|
||||
<button class="audio-btn" @click="playAudio('欢迎来到手工工坊!动动手,变厉害!')">🔊</button>
|
||||
</div>
|
||||
<p class="area-desc">动动手,变厉害!</p>
|
||||
</div>
|
||||
|
||||
<div class="craft-types">
|
||||
<!-- DIY小达人 -->
|
||||
<div class="craft-card" @click="selectCraft('diy')">
|
||||
<div class="craft-icon diy-icon">🎨</div>
|
||||
<h3 class="craft-title">DIY小达人</h3>
|
||||
<p class="craft-desc">跟着 "手工兔" 学做手工</p>
|
||||
<button class="card-audio-btn" @click.stop="playAudio('跟着手工兔,学做卡纸花朵、瓶盖拼图、纸盘手偶等有趣的手工!')">🔊</button>
|
||||
</div>
|
||||
|
||||
<!-- 家务小帮手 -->
|
||||
<div class="craft-card" @click="selectCraft('chore')">
|
||||
<div class="craft-icon chore-icon">🧹</div>
|
||||
<h3 class="craft-title">家务小帮手</h3>
|
||||
<p class="craft-desc">帮星球清理 "杂物角"</p>
|
||||
<button class="card-audio-btn" @click.stop="playAudio('帮家人做家务,比如叠袜子、摆碗筷、整理书包,成为家务小能手!')">🔊</button>
|
||||
</div>
|
||||
|
||||
<!-- 自然探索家 -->
|
||||
<div class="craft-card" @click="selectCraft('nature')">
|
||||
<div class="craft-icon nature-icon">🔍</div>
|
||||
<h3 class="craft-title">自然探索家</h3>
|
||||
<p class="craft-desc">带着 APP 去户外探索</p>
|
||||
<button class="card-audio-btn" @click.stop="playAudio('去户外探索大自然吧!观察蚂蚁搬家、树叶脉络,用画笔记录花朵的样子!')">🔊</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手工内容 -->
|
||||
<div class="craft-content" v-if="currentCraft">
|
||||
<div class="content-header">
|
||||
<h3 class="content-title">{{ craftTitles[currentCraft] }}</h3>
|
||||
<button class="back-btn" @click="currentCraft = null">返回</button>
|
||||
</div>
|
||||
|
||||
<div class="tasks">
|
||||
<div v-for="(task, index) in tasks[currentCraft]" :key="index" class="task-card">
|
||||
<div class="task-icon">{{ task.icon }}</div>
|
||||
<div class="task-info">
|
||||
<h4 class="task-title">{{ task.title }}</h4>
|
||||
<p class="task-desc">{{ task.description }}</p>
|
||||
<div class="task-reward">
|
||||
<span class="reward-icon">⭐</span>
|
||||
<span>{{ task.reward }} 颗星星能量</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
<button class="task-audio-btn" @click="playTaskAudio(task)">🔊</button>
|
||||
<button class="task-btn" @click="completeTask(currentCraft, index)">
|
||||
{{ task.completed ? '已完成' : '开始' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片上传区域 -->
|
||||
<div v-if="currentCraft === 'diy' || currentCraft === 'nature'" class="upload-section">
|
||||
<h4>完成后上传照片</h4>
|
||||
<input type="file" accept="image/*" @change="handleFileUpload" class="file-input">
|
||||
<div class="preview" v-if="imagePreview">
|
||||
<img :src="imagePreview" alt="预览" class="preview-image">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||
|
||||
const starEnergyStore = useStarEnergyStore();
|
||||
const currentCraft = ref<string | null>(null);
|
||||
const imagePreview = ref<string | null>(null);
|
||||
|
||||
// 初始化时加载语音合成API的voices
|
||||
onMounted(() => {
|
||||
// 触发语音合成API的加载
|
||||
window.speechSynthesis.getVoices();
|
||||
});
|
||||
|
||||
// 播放音频 - 库洛米声音风格
|
||||
const playAudio = (text: string) => {
|
||||
try {
|
||||
// 使用Web Speech API进行文本转语音
|
||||
const speech = new SpeechSynthesisUtterance(text);
|
||||
// 设置语言
|
||||
speech.lang = 'zh-CN';
|
||||
// 设置音量、语速和语调 - 库洛米风格(可爱、活泼、略带淘气)
|
||||
speech.volume = 1; // 音量
|
||||
speech.rate = 1.1; // 语速稍快,更有活力
|
||||
speech.pitch = 1.7; // 更高的语调,更可爱
|
||||
|
||||
// 尝试选择更适合儿童的语音
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
// 优先选择女性或儿童语音
|
||||
let selectedVoice = voices.find(voice =>
|
||||
voice.lang === 'zh-CN' &&
|
||||
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女'))
|
||||
) || voices.find(voice => voice.lang === 'zh-CN');
|
||||
|
||||
if (selectedVoice) {
|
||||
speech.voice = selectedVoice;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放
|
||||
window.speechSynthesis.speak(speech);
|
||||
} catch (error) {
|
||||
console.error('音频播放失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 播放任务说明
|
||||
const playTaskAudio = (task: any) => {
|
||||
const audioTexts: Record<string, string> = {
|
||||
'卡纸花朵': '用卡纸制作花朵,先剪出花瓣形状,然后粘贴在花心周围,可以做出五颜六色的花朵哦!',
|
||||
'瓶盖拼图': '收集不同颜色的瓶盖,发挥想象力,拼出各种有趣的图案,比如动物、房子或者汽车!',
|
||||
'纸盘手偶': '用纸盘做底,画上可爱的表情,可以做成小猫、小狗或者小兔子,然后给它们编个小故事吧!',
|
||||
'叠袜子': '学习把袜子配对,然后把它们整齐地叠好,这样可以帮爸爸妈妈节省时间哦!',
|
||||
'摆碗筷': '在吃饭前帮忙摆放碗筷,要确保每个人的碗筷都齐全,摆放整齐,你真是个好帮手!',
|
||||
'整理书包': '每天晚上整理书包,把第二天需要的课本和文具都准备好,这样早上就不会手忙脚乱啦!',
|
||||
'蚂蚁搬家': '仔细观察蚂蚁是怎么搬食物的,它们会排队,还会互相帮助,拍下照片记录下来吧!',
|
||||
'树叶脉络': '收集不同形状的树叶,观察它们的脉络,可以用拓印的方式记录下来,画出美丽的图案!',
|
||||
'花朵写生': '在户外找到漂亮的花朵,用画笔仔细观察,画出它的颜色和形状,成为小画家吧!'
|
||||
};
|
||||
|
||||
playAudio(audioTexts[task.title] || task.description);
|
||||
};
|
||||
|
||||
// 手工类型标题
|
||||
const craftTitles = {
|
||||
diy: 'DIY小达人',
|
||||
chore: '家务小帮手',
|
||||
nature: '自然探索家'
|
||||
};
|
||||
|
||||
// 任务数据
|
||||
const tasks = ref({
|
||||
diy: [
|
||||
{
|
||||
title: '卡纸花朵',
|
||||
description: '跟着 "手工兔" 学做卡纸花朵,步骤有图片 + 语音讲解',
|
||||
reward: 8,
|
||||
completed: false,
|
||||
icon: '🌸'
|
||||
},
|
||||
{
|
||||
title: '瓶盖拼图',
|
||||
description: '用瓶盖制作创意拼图,发挥想象力',
|
||||
reward: 8,
|
||||
completed: false,
|
||||
icon: '🧩'
|
||||
},
|
||||
{
|
||||
title: '纸盘手偶',
|
||||
description: '用纸盘制作可爱的手偶,自己设计表情',
|
||||
reward: 8,
|
||||
completed: false,
|
||||
icon: '🤹'
|
||||
}
|
||||
],
|
||||
chore: [
|
||||
{
|
||||
title: '叠袜子',
|
||||
description: '3-4 岁:学习叠袜子,整齐摆放',
|
||||
reward: 10,
|
||||
completed: false,
|
||||
icon: '🧦'
|
||||
},
|
||||
{
|
||||
title: '摆碗筷',
|
||||
description: '5-6 岁:帮助摆放碗筷,准备吃饭',
|
||||
reward: 10,
|
||||
completed: false,
|
||||
icon: '🍽️'
|
||||
},
|
||||
{
|
||||
title: '整理书包',
|
||||
description: '7-8 岁:自己整理书包,准备第二天的学习用品',
|
||||
reward: 10,
|
||||
completed: false,
|
||||
icon: '🎒'
|
||||
}
|
||||
],
|
||||
nature: [
|
||||
{
|
||||
title: '蚂蚁搬家',
|
||||
description: '拍蚂蚁搬家的照片,观察它们的行为',
|
||||
reward: 8,
|
||||
completed: false,
|
||||
icon: '🐜'
|
||||
},
|
||||
{
|
||||
title: '树叶脉络',
|
||||
description: '收集不同形状的树叶,观察它们的脉络',
|
||||
reward: 8,
|
||||
completed: false,
|
||||
icon: '🍃'
|
||||
},
|
||||
{
|
||||
title: '花朵写生',
|
||||
description: '在户外观察花朵,用画笔记录它们的样子',
|
||||
reward: 8,
|
||||
completed: false,
|
||||
icon: '🖌️'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 选择手工类型
|
||||
const selectCraft = (craft: string) => {
|
||||
currentCraft.value = craft;
|
||||
};
|
||||
|
||||
// 完成任务
|
||||
const completeTask = (craft: string, index: number) => {
|
||||
const task = tasks.value[craft as keyof typeof tasks.value][index];
|
||||
if (!task.completed) {
|
||||
task.completed = true;
|
||||
// 奖励星星能量
|
||||
starEnergyStore.addEnergy(task.reward, 'craft');
|
||||
// 播放完成语音
|
||||
playAudio(`太棒了!完成了${task.title}!获得了${task.reward}颗星星能量!`);
|
||||
// 显示完成动画或提示
|
||||
alert(`任务完成!获得 ${task.reward} 颗星星能量!`);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
imagePreview.value = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
// 这里可以添加上传到服务器的逻辑
|
||||
playAudio('哇!你的作品颜色搭配超棒,创意满分!你真是个小艺术家!');
|
||||
alert('照片上传成功!星宝说:"哇!颜色搭配超棒,创意满分💯"');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.craft-area {
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.area-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header-with-audio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.area-title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #FF69B4;
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.area-desc {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.craft-types {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.craft-card {
|
||||
background: #f9f9f9;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.craft-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.craft-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 0 auto 15px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 35px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.diy-icon {
|
||||
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||
}
|
||||
|
||||
.chore-icon {
|
||||
background: linear-gradient(135deg, #87CEEB, #B0E0E6);
|
||||
}
|
||||
|
||||
.nature-icon {
|
||||
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||
}
|
||||
|
||||
.craft-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.craft-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.card-audio-btn {
|
||||
background: linear-gradient(135deg, #FFD166, #06D6A0);
|
||||
border: none;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-audio-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.craft-content {
|
||||
background: #f9f9f9;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.85));
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
border: 3px solid #FFB6C1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transform: rotate(45deg);
|
||||
animation: shine 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { transform: translateX(-100%) rotate(45deg); }
|
||||
100% { transform: translateX(100%) rotate(45deg); }
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
font-size: 40px;
|
||||
margin-right: 20px;
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.task-reward {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #ff9800;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reward-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-audio-btn {
|
||||
background: linear-gradient(135deg, #FFD166, #06D6A0);
|
||||
border: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.task-audio-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.task-btn {
|
||||
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||
color: #006400;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.task-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
background: linear-gradient(135deg, #90EE90, #32CD32);
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(5deg); }
|
||||
75% { transform: rotate(-5deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.upload-section h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
1310
src/components/HabitArea.vue
Normal file
1310
src/components/HabitArea.vue
Normal file
File diff suppressed because it is too large
Load Diff
3604
src/components/KnowledgeArea.vue
Normal file
3604
src/components/KnowledgeArea.vue
Normal file
File diff suppressed because it is too large
Load Diff
574
src/components/LogicArea.vue
Normal file
574
src/components/LogicArea.vue
Normal file
@@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<div class="logic-area">
|
||||
<div class="area-header">
|
||||
<h2 class="area-title">逻辑迷宫</h2>
|
||||
<p class="area-desc">练思维,变聪明!</p>
|
||||
</div>
|
||||
|
||||
<div class="logic-games">
|
||||
<!-- 迷宫大冒险 -->
|
||||
<div class="game-card" @click="selectGame('maze')">
|
||||
<div class="game-icon maze-icon"></div>
|
||||
<h3 class="game-title">迷宫大冒险</h3>
|
||||
<p class="game-desc">按线索走迷宫,挑战不同难度</p>
|
||||
</div>
|
||||
|
||||
<!-- 找不同挑战 -->
|
||||
<div class="game-card" @click="selectGame('findDiff')">
|
||||
<div class="game-icon findDiff-icon"></div>
|
||||
<h3 class="game-title">找不同挑战</h3>
|
||||
<p class="game-desc">对比两张图片,找出不一样的地方</p>
|
||||
</div>
|
||||
|
||||
<!-- 涂鸦变变变 -->
|
||||
<div class="game-card" @click="selectGame('doodle')">
|
||||
<div class="game-icon doodle-icon"></div>
|
||||
<h3 class="game-title">涂鸦变变变</h3>
|
||||
<p class="game-desc">画出创意,星宝帮你变成动画</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏内容 -->
|
||||
<div class="game-content" v-if="currentGame">
|
||||
<div class="content-header">
|
||||
<h3 class="content-title">{{ gameTitles[currentGame] }}</h3>
|
||||
<button class="back-btn" @click="currentGame = null">返回</button>
|
||||
</div>
|
||||
|
||||
<!-- 迷宫大冒险 -->
|
||||
<div v-if="currentGame === 'maze'" class="maze-game">
|
||||
<div class="difficulty-select">
|
||||
<h4>选择难度</h4>
|
||||
<div class="difficulty-buttons">
|
||||
<button
|
||||
v-for="level in difficultyLevels"
|
||||
:key="level.level"
|
||||
class="difficulty-btn"
|
||||
:class="{ active: selectedDifficulty === level.level }"
|
||||
@click="selectedDifficulty = level.level"
|
||||
>
|
||||
{{ level.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maze-container">
|
||||
<div class="maze-placeholder">
|
||||
<p>迷宫游戏区域</p>
|
||||
<p>难度:{{ difficultyLevels.find(l => l.level === selectedDifficulty)?.name }}</p>
|
||||
<button class="start-maze-btn" @click="completeMaze">开始挑战</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 找不同挑战 -->
|
||||
<div v-if="currentGame === 'findDiff'" class="findDiff-game">
|
||||
<div class="image-pair">
|
||||
<div class="image-container">
|
||||
<h5>图片 A</h5>
|
||||
<div class="game-image-placeholder game-image-a">
|
||||
<span>🦁</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-container">
|
||||
<h5>图片 B</h5>
|
||||
<div class="game-image-placeholder game-image-b">
|
||||
<span>🐯</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diff-finder">
|
||||
<h4>找出 3 个不同之处</h4>
|
||||
<div class="diff-inputs">
|
||||
<input type="text" v-model="diffAnswers[0]" placeholder="第一个不同..." class="diff-input">
|
||||
<input type="text" v-model="diffAnswers[1]" placeholder="第二个不同..." class="diff-input">
|
||||
<input type="text" v-model="diffAnswers[2]" placeholder="第三个不同..." class="diff-input">
|
||||
</div>
|
||||
<button class="check-btn" @click="checkDiff">检查答案</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 涂鸦变变变 -->
|
||||
<div v-if="currentGame === 'doodle'" class="doodle-game">
|
||||
<div class="doodle-area">
|
||||
<h4>画出你的创意</h4>
|
||||
<div class="canvas-container">
|
||||
<canvas ref="doodleCanvas" width="300" height="300" class="doodle-canvas"></canvas>
|
||||
</div>
|
||||
<div class="doodle-controls">
|
||||
<button class="control-btn" @click="clearCanvas">清除</button>
|
||||
<button class="control-btn" @click="completeDoodle">完成</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="story-creator">
|
||||
<h4>故事创编机</h4>
|
||||
<p>用 "兔子、雨伞、森林" 编一个故事</p>
|
||||
<textarea v-model="storyText" placeholder="开始编写你的故事..." class="story-textarea"></textarea>
|
||||
<button class="create-story-btn" @click="createStory">生成动画</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||
|
||||
const starEnergyStore = useStarEnergyStore();
|
||||
const currentGame = ref<string | null>(null);
|
||||
const selectedDifficulty = ref(1);
|
||||
const diffAnswers = ref(['', '', '']);
|
||||
const storyText = ref('');
|
||||
const doodleCanvas = ref<HTMLCanvasElement | null>(null);
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let isDrawing = ref(false);
|
||||
|
||||
// 游戏标题
|
||||
const gameTitles = {
|
||||
maze: '迷宫大冒险',
|
||||
findDiff: '找不同挑战',
|
||||
doodle: '涂鸦变变变'
|
||||
};
|
||||
|
||||
// 难度级别
|
||||
const difficultyLevels = [
|
||||
{ level: 1, name: '简单(3岁)' },
|
||||
{ level: 2, name: '中等(5岁)' },
|
||||
{ level: 3, name: '复杂(8岁)' }
|
||||
];
|
||||
|
||||
// 选择游戏
|
||||
const selectGame = (game: string) => {
|
||||
currentGame.value = game;
|
||||
};
|
||||
|
||||
// 完成迷宫
|
||||
const completeMaze = () => {
|
||||
starEnergyStore.addEnergy(5, 'logic');
|
||||
alert('迷宫挑战成功!获得 5 颗星星能量!');
|
||||
};
|
||||
|
||||
// 检查找不同答案
|
||||
const checkDiff = () => {
|
||||
// 这里可以添加答案检查逻辑
|
||||
starEnergyStore.addEnergy(5, 'logic');
|
||||
alert('找不同挑战成功!获得 5 颗星星能量!');
|
||||
};
|
||||
|
||||
// 清除画布
|
||||
const clearCanvas = () => {
|
||||
if (ctx && doodleCanvas.value) {
|
||||
ctx.clearRect(0, 0, doodleCanvas.value.width, doodleCanvas.value.height);
|
||||
}
|
||||
};
|
||||
|
||||
// 完成涂鸦
|
||||
const completeDoodle = () => {
|
||||
starEnergyStore.addEnergy(8, 'logic');
|
||||
alert('涂鸦完成!星宝把你的画变成了动画!获得 8 颗星星能量!');
|
||||
};
|
||||
|
||||
// 创建故事
|
||||
const createStory = () => {
|
||||
if (storyText.value) {
|
||||
starEnergyStore.addEnergy(8, 'logic');
|
||||
alert('故事创编成功!星宝把你的故事做成了动画!获得 8 颗星星能量!');
|
||||
} else {
|
||||
alert('请先编写故事内容!');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化画布
|
||||
onMounted(() => {
|
||||
if (doodleCanvas.value) {
|
||||
ctx = doodleCanvas.value.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.strokeStyle = '#000';
|
||||
|
||||
// 画布事件监听
|
||||
doodleCanvas.value.addEventListener('mousedown', (e) => {
|
||||
isDrawing.value = true;
|
||||
const rect = doodleCanvas.value?.getBoundingClientRect();
|
||||
if (rect && ctx) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||
}
|
||||
});
|
||||
|
||||
doodleCanvas.value.addEventListener('mousemove', (e) => {
|
||||
if (isDrawing.value && ctx && doodleCanvas.value) {
|
||||
const rect = doodleCanvas.value.getBoundingClientRect();
|
||||
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
doodleCanvas.value.addEventListener('mouseup', () => {
|
||||
isDrawing.value = false;
|
||||
});
|
||||
|
||||
doodleCanvas.value.addEventListener('mouseout', () => {
|
||||
isDrawing.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logic-area {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.area-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.area-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.area-desc {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logic-games {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.game-card {
|
||||
background: #f9f9f9;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.game-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.game-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 15px;
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.maze-icon {
|
||||
background-image: url('/maze.png');
|
||||
}
|
||||
|
||||
.findDiff-icon {
|
||||
background-image: url('/find_diff.png');
|
||||
}
|
||||
|
||||
.doodle-icon {
|
||||
background-image: url('/doodle.png');
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.game-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.game-content {
|
||||
background: #f9f9f9;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
|
||||
/* 迷宫游戏样式 */
|
||||
.maze-game {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.difficulty-select h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.difficulty-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.difficulty-btn {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.difficulty-btn.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.maze-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.maze-placeholder {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.start-maze-btn {
|
||||
margin-top: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 找不同游戏样式 */
|
||||
.findDiff-game {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.image-pair {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-container h5 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.game-image {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.game-image-placeholder {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 64px;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
animation: imagePulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes imagePulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.game-image-placeholder.game-image-a {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.game-image-placeholder.game-image-b {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
}
|
||||
|
||||
.diff-finder h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.diff-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.diff-input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 涂鸦游戏样式 */
|
||||
.doodle-game {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.doodle-area h4,
|
||||
.story-creator h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.doodle-canvas {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.doodle-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.story-creator p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.story-textarea {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.create-story-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
align-self: flex-start;
|
||||
}
|
||||
</style>
|
||||
790
src/components/ParentArea.vue
Normal file
790
src/components/ParentArea.vue
Normal file
@@ -0,0 +1,790 @@
|
||||
<template>
|
||||
<div class="parent-area">
|
||||
<div class="area-header">
|
||||
<h2 class="area-title">亲子守护站</h2>
|
||||
<p class="area-desc">和爸爸妈妈一起冒险!</p>
|
||||
</div>
|
||||
|
||||
<!-- 亲子评价 -->
|
||||
<div class="parent-feedback-section">
|
||||
<h3 class="section-title">亲子评价</h3>
|
||||
<div class="feedback-card">
|
||||
<div class="feedback-icon">💬</div>
|
||||
<div class="feedback-info">
|
||||
<h4 class="feedback-title">基于表现的自动表扬</h4>
|
||||
<p class="feedback-desc">系统会根据孩子的能量值和习惯打卡情况,自动生成表扬的话,点击朗读按钮读给孩子听!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feedback-form">
|
||||
<div class="form-group">
|
||||
<label>孩子当前表现</label>
|
||||
<div class="performance-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总能量值:</span>
|
||||
<span class="stat-value">{{ totalStars }} 颗星星</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">习惯打卡:</span>
|
||||
<span class="stat-value">{{ totalCheckins }} 次</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">击败怪兽:</span>
|
||||
<span class="stat-value">{{ defeatedMonstersCount }} 只</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>自动生成的表扬语</label>
|
||||
<div class="generated-feedback">
|
||||
<p>{{ generatedPraise }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="generate-feedback-btn" @click="generatePraise">重新生成</button>
|
||||
<button class="read-feedback-btn" @click="readFeedback" :disabled="!generatedPraise">读给孩子听</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="savedFeedbacks.length > 0" class="saved-feedbacks">
|
||||
<h4>历史表扬</h4>
|
||||
<div v-for="(feedback, index) in savedFeedbacks" :key="index" class="saved-feedback-item">
|
||||
<div class="feedback-meta">
|
||||
<span class="feedback-type">表扬</span>
|
||||
<span class="feedback-date">{{ feedback.date }}</span>
|
||||
</div>
|
||||
<p class="feedback-text">{{ feedback.content }}</p>
|
||||
<button class="read-saved-btn" @click="readFeedback(feedback.content)">再次朗读</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成长报告 -->
|
||||
<div class="growth-report-section">
|
||||
<h3 class="section-title">成长报告</h3>
|
||||
<div class="report-card">
|
||||
<div class="report-header">
|
||||
<h4>本周成长报告</h4>
|
||||
<span class="report-date">{{ currentWeek }}</span>
|
||||
</div>
|
||||
<div class="report-content">
|
||||
<div class="report-item">
|
||||
<span class="item-label">已击败怪兽:</span>
|
||||
<span class="item-value">{{ defeatedMonstersCount }} 只</span>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<span class="item-label">习惯打卡:</span>
|
||||
<span class="item-value">{{ totalCheckins }} 次</span>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<span class="item-label">数学游戏:</span>
|
||||
<span class="item-value">{{ completedMathTasks }} 个</span>
|
||||
</div>
|
||||
<div class="report-item">
|
||||
<span class="item-label">获得星星:</span>
|
||||
<span class="item-value">{{ totalStars }} 颗</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-tips">
|
||||
<h5>引导建议</h5>
|
||||
<p>继续鼓励孩子坚持好习惯,多参与手工活动,培养动手能力和创造力。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 安全守护 -->
|
||||
<div class="safety-section">
|
||||
<h3 class="section-title">安全守护</h3>
|
||||
<div class="safety-card">
|
||||
<div class="safety-item">
|
||||
<h4>每日冒险时间</h4>
|
||||
<div class="time-setting">
|
||||
<input type="range" v-model="dailyTime" min="10" max="60" step="5" class="time-slider">
|
||||
<span class="time-value">{{ dailyTime }} 分钟</span>
|
||||
</div>
|
||||
<p class="safety-desc">建议每天最多 30 分钟,保护孩子视力。</p>
|
||||
</div>
|
||||
<div class="safety-item">
|
||||
<h4>内容屏蔽</h4>
|
||||
<div class="shield-settings">
|
||||
<label class="shield-option">
|
||||
<input type="checkbox" v-model="shieldSettings.violence">
|
||||
<span>暴力内容</span>
|
||||
</label>
|
||||
<label class="shield-option">
|
||||
<input type="checkbox" v-model="shieldSettings.inappropriate">
|
||||
<span>不当内容</span>
|
||||
</label>
|
||||
<label class="shield-option">
|
||||
<input type="checkbox" v-model="shieldSettings.ads">
|
||||
<span>广告内容</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="safety-item">
|
||||
<h4>好友管理</h4>
|
||||
<p class="safety-desc">爸爸妈妈可以帮你添加好友,不用担心遇到坏人。</p>
|
||||
<button class="add-friend-btn">添加好友</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||
|
||||
const starEnergyStore = useStarEnergyStore();
|
||||
|
||||
// 亲子评价相关
|
||||
const generatedPraise = ref('');
|
||||
const savedFeedbacks = ref<Array<{type: string, content: string, date: string}>>([]);
|
||||
|
||||
// 生成表扬语
|
||||
const generatePraise = () => {
|
||||
const energy = starEnergyStore.getTotalEnergy;
|
||||
const checkins = Object.values(starEnergyStore.habits).reduce((sum, habit) => sum + habit.count, 0);
|
||||
const defeatedMonsters = Object.values(starEnergyStore.monsters).filter(m => m.defeated).length;
|
||||
|
||||
// 根据表现生成不同的表扬语
|
||||
const praiseTemplates = [
|
||||
// 高能量值
|
||||
energy >= 500 ? `太棒了!你已经收集了 ${energy} 颗星星,真是个超级小英雄!` : '',
|
||||
energy >= 300 ? `你太厉害了!已经有 ${energy} 颗星星了,继续保持哦!` : '',
|
||||
energy >= 100 ? `不错哦!你已经有 ${energy} 颗星星了,要继续努力!` : '',
|
||||
|
||||
// 习惯打卡
|
||||
checkins >= 20 ? `你坚持打卡了 ${checkins} 次,真是个有毅力的好孩子!` : '',
|
||||
checkins >= 10 ? `你已经打卡了 ${checkins} 次,好习惯正在慢慢养成!` : '',
|
||||
checkins >= 5 ? `你开始养成好习惯了,已经打卡 ${checkins} 次,继续加油!` : '',
|
||||
|
||||
// 击败怪兽
|
||||
defeatedMonsters >= 3 ? `你已经击败了 ${defeatedMonsters} 只怪兽,真是个勇敢的小战士!` : '',
|
||||
defeatedMonsters >= 1 ? `你已经击败了 ${defeatedMonsters} 只怪兽,继续保护成长星球!` : '',
|
||||
|
||||
// 通用表扬
|
||||
'你是最棒的!爸爸妈妈为你感到骄傲!',
|
||||
'继续努力,你会变得越来越优秀!',
|
||||
'你的进步真大,继续保持哦!',
|
||||
'你真是个好孩子,爸爸妈妈爱你!',
|
||||
'看到你的成长,我们真的很开心!'
|
||||
];
|
||||
|
||||
// 过滤掉空字符串,然后随机选择一个
|
||||
const validPraise = praiseTemplates.filter(p => p);
|
||||
generatedPraise.value = validPraise[Math.floor(Math.random() * validPraise.length)];
|
||||
|
||||
// 保存到历史记录,只保留最近5条
|
||||
const newFeedback = {
|
||||
type: 'praise',
|
||||
content: generatedPraise.value,
|
||||
date: new Date().toLocaleString()
|
||||
};
|
||||
|
||||
savedFeedbacks.value.unshift(newFeedback); // 添加到开头
|
||||
if (savedFeedbacks.value.length > 5) {
|
||||
savedFeedbacks.value = savedFeedbacks.value.slice(0, 5); // 只保留前5条
|
||||
}
|
||||
|
||||
// 保存到 localStorage
|
||||
const user = localStorage.getItem('currentUser') || 'default';
|
||||
localStorage.setItem(`parentFeedback_${user}`, JSON.stringify(savedFeedbacks.value));
|
||||
};
|
||||
|
||||
// 朗读评价
|
||||
const readFeedback = (content?: string) => {
|
||||
let text = content || generatedPraise.value;
|
||||
|
||||
// 确保text是字符串且不为空
|
||||
if (!text || typeof text !== 'string' || text.trim() === '') {
|
||||
// 如果没有表扬语,先生成一个
|
||||
generatePraise();
|
||||
text = generatedPraise.value;
|
||||
|
||||
// 如果仍然没有表扬语,提示用户
|
||||
if (!text || typeof text !== 'string' || text.trim() === '') {
|
||||
alert('请先生成表扬语!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const speech = new SpeechSynthesisUtterance(text);
|
||||
speech.lang = 'zh-CN';
|
||||
speech.volume = 1;
|
||||
speech.rate = 1;
|
||||
speech.pitch = 1.2;
|
||||
|
||||
// 尝试选择更适合儿童的中文语音
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
|
||||
if (voices.length > 0) {
|
||||
// 优先选择女性或儿童语音
|
||||
const selectedVoice = voices.find(voice =>
|
||||
voice.lang === 'zh-CN' &&
|
||||
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女') || voice.name.includes('Microsoft Yaoyao') || voice.name.includes('Microsoft Huihui'))
|
||||
) || voices.find(voice => voice.lang === 'zh-CN');
|
||||
|
||||
if (selectedVoice) {
|
||||
speech.voice = selectedVoice;
|
||||
}
|
||||
|
||||
// 立即播放
|
||||
window.speechSynthesis.speak(speech);
|
||||
} else {
|
||||
// 语音列表尚未加载完成,等待加载完成后再播放
|
||||
const handleVoicesChanged = () => {
|
||||
const updatedVoices = window.speechSynthesis.getVoices();
|
||||
const selectedVoice = updatedVoices.find(voice =>
|
||||
voice.lang === 'zh-CN' &&
|
||||
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女') || voice.name.includes('Microsoft Yaoyao') || voice.name.includes('Microsoft Huihui'))
|
||||
) || updatedVoices.find(voice => voice.lang === 'zh-CN');
|
||||
|
||||
if (selectedVoice) {
|
||||
speech.voice = selectedVoice;
|
||||
}
|
||||
|
||||
window.speechSynthesis.speak(speech);
|
||||
|
||||
// 移除事件监听器
|
||||
window.speechSynthesis.removeEventListener('voiceschanged', handleVoicesChanged);
|
||||
};
|
||||
|
||||
// 添加事件监听器
|
||||
window.speechSynthesis.addEventListener('voiceschanged', handleVoicesChanged);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('音频播放失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载历史评价
|
||||
const loadSavedFeedbacks = () => {
|
||||
const user = localStorage.getItem('currentUser') || 'default';
|
||||
const saved = localStorage.getItem(`parentFeedback_${user}`);
|
||||
if (saved) {
|
||||
try {
|
||||
savedFeedbacks.value = JSON.parse(saved);
|
||||
} catch (error) {
|
||||
console.error('加载评价失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化时加载语音合成API的voices和生成表扬语
|
||||
onMounted(() => {
|
||||
// 触发语音合成API的加载
|
||||
window.speechSynthesis.getVoices();
|
||||
// 加载历史评价并生成第一条表扬语
|
||||
loadSavedFeedbacks();
|
||||
generatePraise();
|
||||
});
|
||||
|
||||
// 成长报告相关
|
||||
const currentWeek = computed(() => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today);
|
||||
startOfWeek.setDate(today.getDate() - today.getDay() + 1);
|
||||
const endOfWeek = new Date(startOfWeek);
|
||||
endOfWeek.setDate(startOfWeek.getDate() + 6);
|
||||
return `${startOfWeek.getMonth() + 1}/${startOfWeek.getDate()} - ${endOfWeek.getMonth() + 1}/${endOfWeek.getDate()}`;
|
||||
});
|
||||
|
||||
const defeatedMonstersCount = computed(() => {
|
||||
return Object.values(starEnergyStore.monsters).filter(m => m.defeated).length;
|
||||
});
|
||||
|
||||
const totalCheckins = computed(() => {
|
||||
return Object.values(starEnergyStore.habits).reduce((sum, habit) => sum + habit.count, 0);
|
||||
});
|
||||
|
||||
const completedMathTasks = ref(5); // 模拟数据
|
||||
const totalStars = computed(() => starEnergyStore.getTotalEnergy);
|
||||
|
||||
// 安全设置
|
||||
const dailyTime = ref(30);
|
||||
const shieldSettings = ref({
|
||||
violence: true,
|
||||
inappropriate: true,
|
||||
ads: true
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.parent-area {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.area-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.area-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.area-desc {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 亲子评价 */
|
||||
.parent-feedback-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.feedback-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.feedback-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.feedback-icon {
|
||||
font-size: 32px;
|
||||
margin-right: 20px;
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feedback-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.feedback-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.feedback-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.feedback-form {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.feedback-type-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
flex: 1;
|
||||
background: #f0f0f0;
|
||||
border: 2px solid #ddd;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.performance-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.generated-feedback {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.generated-feedback p {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.generate-feedback-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.generate-feedback-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.read-feedback-btn {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.read-feedback-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.read-feedback-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.saved-feedbacks {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.saved-feedbacks h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.saved-feedback-item {
|
||||
background: #f9f9f9;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.feedback-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.feedback-type {
|
||||
font-weight: bold;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.feedback-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.read-saved-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.read-saved-btn:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
|
||||
/* 成长报告 */
|
||||
.growth-report-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.report-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.report-header h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.report-date {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.report-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.report-tips {
|
||||
background: #f0f8ff;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.report-tips h5 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.report-tips p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 安全守护 */
|
||||
.safety-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.safety-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.safety-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.safety-item {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.safety-item:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.safety-item h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.time-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.time-slider {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #ddd;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.time-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time-slider::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #667eea;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.safety-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shield-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.shield-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shield-option input {
|
||||
margin-right: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.add-friend-btn {
|
||||
margin-top: 10px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.add-friend-btn:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
</style>
|
||||
813
src/components/RewardArea.vue
Normal file
813
src/components/RewardArea.vue
Normal file
@@ -0,0 +1,813 @@
|
||||
<template>
|
||||
<div class="reward-area">
|
||||
<div class="reward-header">
|
||||
<h2 class="reward-title">🎁 奖励商店</h2>
|
||||
<p class="reward-subtitle">用星星兑换你喜欢的奖励吧!✨</p>
|
||||
</div>
|
||||
|
||||
<!-- 星星余额显示 -->
|
||||
<div class="star-balance">
|
||||
<div class="balance-card">
|
||||
<div class="balance-icon">⭐</div>
|
||||
<div class="balance-info">
|
||||
<div class="balance-label">我的星星</div>
|
||||
<div class="balance-amount">{{ starEnergyStore.getTotalEnergy }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 奖励分类 -->
|
||||
<div class="reward-categories">
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
class="category-btn"
|
||||
:class="{ active: currentCategory === category.id }"
|
||||
@click="currentCategory = category.id"
|
||||
>
|
||||
<span class="category-icon">{{ category.icon }}</span>
|
||||
<span class="category-name">{{ category.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 奖励列表 -->
|
||||
<div class="reward-list">
|
||||
<div
|
||||
v-for="reward in filteredRewards"
|
||||
:key="reward.id"
|
||||
class="reward-card"
|
||||
:class="{
|
||||
disabled: (reward.type === 'boss_defeat' ? starEnergyStore.getBossDefeats < reward.cost : starEnergyStore.getTotalEnergy < reward.cost),
|
||||
claimed: claimedRewards.includes(reward.id)
|
||||
}"
|
||||
>
|
||||
<div class="reward-emoji">{{ reward.emoji }}</div>
|
||||
<div class="reward-info">
|
||||
<h3 class="reward-name">{{ reward.name }}</h3>
|
||||
<p class="reward-desc">{{ reward.description }}</p>
|
||||
<div class="reward-cost">
|
||||
<span class="cost-icon">{{ reward.type === 'boss_defeat' ? '👾' : '⭐' }}</span>
|
||||
<span class="cost-value">{{ reward.cost }}</span>
|
||||
<span class="cost-unit">{{ reward.type === 'boss_defeat' ? '次击败' : '星' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="redeem-btn"
|
||||
:class="{ disabled: ((reward.type === 'boss_defeat' ? starEnergyStore.getBossDefeats < reward.cost : starEnergyStore.getTotalEnergy < reward.cost) || claimedRewards.includes(reward.id)) }"
|
||||
@click="redeemReward(reward)"
|
||||
:disabled="(reward.type === 'boss_defeat' ? starEnergyStore.getBossDefeats < reward.cost : starEnergyStore.getTotalEnergy < reward.cost) || claimedRewards.includes(reward.id)"
|
||||
>
|
||||
{{ getButtonText(reward) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已兑换奖励 -->
|
||||
<div class="claimed-rewards" v-if="claimedRewards.length > 0">
|
||||
<h3 class="claimed-title">🎉 已兑换的奖励</h3>
|
||||
<div class="claimed-grid">
|
||||
<div
|
||||
v-for="id in claimedRewards"
|
||||
:key="id"
|
||||
class="claimed-card"
|
||||
>
|
||||
{{ getRewardById(id)?.emoji }} {{ getRewardById(id)?.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 兑换成功弹窗 -->
|
||||
<div v-if="showSuccessModal" class="modal-overlay" @click="closeModal">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-emoji">🎉</div>
|
||||
<h3 class="modal-title">兑换成功!</h3>
|
||||
<p class="modal-message">恭喜你获得了 {{ selectedReward?.name }}!</p>
|
||||
<button class="modal-btn" @click="closeModal">太棒了!</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 星星不足提示 -->
|
||||
<div v-if="showInsufficientModal" class="modal-overlay" @click="closeModal">
|
||||
<div class="modal-content insufficient" @click.stop>
|
||||
<div class="modal-emoji">😢</div>
|
||||
<h3 class="modal-title">星星不足</h3>
|
||||
<p class="modal-message">你还需要 {{ insufficientStars }} 颗星星才能兑换这个奖励哦!</p>
|
||||
<button class="modal-btn" @click="closeModal">继续努力!</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useStarEnergyStore } from '../stores/starEnergy';
|
||||
|
||||
const starEnergyStore = useStarEnergyStore();
|
||||
|
||||
// 当前分类
|
||||
const currentCategory = ref('all');
|
||||
|
||||
// 已兑换奖励ID列表
|
||||
const claimedRewards = computed(() => starEnergyStore.claimedRewards);
|
||||
|
||||
// 弹窗状态
|
||||
const showSuccessModal = ref(false);
|
||||
const showInsufficientModal = ref(false);
|
||||
const insufficientStars = ref(0);
|
||||
const selectedReward = ref<any>(null);
|
||||
|
||||
// 奖励分类
|
||||
const categories = [
|
||||
{ id: 'all', name: '全部', icon: '🎯' },
|
||||
{ id: 'toy', name: '玩具', icon: '🎮' },
|
||||
{ id: 'entertainment', name: '娱乐', icon: '🎬' },
|
||||
{ id: 'food', name: '美食', icon: '🍔' },
|
||||
{ id: 'activity', name: '活动', icon: '🎪' },
|
||||
{ id: 'boss', name: 'BOSS奖励', icon: '👾' }
|
||||
];
|
||||
|
||||
// 奖励列表
|
||||
const rewards = [
|
||||
// 小奖励(20星)
|
||||
{
|
||||
id: 'cartoon_20',
|
||||
name: '看20分钟动画片',
|
||||
description: '可以看20分钟喜欢的动画片哦!',
|
||||
cost: 20,
|
||||
emoji: '📺',
|
||||
category: 'entertainment'
|
||||
},
|
||||
{
|
||||
id: 'snack_20',
|
||||
name: '小零食一份',
|
||||
description: '可以吃一份健康的小零食!',
|
||||
cost: 20,
|
||||
emoji: '🍪',
|
||||
category: 'food'
|
||||
},
|
||||
// 中等奖励(50星)
|
||||
{
|
||||
id: 'movie_50',
|
||||
name: '看一部电影',
|
||||
description: '可以看一部完整的电影!',
|
||||
cost: 50,
|
||||
emoji: '🎬',
|
||||
category: 'entertainment'
|
||||
},
|
||||
{
|
||||
id: 'icecream_50',
|
||||
name: '冰淇淋一个',
|
||||
description: '可以吃一个美味的冰淇淋!',
|
||||
cost: 50,
|
||||
emoji: '🍦',
|
||||
category: 'food'
|
||||
},
|
||||
{
|
||||
id: 'park_50',
|
||||
name: '去公园玩',
|
||||
description: '可以和爸爸妈妈去公园玩!',
|
||||
cost: 50,
|
||||
emoji: '🎡',
|
||||
category: 'activity'
|
||||
},
|
||||
// 大奖励(100星)
|
||||
{
|
||||
id: 'toy_100',
|
||||
name: '一个小玩具',
|
||||
description: '可以获得一个心仪的小玩具!',
|
||||
cost: 100,
|
||||
emoji: '🧸',
|
||||
category: 'toy'
|
||||
},
|
||||
{
|
||||
id: 'restaurant_100',
|
||||
name: '去餐厅吃饭',
|
||||
description: '可以去喜欢的餐厅吃顿好的!',
|
||||
cost: 100,
|
||||
emoji: '🍽️',
|
||||
category: 'food'
|
||||
},
|
||||
{
|
||||
id: 'zoo_100',
|
||||
name: '去动物园',
|
||||
description: '可以去动物园看小动物!',
|
||||
cost: 100,
|
||||
emoji: '🦁',
|
||||
category: 'activity'
|
||||
},
|
||||
// 超级奖励(200星)
|
||||
{
|
||||
id: 'lego_200',
|
||||
name: '乐高积木套装',
|
||||
description: '可以获得一套乐高积木!',
|
||||
cost: 200,
|
||||
emoji: '🧱',
|
||||
category: 'toy'
|
||||
},
|
||||
{
|
||||
id: 'sticker_book_200',
|
||||
name: '贴纸书',
|
||||
description: '女孩喜欢的精美贴纸书!',
|
||||
cost: 200,
|
||||
emoji: '🖼️',
|
||||
category: 'toy'
|
||||
},
|
||||
{
|
||||
id: 'quiet_book_200',
|
||||
name: '安静书',
|
||||
description: '女孩喜欢的手工安静书!',
|
||||
cost: 200,
|
||||
emoji: '📚',
|
||||
category: 'toy'
|
||||
},
|
||||
{
|
||||
id: 'amusement_200',
|
||||
name: '游乐园一日游',
|
||||
description: '可以和爸爸妈妈去游乐园玩一整天!',
|
||||
cost: 200,
|
||||
emoji: '🎢',
|
||||
category: 'activity'
|
||||
},
|
||||
{
|
||||
id: 'bike_200',
|
||||
name: '自行车一辆',
|
||||
description: '可以获得一辆全新的自行车!',
|
||||
cost: 200,
|
||||
emoji: '🚲',
|
||||
category: 'toy'
|
||||
},
|
||||
// 高级奖励(400星)
|
||||
{
|
||||
id: 'special_toy_400',
|
||||
name: '指定玩具',
|
||||
description: '可以获得一个指定的心仪玩具!',
|
||||
cost: 400,
|
||||
emoji: '🎁',
|
||||
category: 'toy'
|
||||
},
|
||||
// 终极奖励(500星)
|
||||
{
|
||||
id: 'family_trip_500',
|
||||
name: '家庭旅行',
|
||||
description: '可以和家人一起去旅行!',
|
||||
cost: 500,
|
||||
emoji: '✈️',
|
||||
category: 'activity'
|
||||
},
|
||||
{
|
||||
id: 'game_console_500',
|
||||
name: '游戏机',
|
||||
description: '可以获得一台游戏机!',
|
||||
cost: 500,
|
||||
emoji: '🎮',
|
||||
category: 'toy'
|
||||
},
|
||||
// BOSS奖励(需要击败BOSS次数)
|
||||
{
|
||||
id: 'boss_reward_1',
|
||||
name: 'BOSS击败者勋章',
|
||||
description: '击败BOSS一次的荣誉勋章!',
|
||||
cost: 1,
|
||||
emoji: '🏆',
|
||||
category: 'boss',
|
||||
type: 'boss_defeat'
|
||||
},
|
||||
{
|
||||
id: 'boss_reward_3',
|
||||
name: 'BOSS克星称号',
|
||||
description: '击败BOSS三次的尊贵称号!',
|
||||
cost: 3,
|
||||
emoji: '👑',
|
||||
category: 'boss',
|
||||
type: 'boss_defeat'
|
||||
},
|
||||
{
|
||||
id: 'boss_reward_5',
|
||||
name: '终极BOSS猎手',
|
||||
description: '击败BOSS五次的终极荣誉!',
|
||||
cost: 5,
|
||||
emoji: '🌟',
|
||||
category: 'boss',
|
||||
type: 'boss_defeat'
|
||||
}
|
||||
];
|
||||
|
||||
// 筛选奖励
|
||||
const filteredRewards = computed(() => {
|
||||
if (currentCategory.value === 'all') {
|
||||
return rewards;
|
||||
}
|
||||
return rewards.filter(reward => reward.category === currentCategory.value);
|
||||
});
|
||||
|
||||
// 获取按钮文本
|
||||
const getButtonText = (reward: any) => {
|
||||
if (claimedRewards.value.includes(reward.id)) {
|
||||
return '已兑换 ✓';
|
||||
}
|
||||
if (reward.type === 'boss_defeat') {
|
||||
if (starEnergyStore.getBossDefeats < reward.cost) {
|
||||
return `还差${reward.cost - starEnergyStore.getBossDefeats}次击败`;
|
||||
}
|
||||
} else {
|
||||
if (starEnergyStore.getTotalEnergy < reward.cost) {
|
||||
return `还差${reward.cost - starEnergyStore.getTotalEnergy}星`;
|
||||
}
|
||||
}
|
||||
return '立即兑换';
|
||||
};
|
||||
|
||||
// 兑换奖励
|
||||
const redeemReward = (reward: any) => {
|
||||
// 检查是否是BOSS奖励
|
||||
if (reward.type === 'boss_defeat') {
|
||||
if (starEnergyStore.getBossDefeats < reward.cost) {
|
||||
insufficientStars.value = reward.cost - starEnergyStore.getBossDefeats;
|
||||
selectedReward.value = reward;
|
||||
showInsufficientModal.value = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 普通奖励使用星星能量
|
||||
if (starEnergyStore.getTotalEnergy < reward.cost) {
|
||||
insufficientStars.value = reward.cost - starEnergyStore.getTotalEnergy;
|
||||
selectedReward.value = reward;
|
||||
showInsufficientModal.value = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (claimedRewards.value.includes(reward.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedReward.value = reward;
|
||||
let success = false;
|
||||
|
||||
if (reward.type === 'boss_defeat') {
|
||||
// BOSS奖励使用击败次数兑换
|
||||
// 这里可以添加专门的BOSS奖励兑换逻辑
|
||||
success = starEnergyStore.redeemReward(reward.id, 0); // 不消耗星星
|
||||
} else {
|
||||
// 普通奖励使用星星能量
|
||||
success = starEnergyStore.redeemReward(reward.id, reward.cost);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
showSuccessModal.value = true;
|
||||
|
||||
// 播放音效
|
||||
try {
|
||||
const speech = new SpeechSynthesisUtterance('恭喜你,兑换成功!');
|
||||
speech.lang = 'zh-CN';
|
||||
speech.volume = 1;
|
||||
speech.rate = 1;
|
||||
speech.pitch = 1.2;
|
||||
window.speechSynthesis.speak(speech);
|
||||
} catch (error) {
|
||||
console.error('音频播放失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 根据ID获取奖励
|
||||
const getRewardById = (id: string) => {
|
||||
return rewards.find(reward => reward.id === id);
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeModal = () => {
|
||||
showSuccessModal.value = false;
|
||||
showInsufficientModal.value = false;
|
||||
selectedReward.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reward-area {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.reward-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.reward-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #FF69B4;
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.reward-subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
/* 星星余额 */
|
||||
.star-balance {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
border-radius: 25px;
|
||||
padding: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
box-shadow: 0 8px 25px rgba(255, 215, 0, 0.4);
|
||||
border: 4px solid #FF8C00;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.balance-icon {
|
||||
font-size: 50px;
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 16px;
|
||||
color: #8B4513;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #8B4513;
|
||||
font-family: var(--cartoon-font);
|
||||
text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 奖励分类 */
|
||||
.reward-categories {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
flex: 0 0 auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 3px solid #FFB6C1;
|
||||
border-radius: 20px;
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.category-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: linear-gradient(135deg, #FFB6C1, #FFC0CB);
|
||||
border-color: #FF69B4;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 20px;
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-family: var(--cartoon-font);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-btn.active .category-name {
|
||||
color: white;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 奖励列表 */
|
||||
.reward-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.reward-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
border: 4px solid #98FB98;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reward-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transform: rotate(45deg);
|
||||
animation: shine 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { transform: translateX(-100%) rotate(45deg); }
|
||||
100% { transform: translateX(100%) rotate(45deg); }
|
||||
}
|
||||
|
||||
.reward-card:hover:not(.disabled):not(.claimed) {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
border-color: #32CD32;
|
||||
}
|
||||
|
||||
.reward-card.disabled {
|
||||
border-color: #ccc;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.reward-card.claimed {
|
||||
border-color: #FFD700;
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
}
|
||||
|
||||
.reward-emoji {
|
||||
font-size: 50px;
|
||||
flex-shrink: 0;
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.reward-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reward-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.reward-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.reward-cost {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
border-radius: 15px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cost-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.cost-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #8B4513;
|
||||
}
|
||||
|
||||
.cost-unit {
|
||||
font-size: 12px;
|
||||
color: #8B4513;
|
||||
margin-left: 2px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.redeem-btn {
|
||||
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||
color: #006400;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
white-space: nowrap;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.redeem-btn:hover:not(.disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
|
||||
background: linear-gradient(135deg, #90EE90, #32CD32);
|
||||
}
|
||||
|
||||
.redeem-btn.disabled {
|
||||
background: #ccc;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 已兑换奖励 */
|
||||
.claimed-rewards {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 25px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||
border: 4px solid #FFD700;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.claimed-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #FF8C00;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.claimed-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.claimed-card {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
box-shadow: 0 4px 10px rgba(255, 215, 0, 0.4);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 25px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
border: 5px solid #98FB98;
|
||||
animation: modalAppear 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content.insufficient {
|
||||
border-color: #FFB6C1;
|
||||
}
|
||||
|
||||
@keyframes modalAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-emoji {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
animation: bounce 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 25px;
|
||||
line-height: 1.6;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
background: linear-gradient(135deg, #98FB98, #90EE90);
|
||||
color: #006400;
|
||||
border: none;
|
||||
padding: 15px 40px;
|
||||
border-radius: 25px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(5deg); }
|
||||
75% { transform: rotate(-5deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.reward-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.reward-card {
|
||||
padding: 15px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.reward-emoji {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.reward-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.reward-desc {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.redeem-btn {
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.claimed-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
744
src/components/WelcomeArea.vue
Normal file
744
src/components/WelcomeArea.vue
Normal file
@@ -0,0 +1,744 @@
|
||||
<template>
|
||||
<div class="welcome-area">
|
||||
<!-- 漂浮的星星装饰 -->
|
||||
<div class="star-decoration star-1">⭐</div>
|
||||
<div class="star-decoration star-2">✨</div>
|
||||
<div class="star-decoration star-3">⭐</div>
|
||||
<div class="star-decoration star-4">✨</div>
|
||||
<div class="star-decoration star-5">⭐</div>
|
||||
<!-- 漂浮的成长星球图片 -->
|
||||
<div class="planet-decoration planet-1">
|
||||
<img :src="'./儿童成长星球.png'" alt="儿童成长星球" class="floating-planet">
|
||||
</div>
|
||||
<!-- 漂浮的卡通图片装饰 -->
|
||||
<div class="floating-image image-1">
|
||||
<img :src="'./生成卡通图片 (8).png'" alt="卡通装饰" />
|
||||
</div>
|
||||
<div class="floating-image image-2">
|
||||
<img :src="'./生成卡通图片 (9).png'" alt="卡通装饰" />
|
||||
</div>
|
||||
<div class="floating-image image-3">
|
||||
<img :src="'./生成卡通图片 (10).png'" alt="卡通装饰" />
|
||||
</div>
|
||||
<div class="floating-image image-4">
|
||||
<img :src="'./生成卡通图片 (11).png'" alt="卡通装饰" />
|
||||
</div>
|
||||
<div class="floating-image image-5">
|
||||
<img :src="'./生成卡通图片 (12).png'" alt="卡通装饰" />
|
||||
</div>
|
||||
<div class="floating-image image-6">
|
||||
<img :src="'./生成卡通图片 (13).png'" alt="卡通装饰" />
|
||||
</div>
|
||||
<div class="floating-image image-7">
|
||||
<img :src="'./哪吒.png'" alt="卡通装饰" />
|
||||
</div>
|
||||
<div class="floating-image image-8">
|
||||
<img :src="'./孙悟空.png'" alt="卡通装饰" />
|
||||
</div>
|
||||
|
||||
<!-- 星宝角色 -->
|
||||
<div class="xingbao-guide">
|
||||
<div class="xingbao-avatar">🌟</div>
|
||||
<div class="xingbao-speech">
|
||||
<div class="speech-bubble">
|
||||
<p>小勇士,欢迎来到成长星球!我是你的向导星宝~</p>
|
||||
</div>
|
||||
<button class="audio-btn mini" @click="playAudio('小勇士,欢迎来到成长星球!我是你的向导星宝!')">🔊</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-header">
|
||||
<div class="header-with-audio">
|
||||
<h2 class="welcome-title">✨ 欢迎来到成长星球! ✨</h2>
|
||||
<button class="audio-btn" @click="playAudio('欢迎来到成长星球!你准备好成为成长小勇士了吗?')">🔊</button>
|
||||
</div>
|
||||
<p class="welcome-subtitle">你准备好成为成长小勇士了吗?</p>
|
||||
</div>
|
||||
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-image">
|
||||
<div class="planet-container">
|
||||
<img :src="'./成长星球1.png'" alt="成长星球" class="planet-image">
|
||||
<div class="planet-glow"></div>
|
||||
<div class="planet-orbit">
|
||||
<div class="orbit-star">⭐</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-story">
|
||||
<div class="story-card">
|
||||
<div class="story-icon">🌍</div>
|
||||
<div class="story-text">
|
||||
<p>在遥远的宇宙里,有一颗超可爱的成长星球~ 这里长满了知识树、习惯花,还有会说话的小精灵!</p>
|
||||
<button class="story-audio-btn" @click="playAudio('在遥远的宇宙里,有一颗超可爱的成长星球。这里长满了知识树、习惯花,还有会说话的小精灵!')">🔊</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-card warning">
|
||||
<div class="story-icon">😟</div>
|
||||
<div class="story-text">
|
||||
<p>但最近,3 只 "坏习惯怪兽" 偷偷入侵了星球,把知识树的叶子弄蔫了,习惯花也不开了😟</p>
|
||||
<button class="story-audio-btn" @click="playAudio('但是最近,3只坏习惯怪兽偷偷入侵了星球,把知识树的叶子弄蔫了,习惯花也不开了。')">🔊</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-card mission">
|
||||
<div class="story-icon">🦸</div>
|
||||
<div class="story-text">
|
||||
<p>你愿意化身 "成长小勇士",和星球向导 "星宝" 一起,通过学习、动手、养成好习惯,收集 "星星能量",打败怪兽、守护星球,成为全能小英雄吗?</p>
|
||||
<button class="story-audio-btn" @click="playAudio('你愿意化身成长小勇士,和星球向导星宝一起,通过学习、动手、养成好习惯,收集星星能量,打败怪兽、守护星球,成为全能小英雄吗?')">🔊</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-start">
|
||||
<button class="start-btn" @click="startAdventure">
|
||||
<span class="btn-icon">🚀</span>
|
||||
<span class="btn-text">开始冒险</span>
|
||||
</button>
|
||||
<div class="start-hint">💡 点击按钮开启你的冒险之旅!</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡通人物 -->
|
||||
<div class="cartoon-characters">
|
||||
<div class="character kuromi">
|
||||
<div class="character-bubble">加油哦!💪</div>
|
||||
<div class="character-emoji">💜</div>
|
||||
<div class="character-name">库洛米</div>
|
||||
</div>
|
||||
<div class="character nailong">
|
||||
<div class="character-bubble">一起玩吧!🎉</div>
|
||||
<div class="character-emoji">🐲</div>
|
||||
<div class="character-name">奶龙</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'navigate', area: string): void;
|
||||
}>();
|
||||
|
||||
// 初始化时加载语音合成API的voices
|
||||
onMounted(() => {
|
||||
// 触发语音合成API的加载
|
||||
window.speechSynthesis.getVoices();
|
||||
});
|
||||
|
||||
// 播放音频 - 库洛米声音风格
|
||||
const playAudio = (text: string) => {
|
||||
try {
|
||||
// 使用Web Speech API进行文本转语音
|
||||
const speech = new SpeechSynthesisUtterance(text);
|
||||
// 设置语言
|
||||
speech.lang = 'zh-CN';
|
||||
// 设置音量、语速和语调 - 库洛米风格(可爱、活泼、略带淘气)
|
||||
speech.volume = 1; // 音量
|
||||
speech.rate = 1.1; // 语速稍快,更有活力
|
||||
speech.pitch = 1.7; // 更高的语调,更可爱
|
||||
|
||||
// 尝试选择更适合儿童的语音
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
// 优先选择女性或儿童语音
|
||||
let selectedVoice = voices.find(voice =>
|
||||
voice.lang === 'zh-CN' &&
|
||||
(voice.name.includes('女') || voice.name.includes('child') || voice.name.includes('Child') || voice.name.includes('少女'))
|
||||
) || voices.find(voice => voice.lang === 'zh-CN');
|
||||
|
||||
if (selectedVoice) {
|
||||
speech.voice = selectedVoice;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放
|
||||
window.speechSynthesis.speak(speech);
|
||||
} catch (error) {
|
||||
console.error('音频播放失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const startAdventure = () => {
|
||||
// 播放开始冒险的语音
|
||||
playAudio('让我们开始冒险吧!选择你想去的场景吧!');
|
||||
// 触发导航事件,显示冒险地图
|
||||
emit('navigate', 'adventure');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.welcome-area {
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
position: relative;
|
||||
min-height: calc(100vh - 200px);
|
||||
background: linear-gradient(135deg, #FFF5E6 0%, #FFE4E1 50%, #E0F7FA 100%);
|
||||
border-radius: 30px;
|
||||
margin: 10px;
|
||||
box-shadow: 0 10px 30px rgba(255, 182, 193, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 星星装饰 */
|
||||
.star-decoration {
|
||||
position: absolute;
|
||||
font-size: 24px;
|
||||
animation: twinkle 2s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.star-1 { top: 50px; left: 30px; animation-delay: 0s; }
|
||||
.star-2 { top: 80px; right: 50px; animation-delay: 0.5s; }
|
||||
.star-3 { top: 200px; left: 80px; animation-delay: 1s; }
|
||||
.star-4 { bottom: 150px; right: 100px; animation-delay: 1.5s; }
|
||||
.star-5 { bottom: 100px; left: 150px; animation-delay: 2s; }
|
||||
|
||||
/* 漂浮的成长星球图片 */
|
||||
.planet-decoration {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.planet-1 {
|
||||
top: 100px;
|
||||
right: 150px;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.floating-planet {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 40px rgba(255, 182, 193, 0.5);
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 漂浮的卡通图片装饰 */
|
||||
.floating-image {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
animation: float 15s ease-in-out infinite;
|
||||
filter: drop-shadow(0 8px 20px rgba(255, 255, 255, 0.5));
|
||||
z-index: 1;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.floating-image img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.floating-image.image-1 { top: 20%; left: 10%; animation-delay: 0s; animation-duration: 18s; }
|
||||
.floating-image.image-2 { top: 30%; right: 10%; animation-delay: 3s; animation-duration: 20s; }
|
||||
.floating-image.image-3 { bottom: 20%; left: 10%; animation-delay: 6s; animation-duration: 19s; }
|
||||
.floating-image.image-4 { bottom: 30%; right: 10%; animation-delay: 9s; animation-duration: 21s; }
|
||||
.floating-image.image-5 { top: 50%; left: 5%; animation-delay: 12s; animation-duration: 17s; }
|
||||
.floating-image.image-6 { top: 15%; right: 15%; animation-delay: 1s; animation-duration: 16s; }
|
||||
.floating-image.image-7 { bottom: 15%; left: 15%; animation-delay: 4s; animation-duration: 22s; }
|
||||
.floating-image.image-8 { top: 60%; right: 5%; animation-delay: 7s; animation-duration: 15s; }
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 480px) {
|
||||
.floating-image img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.floating-image.image-1 { top: 15%; left: 5%; }
|
||||
.floating-image.image-2 { top: 25%; right: 5%; }
|
||||
.floating-image.image-3 { bottom: 15%; left: 5%; }
|
||||
.floating-image.image-4 { bottom: 25%; right: 5%; }
|
||||
.floating-image.image-5 { top: 45%; left: 5%; }
|
||||
.floating-image.image-6 { top: 10%; right: 10%; }
|
||||
.floating-image.image-7 { bottom: 10%; left: 10%; }
|
||||
.floating-image.image-8 { top: 60%; right: 5%; }
|
||||
}
|
||||
|
||||
/* 星宝角色 */
|
||||
.xingbao-guide {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.xingbao-avatar {
|
||||
font-size: 50px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5));
|
||||
}
|
||||
|
||||
.xingbao-speech {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.speech-bubble {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.speech-bubble::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 20px;
|
||||
border-width: 10px 10px 0;
|
||||
border-style: solid;
|
||||
border-color: white transparent transparent transparent;
|
||||
}
|
||||
|
||||
.speech-bubble p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.welcome-header {
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.header-with-audio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #FF69B4, #FFB6C1);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 15px;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
font-family: var(--cartoon-font);
|
||||
background: white;
|
||||
display: inline-block;
|
||||
padding: 10px 25px;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* 星球容器 */
|
||||
.welcome-image {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.planet-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.planet-image {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 40px rgba(255, 182, 193, 0.5), 0 0 80px rgba(255, 215, 0, 0.3);
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.planet-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle, rgba(255, 215, 0, 0.2) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
animation: glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.planet-orbit {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
border: 2px dashed rgba(255, 215, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
animation: rotate 10s linear infinite;
|
||||
}
|
||||
|
||||
.orbit-star {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
animation: twinkle 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 故事卡片 */
|
||||
.welcome-story {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.story-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
border: 3px solid #FFB6C1;
|
||||
}
|
||||
|
||||
.story-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.story-card.warning {
|
||||
border-color: #FFD700;
|
||||
background: linear-gradient(135deg, #FFFACD, #FFF);
|
||||
}
|
||||
|
||||
.story-card.mission {
|
||||
border-color: #98FB98;
|
||||
background: linear-gradient(135deg, #F0FFF0, #FFF);
|
||||
}
|
||||
|
||||
.story-icon {
|
||||
font-size: 40px;
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.story-text {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.story-text p {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
font-family: var(--cartoon-font);
|
||||
}
|
||||
|
||||
.welcome-start {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: linear-gradient(135deg, #98FB98, #32CD32, #90EE90);
|
||||
background-size: 200% 200%;
|
||||
color: #006400;
|
||||
border: none;
|
||||
padding: 18px 50px;
|
||||
border-radius: 30px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 6px 20px rgba(50, 205, 50, 0.4);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: shine 3s linear infinite;
|
||||
border: 3px solid #006400;
|
||||
}
|
||||
|
||||
.start-btn:hover {
|
||||
transform: translateY(-8px) scale(1.05);
|
||||
box-shadow: 0 12px 30px rgba(50, 205, 50, 0.6);
|
||||
background-position: right center;
|
||||
}
|
||||
|
||||
.start-btn:active {
|
||||
transform: translateY(-4px) scale(0.98);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 28px;
|
||||
animation: wiggle 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.start-hint {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
font-family: var(--cartoon-font);
|
||||
animation: twinkle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 卡通人物 */
|
||||
.cartoon-characters {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.character {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.character:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.character:nth-child(2) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
.character-bubble {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 8px 15px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-family: var(--cartoon-font);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.character-bubble::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 8px 8px 0;
|
||||
border-style: solid;
|
||||
border-color: white transparent transparent transparent;
|
||||
}
|
||||
|
||||
.character-emoji {
|
||||
font-size: 60px;
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.character-name {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-family: var(--cartoon-font);
|
||||
margin-top: 5px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 音频按钮样式 */
|
||||
.audio-btn {
|
||||
background: linear-gradient(135deg, #FFD166, #06D6A0);
|
||||
border: none;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
border: 2px solid #06D6A0;
|
||||
}
|
||||
|
||||
.audio-btn:hover {
|
||||
transform: scale(1.15) rotate(15deg);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.audio-btn.mini {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.story-audio-btn {
|
||||
background: linear-gradient(135deg, #FFB6C1, #FF69B4);
|
||||
border: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
box-shadow: 0 3px 6px rgba(255, 105, 180, 0.3);
|
||||
animation: twinkle 2s ease-in-out infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.story-audio-btn:hover {
|
||||
transform: scale(1.2) rotate(10deg);
|
||||
box-shadow: 0 5px 10px rgba(255, 105, 180, 0.5);
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(5deg); }
|
||||
75% { transform: rotate(-5deg); }
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.9); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-15px); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { background-position: left center; }
|
||||
100% { background-position: right center; }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.welcome-area {
|
||||
padding: 20px 15px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 14px;
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.planet-image {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.planet-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.planet-glow {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.planet-orbit {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.story-card {
|
||||
padding: 15px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.story-icon {
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
.xingbao-guide {
|
||||
display: none; /* 小屏幕隐藏星宝 */
|
||||
}
|
||||
|
||||
.cartoon-characters {
|
||||
padding: 0 20px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.character-emoji {
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
padding: 14px 35px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user