app_router.dart 27 KB

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