feat(app): D.3 中等优先级 UX 改进 — 保存指示器 + 触摸目标 + 主题持久化
D.3.2 三态保存指示器: - 未保存 (灰色) → 保存中 (琥珀色脉冲点) → 已保存 (绿色点) - _PulsingDot 动画组件,800ms 呼吸效果 - 点击'完成'时显示保存中状态 D.3.3 工具栏触摸目标: - BoxConstraints 36x36 → 44x44,符合 WCAG 标准 D.3.4 主题偏好持久化: - SettingsBloc 接受 SharedPreferences,保存/恢复 themeMode - NuanjiApp 改为 StatefulWidget,异步初始化 SharedPreferences - 启动时显示 loading,初始化完成后渲染 app
This commit is contained in:
104
app/lib/app.dart
104
app/lib/app.dart
@@ -15,6 +15,7 @@ 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 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'config/app_config.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
@@ -31,70 +32,99 @@ import 'features/auth/bloc/auth_bloc.dart';
|
||||
import 'features/profile/bloc/settings_bloc.dart';
|
||||
|
||||
/// 暖记 App — 根组件
|
||||
class NuanjiApp extends StatelessWidget {
|
||||
class NuanjiApp extends StatefulWidget {
|
||||
const NuanjiApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 创建全局依赖(App 生命周期内单例)
|
||||
// 开发模式默认使用 localhost,可通过 --dart-define 覆盖
|
||||
State<NuanjiApp> createState() => _NuanjiAppState();
|
||||
}
|
||||
|
||||
class _NuanjiAppState extends State<NuanjiApp> {
|
||||
late final ApiClient _apiClient;
|
||||
late final AuthRepository _authRepository;
|
||||
late final JournalRepository _journalRepository;
|
||||
late final RemoteJournalRepository _remoteJournalRepository;
|
||||
late final SyncEngine _syncEngine;
|
||||
late final ClassRepository _classRepository;
|
||||
late final SettingsBloc _settingsBloc;
|
||||
late final AuthBloc _authBloc;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initApp();
|
||||
}
|
||||
|
||||
Future<void> _initApp() async {
|
||||
final config = kDebugMode ? AppConfig.dev : AppConfig.fromEnvironment();
|
||||
final apiClient = ApiClient(baseUrl: config.apiBaseUrl);
|
||||
_apiClient = ApiClient(baseUrl: config.apiBaseUrl);
|
||||
final tokenStore = createSecureTokenStore();
|
||||
final authRepository = AuthRepository(apiClient: apiClient, tokenStore: tokenStore);
|
||||
// 离线优先:Isar 为主要本地仓库,Remote 供 SyncEngine 推送
|
||||
// Web 平台:Isar 3.x 不支持 Web,直接使用远程仓库
|
||||
final journalRepository = kIsWeb
|
||||
? RemoteJournalRepository(api: apiClient)
|
||||
_authRepository = AuthRepository(apiClient: _apiClient, tokenStore: tokenStore);
|
||||
_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,
|
||||
_remoteJournalRepository = RemoteJournalRepository(api: _apiClient);
|
||||
_syncEngine = SyncEngine(apiClient: _apiClient);
|
||||
_classRepository = ClassRepository(api: _apiClient);
|
||||
_settingsBloc = SettingsBloc(
|
||||
prefs: await SharedPreferences.getInstance(),
|
||||
);
|
||||
_authBloc = AuthBloc(
|
||||
authRepository: _authRepository,
|
||||
classRepository: _classRepository,
|
||||
);
|
||||
|
||||
// 启动时检查认证状态
|
||||
authBloc.add(const AppStarted());
|
||||
_authBloc.add(const AppStarted());
|
||||
|
||||
// 异步恢复 SyncEngine 持久化队列(fire-and-forget,不阻塞 UI)
|
||||
syncEngine.restorePendingQueue();
|
||||
// 启动网络监听 — 网络恢复时自动触发 trySync()
|
||||
syncEngine.startAutoSync();
|
||||
// 异步恢复 SyncEngine 持久化队列
|
||||
_syncEngine.restorePendingQueue();
|
||||
_syncEngine.startAutoSync();
|
||||
|
||||
// 认证状态监听:登出时清除 token
|
||||
// 注意:登录时 token 由 AuthRepository.login() 直接注入 ApiClient
|
||||
authBloc.stream.listen((state) {
|
||||
_authBloc.stream.listen((state) {
|
||||
if (state is! Authenticated) {
|
||||
apiClient.clearToken();
|
||||
_apiClient.clearToken();
|
||||
}
|
||||
});
|
||||
|
||||
// Token 刷新彻底失败时 → 派发 AuthExpired → 路由重定向到登录页
|
||||
apiClient.onAuthFailed = () {
|
||||
authBloc.add(const AuthExpired());
|
||||
// Token 刷新彻底失败时 → 派发 AuthExpired
|
||||
_apiClient.onAuthFailed = () {
|
||||
_authBloc.add(const AuthExpired());
|
||||
};
|
||||
|
||||
setState(() => _initialized = true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_initialized) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: Scaffold(body: Center(child: CircularProgressIndicator())),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
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,
|
||||
value: _settingsBloc,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.watch<SettingsBloc>();
|
||||
return BlocProvider<AuthBloc>.value(
|
||||
value: authBloc,
|
||||
value: _authBloc,
|
||||
child: _AppView(
|
||||
router: createAppRouter(authBloc),
|
||||
router: createAppRouter(_authBloc),
|
||||
themeMode: settings.state.themeMode,
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user