app_router.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  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/app/app_home.dart';
  44. import '../../presentation/screens/user/change_password_screen.dart';
  45. import '../../presentation/screens/user/fund_password_screen.dart';
  46. import '../../presentation/screens/user/google_auth_screen.dart';
  47. import '../../presentation/screens/user/google_auth_bind_screen.dart';
  48. import '../../presentation/screens/user/notification_detail_screen.dart';
  49. import '../../presentation/screens/user/notifications_screen.dart';
  50. import '../../presentation/screens/user/profile_screen.dart';
  51. import '../../presentation/screens/user/security_screen.dart';
  52. import '../../presentation/screens/user/service_route_screen.dart';
  53. import '../../presentation/screens/invite/invite_friends_screen.dart';
  54. import '../../presentation/screens/broker/broker_apply_screen.dart';
  55. import '../../presentation/screens/broker/broker_screen.dart';
  56. import '../../presentation/screens/broker/my_invitations_screen.dart';
  57. import '../../presentation/screens/broker/team_detail_screen.dart';
  58. import '../../presentation/screens/user/help_center_screen.dart';
  59. import '../../presentation/screens/user/help_detail_screen.dart';
  60. import '../../presentation/screens/user/language_screen.dart';
  61. import '../../presentation/screens/user/protocol_screen.dart';
  62. import '../../presentation/screens/finance/finance_hub_screen.dart';
  63. import '../../presentation/screens/finance/staking_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) => const AssetScreen(),
  266. ),
  267. ],
  268. ),
  269. ],
  270. ),
  271. // ── 认证流程(无底部导航,使用右滑入/左滑出转场)───
  272. GoRoute(
  273. path: '/login',
  274. pageBuilder: (context, state) => CustomTransitionPage(
  275. key: state.pageKey,
  276. child: const LoginScreen(),
  277. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  278. const begin = Offset(1, 0);
  279. const end = Offset.zero;
  280. final tween = Tween(begin: begin, end: end)
  281. .chain(CurveTween(curve: Curves.easeInOut));
  282. return SlideTransition(
  283. position: animation.drive(tween), child: child);
  284. },
  285. ),
  286. ),
  287. GoRoute(
  288. path: '/register',
  289. pageBuilder: (context, state) => CustomTransitionPage(
  290. key: state.pageKey,
  291. child: const RegisterScreen(),
  292. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  293. const begin = Offset(1, 0);
  294. const end = Offset.zero;
  295. final tween = Tween(begin: begin, end: end)
  296. .chain(CurveTween(curve: Curves.easeInOut));
  297. return SlideTransition(
  298. position: animation.drive(tween), child: child);
  299. },
  300. ),
  301. ),
  302. GoRoute(
  303. path: '/two-factor',
  304. pageBuilder: (context, state) => CustomTransitionPage(
  305. key: state.pageKey,
  306. child: const TwoFactorScreen(),
  307. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  308. const begin = Offset(1, 0);
  309. const end = Offset.zero;
  310. final tween = Tween(begin: begin, end: end)
  311. .chain(CurveTween(curve: Curves.easeInOut));
  312. return SlideTransition(
  313. position: animation.drive(tween), child: child);
  314. },
  315. ),
  316. ),
  317. GoRoute(
  318. path: '/verify-code',
  319. pageBuilder: (context, state) => CustomTransitionPage(
  320. key: state.pageKey,
  321. child: VerifyCodeScreen(args: state.extra! as VerifyCodeArgs),
  322. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  323. const begin = Offset(1, 0);
  324. const end = Offset.zero;
  325. final tween = Tween(begin: begin, end: end)
  326. .chain(CurveTween(curve: Curves.easeInOut));
  327. return SlideTransition(
  328. position: animation.drive(tween), child: child);
  329. },
  330. ),
  331. ),
  332. GoRoute(
  333. path: '/forgot-password',
  334. pageBuilder: (context, state) => CustomTransitionPage(
  335. key: state.pageKey,
  336. child: const ForgotPasswordScreen(),
  337. transitionsBuilder: (context, animation, secondaryAnimation, child) {
  338. const begin = Offset(1, 0);
  339. const end = Offset.zero;
  340. final tween = Tween(begin: begin, end: end)
  341. .chain(CurveTween(curve: Curves.easeInOut));
  342. return SlideTransition(
  343. position: animation.drive(tween), child: child);
  344. },
  345. ),
  346. ),
  347. // ── 行情详情(无底部导航)──────────────────────────
  348. GoRoute(
  349. path: '/market/futures/:symbol',
  350. builder: (context, state) => FuturesMarketDetailScreen(
  351. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  352. ),
  353. ),
  354. GoRoute(
  355. path: '/market/spot/:symbol',
  356. builder: (context, state) => SpotMarketDetailScreen(
  357. symbol: state.pathParameters['symbol'] ?? 'BTCUSDT',
  358. ),
  359. ),
  360. // ── 我的跟单(无底部导航)──────────────────────────
  361. GoRoute(
  362. path: '/my-copy-trading',
  363. builder: (context, state) => const MyCopyTradingScreen(),
  364. ),
  365. GoRoute(
  366. path: '/app',
  367. builder: (context, state) => const AppHomeScreen(),
  368. ),
  369. GoRoute(
  370. path: '/trader-apply',
  371. builder: (context, state) => const TraderApplyScreen(),
  372. ),
  373. // ── 我的带单(带单员专用)────────────────────────────
  374. GoRoute(
  375. path: '/my-trades',
  376. builder: (context, state) => const MyTradesScreen(),
  377. ),
  378. GoRoute(
  379. path: '/trader-settings',
  380. builder: (context, state) => const TraderSettingsScreen(),
  381. ),
  382. GoRoute(
  383. path: '/trader-detail/:traderId',
  384. builder: (context, state) => TraderDetailScreen(
  385. traderId: state.pathParameters['traderId'] ?? '',
  386. ),
  387. ),
  388. GoRoute(
  389. path: '/follow-setting',
  390. builder: (context, state) => FollowSettingScreen(
  391. trader: state.extra! as Map<String, dynamic>,
  392. ),
  393. ),
  394. // ── 资产操作(无底部导航)──────────────────────────
  395. GoRoute(
  396. path: '/asset/deposit',
  397. builder: (context, state) => const DepositScreen(),
  398. ),
  399. GoRoute(
  400. path: '/asset/deposit/history',
  401. builder: (context, state) => const DepositHistoryScreen(),
  402. ),
  403. GoRoute(
  404. path: '/asset/deposit/detail',
  405. builder: (context, state) {
  406. final record = state.extra as RechargeRecord;
  407. return DepositDetailScreen(record: record);
  408. },
  409. ),
  410. GoRoute(
  411. path: '/asset/withdraw',
  412. builder: (context, state) => const WithdrawScreen(),
  413. ),
  414. GoRoute(
  415. path: '/asset/withdraw/history',
  416. builder: (context, state) => const WithdrawHistoryScreen(),
  417. ),
  418. GoRoute(
  419. path: '/asset/withdraw/detail',
  420. builder: (context, state) {
  421. final record = state.extra as WithdrawRecord;
  422. final isTransfer = state.uri.queryParameters['isTransfer'] == 'true';
  423. return WithdrawDetailScreen(record: record, isTransfer: isTransfer);
  424. },
  425. ),
  426. GoRoute(
  427. path: '/asset/transfer',
  428. builder: (context, state) {
  429. final qp = state.uri.queryParameters;
  430. return TransferScreen(
  431. initialFrom: qp['from'],
  432. initialTo: qp['to'],
  433. initialSymbol: qp['symbol'],
  434. preferDefaultSymbol: qp['preferSymbol'] == '1',
  435. spotTradingBridgeOnly: qp['bridgeOnly'] == '1',
  436. );
  437. },
  438. ),
  439. GoRoute(
  440. path: '/asset/transfer/history',
  441. builder: (context, state) => const TransferHistoryScreen(),
  442. ),
  443. GoRoute(
  444. path: '/asset/history',
  445. builder: (context, state) => const AssetHistoryScreen(),
  446. ),
  447. GoRoute(
  448. path: '/qr-scanner',
  449. builder: (context, state) => const QrScannerScreen(),
  450. ),
  451. // ── 个人中心(无底部导航)──────────────────────────
  452. GoRoute(
  453. path: '/user',
  454. builder: (context, state) => const ProfileScreen(),
  455. ),
  456. GoRoute(
  457. path: '/user/account',
  458. builder: (context, state) =>
  459. const PlaceholderScreen(title: '账户设置', icon: Icons.settings),
  460. ),
  461. GoRoute(
  462. path: '/user/security',
  463. builder: (context, state) => const SecurityScreen(),
  464. ),
  465. GoRoute(
  466. path: '/user/security/google-auth',
  467. builder: (context, state) => const GoogleAuthScreen(),
  468. ),
  469. GoRoute(
  470. path: '/user/security/google-auth/bind',
  471. builder: (context, state) => const GoogleAuthBindScreen(),
  472. ),
  473. GoRoute(
  474. path: '/user/security/fund-password',
  475. builder: (context, state) {
  476. final isReset = state.extra as bool? ?? false;
  477. return FundPasswordScreen(isResetMode: isReset);
  478. },
  479. ),
  480. GoRoute(
  481. path: '/user/security/change-password',
  482. builder: (context, state) => const ChangePasswordScreen(),
  483. ),
  484. GoRoute(
  485. path: '/user/kyc',
  486. builder: (context, state) =>
  487. const PlaceholderScreen(title: '身份认证', icon: Icons.verified_user),
  488. ),
  489. GoRoute(
  490. path: '/user/api-keys',
  491. builder: (context, state) =>
  492. const PlaceholderScreen(title: 'API 管理', icon: Icons.key),
  493. ),
  494. GoRoute(
  495. path: '/user/referral',
  496. builder: (context, state) => const InviteFriendsScreen(),
  497. ),
  498. GoRoute(
  499. path: '/user/messages',
  500. builder: (context, state) => const NotificationsScreen(),
  501. ),
  502. GoRoute(
  503. path: '/user/messages/:id',
  504. builder: (context, state) => NotificationDetailScreen(
  505. id: state.pathParameters['id'] ?? '',
  506. ),
  507. ),
  508. GoRoute(
  509. path: '/user/service-route',
  510. builder: (context, state) => const ServiceRouteScreen(),
  511. ),
  512. GoRoute(
  513. path: '/user/language',
  514. builder: (context, state) => const LanguageScreen(),
  515. ),
  516. GoRoute(
  517. path: '/user/help',
  518. builder: (context, state) => const HelpCenterScreen(),
  519. ),
  520. GoRoute(
  521. path: '/user/help/:id',
  522. builder: (context, state) => HelpDetailScreen(
  523. id: state.pathParameters['id'] ?? '',
  524. ),
  525. ),
  526. GoRoute(
  527. path: '/protocol',
  528. builder: (context, state) {
  529. final args = state.extra as ProtocolArgs?;
  530. return ProtocolScreen(
  531. title: args?.title ?? '',
  532. categoryCode: args?.categoryCode ?? 'PROTOCOL',
  533. );
  534. },
  535. ),
  536. GoRoute(
  537. path: '/finance/ido',
  538. builder: (context, state) {
  539. final tab = state.uri.queryParameters['tab'];
  540. final initialTab = tab == 'airdrop' ? 1 : 0;
  541. return FinanceHubScreen(initialTab: initialTab);
  542. },
  543. ),
  544. GoRoute(
  545. path: '/finance/stake/:configId',
  546. builder: (context, state) => StakingScreen(
  547. configId: state.pathParameters['configId'],
  548. ),
  549. ),
  550. GoRoute(
  551. path: '/finance/airdrop',
  552. builder: (context, state) => const FinanceHubScreen(initialTab: 1),
  553. ),
  554. GoRoute(
  555. path: '/user/about',
  556. builder: (context, state) =>
  557. const PlaceholderScreen(title: '关于', icon: Icons.info),
  558. ),
  559. // ── 经纪商 ────────────────────────────────────────
  560. GoRoute(
  561. path: '/broker',
  562. builder: (context, state) => const BrokerScreen(),
  563. ),
  564. GoRoute(
  565. path: '/broker/apply',
  566. builder: (context, state) => const BrokerApplyScreen(),
  567. ),
  568. GoRoute(
  569. path: '/broker/my-invitations',
  570. builder: (context, state) => const MyInvitationsScreen(),
  571. ),
  572. GoRoute(
  573. path: '/broker/team-detail',
  574. builder: (context, state) => const TeamDetailScreen(),
  575. ),
  576. // ── 网络加载失败页 ────────────────────────────────
  577. GoRoute(
  578. path: '/network-error',
  579. builder: (context, state) {
  580. final extra = state.extra as Map<String, dynamic>?;
  581. return NetworkErrorScreen(
  582. title: extra?['title'] as String? ?? '行情',
  583. errorCode: extra?['errorCode'] as String?,
  584. onRetry: extra?['onRetry'] as VoidCallback?,
  585. );
  586. },
  587. ),
  588. // ── Deep Link 路径映射 ────────────────────────────
  589. GoRoute(
  590. path: '/f/:symbol',
  591. redirect: (context, state) {
  592. final symbol = state.pathParameters['symbol'] ?? 'BTCUSDT';
  593. return '/futures/$symbol';
  594. },
  595. ),
  596. ],
  597. );
  598. });
  599. /// 桥接 Riverpod → GoRouter refreshListenable
  600. class _RouterNotifier extends ChangeNotifier {
  601. /// true 时跳过 isLoggedInProvider 变化引发的 notifyListeners,
  602. /// 用于 session 过期场景:由 dialog 按钮统一执行导航,避免双重导航断言。
  603. bool _suppressLoggedOutNotify = false;
  604. /// 保存 ref,供 dialog 按钮回调中延迟 invalidate 使用
  605. late final Ref _ref;
  606. _RouterNotifier(Ref ref) {
  607. _ref = ref;
  608. ref.listen(isLoggedInProvider, (prev, next) {
  609. if (prev == true && next == false) {
  610. if (_suppressLoggedOutNotify) return;
  611. // 主动登出:先触发 redirect 导航,下一帧再 invalidate provider,
  612. // 避免 invalidate rebuild 与 GoRouter navigation 并发导致 Duplicate GlobalKey 崩溃
  613. notifyListeners();
  614. // 下一帧再 invalidate:profile 由 isLoggedIn 监听同步降为访客,
  615. // 另行 invalidate 易与_uc/member/my-info 等在途请求收尾并发,触发 element 断言。
  616. WidgetsBinding.instance.addPostFrameCallback((_) {
  617. ref.invalidate(assetProvider);
  618. ref.invalidate(homeProvider);
  619. ref.invalidate(futuresProvider);
  620. });
  621. }
  622. // 登录:导航由页面显式 context.go('/') 处理,不在此触发 notifyListeners,
  623. // 避免与 context.go 并发导致 element._lifecycleState 断言失败。
  624. });
  625. ref.listen(sessionExpiredProvider, (_, expired) {
  626. if (expired) {
  627. ref.read(sessionExpiredProvider.notifier).state = false;
  628. // 阻止 isLoggedInProvider 监听器触发 notifyListeners(→ redirect),
  629. // 导航由 dialog 按钮的 context.go('/login') 统一处理,避免双重导航。
  630. _suppressLoggedOutNotify = true;
  631. // 只清本地登录态,不请求 logout 接口(token 已失效,请求也会报 4000)
  632. ref.read(authProvider.notifier).clearLocalSession();
  633. _suppressLoggedOutNotify = false;
  634. // 下一帧再弹 dialog,此时 clearLocalSession 的 rebuild 已完成;
  635. // provider invalidate 推迟到用户点击确认后导航完成再执行,
  636. // 确保 navigation 与 invalidate 完全不在同一帧
  637. WidgetsBinding.instance.addPostFrameCallback((_) {
  638. _showSessionExpiredDialog();
  639. });
  640. }
  641. });
  642. }
  643. void _showSessionExpiredDialog() {
  644. final ctx = rootNavigatorKey.currentContext;
  645. if (ctx == null) return;
  646. final cs = Theme.of(ctx).colorScheme;
  647. final l10n = AppLocalizations.of(ctx)!;
  648. showDialog(
  649. context: ctx,
  650. barrierDismissible: false,
  651. builder: (dialogCtx) => AlertDialog(
  652. backgroundColor: cs.surface,
  653. title: Text(l10n.sessionExpiredTitle, style: TextStyle(color: cs.onSurface)),
  654. content: Text(
  655. l10n.sessionExpiredContent,
  656. style: TextStyle(color: cs.onSurface.withAlpha(153)),
  657. ),
  658. actions: [
  659. TextButton(
  660. onPressed: () {
  661. Navigator.of(dialogCtx).pop();
  662. rootNavigatorKey.currentContext?.go('/login');
  663. // 导航完成后下一帧再 invalidate,避免与 navigation 并发
  664. WidgetsBinding.instance.addPostFrameCallback((_) {
  665. _ref.invalidate(assetProvider);
  666. _ref.invalidate(profileProvider);
  667. _ref.invalidate(homeProvider);
  668. _ref.invalidate(futuresProvider);
  669. });
  670. },
  671. child: Text(l10n.relogin, style: TextStyle(color: cs.onSurface)),
  672. ),
  673. ],
  674. ),
  675. );
  676. }
  677. }