229 lines
7.3 KiB
Dart
229 lines
7.3 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|