import { useState, useEffect, useRef, useMemo } from 'react'; import { useRouter } from 'next/router'; import { isLoggedIn, getTrendStats, getCategoryStats } from '../lib/api'; import { Chart, LineController, LineElement, PointElement, LinearScale, CategoryScale, Filler, Legend, } from 'chart.js'; import AppShell from '../components/AppShell'; import { Icon } from '../components/icons'; import { getMonthStr, formatMonth } from '../lib/utils'; import { useToast } from '../components/ToastProvider'; Chart.register( LineController, LineElement, PointElement, LinearScale, CategoryScale, Filler, Legend ); 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 [selectedCategories, setSelectedCategories] = useState([]); const [trendLoading, setTrendLoading] = useState(true); const trendChartRef = useRef(null); const trendChartInstance = useRef(null); useEffect(() => { if (!isLoggedIn()) { router.push('/login'); return; } loadTrendData(); }, [router, period, currentMonth]); useEffect(() => { if (!isLoggedIn()) return; loadCategoryData(); }, [router, currentMonth, categoryType]); 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 () => { try { const category = await getCategoryStats(getMonthStr(currentMonth), categoryType); setCategoryData(category); } catch (err) { console.error(err); showToast(err.message, { tone: 'error' }); } }; 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]); 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 handleCategoryTypeChange = (type) => { setCategoryType(type); setSelectedCategories([]); // 切换类型时清空选中 }; const toggleCategory = (categoryName) => { if (selectedCategories.includes(categoryName)) { setSelectedCategories(selectedCategories.filter(c => c !== categoryName)); } else { setSelectedCategories([...selectedCategories, categoryName]); } }; // 根据选中分类过滤数据 const filteredCategoryData = useMemo(() => { return selectedCategories.length > 0 ? categoryData.filter(cat => selectedCategories.includes(cat.category)) : categoryData; }, [selectedCategories, categoryData]); const filteredTotalAmount = useMemo(() => { return filteredCategoryData.reduce((sum, item) => sum + item.total, 0); }, [filteredCategoryData]); const headerContent = (
趋势