feat:"初始化仓库,并更新基础代码。"

This commit is contained in:
2026-01-14 09:25:52 +08:00
commit 9a91c40a97
140 changed files with 6513 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';
import '../models/transaction_model.dart';
import '../providers/expense_provider.dart';
class AddTransactionScreen extends StatefulWidget {
const AddTransactionScreen({super.key});
@override
State<AddTransactionScreen> createState() => _AddTransactionScreenState();
}
class _AddTransactionScreenState extends State<AddTransactionScreen> {
final _formKey = GlobalKey<FormState>();
final _amountController = TextEditingController();
final _noteController = TextEditingController();
DateTime _selectedDate = DateTime.now();
String _transactionType = 'expense';
int? _selectedCategoryId;
@override
Widget build(BuildContext context) {
final provider = Provider.of<ExpenseProvider>(context);
// Flatten categories with hierarchy logic for display
final List<DropdownMenuItem<int>> categoryItems = [];
final rootCategories = provider.rootCategories.where((c) => c.type == _transactionType).toList();
for (var root in rootCategories) {
// Add Root
categoryItems.add(
DropdownMenuItem<int>(
value: root.id,
child: Row(
children: [
Icon(_getIconData(root.icon), color: Color(root.color), size: 20),
const SizedBox(width: 8),
Text(root.name, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
),
);
// Add Children
final children = provider.getSubCategories(root.id!);
for (var child in children) {
categoryItems.add(
DropdownMenuItem<int>(
value: child.id,
child: Row(
children: [
const SizedBox(width: 24), // Indent
Icon(_getIconData(child.icon), color: Color(child.color), size: 16),
const SizedBox(width: 8),
Text(child.name),
],
),
),
);
}
}
return Scaffold(
appBar: AppBar(
title: const Text('记一笔'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
// Type Selection
RadioGroup<String>(
groupValue: _transactionType,
onChanged: (value) {
setState(() {
_transactionType = value!;
_selectedCategoryId = null;
});
},
child: Row(
children: [
Expanded(
child: RadioListTile<String>(
title: const Text('支出'),
value: 'expense',
),
),
Expanded(
child: RadioListTile<String>(
title: const Text('收入'),
value: 'income',
),
),
],
),
),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '金额',
prefixText: '¥ ',
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入金额';
}
if (double.tryParse(value) == null) {
return '请输入有效的数字';
}
return null;
},
),
const SizedBox(height: 16),
// Category
DropdownButtonFormField<int>(
key: ValueKey(_selectedCategoryId),
initialValue: _selectedCategoryId,
decoration: const InputDecoration(labelText: '分类'),
items: categoryItems,
onChanged: (value) {
setState(() {
_selectedCategoryId = value;
});
},
validator: (value) {
if (value == null) {
return '请选择分类';
}
return null;
},
),
const SizedBox(height: 16),
// Date Picker
Row(
children: [
Text('日期: ${DateFormat('yyyy-MM-dd').format(_selectedDate)}'),
const Spacer(),
TextButton(
onPressed: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2100),
helpText: '选择日期',
);
if (pickedDate != null) {
setState(() {
_selectedDate = pickedDate;
});
}
},
child: const Text('选择日期'),
),
],
),
const SizedBox(height: 16),
// Note
TextFormField(
controller: _noteController,
decoration: const InputDecoration(labelText: '备注'),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _saveTransaction,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
),
child: const Text('保存', style: TextStyle(fontSize: 18)),
),
],
),
),
),
);
}
void _saveTransaction() {
if (_formKey.currentState!.validate()) {
final newTransaction = TransactionModel(
id: const Uuid().v4(),
amount: double.parse(_amountController.text),
type: _transactionType,
categoryId: _selectedCategoryId!,
date: _selectedDate,
note: _noteController.text,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
Provider.of<ExpenseProvider>(context, listen: false).addTransaction(newTransaction);
Navigator.of(context).pop();
}
}
IconData _getIconData(String iconName) {
switch (iconName) {
case 'restaurant': return Icons.restaurant;
case 'directions_bus': return Icons.directions_bus;
case 'shopping_cart': return Icons.shopping_cart;
case 'movie': return Icons.movie;
case 'local_hospital': return Icons.local_hospital;
case 'attach_money': return Icons.attach_money;
case 'trending_up': return Icons.trending_up;
case 'breakfast_dining': return Icons.breakfast_dining;
case 'lunch_dining': return Icons.lunch_dining;
case 'dinner_dining': return Icons.dinner_dining;
case 'subway': return Icons.subway;
case 'local_taxi': return Icons.local_taxi;
default: return Icons.category;
}
}
}

