import 'package:flutter/material.dart'; import '../../models/record.dart'; import '../../data/categories.dart'; import 'dart:math' as math; class CategoryChart extends StatefulWidget { final List records; const CategoryChart({ Key? key, required this.records, }) : super(key: key); @override State createState() => _CategoryChartState(); } class _CategoryChartState extends State { bool _showExpense = true; // true显示支出,false显示收入 @override Widget build(BuildContext context) { final categoryData = _getCategoryData(); if (categoryData.isEmpty) return const SizedBox(); final isYearView = widget.records.any((r) => r.createTime.month != widget.records.first.createTime.month); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( '${isYearView ? "年度" : ""}分类统计', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const Spacer(), ToggleButtons( isSelected: [_showExpense, !_showExpense], onPressed: (index) { setState(() { _showExpense = index == 0; }); }, borderRadius: BorderRadius.circular(8), selectedColor: Theme.of(context).primaryColor, constraints: const BoxConstraints( minWidth: 60, minHeight: 32, ), children: const [ Text('支出'), Text('收入'), ], ), ], ), const SizedBox(height: 20), SizedBox( height: 200, child: Row( children: [ Expanded( flex: 3, child: CustomPaint( size: const Size(200, 200), painter: PieChartPainter( categories: categoryData, total: categoryData.fold( 0.0, (sum, item) => sum + item.amount, ), ), ), ), Expanded( flex: 2, child: _buildLegend(categoryData), ), ], ), ), ], ), ); } List _getCategoryData() { final Map categoryAmounts = {}; final targetType = _showExpense ? RecordType.expense : RecordType.income; // 统计各分类金额 for (final record in widget.records) { if (record.type == targetType) { categoryAmounts[record.categoryId] = (categoryAmounts[record.categoryId] ?? 0) + record.amount; } } // 转换为列表并排序 final categories = CategoryConfig.getCategoriesByType(targetType); return categoryAmounts.entries.map((entry) { final category = categories.firstWhere( (c) => c.id == entry.key, orElse: () => Category( id: 'unknown', name: '未知', iconKey: 'other', type: targetType, sortOrder: 999, ), ); return CategoryData( category: category, amount: entry.value, ); }).toList() ..sort((a, b) => b.amount.compareTo(a.amount)); } Widget _buildLegend(List data) { return ListView.builder( shrinkWrap: true, itemCount: data.length, itemBuilder: (context, index) { final item = data[index]; final total = data.fold(0.0, (sum, item) => sum + item.amount); final percentage = (item.amount / total * 100).toStringAsFixed(1); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: Colors.primaries[index % Colors.primaries.length], shape: BoxShape.circle, ), ), const SizedBox(width: 8), Expanded( child: Text( item.category.name, style: const TextStyle(fontSize: 12), ), ), Text( '$percentage%', style: const TextStyle(fontSize: 12), ), ], ), ); }, ); } } class CategoryData { final Category category; final double amount; CategoryData({ required this.category, required this.amount, }); } class PieChartPainter extends CustomPainter { final List categories; final double total; PieChartPainter({ required this.categories, required this.total, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = math.min(size.width, size.height) / 2; var startAngle = -math.pi / 2; for (var i = 0; i < categories.length; i++) { final sweepAngle = 2 * math.pi * categories[i].amount / total; final paint = Paint() ..color = Colors.primaries[i % Colors.primaries.length] ..style = PaintingStyle.fill; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint, ); startAngle += sweepAngle; } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }