diff --git a/assets/file.json b/assets/file.json index 9b3beae..1a9180a 100644 --- a/assets/file.json +++ b/assets/file.json @@ -1,3 +1,4 @@ [ - "assets/miniapp/mhtControl_1.0.90.zip" + "assets/miniapp/mhtControl_1.0.90.zip", + "assets/xiaoe/xiaoe_1.0.0.zip" ] \ No newline at end of file diff --git a/assets/miniapp/mhtControl_1.0.90.zip b/assets/miniapp/mhtControl_1.0.90.zip index ce36f8e..13204f9 100644 Binary files a/assets/miniapp/mhtControl_1.0.90.zip and b/assets/miniapp/mhtControl_1.0.90.zip differ diff --git a/assets/miniapp/xiaoe_1.0.0.zip b/assets/miniapp/xiaoe_1.0.0.zip new file mode 100644 index 0000000..d21fc92 Binary files /dev/null and b/assets/miniapp/xiaoe_1.0.0.zip differ diff --git a/assets/xiaoe.json b/assets/xiaoe.json new file mode 100644 index 0000000..1183f82 --- /dev/null +++ b/assets/xiaoe.json @@ -0,0 +1,3 @@ +[ + "assets/xiaoe/xiaoe_1.0.0.zip" +] \ No newline at end of file diff --git a/assets/xiaoe/xiaoe_1.0.0.zip b/assets/xiaoe/xiaoe_1.0.0.zip new file mode 100644 index 0000000..d21fc92 Binary files /dev/null and b/assets/xiaoe/xiaoe_1.0.0.zip differ diff --git a/lib/main.dart b/lib/main.dart index 09dc1d8..23a03e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -128,6 +128,7 @@ Future main() async { // initXUpdate(); initwebService(); + initXiaoeService(); WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) @@ -196,6 +197,14 @@ void initwebService() { }); } +void initXiaoeService() async { + try { + ef.kvRoot.appmanger.register(["xiaoe"]); + } catch (e) { + ef.log("$e"); + } +} + initLanguageSetting() async { try { Get.put(MHLanguageController()); diff --git a/lib/pages/main_bottom/e_page copy 2.dart b/lib/pages/main_bottom/e_page copy 2.dart new file mode 100644 index 0000000..f5b85df --- /dev/null +++ b/lib/pages/main_bottom/e_page copy 2.dart @@ -0,0 +1,738 @@ +// import 'package:ef/ef.dart'; +// import 'package:flutter/material.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/LoginStatus.dart'; +// import 'package:vbvs_app/model/api_response.dart'; +// import 'package:vbvs_app/pages/main_bottom/xiaoe/XiaoeModel.dart'; + +//本地 wsl +// 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(); + +// RxList deviceList = [].obs; +// RxString finalUri = RxString(''); + +// @override +// bool get wantKeepAlive => true; + +// @override +// void initState() { +// super.initState(); +// getDeviceList(); +// } + +// @override +// void dispose() { +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// super.build(context); +// bool isLoggedIn = userInfoController.model.login == LoginStatus.LOGIN.code; + +// return LayoutBuilder( +// builder: (context, bodySize) => GestureDetector( +// child: Obx(() { +// int login = userInfoController.model.login ?? 0; +// print("login: $login"); +// final showNoLoginView = !isLoggedIn || deviceList.isEmpty; + +// final backgroundImage = showNoLoginView +// ? 'assets/img/xiaoe.png' // 无设备或无登录时的背景 +// : 'assets/img/bgNoImg.png'; + +// return Stack( +// children: [ +// // 背景图片 +// Container( +// decoration: BoxDecoration( +// image: DecorationImage( +// image: AssetImage(backgroundImage), +// fit: BoxFit.fill, +// ), +// ), +// ), + +// // 遮罩层:未登录或无设备时显示 +// if (showNoLoginView) +// Positioned.fill( +// child: Container( +// color: Colors.black.withOpacity(0.5), +// ), +// ), + +// // 主内容 +// Scaffold( +// backgroundColor: Colors.transparent, +// appBar: _buildAppBar(), +// body: SafeArea( +// top: true, +// child: _buildContent(isLoggedIn), +// ), +// ), +// ], +// ); +// }), +// ), +// ); +// } + +// PreferredSizeWidget _buildAppBar() { +// return 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, +// ), +// ), +// ], +// ), +// ), +// ); +// } + +// Widget _buildContent(bool isLoggedIn) { +// if (!isLoggedIn) { +// return _buildLoggedOutContent(); +// } + +// return Obx(() { +// // 如果设备列表为空 +// if (deviceList.isEmpty) { +// return _buildNoDeviceContent(); +// } + +// // 设备列表不为空,显示 XiaoeView +// return XiaoeView(); +// }); +// } + +// Widget _buildLoggedOutContent() { +// return GestureDetector( +// onTap: () { +// NewTopSlideNotification.show( +// text: "请先登录".tr, +// textColor: themeController.currentColor.sc9, +// ); +// Get.toNamed("/loginPage"); +// }, +// child: Center( +// child: Image.asset( +// "assets/img/xiaoe.png", +// fit: BoxFit.contain, +// ), +// ), +// ); +// } + +// Widget _buildNoDeviceContent() { +// return GestureDetector( +// onTap: () { +// NewTopSlideNotification.show( +// text: "请先绑定设备".tr, +// textColor: themeController.currentColor.sc9, +// ); +// // Get.toNamed("/mHTDeviceTypePage"); +// }, +// 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 == 1) { +// // 使用200代替HttpStatusCodes.ok +// List rawList = apiResponse.data; + +// // 提取 mac 和 person.name +// 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; +// ef.log("小e页面初始化完成,设备数量:${deviceList.length}"); +// } +// } catch (e) { +// ef.log("小e获取设备列表失败: $e"); +// } +// } +// } + +//本地加载 wyf +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: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/img/bgNoImg.png'), + 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}'; + } + } +} \ No newline at end of file diff --git a/lib/pages/main_bottom/e_page copy.dart b/lib/pages/main_bottom/e_page copy.dart new file mode 100644 index 0000000..3028456 --- /dev/null +++ b/lib/pages/main_bottom/e_page copy.dart @@ -0,0 +1,247 @@ +import 'dart:convert'; + +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.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/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); + RxList deviceList = [].obs; + RxString finalUri = RxString(''); + + @override + bool get wantKeepAlive => true; // 保持页面状态 + + @override + void initState() { + super.initState(); + getDeviceList(); + } + + @override + void dispose() { + isPageLoading.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); // ⚠️必须调用,保证 keepAlive 生效 + bool isLoggedIn = userInfoController.model.login == LoginStatus.LOGIN.code; + + return LayoutBuilder( + builder: (context, bodySize) => GestureDetector( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/img/bgNoImg.png'), + 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 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, + ), + ), + ); + } + + // 设备列表不为空,加载 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( + valueListenable: isPageLoading, + builder: (context, isLoading, child) { + return isLoading + ? Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + themeController.currentColor.sc1, + ), + ), + ) + : 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; + + // 提取 mac 和 person.name + 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; + + // 拼接参数 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; + } + } + + if (language != null && language.isNotEmpty) { + if (finalUri.value.contains("?")) { + finalUri.value += "&lang=$language"; + } else { + finalUri.value += "?lang=$language"; + } + } + ef.log("msg"); + } catch (e) { + ef.log(e.toString()); + } + } +} diff --git a/lib/pages/main_bottom/e_page.dart b/lib/pages/main_bottom/e_page.dart index 12b3733..9b9773d 100644 --- a/lib/pages/main_bottom/e_page.dart +++ b/lib/pages/main_bottom/e_page.dart @@ -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 with AutomaticKeepAliveClientMixin { 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; // 保持页面状态 + 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 _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); // ⚠️必须调用,保证 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 with AutomaticKeepAliveClientMixin { } Widget _buildLoggedInContent() { - return Obx(() { - if (finalUri.isEmpty) { - return Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - themeController.currentColor.sc1, + 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, + ), + ), + ], ), - ), - ); - } + ); + } - // 如果设备列表为空 - 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( - valueListenable: isPageLoading, - builder: (context, isLoading, child) { - return isLoading - ? Center( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - themeController.currentColor.sc1, + 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.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( + 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: () { - TopSlideNotification.show( - context, + NewTopSlideNotification.show( text: "必须登录提示".tr, textColor: themeController.currentColor.sc9, ); @@ -197,7 +469,6 @@ class _EPageState extends State with AutomaticKeepAliveClientMixin { if (apiResponse.code == HttpStatusCodes.ok) { List rawList = apiResponse.data; - // 提取 mac 和 person.name List> newList = rawList.map((item) { String mac = item['mac'] ?? ''; String name = (item['person'] != null && @@ -213,36 +484,46 @@ class _EPageState extends State 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}'; } } } diff --git a/lib/pages/main_bottom/xiaoe/XiaoeModel.dart b/lib/pages/main_bottom/xiaoe/XiaoeModel.dart new file mode 100644 index 0000000..406434b --- /dev/null +++ b/lib/pages/main_bottom/xiaoe/XiaoeModel.dart @@ -0,0 +1,771 @@ +// // lib/controller/xiaoe/xiaoe_controller.dart +// import 'dart:async'; + +// import 'package:EasyDartModule/EasyDartModule.dart' as edm; +// import 'package:easyweb/base/minisdk.dart'; +// import 'package:easyweb/easyweb.dart'; +// import 'package:ef/ef.dart'; +// import 'package:flutter/material.dart'; +// import 'package:vbvs_app/common/util/FitTool.dart'; +// import 'package:vbvs_app/controller/device/body_device_controller.dart'; +// import 'package:vbvs_app/controller/user_info_controller.dart'; +// import 'package:vbvs_app/enum/LoginStatus.dart'; +// import 'package:vbvs_app/model/api_response.dart'; +// import 'package:vbvs_app/pages/main_bottom/component/main_page_b_bottom_change.dart'; + +// class XiaoeModel { +// XiaoeModel(); +// } + +// class XiaoeController extends GetControllerEx { +// XiaoeController() : super(XiaoeModel()) { +// web = WebviewHelper( +// isheadless: false, +// jsbridge: buildsdk( +// father: this, +// ), +// settings: buildsettings(), +// params: PlatformHeadlessInAppWebViewCreationParams( +// onLoadStop: (controller, url) { +// setState(() { +// ready.value = true; +// }); +// }, +// onLoadResource: (controller, resource) { +// // 资源加载回调 +// ef.log("msg"); +// loadRecource.value = true; +// updateAll(); +// }, +// initialUrlRequest: +// URLRequest(url: WebUri("http://127.0.0.1/index.html?"))), +// ); +// } + +// // 变量与WebviewTestController保持一致 +// var selectDevice = {}; +// var lastSelectDevice = {}; +// var bluetooth = 0; +// List personList = []; +// List instantData = []; +// List deviceList = []; // 小e需要的设备列表 +// RxBool initFlag = false.obs; +// Timer? _resourceLoadTimer; +// Widget webviewWidget = Container(); +// var wifiResponseData; + +// late WebviewHelper web; +// var ready = false.obs; +// var cnt = 0.obs; +// var loadRecource = false.obs; +// int sourceTime = 0; + +// // 获取设备列表 +// Future getDeviceList() async { +// try { +// BodyDeviceController bodyDeviceController = Get.find(); +// ApiResponse apiResponse = +// await bodyDeviceController.getDeviceList(isAllDevice: true); + +// if (apiResponse.code == 200) { +// List rawList = apiResponse.data; + +// // 处理设备列表数据 +// List> newList = rawList.map((item) { +// String mac = item['mac']?.toString() ?? ''; +// String name = ''; + +// if (item['person'] != null && +// item['person']['name'] != null && +// item['person']['name'].toString().trim().isNotEmpty) { +// name = '${item['person']['name']}_$mac'; +// } else { +// name = '${'体征检测设备'.tr}_$mac'; +// } + +// return { +// 'mac': mac, +// 'name': name, +// 'deviceType': item['device_type'] ?? '', +// 'person': item['person'] ?? {}, +// 'rawData': item, +// }; +// }).toList(); + +// deviceList = newList; +// ef.log("小e获取到${deviceList.length}个设备"); +// } +// } catch (e) { +// ef.log("小e获取设备列表错误: $e"); +// } +// } + +// Future startXiaoe() async { +// try { +// ef.log('开始启动小e应用...'); + +// // 先获取设备列表 +// await getDeviceList(); + +// ef.log('开始运行小e Web应用...'); +// // 运行小e应用 +// var x1 = await ef.kvRoot.appmanger.find("xiaoe"); +// var x2 = await ef.kvRoot.appmanger.find("mhtControl"); +// print("$x1"); +// print("$x2"); +// await web.runApp('xiaoe').then((x) { +// ef.log('web.runApp 执行完成,结果: $x'); +// ready.value = true; +// ef.log('ready.value 设置为 true'); + +// // 设置资源加载超时定时器 +// _resourceLoadTimer = Timer(Duration(seconds: 15), () { +// ef.log('资源加载超时定时器触发,当前 loadRecource.value = ${loadRecource.value}'); +// if (!loadRecource.value) { +// loadRecource.value = true; +// updateAll(); +// ef.log('小e资源加载超时,强制显示页面'); +// } +// }); +// }); +// } catch (e, s) { +// ef.log('启动小e错误: $e, $s'); +// ef.log('错误堆栈: $s'); +// } +// } + +// @override +// void onInit() { +// super.onInit(); +// ef.log("小e控制器初始化 =>${DateTime.now()}"); + +// // 延迟启动 +// Future.delayed(Duration(milliseconds: 300), () { +// startXiaoe(); +// }); +// } + +// @override +// void onClose() { +// _resourceLoadTimer?.cancel(); +// super.onClose(); +// } +// } + +// class XiaoeView extends GetComponent { +// XiaoeView({super.key, super.oncreate}); + +// @override +// XiaoeController newinstance() { +// if (ef.kvRoot.XiaoeController == null) { +// ef.kvRoot.XiaoeController = XiaoeController(); +// if (Get.isRegistered() == false) { +// Get.put(ef.kvRoot.XiaoeController); +// XiaoeController xiaoeController = Get.find(); +// xiaoeController.global = true; +// return xiaoeController; +// } +// } +// return ef.kvRoot.XiaoeController; +// } + +// @override +// Widget build(BuildContext context) { +// return Obx(() { +// UserInfoController userInfoController = Get.find(); +// BodyDeviceController bodyDeviceController = Get.find(); +// final isLoggedIn = +// userInfoController.model.login == LoginStatus.LOGIN.code; + +// final hasDevice = bodyDeviceController.deviceList.isNotEmpty; + +// final showWeb = isLoggedIn && hasDevice; +// final mainAxisAlignment = +// showWeb ? MainAxisAlignment.start : MainAxisAlignment.center; + +// return Scaffold( +// backgroundColor: Colors.transparent, +// body: Column( +// crossAxisAlignment: CrossAxisAlignment.center, +// mainAxisSize: MainAxisSize.max, +// mainAxisAlignment: mainAxisAlignment, +// children: [ +// // 未登录提示 +// if (!isLoggedIn) +// Center( +// child: InkWell( +// onTap: () => Get.toNamed("/loginPage"), +// child: Container( +// padding: EdgeInsets.symmetric( +// vertical: 20.rpx, horizontal: 40.rpx), +// child: RichText( +// text: TextSpan( +// children: [ +// TextSpan( +// text: "请先".tr, +// style: TextStyle( +// color: Colors.white, +// fontSize: 30.rpx, +// ), +// ), +// WidgetSpan( +// child: Stack( +// children: [ +// Text( +// "登录".tr, +// style: TextStyle( +// color: Colors.blue, +// fontSize: 30.rpx, +// ), +// ), +// Positioned( +// bottom: 0, +// left: 0, +// right: 0, +// child: Container( +// height: 1, +// color: Colors.blue, +// ), +// ), +// ], +// ), +// ), +// TextSpan( +// text: "后,再使用小e".tr, +// style: TextStyle( +// color: Colors.white, +// fontSize: 30.rpx, +// ), +// ), +// ], +// ), +// ), +// ), +// ), +// ), + +// // 已登录但无设备提示 +// if (isLoggedIn && !hasDevice) +// Center( +// child: InkWell( +// onTap: () {}, +// child: Container( +// padding: EdgeInsets.symmetric( +// vertical: 20.rpx, horizontal: 40.rpx), +// child: RichText( +// text: TextSpan( +// children: [ +// TextSpan( +// text: "请先".tr, +// style: TextStyle( +// color: Colors.white, +// fontSize: 30.rpx, +// ), +// ), +// WidgetSpan( +// child: Stack( +// children: [ +// Text( +// "绑定设备".tr, +// style: TextStyle( +// color: Colors.blue, +// fontSize: 30.rpx, +// ), +// ), +// Positioned( +// bottom: 0, +// left: 0, +// right: 0, +// child: Container( +// height: 1, +// color: Colors.blue, +// ), +// ), +// ], +// ), +// ), +// TextSpan( +// text: "后,再使用小e".tr, +// style: TextStyle( +// color: Colors.white, +// fontSize: 30.rpx, +// ), +// ), +// ], +// ), +// ), +// ), +// ), +// ), + +// // 显示 WebView +// if (showWeb) +// Expanded( +// child: Align( +// alignment: Alignment.topLeft, +// child: Obx(() { +// return Stack( +// children: [ +// controller.ready.value +// ? controller.web.build() +// : Container(), +// if (!controller.loadRecource.value) +// Positioned.fill( +// child: Container( +// decoration: BoxDecoration( +// image: DecorationImage( +// image: AssetImage( +// "assets/img/xiaoe.png", +// ), +// fit: BoxFit.fill, +// ), +// ), +// alignment: Alignment.center, +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// CircularProgressIndicator( +// valueColor: AlwaysStoppedAnimation( +// Colors.white), +// ), +// ], +// ), +// ), +// ), +// ], +// ); +// }), +// ), +// ), +// ], +// ), +// ); +// }); +// } +// } + +// lib/controller/xiaoe/xiaoe_controller.dart +import 'dart:async'; +import 'dart:convert'; + +import 'package:easyweb/base/minisdk.dart'; +import 'package:easyweb/easyweb.dart'; +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/controller/device/body_device_controller.dart'; +import 'package:vbvs_app/controller/user_info_controller.dart'; +import 'package:vbvs_app/enum/LoginStatus.dart'; +import 'package:vbvs_app/model/api_response.dart'; + +class XiaoeModel { + XiaoeModel(); +} + +class XiaoeController extends GetControllerEx { + XiaoeController() : super(XiaoeModel()) { + // 初始URL为空,在获取设备列表后设置 + initialUrl = ""; + // web = WebviewHelper( + // isheadless: false, + // jsbridge: buildsdk( + // father: this, + // ), + // settings: buildsettings(), + // params: PlatformHeadlessInAppWebViewCreationParams( + // onLoadStop: (controller, url) { + // setState(() { + // ready.value = true; + // }); + // }, + // onLoadResource: (controller, resource) { + // // 资源加载回调 + // ef.log("msg"); + // loadRecource.value = true; + // updateAll(); + // }, + // initialUrlRequest: URLRequest(url: WebUri(initialUrl)), + // ), + // ); + } + + String initialUrl = ""; // 存储初始URL + + // 变量与WebviewTestController保持一致 + var selectDevice = {}; + var lastSelectDevice = {}; + var bluetooth = 0; + List personList = []; + List instantData = []; + List deviceList = []; // 小e需要的设备列表 + RxBool initFlag = false.obs; + Timer? _resourceLoadTimer; + Widget webviewWidget = Container(); + var wifiResponseData; + + late WebviewHelper web; + var ready = false.obs; + var cnt = 0.obs; + var loadRecource = false.obs; + int sourceTime = 0; + + bool _isStarting = false; // 添加启动标志 + + // 构建最终的URL + String _buildFinalUrl(String baseUrl) { + String queryParams = '?t=${DateTime.now().millisecondsSinceEpoch}'; + + if (deviceList.isNotEmpty) { + // 提取小e需要的格式 + List> xiaoeDeviceList = deviceList.map((device) { + return { + 'mac': device['mac'] ?? '', + 'name': device['name'] ?? '', + }; + }).toList(); + + String personParam = Uri.encodeComponent(jsonEncode(xiaoeDeviceList)); + 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'; + } + + return baseUrl + queryParams; + } + + // 获取设备列表并构建URL + Future getDeviceListAndBuildUrl() async { + try { + BodyDeviceController bodyDeviceController = Get.find(); + ApiResponse apiResponse = + await bodyDeviceController.getDeviceList(isAllDevice: true); + + if (apiResponse.code == 1) { + List rawList = apiResponse.data; + + // 处理设备列表数据 + List> newList = rawList.map((item) { + String mac = item['mac']?.toString() ?? ''; + String name = ''; + + if (item['person'] != null && + item['person']['name'] != null && + item['person']['name'].toString().trim().isNotEmpty) { + name = '${item['person']['name']}_$mac'; + } else { + name = '${'体征检测设备'.tr}_$mac'; + } + + return { + 'mac': mac, + 'name': name, + 'deviceType': item['device_type'] ?? '', + 'person': item['person'] ?? {}, + 'rawData': item, + }; + }).toList(); + + deviceList = newList; + ef.log("小e获取到${deviceList.length}个设备"); + + // 构建最终的URL + String baseUrl = "http://127.0.0.1/index.html"; + initialUrl = _buildFinalUrl(baseUrl); + ef.log("小e初始URL: $initialUrl"); + } else { + // API调用失败时使用基础URL + String baseUrl = "http://127.0.0.1/index.html"; + initialUrl = baseUrl + '?t=${DateTime.now().millisecondsSinceEpoch}'; + ef.log("获取设备列表失败,使用基础URL: $initialUrl"); + } + } catch (e) { + ef.log("小e获取设备列表错误: $e"); + // 出错时使用基础URL + String baseUrl = "http://127.0.0.1/index.html"; + initialUrl = baseUrl + '?t=${DateTime.now().millisecondsSinceEpoch}'; + } + } + + Future startXiaoe() async { + try { + if (_isStarting) { + ef.log('小e已经在启动中,跳过'); + return; + } + _isStarting = true; // 标记为启动中 + ef.log('开始启动小e应用...'); + + // 先获取设备列表并构建URL + await getDeviceListAndBuildUrl(); + + // 重新创建WebviewHelper,传入新的URL + web = WebviewHelper( + isheadless: false, + jsbridge: buildsdk(father: this), + settings: buildsettings(), + params: PlatformHeadlessInAppWebViewCreationParams( + onLoadStop: (controller, url) { + setState(() { + ready.value = true; + }); + }, + onLoadResource: (controller, resource) { + ef.log("msg"); + loadRecource.value = true; + updateAll(); + }, + initialUrlRequest: URLRequest(url: WebUri(initialUrl)), + ), + ); + + ef.log('开始运行小e Web应用...'); + + + + await web.runApp('xiaoe').then((x) { + ef.log('web.runApp 执行完成,结果: $x'); + ready.value = true; + ef.log('ready.value 设置为 true'); + + // 设置资源加载超时定时器 + // _resourceLoadTimer = Timer(Duration(seconds: 15), () { + // ef.log('资源加载超时定时器触发,当前 loadRecource.value = ${loadRecource.value}'); + // if (!loadRecource.value) { + // loadRecource.value = true; + // updateAll(); + // ef.log('小e资源加载超时,强制显示页面'); + // } + // }); + }); + } catch (e, s) { + ef.log('启动小e错误: $e, $s'); + ef.log('错误堆栈: $s'); + } + } + + @override + void onInit() { + super.onInit(); + ef.log("小e控制器初始化 =>${DateTime.now()}"); + + // 延迟启动 + Future.delayed(Duration(milliseconds: 300), () { + startXiaoe(); + }); + } + + @override + void onClose() { + _resourceLoadTimer?.cancel(); + super.onClose(); + } +} + +class XiaoeView extends GetComponent { + XiaoeView({super.key, super.oncreate}); + + @override + XiaoeController newinstance() { + if (ef.kvRoot.XiaoeController == null) { + ef.kvRoot.XiaoeController = XiaoeController(); + if (Get.isRegistered() == false) { + Get.put(ef.kvRoot.XiaoeController); + XiaoeController xiaoeController = Get.find(); + xiaoeController.global = true; + return xiaoeController; + } + } + return ef.kvRoot.XiaoeController; + } + + @override + Widget build(BuildContext context) { + return Obx(() { + UserInfoController userInfoController = Get.find(); + BodyDeviceController bodyDeviceController = Get.find(); + final isLoggedIn = + userInfoController.model.login == LoginStatus.LOGIN.code; + + final hasDevice = bodyDeviceController.deviceList.isNotEmpty; + + final showWeb = isLoggedIn && hasDevice; + final mainAxisAlignment = + showWeb ? MainAxisAlignment.start : MainAxisAlignment.center; + + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: mainAxisAlignment, + children: [ + // 未登录提示 + if (!isLoggedIn) + Center( + child: InkWell( + onTap: () => Get.toNamed("/loginPage"), + child: Container( + padding: EdgeInsets.symmetric( + vertical: 20.rpx, horizontal: 40.rpx), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "请先".tr, + style: TextStyle( + color: Colors.white, + fontSize: 30.rpx, + ), + ), + WidgetSpan( + child: Stack( + children: [ + Text( + "登录".tr, + style: TextStyle( + color: Colors.blue, + fontSize: 30.rpx, + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 1, + color: Colors.blue, + ), + ), + ], + ), + ), + TextSpan( + text: "后,再使用小e".tr, + style: TextStyle( + color: Colors.white, + fontSize: 30.rpx, + ), + ), + ], + ), + ), + ), + ), + ), + + // 已登录但无设备提示 + if (isLoggedIn && !hasDevice) + Center( + child: InkWell( + onTap: () {}, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 20.rpx, horizontal: 40.rpx), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "请先".tr, + style: TextStyle( + color: Colors.white, + fontSize: 30.rpx, + ), + ), + WidgetSpan( + child: Stack( + children: [ + Text( + "绑定设备".tr, + style: TextStyle( + color: Colors.blue, + fontSize: 30.rpx, + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 1, + color: Colors.blue, + ), + ), + ], + ), + ), + TextSpan( + text: "后,再使用小e".tr, + style: TextStyle( + color: Colors.white, + fontSize: 30.rpx, + ), + ), + ], + ), + ), + ), + ), + ), + + // 显示 WebView + if (showWeb) + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: Obx(() { + return Stack( + children: [ + controller.ready.value + ? controller.web.build() + : Container(), + if (!controller.loadRecource.value) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "assets/img/xiaoe.png", + ), + fit: BoxFit.fill, + ), + ), + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ], + ), + ), + ), + ], + ); + }), + ), + ), + ], + ), + ); + }); + } +} diff --git a/lib/pages/mh_page/MattressControl.dart b/lib/pages/mh_page/MattressControl.dart index ec4d89c..bdc13df 100644 --- a/lib/pages/mh_page/MattressControl.dart +++ b/lib/pages/mh_page/MattressControl.dart @@ -2,9 +2,6 @@ import 'dart:ui'; import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:flutter_switch/flutter_switch.dart'; -import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/controller/user_info_controller.dart'; import 'package:vbvs_app/pages/mh_page/homepage/controller/mht_home_controller.dart'; import 'package:vbvs_app/pages/mh_page/test/WebviewTestModel.dart'; @@ -72,227 +69,6 @@ class _MattressControlPageState extends State { ), ); } - - // 床体图示 - Widget _buildBedImageSection(BuildContext context) { - return Padding( - padding: EdgeInsets.fromLTRB(0, 38.rpx, 0, 63.rpx), - child: Image.asset( - 'assets/images/bed_control.png', - width: MediaQuery.of(context).size.width * 0.7, - height: 193.rpx, - ), - ); - } - - Widget _buildModeSelector(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width - 60.rpx; - final spacing = 20.0.rpx; - final thirdWidth = 215.rpx; - final tabCount = 3; - final tabWidth = screenWidth / tabCount; - final sideMargin = (tabWidth - thirdWidth) / 2; - - final labels = ['左'.tr, '全局'.tr, '右'.tr]; - - return Padding( - padding: EdgeInsets.symmetric(horizontal: 30.rpx), - child: Stack( - children: [ - Row( - children: List.generate(tabCount, (index) { - return Expanded( - child: GestureDetector( - onTap: () { - setState(() { - selectedIndex = index; - }); - }, - child: _selectorTab( - labels[index], - isSelected: selectedIndex == index, - sideMargin: sideMargin, - ), - ), - ); - }), - ), - Positioned( - bottom: 0, - left: selectedIndex * tabWidth + sideMargin, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - width: thirdWidth, - height: 3.rpx, - color: Colors.white, - ), - ), - ], - ), - ); - } - - Widget _selectorTab(String label, - {bool isSelected = false, double sideMargin = 0}) { - return Container( - margin: EdgeInsets.symmetric(horizontal: sideMargin), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - label, - style: TextStyle( - color: isSelected ? Colors.white : Colors.grey, - fontSize: 30.rpx, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - if (isSelected) SizedBox(height: 15.rpx), - ], - ), - ); - } - - Widget _buildControlCards(BuildContext context) { - final spacing = 20.0.rpx; - - final List> allCards = [ - {'title': '一键助眠'.tr, 'time': '30:00'}, - {'title': '疲劳缓解'.tr, 'time': '20:00'}, - {'title': '全身放松'.tr, 'time': '20:00'}, - {'title': '背部律动'.tr, 'time': '10:00'}, - {'title': '腿部律动'.tr, 'time': '30:00'}, - {'title': '垂直律动'.tr, 'time': ''}, - {'title': '加热'.tr, 'time': '30:00'}, - {'title': '柔性唤醒'.tr, 'time': 'PM 08:00'}, - {'title': '记忆'.tr, 'time': ''}, - ]; - - final firstRow = allCards.sublist(0, 3); - final restCards = allCards.sublist(3); - - List>> chunkedRows = []; - for (int i = 0; i < restCards.length; i += 2) { - int end = (i + 2 < restCards.length) ? i + 2 : restCards.length; - chunkedRows.add(restCards.sublist(i, end)); - } - - return SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 30.rpx, vertical: 30.rpx), - child: Column( - children: [ - Row( - children: List.generate(firstRow.length * 2 - 1, (index) { - if (index.isOdd) { - return SizedBox(width: spacing); - } else { - final item = firstRow[index ~/ 2]; - return Expanded( - flex: 1, - child: _buildControlCard( - index ~/ 2, - item['title'], - item['time'], - ), - ); - } - }), - ), - SizedBox(height: spacing), - ...chunkedRows.map((row) { - return Padding( - padding: EdgeInsets.only(bottom: spacing), - child: Row( - children: List.generate(row.length * 2 - 1, (index) { - if (index.isOdd) { - return SizedBox(width: spacing); - } else { - final item = row[index ~/ 2]; - return Expanded( - flex: 1, - child: _buildControlCard( - index ~/ 2, - item['title'], - item['time'], - ), - ); - } - }), - ), - ); - }).toList(), - ], - ), - ), - ); - } - - Widget _buildControlCard( - int index, - String title, - String time, - ) { - final controller = Get.find(); - - return Container( - height: 241.rpx, - decoration: BoxDecoration( - color: const Color(0xFF003058), - borderRadius: BorderRadius.circular(8), - ), - padding: EdgeInsets.all(15.rpx), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - children: [ - SvgPicture.asset( - 'assets/img/icon/group.svg', - width: 60.rpx, - height: 60.rpx, - color: Colors.white, - ), - SizedBox(height: 22.rpx), - Text( - title, - style: TextStyle(color: Colors.white, fontSize: 30.rpx), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - time, - style: TextStyle( - color: const Color(0XFF929699), - fontSize: 26.rpx, - fontFamily: 'PingFang SC', - ), - ), - Obx(() => FlutterSwitch( - width: 71.rpx, - height: 36.rpx, - toggleSize: 30.rpx, - activeColor: const Color(0XFF6BFDAC), - inactiveColor: const Color(0XFF011D33), - value: controller.switchStates[index].value, - onToggle: (val) { - controller.switchStates[index].value = val; - }, - )), - ], - ), - ], - ), - ); - } } class ControlCardController extends GetxController { diff --git a/lib/pages/mh_page/test/WebviewTestModel.dart b/lib/pages/mh_page/test/WebviewTestModel.dart index 6ec82ec..e953777 100644 --- a/lib/pages/mh_page/test/WebviewTestModel.dart +++ b/lib/pages/mh_page/test/WebviewTestModel.dart @@ -491,7 +491,6 @@ class WebviewTestController extends GetControllerEx { print("未知数据格式".tr); return; } - if (tmp['data'] != null && tmp['data'] is Map) { var newData = tmp['data']; var mac = newData['mac']; @@ -499,23 +498,6 @@ class WebviewTestController extends GetControllerEx { final webviewTestController = Get.find(); webviewTestController.web.jsbridge?.dart ?.updateDeviceStatusByWifi(order); - // 检查是否是当前设备的MAC - // if (mac != null && mac == selectDevice['mac']) { - // // 更新或添加设备数据 - // int index = wifiResponseData - // .indexWhere((element) => element['mac'] == mac); - // if (index >= 0) { - // // 更新现有数据 - // wifiResponseData[index] = newData; - // } else { - // // 添加新数据 - // wifiResponseData.add(newData); - // } - - // // 可以在这里触发UI更新或其他处理 - // // 例如:Get.find().updateDeviceData(newData); - // print("✅ 收到设备数据: ${newData.toString()}"); - // } } } catch (e) { ef.log("ws error =>$e"); @@ -528,7 +510,9 @@ class WebviewTestController extends GetControllerEx { // 1. 先取消可能存在的旧设备监听 if (lastSelectDevice != null && lastSelectDevice.isNotEmpty) { String? oldMac = lastSelectDevice['mac']; - if (oldMac != null && oldMac.isNotEmpty) { + if (oldMac != null && + oldMac.isNotEmpty && + oldMac != selectDevice['mac']) { bool success = ws.send({ "type": 2, // 取消监听 "path": "/vsbs/web/rt/marttress", // 注意:可能需要不同的路径 diff --git a/pubspec.yaml b/pubspec.yaml index a4735be..be31b03 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: wifi_scan: ^0.4.1+2 network_info_plus: ^5.0.1 # wifi_info_flutter: ^2.0.2 + shelf_static: ^1.1.3 dev_dependencies: @@ -125,7 +126,9 @@ flutter: - assets/img/menu/ - assets/img/icon/ - assets/file.json + - assets/xiaoe.json - assets/miniapp/ + - assets/xiaoe/ - assets/city/ fonts: - family: calculatrix