213 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			213 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Dart
		
	
	
| // main.dart
 | ||
| import 'dart:async';
 | ||
| import 'package:flutter/material.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();
 | ||
| 
 | ||
|   // 初始化 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: '',
 | ||
|       navigatorKey: navigatorKey,
 | ||
|       // 在路由变化时统一取消焦点(防止 push/pop 时焦点回到 TextField)
 | ||
|       navigatorObservers: [KeyboardUnfocusNavigatorObserver(),routeObserver],
 | ||
|       builder: (context, child) {
 | ||
|         return EasyLoading.init(
 | ||
|           builder: (context, widget) {
 | ||
|             return GestureDetector(
 | ||
|               behavior: HitTestBehavior.translucent,
 | ||
|               onTap: () {
 | ||
|                 // 全局点击空白处取消焦点(隐藏键盘)
 | ||
|                 FocusHelper.clearFocus(context);
 | ||
|               },
 | ||
|               child: widget,
 | ||
|             );
 | ||
|           },
 | ||
|         )(context, child);
 | ||
|       },
 | ||
|       theme: ThemeData(
 | ||
|         textTheme: const TextTheme(
 | ||
|           bodyMedium: TextStyle(fontSize: 14), // 默认字体大小
 | ||
|         ),
 | ||
|         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);
 | ||
|   }
 | ||
| }
 |