first commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/statistics_response.dart';
|
||||
import '../../../utils/amount_util.dart';
|
||||
import '../../../widgets/donut_chart.dart';
|
||||
|
||||
/// 支出分类占比环形图卡
|
||||
class CategoryPieCard extends StatelessWidget {
|
||||
const CategoryPieCard({super.key, required this.stats});
|
||||
|
||||
final StatisticsResponse stats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 只画支出分类(收入分类在下方排行列表里看)
|
||||
final items = stats.categoryStatistics
|
||||
.where((e) => e.isExpense && e.amount > 0)
|
||||
.toList()
|
||||
..sort((a, b) => b.amount.compareTo(a.amount));
|
||||
|
||||
if (items.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'支出分类占比',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
DonutChart(
|
||||
size: 150,
|
||||
centerSpaceRadius: 48,
|
||||
slices: items
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => DonutSlice(
|
||||
value: e.value.amount,
|
||||
color: chartColorAt(e.key),
|
||||
))
|
||||
.toList(),
|
||||
center: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('支出', style: TextStyle(fontSize: 11, color: Color(0xFF8D6E63))),
|
||||
Text(
|
||||
'¥${formatAmount(stats.totalExpense)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 图例最多显示前 6 项,避免色块与名称对不上号
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items
|
||||
.take(6)
|
||||
.toList()
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: chartColorAt(e.key),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
e.value.categoryName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF8D6E63),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${percentOf(e.value.amount, stats.totalExpense).toStringAsFixed(1)}%',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/statistics_response.dart';
|
||||
import '../../../utils/amount_util.dart';
|
||||
|
||||
/// 分类排行:emoji + 名称 + 进度条 + 金额 + 占比
|
||||
class CategoryRankList extends StatelessWidget {
|
||||
const CategoryRankList({super.key, required this.stats});
|
||||
|
||||
final StatisticsResponse stats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [...stats.categoryStatistics]
|
||||
..sort((a, b) => b.amount.compareTo(a.amount));
|
||||
|
||||
if (items.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'分类明细',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...items.map(_row),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _row(CategoryStat item) {
|
||||
// 分母按行内类型取:支出项用总支出、收入项用总收入
|
||||
// (uniApp 原版一律用总支出做分母,导致收入项占比显示成百分比 > 100%)
|
||||
final total = item.isExpense ? stats.totalExpense : stats.totalIncome;
|
||||
final pct = percentOf(item.amount, total);
|
||||
final barColor = item.isExpense ? const Color(0xFFC8956E) : const Color(0xFF19BE6B);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(item.categoryIcon, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.categoryName,
|
||||
style: const TextStyle(fontSize: 13.5, color: Color(0xFF5D4037)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'¥${formatAmount(item.amount)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 46,
|
||||
child: Text(
|
||||
'${pct.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF8D6E63)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(height: 6, color: const Color(0xFFF5EEE4)),
|
||||
FractionallySizedBox(
|
||||
widthFactor: (pct / 100).clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [barColor, barColor.withValues(alpha: 0.55)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user