🎯 Personal OS · Year in Review

数据年报

你的 2025 年全景报告

由本地数据自动聚合 · 无需上传 · 完全私密

📊

选择年份,生成你的年报

年报会自动聚合来自 Focus Flow、Mood Calendar、Habit Tracker 和 5-Year Plan 的数据。
开始使用这些工具,让你的年报更加丰富。

🎯 专注打点 🌈 每日情绪 ⚡️ 习惯打卡 🗺️ 五年规划
// ─── Init year selector ─────────────────────────────────── const yearSelect = document.getElementById('year-select'); const currentYear = new Date().getFullYear(); for (let y = currentYear; y >= currentYear - 5; y--) { const opt = document.createElement('option'); opt.value = y; opt.text = `${y} 年`; if (y === currentYear) opt.selected = true; yearSelect.appendChild(opt); } // ─── Main Report Generator ──────────────────────────────── function generateReport() { const year = parseInt(yearSelect.value); document.getElementById('report-year').innerText = year; const focusLog = JSON.parse(localStorage.getItem('xyan_focus_log') || '{}'); const moodCal = JSON.parse(localStorage.getItem('xyan_mood_calendar') || '{}'); const habits = JSON.parse(localStorage.getItem('xyan_dh_habits') || '[]'); const records = JSON.parse(localStorage.getItem('xyan_dh_records') || '{}'); const goals = JSON.parse(localStorage.getItem('xyan_goal_tracker') || '[]'); const plan = JSON.parse(localStorage.getItem('xyan_5year_plan') || '{}'); // Filter data by year const yearPrefix = `${year}-`; const focusYear = Object.fromEntries(Object.entries(focusLog).filter(([k]) => k.startsWith(yearPrefix))); const moodYear = Object.fromEntries(Object.entries(moodCal).filter(([k]) => k.startsWith(yearPrefix))); const recYear = Object.fromEntries(Object.entries(records).filter(([k]) => k.startsWith(yearPrefix))); const hasData = Object.keys(focusYear).length || Object.keys(moodYear).length || Object.keys(recYear).length; if (!hasData) { document.getElementById('report-container').classList.add('hidden'); document.getElementById('empty-state').classList.remove('hidden'); return; } document.getElementById('report-container').classList.remove('hidden'); document.getElementById('empty-state').classList.add('hidden'); renderFocusStats(focusYear); renderMoodStats(moodYear); renderHabitStats(habits, recYear); render5YearReport(plan, goals, year); } // ─── Focus Stats ────────────────────────────────────────── function renderFocusStats(data) { const days = Object.values(data); const totalMins = days.reduce((s, d) => s + (d.minutes || 0), 0); const totalSessions = days.reduce((s, d) => s + (d.sessions || 0), 0); const activeDays = days.filter(d => d.minutes > 0).length; const bestDay = Math.max(0, ...days.map(d => d.minutes || 0)); const avgDaily = activeDays > 0 ? Math.round(totalMins / activeDays) : 0; document.getElementById('stat-focus-total').innerText = Math.round(totalMins / 60); document.getElementById('stat-sessions-total').innerText = totalSessions; document.getElementById('focus-best-day-mins').innerText = bestDay; document.getElementById('focus-avg-daily-mins').innerText = avgDaily; document.getElementById('focus-active-days').innerText = activeDays; // Monthly bars const monthlyMins = Array(12).fill(0); Object.entries(data).forEach(([k, v]) => { const m = parseInt(k.split('-')[1]) - 1; monthlyMins[m] += v.minutes || 0; }); const maxM = Math.max(1, ...monthlyMins); const MONTHS = ['1','2','3','4','5','6','7','8','9','10','11','12']; document.getElementById('focus-monthly-bars').innerHTML = monthlyMins.map((m, i) => { const h = Math.max(4, Math.round((m / maxM) * 100)); return `
`; }).join(''); } // ─── Mood Stats ─────────────────────────────────────────── function renderMoodStats(data) { const days = Object.values(data); document.getElementById('stat-mood-days').innerText = days.length; const counts = {5:0, 4:0, 3:0, 2:0, 1:0}; days.forEach(d => { if (d.level) counts[d.level]++; }); const total = days.length || 1; const avgScore = days.length ? (days.reduce((s, d) => s + (d.level || 3), 0) / days.length).toFixed(1) : '--'; document.getElementById('mood-avg-score').innerText = avgScore; document.getElementById('mood-avg-bar').style.width = `${(avgScore / 5) * 100}%`; const MOODS = { 5: { emoji:'😄', label:'很棒', color:'bg-yellow-400/80' }, 4: { emoji:'😊', label:'不错', color:'bg-green-400/80' }, 3: { emoji:'😐', label:'一般', color:'bg-blue-400/80' }, 2: { emoji:'😔', label:'低落', color:'bg-slate-400/80' }, 1: { emoji:'😢', label:'糟糕', color:'bg-red-400/80' }, }; document.getElementById('mood-distribution').innerHTML = [5,4,3,2,1].map(lv => { const pct = Math.round((counts[lv] / total) * 100); return `
${MOODS[lv].emoji}
${counts[lv]}天
`; }).join(''); // Dominant mood const dominant = [5,4,3,2,1].reduce((best, lv) => counts[lv] > counts[best] ? lv : best, 5); document.getElementById('mood-dominant-emoji').innerText = MOODS[dominant].emoji; document.getElementById('mood-dominant-label').innerText = `主导情绪:${MOODS[dominant].label}`; document.getElementById('mood-dominant-days').innerText = `共 ${counts[dominant]} 天`; } // ─── Habit Stats ────────────────────────────────────────── function renderHabitStats(habits, recYear) { // Calc streak for the year let maxStreak = 0, curStreak = 0; const sortedDays = Object.keys(recYear).sort(); sortedDays.forEach(day => { const rec = recYear[day] || { completed: [] }; const done = rec.completed.filter(id => habits.some(h => h.id === id)).length; if (done > 0) { curStreak++; maxStreak = Math.max(maxStreak, curStreak); } else { curStreak = 0; } }); document.getElementById('stat-habit-streak').innerText = maxStreak; if (!habits.length) { document.getElementById('habit-achievements').innerHTML = '
暂无习惯数据
'; return; } const achieved = document.getElementById('habit-achievements'); achieved.innerHTML = habits.map(h => { const doneDays = Object.values(recYear).filter(r => r.completed && r.completed.includes(h.id)).length; const totalDays = Math.max(1, Object.keys(recYear).length); const rate = Math.round((doneDays / totalDays) * 100); const colorMap = { blue:'#3b82f6', green:'#22c55e', indigo:'#6366f1', purple:'#a855f7', orange:'#f97316', rose:'#f43f5e' }; const c = colorMap[h.color] || '#6366f1'; return `
${h.icon} ${h.name}
完成 ${doneDays} 天${rate}%
`; }).join(''); } // ─── 5-Year Plan Report ─────────────────────────────────── function render5YearReport(plan, goals, year) { const yearData = plan[`y${year}`] || {}; const el = document.getElementById('fiveyear-report'); const fields = [ { key: 'goal', label: '🎯 核心目标 (The One Thing)' }, { key: 'career', label: '💼 事业' }, { key: 'life', label: '🌿 生活' }, { key: 'grow', label: '🧠 成长' }, ]; const planHTML = fields.map(f => { const val = yearData[f.key]; return val ? `
${f.label}
${val}
` : ''; }).filter(Boolean).join(''); const goalsHTML = goals.length ? `
量化目标完成度
${goals.map(g => { const pct = Math.min(100, Math.round((g.current / g.target) * 100)); const done = g.current >= g.target; return `
${g.name}
${done ? '✓' : pct + '%'}
`; }).join('')}
` : ''; el.innerHTML = planHTML || '
暂无五年规划数据,前往填写
'; if (goalsHTML) el.innerHTML += goalsHTML; } // Auto-generate on load for current year generateReport();