feat:"初始化仓库,并更新基础代码。"
This commit is contained in:
234
lib/screens/category_management_screen.dart
Normal file
234
lib/screens/category_management_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user