import 'package:flutter/material.dart'; import '../../models/record.dart'; class MonthlyChart extends StatelessWidget { final List records; const MonthlyChart({ Key? key, required this.records, }) : super(key: key); @override Widget build(BuildContext context) { // 按月份分组数据 final monthlyData = _getMonthlyData(); if (monthlyData.isEmpty) return const SizedBox(); // 计算最大值用于缩放 final maxAmount = monthlyData.fold(0.0, (max, data) { final monthMax = data.income > data.expense ? data.income : data.expense; return monthMax > max ? monthMax : max; }); 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: [ const Text( '每月收支', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), SizedBox( height: 200, child: CustomPaint( size: const Size(double.infinity, 200), painter: ChartPainter( monthlyData: monthlyData, maxAmount: maxAmount, ), ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildLegend('支出', Colors.red), const SizedBox(width: 24), _buildLegend('收入', Colors.green), ], ), ], ), ); } List _getMonthlyData() { final Map monthlyMap = {}; // 初始化12个月的数据 for (int month = 1; month <= 12; month++) { monthlyMap[month] = MonthData(month: month); } // 填充实际数据 for (final record in records) { final month = record.createTime.month; if (record.type == RecordType.expense) { monthlyMap[month]!.expense += record.amount; } else { monthlyMap[month]!.income += record.amount; } } return monthlyMap.values.toList(); } Widget _buildLegend(String label, Color color) { return Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text(label), ], ); } } class MonthData { final int month; double expense; double income; MonthData({ required this.month, this.expense = 0, this.income = 0, }); } class ChartPainter extends CustomPainter { final List monthlyData; final double maxAmount; ChartPainter({ required this.monthlyData, required this.maxAmount, }); @override void paint(Canvas canvas, Size size) { final expensePath = Path(); final incomePath = Path(); final expensePaint = Paint() ..color = Colors.red.withOpacity(0.8) ..style = PaintingStyle.stroke ..strokeWidth = 2; final incomePaint = Paint() ..color = Colors.green.withOpacity(0.8) ..style = PaintingStyle.stroke ..strokeWidth = 2; final width = size.width; final height = size.height - 20; // 留出底部空间显示月份 final monthWidth = width / 11; // 12个月,11个间隔 // 绘制网格线 final gridPaint = Paint() ..color = Colors.grey.withOpacity(0.2) ..style = PaintingStyle.stroke ..strokeWidth = 1; // 横向网格线 for (var i = 0; i <= 4; i++) { final y = height * i / 4; canvas.drawLine( Offset(0, y), Offset(width, y), gridPaint, ); } // 添加金额标签 void drawAmountLabel(double x, double y, double amount) { if (amount == 0) return; final textPainter = TextPainter( text: TextSpan( text: amount >= 1000 ? '${(amount / 1000).toStringAsFixed(1)}k' : amount.toStringAsFixed(0), style: TextStyle( color: Colors.grey[600], fontSize: 10, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, y - 15), ); } // 绘制折线和数据点 for (var i = 0; i < monthlyData.length; i++) { final x = i * monthWidth; final expenseY = height - (height * monthlyData[i].expense / maxAmount); final incomeY = height - (height * monthlyData[i].income / maxAmount); if (i == 0) { expensePath.moveTo(x, expenseY); incomePath.moveTo(x, incomeY); } else { expensePath.lineTo(x, expenseY); incomePath.lineTo(x, incomeY); } // 绘制数据点和金额标签 if (monthlyData[i].expense > 0) { canvas.drawCircle( Offset(x, expenseY), 3, Paint()..color = Colors.red, ); drawAmountLabel(x, expenseY, monthlyData[i].expense); } if (monthlyData[i].income > 0) { canvas.drawCircle( Offset(x, incomeY), 3, Paint()..color = Colors.green, ); drawAmountLabel(x, incomeY, monthlyData[i].income); } // 绘制月份 final textPainter = TextPainter( text: TextSpan( text: '${monthlyData[i].month}月', style: TextStyle( color: Colors.grey[600], fontSize: 10, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, height + 5), ); } canvas.drawPath(expensePath, expensePaint); canvas.drawPath(incomePath, incomePaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }