ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
315
lib/main.dart
Normal file
315
lib/main.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user