Files
CloudHome-Ops/lib/main.dart

316 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
),
),
],
],
),
),
),
);
}
}