diff --git a/lib/data/repositories/local/local_category_repository.dart b/lib/data/repositories/local/local_category_repository.dart index 52ee609b..0f0d6306 100644 --- a/lib/data/repositories/local/local_category_repository.dart +++ b/lib/data/repositories/local/local_category_repository.dart @@ -203,9 +203,10 @@ class LocalCategoryRepository implements CategoryRepository { String? icon, int? sortOrder, }) async { - // name 全局唯一:按 name 找;有则复用,无则用给定 kind/icon/sortOrder 建。 + // Match by name + kind to prevent same-name categories across + // expense/income from being incorrectly reused (e.g. 'transfer'). final existing = await (db.select(db.categories) - ..where((c) => c.name.equals(name))) + ..where((c) => c.name.equals(name) & c.kind.equals(kind))) .get(); if (existing.isNotEmpty) return existing.first.id; return db.into(db.categories).insert(CategoriesCompanion.insert( diff --git a/lib/pages/category/category_manage_page.dart b/lib/pages/category/category_manage_page.dart index 04ccfb05..666b9e3a 100644 --- a/lib/pages/category/category_manage_page.dart +++ b/lib/pages/category/category_manage_page.dart @@ -1019,7 +1019,7 @@ class _SubcategoryDialogState extends ConsumerState<_SubcategoryDialog> { child: Center(child: CircularProgressIndicator()), ) else - GridView.builder( + ReorderableGridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -1028,10 +1028,29 @@ class _SubcategoryDialogState extends ConsumerState<_SubcategoryDialog> { mainAxisSpacing: 10, childAspectRatio: 1, ), - itemCount: (_subCategories?.length ?? 0) + 2, // 子分类 + 添加 + 编辑 + itemCount: (_subCategories?.length ?? 0) + 2, + onReorder: (oldIndex, newIndex) { + final subCategories = _subCategories; + if (subCategories == null) return; + final subCount = subCategories.length; + // Ignore drags involving the add/edit buttons + if (oldIndex >= subCount || newIndex >= subCount) return; + if (oldIndex < newIndex) newIndex -= 1; + // Optimistic local update + final reordered = List<({db.Category category, int transactionCount})>.from(subCategories); + final moved = reordered.removeAt(oldIndex); + reordered.insert(newIndex, moved); + setState(() => _subCategories = reordered); + // Persist to DB + final repo = ref.read(repositoryProvider); + final updates = reordered.asMap().entries.map((e) => ( + id: e.value.category.id, + sortOrder: e.key, + )).toList(); + repo.updateCategorySortOrders(updates); + }, itemBuilder: (context, index) { final subCategories = _subCategories ?? []; - // 添加按钮 if (index == subCategories.length) { return _DialogActionButton( onTap: widget.onAddSubCategory, @@ -1039,7 +1058,6 @@ class _SubcategoryDialogState extends ConsumerState<_SubcategoryDialog> { label: l10n.commonAdd, ); } - // 编辑按钮 if (index == subCategories.length + 1) { return _DialogActionButton( onTap: widget.onEditParentCategory, @@ -1047,9 +1065,9 @@ class _SubcategoryDialogState extends ConsumerState<_SubcategoryDialog> { label: l10n.commonEdit, ); } - // 子分类 final item = subCategories[index]; return _DialogSubCategoryCard( + key: ValueKey(item.category.id), category: item.category, transactionCount: item.transactionCount, onTap: () => widget.onSubCategoryTap(item.category), @@ -1119,6 +1137,7 @@ class _DialogSubCategoryCard extends StatelessWidget { final VoidCallback onTap; const _DialogSubCategoryCard({ + super.key, required this.category, required this.transactionCount, required this.onTap,