Files
App.K12.Student.Miniapp_Flu…/lib/core/router/app_router.dart
T
崔宝新 2aa1927694 refactor: 重构页面路由结构与积分页面UI,优化列表交互体验
1. 重构路由结构:将班级子页移出ShellBranch,解决tabbar遮挡问题
2. 统一修改全局"没有更多了"提示文案为带品牌标识的版本
3. 重构积分排行榜UI,修复切换状态时的布局抖动问题,替换图标为CDN资源
4. 重构我的积分页面:对齐Vue端UI设计,新增展开收起详情功能,优化背景与布局
5. 优化我的互动页面布局与样式,新增背景图支持
2026-09-08 13:42:10 +08:00

170 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()),
],
),
// 班级(只有主 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: '/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;
}