create project
This commit is contained in:
696
frontend/lib/screens/statistics/statistics_screen.dart
Normal file
696
frontend/lib/screens/statistics/statistics_screen.dart
Normal file
@@ -0,0 +1,696 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../providers/bill_provider.dart';
|
||||
import '../../widgets/stat_card.dart';
|
||||
import '../../constants.dart';
|
||||
import '../../theme/colors.dart';
|
||||
|
||||
class StatisticsScreen extends ConsumerStatefulWidget {
|
||||
const StatisticsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<StatisticsScreen> createState() => _StatisticsScreenState();
|
||||
}
|
||||
|
||||
class _StatisticsScreenState extends ConsumerState<StatisticsScreen> {
|
||||
int _monthOffset = 0;
|
||||
|
||||
DateTime get _selectedDate {
|
||||
final now = DateTime.now();
|
||||
return DateTime(now.year, now.month + _monthOffset, 1);
|
||||
}
|
||||
|
||||
String get _displayMonth {
|
||||
return '${_selectedDate.year}年${_selectedDate.month.toString().padLeft(2, '0')}月';
|
||||
}
|
||||
|
||||
List<Bill> _filterBillsByMonth(List<Bill> bills) {
|
||||
return bills.where((bill) {
|
||||
final billDate = DateTime.tryParse(bill.date);
|
||||
if (billDate == null) return false;
|
||||
return billDate.year == _selectedDate.year && billDate.month == _selectedDate.month;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Map<String, double> _getCategoryTotals(List<Bill> bills, bool isExpense) {
|
||||
final Map<String, double> totals = {};
|
||||
for (final bill in bills) {
|
||||
if (isExpense ? bill.isExpense : bill.isIncome) {
|
||||
totals[bill.category] = (totals[bill.category] ?? 0) + bill.amount;
|
||||
}
|
||||
}
|
||||
return totals;
|
||||
}
|
||||
|
||||
Map<int, double> _getDailyTotals(List<Bill> bills) {
|
||||
final Map<int, double> totals = {};
|
||||
for (final bill in bills) {
|
||||
if (bill.isExpense) {
|
||||
final billDate = DateTime.tryParse(bill.date);
|
||||
if (billDate != null) {
|
||||
totals[billDate.day] = (totals[billDate.day] ?? 0) + bill.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totals;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getMonthlyComparison(List<Bill> bills) {
|
||||
final now = DateTime.now();
|
||||
final List<Map<String, dynamic>> result = [];
|
||||
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
final date = DateTime(now.year, now.month - i, 1);
|
||||
final monthBills = bills.where((bill) {
|
||||
final billDate = DateTime.tryParse(bill.date);
|
||||
return billDate != null && billDate.year == date.year && billDate.month == date.month;
|
||||
}).toList();
|
||||
|
||||
double income = 0;
|
||||
double expense = 0;
|
||||
for (final bill in monthBills) {
|
||||
if (bill.isIncome) income += bill.amount;
|
||||
else expense += bill.amount;
|
||||
}
|
||||
|
||||
result.add({
|
||||
'month': '${date.year}-${date.month.toString().padLeft(2, '0')}',
|
||||
'income': income,
|
||||
'expense': expense,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final billsAsync = ref.watch(billProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.pageBg,
|
||||
body: billsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error: $e')),
|
||||
data: (bills) {
|
||||
final monthBills = _filterBillsByMonth(bills);
|
||||
|
||||
double totalIncome = 0;
|
||||
double totalExpense = 0;
|
||||
for (final bill in monthBills) {
|
||||
if (bill.isIncome) totalIncome += bill.amount;
|
||||
else totalExpense += bill.amount;
|
||||
}
|
||||
final balance = totalIncome - totalExpense;
|
||||
|
||||
final expenseCategories = _getCategoryTotals(monthBills, true);
|
||||
final incomeCategories = _getCategoryTotals(monthBills, false);
|
||||
final dailyTotals = _getDailyTotals(monthBills);
|
||||
final monthlyComparison = _getMonthlyComparison(bills);
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
_buildHeader(),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMonthSelector(),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatCards(totalIncome, totalExpense, balance),
|
||||
const SizedBox(height: 20),
|
||||
_buildPieChart(totalExpense, balance),
|
||||
const SizedBox(height: 20),
|
||||
_buildCategoryRanking(expenseCategories, true),
|
||||
const SizedBox(height: 20),
|
||||
_buildCategoryRanking(incomeCategories, false),
|
||||
const SizedBox(height: 20),
|
||||
_buildDailyBarChart(dailyTotals),
|
||||
const SizedBox(height: 20),
|
||||
_buildMonthlyComparison(monthlyComparison),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return SliverAppBar(
|
||||
expandedHeight: 100,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
backgroundColor: AppColors.brand,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: const Text(
|
||||
'统计',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
background: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.brand, AppColors.brandLight],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthSelector() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.8)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left, color: AppColors.textPrimary),
|
||||
onPressed: () => setState(() => _monthOffset--),
|
||||
),
|
||||
Text(
|
||||
_displayMonth,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right, color: AppColors.textPrimary),
|
||||
onPressed: _monthOffset < 0 ? () => setState(() => _monthOffset++) : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCards(double income, double expense, double balance) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
label: '收入',
|
||||
value: '¥${income.toStringAsFixed(2)}',
|
||||
color: AppColors.income,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
label: '支出',
|
||||
value: '¥${expense.toStringAsFixed(2)}',
|
||||
color: AppColors.expense,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
label: '结余',
|
||||
value: '¥${balance.toStringAsFixed(2)}',
|
||||
color: balance >= 0 ? AppColors.income : AppColors.expense,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPieChart(double expense, double balance) {
|
||||
final total = expense + balance;
|
||||
final usedPercent = total > 0 ? (expense / total * 100) : 0.0;
|
||||
final remainingPercent = total > 0 ? (balance / total * 100) : 0.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.8)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.brand.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'预算使用',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: total > 0
|
||||
? PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: expense,
|
||||
color: AppColors.expense,
|
||||
title: '${usedPercent.toStringAsFixed(1)}%',
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 50,
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: balance,
|
||||
color: AppColors.income,
|
||||
title: '${remainingPercent.toStringAsFixed(1)}%',
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 50,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: Text(
|
||||
'暂无数据',
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegendItem('已支出', AppColors.expense),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegendItem('剩余', AppColors.income),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendItem(String label, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryRanking(Map<String, double> categories, bool isExpense) {
|
||||
if (categories.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.8)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isExpense ? '支出分类' : '收入分类',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Center(
|
||||
child: Text(
|
||||
'暂无数据',
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sortedEntries = categories.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
final maxAmount = sortedEntries.first.value;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.8)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isExpense ? '支出分类' : '收入分类',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...sortedEntries.take(6).map((entry) {
|
||||
final emoji = AppConstants.categoryEmoji(entry.key);
|
||||
final progress = maxAmount > 0 ? entry.value / maxAmount : 0.0;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(emoji, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.key,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'¥${entry.value.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isExpense ? AppColors.expense : AppColors.income,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
isExpense ? AppColors.expense : AppColors.income,
|
||||
),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDailyBarChart(Map<int, double> dailyTotals) {
|
||||
final spots = <FlSpot>[];
|
||||
for (int day = 1; day <= 31; day++) {
|
||||
spots.add(FlSpot(day.toDouble(), dailyTotals[day] ?? 0));
|
||||
}
|
||||
|
||||
final maxY = dailyTotals.values.isEmpty
|
||||
? 100.0
|
||||
: dailyTotals.values.reduce((a, b) => a > b ? a : b) * 1.2;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.8)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'日消费趋势',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: maxY,
|
||||
barTouchData: BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
return BarTooltipItem(
|
||||
'¥${rod.toY.toStringAsFixed(2)}',
|
||||
const TextStyle(color: Colors.white, fontSize: 12),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value.toInt() % 5 == 0) {
|
||||
return Text(
|
||||
'${value.toInt()}日',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
reservedSize: 20,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
'¥${value.toInt()}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
reservedSize: 40,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: maxY / 4,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: Colors.grey.shade200,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: List.generate(31, (index) {
|
||||
final day = index + 1;
|
||||
final value = dailyTotals[day] ?? 0;
|
||||
return BarChartGroupData(
|
||||
x: day,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: value,
|
||||
color: AppColors.brand,
|
||||
width: 8,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthlyComparison(List<Map<String, dynamic>> months) {
|
||||
double maxValue = 0;
|
||||
for (final month in months) {
|
||||
final income = month['income'] as double;
|
||||
final expense = month['expense'] as double;
|
||||
if (income > maxValue) maxValue = income;
|
||||
if (expense > maxValue) maxValue = expense;
|
||||
}
|
||||
if (maxValue == 0) maxValue = 1000;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.8)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'月度对比',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...months.map((month) {
|
||||
final monthStr = month['month'] as String;
|
||||
final income = month['income'] as double;
|
||||
final expense = month['expense'] as double;
|
||||
final monthLabel = monthStr.substring(5);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'$monthLabel月',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'收¥${income.toStringAsFixed(0)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.income,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'支¥${expense.toStringAsFixed(0)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.expense,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: LinearProgressIndicator(
|
||||
value: maxValue > 0 ? income / maxValue : 0,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
valueColor: const AlwaysStoppedAnimation(AppColors.income),
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: LinearProgressIndicator(
|
||||
value: maxValue > 0 ? expense / maxValue : 0,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
valueColor: const AlwaysStoppedAnimation(AppColors.expense),
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user