Files
nj/app/lib/core/routing/app_router.dart
iven 8e3e232278 fix: 全链路问题修复 — 编辑器返回/Tab导航/数据库编码/Token注入
修复内容:
- 编辑器返回按钮: 所有 context.go('/editor') 改为 context.push(),pop() 加安全守卫 fallback 到 /home
- Tab 导航: Web 平台强制使用移动端底部 TabBar 布局 (kIsWeb 守卫)
- 数据库编码: db.rs 自动追加 client_encoding=utf8 参数,修复中文 display_name 乱码
- AuthBloc token: 清理冗余 TODO,token 注入已在 AuthRepository 中正常工作
- 影响 9 个文件的编辑器导航调用点统一修改
2026-06-01 18:08:09 +08:00

270 lines
8.0 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.
// 暖记路由表 — go_router 20 页面 + 认证守卫
//
// 路由守卫逻辑:
// - 未认证用户访问受保护路由 → 重定向到 /login
// - 已认证用户访问 /login → 重定向到 /home
// - 需要角色选择 → 重定向到 /role-selection
// - 需要班级码 → 重定向到 /class-code
export '../../widgets/responsive_scaffold.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/responsive_scaffold.dart';
import '../../features/home/views/home_page.dart';
import '../../features/calendar/views/calendar_page.dart';
import '../../features/mood/views/mood_page.dart';
import '../../features/search/views/search_page.dart';
import '../../features/profile/views/profile_page.dart';
import '../../features/editor/views/editor_page.dart';
import '../../features/auth/views/login_page.dart';
import '../../features/auth/views/role_selection_page.dart';
import '../../features/auth/views/class_code_join_page.dart';
import '../../features/class_/views/class_page.dart';
import '../../features/teacher/views/teacher_page.dart';
import '../../features/parent/views/parent_page.dart';
import '../../features/achievement/views/achievement_page.dart';
import '../../features/stickers/views/sticker_library_page.dart';
import '../../features/templates/views/template_gallery_page.dart';
import '../../features/settings/views/settings_page.dart';
import '../../features/auth/bloc/auth_bloc.dart';
// Shell 分支键
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
/// 不需要认证的白名单路径
const _publicPaths = ['/login', '/role-selection', '/class-code'];
/// 创建路由配置 — 需要注入 AuthBloc
GoRouter createAppRouter(AuthBloc authBloc) {
return GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/home',
debugLogDiagnostics: true,
// ===== 认证路由守卫 =====
redirect: (context, state) {
final authState = authBloc.state;
final currentPath = state.uri.path;
// 加载中 → 不做重定向
if (authState is AuthInitial || authState is AuthLoading) {
return null;
}
final isAuthenticated = authState is Authenticated;
final isPublicPath = _publicPaths.contains(currentPath);
// 已认证 + 访问公开页面 → 根据状态重定向
if (isAuthenticated && isPublicPath) {
if (authState.needsRoleSelection) return '/role-selection';
if (authState.needsClassCode) return '/class-code';
return '/home';
}
// 已认证 + 访问受保护页面 → 检查是否需要额外步骤
if (isAuthenticated) {
if (authState.needsRoleSelection && currentPath != '/role-selection') {
return '/role-selection';
}
if (authState.needsClassCode &&
currentPath != '/class-code' &&
currentPath != '/role-selection') {
return '/class-code';
}
return null;
}
// 未认证 + 访问公开页面 → 放行
if (isPublicPath) return null;
// 未认证 + 访问受保护页面 → 重定向到登录
return '/login';
},
// 监听认证状态变化,自动触发重定向
refreshListenable: _AuthListenable(authBloc),
routes: [
// 认证路由(无 Shell
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: '/role-selection',
name: 'roleSelection',
builder: (context, state) => const RoleSelectionPage(),
),
GoRoute(
path: '/class-code',
name: 'classCode',
builder: (context, state) => const ClassCodeJoinPage(),
),
// 主 Shell 路由(底部导航 + 侧边导航)
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
final index = _selectedIndexFromLocation(state.uri.path);
return _AppShell(
selectedIndex: index,
child: child,
);
},
routes: [
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/calendar',
name: 'calendar',
builder: (context, state) => const CalendarPage(),
),
GoRoute(
path: '/mood',
name: 'mood',
builder: (context, state) => const MoodPage(),
),
GoRoute(
path: '/search',
name: 'search',
builder: (context, state) => const SearchPage(),
),
GoRoute(
path: '/profile',
name: 'profile',
builder: (context, state) => const ProfilePage(),
),
],
),
// 全屏页面(无底部导航)
GoRoute(
path: '/editor',
name: 'editor',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) {
final journalId = state.uri.queryParameters['id'];
return EditorPage(journalId: journalId);
},
),
GoRoute(
path: '/class',
name: 'class',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const ClassPage(),
),
GoRoute(
path: '/teacher',
name: 'teacher',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const TeacherPage(),
),
GoRoute(
path: '/parent',
name: 'parent',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const ParentPage(),
),
GoRoute(
path: '/achievements',
name: 'achievements',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const AchievementPage(),
),
GoRoute(
path: '/stickers',
name: 'stickers',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const StickerLibraryPage(),
),
GoRoute(
path: '/templates',
name: 'templates',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const TemplateGalleryPage(),
),
GoRoute(
path: '/settings',
name: 'settings',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const SettingsPage(),
),
],
);
}
/// 路径 → Tab index 映射
int _selectedIndexFromLocation(String location) {
if (location.startsWith('/calendar')) return 1;
if (location.startsWith('/mood')) return 2;
if (location.startsWith('/search')) return 3;
if (location.startsWith('/profile')) return 4;
return 0;
}
/// AuthBloc 变化监听器 — 驱动 GoRouter refreshListenable
class _AuthListenable extends ChangeNotifier {
_AuthListenable(AuthBloc authBloc) {
_subscription = authBloc.stream.listen((_) {
notifyListeners();
});
}
late final StreamSubscription<AuthState> _subscription;
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
/// App Shell — 包裹 ResponsiveScaffold
class _AppShell extends StatelessWidget {
const _AppShell({
required this.selectedIndex,
required this.child,
});
final int selectedIndex;
final Widget child;
@override
Widget build(BuildContext context) {
return ResponsiveScaffold(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
switch (index) {
case 0:
context.go('/home');
case 1:
context.go('/calendar');
case 2:
context.go('/mood');
case 3:
context.go('/search');
case 4:
context.go('/profile');
}
},
body: child,
floatingActionButton: selectedIndex == 0
? FloatingActionButton(
onPressed: () => context.push('/editor'),
child: const Icon(Icons.edit_rounded),
)
: null,
);
}
}