import { useState, useEffect, useRef, useMemo } 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 [selectedCategories, setSelectedCategories] = useState([]); const [trendLoading, setTrendLoading] = useState(true); const [categoryLoading, setCategoryLoading] = 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 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]); 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 = (
{formatMonth(currentMonth)}
); return (

趋势

收支趋势

{trendLoading ?
加载中...
: }

分类明细

{/* 总额显示 */}
{categoryType === 'expense' ? '支出' : '收入'} ¥{filteredTotalAmount.toFixed(2)}
{/* 分类选择chips */}
{categoryData.map((cat) => ( ))}
{filteredCategoryData.length > 0 ? filteredCategoryData.map((cat, index) => { const percent = filteredTotalAmount ? ((cat.total / filteredTotalAmount) * 100).toFixed(1) : '0.0'; return ( ); }) :
暂无数据
}
); }