View File

@@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/category_model.dart';
import '../providers/expense_provider.dart';
class CategoryManagementScreen extends StatelessWidget {
const CategoryManagementScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('分类管理'),
),
body: Consumer<ExpenseProvider>(
builder: (context, provider, child) {
final rootCategories = provider.rootCategories;
return ListView.builder(
itemCount: rootCategories.length,
itemBuilder: (context, index) {
return _buildCategoryTile(context, rootCategories[index], provider);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddEditDialog(context, null, null),
child: const Icon(Icons.add),
),
);
}
Widget _buildCategoryTile(BuildContext context, Category category, ExpenseProvider provider) {
final subCategories = provider.getSubCategories(category.id!);
if (subCategories.isEmpty) {
return ListTile(
leading: CircleAvatar(
backgroundColor: Color(category.color),
child: Icon(_getIconData(category.icon), color: Colors.white, size: 20),
),
title: Text(category.name),
trailing: _buildActionButtons(context, category),
);
}
return ExpansionTile(
leading: CircleAvatar(
backgroundColor: Color(category.color),
child: Icon(_getIconData(category.icon), color: Colors.white, size: 20),
),
title: Text(category.name),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// This trailing overrides the expansion arrow, so maybe we put actions in leading or long press?
// ExpansionTile doesn't easily support custom trailing + expansion arrow.
// Let's use PopupMenuButton for actions instead.
_buildActionButtons(context, category),
const Icon(Icons.expand_more), // Visual cue, though functionality is tricky if trailing is used
],
),
children: subCategories.map((sub) => _buildCategoryTile(context, sub, provider)).toList(),
);
}
// Adjusted to use PopupMenu for cleaner UI on tiles
Widget _buildActionButtons(BuildContext context, Category category) {
return PopupMenuButton<String>(
onSelected: (value) {
if (value == 'edit') {
_showAddEditDialog(context, category, category.parentId);
} else if (value == 'delete') {
_confirmDelete(context, category);
} else if (value == 'add_sub') {
_showAddEditDialog(context, null, category.id);
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'edit',
child: Text('编辑'),
),
const PopupMenuItem<String>(
value: 'add_sub',
child: Text('添加子分类'),
),
const PopupMenuItem<String>(
value: 'delete',
child: Text('删除', style: TextStyle(color: Colors.red)),
),
],
);
}
void _confirmDelete(BuildContext context, Category category) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除分类 "${category.name}" 吗?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Provider.of<ExpenseProvider>(context, listen: false).deleteCategory(category.id!);
Navigator.of(ctx).pop();
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
);
}
void _showAddEditDialog(BuildContext context, Category? category, int? parentId) {
final isEditing = category != null;
final nameController = TextEditingController(text: isEditing ? category.name : '');
String selectedType = isEditing ? category.type : 'expense';
String selectedIcon = isEditing ? category.icon : 'category';
int selectedColor = isEditing ? category.color : 0xFF2196F3;
showDialog(
context: context,
builder: (ctx) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(isEditing ? '编辑分类' : '添加分类'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: '分类名称'),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
key: ValueKey(selectedType),
initialValue: selectedType,
items: const [
DropdownMenuItem(value: 'expense', child: Text('支出')),
DropdownMenuItem(value: 'income', child: Text('收入')),
],
onChanged: (val) {
setState(() => selectedType = val!);
},
decoration: const InputDecoration(labelText: '类型'),
),
const SizedBox(height: 16),
// Icon Picker (Simple version)
DropdownButtonFormField<String>(
key: ValueKey(selectedIcon),
initialValue: selectedIcon,
items: const [
DropdownMenuItem(value: 'restaurant', child: Icon(Icons.restaurant)),
DropdownMenuItem(value: 'directions_bus', child: Icon(Icons.directions_bus)),
DropdownMenuItem(value: 'shopping_cart', child: Icon(Icons.shopping_cart)),
DropdownMenuItem(value: 'movie', child: Icon(Icons.movie)),
DropdownMenuItem(value: 'local_hospital', child: Icon(Icons.local_hospital)),
DropdownMenuItem(value: 'attach_money', child: Icon(Icons.attach_money)),
DropdownMenuItem(value: 'trending_up', child: Icon(Icons.trending_up)),
DropdownMenuItem(value: 'category', child: Icon(Icons.category)),
DropdownMenuItem(value: 'breakfast_dining', child: Icon(Icons.breakfast_dining)),
DropdownMenuItem(value: 'lunch_dining', child: Icon(Icons.lunch_dining)),
DropdownMenuItem(value: 'dinner_dining', child: Icon(Icons.dinner_dining)),
DropdownMenuItem(value: 'subway', child: Icon(Icons.subway)),
DropdownMenuItem(value: 'local_taxi', child: Icon(Icons.local_taxi)),
],
onChanged: (val) {
setState(() => selectedIcon = val!);
},
decoration: const InputDecoration(labelText: '图标'),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
if (nameController.text.isEmpty) return;
final newCategory = Category(
id: category?.id,
name: nameController.text,
type: selectedType,
icon: selectedIcon,
color: selectedColor, // Fixed color for now or add picker
parentId: parentId,
);
if (isEditing) {
Provider.of<ExpenseProvider>(context, listen: false).updateCategory(newCategory);
} else {
Provider.of<ExpenseProvider>(context, listen: false).addCategory(newCategory);
}
Navigator.of(ctx).pop();
},
child: const Text('保存'),
),
],
);
},
);
},
);
}
IconData _getIconData(String iconName) {
switch (iconName) {
case 'restaurant': return Icons.restaurant;
case 'directions_bus': return Icons.directions_bus;
case 'shopping_cart': return Icons.shopping_cart;
case 'movie': return Icons.movie;
case 'local_hospital': return Icons.local_hospital;
case 'attach_money': return Icons.attach_money;
case 'trending_up': return Icons.trending_up;
case 'breakfast_dining': return Icons.breakfast_dining;
case 'lunch_dining': return Icons.lunch_dining;
case 'dinner_dining': return Icons.dinner_dining;
case 'subway': return Icons.subway;
case 'local_taxi': return Icons.local_taxi;
default: return Icons.category;
}
}
}

