Files
tuiche/lib/pages/main_bottom/e_page.dart
2026-04-07 14:49:31 +08:00

535 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: BoxDecoration(
image: DecorationImage(
image: AssetImage(getBackgroundImageNoImage()),
fit: BoxFit.fill,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent, // 状态栏背景色
statusBarIconBrightness: Brightness.light, // 图标颜色Android
statusBarBrightness: Brightness.light, // 图标颜色iOS
),
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}';
}
}
}