app_router.dart 27 KB

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