View File

@@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../providers/expense_provider.dart';
import 'add_transaction_screen.dart';
import 'stats_screen.dart';
import 'category_management_screen.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('我的记账本'),
actions: [
IconButton(
icon: const Icon(Icons.bar_chart),
tooltip: '统计',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const StatsScreen()),
);
},
),
],
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const UserAccountsDrawerHeader(
accountName: Text('用户'),
accountEmail: Text('本地用户'),
currentAccountPicture: CircleAvatar(
backgroundColor: Colors.white,
child: Icon(Icons.person, color: Colors.deepPurple),
),
decoration: BoxDecoration(
color: Colors.deepPurple,
),
),
ListTile(
leading: const Icon(Icons.category),
title: const Text('分类管理'),
onTap: () {
Navigator.pop(context); // Close drawer
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const CategoryManagementScreen()),
);
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('设置'),
onTap: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('设置功能开发中...')));
},
),
],
),
),
body: Consumer<ExpenseProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.transactions.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('暂无账单,快去记一笔吧!', style: TextStyle(color: Colors.grey)),
],
),
);
}
return Column(
children: [
_buildSummaryCard(provider),
Expanded(
child: ListView.builder(
itemCount: provider.transactions.length,
itemBuilder: (context, index) {
final transaction = provider.transactions[index];
final category = provider.getCategoryById(transaction.categoryId);
return Dismissible(
key: Key(transaction.id),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('确认删除'),
content: const Text('确定要删除这条记录吗?'),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text('删除', style: TextStyle(color: Colors.red))),
],
),
);
},
onDismissed: (direction) {
provider.deleteTransaction(transaction.id);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已删除')));
},
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Color(category.color),
child: Icon(_getIconData(category.icon), color: Colors.white),
),
title: Text(category.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(DateFormat('yyyy-MM-dd HH:mm').format(transaction.date)),
if (transaction.note != null && transaction.note!.isNotEmpty)
Text(transaction.note!, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
trailing: Text(
'${transaction.type == 'income' ? '+' : '-'}${transaction.amount.toStringAsFixed(2)}',
style: TextStyle(
color: transaction.type == 'income' ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
);
},
),
),
],
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const AddTransactionScreen()),
);
},
child: const Icon(Icons.add),
),
);
}
Widget _buildSummaryCard(ExpenseProvider provider) {
return Card(
margin: const EdgeInsets.all(16),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.deepPurple.shade400, Colors.deepPurple.shade700],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.all(20),
child: Column(
children: [
const Text('本月结余', style: TextStyle(color: Colors.white70, fontSize: 14)),
const SizedBox(height: 8),
Text(
'¥${(provider.totalIncome - provider.totalExpense).toStringAsFixed(2)}',
style: const TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.arrow_downward, color: Colors.greenAccent, size: 16),
const SizedBox(width: 4),
const Text('收入', style: TextStyle(color: Colors.white70)),
],
),
Text('¥${provider.totalIncome.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
children: [
const Icon(Icons.arrow_upward, color: Colors.redAccent, size: 16),
const SizedBox(width: 4),
const Text('支出', style: TextStyle(color: Colors.white70)),
],
),
Text('¥${provider.totalExpense.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
],
),
],
),
],
),
),
);
}
IconData _getIconData(String iconName) {
switch (iconName) {
case 'restaurant': return Icons.restaurant;
case 'directions_bus': return Icons.directions_bus;
case 'shopping_cart': return Icons.shopping_cart;
case 'movie': return Icons.movie;
case 'local_hospital': return Icons.local_hospital;
case 'attach_money': return Icons.attach_money;
case 'trending_up': return Icons.trending_up;
case 'breakfast_dining': return Icons.breakfast_dining;
case 'lunch_dining': return Icons.lunch_dining;
case 'dinner_dining': return Icons.dinner_dining;
case 'subway': return Icons.subway;
case 'local_taxi': return Icons.local_taxi;
default: return Icons.category;
}
}
}

