|
| 1 | +import { useState, useEffect } from 'react'; |
| 2 | +import { Target, TrendingUp, CheckCircle2 } from 'lucide-react'; |
| 3 | +import toast from 'react-hot-toast'; |
| 4 | +import { useAuth } from '../context/AuthContext'; |
| 5 | + |
| 6 | +const GOAL_KEY = 'cj_daily_goal'; |
| 7 | + |
| 8 | +export default function DailyGoal() { |
| 9 | + const { user } = useAuth(); |
| 10 | + const [goal, setGoal] = useState(() => { |
| 11 | + return Number(localStorage.getItem(GOAL_KEY)) || 5; |
| 12 | + }); |
| 13 | + const [editing, setEditing] = useState(false); |
| 14 | + const [inputValue, setInputValue] = useState(String(goal)); |
| 15 | + const [todaySolved, setTodaySolved] = useState(0); |
| 16 | + |
| 17 | + useEffect(() => { |
| 18 | + if (!user) return; |
| 19 | + const token = localStorage.getItem('oj_token'); |
| 20 | + if (!token) return; |
| 21 | + const today = new Date().toISOString().slice(0, 10); |
| 22 | + fetch('/api/auth/solved-calendar', { headers: { Authorization: `Bearer ${token}` } }) |
| 23 | + .then(r => r.json()) |
| 24 | + .then(data => { |
| 25 | + const dates = data.dates || []; |
| 26 | + setTodaySolved(dates.filter((d: string) => d === today).length); |
| 27 | + }) |
| 28 | + .catch(() => {}); |
| 29 | + }, [user]); |
| 30 | + |
| 31 | + const saveGoal = () => { |
| 32 | + const n = parseInt(inputValue); |
| 33 | + if (isNaN(n) || n < 1) { toast.error('请输入有效数字'); return; } |
| 34 | + setGoal(n); |
| 35 | + localStorage.setItem(GOAL_KEY, String(n)); |
| 36 | + setEditing(false); |
| 37 | + toast.success(`每日目标已设为 ${n} 题`); |
| 38 | + }; |
| 39 | + |
| 40 | + if (!user) return null; |
| 41 | + |
| 42 | + const progress = Math.min(100, (todaySolved / goal) * 100); |
| 43 | + const remaining = Math.max(0, goal - todaySolved); |
| 44 | + |
| 45 | + return ( |
| 46 | + <div className="card p-6"> |
| 47 | + <div className="flex items-center gap-3 mb-4"> |
| 48 | + <Target className="w-5 h-5 text-primary-400" /> |
| 49 | + <h3 className="text-lg font-semibold text-white">每日目标</h3> |
| 50 | + </div> |
| 51 | + |
| 52 | + {editing ? ( |
| 53 | + <div className="flex items-center gap-2 mb-3"> |
| 54 | + <input |
| 55 | + type="number" |
| 56 | + min="1" |
| 57 | + max="100" |
| 58 | + value={inputValue} |
| 59 | + onChange={e => setInputValue(e.target.value)} |
| 60 | + className="input w-20 text-center" |
| 61 | + autoFocus |
| 62 | + onKeyDown={e => e.key === 'Enter' && saveGoal()} |
| 63 | + /> |
| 64 | + <span className="text-dark-400 text-sm">题/天</span> |
| 65 | + <button onClick={saveGoal} className="btn-primary text-xs px-3 py-1.5">确定</button> |
| 66 | + <button onClick={() => setEditing(false)} className="btn-ghost text-xs px-3 py-1.5">取消</button> |
| 67 | + </div> |
| 68 | + ) : ( |
| 69 | + <div className="flex items-center justify-between mb-3"> |
| 70 | + <div className="flex items-center gap-2"> |
| 71 | + <TrendingUp className="w-4 h-4 text-emerald-400" /> |
| 72 | + <span className="text-2xl font-bold text-white">{todaySolved}</span> |
| 73 | + <span className="text-dark-400 text-sm">/ {goal}</span> |
| 74 | + </div> |
| 75 | + <button onClick={() => { setInputValue(String(goal)); setEditing(true); }} className="text-xs text-dark-400 hover:text-white transition-colors"> |
| 76 | + 修改 |
| 77 | + </button> |
| 78 | + </div> |
| 79 | + )} |
| 80 | + |
| 81 | + {/* Progress bar */} |
| 82 | + <div className="h-2.5 bg-dark-700 rounded-full overflow-hidden"> |
| 83 | + <div |
| 84 | + className={`h-full rounded-full transition-all duration-500 ${ |
| 85 | + progress >= 100 ? 'bg-emerald-500' : 'bg-primary-500' |
| 86 | + }`} |
| 87 | + style={{ width: `${progress}%` }} |
| 88 | + /> |
| 89 | + </div> |
| 90 | + |
| 91 | + <div className="flex items-center justify-between mt-2"> |
| 92 | + {progress >= 100 ? ( |
| 93 | + <span className="flex items-center gap-1 text-emerald-400 text-xs"> |
| 94 | + <CheckCircle2 className="w-3.5 h-3.5" /> 今日目标已完成! |
| 95 | + </span> |
| 96 | + ) : ( |
| 97 | + <span className="text-dark-400 text-xs">还差 {remaining} 题完成今日目标</span> |
| 98 | + )} |
| 99 | + </div> |
| 100 | + </div> |
| 101 | + ); |
| 102 | +} |
0 commit comments