import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:new_learn/core/providers/privacy_provider.dart'; import 'package:new_learn/core/router/router_keys.dart'; import 'package:new_learn/features/auth/legal_document_page.dart'; import 'package:new_learn/features/auth/legal_documents.dart'; import 'package:new_learn/features/auth/login_page.dart'; import 'package:new_learn/features/auth/privacy_consent_page.dart'; import 'package:new_learn/features/auth/register_page.dart'; import 'package:new_learn/features/class/all_student_view.dart'; import 'package:new_learn/features/class/class_page.dart'; import 'package:new_learn/features/class/my_interact_page.dart'; import 'package:new_learn/features/class/my_update_detail_page.dart'; import 'package:new_learn/core/api/class/model.dart'; import 'package:new_learn/features/my/my_page.dart'; import 'package:new_learn/features/conquerPackage/captureList/index.dart'; import 'package:new_learn/features/my/my_point/my_point_page.dart'; import 'package:new_learn/features/my/my_report/my_report_page.dart'; import 'package:new_learn/features/my/my_report/report_detail_page.dart'; import 'package:new_learn/features/my/settings/settings_page.dart'; import 'package:new_learn/features/my/settings/about_app_page.dart'; import 'package:new_learn/features/recite/recite_page.dart'; import 'package:new_learn/features/task/task_page.dart'; import 'package:new_learn/core/router/main_tab_page.dart'; import 'package:new_learn/core/network/dio_client.dart'; import 'package:new_learn/core/providers/user_provider.dart'; /// ============================================================ /// 【AppRouter】全局路由表(对标 Vue 的 router/index.ts) /// /// 用户点退出 /// → ref.read(currentUserProvider.notifier).set(null) /// → state = AsyncData(null) // 清空用户信息 /// → authTick.value++ // 0 变成 1 /// → GoRouter 收到通知,执行 refresh() /// → 重新跑 _guardLogin() /// → 发现 accessToken == null(登出时已清除) /// → 当前不在 /login /// → return '/login' // 自动跳转到登录页 /// /// 一、为什么用 go_router(Flutter 官方推荐声明式路由): /// 页面以后会很多,集中管路由表比散落各页面的 Navigator.push /// 更可控,且天然支持登录守卫 / tab 嵌套 / 深链。 /// /// 二、与 vue-router 的对应关系: /// routes 路由表 = vue-router 的 routes /// redirect 登录守卫 = 路由前置守卫 beforeEach /// ShellRoute 4 tab 壳 = uni-app 的 tabBar + 子页面 /// context.push('/x') = router.push('/x') /// context.go('/x') = router.replace('/x')(不留历史,如登录后) /// /// 三、路由结构: /// /login → 登录页(无 token 时被 redirect 强制到这里) /// / → 任务页(4 tab 第 1 个) /// /recite /class /my → 其余 3 个 tab(由 ShellRoute 包住,共用一个底部导航) /// 以后新业务页:在 routes 里加一行 GoRoute 即可。 /// /// 四、登录守卫说明: /// 判断依据是 DioClient.accessToken(内存)——main() 里已 restoreToken /// 回填,所以启动时第一个跳转就能正确判断。 /// ⚠ redirect 只在【导航发生时】触发,登录/登出不导航守卫不会动, /// 所以配了 refreshListenable: authTick —— 登录态一变(user_provider /// 的 set() 里 authTick.value++)就主动 GoRouter.refresh() 重算一次, /// 未登录访问受限页 → 自动踢 /login,已登录访问 /login → 踢回 /。 /// 业务代码永远不用手动跳登录页(这就是"响应式登录守卫")。 /// /// 五、Web 地址栏同步(go_router 17.x 大坑): /// 17.x 起 optionURLReflectsImperativeAPIs 默认 false:context.push/go /// 导航时页面正常切换,但浏览器地址栏不更新(刷新会退回上一个路由)。 /// 必须在创建 GoRouter 前全局开启,见 main.dart 的 main()。 /// ============================================================ /// 全局路由表(对标 Vue 的 router/index.ts) final GoRouter appRouter = GoRouter( navigatorKey: rootNavigatorKey, // 告诉 GoRouter:这是根 Navigator 作用: 在拿不到 BuildContext 的地方,也能操作导航和弹窗 redirect: _guardLogin, // 登录守卫:未登录访问受限页 → 自动跳转到登录页 // 两类全局门禁都要能触发路由重算: // 1. authTick:登录/登出变化; // 2. privacyAcceptedNotifier:隐私协议同意状态变化。 // 响应式登录守卫的核心:监听登录态变化信号(user_provider 的 set() // 里自增),变化时自动 GoRouter.refresh() 重算 redirect。 // 没有它,登出后只有下一次手动导航才会被拦截。 refreshListenable: Listenable.merge([ authTick, privacyAcceptedNotifier, ]), // 响应式登录守卫:监听登录态变化信号,变化时自动重算 redirect routes: [ // ---------- 4 tab 主框架(StatefulShellRoute:切 tab 用 IndexedStack 保活页面状态) ---------- StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => MainTabPage(navigationShell: navigationShell), branches: [ // 任务 StatefulShellBranch( routes: [GoRoute(path: '/', builder: (_, _) => const TaskPage())], ), // 背诵 StatefulShellBranch( routes: [ GoRoute(path: '/recite', builder: (_, _) => const RecitePage()), ], ), // 班级(只有主 tab 页在 ShellBranch 内,子页放顶层避免 tabbar 遮挡) StatefulShellBranch( routes: [ GoRoute(path: '/class', builder: (_, _) => const ClassPage()), ], ), // 我的 StatefulShellBranch( routes: [GoRoute(path: '/my', builder: (_, _) => const MyPage())], ), ], ), // ---------- 二级业务页(不在 ShellBranch 内,push 时不显示 tabbar) ---------- GoRoute( path: '/class/all-student', builder: (_, _) => const AllStudentPage(), ), GoRoute( path: '/class/my-interact', builder: (_, _) => const MyInteractPage(), ), GoRoute( path: '/class/my-update-detail', builder: (_, state) => MyUpdateDetailPage(initialPost: state.extra as UserPostDto), ), GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()), GoRoute(path: '/myReport', builder: (_, _) => const MyReportPage()), GoRoute( path: '/report-detail', builder: (_, state) { // extra 格式:{'title': '...', 'url': '...'} final extra = state.extra as Map? ?? {}; return ReportDetailPage( title: extra['title'] as String? ?? '我的报告', url: extra['url'] as String? ?? '', ); }, ), GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()), GoRoute(path: '/about-app', builder: (_, _) => const AboutAppPage()), // ---------- 首次启动隐私协议页 ---------- GoRoute( path: '/privacy-consent', builder: (_, _) => const PrivacyConsentPage(), ), GoRoute( path: '/user-agreement', builder: (_, _) => const LegalDocumentPage( title: kUserAgreementTitle, content: kUserAgreementContent, ), ), GoRoute( path: '/privacy-policy', builder: (_, _) => const LegalDocumentPage( title: kPrivacyPolicyTitle, content: kPrivacyPolicyContent, ), ), // ---------- 登录/注册页(未登录可访问) ---------- GoRoute(path: '/login', builder: (_, _) => const LoginPage()), // ---------- 攻克包 - 截图名单 ---------- GoRoute( path: '/conquerPackage/captureList', builder: (_, state) => CaptureListPage(courseId: state.extra as String), ), GoRoute(path: '/register', builder: (_, _) => const RegisterPage()), // app_router.dart 的 routes 里加,不用管跳转逻辑 // GoRoute(path: '/task/:id', builder: (_, _) => const TaskDetailPage()), // ---------- 以后新业务页在这里加,一行一个 ---------- ], ); /// 登录守卫(对标 vue-router beforeEach): /// 没登录 → 强制去 /login;已登录还访问 /login 或 /register → 踢回首页 String? _guardLogin(BuildContext context, GoRouterState state) { final acceptedPrivacy = privacyAcceptedNotifier.value; final loggedIn = DioClient.instance.accessToken != null; final atPrivacyConsent = state.matchedLocation == '/privacy-consent'; final atLegalDocument = state.matchedLocation == '/user-agreement' || state.matchedLocation == '/privacy-policy'; final atLogin = state.matchedLocation == '/login'; final atRegister = state.matchedLocation == '/register'; // 未登录可白名单:隐私协议页 / 协议文档 / 登录页 / 注册页 final atAuthFree = atLogin || atRegister || atLegalDocument; if (!acceptedPrivacy && !atPrivacyConsent && !atLegalDocument) { return '/privacy-consent'; } if (acceptedPrivacy && atPrivacyConsent) return loggedIn ? '/' : '/login'; if (!loggedIn && !atAuthFree && !atPrivacyConsent) return '/login'; if (loggedIn && (atLogin || atRegister)) return '/'; return null; }