diff --git a/jive-flutter/lib/core/router/app_router.dart b/jive-flutter/lib/core/router/app_router.dart index 3a691b4c..7d30a3de 100644 --- a/jive-flutter/lib/core/router/app_router.dart +++ b/jive-flutter/lib/core/router/app_router.dart @@ -11,6 +11,7 @@ import 'package:jive_money/screens/home/home_screen.dart'; import 'package:jive_money/screens/dashboard/dashboard_screen.dart'; import 'package:jive_money/screens/transactions/transactions_screen.dart'; import 'package:jive_money/screens/accounts/accounts_screen.dart'; +import 'package:jive_money/screens/accounts/user_assets_screen.dart'; import 'package:jive_money/screens/budgets/budgets_screen.dart'; import 'package:jive_money/screens/settings/settings_screen.dart'; import 'package:jive_money/screens/settings/theme_settings_screen.dart'; @@ -28,6 +29,7 @@ import 'package:jive_money/providers/ledger_provider.dart'; /// 路由路径常量 class AppRoutes { + static const userAssets = '/accounts/assets'; static const splash = '/'; static const login = '/login'; static const register = '/register'; @@ -152,6 +154,10 @@ final appRouterProvider = Provider((ref) { path: AppRoutes.accounts, builder: (context, state) => const AccountsScreen(), routes: [ + GoRoute( + path: 'assets', + builder: (context, state) => const UserAssetsScreen(), + ), GoRoute( path: 'add', builder: (context, state) => const AccountAddScreen(), diff --git a/jive-flutter/lib/screens/accounts/accounts_screen.dart b/jive-flutter/lib/screens/accounts/accounts_screen.dart index c6389f95..30335b3c 100644 --- a/jive-flutter/lib/screens/accounts/accounts_screen.dart +++ b/jive-flutter/lib/screens/accounts/accounts_screen.dart @@ -27,6 +27,12 @@ class _AccountsScreenState extends ConsumerState { appBar: AppBar( title: const Text('账户管理'), actions: [ + // 资产总览快捷入口(A) + IconButton( + tooltip: '资产总览', + icon: const Icon(Icons.pie_chart_outline), + onPressed: () => context.go(AppRoutes.userAssets), + ), // 视图切换 IconButton( icon: Icon(_viewMode == 'list' ? Icons.folder : Icons.list), @@ -50,6 +56,9 @@ class _AccountsScreenState extends ConsumerState { case 'archive': _navigateToArchived(); break; + case 'assets': + context.go(AppRoutes.userAssets); + break; } }, itemBuilder: (context) => [ @@ -65,6 +74,10 @@ class _AccountsScreenState extends ConsumerState { value: 'archive', child: Text('已归档账户'), ), + const PopupMenuItem( + value: 'assets', + child: Text('资产总览'), + ), ], ), ], diff --git a/jive-flutter/lib/screens/accounts/user_assets_screen.dart b/jive-flutter/lib/screens/accounts/user_assets_screen.dart new file mode 100644 index 00000000..bd849b78 --- /dev/null +++ b/jive-flutter/lib/screens/accounts/user_assets_screen.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:jive_money/core/constants/app_constants.dart'; +import 'package:jive_money/models/account.dart'; +import 'package:jive_money/providers/account_provider.dart'; + +class UserAssetsScreen extends ConsumerWidget { + const UserAssetsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final stats = ref.watch(accountStatsProvider); + final accounts = ref.watch(accountsProvider); + final assetAccounts = accounts.where((a) => a.isAsset).toList(); + final liabilityAccounts = accounts.where((a) => a.isLiability).toList(); + + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('资产总览'), + ), + body: RefreshIndicator( + onRefresh: () async => ref.read(accountProvider.notifier).refresh(), + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildNetWorthCard(theme, stats), + const SizedBox(height: 16), + _buildSectionHeader(theme, '资产账户', Icons.account_balance_wallet), + ..._buildAccountRows(theme, assetAccounts), + const SizedBox(height: 16), + _buildSectionHeader(theme, '负债账户', Icons.credit_card), + ..._buildAccountRows(theme, liabilityAccounts), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildNetWorthCard(ThemeData theme, AccountStats stats) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('净资产', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text( + stats.netWorth.toStringAsFixed(2), + style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildKpi(theme, '总资产', stats.totalAssets, color: AppConstants.successColor), + ), + Container(width: 1, height: 24, color: theme.dividerColor.withValues(alpha: 0.5)), + Expanded( + child: _buildKpi(theme, '总负债', stats.totalLiabilities, color: AppConstants.errorColor), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildKpi(ThemeData theme, String label, double value, {required Color color}) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.6))), + const SizedBox(height: 4), + Text( + value.toStringAsFixed(2), + style: theme.textTheme.titleMedium?.copyWith(color: color, fontWeight: FontWeight.w600), + ), + ], + ), + ); + } + + Widget _buildSectionHeader(ThemeData theme, String title, IconData icon) { + return Row( + children: [ + Icon(icon, color: theme.primaryColor), + const SizedBox(width: 8), + Text(title, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + ], + ); + } + + List _buildAccountRows(ThemeData theme, List accounts) { + if (accounts.isEmpty) { + return [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text('暂无', style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor)), + ), + ]; + } + return accounts + .map((a) => ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 0), + leading: CircleAvatar( + backgroundColor: (a.displayColor).withValues(alpha: 0.15), + child: Icon(a.icon, color: a.displayColor), + ), + title: Text(a.name), + subtitle: Text(a.type.label), + trailing: Text( + a.formattedBalance, + style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + ), + )) + .toList(); + } +} diff --git a/jive-flutter/lib/screens/auth/wechat_qr_screen.dart b/jive-flutter/lib/screens/auth/wechat_qr_screen.dart index 7001607e..5da6fcad 100644 --- a/jive-flutter/lib/screens/auth/wechat_qr_screen.dart +++ b/jive-flutter/lib/screens/auth/wechat_qr_screen.dart @@ -96,7 +96,7 @@ class _WeChatQRScreenState extends State try { final userInfo = await WeChatService.simulateGetUserInfo(); final authResult = await WeChatService.simulateLogin(); - if (!context.mounted) return; + if (!mounted) return; if (userInfo != null && authResult != null) { if (widget.isLogin) { @@ -108,7 +108,8 @@ class _WeChatQRScreenState extends State }); } else { // 注册流程:跳转到注册表单 - final result = await Navigator.of(context).push( + final navigator = Navigator.of(context); + final result = await navigator.push( MaterialPageRoute( builder: (context) => WeChatRegisterFormScreen( weChatUserInfo: userInfo, @@ -117,10 +118,10 @@ class _WeChatQRScreenState extends State ), ); - if (!context.mounted) return; + if (!mounted) return; if (result != null) { - Navigator.of(context).pop(result); + navigator.pop(result); } } } diff --git a/jive-flutter/lib/screens/auth/wechat_register_form_screen.dart b/jive-flutter/lib/screens/auth/wechat_register_form_screen.dart index 9a643406..e9b09a35 100644 --- a/jive-flutter/lib/screens/auth/wechat_register_form_screen.dart +++ b/jive-flutter/lib/screens/auth/wechat_register_form_screen.dart @@ -76,6 +76,8 @@ class _WeChatRegisterFormScreenState extends State { _isLoading = true; }); + final messenger = ScaffoldMessenger.of(context); + final navigator = Navigator.of(context); try { // 首先尝试注册账户 final result = await _authService.register( @@ -87,32 +89,32 @@ class _WeChatRegisterFormScreenState extends State { if (result.success && result.userData != null) { // 注册成功后绑定微信 final bindResult = await _authService.bindWechat(); - if (!context.mounted) return; + if (!mounted) return; if (bindResult.success) { // 绑定成功,返回成功结果 - Navigator.of(context).pop({ + navigator.pop({ 'success': true, 'userData': result.userData, 'message': '微信注册成功!', }); } else { // 绑定失败但账户已创建,提示用户 - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text('账户创建成功,但微信绑定失败: ${bindResult.message}'), backgroundColor: Colors.orange, ), ); - Navigator.of(context).pop({ + navigator.pop({ 'success': true, 'userData': result.userData, 'message': '账户创建成功,请手动绑定微信', }); } } else { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text(result.message ?? '注册失败'), backgroundColor: Colors.red, @@ -120,7 +122,7 @@ class _WeChatRegisterFormScreenState extends State { ); } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text('注册过程中发生错误: $e'), backgroundColor: Colors.red, diff --git a/jive-flutter/lib/screens/dashboard/dashboard_screen.dart b/jive-flutter/lib/screens/dashboard/dashboard_screen.dart index d95d9b14..6e3455b4 100644 --- a/jive-flutter/lib/screens/dashboard/dashboard_screen.dart +++ b/jive-flutter/lib/screens/dashboard/dashboard_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:jive_money/core/router/app_router.dart'; import 'package:jive_money/ui/components/dashboard/quick_actions.dart'; import 'package:jive_money/ui/components/dashboard/account_overview.dart'; import 'package:jive_money/ui/components/dashboard/recent_transactions.dart'; @@ -34,6 +36,12 @@ class DashboardScreen extends ConsumerWidget { ], ), actions: [ + // 资产入口(C) + IconButton( + tooltip: '资产', + icon: const Icon(Icons.pie_chart_outline), + onPressed: () => context.go(AppRoutes.userAssets), + ), // 使用新的家庭切换器组件 const Padding( padding: EdgeInsets.only(right: 8), diff --git a/jive-flutter/lib/screens/family/family_activity_log_screen.dart b/jive-flutter/lib/screens/family/family_activity_log_screen.dart index fc2055a1..2dbf74eb 100644 --- a/jive-flutter/lib/screens/family/family_activity_log_screen.dart +++ b/jive-flutter/lib/screens/family/family_activity_log_screen.dart @@ -116,9 +116,14 @@ class _FamilyActivityLogScreenState Future _loadStatistics() async { try { +<<<<<<< HEAD + final statsMap = await _auditService.getActivityStatistics(familyId: widget.familyId); + setState(() => _statistics = _parseActivityStatistics(statsMap)); +======= final stats = await _auditService.getActivityStatistics(familyId: widget.familyId); setState(() => _statistics = stats); +>>>>>>> origin/main } catch (e) { // 忽略统计加载失败 } diff --git a/jive-flutter/lib/screens/theme_management_screen.dart b/jive-flutter/lib/screens/theme_management_screen.dart index d5cef867..3778ecdd 100644 --- a/jive-flutter/lib/screens/theme_management_screen.dart +++ b/jive-flutter/lib/screens/theme_management_screen.dart @@ -3,6 +3,7 @@ import 'package:jive_money/models/theme_models.dart' as models; import 'package:jive_money/services/theme_service.dart'; import 'package:jive_money/widgets/theme_preview_card.dart'; import 'package:jive_money/widgets/theme_share_dialog.dart'; +import 'package:jive_money/widgets/custom_theme_editor.dart'; /// 主题管理页面 class ThemeManagementScreen extends StatefulWidget { @@ -529,7 +530,11 @@ class _ThemeManagementScreenState extends State try { await _themeService.copyThemeToClipboard(theme.id); if (!mounted) return; +<<<<<<< HEAD + ScaffoldMessenger.of(context).showSnackBar( +======= messenger.showSnackBar( +>>>>>>> origin/main const SnackBar( content: Text('主题已复制到剪贴板'), backgroundColor: Colors.green, @@ -548,8 +553,11 @@ class _ThemeManagementScreenState extends State Future _deleteTheme(models.CustomThemeData theme) async { final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); +<<<<<<< HEAD +======= final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); +>>>>>>> origin/main final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -576,7 +584,11 @@ class _ThemeManagementScreenState extends State try { await _themeService.deleteCustomTheme(theme.id); if (!mounted) return; +<<<<<<< HEAD + ScaffoldMessenger.of(context).showSnackBar( +======= messenger.showSnackBar( +>>>>>>> origin/main SnackBar( content: Text('主题"${theme.name}"已删除'), backgroundColor: Colors.orange, @@ -703,8 +715,11 @@ class _ThemeManagementScreenState extends State Future _resetToDefault() async { final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); +<<<<<<< HEAD +======= final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); +>>>>>>> origin/main final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -730,7 +745,11 @@ class _ThemeManagementScreenState extends State if (confirmed == true) { await _themeService.resetToSystemTheme(); if (!mounted) return; +<<<<<<< HEAD + ScaffoldMessenger.of(context).showSnackBar( +======= messenger.showSnackBar( +>>>>>>> origin/main const SnackBar( content: Text('已重置为系统默认主题'), backgroundColor: Colors.green, @@ -738,4 +757,4 @@ class _ThemeManagementScreenState extends State ); } } -} +} \ No newline at end of file diff --git a/jive-flutter/lib/services/email_notification_service.dart b/jive-flutter/lib/services/email_notification_service.dart index 5632034d..e4bc6d71 100644 --- a/jive-flutter/lib/services/email_notification_service.dart +++ b/jive-flutter/lib/services/email_notification_service.dart @@ -684,6 +684,14 @@ class CategoryUsage { CategoryUsage({required this.name, required this.amount}); } + +/// Stub Address class compatible with `const Address(email, name)` usage +class Address { + final String email; + final String? name; + const Address(this.email, [this.name]); +} + /// Stub implementation for Message class class _StubMessage { dynamic from; diff --git a/jive-flutter/lib/services/family_settings_service.dart b/jive-flutter/lib/services/family_settings_service.dart index d6b4cc2f..c01e7cfa 100644 --- a/jive-flutter/lib/services/family_settings_service.dart +++ b/jive-flutter/lib/services/family_settings_service.dart @@ -178,6 +178,12 @@ class FamilySettingsService extends ChangeNotifier { switch (change.entityType) { case 'family_settings': if (change.type == ChangeType.update) { +<<<<<<< HEAD + await _familyService.updateFamilySettings(); + success = true; + } else if (change.type == ChangeType.delete) { + await _familyService.deleteFamilySettings(); +======= await _familyService.updateFamilySettings( change.entityId, FamilySettings.fromJson(change.data!).toJson(), @@ -185,6 +191,7 @@ class FamilySettingsService extends ChangeNotifier { success = true; } else if (change.type == ChangeType.delete) { await _familyService.deleteFamilySettings(change.entityId); +>>>>>>> origin/main success = true; } break;