import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'app/router.dart'; import 'app/theme.dart'; import 'providers/locale_provider.dart'; import 'providers/theme_provider.dart'; import 'providers/display_provider.dart'; import 'utils/constants.dart'; import 'utils/credential_storage.dart'; import 'utils/i10n.dart'; import 'utils/store_local_cache.dart'; import 'config/prod_guard.dart'; /// 应用入口。 /// /// 三端(Android / iOS / HarmonyOS)共用同一套 lib/ 代码, /// 通过 [ProviderScope] 注入全局状态,使用 go_router 进行声明式路由。 /// 根组件 key;重试时更换以强制整棵树重建。 GlobalKey _appRootKey = GlobalKey(); void main() { // 全局 ErrorWidget:任何 widget 在 build/layout/paint 阶段抛异常时, // 直接显示可读的红色错误页,而不是 release 模式默认的灰色错误块(静默灰屏)。 ErrorWidget.builder = (FlutterErrorDetails details) { return _ErrorWidgetScreen( message: details.exceptionAsString(), stack: details.stack, ); }; // 框架级异常兜底:记录并写入 _fatalError,供根组件在 build 时渲染错误页。 // 注意:不再调用 FlutterError.presentError,避免默认灰屏覆盖自定义错误页。 FlutterError.onError = (details) { _reportFatal(details.exceptionAsString(), details.stack); }; // 异步/Zone 异常兜底。 PlatformDispatcher.instance.onError = (error, stack) { _reportFatal(error.toString(), stack); return true; }; runZonedGuarded( () async { WidgetsFlutterBinding.ensureInitialized(); // 启用 Edge-to-Edge 全屏:内容延伸至状态栏与导航栏区域, // 避免手机四周出现黑边;同时保持系统手势/状态栏可见。 await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // 状态栏 / 导航栏图标亮度跟随系统明暗(修复此前硬编码 Brightness.light // 导致浅色模式下图标不可见的 bug)。 final isDark = WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent, statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark, ), ); // 预初始化凭据持久化(记住密码 / 免登录恢复依赖 SharedPreferences 同步读取)。 await CredentialStorage.init(); // 生产环境强制守卫:启动即校验当前后端非生产,命中则失败退出并阻断启动。 ProductionGuard.enforce(); // 预初始化本地缓存(语言、主题等配置需要同步读取)。 // 即便失败也用默认值兜底,不让启动崩溃。 StoreLocalCache.instance.initialize().catchError((_) { // 忽略:SharedPreferences 不可用时退化为内存态。 }); // 应用已保存的显示方向偏好(跟随系统 / 竖屏 / 横屏), // 确保首帧即按用户设置的方向渲染,不闪一下系统默认朝向。 final savedOrientation = await loadSavedOrientation(); applyDisplayOrientation(savedOrientation); _launchApp(); }, (error, stack) { _reportFatal(error.toString(), stack); }, ); } /// 启动根组件(重试时再次调用即可重建整棵树)。 void _launchApp() { runApp( ProviderScope( key: _appRootKey, child: const OpsConsoleApp(), ), ); } /// 全局致命错误信息;非空时 [OpsConsoleApp] 渲染错误页而非正常界面。 String? _fatalError; /// 记录致命错误并写入全局变量,供 [OpsConsoleApp] 在 build 时渲染错误页。 void _reportFatal(Object error, StackTrace? stack) { if (_fatalError == null) { _fatalError = '$error\n\n${stack?.toString().split('\n').take(8).join('\n') ?? ''}'; } debugPrint('FATAL: $error'); debugPrint('STACK: $stack'); } /// 清空致命错误并重启根组件(错误页「重试」按钮调用)。 void _retryApp() { _fatalError = null; _appRootKey = GlobalKey(); _launchApp(); } /// 运维管理台根组件。 class OpsConsoleApp extends ConsumerWidget { const OpsConsoleApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // 全局致命错误:直接渲染可读错误页,避免 release 模式静默灰屏。 if (_fatalError != null) { return MaterialApp( title: AppConstants.appName, debugShowCheckedModeBanner: false, theme: AppTheme.light(), darkTheme: AppTheme.dark(), themeMode: ThemeMode.system, home: _FatalErrorScreen(error: _fatalError!), ); } final router = ref.watch(appRouterProvider); final themeMode = ref.watch(themeModeProvider); final appLocale = ref.watch(localeProvider); return MaterialApp.router( title: AppConstants.appName, // 应用级名称,与 AndroidManifest/iOS Info.plist/鸿蒙配置保持一致 debugShowCheckedModeBanner: false, theme: AppTheme.light(), darkTheme: AppTheme.dark(), // 默认深色主题,可在“设置”页切换 themeMode: themeMode, locale: appLocale.locale, supportedLocales: const [ Locale('zh', 'CN'), Locale('en', 'US'), ], localizationsDelegates: const [ // 必须包含 Flutter 默认的本地化委托,否则 MaterialLocalizations // 不会被注册,所有 TextField/InputDecoration 会因取 null 而崩溃。 GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, _AppLocaleDelegate(), ], routerConfig: router, ); } } /// [AppLocale] 的轻量 [LocalizationsDelegate], /// 使 [AppLocalizations.of] 能按当前 locale 取到对应文案。 class _AppLocaleDelegate extends LocalizationsDelegate { const _AppLocaleDelegate(); @override bool isSupported(Locale locale) => locale.languageCode == 'zh' || locale.languageCode == 'en'; @override Future load(Locale locale) async { return locale.languageCode == 'en' ? AppLocale.en : AppLocale.zh; } @override bool shouldReload(covariant LocalizationsDelegate old) => false; } /// 全局致命错误展示页。 /// /// 当 [FlutterError.onError] / [PlatformDispatcher.onError] / Zone 异常捕获到 /// 未处理异常时,用可读的红色错误页替代默认的灰/白屏,便于真机直接看到根因。 class _FatalErrorScreen extends StatelessWidget { const _FatalErrorScreen({required this.error}); final String error; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.red.shade50, body: SafeArea( child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Icon(Icons.error_outline, size: 48, color: Colors.red), const SizedBox(height: 16), const Text( '应用启动时发生错误', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.red, ), ), const SizedBox(height: 8), const Text( '请将下方错误信息反馈给开发人员:', style: TextStyle(color: Colors.black87), ), const SizedBox(height: 12), Expanded( child: SingleChildScrollView( child: SelectableText( error, style: const TextStyle( fontFamily: 'monospace', fontSize: 12, color: Colors.black87, ), ), ), ), const SizedBox(height: 12), FilledButton( onPressed: _retryApp, child: const Text('重试'), ), ], ), ), ), ); } } /// 全局渲染异常兜底页([ErrorWidget.builder] 使用)。 /// /// 与 [_FatalErrorScreen] 不同,此组件不依赖 MaterialApp/Theme(此时 app 可能 /// 尚未建立),是一个完全自包含、硬编码红色背景的可读错误页,确保任何 widget /// 渲染异常都能暴露文字而非静默灰屏。 class _ErrorWidgetScreen extends StatelessWidget { const _ErrorWidgetScreen({required this.message, this.stack}); final String message; final StackTrace? stack; @override Widget build(BuildContext context) { return Material( color: Colors.red.shade50, child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.error_outline, size: 40, color: Colors.red), const SizedBox(height: 12), const Text( '渲染异常(Widget Error)', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.red, ), ), const SizedBox(height: 8), const Text( '请将下方文字反馈给开发人员:', style: TextStyle(color: Colors.black87), ), const SizedBox(height: 8), SelectableText( message, style: const TextStyle( fontFamily: 'monospace', fontSize: 12, color: Colors.black87, ), ), if (stack != null) ...[ const SizedBox(height: 8), SelectableText( stack.toString(), style: const TextStyle( fontFamily: 'monospace', fontSize: 11, color: Colors.black54, ), ), ], ], ), ), ), ); } }