View File

@@ -0,0 +1,282 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import '../providers/expense_provider.dart';
import '../models/transaction_model.dart';
class StatsScreen extends StatefulWidget {
const StatsScreen({super.key});
@override
State<StatsScreen> createState() => _StatsScreenState();
}
class _StatsScreenState extends State<StatsScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
DateTime _selectedMonth = DateTime.now();
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('数据统计'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: '概览'),
Tab(text: '趋势'),
Tab(text: '对比'),
],
),
actions: [
IconButton(
icon: const Icon(Icons.calendar_month),
onPressed: _pickMonth,
),
],
),
body: Consumer<ExpenseProvider>(
builder: (context, provider, child) {
final transactions = provider.transactions.where((t) {
return t.date.year == _selectedMonth.year && t.date.month == _selectedMonth.month;
}).toList();
if (transactions.isEmpty) {
return Center(child: Text('${DateFormat('yyyy年MM月').format(_selectedMonth)} 无数据'));
}
return TabBarView(
controller: _tabController,
children: [
_buildPieChart(provider, transactions),
_buildLineChart(provider, transactions),
_buildBarChart(provider, transactions),
],
);
},
),
);
}
Future<void> _pickMonth() async {
final picked = await showDatePicker(
context: context,
initialDate: _selectedMonth,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
helpText: '选择月份',
);
if (picked != null) {
setState(() {
_selectedMonth = picked;
});
}
}
Widget _buildPieChart(ExpenseProvider provider, List<TransactionModel> transactions) {
final expenseTransactions = transactions.where((t) => t.type == 'expense').toList();
if (expenseTransactions.isEmpty) return const Center(child: Text('无支出数据'));
final categoryTotals = <int, double>{};
for (var t in expenseTransactions) {
categoryTotals[t.categoryId] = (categoryTotals[t.categoryId] ?? 0) + t.amount;
}
final totalExpense = expenseTransactions.fold(0.0, (sum, t) => sum + t.amount);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text('总支出: ¥${totalExpense.toStringAsFixed(2)}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
SizedBox(
height: 250,
child: PieChart(
PieChartData(
sections: categoryTotals.entries.map((entry) {
final category = provider.getCategoryById(entry.key);
final percentage = (entry.value / totalExpense) * 100;
return PieChartSectionData(
color: Color(category.color),
value: entry.value,
title: '${percentage.toStringAsFixed(1)}%',
radius: 50,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
}).toList(),
sectionsSpace: 2,
centerSpaceRadius: 40,
),
),
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: categoryTotals.length,
itemBuilder: (context, index) {
final categoryId = categoryTotals.keys.elementAt(index);
final amount = categoryTotals[categoryId]!;
final category = provider.getCategoryById(categoryId);
return ListTile(
leading: CircleAvatar(
backgroundColor: Color(category.color),
child: Icon(_getIconData(category.icon), color: Colors.white),
),
title: Text(category.name),
trailing: Text('¥${amount.toStringAsFixed(2)}'),
);
},
),
),
],
),
);
}
Widget _buildLineChart(ExpenseProvider provider, List<TransactionModel> transactions) {
// Daily Expense Trend
final dailyTotals = <int, double>{};
final daysInMonth = DateUtils.getDaysInMonth(_selectedMonth.year, _selectedMonth.month);
// Initialize all days to 0
for (int i = 1; i <= daysInMonth; i++) {
dailyTotals[i] = 0.0;
}
for (var t in transactions.where((t) => t.type == 'expense')) {
dailyTotals[t.date.day] = (dailyTotals[t.date.day] ?? 0) + t.amount;
}
final spots = dailyTotals.entries
.map((e) => FlSpot(e.key.toDouble(), e.value))
.toList()
..sort((a, b) => a.x.compareTo(b.x));
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text('每日支出趋势', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
Expanded(
child: LineChart(
LineChartData(
gridData: const FlGridData(show: false),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 5,
getTitlesWidget: (value, meta) {
return Text('${value.toInt()}', style: const TextStyle(fontSize: 10));
},
),
),
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: true, border: Border.all(color: Colors.grey.withValues(alpha: 0.2))),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: Colors.blue,
barWidth: 3,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: true, color: Colors.blue.withValues(alpha: 0.1)),
),
],
),
),
),
],
),
);
}
Widget _buildBarChart(ExpenseProvider provider, List<TransactionModel> transactions) {
final income = transactions.where((t) => t.type == 'income').fold(0.0, (sum, t) => sum + t.amount);
final expense = transactions.where((t) => t.type == 'expense').fold(0.0, (sum, t) => sum + t.amount);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text('收支对比', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
Expanded(
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: (income > expense ? income : expense) * 1.2,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
switch (value.toInt()) {
case 0: return const Text('收入', style: TextStyle(color: Colors.green));
case 1: return const Text('支出', style: TextStyle(color: Colors.red));
default: return const Text('');
}
},
),
),
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
barGroups: [
BarChartGroupData(
x: 0,
barRods: [BarChartRodData(toY: income, color: Colors.green, width: 20)],
),
BarChartGroupData(
x: 1,
barRods: [BarChartRodData(toY: expense, color: Colors.red, width: 20)],
),
],
),
),
),
],
),
);
}
IconData _getIconData(String iconName) {
switch (iconName) {
case 'restaurant': return Icons.restaurant;
case 'directions_bus': return Icons.directions_bus;
case 'shopping_cart': return Icons.shopping_cart;
case 'movie': return Icons.movie;
case 'local_hospital': return Icons.local_hospital;
case 'attach_money': return Icons.attach_money;
case 'trending_up': return Icons.trending_up;
case 'breakfast_dining': return Icons.breakfast_dining;
case 'lunch_dining': return Icons.lunch_dining;
case 'dinner_dining': return Icons.dinner_dining;
case 'subway': return Icons.subway;
case 'local_taxi': return Icons.local_taxi;
default: return Icons.category;
}
}
}