app_router.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../presentation/screens/home/home_screen.dart';
  5. import '../../presentation/screens/market/futures_market_detail_screen.dart';
  6. import '../../presentation/screens/market/spot_market_detail_screen.dart';
  7. import '../../presentation/screens/market/market_screen.dart';
  8. import '../../presentation/screens/copy_trading/copy_trading_screen.dart';
  9. import '../../presentation/screens/copy_trading/trader_apply_screen.dart';
  10. import '../../presentation/screens/copy_trading/my_trades_screen.dart';
  11. import '../../presentation/screens/copy_trading/trader_settings_screen.dart';
  12. import '../../presentation/screens/copy_trading/my_copy_trading_screen.dart';
  13. import '../../presentation/screens/copy_trading/trader_detail_screen.dart';
  14. import '../../presentation/screens/copy_trading/follow_setting_screen.dart';
  15. import '../../presentation/screens/placeholder_screen.dart';
  16. import '../../presentation/screens/network_error_screen.dart';
  17. import '../../presentation/screens/asset/asset_screen.dart';
  18. import '../../presentation/screens/asset/deposit_screen.dart';
  19. import '../../presentation/screens/asset/deposit_history_screen.dart';
  20. import '../../presentation/screens/asset/withdraw_screen.dart';
  21. import '../../presentation/screens/asset/withdraw_history_screen.dart';
  22. import '../../presentation/screens/asset/withdraw_detail_screen.dart';
  23. import '../../presentation/screens/asset/deposit_detail_screen.dart';
  24. import '../../presentation/screens/asset/transfer_screen.dart';
  25. import '../../presentation/screens/asset/transfer_history_screen.dart';
  26. import '../../presentation/screens/asset/asset_history_screen.dart';
  27. import '../../data/models/asset/withdraw_record.dart';
  28. import '../../data/models/asset/recharge_record.dart';
  29. import '../../presentation/screens/futures/futures_screen.dart';
  30. import '../../presentation/screens/futures/futures_history_screen.dart';
  31. import '../../presentation/screens/futures/position_detail_screen.dart';
  32. import '../../presentation/screens/futures/order_detail_screen.dart';
  33. import '../../presentation/screens/spot/spot_screen.dart';
  34. import '../../presentation/screens/spot/spot_history_screen.dart';
  35. import '../../presentation/screens/spot/spot_account_records_screen.dart';
  36. import '../../providers/futures_provider.dart'
  37. show FuturesPosition, FuturesOrder, futuresProvider;
  38. import '../../presentation/screens/auth/login_screen.dart';
  39. import '../../presentation/screens/auth/register_screen.dart';
  40. import '../../presentation/screens/auth/forgot_password_screen.dart';
  41. import '../../presentation/screens/auth/two_factor_screen.dart';
  42. import '../../presentation/screens/auth/verify_code_screen.dart';
  43. import '../../presentation/screens/user/change_password_screen.dart';
  44. import '../../presentation/screens/user/fund_password_screen.dart';
  45. import '../../presentation/screens/user/google_auth_screen.dart';
  46. import '../../presentation/screens/user/google_auth_bind_screen.dart';
  47. import '../../presentation/screens/user/notification_detail_screen.dart';
  48. import '../../presentation/screens/user/notifications_screen.dart';
  49. import '../../presentation/screens/user/profile_screen.dart';
  50. import '../../presentation/screens/user/security_screen.dart';
  51. import '../../presentation/screens/user/service_route_screen.dart';
  52. import '../../presentation/screens/invite/invite_friends_screen.dart';
  53. import '../../presentation/screens/broker/broker_apply_screen.dart';
  54. import '../../presentation/screens/broker/broker_screen.dart';
  55. import '../../presentation/screens/broker/my_invitations_screen.dart';
  56. import '../../presentation/screens/broker/team_detail_screen.dart';
  57. import '../../presentation/screens/user/help_center_screen.dart';
  58. import '../../presentation/screens/user/help_detail_screen.dart';
  59. import '../../presentation/screens/user/language_screen.dart';
  60. import '../../presentation/screens/user/protocol_screen.dart';
  61. import '../../presentation/screens/finance/finance_hub_screen.dart';
  62. import '../../presentation/screens/finance/staking_screen.dart';
  63. import '../../presentation/screens/finance/staking_team_screen.dart';
  64. import '../../presentation/screens/common/qr_scanner_screen.dart';
  65. import '../../presentation/widgets/common/bottom_nav_shell.dart';
  66. import '../../providers/auth_provider.dart';
  67. import '../../providers/asset_provider.dart';
  68. import '../../providers/profile_provider.dart';
  69. import '../../providers/home_provider.dart';
  70. import '../l10n/app_localizations.dart';
  71. import '../network/dio_client.dart' show sessionExpiredProvider;
  72. /// 需要登录的路由前缀(startsWith 匹配)
  73. const _authRequiredPrefixes = [
  74. '/asset',
  75. '/asset/deposit',
  76. '/asset/withdraw',
  77. '/asset/transfer',
  78. '/user/account',
  79. '/user/security',
  80. '/user/kyc',
  81. '/user/api-keys',
  82. '/user/referral',
  83. '/user/messages',
  84. '/broker',
  85. '/broker/apply',
  86. '/broker/my-invitations',
  87. '/broker/team-detail',
  88. ];
  89. /// 全局 Navigator Key,用于在非 Widget 上下文中弹出对话框
  90. final rootNavigatorKey = GlobalKey<NavigatorState>();
  91. // 各 Tab 分支固定 NavigatorKey,避免 StatefulShellRoute 热重载/重建时 GlobalKey 冲突
  92. final _homeTabKey = GlobalKey<NavigatorState>(debugLabel: 'homeTab');
  93. final _marketTabKey = GlobalKey<NavigatorState>(debugLabel: 'marketTab');
  94. final _futuresTabKey = GlobalKey<NavigatorState>(debugLabel: 'futuresTab');
  95. final _copyTabKey = GlobalKey<NavigatorState>(debugLabel: 'copyTab');
  96. final _assetTabKey = GlobalKey<NavigatorState>(debugLabel: 'assetTab');
  97. /// 路由 Provider — 监听 isLoggedInProvider 实现登录状态自动刷新路由
  98. final appRouterProvider = Provider<GoRouter>((ref) {
  99. final notifier = _RouterNotifier(ref);
  100. return GoRouter(
  101. navigatorKey: rootNavigatorKey,
  102. initialLocation: '/',
  103. debugLogDiagnostics: true,
  104. refreshListenable: notifier,
  105. redirect: (context, state) {
  106. final isLoggedIn = ref.read(isLoggedInProvider);
  107. final path = state.matchedLocation;
  108. // 未登录访问受保护路由 → 跳转登录
  109. final needsAuth = _authRequiredPrefixes.any((p) => path.startsWith(p));
  110. if (!isLoggedIn && needsAuth) {
  111. return '/login';
  112. }
  113. // 已登录访问登录/注册页 → 跳转首页
  114. if (isLoggedIn && (path == '/login' || path == '/register')) {
  115. return '/';
  116. }
  117. return null;
  118. },
  119. routes: [
  120. // ── 带底部导航的 Tab 路由 ─────────────────────────
  121. StatefulShellRoute.indexedStack(
  122. pageBuilder: (context, state, navigationShell) {
  123. return NoTransitionPage(
  124. child: BottomNavShell(navigationShell: navigationShell),
  125. );
  126. },
  127. branches: [
  128. // Tab 0: 首页
  129. StatefulShellBranch(
  130. navigatorKey: _homeTabKey,
  131. routes: [
  132. GoRoute(
  133. path: '/',
  134. builder: (context, state) => const HomeScreen(),
  135. ),
  136. ],
  137. ),
  138. // Tab 1: 行情
  139. StatefulShellBranch(
  140. navigatorKey: _marketTabKey,
  141. routes: [
  142. GoRoute(
  143. path: '/market',
  144. builder: (context, state) => const MarketScreen(),
  145. ),
  146. ],
  147. ),
  148. // Tab 2: 合约 / 现货
  149. StatefulShellBranch(
  150. navigatorKey: _futuresTabKey,
  151. initialLocation: '/spot/BTCUSDT',
  152. routes: [
  153. GoRoute(
  154. path: '/spot/:symbol',
  155. builder: (context, state) => SpotScreen(
  156. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  157. ),
  158. routes: [
  159. GoRoute(
  160. path: 'history',
  161. redirect: (context, state) {
  162. final loggedIn = ref.read(isLoggedInProvider);
  163. return loggedIn ? null : '/login';
  164. },
  165. builder: (context, state) => SpotHistoryScreen(
  166. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  167. ),
  168. ),
  169. GoRoute(
  170. path: 'records',
  171. redirect: (context, state) {
  172. final loggedIn = ref.read(isLoggedInProvider);
  173. return loggedIn ? null : '/login';
  174. },
  175. builder: (context, state) => SpotAccountRecordsScreen(
  176. initialSymbol: state.uri.queryParameters['symbol'],
  177. ),
  178. ),
  179. ],
  180. ),
  181. GoRoute(
  182. path: '/futures/:symbol',
  183. builder: (context, state) => FuturesScreen(
  184. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  185. ),
  186. routes: [
  187. GoRoute(
  188. path: 'history',
  189. redirect: (context, state) {
  190. final loggedIn = ref.read(isLoggedInProvider);
  191. return loggedIn ? null : '/login';
  192. },
  193. builder: (context, state) => const FuturesHistoryScreen(),
  194. routes: [
  195. GoRoute(
  196. path: 'position-detail',
  197. builder: (context, state) =>
  198. PositionHistoryDetailScreen(
  199. data: state.extra! as Map<String, dynamic>,
  200. ),
  201. ),
  202. GoRoute(
  203. path: 'order-detail',
  204. builder: (context, state) => OrderHistoryDetailScreen(
  205. data: state.extra! as Map<String, dynamic>,
  206. ),
  207. ),
  208. ],
  209. ),
  210. GoRoute(
  211. path: 'position-detail',
  212. builder: (context, state) => PositionDetailScreen(
  213. position: state.extra! as FuturesPosition,
  214. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  215. ),
  216. ),
  217. GoRoute(
  218. path: 'order-detail',
  219. builder: (context, state) => OrderDetailScreen(
  220. order: state.extra! as FuturesOrder,
  221. ),
  222. ),
  223. ],
  224. ),
  225. ],
  226. ),
  227. // Tab 3: 合约
  228. StatefulShellBranch(
  229. initialLocation: '/contracts',
  230. routes: [
  231. GoRoute(
  232. path: '/contracts',
  233. builder: (context, state) => const FuturesScreen(
  234. symbol: 'BTCUSDT',
  235. showSpotSwitcher: false,
  236. ),
  237. routes: [
  238. GoRoute(
  239. path: ':symbol',
  240. builder: (context, state) => FuturesScreen(
  241. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  242. showSpotSwitcher: false,
  243. ),
  244. ),
  245. ],
  246. ),
  247. ],
  248. ),
  249. // Tab 4: 跟单
  250. StatefulShellBranch(
  251. navigatorKey: _copyTabKey,
  252. routes: [
  253. GoRoute(
  254. path: '/copy-trading',
  255. builder: (context, state) => const CopyTradingScreen(),
  256. ),
  257. ],
  258. ),
  259. // Tab 5: 资产
  260. StatefulShellBranch(
  261. navigatorKey: _assetTabKey,
  262. routes: [
  263. GoRoute(
  264. path: '/asset',
  265. builder: (context, state) {
  266. final tab = state.uri.queryParameters['tab'];
  267. int? initialSubTab;
  268. if (tab == 'staking' || tab == 'locked') {
  269. initialSubTab = 5;
  270. }
  271. return AssetScreen(initialSubTab: initialSubTab);
  272. },
  273. ),
  274. ],
  275. ),
  276. ],
  277. ),
  278. // ── 认证流程(无底部导航,使用右滑入/左滑出转场)───
  279. GoRoute(
  280. path: '/login',
  281. pageBuilder: (context, state) => CustomTransitionPage(
  282. key: state.pageKey,
  283. child: const LoginScreen(),
  284. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  285. const begin = Offset(1, 0);
  286. const end = Offset.zero;
  287. final tween = Tween(begin: begin, end: end)
  288. .chain(CurveTween(curve: Curves.easeInOut));
  289. return SlideTransition(
  290. position: animation.drive(tween), child: child);
  291. },
  292. ),
  293. ),
  294. GoRoute(
  295. path: '/register',
  296. pageBuilder: (context, state) => CustomTransitionPage(
  297. key: state.pageKey,
  298. child: const RegisterScreen(),
  299. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  300. const begin = Offset(1, 0);
  301. const end = Offset.zero;
  302. final tween = Tween(begin: begin, end: end)
  303. .chain(CurveTween(curve: Curves.easeInOut));
  304. return SlideTransition(
  305. position: animation.drive(tween), child: child);
  306. },
  307. ),
  308. ),
  309. GoRoute(
  310. path: '/two-factor',
  311. pageBuilder: (context, state) => CustomTransitionPage(
  312. key: state.pageKey,
  313. child: const TwoFactorScreen(),
  314. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  315. const begin = Offset(1, 0);
  316. const end = Offset.zero;
  317. final tween = Tween(begin: begin, end: end)
  318. .chain(CurveTween(curve: Curves.easeInOut));
  319. return SlideTransition(
  320. position: animation.drive(tween), child: child);
  321. },
  322. ),
  323. ),
  324. GoRoute(
  325. path: '/verify-code',
  326. pageBuilder: (context, state) => CustomTransitionPage(
  327. key: state.pageKey,
  328. child: VerifyCodeScreen(args: state.extra! as VerifyCodeArgs),
  329. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  330. const begin = Offset(1, 0);
  331. const end = Offset.zero;
  332. final tween = Tween(begin: begin, end: end)
  333. .chain(CurveTween(curve: Curves.easeInOut));
  334. return SlideTransition(
  335. position: animation.drive(tween), child: child);
  336. },
  337. ),
  338. ),
  339. GoRoute(
  340. path: '/forgot-password',
  341. pageBuilder: (context, state) => CustomTransitionPage(
  342. key: state.pageKey,
  343. child: const ForgotPasswordScreen(),
  344. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  345. const begin = Offset(1, 0);
  346. const end = Offset.zero;
  347. final tween = Tween(begin: begin, end: end)
  348. .chain(CurveTween(curve: Curves.easeInOut));
  349. return SlideTransition(
  350. position: animation.drive(tween), child: child);
  351. },
  352. ),
  353. ),
  354. // ── 行情详情(无底部导航)──────────────────────────
  355. GoRoute(
  356. path: '/market/futures/:symbol',
  357. builder: (context, state) => FuturesMarketDetailScreen(
  358. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  359. ),
  360. ),
  361. GoRoute(
  362. path: '/market/spot/:symbol',
  363. builder: (context, state) => SpotMarketDetailScreen(
  364. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  365. ),
  366. ),
  367. // ── 我的跟单(无底部导航)──────────────────────────
  368. GoRoute(
  369. path: '/my-copy-trading',
  370. builder: (context, state) => const MyCopyTradingScreen(),
  371. ),
  372. GoRoute(
  373. path: '/trader-apply',
  374. builder: (context, state) => const TraderApplyScreen(),
  375. ),
  376. // ── 我的带单(带单员专用)────────────────────────────
  377. GoRoute(
  378. path: '/my-trades',
  379. builder: (context, state) => const MyTradesScreen(),
  380. ),
  381. GoRoute(
  382. path: '/trader-settings',
  383. builder: (context, state) => const TraderSettingsScreen(),
  384. ),
  385. GoRoute(
  386. path: '/trader-detail/:traderId',
  387. builder: (context, state) => TraderDetailScreen(
  388. traderId: state.pathParameters['traderId'] ?? '',
  389. ),
  390. ),
  391. GoRoute(
  392. path: '/follow-setting',
  393. builder: (context, state) => FollowSettingScreen(
  394. trader: state.extra! as Map<String, dynamic>,
  395. ),
  396. ),
  397. // ── 资产操作(无底部导航)──────────────────────────
  398. GoRoute(
  399. path: '/asset/deposit',
  400. builder: (context, state) => const DepositScreen(),
  401. ),
  402. GoRoute(
  403. path: '/asset/deposit/history',
  404. builder: (context, state) => const DepositHistoryScreen(),
  405. ),
  406. GoRoute(
  407. path: '/asset/deposit/detail',
  408. builder: (context, state) {
  409. final record = state.extra as RechargeRecord;
  410. return DepositDetailScreen(record: record);
  411. },
  412. ),
  413. GoRoute(
  414. path: '/asset/withdraw',
  415. builder: (context, state) => const WithdrawScreen(),
  416. ),
  417. GoRoute(
  418. path: '/asset/withdraw/history',
  419. builder: (context, state) => const WithdrawHistoryScreen(),
  420. ),
  421. GoRoute(
  422. path: '/asset/withdraw/detail',
  423. builder: (context, state) {
  424. final record = state.extra as WithdrawRecord;
  425. final isTransfer = state.uri.queryParameters['isTransfer'] == 'true';
  426. return WithdrawDetailScreen(record: record, isTransfer: isTransfer);
  427. },
  428. ),
  429. GoRoute(
  430. path: '/asset/transfer',
  431. builder: (context, state) {
  432. final qp = state.uri.queryParameters;
  433. return TransferScreen(
  434. initialFrom: qp['from'],
  435. initialTo: qp['to'],
  436. initialSymbol: qp['symbol'],
  437. preferDefaultSymbol: qp['preferSymbol'] == '1',
  438. spotTradingBridgeOnly: qp['bridgeOnly'] == '1',
  439. );
  440. },
  441. ),
  442. GoRoute(
  443. path: '/asset/transfer/history',
  444. builder: (context, state) => const TransferHistoryScreen(),
  445. ),
  446. GoRoute(
  447. path: '/asset/history',
  448. builder: (context, state) => const AssetHistoryScreen(),
  449. ),
  450. GoRoute(
  451. path: '/qr-scanner',
  452. builder: (context, state) => const QrScannerScreen(),
  453. ),
  454. // ── 个人中心(无底部导航)──────────────────────────
  455. GoRoute(
  456. path: '/user',
  457. builder: (context, state) => const ProfileScreen(),
  458. ),
  459. GoRoute(
  460. path: '/user/account',
  461. builder: (context, state) =>
  462. const PlaceholderScreen(title: '账户设置', icon: Icons.settings),
  463. ),
  464. GoRoute(
  465. path: '/user/security',
  466. builder: (context, state) => const SecurityScreen(),
  467. ),
  468. GoRoute(
  469. path: '/user/security/google-auth',
  470. builder: (context, state) => const GoogleAuthScreen(),
  471. ),
  472. GoRoute(
  473. path: '/user/security/google-auth/bind',
  474. builder: (context, state) => const GoogleAuthBindScreen(),
  475. ),
  476. GoRoute(
  477. path: '/user/security/fund-password',
  478. builder: (context, state) {
  479. final isReset = state.extra as bool? ?? false;
  480. return FundPasswordScreen(isResetMode: isReset);
  481. },
  482. ),
  483. GoRoute(
  484. path: '/user/security/change-password',
  485. builder: (context, state) => const ChangePasswordScreen(),
  486. ),
  487. GoRoute(
  488. path: '/user/kyc',
  489. builder: (context, state) =>
  490. const PlaceholderScreen(title: '身份认证', icon: Icons.verified_user),
  491. ),
  492. GoRoute(
  493. path: '/user/api-keys',
  494. builder: (context, state) =>
  495. const PlaceholderScreen(title: 'API 管理', icon: Icons.key),
  496. ),
  497. GoRoute(
  498. path: '/user/referral',
  499. builder: (context, state) => const InviteFriendsScreen(),
  500. ),
  501. GoRoute(
  502. path: '/user/messages',
  503. builder: (context, state) => const NotificationsScreen(),
  504. ),
  505. GoRoute(
  506. path: '/user/messages/:id',
  507. builder: (context, state) => NotificationDetailScreen(
  508. id: state.pathParameters['id'] ?? '',
  509. ),
  510. ),
  511. GoRoute(
  512. path: '/user/service-route',
  513. builder: (context, state) => const ServiceRouteScreen(),
  514. ),
  515. GoRoute(
  516. path: '/user/language',
  517. builder: (context, state) => const LanguageScreen(),
  518. ),
  519. GoRoute(
  520. path: '/user/help',
  521. builder: (context, state) => const HelpCenterScreen(),
  522. ),
  523. GoRoute(
  524. path: '/user/help/:id',
  525. builder: (context, state) => HelpDetailScreen(
  526. id: state.pathParameters['id'] ?? '',
  527. ),
  528. ),
  529. GoRoute(
  530. path: '/protocol',
  531. builder: (context, state) {
  532. final args = state.extra as ProtocolArgs?;
  533. return ProtocolScreen(
  534. title: args?.title ?? '',
  535. categoryCode: args?.categoryCode ?? 'PROTOCOL',
  536. );
  537. },
  538. ),
  539. GoRoute(
  540. path: '/finance/ido',
  541. builder: (context, state) {
  542. final tab = state.uri.queryParameters['tab'];
  543. final initialTab = tab == 'airdrop' ? 1 : 0;
  544. return FinanceHubScreen(initialTab: initialTab);
  545. },
  546. ),
  547. GoRoute(
  548. path: '/finance/stake/:configId',
  549. builder: (context, state) => StakingScreen(
  550. configId: state.pathParameters['configId'],
  551. ),
  552. ),
  553. GoRoute(
  554. path: '/finance/airdrop',
  555. builder: (context, state) => const FinanceHubScreen(initialTab: 1),
  556. ),
  557. GoRoute(
  558. path: '/finance/team',
  559. builder: (context, state) => const StakingTeamScreen(),
  560. ),
  561. GoRoute(
  562. path: '/user/about',
  563. builder: (context, state) =>
  564. const PlaceholderScreen(title: '关于', icon: Icons.info),
  565. ),
  566. // ── 经纪商 ────────────────────────────────────────
  567. GoRoute(
  568. path: '/broker',
  569. builder: (context, state) => const BrokerScreen(),
  570. ),
  571. GoRoute(
  572. path: '/broker/apply',
  573. builder: (context, state) => const BrokerApplyScreen(),
  574. ),
  575. GoRoute(
  576. path: '/broker/my-invitations',
  577. builder: (context, state) => const MyInvitationsScreen(),
  578. ),
  579. GoRoute(
  580. path: '/broker/team-detail',
  581. builder: (context, state) => const TeamDetailScreen(),
  582. ),
  583. // ── 网络加载失败页 ────────────────────────────────
  584. GoRoute(
  585. path: '/network-error',
  586. builder: (context, state) {
  587. final extra = state.extra as Map<String, dynamic>?;
  588. return NetworkErrorScreen(
  589. title: extra?['title'] as String? ?? '行情',
  590. errorCode: extra?['errorCode'] as String?,
  591. onRetry: extra?['onRetry'] as VoidCallback?,
  592. );
  593. },
  594. ),
  595. // ── Deep Link 路径映射 ────────────────────────────
  596. GoRoute(
  597. path: '/f/:symbol',
  598. redirect: (context, state) {
  599. final symbol = state.pathParameters['symbol'] ?? 'BTCUSDT';
  600. return '/futures/$symbol';
  601. },
  602. ),
  603. ],
  604. );
  605. });
  606. /// 桥接 Riverpod → GoRouter refreshListenable
  607. class _RouterNotifier extends ChangeNotifier {
  608. /// true 时跳过 isLoggedInProvider 变化引发的 notifyListeners,
  609. /// 用于 session 过期场景:由 dialog 按钮统一执行导航,避免双重导航断言。
  610. bool _suppressLoggedOutNotify = false;
  611. /// 保存 ref,供 dialog 按钮回调中延迟 invalidate 使用
  612. late final Ref _ref;
  613. _RouterNotifier(Ref ref) {
  614. _ref = ref;
  615. ref.listen(isLoggedInProvider, (prev, next) {
  616. if (prev == true && next == false) {
  617. if (_suppressLoggedOutNotify) return;
  618. // 主动登出:先触发 redirect 导航,下一帧再 invalidate provider,
  619. // 避免 invalidate rebuild 与 GoRouter navigation 并发导致 Duplicate GlobalKey 崩溃
  620. notifyListeners();
  621. // 下一帧再 invalidate:profile 由 isLoggedIn 监听同步降为访客,
  622. // 另行 invalidate 易与_uc/member/my-info 等在途请求收尾并发,触发 element 断言。
  623. WidgetsBinding.instance.addPostFrameCallback((_) {
  624. ref.invalidate(assetProvider);
  625. ref.invalidate(homeProvider);
  626. ref.invalidate(futuresProvider);
  627. });
  628. }
  629. // 登录:导航由页面显式 context.go('/') 处理,不在此触发 notifyListeners,
  630. // 避免与 context.go 并发导致 element._lifecycleState 断言失败。
  631. });
  632. ref.listen(sessionExpiredProvider, (_, expired) {
  633. if (expired) {
  634. ref.read(sessionExpiredProvider.notifier).state = false;
  635. // 阻止 isLoggedInProvider 监听器触发 notifyListeners(→ redirect),
  636. // 导航由 dialog 按钮的 context.go('/login') 统一处理,避免双重导航。
  637. _suppressLoggedOutNotify = true;
  638. // 只清本地登录态,不请求 logout 接口(token 已失效,请求也会报 4000)
  639. ref.read(authProvider.notifier).clearLocalSession();
  640. _suppressLoggedOutNotify = false;
  641. // 下一帧再弹 dialog,此时 clearLocalSession 的 rebuild 已完成;
  642. // provider invalidate 推迟到用户点击确认后导航完成再执行,
  643. // 确保 navigation 与 invalidate 完全不在同一帧
  644. WidgetsBinding.instance.addPostFrameCallback((_) {
  645. _showSessionExpiredDialog();
  646. });
  647. }
  648. });
  649. }
  650. void _showSessionExpiredDialog() {
  651. final ctx = rootNavigatorKey.currentContext;
  652. if (ctx == null) return;
  653. final cs = Theme.of(ctx).colorScheme;
  654. final l10n = AppLocalizations.of(ctx)!;
  655. showDialog(
  656. context: ctx,
  657. barrierDismissible: false,
  658. builder: (dialogCtx) => AlertDialog(
  659. backgroundColor: cs.surface,
  660. title: Text(l10n.sessionExpiredTitle, style: TextStyle(color: cs.onSurface)),
  661. content: Text(
  662. l10n.sessionExpiredContent,
  663. style: TextStyle(color: cs.onSurface.withAlpha(153)),
  664. ),
  665. actions: [
  666. TextButton(
  667. onPressed: () {
  668. Navigator.of(dialogCtx).pop();
  669. rootNavigatorKey.currentContext?.go('/login');
  670. // 导航完成后下一帧再 invalidate,避免与 navigation 并发
  671. WidgetsBinding.instance.addPostFrameCallback((_) {
  672. _ref.invalidate(assetProvider);
  673. _ref.invalidate(profileProvider);
  674. _ref.invalidate(homeProvider);
  675. _ref.invalidate(futuresProvider);
  676. });
  677. },
  678. child: Text(l10n.relogin, style: TextStyle(color: cs.onSurface)),
  679. ),
  680. ],
  681. ),
  682. );
  683. }
  684. }