Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| function Achievement() { | |
| const [stats, setStats] = useState({ | |
| total_sessions: 0, | |
| total_focus_time: 0, | |
| avg_focus_score: 0, | |
| streak_days: 0 | |
| }); | |
| const [badges, setBadges] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| // 格式化时间显示 | |
| const formatTime = (seconds) => { | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| if (hours > 0) return `${hours}h ${minutes}m`; | |
| return `${minutes}m`; | |
| }; | |
| // 加载统计数据 | |
| useEffect(() => { | |
| fetch('/api/stats/summary') | |
| .then(res => res.json()) | |
| .then(data => { | |
| setStats(data); | |
| calculateBadges(data); | |
| setLoading(false); | |
| }) | |
| .catch(err => { | |
| console.error('Failed to load stats:', err); | |
| setLoading(false); | |
| }); | |
| }, []); | |
| // 根据统计数据计算徽章 | |
| const calculateBadges = (data) => { | |
| const earnedBadges = []; | |
| // 首次会话徽章 | |
| if (data.total_sessions >= 1) { | |
| earnedBadges.push({ | |
| id: 'first-session', | |
| name: 'First Step', | |
| description: 'Complete your first focus session', | |
| icon: '🎯', | |
| unlocked: true | |
| }); | |
| } | |
| // 10次会话徽章 | |
| if (data.total_sessions >= 10) { | |
| earnedBadges.push({ | |
| id: 'ten-sessions', | |
| name: 'Getting Started', | |
| description: 'Complete 10 focus sessions', | |
| icon: '⭐', | |
| unlocked: true | |
| }); | |
| } | |
| // 50次会话徽章 | |
| if (data.total_sessions >= 50) { | |
| earnedBadges.push({ | |
| id: 'fifty-sessions', | |
| name: 'Dedicated', | |
| description: 'Complete 50 focus sessions', | |
| icon: '🏆', | |
| unlocked: true | |
| }); | |
| } | |
| // 专注大师徽章 (平均专注度 > 80%) | |
| if (data.avg_focus_score >= 0.8 && data.total_sessions >= 5) { | |
| earnedBadges.push({ | |
| id: 'focus-master', | |
| name: 'Focus Master', | |
| description: 'Maintain 80%+ average focus score', | |
| icon: '🧠', | |
| unlocked: true | |
| }); | |
| } | |
| // 连续天数徽章 | |
| if (data.streak_days >= 7) { | |
| earnedBadges.push({ | |
| id: 'week-streak', | |
| name: 'Week Warrior', | |
| description: '7 day streak', | |
| icon: '🔥', | |
| unlocked: true | |
| }); | |
| } | |
| if (data.streak_days >= 30) { | |
| earnedBadges.push({ | |
| id: 'month-streak', | |
| name: 'Month Master', | |
| description: '30 day streak', | |
| icon: '💎', | |
| unlocked: true | |
| }); | |
| } | |
| // 时长徽章 (10小时+) | |
| if (data.total_focus_time >= 36000) { | |
| earnedBadges.push({ | |
| id: 'ten-hours', | |
| name: 'Endurance', | |
| description: '10+ hours total focus time', | |
| icon: '⏱️', | |
| unlocked: true | |
| }); | |
| } | |
| // 未解锁徽章(示例) | |
| const allBadges = [ | |
| { | |
| id: 'first-session', | |
| name: 'First Step', | |
| description: 'Complete your first focus session', | |
| icon: '🎯', | |
| unlocked: data.total_sessions >= 1 | |
| }, | |
| { | |
| id: 'ten-sessions', | |
| name: 'Getting Started', | |
| description: 'Complete 10 focus sessions', | |
| icon: '⭐', | |
| unlocked: data.total_sessions >= 10 | |
| }, | |
| { | |
| id: 'fifty-sessions', | |
| name: 'Dedicated', | |
| description: 'Complete 50 focus sessions', | |
| icon: '🏆', | |
| unlocked: data.total_sessions >= 50 | |
| }, | |
| { | |
| id: 'focus-master', | |
| name: 'Focus Master', | |
| description: 'Maintain 80%+ average focus score', | |
| icon: '🧠', | |
| unlocked: data.avg_focus_score >= 0.8 && data.total_sessions >= 5 | |
| }, | |
| { | |
| id: 'week-streak', | |
| name: 'Week Warrior', | |
| description: '7 day streak', | |
| icon: '🔥', | |
| unlocked: data.streak_days >= 7 | |
| }, | |
| { | |
| id: 'month-streak', | |
| name: 'Month Master', | |
| description: '30 day streak', | |
| icon: '💎', | |
| unlocked: data.streak_days >= 30 | |
| }, | |
| { | |
| id: 'ten-hours', | |
| name: 'Endurance', | |
| description: '10+ hours total focus time', | |
| icon: '⏱️', | |
| unlocked: data.total_focus_time >= 36000 | |
| }, | |
| { | |
| id: 'hundred-sessions', | |
| name: 'Centurion', | |
| description: 'Complete 100 focus sessions', | |
| icon: '👑', | |
| unlocked: data.total_sessions >= 100 | |
| } | |
| ]; | |
| setBadges(allBadges); | |
| }; | |
| return ( | |
| <main id="page-c" className="page"> | |
| <h1 className="page-title">My Achievement</h1> | |
| {loading ? ( | |
| <div style={{ textAlign: 'center', padding: '40px', color: '#888' }}> | |
| Loading stats... | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="stats-grid"> | |
| <div className="stat-card"> | |
| <div className="stat-number" id="total-sessions">{stats.total_sessions}</div> | |
| <div className="stat-label">Total Sessions</div> | |
| </div> | |
| <div className="stat-card"> | |
| <div className="stat-number" id="total-hours">{formatTime(stats.total_focus_time)}</div> | |
| <div className="stat-label">Total Focus Time</div> | |
| </div> | |
| <div className="stat-card"> | |
| <div className="stat-number" id="avg-focus">{(stats.avg_focus_score * 100).toFixed(1)}%</div> | |
| <div className="stat-label">Average Focus</div> | |
| </div> | |
| <div className="stat-card"> | |
| <div className="stat-number" id="current-streak">{stats.streak_days}</div> | |
| <div className="stat-label">Day Streak</div> | |
| </div> | |
| </div> | |
| <div className="achievements-section"> | |
| <h2>Badges</h2> | |
| <div id="badges-container" className="badges-grid"> | |
| {badges.map(badge => ( | |
| <div | |
| key={badge.id} | |
| className={`badge ${badge.unlocked ? 'unlocked' : 'locked'}`} | |
| style={{ | |
| padding: '20px', | |
| textAlign: 'center', | |
| border: '2px solid', | |
| borderColor: badge.unlocked ? '#00FF00' : '#444', | |
| borderRadius: '10px', | |
| backgroundColor: badge.unlocked ? 'rgba(0, 255, 0, 0.1)' : 'rgba(68, 68, 68, 0.1)', | |
| opacity: badge.unlocked ? 1 : 0.5, | |
| transition: 'all 0.3s' | |
| }} | |
| > | |
| <div style={{ fontSize: '48px', marginBottom: '10px' }}> | |
| {badge.unlocked ? badge.icon : '🔒'} | |
| </div> | |
| <div style={{ fontWeight: 'bold', marginBottom: '5px' }}> | |
| {badge.name} | |
| </div> | |
| <div style={{ fontSize: '12px', color: '#888' }}> | |
| {badge.description} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </main> | |
| ); | |
| } | |
| export default Achievement; | |