import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../providers/expense_provider.dart'; import '../models/transaction_model.dart'; class StatsScreen extends StatefulWidget { const StatsScreen({super.key}); @override State createState() => _StatsScreenState(); } class _StatsScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; DateTime _selectedMonth = DateTime.now(); @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('数据统计'), bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: '概览'), Tab(text: '趋势'), Tab(text: '对比'), ], ), actions: [ IconButton( icon: const Icon(Icons.calendar_month), onPressed: _pickMonth, ), ], ), body: Consumer( builder: (context, provider, child) { final transactions = provider.transactions.where((t) { return t.date.year == _selectedMonth.year && t.date.month == _selectedMonth.month; }).toList(); if (transactions.isEmpty) { return Center(child: Text('${DateFormat('yyyy年MM月').format(_selectedMonth)} 无数据')); } return TabBarView( controller: _tabController, children: [ _buildPieChart(provider, transactions), _buildLineChart(provider, transactions), _buildBarChart(provider, transactions), ], ); }, ), ); } Future _pickMonth() async { final picked = await showDatePicker( context: context, initialDate: _selectedMonth, firstDate: DateTime(2020), lastDate: DateTime(2030), helpText: '选择月份', ); if (picked != null) { setState(() { _selectedMonth = picked; }); } } Widget _buildPieChart(ExpenseProvider provider, List transactions) { final expenseTransactions = transactions.where((t) => t.type == 'expense').toList(); if (expenseTransactions.isEmpty) return const Center(child: Text('无支出数据')); final categoryTotals = {}; for (var t in expenseTransactions) { categoryTotals[t.categoryId] = (categoryTotals[t.categoryId] ?? 0) + t.amount; } final totalExpense = expenseTransactions.fold(0.0, (sum, t) => sum + t.amount); return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Text('总支出: ¥${totalExpense.toStringAsFixed(2)}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 32), SizedBox( height: 250, child: PieChart( PieChartData( sections: categoryTotals.entries.map((entry) { final category = provider.getCategoryById(entry.key); final percentage = (entry.value / totalExpense) * 100; return PieChartSectionData( color: Color(category.color), value: entry.value, title: '${percentage.toStringAsFixed(1)}%', radius: 50, titleStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white, ), ); }).toList(), sectionsSpace: 2, centerSpaceRadius: 40, ), ), ), const SizedBox(height: 20), Expanded( child: ListView.builder( itemCount: categoryTotals.length, itemBuilder: (context, index) { final categoryId = categoryTotals.keys.elementAt(index); final amount = categoryTotals[categoryId]!; final category = provider.getCategoryById(categoryId); return ListTile( leading: CircleAvatar( backgroundColor: Color(category.color), child: Icon(_getIconData(category.icon), color: Colors.white), ), title: Text(category.name), trailing: Text('¥${amount.toStringAsFixed(2)}'), ); }, ), ), ], ), ); } Widget _buildLineChart(ExpenseProvider provider, List transactions) { // Daily Expense Trend final dailyTotals = {}; final daysInMonth = DateUtils.getDaysInMonth(_selectedMonth.year, _selectedMonth.month); // Initialize all days to 0 for (int i = 1; i <= daysInMonth; i++) { dailyTotals[i] = 0.0; } for (var t in transactions.where((t) => t.type == 'expense')) { dailyTotals[t.date.day] = (dailyTotals[t.date.day] ?? 0) + t.amount; } final spots = dailyTotals.entries .map((e) => FlSpot(e.key.toDouble(), e.value)) .toList() ..sort((a, b) => a.x.compareTo(b.x)); return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ const Text('每日支出趋势', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 32), Expanded( child: LineChart( LineChartData( gridData: const FlGridData(show: false), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 5, getTitlesWidget: (value, meta) { return Text('${value.toInt()}日', style: const TextStyle(fontSize: 10)); }, ), ), leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: true, border: Border.all(color: Colors.grey.withValues(alpha: 0.2))), lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, color: Colors.blue, barWidth: 3, dotData: const FlDotData(show: false), belowBarData: BarAreaData(show: true, color: Colors.blue.withValues(alpha: 0.1)), ), ], ), ), ), ], ), ); } Widget _buildBarChart(ExpenseProvider provider, List transactions) { final income = transactions.where((t) => t.type == 'income').fold(0.0, (sum, t) => sum + t.amount); final expense = transactions.where((t) => t.type == 'expense').fold(0.0, (sum, t) => sum + t.amount); return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ const Text('收支对比', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 32), Expanded( child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: (income > expense ? income : expense) * 1.2, barTouchData: BarTouchData(enabled: false), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { switch (value.toInt()) { case 0: return const Text('收入', style: TextStyle(color: Colors.green)); case 1: return const Text('支出', style: TextStyle(color: Colors.red)); default: return const Text(''); } }, ), ), leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), barGroups: [ BarChartGroupData( x: 0, barRods: [BarChartRodData(toY: income, color: Colors.green, width: 20)], ), BarChartGroupData( x: 1, barRods: [BarChartRodData(toY: expense, color: Colors.red, width: 20)], ), ], ), ), ), ], ), ); } IconData _getIconData(String iconName) { switch (iconName) { case 'restaurant': return Icons.restaurant; case 'directions_bus': return Icons.directions_bus; case 'shopping_cart': return Icons.shopping_cart; case 'movie': return Icons.movie; case 'local_hospital': return Icons.local_hospital; case 'attach_money': return Icons.attach_money; case 'trending_up': return Icons.trending_up; case 'breakfast_dining': return Icons.breakfast_dining; case 'lunch_dining': return Icons.lunch_dining; case 'dinner_dining': return Icons.dinner_dining; case 'subway': return Icons.subway; case 'local_taxi': return Icons.local_taxi; default: return Icons.category; } } }