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/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'; import 'package:vbvs_app/controller/main_bottom/global_controller.dart'; import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; import 'package:vbvs_app/controller/user_info_controller.dart'; 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}); @override State createState() => _EPageState(); } class _EPageState extends State with AutomaticKeepAliveClientMixin { GlobalController globalController = Get.find(); UserInfoController userInfoController = Get.find(); BlueteethBindController blueteethBindController = Get.find(); ThemeController themeController = Get.find(); DeviceTypeController deviceTypeController = Get.find(); ValueNotifier isPageLoading = ValueNotifier(true); ValueNotifier isServerStarting = ValueNotifier(true); ValueNotifier serverError = ValueNotifier(''); RxList deviceList = [].obs; RxString finalUri = RxString(''); // 本地服务器相关 HttpServer? _localServer; int _serverPort = 0; String _localServerUrl = ''; Directory? _tempExtractDir; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); // 启动本地服务器,成功后获取设备列表 _startLocalWebServer().then((success) { if (success) { getDeviceList(); } else { isPageLoading.value = false; } }); } @override void dispose() { // 清理资源 _cleanupResources(); isPageLoading.dispose(); isServerStarting.dispose(); serverError.dispose(); super.dispose(); } Future _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 _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 data = file.content as List; await outputFile.writeAsBytes(data, flush: true); } } print('解压完成,文件数量: ${archive.files.length}'); return true; } catch (e, stackTrace) { print('解压失败: $e\n$stackTrace'); return false; } } Future _findEntryFile(Directory dir) async { // 优先查找 index.html final List 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 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); bool isLoggedIn = userInfoController.model.login == LoginStatus.LOGIN.code; return LayoutBuilder( builder: (context, bodySize) => GestureDetector( child: Container( decoration: BoxDecoration( image: DecorationImage( image: AssetImage(getBackgroundImageNoImage()), fit: BoxFit.fill, ), ), child: Scaffold( backgroundColor: Colors.transparent, appBar: AppBar( backgroundColor: themeController.currentColor.sc17, automaticallyImplyLeading: false, iconTheme: IconThemeData(color: themeController.currentColor.sc3), titleSpacing: 0, title: Container( width: double.infinity, height: 180.rpx, child: Stack( alignment: Alignment.center, children: [ Text( '菜单.小e'.tr, style: TextStyle( fontFamily: 'Readex Pro', color: themeController.currentColor.sc3, fontSize: 30.rpx, ), ), ], ), ), ), body: SafeArea( top: true, child: isLoggedIn ? _buildLoggedInContent() : _buildLoggedOutContent(), ), ), ), ), ); } Widget _buildLoggedInContent() { return ValueListenableBuilder( valueListenable: isServerStarting, builder: (context, isStarting, child) { if (isStarting) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( themeController.currentColor.sc1, ), ), SizedBox(height: 16.rpx), Text( '正在启动小e服务...'.tr, style: TextStyle( color: themeController.currentColor.sc3, fontSize: 28.rpx, ), ), ], ), ); } return ValueListenableBuilder( 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(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( 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( valueListenable: isPageLoading, builder: (context, isLoading, child) { return isLoading ? Center( child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( themeController.currentColor.sc1, ), ), ) : const SizedBox.shrink(); }, ), ], ); }); }, ); }, ); } Widget _buildLoggedOutContent() { return GestureDetector( onTap: () { NewTopSlideNotification.show( text: "必须登录提示".tr, textColor: themeController.currentColor.sc9, ); Get.toNamed("/otherLoginPage"); }, child: Center( child: Image.asset( "assets/img/xiaoe.png", fit: BoxFit.contain, ), ), ); } Future getDeviceList() async { try { BodyDeviceController bodyDeviceController = Get.find(); ApiResponse apiResponse = await bodyDeviceController.getDeviceList(isAllDevice: true); if (apiResponse.code == HttpStatusCodes.ok) { List rawList = apiResponse.data; List> newList = rawList.map((item) { String mac = item['mac'] ?? ''; String name = (item['person'] != null && item['person']['name'] != null && item['person']['name'].toString().trim().isNotEmpty) ? item['person']['name'] + "_${mac}" : '体征检测设备'.tr + "_${mac}"; return { 'mac': mac, 'name': name, }; }).toList(); deviceList.value = newList; // 构建最终 URL String baseUrl = _localServerUrl; String queryParams = '?t=${DateTime.now().millisecondsSinceEpoch}'; 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}'; } } catch (e) { print('获取设备列表失败: $e'); // 出错时仍然加载基础页面 finalUri.value = '$_localServerUrl?t=${DateTime.now().millisecondsSinceEpoch}'; } } }