import { useState, useEffect, useRef } from 'react'; import { useRouter } from 'next/router'; import { isLoggedIn, getTrendStats, getCategoryStats } from '../lib/api'; import { Chart, registerables } from 'chart.js'; import AppShell from '../components/AppShell'; import { Icon } from '../components/icons'; import { useToast } from '../components/ToastProvider'; Chart.register(...registerables); export default function Stats() { const router = useRouter(); const { showToast } = useToast(); const [period, setPeriod] = useState('month'); const [currentMonth, setCurrentMonth] = useState(new Date()); const [trendData, setTrendData] = useState([]); const [categoryData, setCategoryData] = useState([]); const [categoryType, setCategoryType] = useState('expense'); const [trendLoading, setTrendLoading] = useState(true); const [categoryLoading, setCategoryLoading] = useState(true); const trendChartRef = useRef(null); const categoryChartRef = useRef(null); const trendChartInstance = useRef(null); const categoryChartInstance = useRef(null); useEffect(() => { if (!isLoggedIn()) { router.push('/login'); return; } loadTrendData(); }, [router, period, currentMonth]); useEffect(() => { if (!isLoggedIn()) return; loadCategoryData(); }, [router, currentMonth, categoryType]); const getMonthStr = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; const formatMonth = (date) => `${date.getFullYear()}年${date.getMonth() + 1}月`; const loadTrendData = async () => { setTrendLoading(true); try { const trend = await getTrendStats(period, getMonthStr(currentMonth)); setTrendData(trend); } catch (err) { console.error(err); showToast(err.message, { tone: 'error' }); } finally { setTrendLoading(false); } }; const loadCategoryData = async () => { setCategoryLoading(true); try { const category = await getCategoryStats(getMonthStr(currentMonth), categoryType); setCategoryData(category); } catch (err) { console.error(err); showToast(err.message, { tone: 'error' }); } finally { setCategoryLoading(false); } }; useEffect(() => { if (trendChartInstance.current) { trendChartInstance.current.destroy(); } if (trendChartRef.current && trendData.length > 0) { const ctx = trendChartRef.current.getContext('2d'); trendChartInstance.current = new Chart(ctx, { type: 'line', data: { labels: trendData.map((item) => item.period), datasets: [ { label: '收入', data: trendData.map((item) => item.income), borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', fill: true, tension: 0.4, }, { label: '支出', data: trendData.map((item) => item.expense), borderColor: '#ef4444', backgroundColor: 'rgba(239, 68, 68, 0.1)', fill: true, tension: 0.4, }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', }, }, scales: { y: { beginAtZero: true, }, }, }, }); } }, [trendData]); useEffect(() => { if (categoryChartInstance.current) { categoryChartInstance.current.destroy(); } if (categoryChartRef.current && categoryData.length > 0) { const ctx = categoryChartRef.current.getContext('2d'); const colors = ['#2563eb', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1']; categoryChartInstance.current = new Chart(ctx, { type: 'doughnut', data: { labels: categoryData.map((item) => item.category), datasets: [{ data: categoryData.map((item) => item.total), backgroundColor: colors.slice(0, categoryData.length), }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', }, }, }, }); } }, [categoryData]); const prevMonth = () => { const newDate = new Date(currentMonth); newDate.setMonth(newDate.getMonth() - 1); setCurrentMonth(newDate); }; const nextMonth = () => { const newDate = new Date(currentMonth); newDate.setMonth(newDate.getMonth() + 1); setCurrentMonth(newDate); }; const totalCategoryAmount = categoryData.reduce((sum, item) => sum + item.total, 0); const headerContent = (
趋势