| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | // main.dart
 | 
					
						
							|  |  |  |  | import 'dart:async'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:flutter/material.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-07 17:33:16 +08:00
										 |  |  |  | import 'package:qhd_prevention/pages/badge_manager.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-21 16:44:24 +08:00
										 |  |  |  | import 'package:qhd_prevention/services/auth_service.dart'; | 
					
						
							|  |  |  |  | import 'package:qhd_prevention/tools/tools.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:shared_preferences/shared_preferences.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | import './pages/login_page.dart'; | 
					
						
							|  |  |  |  | import './pages/main_tab.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:intl/date_symbol_data_local.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | import 'http/HttpManager.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-08 10:52:15 +08:00
										 |  |  |  | import 'package:flutter_easyloading/flutter_easyloading.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | import 'package:flutter/services.dart'; // for TextInput.hide
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | // 全局导航键
 | 
					
						
							|  |  |  |  | final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | // 全局路由
 | 
					
						
							|  |  |  |  | final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>(); | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | // 全局消息控制器
 | 
					
						
							|  |  |  |  | class GlobalMessage { | 
					
						
							|  |  |  |  |   static void showError(String message) { | 
					
						
							|  |  |  |  |     final context = navigatorKey.currentContext; | 
					
						
							|  |  |  |  |     if (context != null) { | 
					
						
							|  |  |  |  |       ScaffoldMessenger.of(context).showSnackBar( | 
					
						
							|  |  |  |  |         SnackBar( | 
					
						
							|  |  |  |  |           content: Text(message), | 
					
						
							|  |  |  |  |           backgroundColor: Colors.red, | 
					
						
							|  |  |  |  |           duration: const Duration(seconds: 3), | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |       ); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | /// 全局 helper:在弹窗前取消焦点并尽量隐藏键盘,避免弹窗后键盘自动弹起
 | 
					
						
							|  |  |  |  | Future<T?> showDialogAfterUnfocus<T>(BuildContext context, Widget dialog) async { | 
					
						
							|  |  |  |  |   // 取消焦点并尝试隐藏键盘
 | 
					
						
							|  |  |  |  |   FocusScope.of(context).unfocus(); | 
					
						
							|  |  |  |  |   try { | 
					
						
							|  |  |  |  |     await SystemChannels.textInput.invokeMethod('TextInput.hide'); | 
					
						
							|  |  |  |  |   } catch (_) {} | 
					
						
							|  |  |  |  |   // 给系统一点时间,避免竞态(100-200ms 足够)
 | 
					
						
							|  |  |  |  |   await Future.delayed(const Duration(milliseconds: 150)); | 
					
						
							|  |  |  |  |   return showDialog<T>(context: context, builder: (_) => dialog); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /// 同理:在展示底部模态前确保键盘隐藏
 | 
					
						
							|  |  |  |  | Future<T?> showModalBottomSheetAfterUnfocus<T>({ | 
					
						
							|  |  |  |  |   required BuildContext context, | 
					
						
							|  |  |  |  |   required WidgetBuilder builder, | 
					
						
							|  |  |  |  |   bool isScrollControlled = false, | 
					
						
							|  |  |  |  | }) async { | 
					
						
							|  |  |  |  |   FocusScope.of(context).unfocus(); | 
					
						
							|  |  |  |  |   try { | 
					
						
							|  |  |  |  |     await SystemChannels.textInput.invokeMethod('TextInput.hide'); | 
					
						
							|  |  |  |  |   } catch (_) {} | 
					
						
							|  |  |  |  |   await Future.delayed(const Duration(milliseconds: 150)); | 
					
						
							|  |  |  |  |   return showModalBottomSheet<T>( | 
					
						
							|  |  |  |  |     context: context, | 
					
						
							|  |  |  |  |     isScrollControlled: isScrollControlled, | 
					
						
							|  |  |  |  |     builder: builder, | 
					
						
							|  |  |  |  |   ); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | void main() async { | 
					
						
							|  |  |  |  |   WidgetsFlutterBinding.ensureInitialized(); | 
					
						
							| 
									
										
										
										
											2025-08-08 10:52:15 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 初始化 EasyLoading
 | 
					
						
							|  |  |  |  |   EasyLoading.instance | 
					
						
							| 
									
										
										
										
											2025-08-11 17:40:03 +08:00
										 |  |  |  |     ..displayDuration = const Duration(seconds: 20) | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |     ..indicatorType = EasyLoadingIndicatorType.ring | 
					
						
							|  |  |  |  |     ..loadingStyle = EasyLoadingStyle.custom | 
					
						
							|  |  |  |  |     ..indicatorSize = 36.0 | 
					
						
							|  |  |  |  |     ..radius = 0 | 
					
						
							|  |  |  |  |     ..progressColor = Colors.blue | 
					
						
							| 
									
										
										
										
											2025-08-12 10:57:07 +08:00
										 |  |  |  |     ..backgroundColor = Colors.grey.shade300 | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |     ..indicatorColor = Colors.blue | 
					
						
							|  |  |  |  |     ..textColor = Colors.black | 
					
						
							|  |  |  |  |     ..userInteractions = false | 
					
						
							| 
									
										
										
										
											2025-08-08 10:52:15 +08:00
										 |  |  |  |     ..dismissOnTap = false; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   await initializeDateFormatting('zh_CN', null); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |   // 初始化HTTP管理器未授权回调
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   HttpManager.onUnauthorized = () async { | 
					
						
							|  |  |  |  |     final prefs = await SharedPreferences.getInstance(); | 
					
						
							|  |  |  |  |     await prefs.setBool('isLoggedIn', false); | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |     await prefs.remove('token'); | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |     navigatorKey.currentState?.pushNamedAndRemoveUntil( | 
					
						
							|  |  |  |  |       '/login', | 
					
						
							|  |  |  |  |           (route) => false, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     Future.delayed(const Duration(milliseconds: 100), () { | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |       GlobalMessage.showError('您的账号已在其他设备登录,已自动下线,请使用单一设备进行学习。'); | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |     }); | 
					
						
							|  |  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |   // 自动登录逻辑
 | 
					
						
							|  |  |  |  |   final prefs = await SharedPreferences.getInstance(); | 
					
						
							|  |  |  |  |   final savedLogin = prefs.getBool('isLoggedIn') ?? false; | 
					
						
							|  |  |  |  |   bool isLoggedIn = false; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   if (savedLogin) { | 
					
						
							|  |  |  |  |     // 如果本地标记已登录,进一步验证 token 是否有效
 | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       isLoggedIn = await AuthService.isLoggedIn(); | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       isLoggedIn = false; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   runApp(MyApp(isLoggedIn: isLoggedIn)); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | /// MyApp:恢复为 Stateless(无需监听 viewInsets)
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | class MyApp extends StatelessWidget { | 
					
						
							|  |  |  |  |   final bool isLoggedIn; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |   const MyApp({super.key, required this.isLoggedIn}); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |  |     return MaterialApp( | 
					
						
							|  |  |  |  |       title: '', | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |       navigatorKey: navigatorKey, | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |       // 在路由变化时统一取消焦点(防止 push/pop 时焦点回到 TextField)
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |       navigatorObservers: [KeyboardUnfocusNavigatorObserver(),routeObserver], | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       builder: (context, child) { | 
					
						
							| 
									
										
										
										
											2025-08-08 10:52:15 +08:00
										 |  |  |  |         return EasyLoading.init( | 
					
						
							|  |  |  |  |           builder: (context, widget) { | 
					
						
							|  |  |  |  |             return GestureDetector( | 
					
						
							|  |  |  |  |               behavior: HitTestBehavior.translucent, | 
					
						
							|  |  |  |  |               onTap: () { | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  |                 // 全局点击空白处取消焦点(隐藏键盘)
 | 
					
						
							| 
									
										
										
										
											2025-08-22 09:02:35 +08:00
										 |  |  |  |                 FocusHelper.clearFocus(context); | 
					
						
							| 
									
										
										
										
											2025-08-08 10:52:15 +08:00
										 |  |  |  |               }, | 
					
						
							|  |  |  |  |               child: widget, | 
					
						
							|  |  |  |  |             ); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |           }, | 
					
						
							| 
									
										
										
										
											2025-08-08 10:52:15 +08:00
										 |  |  |  |         )(context, child); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       }, | 
					
						
							|  |  |  |  |       theme: ThemeData( | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         textTheme: const TextTheme( | 
					
						
							|  |  |  |  |           bodyMedium: TextStyle(fontSize: 14), // 默认字体大小
 | 
					
						
							|  |  |  |  |         ), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |         dividerTheme: const DividerThemeData( | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |           color: Colors.black12, | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |           thickness: .5, | 
					
						
							|  |  |  |  |           indent: 0, | 
					
						
							|  |  |  |  |           endIndent: 0, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |         ), | 
					
						
							|  |  |  |  |         primarySwatch: Colors.blue, | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |         scaffoldBackgroundColor: const Color(0xFFF1F1F1), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |         colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), | 
					
						
							|  |  |  |  |         inputDecorationTheme: const InputDecorationTheme( | 
					
						
							|  |  |  |  |           border: InputBorder.none, | 
					
						
							|  |  |  |  |           contentPadding: EdgeInsets.symmetric(horizontal: 8), | 
					
						
							|  |  |  |  |         ), | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |         snackBarTheme: const SnackBarThemeData( | 
					
						
							|  |  |  |  |           behavior: SnackBarBehavior.floating, | 
					
						
							|  |  |  |  |           shape: RoundedRectangleBorder( | 
					
						
							|  |  |  |  |             borderRadius: BorderRadius.all(Radius.circular(8)), | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |         ), | 
					
						
							| 
									
										
										
										
											2025-08-08 10:52:15 +08:00
										 |  |  |  |         progressIndicatorTheme: const ProgressIndicatorThemeData( | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |           color: Colors.blue, | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |         ), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |       ), | 
					
						
							|  |  |  |  |       home: isLoggedIn ? const MainPage() : const LoginPage(), | 
					
						
							|  |  |  |  |       debugShowCheckedModeBanner: false, | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |       routes: { | 
					
						
							|  |  |  |  |         '/login': (_) => const LoginPage(), | 
					
						
							|  |  |  |  |       }, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-08-27 16:14:50 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | /// NavigatorObserver:在 push/pop/remove/replace 等路由变化时统一取消焦点
 | 
					
						
							|  |  |  |  | class KeyboardUnfocusNavigatorObserver extends NavigatorObserver { | 
					
						
							|  |  |  |  |   void _unfocus() { | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       FocusManager.instance.primaryFocus?.unfocus(); | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       debugPrint('NavigatorObserver unfocus error: $e'); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void didPush(Route route, Route? previousRoute) { | 
					
						
							|  |  |  |  |     _unfocus(); | 
					
						
							|  |  |  |  |     super.didPush(route, previousRoute); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void didPop(Route route, Route? previousRoute) { | 
					
						
							|  |  |  |  |     _unfocus(); | 
					
						
							|  |  |  |  |     super.didPop(route, previousRoute); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void didRemove(Route route, Route? previousRoute) { | 
					
						
							|  |  |  |  |     _unfocus(); | 
					
						
							|  |  |  |  |     super.didRemove(route, previousRoute); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void didReplace({Route? newRoute, Route? oldRoute}) { | 
					
						
							|  |  |  |  |     _unfocus(); | 
					
						
							|  |  |  |  |     super.didReplace(newRoute: newRoute, oldRoute: oldRoute); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } |