2025-08-27 16:14:50 +08:00
|
|
|
|
// main.dart
|
|
|
|
|
import 'dart:async';
|
2025-09-06 17:38:07 +08:00
|
|
|
|
import 'dart:io';
|
2025-07-11 11:03:21 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
2025-09-01 17:25:55 +08:00
|
|
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
2025-08-21 16:44:24 +08:00
|
|
|
|
import 'package:qhd_prevention/services/auth_service.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-09-06 17:38:07 +08:00
|
|
|
|
import 'package:flutter_baidu_mapapi_base/flutter_baidu_mapapi_base.dart'
|
|
|
|
|
show BMFMapSDK, BMF_COORD_TYPE;
|
|
|
|
|
import 'package:flutter_baidu_mapapi_map/flutter_baidu_mapapi_map.dart';
|
2025-07-18 17:13:38 +08:00
|
|
|
|
|
2025-09-09 20:08:10 +08:00
|
|
|
|
import 'pages/mine/mine_set_pwd_page.dart';
|
|
|
|
|
|
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-09-18 21:45:41 +08:00
|
|
|
|
bool _isLoggingOut = false;
|
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:在弹窗前取消焦点并尽量隐藏键盘,避免弹窗后键盘自动弹起
|
2025-09-01 17:25:55 +08:00
|
|
|
|
Future<T?> showDialogAfterUnfocus<T>(
|
2025-09-05 09:16:54 +08:00
|
|
|
|
BuildContext context,
|
|
|
|
|
Widget dialog,
|
|
|
|
|
) async {
|
2025-08-27 16:14:50 +08:00
|
|
|
|
// 取消焦点并尝试隐藏键盘
|
|
|
|
|
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-09-09 20:08:10 +08:00
|
|
|
|
void main( ) async {
|
2025-07-11 11:03:21 +08:00
|
|
|
|
WidgetsFlutterBinding.ensureInitialized();
|
2025-09-06 17:38:07 +08:00
|
|
|
|
|
|
|
|
|
// 1) 同意 SDK 隐私(百度 SDK 要求)
|
|
|
|
|
BMFMapSDK.setAgreePrivacy(true);
|
|
|
|
|
|
|
|
|
|
// 2) 初始化鉴权 / 坐标类型
|
|
|
|
|
if (Platform.isIOS) {
|
|
|
|
|
// iOS 可以通过接口直接设置 AK
|
|
|
|
|
BMFMapSDK.setApiKeyAndCoordType('g3lZyqt0KkFnZGUsjIVO7U6lTCfpjSCt', BMF_COORD_TYPE.BD09LL);
|
|
|
|
|
} else if (Platform.isAndroid) {
|
|
|
|
|
// Android 插件示例:Android 的 AK 通过 Manifest 配置
|
|
|
|
|
BMFMapSDK.setApiKeyAndCoordType('43G1sKuHV6oRTrdR9VTIGPF9soej7V5a', BMF_COORD_TYPE.BD09LL);
|
|
|
|
|
await BMFAndroidVersion.initAndroidVersion(); // 可选,插件示例中有
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 17:25:55 +08:00
|
|
|
|
await SystemChrome.setPreferredOrientations([
|
|
|
|
|
DeviceOrientation.portraitUp,
|
|
|
|
|
DeviceOrientation.portraitDown,
|
|
|
|
|
]);
|
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 {
|
2025-09-18 21:45:41 +08:00
|
|
|
|
final navigatorState = navigatorKey.currentState;
|
|
|
|
|
if (navigatorState == null) return;
|
|
|
|
|
|
|
|
|
|
// 尝试获取当前路由名(注意:ModalRoute 可能为 null)
|
|
|
|
|
final currentRouteName = ModalRoute.of(navigatorState.context)?.settings.name;
|
|
|
|
|
if (currentRouteName == '/login') {
|
|
|
|
|
// 已在登录页,直接返回,避免重复登出流程与提示
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_isLoggingOut) return; // 防止并发多次触发
|
|
|
|
|
_isLoggingOut = true;
|
2025-07-18 17:13:38 +08:00
|
|
|
|
|
2025-09-18 21:45:41 +08:00
|
|
|
|
try {
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
|
await prefs.setBool('isLoggedIn', false);
|
|
|
|
|
await prefs.remove('token');
|
|
|
|
|
|
|
|
|
|
navigatorState.pushNamedAndRemoveUntil('/login', (route) => false);
|
|
|
|
|
|
|
|
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
|
|
|
GlobalMessage.showError('您的账号已在其他设备登录,已自动下线,请使用单一设备进行学习。');
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
_isLoggingOut = false;
|
|
|
|
|
}
|
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;
|
2025-09-09 20:08:10 +08:00
|
|
|
|
// bool isLoggedIn = false;
|
|
|
|
|
Map<String, dynamic> data={};
|
2025-08-19 11:06:16 +08:00
|
|
|
|
|
|
|
|
|
if (savedLogin) {
|
|
|
|
|
// 如果本地标记已登录,进一步验证 token 是否有效
|
|
|
|
|
try {
|
2025-09-09 20:08:10 +08:00
|
|
|
|
// isLoggedIn = await AuthService.isLoggedIn();
|
|
|
|
|
data =await AuthService.isLoggedIn();
|
|
|
|
|
// if (data.isEmpty||data['result']!= 'success') {
|
|
|
|
|
// isLoggedIn =false;
|
|
|
|
|
// }else{
|
|
|
|
|
// isLoggedIn = true;
|
|
|
|
|
// }
|
2025-08-19 11:06:16 +08:00
|
|
|
|
} catch (e) {
|
2025-09-09 20:08:10 +08:00
|
|
|
|
data={};
|
|
|
|
|
// isLoggedIn = false;
|
2025-08-19 11:06:16 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 20:08:10 +08:00
|
|
|
|
// runApp(MyApp(isLoggedIn: isLoggedIn));
|
|
|
|
|
runApp(MyApp(data: data));
|
2025-07-11 11:03:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 16:14:50 +08:00
|
|
|
|
/// MyApp:恢复为 Stateless(无需监听 viewInsets)
|
2025-07-11 11:03:21 +08:00
|
|
|
|
class MyApp extends StatelessWidget {
|
2025-09-09 20:08:10 +08:00
|
|
|
|
// final bool isLoggedIn;
|
|
|
|
|
final Map<String, dynamic> data;
|
|
|
|
|
const MyApp({super.key, required this.data});
|
|
|
|
|
|
|
|
|
|
Widget getHomePage() {
|
|
|
|
|
|
|
|
|
|
// isLoggedIn ? const MainPage() : const LoginPage(),
|
|
|
|
|
if (data.isNotEmpty&&data['result'] == 'success') {
|
|
|
|
|
// data['LONG_TERM_PASSWORD_NOT_CHANGED']='1';
|
|
|
|
|
if(data['WEAK_PASSWORD']=='1'){
|
|
|
|
|
return const MineSetPwdPage("3");
|
2025-07-11 11:03:21 +08:00
|
|
|
|
|
2025-09-09 20:08:10 +08:00
|
|
|
|
}else if(data['LONG_TERM_PASSWORD_NOT_CHANGED']=='1'){
|
|
|
|
|
return const MineSetPwdPage("4");
|
|
|
|
|
}else{
|
|
|
|
|
return const MainPage();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}else{
|
|
|
|
|
return const LoginPage();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-11 11:03:21 +08:00
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return MaterialApp(
|
|
|
|
|
title: '',
|
2025-09-01 17:25:55 +08:00
|
|
|
|
localizationsDelegates: [
|
|
|
|
|
GlobalMaterialLocalizations.delegate,
|
|
|
|
|
GlobalCupertinoLocalizations.delegate,
|
|
|
|
|
GlobalWidgetsLocalizations.delegate,
|
|
|
|
|
// 如果使用了其他本地化包,请添加对应的 delegate
|
|
|
|
|
],
|
2025-09-05 09:16:54 +08:00
|
|
|
|
supportedLocales: const [
|
|
|
|
|
Locale('zh', 'CN'), // 中文
|
|
|
|
|
Locale('en', 'US'), // 英文(备用)
|
2025-09-01 17:25:55 +08:00
|
|
|
|
],
|
|
|
|
|
locale: const Locale('zh', 'CN'),
|
|
|
|
|
// 强制使用中文
|
2025-08-19 11:06:16 +08:00
|
|
|
|
navigatorKey: navigatorKey,
|
2025-08-27 16:14:50 +08:00
|
|
|
|
// 在路由变化时统一取消焦点(防止 push/pop 时焦点回到 TextField)
|
2025-09-01 17:25:55 +08:00
|
|
|
|
navigatorObservers: [KeyboardUnfocusNavigatorObserver(), routeObserver],
|
2025-07-11 11:03:21 +08:00
|
|
|
|
builder: (context, child) {
|
2025-09-05 09:16:54 +08:00
|
|
|
|
// 使用 EasyLoading.init,同时在其内部把 textScaleFactor 固定为 1.0
|
2025-08-08 10:52:15 +08:00
|
|
|
|
return EasyLoading.init(
|
|
|
|
|
builder: (context, widget) {
|
2025-09-05 09:16:54 +08:00
|
|
|
|
// 拿到当前 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,
|
|
|
|
|
),
|
2025-08-08 10:52:15 +08:00
|
|
|
|
);
|
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(
|
2025-09-01 17:25:55 +08:00
|
|
|
|
bodyMedium: TextStyle(fontSize: 13), // 默认字体大小
|
2025-08-29 09:52:48 +08:00
|
|
|
|
),
|
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
|
|
|
|
),
|
2025-09-09 20:08:10 +08:00
|
|
|
|
home:getHomePage(),
|
|
|
|
|
// isLoggedIn ? const MainPage() : const LoginPage(),
|
2025-07-11 11:03:21 +08:00
|
|
|
|
debugShowCheckedModeBanner: false,
|
2025-09-01 17:25:55 +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);
|
|
|
|
|
}
|
|
|
|
|
}
|