Files
CloudHome-Ops/lib/main.dart

316 lines
11 KiB
Dart
Raw Normal View History

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<void>(
() 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<AppLocale> {
const _AppLocaleDelegate();
@override
bool isSupported(Locale locale) =>
locale.languageCode == 'zh' || locale.languageCode == 'en';
@override
Future<AppLocale> load(Locale locale) async {
return locale.languageCode == 'en' ? AppLocale.en : AppLocale.zh;
}
@override
bool shouldReload(covariant LocalizationsDelegate<AppLocale> 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,
),
),
],
],
),
),
),
);
}
}