import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../presentation/screens/home/home_screen.dart'; import '../../presentation/screens/market/futures_market_detail_screen.dart'; import '../../presentation/screens/market/spot_market_detail_screen.dart'; import '../../presentation/screens/market/market_screen.dart'; import '../../presentation/screens/copy_trading/copy_trading_screen.dart'; import '../../presentation/screens/copy_trading/trader_apply_screen.dart'; import '../../presentation/screens/copy_trading/my_trades_screen.dart'; import '../../presentation/screens/copy_trading/trader_settings_screen.dart'; import '../../presentation/screens/copy_trading/my_copy_trading_screen.dart'; import '../../presentation/screens/copy_trading/trader_detail_screen.dart'; import '../../presentation/screens/copy_trading/follow_setting_screen.dart'; import '../../presentation/screens/placeholder_screen.dart'; import '../../presentation/screens/network_error_screen.dart'; import '../../presentation/screens/asset/asset_screen.dart'; import '../../presentation/screens/asset/deposit_screen.dart'; import '../../presentation/screens/asset/deposit_history_screen.dart'; import '../../presentation/screens/asset/withdraw_screen.dart'; import '../../presentation/screens/asset/withdraw_history_screen.dart'; import '../../presentation/screens/asset/withdraw_detail_screen.dart'; import '../../presentation/screens/asset/deposit_detail_screen.dart'; import '../../presentation/screens/asset/transfer_screen.dart'; import '../../presentation/screens/asset/transfer_history_screen.dart'; import '../../presentation/screens/asset/asset_history_screen.dart'; import '../../data/models/asset/withdraw_record.dart'; import '../../data/models/asset/recharge_record.dart'; import '../../presentation/screens/futures/futures_screen.dart'; import '../../presentation/screens/futures/futures_history_screen.dart'; import '../../presentation/screens/futures/position_detail_screen.dart'; import '../../presentation/screens/futures/order_detail_screen.dart'; import '../../presentation/screens/spot/spot_screen.dart'; import '../../presentation/screens/spot/spot_history_screen.dart'; import '../../presentation/screens/spot/spot_account_records_screen.dart'; import '../../providers/futures_provider.dart' show FuturesPosition, FuturesOrder, futuresProvider; import '../../presentation/screens/auth/login_screen.dart'; import '../../presentation/screens/auth/register_screen.dart'; import '../../presentation/screens/auth/forgot_password_screen.dart'; import '../../presentation/screens/auth/two_factor_screen.dart'; import '../../presentation/screens/auth/verify_code_screen.dart'; import '../../presentation/screens/user/change_password_screen.dart'; import '../../presentation/screens/user/fund_password_screen.dart'; import '../../presentation/screens/user/google_auth_screen.dart'; import '../../presentation/screens/user/google_auth_bind_screen.dart'; import '../../presentation/screens/user/notification_detail_screen.dart'; import '../../presentation/screens/user/notifications_screen.dart'; import '../../presentation/screens/user/profile_screen.dart'; import '../../presentation/screens/user/security_screen.dart'; import '../../presentation/screens/user/service_route_screen.dart'; import '../../presentation/screens/invite/invite_friends_screen.dart'; import '../../presentation/screens/broker/broker_apply_screen.dart'; import '../../presentation/screens/broker/broker_screen.dart'; import '../../presentation/screens/broker/my_invitations_screen.dart'; import '../../presentation/screens/broker/team_detail_screen.dart'; import '../../presentation/screens/user/help_center_screen.dart'; import '../../presentation/screens/user/help_detail_screen.dart'; import '../../presentation/screens/user/language_screen.dart'; import '../../presentation/screens/user/protocol_screen.dart'; import '../../presentation/screens/finance/finance_hub_screen.dart'; import '../../presentation/screens/finance/staking_screen.dart'; import '../../presentation/screens/common/qr_scanner_screen.dart'; import '../../presentation/widgets/common/bottom_nav_shell.dart'; import '../../providers/auth_provider.dart'; import '../../providers/asset_provider.dart'; import '../../providers/profile_provider.dart'; import '../../providers/home_provider.dart'; import '../l10n/app_localizations.dart'; import '../network/dio_client.dart' show sessionExpiredProvider; /// 需要登录的路由前缀(startsWith 匹配) const _authRequiredPrefixes = [ '/asset', '/asset/deposit', '/asset/withdraw', '/asset/transfer', '/user/account', '/user/security', '/user/kyc', '/user/api-keys', '/user/referral', '/user/messages', '/broker', '/broker/apply', '/broker/my-invitations', '/broker/team-detail', ]; /// 全局 Navigator Key,用于在非 Widget 上下文中弹出对话框 final rootNavigatorKey = GlobalKey(); // 各 Tab 分支固定 NavigatorKey,避免 StatefulShellRoute 热重载/重建时 GlobalKey 冲突 final _homeTabKey = GlobalKey(debugLabel: 'homeTab'); final _marketTabKey = GlobalKey(debugLabel: 'marketTab'); final _futuresTabKey = GlobalKey(debugLabel: 'futuresTab'); final _copyTabKey = GlobalKey(debugLabel: 'copyTab'); final _assetTabKey = GlobalKey(debugLabel: 'assetTab'); /// 路由 Provider — 监听 isLoggedInProvider 实现登录状态自动刷新路由 final appRouterProvider = Provider((ref) { final notifier = _RouterNotifier(ref); return GoRouter( navigatorKey: rootNavigatorKey, initialLocation: '/', debugLogDiagnostics: true, refreshListenable: notifier, redirect: (context, state) { final isLoggedIn = ref.read(isLoggedInProvider); final path = state.matchedLocation; // 未登录访问受保护路由 → 跳转登录 final needsAuth = _authRequiredPrefixes.any((p) => path.startsWith(p)); if (!isLoggedIn && needsAuth) { return '/login'; } // 已登录访问登录/注册页 → 跳转首页 if (isLoggedIn && (path == '/login' || path == '/register')) { return '/'; } return null; }, routes: [ // ── 带底部导航的 Tab 路由 ───────────────────────── StatefulShellRoute.indexedStack( pageBuilder: (context, state, navigationShell) { return NoTransitionPage( child: BottomNavShell(navigationShell: navigationShell), ); }, branches: [ // Tab 0: 首页 StatefulShellBranch( navigatorKey: _homeTabKey, routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), ], ), // Tab 1: 行情 StatefulShellBranch( navigatorKey: _marketTabKey, routes: [ GoRoute( path: '/market', builder: (context, state) => const MarketScreen(), ), ], ), // Tab 2: 合约 / 现货 StatefulShellBranch( navigatorKey: _futuresTabKey, initialLocation: '/spot/BTCUSDT', routes: [ GoRoute( path: '/spot/:symbol', builder: (context, state) => SpotScreen( symbol: state.pathParameters['symbol'] ?? 'BTCUSDT', ), routes: [ GoRoute( path: 'history', redirect: (context, state) { final loggedIn = ref.read(isLoggedInProvider); return loggedIn ? null : '/login'; }, builder: (context, state) => SpotHistoryScreen( symbol: state.pathParameters['symbol'] ?? 'BTCUSDT', ), ), GoRoute( path: 'records', redirect: (context, state) { final loggedIn = ref.read(isLoggedInProvider); return loggedIn ? null : '/login'; }, builder: (context, state) => SpotAccountRecordsScreen( initialSymbol: state.uri.queryParameters['symbol'], ), ), ], ), GoRoute( path: '/futures/:symbol', builder: (context, state) => FuturesScreen( symbol: state.pathParameters['symbol'] ?? 'BTCUSDT', ), routes: [ GoRoute( path: 'history', redirect: (context, state) { final loggedIn = ref.read(isLoggedInProvider); return loggedIn ? null : '/login'; }, builder: (context, state) => const FuturesHistoryScreen(), routes: [ GoRoute( path: 'position-detail', builder: (context, state) => PositionHistoryDetailScreen( data: state.extra! as Map, ), ), GoRoute( path: 'order-detail', builder: (context, state) => OrderHistoryDetailScreen( data: state.extra! as Map, ), ), ], ), GoRoute( path: 'position-detail', builder: (context, state) => PositionDetailScreen( position: state.extra! as FuturesPosition, symbol: state.pathParameters['symbol'] ?? 'BTCUSDT', ), ), GoRoute( path: 'order-detail', builder: (context, state) => OrderDetailScreen( order: state.extra! as FuturesOrder, ), ), ], ), ], ), // Tab 3: 合约 StatefulShellBranch( initialLocation: '/contracts', routes: [ GoRoute( path: '/contracts', builder: (context, state) => const FuturesScreen( symbol: 'BTCUSDT', showSpotSwitcher: false, ), routes: [ GoRoute( path: ':symbol', builder: (context, state) => FuturesScreen( symbol: state.pathParameters['symbol'] ?? 'BTCUSDT', showSpotSwitcher: false, ), ), ], ), ], ), // Tab 4: 跟单 StatefulShellBranch( navigatorKey: _copyTabKey, routes: [ GoRoute( path: '/copy-trading', builder: (context, state) => const CopyTradingScreen(), ), ], ), // Tab 5: 资产 StatefulShellBranch( navigatorKey: _assetTabKey, routes: [ GoRoute( path: '/asset', builder: (context, state) => const AssetScreen(), ), ], ), ], ), // ── 认证流程(无底部导航,使用右滑入/左滑出转场)─── GoRoute( path: '/login', pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: const LoginScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1, 0); const end = Offset.zero; final tween = Tween(begin: begin, end: end) .chain(CurveTween(curve: Curves.easeInOut)); return SlideTransition( position: animation.drive(tween), child: child); }, ), ), GoRoute( path: '/register', pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: const RegisterScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1, 0); const end = Offset.zero; final tween = Tween(begin: begin, end: end) .chain(CurveTween(curve: Curves.easeInOut)); return SlideTransition( position: animation.drive(tween), child: child); }, ), ), GoRoute( path: '/two-factor', pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: const TwoFactorScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1, 0); const end = Offset.zero; final tween = Tween(begin: begin, end: end) .chain(CurveTween(curve: Curves.easeInOut)); return SlideTransition( position: animation.drive(tween), child: child); }, ), ), GoRoute( path: '/verify-code', pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: VerifyCodeScreen(args: state.extra! as VerifyCodeArgs), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1, 0); const end = Offset.zero; final tween = Tween(begin: begin, end: end) .chain(CurveTween(curve: Curves.easeInOut)); return SlideTransition( position: animation.drive(tween), child: child); }, ), ), GoRoute( path: '/forgot-password', pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: const ForgotPasswordScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1, 0); const end = Offset.zero; final tween = Tween(begin: begin, end: end) .chain(CurveTween(curve: Curves.easeInOut)); return SlideTransition( position: animation.drive(tween), child: child); }, ), ), // ── 行情详情(无底部导航)────────────────────────── GoRoute( path: '/market/futures/:symbol', builder: (context, state) => FuturesMarketDetailScreen( symbol: state.pathParameters['symbol'] ?? 'BTCUSDT', ), ), GoRoute( path: '/market/spot/:symbol', builder: (context, state) => SpotMarketDetailScreen( symbol: state.pathParameters['symbol'] ?? 'BTCUSDT', ), ), // ── 我的跟单(无底部导航)────────────────────────── GoRoute( path: '/my-copy-trading', builder: (context, state) => const MyCopyTradingScreen(), ), GoRoute( path: '/trader-apply', builder: (context, state) => const TraderApplyScreen(), ), // ── 我的带单(带单员专用)──────────────────────────── GoRoute( path: '/my-trades', builder: (context, state) => const MyTradesScreen(), ), GoRoute( path: '/trader-settings', builder: (context, state) => const TraderSettingsScreen(), ), GoRoute( path: '/trader-detail/:traderId', builder: (context, state) => TraderDetailScreen( traderId: state.pathParameters['traderId'] ?? '', ), ), GoRoute( path: '/follow-setting', builder: (context, state) => FollowSettingScreen( trader: state.extra! as Map, ), ), // ── 资产操作(无底部导航)────────────────────────── GoRoute( path: '/asset/deposit', builder: (context, state) => const DepositScreen(), ), GoRoute( path: '/asset/deposit/history', builder: (context, state) => const DepositHistoryScreen(), ), GoRoute( path: '/asset/deposit/detail', builder: (context, state) { final record = state.extra as RechargeRecord; return DepositDetailScreen(record: record); }, ), GoRoute( path: '/asset/withdraw', builder: (context, state) => const WithdrawScreen(), ), GoRoute( path: '/asset/withdraw/history', builder: (context, state) => const WithdrawHistoryScreen(), ), GoRoute( path: '/asset/withdraw/detail', builder: (context, state) { final record = state.extra as WithdrawRecord; final isTransfer = state.uri.queryParameters['isTransfer'] == 'true'; return WithdrawDetailScreen(record: record, isTransfer: isTransfer); }, ), GoRoute( path: '/asset/transfer', builder: (context, state) { final qp = state.uri.queryParameters; return TransferScreen( initialFrom: qp['from'], initialTo: qp['to'], initialSymbol: qp['symbol'], preferDefaultSymbol: qp['preferSymbol'] == '1', spotTradingBridgeOnly: qp['bridgeOnly'] == '1', ); }, ), GoRoute( path: '/asset/transfer/history', builder: (context, state) => const TransferHistoryScreen(), ), GoRoute( path: '/asset/history', builder: (context, state) => const AssetHistoryScreen(), ), GoRoute( path: '/qr-scanner', builder: (context, state) => const QrScannerScreen(), ), // ── 个人中心(无底部导航)────────────────────────── GoRoute( path: '/user', builder: (context, state) => const ProfileScreen(), ), GoRoute( path: '/user/account', builder: (context, state) => const PlaceholderScreen(title: '账户设置', icon: Icons.settings), ), GoRoute( path: '/user/security', builder: (context, state) => const SecurityScreen(), ), GoRoute( path: '/user/security/google-auth', builder: (context, state) => const GoogleAuthScreen(), ), GoRoute( path: '/user/security/google-auth/bind', builder: (context, state) => const GoogleAuthBindScreen(), ), GoRoute( path: '/user/security/fund-password', builder: (context, state) { final isReset = state.extra as bool? ?? false; return FundPasswordScreen(isResetMode: isReset); }, ), GoRoute( path: '/user/security/change-password', builder: (context, state) => const ChangePasswordScreen(), ), GoRoute( path: '/user/kyc', builder: (context, state) => const PlaceholderScreen(title: '身份认证', icon: Icons.verified_user), ), GoRoute( path: '/user/api-keys', builder: (context, state) => const PlaceholderScreen(title: 'API 管理', icon: Icons.key), ), GoRoute( path: '/user/referral', builder: (context, state) => const InviteFriendsScreen(), ), GoRoute( path: '/user/messages', builder: (context, state) => const NotificationsScreen(), ), GoRoute( path: '/user/messages/:id', builder: (context, state) => NotificationDetailScreen( id: state.pathParameters['id'] ?? '', ), ), GoRoute( path: '/user/service-route', builder: (context, state) => const ServiceRouteScreen(), ), GoRoute( path: '/user/language', builder: (context, state) => const LanguageScreen(), ), GoRoute( path: '/user/help', builder: (context, state) => const HelpCenterScreen(), ), GoRoute( path: '/user/help/:id', builder: (context, state) => HelpDetailScreen( id: state.pathParameters['id'] ?? '', ), ), GoRoute( path: '/protocol', builder: (context, state) { final args = state.extra as ProtocolArgs?; return ProtocolScreen( title: args?.title ?? '', categoryCode: args?.categoryCode ?? 'PROTOCOL', ); }, ), GoRoute( path: '/finance/ido', builder: (context, state) { final tab = state.uri.queryParameters['tab']; final initialTab = tab == 'airdrop' ? 1 : 0; return FinanceHubScreen(initialTab: initialTab); }, ), GoRoute( path: '/finance/stake/:configId', builder: (context, state) => StakingScreen( configId: state.pathParameters['configId'], ), ), GoRoute( path: '/finance/airdrop', builder: (context, state) => const FinanceHubScreen(initialTab: 1), ), GoRoute( path: '/user/about', builder: (context, state) => const PlaceholderScreen(title: '关于', icon: Icons.info), ), // ── 经纪商 ──────────────────────────────────────── GoRoute( path: '/broker', builder: (context, state) => const BrokerScreen(), ), GoRoute( path: '/broker/apply', builder: (context, state) => const BrokerApplyScreen(), ), GoRoute( path: '/broker/my-invitations', builder: (context, state) => const MyInvitationsScreen(), ), GoRoute( path: '/broker/team-detail', builder: (context, state) => const TeamDetailScreen(), ), // ── 网络加载失败页 ──────────────────────────────── GoRoute( path: '/network-error', builder: (context, state) { final extra = state.extra as Map?; return NetworkErrorScreen( title: extra?['title'] as String? ?? '行情', errorCode: extra?['errorCode'] as String?, onRetry: extra?['onRetry'] as VoidCallback?, ); }, ), // ── Deep Link 路径映射 ──────────────────────────── GoRoute( path: '/f/:symbol', redirect: (context, state) { final symbol = state.pathParameters['symbol'] ?? 'BTCUSDT'; return '/futures/$symbol'; }, ), ], ); }); /// 桥接 Riverpod → GoRouter refreshListenable class _RouterNotifier extends ChangeNotifier { /// true 时跳过 isLoggedInProvider 变化引发的 notifyListeners, /// 用于 session 过期场景:由 dialog 按钮统一执行导航,避免双重导航断言。 bool _suppressLoggedOutNotify = false; /// 保存 ref,供 dialog 按钮回调中延迟 invalidate 使用 late final Ref _ref; _RouterNotifier(Ref ref) { _ref = ref; ref.listen(isLoggedInProvider, (prev, next) { if (prev == true && next == false) { if (_suppressLoggedOutNotify) return; // 主动登出:先触发 redirect 导航,下一帧再 invalidate provider, // 避免 invalidate rebuild 与 GoRouter navigation 并发导致 Duplicate GlobalKey 崩溃 notifyListeners(); // 下一帧再 invalidate:profile 由 isLoggedIn 监听同步降为访客, // 另行 invalidate 易与_uc/member/my-info 等在途请求收尾并发,触发 element 断言。 WidgetsBinding.instance.addPostFrameCallback((_) { ref.invalidate(assetProvider); ref.invalidate(homeProvider); ref.invalidate(futuresProvider); }); } // 登录:导航由页面显式 context.go('/') 处理,不在此触发 notifyListeners, // 避免与 context.go 并发导致 element._lifecycleState 断言失败。 }); ref.listen(sessionExpiredProvider, (_, expired) { if (expired) { ref.read(sessionExpiredProvider.notifier).state = false; // 阻止 isLoggedInProvider 监听器触发 notifyListeners(→ redirect), // 导航由 dialog 按钮的 context.go('/login') 统一处理,避免双重导航。 _suppressLoggedOutNotify = true; // 只清本地登录态,不请求 logout 接口(token 已失效,请求也会报 4000) ref.read(authProvider.notifier).clearLocalSession(); _suppressLoggedOutNotify = false; // 下一帧再弹 dialog,此时 clearLocalSession 的 rebuild 已完成; // provider invalidate 推迟到用户点击确认后导航完成再执行, // 确保 navigation 与 invalidate 完全不在同一帧 WidgetsBinding.instance.addPostFrameCallback((_) { _showSessionExpiredDialog(); }); } }); } void _showSessionExpiredDialog() { final ctx = rootNavigatorKey.currentContext; if (ctx == null) return; final cs = Theme.of(ctx).colorScheme; final l10n = AppLocalizations.of(ctx)!; showDialog( context: ctx, barrierDismissible: false, builder: (dialogCtx) => AlertDialog( backgroundColor: cs.surface, title: Text(l10n.sessionExpiredTitle, style: TextStyle(color: cs.onSurface)), content: Text( l10n.sessionExpiredContent, style: TextStyle(color: cs.onSurface.withAlpha(153)), ), actions: [ TextButton( onPressed: () { Navigator.of(dialogCtx).pop(); rootNavigatorKey.currentContext?.go('/login'); // 导航完成后下一帧再 invalidate,避免与 navigation 并发 WidgetsBinding.instance.addPostFrameCallback((_) { _ref.invalidate(assetProvider); _ref.invalidate(profileProvider); _ref.invalidate(homeProvider); _ref.invalidate(futuresProvider); }); }, child: Text(l10n.relogin, style: TextStyle(color: cs.onSurface)), ), ], ), ); } }