Files
nj/app/lib/core/routing/app_router.dart
iven 0fe3bc705c feat(app): 实现认证模块 — F2 Auth BLoC + 登录/注册/角色选择/班级码加入
新增文件 (10):
- data/models/user.dart — 用户+角色模型 (匹配后端 UserResp/RoleResp)
- data/models/auth_token.dart — 认证令牌模型 (匹配后端 LoginResp)
- data/repositories/auth_repository.dart — 认证仓库 (JWT 安全持久化 + PIPL 合规)
- features/auth/bloc/auth_bloc.dart — 认证 BLoC (8 种事件, 6 种状态)
- features/auth/bloc/auth_event.dart — 认证事件 (sealed class 穷尽匹配)
- features/auth/bloc/auth_state.dart — 认证状态 (Authenticated 含角色/班级码流程)
- features/auth/views/login_page.dart — 登录/注册页面 (重写占位页面)
- features/auth/views/role_selection_page.dart — 角色选择页 (4 种角色卡片)
- features/auth/views/class_code_join_page.dart — 班级码加入页 (6 位输入)

修改文件 (5):
- pubspec.yaml — 添加 flutter_secure_storage 依赖
- app.dart — 注入 AuthBloc + RepositoryProvider
- main.dart — 简化入口 (认证恢复在 BLoC 中处理)
- core/routing/app_router.dart — 添加认证路由守卫 + 2 新路由
- erp-diary/service/class_service.rs — 移除未使用的 PaginatorTrait import

验证: flutter analyze (0 error) + cargo check 通过
2026-06-01 01:22:53 +08:00

263 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.
// 暖记路由表 — 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/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(),
),
],
);
}
/// 路径 → 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.go('/editor'),
child: const Icon(Icons.edit_rounded),
)
: null,
);
}
}