Files
Aq-Accounting-Flutter/lib/pages/bill/widgets/budget_card.dart
T
2026-09-23 15:33:59 +08:00

142 lines
4.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/budget.dart';
import '../../../utils/amount_util.dart';
import '../../../widgets/donut_chart.dart';
import '../../../widgets/state_views.dart';
/// 本月预算卡:环形图 + 预算 / 剩余 / 日均可用
class BudgetCard extends StatelessWidget {
const BudgetCard({super.key, required this.budget});
final AsyncValue<BudgetInfo> budget;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: budget.when(
loading: () => const LoadingView(tip: '预算加载中...', padding: EdgeInsets.all(20)),
error: (e, _) => ErrorView(message: '预算加载失败'),
data: (info) => info.hasBudget ? _content(info) : _noBudget(),
),
);
}
Widget _noBudget() {
return const EmptyView(
icon: '🎯',
title: '本月未设置预算',
tip: '可在「我的 → 预算设置」中设置',
padding: EdgeInsets.symmetric(vertical: 20),
);
}
Widget _content(BudgetInfo info) {
final over = info.isOverBudget;
final ringColor = over ? const Color(0xFFE53935) : const Color(0xFFC8956E);
final usedRatio = info.usedRatio;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 12),
child: Text(
'本月预算',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5D4037)),
),
),
Row(
children: [
DonutChart(
size: 116,
centerSpaceRadius: 38,
slices: [
DonutSlice(value: usedRatio, color: ringColor),
DonutSlice(value: 1 - usedRatio, color: const Color(0xFFF0E6D8)),
],
center: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
over ? '超支' : '${(usedRatio * 100).round()}%',
style: TextStyle(
fontSize: over ? 14 : 17,
fontWeight: FontWeight.bold,
color: over ? const Color(0xFFE53935) : const Color(0xFF5D4037),
),
),
if (!over)
const Text('已用', style: TextStyle(fontSize: 10, color: Color(0xFF8D6E63))),
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_amountRow('预算总额', '¥${formatAmount(info.amount)}', const Color(0xFF5D4037)),
const SizedBox(height: 8),
_amountRow('已用', '¥${formatAmount(info.usedAmount)}', ringColor),
const SizedBox(height: 8),
_amountRow(
'剩余',
'¥${formatAmount(info.remainingAmount)}',
info.remainingAmount < 0 ? const Color(0xFFE53935) : const Color(0xFF19BE6B),
),
],
),
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0x33C8956E), Color(0x33FFD700)],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFC8956E)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('💰 日均可用 ', style: TextStyle(fontSize: 13, color: Color(0xFF6D4C41))),
Text(
'¥${formatAmount(info.remainingDaily)}',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF5D4037),
),
),
],
),
),
],
);
}
Widget _amountRow(String label, String value, Color valueColor) {
return Row(
children: [
Text(label, style: const TextStyle(fontSize: 12.5, color: Color(0xFF8D6E63))),
const Spacer(),
Text(
value,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: valueColor),
),
],
);
}
}