swing_account/lib/widgets/statistics/category_chart.dart
2025-01-02 13:39:25 +08:00

225 lines
6.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../../models/record.dart';
import '../../data/categories.dart';
import 'dart:math' as math;
class CategoryChart extends StatefulWidget {
final List<Record> records;
const CategoryChart({
Key? key,
required this.records,
}) : super(key: key);
@override
State<CategoryChart> createState() => _CategoryChartState();
}
class _CategoryChartState extends State<CategoryChart> {
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<CategoryData> _getCategoryData() {
final Map<String, double> 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<CategoryData> 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<CategoryData> 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;
}