fix: 修复 Flutter Web 白屏问题
- main.dart: 添加全局错误处理和 FlutterError.onError - index.html: 添加加载指示器,vconsole 仅在开发环境启用 - dio_client.dart: 添加详细错误日志,优化超时配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
|||||||
2b1d2ed877ca1d041aef5d6561fbfcf5
|
cd059bcd8df9e9b2b7bfff5ee9fb7ba7
|
||||||
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"e4b8dca3f1b4ede4c30371002441c88c12187e
|
|||||||
|
|
||||||
_flutter.loader.load({
|
_flutter.loader.load({
|
||||||
serviceWorkerSettings: {
|
serviceWorkerSettings: {
|
||||||
serviceWorkerVersion: "2873482940" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
serviceWorkerVersion: "1409326861" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,21 +18,79 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="Monisuo - Crypto Trading App">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="flutter_monisuo">
|
<meta name="apple-mobile-web-app-title" content="Monisuo">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>flutter_monisuo</title>
|
<title>Monisuo</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 加载指示器样式 */
|
||||||
|
.loading-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-top: 3px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏加载指示器 */
|
||||||
|
.loading-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- 加载指示器 -->
|
||||||
|
<div id="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div style="text-align: center; padding: 50px; font-family: sans-serif;">
|
||||||
|
<h1>JavaScript Required</h1>
|
||||||
|
<p>Please enable JavaScript to run this application.</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
You can customize the "flutter_bootstrap.js" script.
|
You can customize the "flutter_bootstrap.js" script.
|
||||||
This is useful to provide a custom configuration to the Flutter loader
|
This is useful to provide a custom configuration to the Flutter loader
|
||||||
@@ -43,12 +101,34 @@
|
|||||||
-->
|
-->
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
|
||||||
<!-- vConsole 调试控制台 -->
|
<!-- 隐藏加载指示器 -->
|
||||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('flutter-first-frame', function() {
|
||||||
new VConsole();
|
var loading = document.getElementById('loading');
|
||||||
|
if (loading) {
|
||||||
|
loading.classList.add('loading-hidden');
|
||||||
|
setTimeout(function() {
|
||||||
|
loading.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- vConsole 调试控制台 - 仅在开发环境启用 -->
|
||||||
|
<script>
|
||||||
|
// 检测是否为开发环境(localhost 或特定端口)
|
||||||
|
var isDev = window.location.hostname === 'localhost' ||
|
||||||
|
window.location.hostname === '127.0.0.1' ||
|
||||||
|
window.location.port !== '' && window.location.port !== '80' && window.location.port !== '443';
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'https://unpkg.com/vconsole@latest/dist/vconsole.min.js';
|
||||||
|
script.onload = function() {
|
||||||
|
new VConsole();
|
||||||
|
};
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import '../constants/api_endpoints.dart';
|
import '../constants/api_endpoints.dart';
|
||||||
import '../storage/local_storage.dart';
|
import '../storage/local_storage.dart';
|
||||||
import 'api_exception.dart';
|
import 'api_exception.dart';
|
||||||
@@ -7,8 +8,9 @@ import 'api_response.dart';
|
|||||||
/// 网络配置常量
|
/// 网络配置常量
|
||||||
class NetworkConfig {
|
class NetworkConfig {
|
||||||
static const String baseUrl = ApiEndpoints.baseUrl;
|
static const String baseUrl = ApiEndpoints.baseUrl;
|
||||||
static const Duration connectTimeout = Duration(seconds: 30);
|
static const Duration connectTimeout = Duration(seconds: 15);
|
||||||
static const Duration receiveTimeout = Duration(seconds: 30);
|
static const Duration receiveTimeout = Duration(seconds: 15);
|
||||||
|
static const Duration sendTimeout = Duration(seconds: 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dio 网络客户端
|
/// Dio 网络客户端
|
||||||
@@ -18,6 +20,7 @@ class DioClient {
|
|||||||
DioClient() {
|
DioClient() {
|
||||||
_dio = _createDio();
|
_dio = _createDio();
|
||||||
_setupInterceptors();
|
_setupInterceptors();
|
||||||
|
debugPrint('DioClient initialized with baseUrl: ${NetworkConfig.baseUrl}');
|
||||||
}
|
}
|
||||||
|
|
||||||
Dio _createDio() {
|
Dio _createDio() {
|
||||||
@@ -25,6 +28,7 @@ class DioClient {
|
|||||||
baseUrl: NetworkConfig.baseUrl,
|
baseUrl: NetworkConfig.baseUrl,
|
||||||
connectTimeout: NetworkConfig.connectTimeout,
|
connectTimeout: NetworkConfig.connectTimeout,
|
||||||
receiveTimeout: NetworkConfig.receiveTimeout,
|
receiveTimeout: NetworkConfig.receiveTimeout,
|
||||||
|
sendTimeout: NetworkConfig.sendTimeout,
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -32,11 +36,7 @@ class DioClient {
|
|||||||
void _setupInterceptors() {
|
void _setupInterceptors() {
|
||||||
_dio.interceptors.addAll([
|
_dio.interceptors.addAll([
|
||||||
_AuthInterceptor(),
|
_AuthInterceptor(),
|
||||||
LogInterceptor(
|
_LoggingInterceptor(),
|
||||||
requestHeader: false,
|
|
||||||
responseHeader: false,
|
|
||||||
error: true,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +80,15 @@ class DioClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<T> _handleError<T>(DioException e) {
|
ApiResponse<T> _handleError<T>(DioException e) {
|
||||||
|
// 详细错误日志
|
||||||
|
debugPrint('=== Network Error ===');
|
||||||
|
debugPrint('Type: ${e.type}');
|
||||||
|
debugPrint('Message: ${e.message}');
|
||||||
|
debugPrint('URL: ${e.requestOptions.uri}');
|
||||||
|
debugPrint('StatusCode: ${e.response?.statusCode}');
|
||||||
|
debugPrint('ResponseData: ${e.response?.data}');
|
||||||
|
debugPrint('====================');
|
||||||
|
|
||||||
if (_isUnauthorized(e)) {
|
if (_isUnauthorized(e)) {
|
||||||
_clearUserData();
|
_clearUserData();
|
||||||
return ApiResponse.unauthorized('登录已过期,请重新登录');
|
return ApiResponse.unauthorized('登录已过期,请重新登录');
|
||||||
@@ -106,15 +115,55 @@ class DioClient {
|
|||||||
case DioExceptionType.receiveTimeout:
|
case DioExceptionType.receiveTimeout:
|
||||||
return '响应超时,请重试';
|
return '响应超时,请重试';
|
||||||
case DioExceptionType.connectionError:
|
case DioExceptionType.connectionError:
|
||||||
return '网络连接失败';
|
return '网络连接失败,请检查网络设置';
|
||||||
case DioExceptionType.badResponse:
|
case DioExceptionType.badResponse:
|
||||||
return '服务器错误 (${e.response?.statusCode})';
|
final statusCode = e.response?.statusCode;
|
||||||
|
if (statusCode == 500) {
|
||||||
|
return '服务器内部错误';
|
||||||
|
} else if (statusCode == 502 || statusCode == 503) {
|
||||||
|
return '服务暂时不可用';
|
||||||
|
}
|
||||||
|
return '服务器错误 ($statusCode)';
|
||||||
default:
|
default:
|
||||||
return e.message ?? '网络请求失败';
|
return e.message ?? '网络请求失败';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 日志拦截器
|
||||||
|
class _LoggingInterceptor extends Interceptor {
|
||||||
|
@override
|
||||||
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
|
debugPrint('┌──────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('│ REQUEST: ${options.method} ${options.uri}');
|
||||||
|
debugPrint('│ Headers: ${options.headers}');
|
||||||
|
if (options.data != null) {
|
||||||
|
debugPrint('│ Data: ${options.data}');
|
||||||
|
}
|
||||||
|
debugPrint('└──────────────────────────────────────────────────────────');
|
||||||
|
super.onRequest(options, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||||
|
debugPrint('┌──────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('│ RESPONSE: ${response.statusCode} ${response.requestOptions.uri}');
|
||||||
|
debugPrint('│ Data: ${response.data}');
|
||||||
|
debugPrint('└──────────────────────────────────────────────────────────');
|
||||||
|
super.onResponse(response, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
|
debugPrint('┌──────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('│ ERROR: ${err.type} ${err.requestOptions.uri}');
|
||||||
|
debugPrint('│ Message: ${err.message}');
|
||||||
|
debugPrint('│ StatusCode: ${err.response?.statusCode}');
|
||||||
|
debugPrint('└──────────────────────────────────────────────────────────');
|
||||||
|
super.onError(err, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 认证拦截器
|
/// 认证拦截器
|
||||||
class _AuthInterceptor extends Interceptor {
|
class _AuthInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
@@ -21,13 +22,37 @@ import 'ui/pages/auth/login_page.dart';
|
|||||||
import 'ui/pages/main/main_page.dart';
|
import 'ui/pages/main/main_page.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
// 确保 Flutter 绑定初始化
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
Provider.debugCheckInvalidValueType = null;
|
|
||||||
|
|
||||||
await SharedPreferences.getInstance();
|
// 全局错误处理
|
||||||
await LocalStorage.init();
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
|
FlutterError.presentError(details);
|
||||||
|
debugPrint('Flutter Error: ${details.exception}');
|
||||||
|
debugPrint('Stack trace: ${details.stack}');
|
||||||
|
};
|
||||||
|
|
||||||
runApp(const MyApp());
|
// 捕获异步错误
|
||||||
|
runZonedGuarded<Future<void>>(
|
||||||
|
() async {
|
||||||
|
Provider.debugCheckInvalidValueType = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SharedPreferences.getInstance();
|
||||||
|
await LocalStorage.init();
|
||||||
|
debugPrint('App initialized successfully');
|
||||||
|
} catch (e, stack) {
|
||||||
|
debugPrint('Initialization error: $e');
|
||||||
|
debugPrint('Stack: $stack');
|
||||||
|
}
|
||||||
|
|
||||||
|
runApp(const MyApp());
|
||||||
|
},
|
||||||
|
(error, stack) {
|
||||||
|
debugPrint('Uncaught error: $error');
|
||||||
|
debugPrint('Stack: $stack');
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
|
|||||||
@@ -18,21 +18,79 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="Monisuo - Crypto Trading App">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="flutter_monisuo">
|
<meta name="apple-mobile-web-app-title" content="Monisuo">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>flutter_monisuo</title>
|
<title>Monisuo</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 加载指示器样式 */
|
||||||
|
.loading-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-top: 3px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏加载指示器 */
|
||||||
|
.loading-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- 加载指示器 -->
|
||||||
|
<div id="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div style="text-align: center; padding: 50px; font-family: sans-serif;">
|
||||||
|
<h1>JavaScript Required</h1>
|
||||||
|
<p>Please enable JavaScript to run this application.</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
You can customize the "flutter_bootstrap.js" script.
|
You can customize the "flutter_bootstrap.js" script.
|
||||||
This is useful to provide a custom configuration to the Flutter loader
|
This is useful to provide a custom configuration to the Flutter loader
|
||||||
@@ -43,12 +101,34 @@
|
|||||||
-->
|
-->
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
|
||||||
<!-- vConsole 调试控制台 -->
|
<!-- 隐藏加载指示器 -->
|
||||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('flutter-first-frame', function() {
|
||||||
new VConsole();
|
var loading = document.getElementById('loading');
|
||||||
|
if (loading) {
|
||||||
|
loading.classList.add('loading-hidden');
|
||||||
|
setTimeout(function() {
|
||||||
|
loading.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- vConsole 调试控制台 - 仅在开发环境启用 -->
|
||||||
|
<script>
|
||||||
|
// 检测是否为开发环境(localhost 或特定端口)
|
||||||
|
var isDev = window.location.hostname === 'localhost' ||
|
||||||
|
window.location.hostname === '127.0.0.1' ||
|
||||||
|
window.location.port !== '' && window.location.port !== '80' && window.location.port !== '443';
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'https://unpkg.com/vconsole@latest/dist/vconsole.min.js';
|
||||||
|
script.onload = function() {
|
||||||
|
new VConsole();
|
||||||
|
};
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user