235 lines
9.3 KiB
Dart
235 lines
9.3 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|