Files
App.K12.Student.Miniapp_Flu…/lib/core/router/app_router.dart
T
崔宝新 586c75f841 feat: 完成班级模块全量功能迭代
本次提交完成了班级模块的多项核心功能开发与优化:
1. 新增DELETE请求封装,支持业务删除接口且不自动重试
2. 完善UserPostCommonDto模型,新增头像字段适配详情接口
3. 重构路由架构,使用StatefulShellRoute实现Tab页面保活
4. 新增我的互动页面,包含我的动态/点赞/留言三个子页
5. 新增全部同学页面,展示班级全员信息与在线状态
6. 新增动态详情页面,支持查看完整评论与点赞头像
7. 抽离通用PostListBody组件,复用列表分页与交互逻辑
8. 优化班级列表页加载逻辑与图片预览交互
9. 新增评论删除、多图预览等细节交互功能
2026-09-07 09:20:06 +08:00

171 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/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/my_point/my_point_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()),
],
),
// 班级(含班级模块子页:全部同学 / 我的互动 / 动态详情)
StatefulShellBranch(
routes: [
GoRoute(path: '/class', builder: (_, _) => const ClassPage()),
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),
),
],
),
// 我的(含我的积分页)
StatefulShellBranch(
routes: [
GoRoute(path: '/my', builder: (_, _) => const MyPage()),
GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()),
],
),
],
),
// ---------- 首次启动隐私协议页 ----------
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()),
// app_router.dart 的 routes 里加,不用管跳转逻辑
// GoRoute(path: '/task/:id', builder: (_, _) => const TaskDetailPage()),
// ---------- 以后新业务页在这里加,一行一个 ----------
],
);
/// 登录守卫(对标 vue-router beforeEach):
/// 没登录 → 强制去 /login;已登录还访问 /login → 踢回首页
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';
if (!acceptedPrivacy && !atPrivacyConsent && !atLegalDocument) {
return '/privacy-consent';
}
if (acceptedPrivacy && atPrivacyConsent) return loggedIn ? '/' : '/login';
if (!loggedIn && !atLogin && !atLegalDocument) return '/login';
if (loggedIn && atLogin) return '/';
return null;
}