import 'package:flutter/material.dart'; import '../../models/record.dart'; class DailyChart extends StatelessWidget { final List records; const DailyChart({ Key? key, required this.records, }) : super(key: key); @override Widget build(BuildContext context) { // 按日期分组数据 final dailyData = _getDailyData(); if (dailyData.isEmpty) return const SizedBox(); // 计算最大值用于缩放 final maxAmount = dailyData.fold(0.0, (max, data) { final dayMax = data.income > data.expense ? data.income : data.expense; return dayMax > max ? dayMax : 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( dailyData: dailyData, maxAmount: maxAmount, ), ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildLegend('支出', Colors.red), const SizedBox(width: 24), _buildLegend('收入', Colors.green), ], ), ], ), ); } List _getDailyData() { final Map dailyMap = {}; // 获取月份的第一天和最后一天 final firstRecord = records.first; final firstDay = DateTime(firstRecord.createTime.year, firstRecord.createTime.month, 1); final lastDay = DateTime(firstRecord.createTime.year, firstRecord.createTime.month + 1, 0); // 初始化每一天的数据 for (var day = firstDay; day.isBefore(lastDay.add(const Duration(days: 1))); day = day.add(const Duration(days: 1))) { dailyMap[DateTime(day.year, day.month, day.day)] = DailyData( date: day, expense: 0, income: 0, ); } // 填充实际数据 for (final record in records) { final date = DateTime(record.createTime.year, record.createTime.month, record.createTime.day); final data = dailyMap[date]!; if (record.type == RecordType.expense) { data.expense += record.amount; } else { data.income += record.amount; } } return dailyMap.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 DailyData { final DateTime date; double expense; double income; DailyData({ required this.date, this.expense = 0, this.income = 0, }); } class ChartPainter extends CustomPainter { final List dailyData; final double maxAmount; ChartPainter({ required this.dailyData, 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 dayWidth = width / (dailyData.length - 1); // 绘制网格线 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, ); } // 绘制折线 for (var i = 0; i < dailyData.length; i++) { final x = i * dayWidth; final expenseY = height - (height * dailyData[i].expense / maxAmount); final incomeY = height - (height * dailyData[i].income / maxAmount); if (i == 0) { expensePath.moveTo(x, expenseY); incomePath.moveTo(x, incomeY); } else { expensePath.lineTo(x, expenseY); incomePath.lineTo(x, incomeY); } // 绘制数据点 canvas.drawCircle( Offset(x, expenseY), 3, Paint()..color = Colors.red, ); canvas.drawCircle( Offset(x, incomeY), 3, Paint()..color = Colors.green, ); // 绘制日期 if (i % 5 == 0 || i == dailyData.length - 1) { final textPainter = TextPainter( text: TextSpan( text: '${dailyData[i].date.day}日', 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; }