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,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;
}
}
}