2025-01-02 13:28:37 +08:00

227 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import '../../models/record.dart';
class DailyChart extends StatelessWidget {
final List<Record> 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<DailyData> _getDailyData() {
final Map<DateTime, DailyData> 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> 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;
}