更新小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

@@ -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<EPage> createState() => _EPageState();
// }
// class _EPageState extends State<EPage> 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<void> getDeviceList() async {
// try {
// BodyDeviceController bodyDeviceController = Get.find();
// ApiResponse apiResponse =
// await bodyDeviceController.getDeviceList(isAllDevice: true);
// if (apiResponse.code == 1) {
// // 使用200代替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 &&
// 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<EPage> createState() => _EPageState();
}
class _EPageState extends State<EPage> with AutomaticKeepAliveClientMixin {
GlobalController globalController = Get.find();
UserInfoController userInfoController = Get.find();
BlueteethBindController blueteethBindController = Get.find();
ThemeController themeController = Get.find();
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;
@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<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);
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<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,
),
),
],
),
);
}
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(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: () {
NewTopSlideNotification.show(
text: "必须登录提示".tr,
textColor: themeController.currentColor.sc9,
);
Get.toNamed("/otherLoginPage");
},
child: Center(
child: Image.asset(
"assets/img/xiaoe.png",
fit: BoxFit.contain,
),
),
);
}
Future<void> getDeviceList() async {
try {
BodyDeviceController bodyDeviceController = Get.find();
ApiResponse apiResponse =
await bodyDeviceController.getDeviceList(isAllDevice: true);
if (apiResponse.code == HttpStatusCodes.ok) {
List<dynamic> rawList = apiResponse.data;
List<Map<String, dynamic>> 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}';
}
}
}

View File

@@ -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<EPage> createState() => _EPageState();
}
class _EPageState extends State<EPage> with AutomaticKeepAliveClientMixin {
GlobalController globalController = Get.find();
UserInfoController userInfoController = Get.find();
BlueteethBindController blueteethBindController = Get.find();
ThemeController themeController = Get.find();
DeviceTypeController deviceTypeController = Get.find();
ValueNotifier<bool> isPageLoading = ValueNotifier<bool>(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<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,
),
),
);
}
// 设备列表不为空,加载 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,
),
),
)
: 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<void> getDeviceList() async {
try {
BodyDeviceController bodyDeviceController = Get.find();
ApiResponse apiResponse =
await bodyDeviceController.getDeviceList(isAllDevice: true);
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 &&
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());
}
}
}

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}';
}
}
}

View File

@@ -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<XiaoeModel> {
// 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<void> getDeviceList() async {
// try {
// BodyDeviceController bodyDeviceController = Get.find();
// ApiResponse apiResponse =
// await bodyDeviceController.getDeviceList(isAllDevice: true);
// if (apiResponse.code == 200) {
// List<dynamic> rawList = apiResponse.data;
// // 处理设备列表数据
// List<Map<String, dynamic>> 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<void> 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<XiaoeController> {
// XiaoeView({super.key, super.oncreate});
// @override
// XiaoeController newinstance() {
// if (ef.kvRoot.XiaoeController == null) {
// ef.kvRoot.XiaoeController = XiaoeController();
// if (Get.isRegistered<XiaoeController>() == false) {
// Get.put<XiaoeController>(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<Color>(
// 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<XiaoeModel> {
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<Map<String, dynamic>> 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<void> getDeviceListAndBuildUrl() async {
try {
BodyDeviceController bodyDeviceController = Get.find();
ApiResponse apiResponse =
await bodyDeviceController.getDeviceList(isAllDevice: true);
if (apiResponse.code == 1) {
List<dynamic> rawList = apiResponse.data;
// 处理设备列表数据
List<Map<String, dynamic>> 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<void> 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<XiaoeController> {
XiaoeView({super.key, super.oncreate});
@override
XiaoeController newinstance() {
if (ef.kvRoot.XiaoeController == null) {
ef.kvRoot.XiaoeController = XiaoeController();
if (Get.isRegistered<XiaoeController>() == false) {
Get.put<XiaoeController>(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<Color>(
Colors.white),
),
],
),
),
),
],
);
}),
),
),
],
),
);
});
}
}

View File

@@ -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<MattressControlPage> {
),
);
}
// 床体图示
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<Map<String, dynamic>> 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<List<Map<String, dynamic>>> 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<ControlCardController>();
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 {

View File

@@ -491,7 +491,6 @@ class WebviewTestController extends GetControllerEx<WebviewTestModel> {
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<WebviewTestModel> {
final webviewTestController = Get.find<WebviewTestController>();
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<SomeController>().updateDeviceData(newData);
// print("✅ 收到设备数据: ${newData.toString()}");
// }
}
} catch (e) {
ef.log("ws error =>$e");
@@ -528,7 +510,9 @@ class WebviewTestController extends GetControllerEx<WebviewTestModel> {
// 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", // 注意:可能需要不同的路径