- 为conquerPackage模块添加过渡页面路由并实现页面跳转逻辑 - 新增通用多图上传组件Upload,支持多种模式与图片操作功能 - 新增任务相关的API及Model基础文件 - 注释课前课中页面的测试试题内容
249 lines
11 KiB
Dart
249 lines
11 KiB
Dart
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/my/note/note_page.dart';
|
||
import 'package:new_learn/features/conquerPackage/captureList/index.dart';
|
||
import 'package:new_learn/features/conquerPackage/beforeDuringClass/index.dart';
|
||
import 'package:new_learn/features/conquerPackage/transitionPage/transition_page.dart';
|
||
import 'package:new_learn/features/my/my_point/my_point_page.dart';
|
||
import 'package:new_learn/features/my/question_list/question_list_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/exam/exam_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/report/report_computation_page.dart';
|
||
import 'package:new_learn/features/report/report_web_view_page.dart';
|
||
import 'package:new_learn/features/report/student_info_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()。
|
||
/// ============================================================
|
||
|
||
/// 【kTabPaths】四个底部 tab 路径集合(动态来源,别人改 tab 路由时同步改这里即可)
|
||
///
|
||
/// 全局拦截器 ReportRedirectNotifier 用它判断"当前是否在 tab 页面",
|
||
/// 避免硬编码分散在多个地方。
|
||
const kTabPaths = <String>{'/', '/recite', '/class', '/my'};
|
||
|
||
/// 全局路由表(对标 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: '/myNote', builder: (_, _) => const NotePage()),
|
||
// 错题本(type=2)/ 我的收藏(type=1)共用 QuestionListPage
|
||
GoRoute(
|
||
path: '/myWrong',
|
||
builder: (_, _) => const QuestionListPage(type: 2),
|
||
),
|
||
GoRoute(
|
||
path: '/myCollect',
|
||
builder: (_, _) => const QuestionListPage(type: 1),
|
||
),
|
||
GoRoute(path: '/myReport', builder: (_, _) => const MyReportPage()),
|
||
GoRoute(path: '/myExam', builder: (_, _) => const ExamPage()),
|
||
GoRoute(
|
||
path: '/report-detail',
|
||
builder: (_, state) {
|
||
// extra 格式:{'title': '...', 'url': '...'}
|
||
final extra = state.extra as Map<String, dynamic>? ?? {};
|
||
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()),
|
||
// ---------- 诊断报告包三个页面(不在 ShellBranch 内,push 时不显示 tabbar) ----------
|
||
GoRoute(path: '/studentInfo', builder: (_, _) => const StudentInfoPage()),
|
||
GoRoute(
|
||
path: '/reportWebView',
|
||
builder: (_, _) => const ReportWebViewPage(),
|
||
),
|
||
GoRoute(
|
||
path: '/reportComputation',
|
||
builder: (_, _) => const ReportComputationPage(),
|
||
),
|
||
// ---------- 首次启动隐私协议页 ----------
|
||
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: '/conquerPackage/beforeDuringClass',
|
||
builder: (_, state) => BeforeDuringClassPage(
|
||
extra: state.extra as Map<String, dynamic>?,
|
||
),
|
||
),
|
||
GoRoute(
|
||
path: '/conquerPackage/transitionPage',
|
||
builder: (_, state) => TransitionPage(
|
||
extra: state.extra as Map<String, dynamic>?,
|
||
),
|
||
),
|
||
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;
|
||
}
|