更新小e界面白屏问题

This commit is contained in:
wyf
2026-02-05 15:25:20 +08:00
parent 3ef22a36c0
commit 6709bcb446
13 changed files with 2151 additions and 338 deletions

View File

@@ -1,13 +1,21 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_static/shelf_static.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/color/app_uri_status.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
import 'package:vbvs_app/component/tool/NewTopSlideNotification.dart';
import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart';
import 'package:vbvs_app/controller/device/body_device_controller.dart';
import 'package:vbvs_app/controller/device/device_type_controller.dart';
@@ -18,6 +26,7 @@ import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/enum/LoginStatus.dart';
import 'package:vbvs_app/model/api_response.dart';
//本地使用
class EPage extends StatefulWidget {
final String sleepUri;
const EPage({super.key, required this.sleepUri});
@@ -34,33 +43,197 @@ class _EPageState extends State<EPage> with AutomaticKeepAliveClientMixin {
DeviceTypeController deviceTypeController = Get.find();
ValueNotifier<bool> isPageLoading = ValueNotifier<bool>(true);
ValueNotifier<bool> isServerStarting = ValueNotifier<bool>(true);
ValueNotifier<String> serverError = ValueNotifier<String>('');
RxList deviceList = [].obs;
RxString finalUri = RxString('');
// 本地服务器相关
HttpServer? _localServer;
int _serverPort = 0;
String _localServerUrl = '';
Directory? _tempExtractDir;
@override
bool get wantKeepAlive => true; // 保持页面状态
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
getDeviceList();
// 启动本地服务器,成功后获取设备列表
_startLocalWebServer().then((success) {
if (success) {
getDeviceList();
} else {
isPageLoading.value = false;
}
});
}
@override
void dispose() {
// 清理资源
_cleanupResources();
isPageLoading.dispose();
isServerStarting.dispose();
serverError.dispose();
super.dispose();
}
Future<bool> _startLocalWebServer() async {
try {
isServerStarting.value = true;
serverError.value = '';
// 1. 从 assets 加载 ZIP 文件
final ByteData zipData;
try {
zipData = await rootBundle.load('assets/xiaoe/xiaoe_1.0.0.zip');
} catch (e) {
serverError.value = '找不到小e资源文件: $e';
return false;
}
final Uint8List zipBytes = zipData.buffer.asUint8List();
// 2. 创建临时目录并解压 ZIP
_tempExtractDir = await Directory.systemTemp.createTemp('xiaoe_web_');
final bool extractSuccess =
await _extractZipToDirectory(zipBytes, _tempExtractDir!);
if (!extractSuccess) {
serverError.value = '解压小e资源文件失败';
return false;
}
// 3. 查找入口文件 (index.html)
final File? entryFile = await _findEntryFile(_tempExtractDir!);
if (entryFile == null) {
serverError.value = '找不到入口文件 (index.html)';
return false;
}
// 4. 创建静态文件处理器
final staticHandler = createStaticHandler(
_tempExtractDir!.path,
defaultDocument: 'index.html',
serveFilesOutsidePath: false,
);
// 5. 添加日志中间件(可选)
final handler =
Pipeline().addMiddleware(logRequests()).addHandler(staticHandler);
// 6. 启动服务器(端口 0 表示自动分配)
_localServer = await io.serve(handler, InternetAddress.loopbackIPv4, 0);
_serverPort = _localServer!.port;
_localServerUrl = 'http://127.0.0.1:$_serverPort';
print('小e本地服务启动成功: $_localServerUrl');
print('资源目录: ${_tempExtractDir!.path}');
isServerStarting.value = false;
return true;
} catch (e, stackTrace) {
serverError.value = '启动本地服务器失败: $e';
print('服务器启动错误: $e\n$stackTrace');
isServerStarting.value = false;
return false;
}
}
Future<bool> _extractZipToDirectory(
Uint8List zipBytes, Directory targetDir) async {
try {
// 解码 ZIP 文件
final Archive archive = ZipDecoder().decodeBytes(zipBytes);
for (final ArchiveFile file in archive) {
final String filename = file.name;
// 跳过目录条目(它们会通过文件创建自动生成)
if (file.isFile) {
// 确保目录存在
final String filePath = p.join(targetDir.path, filename);
final File outputFile = File(filePath);
await outputFile.parent.create(recursive: true);
// 写入文件内容
final List<int> data = file.content as List<int>;
await outputFile.writeAsBytes(data, flush: true);
}
}
print('解压完成,文件数量: ${archive.files.length}');
return true;
} catch (e, stackTrace) {
print('解压失败: $e\n$stackTrace');
return false;
}
}
Future<File?> _findEntryFile(Directory dir) async {
// 优先查找 index.html
final List<String> possibleEntries = [
'index.html',
'Index.html',
'INDEX.HTML',
'main.html',
'default.html',
];
for (final entry in possibleEntries) {
final File file = File(p.join(dir.path, entry));
if (await file.exists()) {
return file;
}
}
// 如果没有找到标准入口,查找任何 .html 文件
try {
final List<FileSystemEntity> entities =
await dir.list(recursive: false).toList();
for (final entity in entities) {
if (entity is File && entity.path.toLowerCase().endsWith('.html')) {
return entity;
}
}
} catch (e) {
print('查找入口文件失败: $e');
}
return null;
}
void _cleanupResources() {
// 关闭服务器
if (_localServer != null) {
_localServer!.close();
_localServer = null;
print('本地服务器已关闭');
}
// 清理临时目录
if (_tempExtractDir != null && _tempExtractDir!.existsSync()) {
try {
_tempExtractDir!.deleteSync(recursive: true);
print('临时目录已清理: ${_tempExtractDir!.path}');
} catch (e) {
print('清理临时目录失败: $e');
}
_tempExtractDir = null;
}
}
@override
Widget build(BuildContext context) {
super.build(context); // ⚠️必须调用,保证 keepAlive 生效
super.build(context);
bool isLoggedIn = userInfoController.model.login == LoginStatus.LOGIN.code;
return LayoutBuilder(
builder: (context, bodySize) => GestureDetector(
child: Container(
decoration: BoxDecoration(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/img/bgNoImg.png'),
fit: BoxFit.fill,
@@ -104,76 +277,175 @@ class _EPageState extends State<EPage> with AutomaticKeepAliveClientMixin {
}
Widget _buildLoggedInContent() {
return Obx(() {
if (finalUri.isEmpty) {
return Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
themeController.currentColor.sc1,
return ValueListenableBuilder<bool>(
valueListenable: isServerStarting,
builder: (context, isStarting, child) {
if (isStarting) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
themeController.currentColor.sc1,
),
),
SizedBox(height: 16.rpx),
Text(
'正在启动小e服务...'.tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: 28.rpx,
),
),
],
),
),
);
}
);
}
// 如果设备列表为空
if (deviceList.isEmpty) {
return GestureDetector(
onTap: () {
TopSlideNotification.show(
context,
text: "请先绑定设备".tr,
textColor: themeController.currentColor.sc9,
);
},
child: Center(
child: Image.asset(
"assets/img/xiaoe.png", // 可以显示默认背景
fit: BoxFit.contain,
),
),
);
}
// 设备列表不为空,加载 WebView
return Stack(
children: [
InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(finalUri.value +
"?t=${DateTime.now().millisecondsSinceEpoch}")),
onLoadStart: (controller, url) {
isPageLoading.value = true;
},
onLoadStop: (controller, url) {
isPageLoading.value = false;
},
),
ValueListenableBuilder<bool>(
valueListenable: isPageLoading,
builder: (context, isLoading, child) {
return isLoading
? Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
themeController.currentColor.sc1,
return ValueListenableBuilder<String>(
valueListenable: serverError,
builder: (context, error, child) {
if (error.isNotEmpty) {
return Center(
child: Padding(
padding: EdgeInsets.all(40.rpx),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
color: Colors.red,
size: 60.rpx,
),
SizedBox(height: 20.rpx),
Text(
'服务启动失败'.tr,
style: TextStyle(
color: Colors.red,
fontSize: 32.rpx,
fontWeight: FontWeight.bold,
),
),
)
: SizedBox.shrink();
},
),
],
);
});
SizedBox(height: 12.rpx),
Text(
error,
textAlign: TextAlign.center,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: 26.rpx,
),
),
SizedBox(height: 24.rpx),
ElevatedButton(
onPressed: () {
serverError.value = '';
_startLocalWebServer().then((success) {
if (success) {
getDeviceList();
}
});
},
style: ElevatedButton.styleFrom(
backgroundColor: themeController.currentColor.sc1,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 32.rpx,
vertical: 16.rpx,
),
),
child: Text('重试'.tr),
),
],
),
),
);
}
return Obx(() {
if (finalUri.isEmpty) {
return Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
themeController.currentColor.sc1,
),
),
);
}
if (deviceList.isEmpty) {
return GestureDetector(
onTap: () {
NewTopSlideNotification.show(
text: "请先绑定设备".tr,
textColor: themeController.currentColor.sc9,
);
},
child: Center(
child: Image.asset(
"assets/img/xiaoe.png",
fit: BoxFit.contain,
),
),
);
}
return Stack(
children: [
InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(finalUri.value),
),
onLoadStart: (controller, url) {
isPageLoading.value = true;
print('WebView 开始加载: $url');
},
onLoadStop: (controller, url) {
isPageLoading.value = false;
print('WebView 加载完成: $url');
},
onLoadError: (controller, url, code, message) {
isPageLoading.value = false;
print('WebView 加载错误: $message (code: $code)');
serverError.value = '页面加载失败: $message';
},
onWebViewCreated: (controller) {
// 启用调试(仅在开发模式下)
// if (!kReleaseMode) {
// controller.setWebContentsDebuggingEnabled(true);
// }
},
),
ValueListenableBuilder<bool>(
valueListenable: isPageLoading,
builder: (context, isLoading, child) {
return isLoading
? Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
themeController.currentColor.sc1,
),
),
)
: const SizedBox.shrink();
},
),
],
);
});
},
);
},
);
}
Widget _buildLoggedOutContent() {
return GestureDetector(
onTap: () {
TopSlideNotification.show(
context,
NewTopSlideNotification.show(
text: "必须登录提示".tr,
textColor: themeController.currentColor.sc9,
);
@@ -197,7 +469,6 @@ class _EPageState extends State<EPage> with AutomaticKeepAliveClientMixin {
if (apiResponse.code == HttpStatusCodes.ok) {
List<dynamic> rawList = apiResponse.data;
// 提取 mac 和 person.name
List<Map<String, dynamic>> newList = rawList.map((item) {
String mac = item['mac'] ?? '';
String name = (item['person'] != null &&
@@ -213,36 +484,46 @@ class _EPageState extends State<EPage> with AutomaticKeepAliveClientMixin {
deviceList.value = newList;
// 拼接参数 person
if (deviceList.isNotEmpty) {
// JSON 编码整个 deviceList 对象数组
String personParam = Uri.encodeComponent(jsonEncode(deviceList));
finalUri.value = "${widget.sleepUri}?person=$personParam";
} else {
finalUri.value = widget.sleepUri;
}
}
String? language = "";
if (AppConstants().ent_type == APPPackageType.MHT.code) {
if (mhLanguageController.selectLanguage != null) {
language = mhLanguageController.selectLanguage.value!.language_code;
}
} else {
if (languageController.selectLanguage != null) {
language = languageController.selectLanguage.value!.language_code;
}
}
// 构建最终 URL
String baseUrl = _localServerUrl;
String queryParams = '?t=${DateTime.now().millisecondsSinceEpoch}';
if (language != null && language.isNotEmpty) {
if (finalUri.value.contains("?")) {
finalUri.value += "&lang=$language";
} else {
finalUri.value += "?lang=$language";
if (deviceList.isNotEmpty) {
String personParam = Uri.encodeComponent(jsonEncode(deviceList));
queryParams += '&person=$personParam';
}
// 添加语言参数
String? language = "";
// 这里保持你原来的语言逻辑
if (AppConstants().ent_type == APPPackageType.MHT.code) {
// 假设你有 mhLanguageController
// if (mhLanguageController.selectLanguage != null) {
// language = mhLanguageController.selectLanguage.value!.language_code;
// }
} else {
// 假设你有 languageController
// if (languageController.selectLanguage != null) {
// language = languageController.selectLanguage.value!.language_code;
// }
}
if (language != null && language.isNotEmpty) {
queryParams += '&lang=$language';
}
finalUri.value = baseUrl + queryParams;
print('最终加载 URL: ${finalUri.value}');
} else {
// API 调用失败,使用基础 URL
finalUri.value =
'$_localServerUrl?t=${DateTime.now().millisecondsSinceEpoch}';
}
ef.log("msg");
} catch (e) {
ef.log(e.toString());
print('获取设备列表失败: $e');
// 出错时仍然加载基础页面
finalUri.value =
'$_localServerUrl?t=${DateTime.now().millisecondsSinceEpoch}';
}
}
}