| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- <template>
- <view class="container">
- <!-- 空状态:没有孩子 -->
- <view class="empty-state" v-if="!hasChildren">
- <view class="empty-icon">👶</view>
- <text class="empty-title">还没有孩子信息</text>
- <text class="empty-desc">请先添加孩子,开始任务管理之旅</text>
- <button class="btn-add-child" @click="goToAddChild">添加孩子</button>
- <button class="btn-switch" @click="switchMode">切换回家长模式</button>
- </view>
- <!-- 有孩子时显示正常内容 -->
- <PlayfulCard v-else elevation="1">
- <!-- 欢迎区域 -->
- <view class="welcome-section">
- <view class="avatar-container">
- <view class="avatar-glow"></view>
- <image class="avatar" :src="avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
- <view class="level-badge" v-if="level">{{ level }}</view>
- </view>
- <view class="welcome-text">
- <text class="hello">你好,{{ nickname || '小朋友' }} 👋</text>
- <text class="subtitle">{{ getGreeting() }}</text>
- </view>
- <view class="streak-badge" v-if="streakDays > 0" @click="showStreakDetail">
- <text class="fire">🔥</text>
- <text class="days">{{ streakDays }}天</text>
- <text class="streak-label">连续打卡</text>
- </view>
- </view>
-
- <!-- 打卡里程碑进度条 -->
- <view class="milestone-bar" v-if="streakProgress && streakProgress.nextMilestone">
- <view class="milestone-info">
- <text class="milestone-label">距{{ streakProgress.nextMilestone }}天里程碑</text>
- <text class="milestone-progress">{{ streakProgress.daysToNext }}天后达标</text>
- </view>
- <view class="milestone-progress-bar">
- <view class="milestone-fill" :style="{ width: getMilestoneProgress() + '%' }"></view>
- </view>
- </view>
- <!-- 积分进度环 -->
- <view class="ring-section">
- <PointsProgressRing :points="totalPoints" :currentMilestone="totalPoints % 100" :nextMilestone="100" />
- </view>
- <!-- 积分卡片 -->
- <view class="points-card">
- <view class="points-main">
- <view class="points-label">我的积分</view>
- <view class="points-value">{{ totalPoints }} <text class="unit">分</text></view>
- </view>
- <view class="points-actions">
- <view class="action-btn" @click="goToRewards">
- <text class="action-icon">🎁</text>
- <text class="action-text">兑换</text>
- </view>
- <view class="action-btn" @click="showPointsHistory">
- <text class="action-icon">📊</text>
- <text class="action-text">明细</text>
- </view>
- </view>
- <view class="points-expire" v-if="expireDate">📅 有效期至 {{ expireDate }}</view>
- </view>
- <!-- 今日任务概览 -->
- <view class="section">
- <view class="section-header">
- <view class="section-title">📝 今日任务</view>
- <view class="progress-bar">
- <view class="progress-fill" :style="{ width: taskProgress + '%' }"></view>
- </view>
- <text class="progress-text">{{ completedCount }}/{{ totalTaskCount }}</text>
- </view>
- <view class="task-summary">
- <view class="summary-item completed">
- <text class="emoji">✅</text>
- <text class="num">{{ completedCount }}</text>
- <text class="label">已完成</text>
- </view>
- <view class="summary-item pending">
- <text class="emoji">⏳</text>
- <text class="num">{{ pendingCount }}</text>
- <text class="label">待完成</text>
- </view>
- <view class="summary-item points-today">
- <text class="emoji">⭐</text>
- <text class="num">+{{ todayPoints }}</text>
- <text class="label">今日获得</text>
- </view>
- </view>
- <button class="btn-primary view-all-btn" @click="goToTasks">
- <text>🚀</text> 查看全部任务
- </button>
- </view>
- <!-- 快捷操作 -->
- <view class="section">
- <view class="section-title">⚡ 快捷操作</view>
- <view class="quick-actions">
- <view class="action-item" @click="goToRewards">
- <view class="action-icon-wrapper reward">🎁</view>
- <text class="label">心愿单</text>
- </view>
- <view class="action-item" @click="goToFocus">
- <view class="action-icon-wrapper focus">⏱️</view>
- <text class="label">专注训练</text>
- </view>
- <view class="action-item" @click="showAchievements">
- <view class="action-icon-wrapper trophy">🏆</view>
- <text class="label">成就</text>
- </view>
- <view class="action-item" @click="switchMode">
- <view class="action-icon-wrapper switch">🔄</view>
- <text class="label">切换</text>
- </view>
- </view>
- </view>
- <!-- 趣味提示 -->
- <view class="tip-card" v-if="tip">
- <text class="tip-icon">💡</text>
- <text class="tip-text">{{ tip }}</text>
- </view>
- <ConfettiCelebration :show="showConfetti" :duration="3000" />
- </PlayfulCard>
- </view>
- </template>
- <script>
- import PlayfulCard from '@/components/PlayfulCard.vue'
- import PlayfulButton from '@/components/PlayfulButton.vue'
- import ConfettiCelebration from '@/components/ConfettiCelebration.vue'
- import PointsProgressRing from '@/components/PointsProgressRing.vue'
- import { getTodayTasks, getPointsBalance, getChildren, switchBackToParent, verifyPassword, getStreakProgress } from '../../utils/api.js'
- export default {
- components: { PlayfulCard, PlayfulButton, ConfettiCelebration, PointsProgressRing },
- data() {
- return {
- hasChildren: true, // 默认有孩子,加载后更新
- nickname: '',
- avatar: '',
- totalPoints: 0,
- todayPoints: 0,
- streakDays: 0,
- streakProgress: null,
- expireDate: '',
- completedCount: 0,
- pendingCount: 0,
- currentChildId: '',
- showConfetti: false,
- level: '',
- tip: ''
- }
- },
- computed: {
- totalTaskCount() { return this.completedCount + this.pendingCount },
- taskProgress() {
- if (this.totalTaskCount === 0) return 0
- return (this.completedCount / this.totalTaskCount) * 100
- }
- },
- onLoad() { this.checkLogin() },
- onShow() {
- if (uni.getStorageSync('token')) this.loadData()
- },
- methods: {
- checkLogin() {
- if (!uni.getStorageSync('token')) {
- uni.reLaunch({ url: '/pages/login/login' })
- }
- },
- async loadData() {
- try {
- const childrenRes = await getChildren()
- const children = childrenRes.data
- if (children && children.length > 0) {
- this.hasChildren = true
- const child = children[0]
- this.currentChildId = child.id
- this.nickname = child.nickname
- this.avatar = child.avatar
- this.totalPoints = child.totalPoints || 0
- this.streakDays = child.streakDays || 0
- const balanceRes = await getPointsBalance(child.id)
- this.totalPoints = balanceRes.data.balance
- this.expireDate = this.formatDate(balanceRes.data.expireDate)
- const tasksRes = await getTodayTasks(child.id)
- const tasks = tasksRes.data
- this.completedCount = tasks.filter(t => t.status === 'completed').length
- this.pendingCount = tasks.filter(t => t.status === 'pending').length
- this.todayPoints = tasks.filter(t => t.status === 'completed').reduce((sum, t) => sum + (t.points || 0), 0)
- this.calculateLevel()
- this.setRandomTip()
- // 加载打卡进度
- this.loadStreakProgress(child.id)
- } else {
- // 没有孩子,显示空状态
- this.hasChildren = false
- }
- } catch (e) {
- console.error('加载数据失败', e)
- this.hasChildren = false
- }
- },
- async loadStreakProgress(childId) {
- try {
- const res = await getStreakProgress(childId)
- if (res.data) {
- this.streakProgress = res.data
- }
- } catch (e) { console.error('加载打卡进度失败', e) }
- },
- getMilestoneProgress() {
- if (!this.streakProgress || !this.streakProgress.nextMilestone) return 0
- const current = this.streakDays || 0
- const next = this.streakProgress.nextMilestone
- // 计算距离下一个里程碑的进度
- // 找到上一个已完成的里程碑
- const milestones = this.streakProgress.milestones || []
- let prevAchieved = 0
- for (let m of milestones) {
- if (m.achieved) prevAchieved = m.days
- }
- const total = next - prevAchieved
- const progress = current - prevAchieved
- return Math.min(100, Math.max(0, (progress / total) * 100))
- },
- calculateLevel() {
- const levels = ['🌱', '🌿', '🌳', '⭐', '🌟', '💫', '🏆', '👑']
- this.level = levels[Math.min(Math.floor(this.totalPoints / 100), levels.length - 1)]
- },
- setRandomTip() {
- const tips = ['坚持就是胜利!今天也要加油哦 💪', '完成任务可以获得更多积分 🎯', '连续打卡可以获得额外奖励 🌟', '今天也要做一个自律的小朋友!', '加油!你是最棒的! 🚀']
- this.tip = tips[Math.floor(Math.random() * tips.length)]
- },
- getGreeting() {
- const hour = new Date().getHours()
- if (hour < 12) return '早上好!今天也要元气满满 🌞'
- if (hour < 18) return '下午好!继续加油哦 ⛅'
- return '晚上好!辛苦啦 🎉'
- },
- formatDate(dateStr) {
- if (!dateStr) return ''
- const date = new Date(dateStr)
- return `${date.getMonth() + 1}月${date.getDate()}日`
- },
- goToTasks() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
- goToRewards() { uni.switchTab({ url: '/pages/rewards/rewards' }) },
- goToFocus() { uni.navigateTo({ url: '/pages/focus/focus' }) },
- showPointsHistory() { uni.showToast({ title: '积分明细功能开发中', icon: 'none' }) },
- showAchievements() { uni.showToast({ title: '成就功能开发中', icon: 'none' }) },
- showStreakDetail() {
- if (!this.streakProgress) {
- uni.showModal({ title: '连续打卡', content: `你已经连续打卡 ${this.streakDays} 天了!太棒了!🎉`, showCancel: false })
- return
- }
-
- const milestones = this.streakProgress.milestones || []
- let content = `连续打卡 ${this.streakDays} 天\n\n📋 里程碑进度:\n`
-
- milestones.forEach(m => {
- const status = m.achieved ? '✅' : '⭕'
- const reward = m.rewardPoints > 0 ? ` (+${m.rewardPoints}分)` : ''
- content += `${status} ${m.days}天${reward}\n`
- })
-
- if (this.streakProgress.nextMilestone) {
- content += `\n🎯 距下一里程碑: ${this.streakProgress.daysToNext} 天`
- }
-
- uni.showModal({ title: '🔥 连续打卡', content: content, showCancel: false })
- },
- switchMode() {
- // 弹出密码输入框
- uni.showModal({
- title: '切换模式',
- content: '请输入家长密码以确认切换',
- editable: true,
- success: async (res) => {
- if (res.confirm && res.content) {
- // 验证密码
- try {
- const verifyResult = await verifyPassword(res.content)
- if (verifyResult.code === 200) {
- await this.switchBackToParentMode()
- } else {
- uni.showToast({ title: '密码错误', icon: 'none' })
- }
- } catch (e) {
- uni.showToast({ title: '密码验证失败', icon: 'none' })
- }
- }
- }
- })
- },
- async switchBackToParentMode() {
- try {
- const result = await switchBackToParent()
- if (result.data) {
- uni.showToast({ title: '已切换回家长模式', icon: 'success' })
- uni.reLaunch({ url: '/pages/index/parent-index' })
- }
- } catch (e) { uni.showToast({ title: '切换失败', icon: 'none' }) }
- },
- triggerConfetti() {
- this.showConfetti = true
- setTimeout(() => { this.showConfetti = false }, 3000)
- },
- goToAddChild() {
- uni.navigateTo({ url: '/pages/profile/create-child' })
- }
- }
- }
- </script>
- <style scoped>
- .container { padding: 30rpx; }
- .welcome-section { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; flex-wrap: wrap; }
- .avatar-container { position: relative; width: 120rpx; height: 120rpx; }
- .avatar-glow { position: absolute; width: 120rpx; height: 120rpx; border-radius: 50%; background: linear-gradient(135deg, #FFD700, #FFA500); animation: pulse 2s infinite; }
- .avatar { width: 120rpx; height: 120rpx; border-radius: 50%; border: 4rpx solid #fff; position: relative; z-index: 1; }
- .level-badge { position: absolute; bottom: -10rpx; left: 50%; transform: translateX(-50%); font-size: 24rpx; }
- @keyframes pulse { 0%, 100% { opacity: 0.5; transform: scale(1); } 50% { opacity: 0.8; transform: scale(1.1); } }
- .welcome-text { flex: 1; margin-left: 20rpx; }
- .hello { font-size: 36rpx; font-weight: bold; color: #333; display: block; }
- .subtitle { font-size: 26rpx; color: #666; }
- .streak-badge { display: flex; align-items: center; background: linear-gradient(135deg, #FF6B6B, #FF8E53); padding: 10rpx 20rpx; border-radius: 30rpx; }
- .streak-label { color: #fff; font-size: 20rpx; margin-left: 8rpx; }
- .ring-section { display: flex; justify-content: center; margin: 30rpx 0; }
- .points-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20rpx; padding: 40rpx; text-align: center; margin-bottom: 30rpx; }
- .points-main { margin-bottom: 20rpx; }
- .points-label { color: rgba(255, 255, 255, 0.8); font-size: 26rpx; }
- .points-value { color: #fff; font-size: 72rpx; font-weight: bold; }
- .unit { font-size: 32rpx; }
- .points-actions { display: flex; justify-content: center; gap: 40rpx; margin-bottom: 20rpx; }
- .action-btn { display: flex; align-items: center; background: rgba(255,255,255,0.2); padding: 10rpx 30rpx; border-radius: 30rpx; }
- .action-icon { font-size: 32rpx; margin-right: 10rpx; }
- .action-text { color: #fff; font-size: 24rpx; }
- .points-expire { color: rgba(255, 255, 255, 0.6); font-size: 22rpx; }
- .section { margin-bottom: 30rpx; }
- .section-header { display: flex; align-items: center; margin-bottom: 20rpx; flex-wrap: wrap; }
- .section-title { font-size: 32rpx; font-weight: bold; color: #333; margin-right: 20rpx; }
- .progress-bar { flex: 1; height: 16rpx; background: #E8F4FD; border-radius: 8rpx; overflow: hidden; max-width: 200rpx; }
- .progress-fill { height: 100%; background: linear-gradient(90deg, #0EA5E9, #10B981); border-radius: 8rpx; transition: width 0.3s; }
- .progress-text { font-size: 24rpx; color: #666; margin-left: 20rpx; }
- .task-summary { display: flex; justify-content: space-around; background: #fff; border-radius: 20rpx; padding: 30rpx; margin-bottom: 20rpx; }
- .summary-item { text-align: center; }
- .emoji { font-size: 40rpx; display: block; margin-bottom: 10rpx; }
- .summary-item .num { font-size: 40rpx; font-weight: bold; display: block; }
- .summary-item.completed .num { color: #10B981; }
- .summary-item.pending .num { color: #F59E0B; }
- .summary-item.points-today .num { color: #0EA5E9; }
- .summary-item .label { font-size: 24rpx; color: #666; }
- .view-all-btn { width: 100%; }
- .quick-actions { display: flex; justify-content: space-around; background: #fff; border-radius: 20rpx; padding: 30rpx; }
- .action-item { display: flex; flex-direction: column; align-items: center; }
- .action-icon-wrapper { width: 100rpx; height: 100rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; margin-bottom: 10rpx; }
- .action-icon-wrapper.reward { background: linear-gradient(135deg, #FFD700, #FFA500); }
- .action-icon-wrapper.focus { background: linear-gradient(135deg, #0EA5E9, #06B6D4); }
- .action-icon-wrapper.trophy { background: linear-gradient(135deg, #8B5CF6, #A855F7); }
- .action-icon-wrapper.switch { background: linear-gradient(135deg, #10B981, #34D399); }
- .action-item .label { font-size: 24rpx; color: #666; }
- .tip-card { background: linear-gradient(135deg, #FEF3C7, #FDE68A); border-radius: 20rpx; padding: 20rpx; display: flex; align-items: center; }
- .tip-icon { font-size: 32rpx; margin-right: 16rpx; }
- .tip-text { font-size: 26rpx; color: #92400E; flex: 1; }
- .milestone-bar { background: linear-gradient(135deg, #FF6B6B, #FF8E53); border-radius: 16rpx; padding: 20rpx; margin-bottom: 20rpx; }
- .milestone-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
- .milestone-label { color: #fff; font-size: 24rpx; }
- .milestone-progress { color: rgba(255,255,255,0.8); font-size: 22rpx; }
- .milestone-progress-bar { height: 12rpx; background: rgba(255,255,255,0.3); border-radius: 6rpx; overflow: hidden; }
- .milestone-fill { height: 100%; background: #fff; border-radius: 6rpx; transition: width 0.3s; }
- /* 空状态样式 */
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 100rpx 60rpx;
- text-align: center;
- }
- .empty-icon { font-size: 120rpx; margin-bottom: 30rpx; }
- .empty-title { font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 20rpx; }
- .empty-desc { font-size: 28rpx; color: #666; margin-bottom: 40rpx; }
- .btn-add-child {
- background: linear-gradient(135deg, #FF6B6B, #FF8E53);
- color: #fff;
- border-radius: 40rpx;
- padding: 20rpx 60rpx;
- font-size: 30rpx;
- margin-bottom: 20rpx;
- }
- .btn-switch {
- background: #fff;
- color: #666;
- border: 1rpx solid #ddd;
- border-radius: 40rpx;
- padding: 20rpx 60rpx;
- font-size: 28rpx;
- }
- </style>
|