242 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			242 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Dart
		
	
	
| // main.dart
 | ||
| import 'dart:async';
 | ||
| import 'package:flutter/material.dart';
 | ||
| import 'package:flutter_localizations/flutter_localizations.dart';
 | ||
| import 'package:qhd_prevention/pages/badge_manager.dart';
 | ||
| import 'package:qhd_prevention/services/auth_service.dart';
 | ||
| import 'package:qhd_prevention/tools/tools.dart';
 | ||
| import 'package:shared_preferences/shared_preferences.dart';
 | ||
| import './pages/login_page.dart';
 | ||
| import './pages/main_tab.dart';
 | ||
| import 'package:intl/date_symbol_data_local.dart';
 | ||
| import 'http/HttpManager.dart';
 | ||
| import 'package:flutter_easyloading/flutter_easyloading.dart';
 | ||
| import 'package:flutter/services.dart'; // for TextInput.hide
 | ||
| 
 | ||
| // 全局导航键
 | ||
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
 | ||
| // 全局路由
 | ||
| final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
 | ||
| 
 | ||
| // 全局消息控制器
 | ||
| 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),
 | ||
|         ),
 | ||
|       );
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// 全局 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,
 | ||
|   );
 | ||
| }
 | ||
| 
 | ||
| void main() async {
 | ||
|   WidgetsFlutterBinding.ensureInitialized();
 | ||
|   await SystemChrome.setPreferredOrientations([
 | ||
|     DeviceOrientation.portraitUp,
 | ||
|     DeviceOrientation.portraitDown,
 | ||
|   ]);
 | ||
|   // 初始化 EasyLoading
 | ||
|   EasyLoading.instance
 | ||
|     ..displayDuration = const Duration(seconds: 20)
 | ||
|     ..indicatorType = EasyLoadingIndicatorType.ring
 | ||
|     ..loadingStyle = EasyLoadingStyle.custom
 | ||
|     ..indicatorSize = 36.0
 | ||
|     ..radius = 0
 | ||
|     ..progressColor = Colors.blue
 | ||
|     ..backgroundColor = Colors.grey.shade300
 | ||
|     ..indicatorColor = Colors.blue
 | ||
|     ..textColor = Colors.black
 | ||
|     ..userInteractions = false
 | ||
|     ..dismissOnTap = false;
 | ||
| 
 | ||
|   await initializeDateFormatting('zh_CN', null);
 | ||
| 
 | ||
|   // 初始化HTTP管理器未授权回调
 | ||
|   HttpManager.onUnauthorized = () async {
 | ||
|     final prefs = await SharedPreferences.getInstance();
 | ||
|     await prefs.setBool('isLoggedIn', false);
 | ||
|     await prefs.remove('token');
 | ||
|     navigatorKey.currentState?.pushNamedAndRemoveUntil(
 | ||
|       '/login',
 | ||
|           (route) => false,
 | ||
|     );
 | ||
| 
 | ||
|     Future.delayed(const Duration(milliseconds: 100), () {
 | ||
|       GlobalMessage.showError('您的账号已在其他设备登录,已自动下线,请使用单一设备进行学习。');
 | ||
|     });
 | ||
|   };
 | ||
|   // 自动登录逻辑
 | ||
|   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;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   runApp(MyApp(isLoggedIn: isLoggedIn));
 | ||
| }
 | ||
| 
 | ||
| /// MyApp:恢复为 Stateless(无需监听 viewInsets)
 | ||
| class MyApp extends StatelessWidget {
 | ||
|   final bool isLoggedIn;
 | ||
| 
 | ||
|   const MyApp({super.key, required this.isLoggedIn});
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     return MaterialApp(
 | ||
|       title: '',
 | ||
|       localizationsDelegates: [
 | ||
|         GlobalMaterialLocalizations.delegate,
 | ||
|         GlobalCupertinoLocalizations.delegate,
 | ||
|         GlobalWidgetsLocalizations.delegate,
 | ||
|         // 如果使用了其他本地化包,请添加对应的 delegate
 | ||
|       ],
 | ||
|       supportedLocales: const [
 | ||
|         Locale('zh', 'CN'), // 中文
 | ||
|         Locale('en', 'US'), // 英文(备用)
 | ||
|       ],
 | ||
|       locale: const Locale('zh', 'CN'),
 | ||
|       // 强制使用中文
 | ||
|       navigatorKey: navigatorKey,
 | ||
|       // 在路由变化时统一取消焦点(防止 push/pop 时焦点回到 TextField)
 | ||
|       navigatorObservers: [KeyboardUnfocusNavigatorObserver(), routeObserver],
 | ||
|       builder: (context, child) {
 | ||
|         // 使用 EasyLoading.init,同时在其内部把 textScaleFactor 固定为 1.0
 | ||
|         return EasyLoading.init(
 | ||
|           builder: (context, widget) {
 | ||
|             // 拿到当前 MediaQuery 并创建一个 textScaleFactor = 1.0 的副本
 | ||
|             final mq = MediaQuery.maybeOf(context) ?? MediaQueryData.fromWindow(WidgetsBinding.instance.window);
 | ||
|             final fixed = mq.copyWith(textScaleFactor: 1.0);
 | ||
| 
 | ||
|             return MediaQuery(
 | ||
|               data: fixed,
 | ||
|               child: GestureDetector(
 | ||
|                 behavior: HitTestBehavior.translucent,
 | ||
|                 onTap: () {
 | ||
|                   // 全局点击空白处取消焦点(隐藏键盘)
 | ||
|                   try {
 | ||
|                     FocusManager.instance.primaryFocus?.unfocus();
 | ||
|                   } catch (e) {
 | ||
|                     debugPrint('NavigatorObserver unfocus error: $e');
 | ||
|                   }
 | ||
|                 },
 | ||
|                 child: widget,
 | ||
|               ),
 | ||
|             );
 | ||
|           },
 | ||
|         )(context, child);
 | ||
|       },
 | ||
|       theme: ThemeData(
 | ||
|         textTheme: const TextTheme(
 | ||
|           bodyMedium: TextStyle(fontSize: 13), // 默认字体大小
 | ||
|         ),
 | ||
|         dividerTheme: const DividerThemeData(
 | ||
|           color: Colors.black12,
 | ||
|           thickness: .5,
 | ||
|           indent: 0,
 | ||
|           endIndent: 0,
 | ||
|         ),
 | ||
|         primarySwatch: Colors.blue,
 | ||
|         scaffoldBackgroundColor: const Color(0xFFF1F1F1),
 | ||
|         colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
 | ||
|         inputDecorationTheme: const InputDecorationTheme(
 | ||
|           border: InputBorder.none,
 | ||
|           contentPadding: EdgeInsets.symmetric(horizontal: 8),
 | ||
|         ),
 | ||
|         snackBarTheme: const SnackBarThemeData(
 | ||
|           behavior: SnackBarBehavior.floating,
 | ||
|           shape: RoundedRectangleBorder(
 | ||
|             borderRadius: BorderRadius.all(Radius.circular(8)),
 | ||
|           ),
 | ||
|         ),
 | ||
|         progressIndicatorTheme: const ProgressIndicatorThemeData(
 | ||
|           color: Colors.blue,
 | ||
|         ),
 | ||
|       ),
 | ||
|       home: isLoggedIn ? const MainPage() : const LoginPage(),
 | ||
|       debugShowCheckedModeBanner: false,
 | ||
|       routes: {'/login': (_) => const LoginPage()},
 | ||
|     );
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// 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);
 | ||
|   }
 | ||
| }
 |