Files
nj/app/lib/core/routing/app_router.dart
iven 749ef55b89
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
feat: Week 4 收尾 + 架构治理 — 搜索/家长中心/Feature Flag/Docker/环境配置
架构治理:
- Feature Flag 落地: Cargo.toml [features] default=["diary"] + main.rs cfg 条件编译
- 环境配置统一: AppConfig 类 + --dart-define 注入 + SSE 端口 8080→3000 修复

搜索替代方案 (无 FTS):
- SearchBloc + 标签/心情筛选接入后端 API
- JournalRepository 扩展 mood/tag 筛选参数
- 搜索页 UI 接入实际数据(替换占位文本)

家长中心最小集 (PIPL 合规):
- 后端: parent_service (绑定/查看/导出/删除/解绑) + parent_handler (6 个 API 端点)
- 前端: ParentBloc + ParentPage 功能完整实现
- 绑定孩子、只读查看日记、导出数据、删除数据、解绑

Docker 部署:
- verify.sh 健康检查脚本 (Axum/PG/Redis/OpenAPI 四项检查)

测试修复:
- home_bloc_test / calendar_bloc_test 适配 JournalRepository 新参数

验证: flutter test 84/84 pass, cargo test 76/76 pass, cargo check pass
2026-06-01 23:53:34 +08:00

315 lines
9.9 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 + 认证守卫
// 对齐 Open Design: TabBar = 首页/日历/发现/我的,中心 FAB = 写日记
//
// 路由守卫逻辑:
// - 未认证用户访问受保护路由 → 重定向到 /login
// - 已认证用户访问 /login → 重定向到 /home
// - 需要角色选择 → 重定向到 /role-selection
// - 需要班级码 → 重定向到 /class-code
export '../../widgets/responsive_scaffold.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/calendar/views/weekly_page.dart';
import '../../features/calendar/views/monthly_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/onboarding/views/splash_page.dart';
import '../../features/onboarding/views/onboarding_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/parent/bloc/parent_bloc.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';
import '../../features/search/bloc/search_bloc.dart';
import '../../data/repositories/journal_repository.dart';
import '../../data/remote/api_client.dart';
// Shell 分支键
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
/// 不需要认证的白名单路径
const _publicPaths = ['/splash', '/onboarding', '/login', '/role-selection', '/class-code'];
/// 创建路由配置 — 需要注入 AuthBloc
GoRouter createAppRouter(AuthBloc authBloc) {
return GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/splash',
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: '/splash',
name: 'splash',
builder: (context, state) => const SplashPage(),
),
GoRoute(
path: '/onboarding',
name: 'onboarding',
builder: (context, state) => const OnboardingPage(),
),
// 认证路由(无 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: '/discover',
name: 'discover',
builder: (context, state) {
final journalRepo = context.read<JournalRepository>();
return BlocProvider(
create: (_) => SearchBloc(journalRepository: journalRepo),
child: 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'];
final templateId = state.uri.queryParameters['template'];
return EditorPage(journalId: journalId, templateId: templateId);
},
),
// 心情追踪(全屏,从首页心情卡片进入)
GoRoute(
path: '/mood',
name: 'mood',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const MoodPage(),
),
// 周概览(全屏,从日历页进入)
GoRoute(
path: '/weekly',
name: 'weekly',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const WeeklyPage(),
),
// 月度概览(全屏,从日历页进入)
GoRoute(
path: '/monthly',
name: 'monthly',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const MonthlyPage(),
),
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) {
return BlocProvider(
create: (_) => ParentBloc(api: context.read<ApiClient>()),
child: 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 映射4 项: 首页=0, 日历=1, 发现=2, 我的=3
int _selectedIndexFromLocation(String location) {
if (location.startsWith('/calendar')) return 1;
if (location.startsWith('/discover')) return 2;
if (location.startsWith('/profile')) return 3;
return 0; // /home 或未知路径
}
/// 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
/// TabBar: 首页(0) / 日历(1) / 发现(2) / 我的(3)
/// 中心 FAB: 写日记
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('/discover');
case 3:
context.go('/profile');
}
},
body: child,
onCenterButtonPressed: () => context.push('/editor'),
);
}
}