架构治理: - 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
117 lines
4.1 KiB
Dart
117 lines
4.1 KiB
Dart
// 暖记 App 根组件 — MaterialApp + BLoC Provider 注入
|
||
//
|
||
// 依赖注入结构:
|
||
// MultiRepositoryProvider
|
||
// ├─ ApiClient
|
||
// ├─ AuthRepository
|
||
// ├─ JournalRepository (IsarJournalRepository — 离线优先)
|
||
// ├─ RemoteJournalRepository (供 SyncEngine 使用)
|
||
// └─ ClassRepository
|
||
// └─ BlocProvider<AuthBloc>
|
||
// └─ MaterialApp.router
|
||
|
||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:provider/provider.dart' show ListenableProvider;
|
||
|
||
import 'config/app_config.dart';
|
||
import 'core/theme/app_theme.dart';
|
||
import 'core/routing/app_router.dart';
|
||
import 'data/remote/api_client.dart';
|
||
import 'data/repositories/auth_repository.dart';
|
||
import 'data/repositories/journal_repository.dart';
|
||
import 'data/repositories/isar_journal_repository.dart';
|
||
import 'data/repositories/remote_journal_repository.dart';
|
||
import 'data/repositories/class_repository.dart';
|
||
import 'data/services/sync_engine.dart';
|
||
import 'features/auth/bloc/auth_bloc.dart';
|
||
import 'features/profile/bloc/settings_bloc.dart';
|
||
|
||
/// 暖记 App — 根组件
|
||
class NuanjiApp extends StatelessWidget {
|
||
const NuanjiApp({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 创建全局依赖(App 生命周期内单例)
|
||
final config = AppConfig.fromEnvironment();
|
||
final apiClient = ApiClient(baseUrl: config.apiBaseUrl);
|
||
final authRepository = AuthRepository(apiClient: apiClient);
|
||
// 离线优先:Isar 为主要本地仓库,Remote 供 SyncEngine 推送
|
||
// Web 平台:Isar 3.x 不支持 Web,直接使用远程仓库
|
||
final journalRepository = kIsWeb
|
||
? RemoteJournalRepository(api: apiClient)
|
||
: IsarJournalRepository();
|
||
final remoteJournalRepository = RemoteJournalRepository(api: apiClient);
|
||
final syncEngine = SyncEngine(apiClient: apiClient);
|
||
final classRepository = ClassRepository(api: apiClient);
|
||
final settingsBloc = SettingsBloc();
|
||
final authBloc = AuthBloc(
|
||
authRepository: authRepository,
|
||
classRepository: classRepository,
|
||
);
|
||
|
||
// 启动时检查认证状态
|
||
authBloc.add(const AppStarted());
|
||
|
||
// 异步恢复 SyncEngine 持久化队列(fire-and-forget,不阻塞 UI)
|
||
syncEngine.restorePendingQueue();
|
||
|
||
// 认证状态监听:登出时清除 token
|
||
// 注意:登录时 token 由 AuthRepository.login() 直接注入 ApiClient
|
||
authBloc.stream.listen((state) {
|
||
if (state is! Authenticated) {
|
||
apiClient.clearToken();
|
||
}
|
||
});
|
||
|
||
return MultiRepositoryProvider(
|
||
providers: [
|
||
RepositoryProvider<ApiClient>.value(value: apiClient),
|
||
RepositoryProvider<AuthRepository>.value(value: authRepository),
|
||
RepositoryProvider<JournalRepository>.value(value: journalRepository),
|
||
RepositoryProvider<RemoteJournalRepository>.value(value: remoteJournalRepository),
|
||
RepositoryProvider<SyncEngine>.value(value: syncEngine),
|
||
RepositoryProvider<ClassRepository>.value(value: classRepository),
|
||
],
|
||
child: ListenableProvider<SettingsBloc>.value(
|
||
value: settingsBloc,
|
||
child: Builder(
|
||
builder: (context) {
|
||
final settings = context.watch<SettingsBloc>();
|
||
return BlocProvider<AuthBloc>.value(
|
||
value: authBloc,
|
||
child: _AppView(
|
||
router: createAppRouter(authBloc),
|
||
themeMode: settings.state.themeMode,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// App 视图 — MaterialApp.router 包装
|
||
class _AppView extends StatelessWidget {
|
||
final GoRouter router;
|
||
final ThemeMode themeMode;
|
||
|
||
const _AppView({required this.router, this.themeMode = ThemeMode.system});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp.router(
|
||
title: '暖记',
|
||
debugShowCheckedModeBanner: false,
|
||
theme: AppTheme.light(),
|
||
darkTheme: AppTheme.dark(),
|
||
themeMode: themeMode,
|
||
routerConfig: router,
|
||
);
|
||
}
|
||
}
|