diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcherdh.png b/android/app/src/main/res/mipmap-hdpi/ic_launcherdh.png new file mode 100644 index 0000000..0c9a4c5 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcherdh.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcherwd.jpg b/android/app/src/main/res/mipmap-hdpi/ic_launcherwd.jpg new file mode 100644 index 0000000..c3b6ede Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcherwd.jpg differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcherdh.png b/android/app/src/main/res/mipmap-ldpi/ic_launcherdh.png new file mode 100644 index 0000000..0c9a4c5 Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcherdh.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcherwd.jpg b/android/app/src/main/res/mipmap-ldpi/ic_launcherwd.jpg new file mode 100644 index 0000000..c3b6ede Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcherwd.jpg differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcherdh.png b/android/app/src/main/res/mipmap-mdpi/ic_launcherdh.png new file mode 100644 index 0000000..0c9a4c5 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcherdh.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcherwd.jpg b/android/app/src/main/res/mipmap-mdpi/ic_launcherwd.jpg new file mode 100644 index 0000000..c3b6ede Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcherwd.jpg differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcherdh.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcherdh.png new file mode 100644 index 0000000..0c9a4c5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcherdh.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcherwd.jpg b/android/app/src/main/res/mipmap-xhdpi/ic_launcherwd.jpg new file mode 100644 index 0000000..c3b6ede Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcherwd.jpg differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcherdh.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcherdh.png new file mode 100644 index 0000000..0c9a4c5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcherdh.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcherwd.jpg b/android/app/src/main/res/mipmap-xxhdpi/ic_launcherwd.jpg new file mode 100644 index 0000000..c3b6ede Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcherwd.jpg differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcherdh.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcherdh.png new file mode 100644 index 0000000..0c9a4c5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcherdh.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcherwd.jpg b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcherwd.jpg new file mode 100644 index 0000000..c3b6ede Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcherwd.jpg differ diff --git a/assets/img/icon/location.svg b/assets/img/icon/location.svg new file mode 100644 index 0000000..7c0341c --- /dev/null +++ b/assets/img/icon/location.svg @@ -0,0 +1 @@ +资源 319 \ No newline at end of file diff --git a/assets/langs/en_US.json b/assets/langs/en_US.json index 66832cd..6b0eeda 100644 --- a/assets/langs/en_US.json +++ b/assets/langs/en_US.json @@ -638,5 +638,22 @@ "前往AppStore": "Go to AppStore", "下载中...": "Downloading...", "欢迎使用东华智能睡眠": "Welcome to use Donghua Smart Sleep", + "健康快检": "Health Quick Check", + "开始快检": "Start Quick Check", + "健康等级:": "Health Level:", + "快检得分:": "Quick Check Score: ", + "设备ID:": "Device ID:", + "快检时间:": "Quick Check Time:", + "终止": "Stop", + "性别": "Gender", + "快检历史": "Quick Check History", + "快检得分": "Quick Check Score", + "创建时间": "Create Time", + "疲劳指数": "Fatigue Index", + "快检中...": "Quick Checking...", + "体验异常结束": "Abnormal experience ended", + "是否确认结束?": "Are you sure to end?", + "使用方式:人员使用健康快检功能时,只需平躺或坐在正常运行中的体征传感器传感器上方,保持静止,然后点击“启动快检”,待进度条完成,即可得出快检报告。": "Usage Instructions:When using the Health Quick Check function, the user only needs to lie flat or sit above the vital signs sensor while it is operating normally, remain still, and then tap **“Start Quick Check.”", + "注:本页报告是基于心率、呼吸等体征波形数据,通过AI算法模型分析完成,其结果仅供参考;其中报告未见数据异常部分,并不代表没有潜在性的疾病风险,如有不适请及时就医。": "Note:This report is generated based on physiological waveform data such as heart rate and respiration, analyzed using an AI algorithm model. The results are for reference only. Sections of the report showing no abnormal data do not necessarily indicate the absence of potential health risks. If you feel unwell, please seek medical attention promptly.", "Copyright © 2020-2030 东华智能睡眠中心 版权所有": "Copyright © 2020-2030 Donghua Smart Sleep Center All Rights Reserved" } \ No newline at end of file diff --git a/assets/langs/zh_CN.json b/assets/langs/zh_CN.json index 0a40bda..cc39c5b 100644 --- a/assets/langs/zh_CN.json +++ b/assets/langs/zh_CN.json @@ -638,5 +638,22 @@ "前往AppStore": "前往AppStore", "下载中...": "下载中...", "欢迎使用东华智能睡眠": "欢迎使用东华智能睡眠", + "健康快检": "健康快检", + "开始快检": "开始快检", + "终止": "终止", + "性别": "性别", + "快检历史": "快检历史", + "快检得分": "快检得分", + "创建时间": "创建时间", + "疲劳指数": "疲劳指数", + "健康等级:": "健康等级:", + "快检得分:": "快检得分:", + "设备ID:": "设备ID:", + "快检时间:": "快检时间:", + "快检中...": "快检中...", + "体验异常结束": "体验异常结束", + "是否确认结束?": "是否确认结束?", + "使用方式:人员使用健康快检功能时,只需平躺或坐在正常运行中的体征传感器传感器上方,保持静止,然后点击“启动快检”,待进度条完成,即可得出快检报告。": "使用方式:人员使用健康快检功能时,只需平躺或坐在正常运行中的体征传感器传感器上方,保持静止,然后点击“启动快检”,待进度条完成,即可得出快检报告。", + "注:本页报告是基于心率、呼吸等体征波形数据,通过AI算法模型分析完成,其结果仅供参考;其中报告未见数据异常部分,并不代表没有潜在性的疾病风险,如有不适请及时就医。":"注:本页报告是基于心率、呼吸等体征波形数据,通过AI算法模型分析完成,其结果仅供参考;其中报告未见数据异常部分,并不代表没有潜在性的疾病风险,如有不适请及时就医。", "Copyright © 2020-2030 东华智能睡眠中心 版权所有": "Copyright © 2020-2030 东华智能睡眠中心 版权所有" } \ No newline at end of file diff --git a/assets/langs/zh_TW.json b/assets/langs/zh_TW.json index f384058..960cfe7 100644 --- a/assets/langs/zh_TW.json +++ b/assets/langs/zh_TW.json @@ -636,5 +636,22 @@ "前往AppStore": "前往AppStore", "下载中...": "下載中...", "欢迎使用东华智能睡眠": "歡迎使用東華智能睡眠", + "健康快检": "健康快检", + "开始快检": "開始快檢", + "终止": "終止", + "性别": "性别", + "快检历史": "快檢歷史", + "快检得分": "快檢得分", + "创建时间": "创建时间", + "疲劳指数": "疲力指數", + "健康等级:": "健康等級:", + "快检得分:": "快檢得分:", + "设备ID:": "设备ID:", + "快检时间:": "快檢時間:", + "快检中...": "快檢中...", + "体验异常结束": "體驗異常結束", + "是否确认结束?": "是否確認結束?", + "注:本页报告是基于心率、呼吸等体征波形数据,通过AI算法模型分析完成,其结果仅供参考;其中报告未见数据异常部分,并不代表没有潜在性的疾病风险,如有不适请及时就医。": "注意:本頁報告基於心率、呼吸等體徵波形數據,通過AI算法模型分析完成,其結果僅供參考;其中報告未見數據異常部分,並不表示沒有潛在的疾病風險,如有不適請即時就医。", + "使用方式:人员使用健康快检功能时,只需平躺或坐在正常运行中的体征传感器传感器上方,保持静止,然后点击“启动快检”,待进度条完成,即可得出快检报告。": "使用方式:人员使用健康快检功能時,只需平躺或坐在正常運行中的體徵传感器传感器上方,保持靜止,然後點擊“啟動快檢”,待進度條完成,即可得出快檢報告。", "Copyright © 2020-2030 东华智能睡眠中心 版权所有": " 版權所有©2020-2030 東華智能睡眠中心" } \ 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 d8279c0..d53274a 100644 Binary files a/assets/miniapp/mhtControl_1.0.90.zip and b/assets/miniapp/mhtControl_1.0.90.zip differ diff --git a/lib/common/color/ServiceConstant.dart b/lib/common/color/ServiceConstant.dart index 6a0f2bd..a4ce9d3 100644 --- a/lib/common/color/ServiceConstant.dart +++ b/lib/common/color/ServiceConstant.dart @@ -5,6 +5,8 @@ class ServiceConstant { // static const String baseHost = "vsbst-api.he-info.com";//服务地址 // static const String service_address = "http://$baseHost"; static String service_address = "https://$baseHost"; + static String qc_service_address = "https://fast-check.he-info.cn"; + static String server_service = "/vsbs_app_server"; //服务名称 @@ -63,4 +65,10 @@ class ServiceConstant { //下发wifi控制指令 static const String sendWifiCommand = "/api/device/cmd/ttd"; + //查询快检状态 + static const String checkQuickStatus = "/api/fc/status"; + //查询历史快检 + static const String getCheckHistory = "/api/fc/history"; + //更新快检状态 + static const String qcControl = "/api/fc/control"; } diff --git a/lib/common/color/appConstants.dart b/lib/common/color/appConstants.dart index 106c102..94a0b1a 100644 --- a/lib/common/color/appConstants.dart +++ b/lib/common/color/appConstants.dart @@ -8,7 +8,7 @@ import 'package:vbvs_app/enum/APPPackageType.dart'; class AppConstants { // App-related constants - static const String zhmht_app_version = "SWES_1.2026.1.8"; //眠花糖 + static const String zhmht_app_version = "SWES_1.2026.3.6"; //眠花糖 static const String theh_app_version = "1.2601.12"; //太和 // 1. 纯字符串列表格式 @@ -61,9 +61,11 @@ class AppConstants { double smaller_text_fontSize = 18.rpx; //普通文字字号 double small_text_fontSize = 20.rpx; //普通文字字号 + double small_an_text_fontSize = 21.rpx; //普通文字字号 double middler_text_fontSize = 24.rpx; //普通文字字号 double normal_text_fontSize = 26.rpx; //普通文字字号 double title_text_fontSize = 30.rpx; //标题文字字号 + double bigger_text_fontSize = 36.rpx; //标题文字字号 double dropdown_height = 90.rpx; //标题文字字号 double border_width = 1.rpx; //标题文字字号//border宽度 @@ -89,8 +91,8 @@ class AppConstants { //系统参数 //运行打包APP模式 - int ent_type = APPPackageType.MHT.code; //1.默认太和 2.欢睡 3.眠花糖 - // int ent_type = APPPackageType.TH.code; //1.默认太和 2.欢睡 3.眠花糖 + // int ent_type = APPPackageType.MHT.code; //1.默认太和 2.欢睡 3.眠花糖 + int ent_type = APPPackageType.TH.code; //1.默认太和 2.欢睡 3.眠花糖 // int ent_type = APPPackageType.HUANSHUI.code; //1.默认太和 2.欢睡 3.眠花糖 // int ent_type = APPPackageType.DONGHUA.code; //1.默认太和 2.欢睡 3.眠花糖 4.东华 // int ent_type = APPPackageType.HAIER.code; //1.默认太和 2.欢睡 3.眠花糖 4.东华 5.海尔沃棣 diff --git a/lib/component/home_page/DynamicReportDetailWidget.dart b/lib/component/home_page/DynamicReportDetailWidget.dart index 9180dd2..db66b02 100644 --- a/lib/component/home_page/DynamicReportDetailWidget.dart +++ b/lib/component/home_page/DynamicReportDetailWidget.dart @@ -565,23 +565,25 @@ class _DynamicReportDetailWidgetState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ClickableContainer( - backgroundColor: Colors.transparent, - highlightColor: themeController.currentColor.sc3.withOpacity(0.2), - padding: EdgeInsets.zero, - onTap: () async { - await Get.toNamed("/bodyDevice", arguments: targetDevice); - }, - child: Text( - targetDevice['person']?['name']?.isNotEmpty == true - ? targetDevice['person']['name'] - : '体征监测设备'.tr, - style: TextStyle( - fontSize: 30.rpx, - color: themeController.currentColor.sc3, + Expanded( + child: ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: themeController.currentColor.sc3.withOpacity(0.2), + padding: EdgeInsets.zero, + onTap: () async { + await Get.toNamed("/bodyDevice", arguments: targetDevice); + }, + child: Text( + targetDevice['person']?['name']?.isNotEmpty == true + ? targetDevice['person']['name'] + : '体征监测设备'.tr, + style: TextStyle( + fontSize: 30.rpx, + color: themeController.currentColor.sc3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), ), if (!AppConstants.is_test_account) diff --git a/lib/controller/device/device_type_controller.dart b/lib/controller/device/device_type_controller.dart index 665ddfd..61bd427 100644 --- a/lib/controller/device/device_type_controller.dart +++ b/lib/controller/device/device_type_controller.dart @@ -8,6 +8,7 @@ import 'package:vbvs_app/common/color/ServiceConstant.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/MyUtils.dart'; +import 'package:vbvs_app/common/util/requestWithLog.dart'; import 'package:vbvs_app/controller/setting/language/language_controller.dart'; import 'package:vbvs_app/enum/APPPackageType.dart'; import 'package:vbvs_app/model/api_response.dart'; @@ -42,7 +43,13 @@ class DeviceTypeController extends GetControllerEx { attr = GetModel(DeviceTypeModel()).obs; } RxList deviceTypeList = [].obs; + RxList cqHistoryList = [].obs; + RxMap currentCq = {}.obs; LanguageController languageController = Get.find(); + // 404:未找到快检信息 200:快检进行中 201:快检完成 202:快检超时 203:已取消 500:计算错误 + RxInt experience_status = 404.obs; + RxInt experience_percent = 0.obs; //体验进度 + RxString experience_id = "".obs; //体验id Future getDeviceType() async { ApiResponse apiResponse = ApiResponse(code: -1, msg: "请求失败".tr); @@ -51,7 +58,7 @@ class DeviceTypeController extends GetControllerEx { String serviceApi = ServiceConstant.device_type; String queryUrl = "${serviceAddress}${serviceName}${serviceApi}"; - String? language = ""; + String? language = ""; if (AppConstants().ent_type == APPPackageType.MHT.code) { if (mhLanguageController.selectLanguage != null) { language = mhLanguageController.selectLanguage.value!.language_code; @@ -90,4 +97,177 @@ class DeviceTypeController extends GetControllerEx { return apiResponse; } } + + Future checkReportStatus(String mac) async { + String serviceAddress = ServiceConstant.qc_service_address; + String serviceApi = ServiceConstant.checkQuickStatus; + String queryUrl = "$serviceAddress$serviceApi?mac=${mac}"; + bool flag = false; + await requestWithLog( + logTitle: "查询快检状态", + method: MyHttpMethod.get, + queryUrl: queryUrl, + onSuccess: (res) { + flag = res.data['status'] != 200; + experience_status.value = res.data['status']; + experience_percent.value = res.data['per'] ?? 0; + experience_id.value = res.data['id'] ?? ""; + updateAll(); + }, + onFailure: (res) { + flag = false; + }, + ); + return flag; + } + + Future getCheckHistory({String? id}) async { + String serviceAddress = ServiceConstant.qc_service_address; + String serviceApi = ServiceConstant.getCheckHistory; + String queryUrl = "$serviceAddress$serviceApi"; + + // 如果id不为空,拼接到url上 + if (id != null && id.isNotEmpty) { + queryUrl = "$queryUrl?id=$id"; + } + + bool flag = false; + await requestWithLog( + logTitle: "查询快检历史", + method: MyHttpMethod.get, + queryUrl: queryUrl, + onSuccess: (res) { + flag = true; + // 判断返回数据类型 + if (res.data is List) { + // 如果是列表,赋值给cqHistoryList + cqHistoryList.value = (res.data as List).cast>(); + } else if (res.data is Map) { + // 如果是Map,赋值给currentCq + currentCq.value = res.data as Map; + } + updateAll(); + }, + onFailure: (res) { + flag = false; + currentCq.value = {}; + }, + ); + return flag; + } + + //快检状态控制 type 1:开始快检2:停止快检 + qcCheckControl(Map device, int type) async { + String serviceAddress = ServiceConstant.qc_service_address; + String serviceApi = ServiceConstant.qcControl; + String queryUrl = "$serviceAddress$serviceApi"; + + // 从device中获取person信息,如果person存在则使用,否则使用默认值 + Map personData = {}; + if (device['person'] != null && device['person'] is Map) { + // 处理name:null或空字符串时使用默认值 + String name = device['person']['name']; + if (name == null || name.trim().isEmpty) { + name = "体征检测设备".tr; + } + + // 处理gender:null或空字符串时使用默认值"男" + String gender = "男"; // 默认值 + if (device['person']['gender'] != null) { + var genderValue = device['person']['gender']; + if (genderValue is int) { + gender = genderValue == 1 ? "男" : "女"; + } else if (genderValue is String && genderValue.isNotEmpty) { + gender = genderValue == "1" ? "男" : "女"; + } + } + + // 处理birthday:null或空字符串时使用默认年龄52 + int age = 52; // 默认年龄 + String? birthday = device['person']['birthday']; + if (birthday != null && birthday.trim().isNotEmpty) { + age = _calculateAge(birthday); + } + + // 处理weight:null或空字符串时使用默认值 + dynamic weightValue = device['person']['weight']; + double weight = 37.5; // 默认值 + if (weightValue != null) { + if (weightValue is num) { + weight = weightValue.toDouble(); + } else if (weightValue is String && weightValue.trim().isNotEmpty) { + weight = double.tryParse(weightValue) ?? 37.5; + } + } + + // 处理height:null或空字符串时使用默认值 + dynamic heightValue = device['person']['height']; + double height = 165.0; // 默认值 + if (heightValue != null) { + if (heightValue is num) { + height = heightValue.toDouble(); + } else if (heightValue is String && heightValue.trim().isNotEmpty) { + height = double.tryParse(heightValue) ?? 165.0; + } + } + + personData = { + "name": name, + "gender": gender, + "age": age, + "weight": weight, + "height": height + }; + } else { + // person为空时的默认值 + personData = { + "name": "姓名", + "gender": "男", + "age": 52, + "weight": 37.5, + "height": 165 + }; + } + + // 构建请求参数 + Map data = { + "person": personData, + "mac": device['mac'], // 使用device中的mac + "type": type + }; + + bool flag = false; + await requestWithLog( + logTitle: "更新快检状态", + method: MyHttpMethod.post, + queryUrl: queryUrl, + data: data, + onSuccess: (res) { + flag = true; + }, + onFailure: (res) { + flag = false; + }, + ); + return flag; + } + +// 添加一个辅助方法来根据生日计算年龄 + int _calculateAge(String birthday) { + if (birthday.isEmpty) return 52; + + try { + // 假设生日格式为 "2017/03/06" + List parts = birthday.split('/'); + if (parts.length == 3) { + int birthYear = int.parse(parts[0]); + int currentYear = DateTime.now().year; + return currentYear - birthYear; + } + } catch (e) { + print("计算年龄出错: $e"); + } + + return 52; // 出错时返回默认年龄 + } } diff --git a/lib/main.dart b/lib/main.dart index 67f3968..07bfe5c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -697,7 +697,7 @@ void initEasyDartModule() { try { EasyDartModule.init( loggerConfig: LoggerConfig( - host: ServiceConstant.logService, serviceName: "眠花糖在线2025-10-28"), + host: "https://zhmht.swes.com.cn:27020/vsbs_log", serviceName: "眠花糖在线2026-03-03"), webSocketConfig: WebSocketConfig(ServiceConstant.webSocketService, (data) { // 接收到服务消息 diff --git a/lib/pages/device/component/DeviceDataComponentWidget.dart b/lib/pages/device/component/DeviceDataComponentWidget.dart index 300c81e..9d80d87 100644 --- a/lib/pages/device/component/DeviceDataComponentWidget.dart +++ b/lib/pages/device/component/DeviceDataComponentWidget.dart @@ -99,6 +99,7 @@ class _DeviceDataComponentWidgetState extends State { final allTexts = [ "体征检测设备.首页展示".tr, + "体征检测设备.人员资料".tr, "体征检测设备.设备详情".tr, "WIFI配置".tr, "设备校准".tr, @@ -286,6 +287,69 @@ class _DeviceDataComponentWidgetState extends State { ? themeController.currentColor.sc2 : themeController.currentColor.sc3, ), + _buildMenuItem( + text: "人员资料.标题".tr, + onTap: () async { + setState(() { + _isPopupOpen = false; + }); + _popupEntry?.remove(); + _popupEntry = null; + if (widget.device['person'] != null) { + personController.currentPersonId.value = widget.device['_id']; + personController.name.value = widget.device['person']['name']; + personController.update_person_mac.value = widget.device['mac']; + personController.gender.value = + widget.device['person']['gender'] ?? 1; + personController.weight?.value = + widget.device['person']['weight'] == null + ? '' + : widget.device['person']['weight'].toString(); + + personController.height.value = + widget.device['person']['height'] == null + ? '' + : widget.device['person']['height'].toString(); + + personController.selectedDiseaseIds.value = + widget.device['person']['disease'] ?? []; + personController.birthday.value = + widget.device['person']['birthday'] ?? ''; + personController.dateTime = + MyUtils.formatBirthdayTime(widget.device['person']['birthday']); + personController.timeZone.value = + widget.device['person']['UTC'] ?? ''; + if (widget.device['person']['city_id'] != null) { + // 根据city_id查找完整的城市数据 + final int cityId = widget.device['person']['city_id']; + await initializeCityData(); + final CityModel? completeCityData = _findCityDataById(cityId); + + if (completeCityData != null) { + personController.cityModel = completeCityData; + } else { + personController.cityModel = null; + } + } else { + personController.cityModel = null; + } + } else { + personController.update_person_mac.value = widget.device['mac']; + personController.currentPersonId.value = widget.device['_id']; + personController.name.value = ""; + personController.gender.value = 1; + personController.dateTime = null; + personController.height.value = ""; + personController.weight.value = ""; + personController.diseaseList.value = []; + personController.cityModel = null; + personController.timeZone.value = ""; + } + await Get.toNamed("/updatePersonPage", + arguments: widget.device['bind_type']); + bodyDeviceController.getDeviceList(); + }, + ), _buildMenuItem( text: "体征检测设备.设备详情".tr, onTap: () { @@ -1158,69 +1222,8 @@ class _DeviceDataComponentWidgetState extends State { child: CustomCard( borderRadius: AppConstants().button_container_radius, onTap: () async { - if (widget.device['person'] != null) { - personController.currentPersonId.value = - widget.device['_id']; - personController.name.value = - widget.device['person']['name']; - personController.update_person_mac.value = - widget.device['mac']; - personController.gender.value = - widget.device['person']['gender'] ?? 1; - personController.weight?.value = - widget.device['person']['weight'] == null - ? '' - : widget.device['person']['weight'] - .toString(); - - personController.height.value = - widget.device['person']['height'] == null - ? '' - : widget.device['person']['height'] - .toString(); - - personController.selectedDiseaseIds.value = - widget.device['person']['disease'] ?? []; - personController.birthday.value = - widget.device['person']['birthday'] ?? ''; - personController.dateTime = - MyUtils.formatBirthdayTime( - widget.device['person']['birthday']); - personController.timeZone.value = - widget.device['person']['UTC'] ?? ''; - if (widget.device['person']['city_id'] != null) { - // 根据city_id查找完整的城市数据 - final int cityId = - widget.device['person']['city_id']; - await initializeCityData(); - final CityModel? completeCityData = - _findCityDataById(cityId); - - if (completeCityData != null) { - personController.cityModel = completeCityData; - } else { - personController.cityModel = null; - } - } else { - personController.cityModel = null; - } - } else { - personController.update_person_mac.value = - widget.device['mac']; - personController.currentPersonId.value = - widget.device['_id']; - personController.name.value = ""; - personController.gender.value = 1; - personController.dateTime = null; - personController.height.value = ""; - personController.weight.value = ""; - personController.diseaseList.value = []; - personController.cityModel = null; - personController.timeZone.value = ""; - } - await Get.toNamed("/updatePersonPage", - arguments: widget.device['bind_type']); - bodyDeviceController.getDeviceList(); + await Get.toNamed("/healthCheckPage", + arguments: widget.device); }, colors: [ themeController.currentColor.sc1, @@ -1241,7 +1244,7 @@ class _DeviceDataComponentWidgetState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - "体征检测设备.人员资料".tr, + "健康快检".tr, style: TextStyle( color: themeController.currentColor.sc3, fontFamily: 'Inter', diff --git a/lib/pages/device/component/HealthReportCard.dart b/lib/pages/device/component/HealthReportCard.dart new file mode 100644 index 0000000..b1df062 --- /dev/null +++ b/lib/pages/device/component/HealthReportCard.dart @@ -0,0 +1,234 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/component/tool/NewTopSlideNotification.dart'; +import 'package:vbvs_app/controller/device/device_share_controller.dart'; +import 'package:vbvs_app/controller/device/device_type_controller.dart'; +import 'package:vbvs_app/controller/message/message_controller.dart'; +import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; + +class HealthReportCardWidget extends StatefulWidget { + final data; + + const HealthReportCardWidget({super.key, required this.data}); + + @override + State createState() => _HealthReportCardWidgetState(); +} + +class _HealthReportCardWidgetState extends State { + ThemeController themeController = Get.find(); + MessageController messageController = Get.find(); + DeviceShareController deviceShareController = Get.find(); + + @override + Widget build(BuildContext context) { + var reportData = widget.data; + return ClickableContainer( + backgroundColor: themeController.currentColor.sc5, + highlightColor: themeController.currentColor.sc21, + borderRadius: 20.rpx, + padding: EdgeInsetsDirectional.fromSTEB(31.rpx, 30.rpx, 31.rpx, 33.rpx), + onTap: () {}, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + // Container( + // width: double.infinity, + // constraints: BoxConstraints( + // minHeight: 66.rpx, + // ), + // child: Align( + // alignment: AlignmentDirectional(-1, 0), + // child: Text( + // reportData['name'] ?? '健康报告', + // style: TextStyle( + // fontFamily: 'Inter', + // fontSize: 30.rpx, + // letterSpacing: 0.0, + // color: themeController.currentColor.sc3, + // ), + // ), + // ), + // ), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 左侧标签列 + Container( + constraints: BoxConstraints( + minWidth: 30.rpx, + maxWidth: 140.rpx, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoItem(context, '姓名'.tr), + _buildInfoItem(context, '设备ID'.tr), + _buildInfoItem(context, '快检得分'.tr), + _buildInfoItem(context, '创建时间'.tr), + ], + ), + ), + // 右侧数值列 + Expanded( + child: Container( + constraints: BoxConstraints( + minWidth: 30.rpx, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildValueItem(context, reportData['name'] ?? '--'), + _buildValueItem(context, reportData['mac'] ?? '--'), + _buildValueItem( + context, '${reportData['score'] ?? '--'}分'), + _buildValueRowWithButton( + context, + _formatTime(reportData['create_time']), + onViewTap: () async { + // 查看按钮点击事件 + String ID = reportData['id']; + DeviceTypeController deviceTypeController = + Get.find(); + await deviceTypeController.getCheckHistory(id: ID); + if (deviceTypeController.currentCq.value == null || + deviceTypeController.currentCq.value.isEmpty) { + NewTopSlideNotification.show( + text: "未找到快检报告".tr, + textColor: themeController.currentColor.sc9); + return; + } + _onViewReport(deviceTypeController.currentCq.value); + }, + ), + ], + ), + ), + ), + ].divide(SizedBox(width: 35.rpx)), + ), + ], + ), + ); + } + + Widget _buildInfoItem(BuildContext context, label) { + return Container( + constraints: BoxConstraints( + minHeight: 62.rpx, + ), + child: Align( + alignment: AlignmentDirectional(-1, 0), + child: Text( + overflow: TextOverflow.ellipsis, + maxLines: 1, + "$label", + style: TextStyle( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController.currentColor.sc4, + ), + ), + ), + ); + } + + Widget _buildValueItem(BuildContext context, value) { + return Container( + constraints: BoxConstraints( + minHeight: 62.rpx, + ), + child: Align( + alignment: AlignmentDirectional(-1, 0), + child: Text( + overflow: TextOverflow.ellipsis, + maxLines: 1, + "$value", + style: TextStyle( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController.currentColor.sc3, + ), + ), + ), + ); + } + + // 创建时间行,包含时间和查看按钮 + Widget _buildValueRowWithButton(BuildContext context, String value, + {required VoidCallback onViewTap}) { + return Container( + constraints: BoxConstraints( + minHeight: 62.rpx, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + overflow: TextOverflow.ellipsis, + maxLines: 1, + value, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController.currentColor.sc3, + ), + ), + ), + // 查看按钮 + ClickableContainer( + onTap: onViewTap, + borderRadius: 50.rpx, + padding: EdgeInsets.symmetric( + horizontal: 26.rpx, + vertical: 6.rpx, + ), + backgroundColor: themeController.currentColor.sc2, + highlightColor: Colors.transparent, + child: Text( + '查看', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 24.rpx, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } + + // 查看报告的方法 + void _onViewReport(Map reportData) { + // TODO: 实现查看报告详情的逻辑 + // 可以根据需要跳转到报告详情页面 + print('查看报告: ${reportData['_id']}'); + // 示例:Get.to(() => ReportDetailPage(reportId: reportData['_id'])); + Get.toNamed('/healthQuickCheckReportPage', arguments: reportData); + } + + // 格式化时间戳 + String _formatTime(dynamic timestamp) { + if (timestamp == null) return '--'; + + try { + // 假设时间戳是毫秒级 + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + // 格式化为:年/月/日 时:分 + return "${dateTime.year}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; + } catch (e) { + return '--'; + } + } +} diff --git a/lib/pages/device/deviceCopy.dart b/lib/pages/device/deviceCopy.dart index eb6e6bd..393b907 100644 --- a/lib/pages/device/deviceCopy.dart +++ b/lib/pages/device/deviceCopy.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:math'; + import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -9,7 +9,6 @@ import 'package:vbvs_app/common/color/app_uri_status.dart'; import 'package:vbvs_app/common/util/EventBus.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; -import 'package:vbvs_app/common/util/eventType.dart'; import 'package:vbvs_app/component/NullDataComponentWidget.dart'; import 'package:vbvs_app/component/tool/ClickableContainer.dart'; import 'package:vbvs_app/component/tool/CustomCard.dart'; diff --git a/lib/pages/device/health_experience.dart b/lib/pages/device/health_experience.dart new file mode 100644 index 0000000..5b59eb2 --- /dev/null +++ b/lib/pages/device/health_experience.dart @@ -0,0 +1,1821 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:EasyDartModule/EasyDartModule.dart' as edm; +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/CommonVariables.dart'; +import 'package:vbvs_app/common/util/DailyLogUtils.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/component/tool/CustomCard.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/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/model/WebSocketMessage.dart'; +import 'package:vbvs_app/pages/device/component/DeviceStatusInfoWidget.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/CalibrationProgressWidget.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; + +class HealthCheckPage extends StatefulWidget { + var personInfo; + HealthCheckPage({super.key, required this.personInfo}); + + @override + State createState() => _HealthCheckPageState(); +} + +class _HealthCheckPageState extends State + with WidgetsBindingObserver { + GlobalController globalController = Get.find(); + UserInfoController userInfoController = Get.find(); + BlueteethBindController blueteethBindController = Get.find(); + ThemeController themeController = Get.find(); + DeviceTypeController deviceTypeController = Get.find(); + + int maxBodyMotion = 1; + String breathState = "-"; + String inBed = "-"; + String onlineState = "离线".tr; + Timer? _onlineTimer; // 添加 Timer 引用 + int bodyMotion = -1; + int breathrate = -1; + String snores = "-"; + int heartrate = -1; + + final ValueNotifier progressNotifier = ValueNotifier(0.0); + final ValueNotifier failureNotifier = ValueNotifier(false); + + Timer? _checkStatusTimer; // 添加状态查询定时器 + bool _isInitialized = false; // 添加初始化标志 + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); // 添加生命周期观察者 + try { + deviceTypeController + .checkReportStatus(widget.personInfo['mac']) + .then((_) { + setState(() { + _isInitialized = true; + }); + // 如果当前状态是体验中,启动定时器 + if (deviceTypeController.experience_status.value == 200) { + _startCheckStatusTimer(); + } + }); + } catch (e) { + ef.log("快检初始化数据失败"); + } + try { + Future.delayed(Duration(seconds: 0), () { + CommonVariables.callMap["/vsbs/web/rt/marttress"] = (data) { + edm.EasyDartModule.logger.info("[websocket]实时体征页面数据-->${data}]"); + ef.log("[websocket]实时体征页面数据-->${data}]"); + if (data['status'] == "离线") { + inBed = "-"; + bodyMotion = -1; + heartrate = -1; + snores = "-"; + breathrate = -1; + breathState = "-"; + onlineState = "离线".tr; + return; + } + inBed = data["inBed"]; + // 心率 呼吸 体动 呼吸暂停 + if ("离床".tr == inBed) { + breathState = "否".tr; + bodyMotion = 0; + breathrate = 0; + heartrate = 0; + snores = "否".tr; + } else { + onlineState = "在线".tr; // 接收到数据,设置为在线 + breathState = + data["breathState"] == null || data["breathState"] == "" + ? "-" + : data["breathState"].toString().tr; + + bodyMotion = data['bodyMotion'] == null ? -1 : data['bodyMotion']; + breathrate = data["breathRate"] == null ? -1 : data["breathRate"]; + heartrate = data['heartRate'] == null ? -1 : data['heartRate']; + snores = data['snores'] == null || + data['snores'] == "" || + data['snores'] == "否".tr + ? "否".tr + : "${data['snores']}".tr; + } + + if (mounted) { + setState(() { + onlineState = "在线".tr; // 接收到数据,设置为在线 + }); + } + _startOnlineTimer(); // 重置定时器 + }; + }); + } catch (e) { + print(e); + edm.EasyDartModule.logger + .error("[webscoekt]格式化数据错误-->${{"mac": widget.personInfo['mac']}}"); + } + _initWebSocket(); + + super.initState(); + } + + void _initWebSocket() { + // 发送WebSocket请求 + if (widget.personInfo['status'] != null) { + try { + onlineState = + widget.personInfo['status']['status'] == 1 ? "在线".tr : "离线".tr; + if (widget.personInfo['status']['status'] != 0) { + inBed = widget.personInfo['status']['inBed'] == 1 ? "在床".tr : "离床".tr; + } + } catch (e) { + edm.EasyDartModule.logger + .error("[webscoekt]格式化数据错误-->${{"mac": widget.personInfo['mac']}}"); + } + } + edm.EasyDartModule.logger + .info("[webscoekt]发送请求:数据-->${{"mac": widget.personInfo['mac']}}"); + DailyLogUtils.writeLog( + "[webscoekt]发送请求:数据-->${{"mac": widget.personInfo['mac']}}"); + edm.EasyDartModule.websocket.sendData(jsonEncode(WebSocketMessage( + path: "/vsbs/web/rt/marttress", + type: 1, + data: {"mac": widget.personInfo['mac']}))); + _startOnlineTimer(); + } + //y + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); // 移除生命周期观察者 + _onlineTimer?.cancel(); + _checkStatusTimer?.cancel(); // 取消状态查询定时器 + _closeWebSocket(); + CommonVariables.callMap.remove("/vsbs/web/rt/marttress"); + super.dispose(); + } + + // 监听应用生命周期变化 + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + // 应用回到前台时重新连接WebSocket + edm.EasyDartModule.logger.info("app切回页面,重连websocket"); + _initWebSocket(); + + // 重新查询状态,然后根据状态决定是否启动定时器 + deviceTypeController + .checkReportStatus(widget.personInfo['mac']) + .then((_) { + // 如果当前状态是体验中,重新启动定时器 + if (deviceTypeController.experience_status.value != 404) { + _startCheckStatusTimer(); + } + }); + } else if (state == AppLifecycleState.paused) { + // 应用进入后台时关闭WebSocket和定时器 + _closeWebSocket(); + _checkStatusTimer?.cancel(); + } + } + + void _startOnlineTimer() { + _onlineTimer?.cancel(); // 取消之前的定时器 + _onlineTimer = Timer.periodic(Duration(seconds: 60), (timer) { + if (mounted) { + setState(() { + edm.EasyDartModule.logger.info("60 秒内没有接收到数据,设置为离线"); + onlineState = "离线".tr; // 30 秒内没有接收到数据,设置为离线 + inBed = "-"; + bodyMotion = -1; + heartrate = -1; + snores = "-"; + breathrate = -1; + breathState = "-"; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + Map device = widget.personInfo; + + return LayoutBuilder( + builder: (context, bodySize) => GestureDetector( + // onTap: () => FocusScope.of(context).unfocus(),, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(getBackgroundImageNoImage()), + fit: BoxFit.fill, + ), + ), + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: themeController.currentColor.sc17, + automaticallyImplyLeading: false, + iconTheme: IconThemeData(color: themeController.currentColor.sc3), + titleSpacing: 0.rpx, + title: Container( + width: double.infinity, + height: 180.rpx, + child: Stack( + alignment: Alignment.center, + children: [ + RichText( + text: TextSpan( + style: TextStyle( + fontFamily: 'Readex Pro', + color: themeController.currentColor.sc3, + fontSize: 30.rpx, + letterSpacing: 0.0, + ), + children: [ + TextSpan( + text: '健康快检'.tr, + ), + ], + ), + ), + Positioned( + left: 0.rpx, + child: returnIconButtom, + ), + Obx(() { + return deviceTypeController.experience_status.value != 200 + ? Positioned( + right: 0.rpx, + child: ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.transparent, + padding: EdgeInsets.fromLTRB( + 20.rpx, 20.rpx, 20.rpx, 20.rpx), + onTap: () { + Get.toNamed('/healthExperienceHistory', + arguments: widget.personInfo); + // Get.toNamed('/healthQuickCheckReportPage', + // arguments: widget.personInfo); + }, + child: SvgPicture.asset( + 'assets/img/icon/history.svg', + width: 35.rpx, + height: 35.rpx, + color: themeController.currentColor.sc3, + ), + ), + ) + : Container(); + }) + ], + ), + ), + actions: [], + centerTitle: false, + ), + body: SafeArea( + top: true, + child: Obx(() { + return Stack( + children: [ + Align( + alignment: Alignment.center, + child: FractionallySizedBox( + heightFactor: 0.5, + widthFactor: 0.5, + child: Opacity( + opacity: 0.7, + child: (onlineState == "离线".tr || inBed == '离床'.tr) + ? Image.asset( + 'assets/img/black_body_still.png', + fit: BoxFit.contain, + ) + : SpeedControlledGif( + 'assets/img/body_black.gif', + speedFactor: + 2, // 2.0 for 2x speed, 0.5 for half speed + fit: BoxFit.contain, + ), + ), + ), + ), + // 其余内容放在上层 + Positioned.fill( + child: Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0.rpx, 29.rpx, 0.rpx, 0.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + if (deviceTypeController + .experience_status.value != + 200) + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 100.rpx), + child: ClickableContainer( + backgroundColor: + themeController.currentColor.sc5, + highlightColor: + Colors.transparent, // 点击涟漪颜色 + borderRadius: AppConstants() + .normal_container_radius, // 如果你想加圆角可以设置 eg. 12.rpx + padding: EdgeInsets.zero, + onTap: () { + print('点击了体征卡片'); + }, + child: Column( + children: [ + // Row( + // mainAxisSize: MainAxisSize.max, + // children: [ + // Flexible( + // flex: 2, + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Row( + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .end, + // children: [ + // Text( + // '实时体征.姓名'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc4, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Container( + // width: MediaQuery + // .sizeOf( + // context) + // .width * + // 0.2, + // child: Text( + // device['person'] != null && + // device['person'] + // [ + // 'name'] != + // null && + // device['person'] + // [ + // 'name'] + // .toString() + // .trim() + // .isNotEmpty + // ? device['person'] + // [ + // 'name'] + // .toString() + // : '体征检测设备'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: + // 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc3, + // ), + // maxLines: 1, + // overflow: + // TextOverflow + // .ellipsis, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // ] + // .divide(SizedBox( + // width: 33.rpx)) + // .addToStart(SizedBox( + // width: 37.rpx)), + // ), + // Row( + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .end, + // children: [ + // Text( + // '性别'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc4, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Container( + // width: MediaQuery + // .sizeOf( + // context) + // .width * + // 0.2, + // child: Text( + // device['person'] != null && + // device['person'] + // [ + // 'gender'] != + // null && + // device['person'] + // [ + // 'gender'] + // .toString() + // .trim() + // .isNotEmpty + // ? _getGenderText( + // device['person'] + // [ + // 'gender']) + // : '-'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: + // 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc3, + // ), + // maxLines: 1, + // overflow: + // TextOverflow + // .ellipsis, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // ] + // .divide(SizedBox( + // width: 33.rpx)) + // .addToStart(SizedBox( + // width: 37.rpx)), + // ), + // Row( + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .end, + // children: [ + // Text( + // '身高'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc4, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Container( + // width: MediaQuery + // .sizeOf( + // context) + // .width * + // 0.2, + // child: Text( + // device['person'] != null && + // device['person'] + // [ + // 'height'] != + // null && + // device['person'] + // [ + // 'height'] + // .toString() + // .trim() + // .isNotEmpty + // ? device['person'] + // [ + // 'height'] + // .toString() + + // "cm" + // : '-'.tr + + // "cm", + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: + // 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc3, + // ), + // maxLines: 1, + // overflow: + // TextOverflow + // .ellipsis, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // ] + // .divide(SizedBox( + // width: 33.rpx)) + // .addToStart(SizedBox( + // width: 37.rpx)), + // ), + // ] + // .addToStart( + // SizedBox(height: 0.rpx)) + // .addToEnd( + // SizedBox(height: 0.rpx)) + // .divide(SizedBox( + // height: 36.rpx, + // )), + // ), + // ), + // Flexible( + // flex: 3, + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // Row( + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .end, + // children: [ + // Text( + // '实时体征.设备ID'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc4, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // Expanded( + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Text( + // '${device['code'] ?? '未知数据'.tr}', + // // "D11250300003", + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: + // 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc3, + // ), + // maxLines: 1, + // overflow: + // TextOverflow + // .ellipsis, + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // ), + // ] + // .divide(SizedBox( + // width: 33.rpx)) + // .addToStart(SizedBox( + // width: 37.rpx)), + // ), + // Row( + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .end, + // children: [ + // Text( + // '年龄'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc4, + // ), + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // Expanded( + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Text( + // device['person'] != null && + // device['person'] + // [ + // 'birthday'] != + // null && + // device['person'] + // [ + // 'birthday'] + // .toString() + // .trim() + // .isNotEmpty + // ? _calculateAge( + // device['person'] + // [ + // 'birthday'] + // .toString()) + // : '-'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: + // 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc3, + // ), + // maxLines: 1, + // overflow: + // TextOverflow + // .ellipsis, + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // ), + // ] + // .divide(SizedBox( + // width: 33.rpx)) + // .addToStart(SizedBox( + // width: 37.rpx)), + // ), + // Row( + // children: [ + // Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .end, + // children: [ + // Text( + // '体重'.tr, + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc4, + // ), + // ), + // // Text( + // // '实时体征.体重'.tr, + // // style: TextStyle( + // // fontFamily: 'Inter', + // // fontSize: 26.rpx, + // // letterSpacing: 0.0, + // // color: themeController + // // .currentColor.sc4, + // // ), + // // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // Expanded( + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Text( + // device['person'] != null && + // device['person'] + // [ + // 'weight'] != + // null && + // device['person'] + // [ + // 'weight'] + // .toString() + // .trim() + // .isNotEmpty + // ? device['person'] + // [ + // 'weight'] + // .toString() + + // "kg" + // : '-'.tr + + // "kg", + // style: TextStyle( + // fontFamily: + // 'Inter', + // fontSize: + // 26.rpx, + // letterSpacing: + // 0.0, + // color: themeController + // .currentColor + // .sc3, + // ), + // maxLines: 1, + // overflow: + // TextOverflow + // .ellipsis, + // ), + // ].divide(SizedBox( + // height: 34.rpx)), + // ), + // ), + // ] + // .divide(SizedBox( + // width: 33.rpx)) + // .addToStart(SizedBox( + // width: 37.rpx)), + // ), + // ] + // .addToStart( + // SizedBox(height: 0.rpx)) + // .addToEnd( + // SizedBox(height: 0.rpx)) + // .divide(SizedBox( + // height: 36.rpx, + // )), + // ), + // ), + // ], + // ), + Container( + padding: EdgeInsets.fromLTRB( + 37.rpx, 37.rpx, 37.rpx, 37.rpx), + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // 左侧列 - 标签和值 + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // 姓名行 + Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + // 标签 - 也不能换行,超出显示... + Expanded( + flex: 1, + child: Text( + '实时体征.姓名'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc4, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + SizedBox( + width: 20 + .rpx), // 标签和值之间的间距 + // 值 + Expanded( + flex: 2, + child: Text( + device['person'] != + null && + device['person'][ + 'name'] != + null && + device['person'] + [ + 'name'] + .toString() + .trim() + .isNotEmpty + ? device['person'] + ['name'] + .toString() + : '体征检测设备'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc3, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + SizedBox(height: 36.rpx), + + // 性别行 + Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Expanded( + flex: 1, + child: Text( + '性别'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc4, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + SizedBox(width: 20.rpx), + Expanded( + flex: 2, + child: Text( + device['person'] != + null && + device['person'] + [ + 'gender'] != + null && + device['person'] + [ + 'gender'] + .toString() + .trim() + .isNotEmpty + ? _getGenderText( + device['person'] + [ + 'gender']) + : '-'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc3, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + SizedBox(height: 36.rpx), + + // 身高行 + Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Expanded( + flex: 1, + child: Text( + '身高'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc4, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + SizedBox(width: 20.rpx), + Expanded( + flex: 2, + child: Text( + device['person'] != + null && + device['person'] + [ + 'height'] != + null && + device['person'] + [ + 'height'] + .toString() + .trim() + .isNotEmpty + ? '${device['person']['height']}cm' + : '-'.tr + "cm", + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc3, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + ], + ), + ), + + SizedBox( + width: 30.rpx), // 左右两列之间的间距 + + // 右侧列 - 标签和值 + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // 设备ID行 + Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Expanded( + flex: 1, + child: Text( + '实时体征.设备ID'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc4, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + SizedBox(width: 20.rpx), + Expanded( + flex: 2, + child: Text( + '${device['code'] ?? '未知数据'.tr}', + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc3, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + SizedBox(height: 36.rpx), + + // 年龄行 + Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Expanded( + flex: 1, + child: Text( + '年龄'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc4, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + SizedBox(width: 20.rpx), + Expanded( + flex: 2, + child: Text( + device['person'] != + null && + device['person'] + [ + 'birthday'] != + null && + device['person'] + [ + 'birthday'] + .toString() + .trim() + .isNotEmpty + ? _calculateAge( + device['person'] + [ + 'birthday'] + .toString()) + : '-'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc3, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + SizedBox(height: 36.rpx), + + // 体重行 + Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Expanded( + flex: 1, + child: Text( + '体重'.tr, + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc4, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + SizedBox(width: 20.rpx), + Expanded( + flex: 2, + child: Text( + device['person'] != + null && + device['person'] + [ + 'weight'] != + null && + device['person'] + [ + 'weight'] + .toString() + .trim() + .isNotEmpty + ? '${device['person']['weight']}kg' + : '-'.tr + "kg", + style: TextStyle( + fontFamily: + 'Inter', + fontSize: 26.rpx, + color: themeController + .currentColor + .sc3, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + if (deviceTypeController + .experience_status.value == + 200) + Padding( + padding: + EdgeInsets.fromLTRB(46.rpx, 0, 46.rpx, 0), + child: Column( + children: [ + SizedBox( + height: 103.rpx, + ), + Text( + "快检中...".tr, + style: TextStyle( + color: + themeController.currentColor.sc1, + fontSize: AppConstants() + .title_text_fontSize, + ), + ), + SizedBox( + height: 30.rpx, + ), + Text( + "MAC号".tr + + ": ${widget.personInfo['mac'] ?? '未知数据'.tr}", + style: TextStyle( + color: + themeController.currentColor.sc4, + fontSize: AppConstants() + .smaller_text_fontSize, + ), + ), + SizedBox( + height: 4.rpx, + ), + Text( + "睡眠报告提示".tr, + style: TextStyle( + color: + themeController.currentColor.sc4, + fontSize: AppConstants() + .smaller_text_fontSize, + ), + ), + SizedBox( + height: 48.rpx, + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + child: deviceTypeController + .experience_status.value != + 200 + ? Container() + : Column( + children: [ + Padding( + padding: + EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0, 30.rpx, 0), + child: Container( + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + DeviceStatusInfoWidget( + title: "在离床".tr, + iconAsset: + "assets/img/icon/bed_status.svg", + value: inBed, + ), + DeviceStatusInfoWidget( + title: "体动".tr, + iconAsset: + "assets/img/icon/bodymotion.svg", + value: inBed == "离床".tr + ? ("-") + : (bodyMotion == + null || + bodyMotion == + -1) + ? "-" + : "$bodyMotion", + ), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + DeviceStatusInfoWidget( + title: "心率".tr, + iconAsset: + "assets/img/icon/heart.svg", + value: inBed == "离床".tr + ? "-" + : ((heartrate == + null || + heartrate == + -1) + ? "-" + : "$heartrate"), + ), + DeviceStatusInfoWidget( + title: "打鼾".tr, + iconAsset: + "assets/img/icon/snore.svg", + value: inBed == "离床".tr + ? "-" + : ('${snores}'.tr), + ), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + DeviceStatusInfoWidget( + title: "呼吸".tr, + iconAsset: + "assets/img/icon/breathe.svg", + value: inBed == "离床".tr + ? ("-") + : ((breathrate == + null || + breathrate == + -1) + ? "-" + : "$breathrate"), + ), + DeviceStatusInfoWidget( + title: "呼吸暂停".tr, + iconAsset: + "assets/img/icon/breathe_pause.svg", + value: inBed == "离床".tr + ? "-" + : ('${breathState}'), + ), + ], + ), + ].divide( + SizedBox(height: 49.rpx)), + ), + ), + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB( + 0.rpx, + 67.rpx, + 0.rpx, + 0.rpx), + child: Container( + height: 40.rpx, + child: Text( + bodyMotion >= maxBodyMotion + ? '请保持静止'.tr + : "", + style: TextStyle( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc9, + ), + ), + ), + ), + // SizedBox( + // height: 207.rpx, + // ), + ], + ), + )), + if (deviceTypeController + .experience_status.value != + 200) + ClickableContainer( + backgroundColor: + Colors.transparent, // 可自定义背景色 + highlightColor: Colors.transparent, // 点击涟漪颜色 + borderRadius: 16.rpx, // 圆角大小,可按需调整 + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + onTap: () {}, + child: Container( + padding: EdgeInsetsDirectional.fromSTEB( + 26.rpx, 26.rpx, 26.rpx, 26.rpx), + decoration: BoxDecoration( + // color: FlutterFlowTheme.of(context) + // .primaryBackground + // .withOpacity(0.6), // 半透明背景 + borderRadius: + BorderRadius.circular(16.rpx), + border: Border.all( + color: themeController.currentColor.sc4 + .withOpacity(0.5), + width: 0.5.rpx, + ), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Padding( + padding: + EdgeInsetsDirectional.fromSTEB( + 0, 8.rpx, 0, 0), + child: Container( + width: 23.rpx, + height: 23.rpx, + // width: double.infinity, + decoration: BoxDecoration(), + child: SvgPicture.asset( + 'assets/img/icon/tips.svg', + fit: BoxFit.cover, + color: themeController + .currentColor.sc4, + ), + ), + ), + Expanded( + child: Text( + '使用方式:人员使用健康快检功能时,只需平躺或坐在正常运行中的体征传感器传感器上方,保持静止,然后点击“启动快检”,待进度条完成,即可得出快检报告。' + .tr, + style: TextStyle( + fontFamily: 'Inter', + letterSpacing: 0.0, + color: themeController + .currentColor.sc4, + ), + ), + ), + ].divide(SizedBox(width: 23.rpx)), + ), + ), + ), + SizedBox( + height: 40.rpx, + ), + if (deviceTypeController + .experience_status.value != + 200) + Padding( + padding: EdgeInsets.fromLTRB( + 100.rpx, 0, 100.rpx, 0), + child: Column( + children: [ + CustomCard( + borderRadius: AppConstants() + .button_container_radius, // 圆角半径 + onTap: () async { + bool canStart = + await deviceTypeController + .checkReportStatus( + widget.personInfo['mac']); + if (!canStart) { + return; + } + bool opRes = + await deviceTypeController + .qcCheckControl( + widget.personInfo, 1); + if (!opRes) { + return; + } + + deviceTypeController + .experience_percent.value = 0; + progressNotifier.value = 0; + deviceTypeController + .experience_status.value = 200; + _startCheckStatusTimer(); + deviceTypeController.updateAll(); + }, + colors: AppConstants() + .thNormalButton, // 渐变色是同一个色,也可以根据需要调整 + child: Container( + width: + // MediaQuery.sizeOf(context).width * 0.66, + bodySize.maxWidth, + height: MediaQuery.sizeOf(context) + .height * + 0.055, + constraints: BoxConstraints( + minWidth: 500.rpx, + minHeight: 90.rpx, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + '开始快检'.tr, + style: TextStyle( + color: themeController + .currentColor.sc3, + fontFamily: 'Inter', + fontSize: AppConstants() + .normal_text_fontSize, + letterSpacing: 0.0, + ), + ), + ].divide(SizedBox( + width: 17.rpx, + )), + ), + ), + ), + ], + ), + ), + if (deviceTypeController + .experience_status.value == + 200) + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 100.rpx, 0.rpx, 100.rpx, 60.rpx), + child: CalibrationProgressWidget( + progressNotifier: progressNotifier, + failureNotifier: failureNotifier, + ), + ), + if (deviceTypeController + .experience_status.value == + 200) + Padding( + padding: EdgeInsets.fromLTRB( + 100.rpx, 0, 100.rpx, 0), + child: Column( + children: [ + CustomCard( + borderRadius: AppConstants() + .button_container_radius, // 圆角半径 + onTap: () { + _showCancelConfirmDialog(); + }, + colors: [ + themeController.currentColor.sc9 + ], // 渐变色是同一个色,也可以根据需要调整 + child: Container( + width: + // MediaQuery.sizeOf(context).width * 0.66, + bodySize.maxWidth, + height: MediaQuery.sizeOf(context) + .height * + 0.055, + constraints: BoxConstraints( + minWidth: 500.rpx, + minHeight: 90.rpx, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + '终止'.tr, + style: TextStyle( + color: themeController + .currentColor.sc3, + fontFamily: 'Inter', + fontSize: AppConstants() + .normal_text_fontSize, + letterSpacing: 0.0, + ), + ), + ].divide(SizedBox( + width: 17.rpx, + )), + ), + ), + ), + ], + ), + ), + SizedBox( + height: 40.rpx, + ), + SizedBox( + height: 26.rpx, + ), + ], + ), + ), + ), + ], + ); + })), + ), + ), + ), + ); + } + + void _closeWebSocket() { + // 取消WebSocket订阅 + edm.EasyDartModule.websocket.sendData( + jsonEncode(WebSocketMessage(path: "/vsbs/web/rt/marttress", type: 2))); + _onlineTimer?.cancel(); + } + + String _getGenderText(dynamic gender) { + var genderMap = { + '1': '男'.tr, + '2': '女'.tr, + }; + + String genderStr = gender.toString().trim(); + return genderMap[genderStr] ?? '-'.tr; + } + + String _calculateAge(String birthdayStr) { + try { + // 解析生日字符串 (格式: yyyy/MM/dd) + List parts = birthdayStr.trim().split('/'); + if (parts.length != 3) return '-'.tr; + + int year = int.parse(parts[0]); + int month = int.parse(parts[1]); + int day = int.parse(parts[2]); + + DateTime birthDate = DateTime(year, month, day); + DateTime today = DateTime.now(); + + // 计算年龄 + int age = today.year - birthDate.year; + + // 如果今年还没过生日,年龄减1 + if (today.month < birthDate.month || + (today.month == birthDate.month && today.day < birthDate.day)) { + age--; + } + + return age.toString(); + } catch (e) { + return '-'.tr; + } + } + + // 显示解绑确认对话框 + void _showCancelConfirmDialog() { + showConfirmDialog( + context, + Container(), + "是否确认结束?".tr, + onConfirm: () async { + bool opRes = + await deviceTypeController.qcCheckControl(widget.personInfo, 2); + if (!opRes) { + return; + } + deviceTypeController.experience_status.value = 404; + deviceTypeController.updateAll(); + }, + onCancel: () {}, + ); + } + + // 开始状态查询定时器 + void _startCheckStatusTimer() { + _checkStatusTimer?.cancel(); // 取消之前的定时器 + + _checkStatusTimer = Timer.periodic(Duration(seconds: 3), (timer) { + if (mounted) { + edm.EasyDartModule.logger.info("定时查询快检状态"); + deviceTypeController + .checkReportStatus(widget.personInfo['mac']) + .then((_) async { + // 如果状态变回404(非体验中),停止定时器 + progressNotifier.value = + deviceTypeController.experience_percent.value.toDouble(); + deviceTypeController.updateAll(); + if (deviceTypeController.experience_status.value == 201) { + //体验正常结束 + deviceTypeController.experience_status.value = 404; + deviceTypeController.experience_status.value = 0; + progressNotifier.value = 0; + edm.EasyDartModule.logger.info("快检结束,停止定时查询"); + _checkStatusTimer?.cancel(); + await deviceTypeController.getCheckHistory( + id: deviceTypeController.experience_id.value); + Get.toNamed('/healthQuickCheckReportPage', + arguments: deviceTypeController.currentCq.value); + } + if (deviceTypeController.experience_status.value != 200 && + deviceTypeController.experience_status.value != 201 && + deviceTypeController.experience_status.value != 404 && + deviceTypeController.experience_status.value != 203) { + //体验异常结束 + deviceTypeController.experience_status.value = 404; + deviceTypeController.experience_percent.value = 0; + progressNotifier.value = 0; + edm.EasyDartModule.logger.info("快检结束,停止定时查询"); + _checkStatusTimer?.cancel(); + // NewTopSlideNotification.show( + // text: + // "体验异常结束".tr, + // textColor: themeController.currentColor.sc9); + } + }); + } + }); + } +} + +class SpeedControlledGif extends StatefulWidget { + final String assetPath; + final double speedFactor; + final BoxFit? fit; + + /// [speedFactor] 播放速度倍数,默认1.0, + /// 大于1表示加速播放,小于1表示减速播放 + const SpeedControlledGif( + this.assetPath, { + Key? key, + this.speedFactor = 1.0, + this.fit, + }) : super(key: key); + + @override + _SpeedControlledGifState createState() => _SpeedControlledGifState(); +} + +class _SpeedControlledGifState extends State { + ui.Codec? _codec; + ui.FrameInfo? _currentFrame; + Timer? _timer; + bool _isDisposed = false; + + @override + void initState() { + super.initState(); + _loadGif(); + } + + Future _loadGif() async { + final data = await rootBundle.load(widget.assetPath); + _codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + + if (_isDisposed) return; + + _showNextFrame(); + } + + Future _showNextFrame() async { + if (_isDisposed || _codec == null) return; + + _currentFrame = await _codec!.getNextFrame(); + + if (_isDisposed) return; + + if (mounted) { + setState(() {}); + } + + // 取当前帧持续时间,单位毫秒 + final baseDuration = _currentFrame?.duration.inMilliseconds ?? 100; + + // 限制最小帧间隔,防止刷新过快 + const minFrameDuration = 50; + + // 计算实际播放帧间隔,speedFactor越大速度越快 + final adjustedDuration = (baseDuration / widget.speedFactor) + .round() + .clamp(minFrameDuration, 10000); + + _timer = Timer(Duration(milliseconds: adjustedDuration), _showNextFrame); + } + + @override + void dispose() { + _isDisposed = true; + _timer?.cancel(); + _codec?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_currentFrame == null) { + // 加载中或无帧时显示空容器或占位 + return Container(); + } + return RawImage( + image: _currentFrame!.image, + fit: widget.fit, + ); + } +} diff --git a/lib/pages/device/health_experience_history.dart b/lib/pages/device/health_experience_history.dart new file mode 100644 index 0000000..55f173d --- /dev/null +++ b/lib/pages/device/health_experience_history.dart @@ -0,0 +1,141 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/NullDataComponentWidget.dart'; +import 'package:vbvs_app/controller/device/device_type_controller.dart'; +import 'package:vbvs_app/controller/message/message_review_controller.dart'; +import 'package:vbvs_app/pages/device/component/HealthReportCard.dart'; + +class HealthExperienceHistory extends StatefulWidget { + var data; //1.绑定时 2.绑定后 + HealthExperienceHistory({super.key, required this.data}); + + @override + State createState() => + _HealthExperienceHistoryState(); +} + +class _HealthExperienceHistoryState extends State { + MessageReviewController messageReviewController = Get.find(); + DeviceTypeController deviceTypeController = Get.find(); + + @override + void initState() { + super.initState(); + loadData(widget.data); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, bodysize) => GestureDetector( + // onTap: () => FocusScope.of(context).unfocus(),, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(getBackgroundImageNoImage()), // 本地图片 + fit: BoxFit.fill, // 填满整个 Container + ), + ), + 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( + '快检历史'.tr, + style: TextStyle( + fontFamily: 'Readex Pro', + color: themeController.currentColor.sc3, + letterSpacing: 0, + fontSize: 30.rpx, + ), + ), + Positioned( + left: 0, + // child: returnIconButtom, + child: returnIconButtomAddCallback(() {}), + ), + ], + ), + ), + actions: [], + centerTitle: false, + ), + body: SafeArea( + top: true, + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0), + child: Column( + children: [ + Expanded( + child: Obx(() { + final list = deviceTypeController.cqHistoryList.value; + return list.isEmpty + ? const NullDataWidget() + : _buildMessageListView(list); + }), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Future loadData(data) async { + //todo 请求历史体征数据 + // String serviceAddress = ServiceConstant.service_address; + // String serviceName = ServiceConstant.server_service; + // String serviceApi = ServiceConstant.message_list; + // String queryUrl = + // "${serviceAddress}${serviceName}${serviceApi}?type=app_vsm&mac=${data['mac']}"; + // ApiResponse apiResponse = await requestWithLog( + // logTitle: "查询消息回看数据", method: MyHttpMethod.get, queryUrl: queryUrl); + // messageReviewController.messageList.value = apiResponse.data; + // messageReviewController.updateAll(); + deviceTypeController.getCheckHistory(); + } + + Widget _buildMessageListView(List dataList) { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 0.rpx), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 30.rpx), + ...dataList + .map((item) => HealthReportCardWidget(data: item)) + .toList() + .divide(SizedBox(height: 30.rpx)), + SizedBox(height: 30.rpx), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device/instant_body_page.dart b/lib/pages/device/instant_body_page.dart index 11b8d22..720c755 100644 --- a/lib/pages/device/instant_body_page.dart +++ b/lib/pages/device/instant_body_page.dart @@ -659,6 +659,7 @@ class _InstantBodyPageState extends State ], ), ), + SizedBox( height: 40.rpx, ), diff --git a/lib/pages/main_bottom/component/MessageWidgetWidget.dart b/lib/pages/main_bottom/component/MessageWidgetWidget.dart index a51c7d6..80a6ad8 100644 --- a/lib/pages/main_bottom/component/MessageWidgetWidget.dart +++ b/lib/pages/main_bottom/component/MessageWidgetWidget.dart @@ -158,6 +158,7 @@ class _MessageWidgetWidgetState extends State { ), ), ), + ], ); } diff --git a/lib/pages/mh_page/device/controller/mht_bluetooth_controller.dart b/lib/pages/mh_page/device/controller/mht_bluetooth_controller.dart index b1c2469..e03f0e1 100644 --- a/lib/pages/mh_page/device/controller/mht_bluetooth_controller.dart +++ b/lib/pages/mh_page/device/controller/mht_bluetooth_controller.dart @@ -83,6 +83,18 @@ class MHTBlueToothController extends GetControllerEx { RxBool allSelect = false.obs; //升级是否全选 RxBool autoUpgrade = false.obs; //是否自动升级 + // wifi下发指令 + final List> _commandQueue = []; // 指令队列 + bool _isSendingCommand = false; // 是否正在发送指令 + Timer? _commandTimeoutTimer; // 指令超时定时器 + int _currentRetryCount = 0; // 当前重试次数 + Map? _currentCommand; // 当前正在发送的指令 + + // 最大重试次数 + static const int _maxRetryCount = 3; + // 超时时间(秒) + static const int _commandTimeoutSeconds = 10; + void startStatusPolling() { updateDeviceStatus().then((res) { if (res.code == HttpStatusCodes.ok) { @@ -424,6 +436,10 @@ class MHTBlueToothController extends GetControllerEx { if (commandData == null || commandData.isEmpty) { throw "指令数据不能为空"; } + if (commandData['data'] == "FFFFFFFF00031000010014FD") { + return; + } + ef.log("下发指令内容为: ${commandData['data']}"); commandData['data'] = Base64Tool.encode(commandData['data']); if (Base64Tool.decode(commandData['data']) != "FFFFFFFF00031000010014FD" && Base64Tool.decode(commandData['data']).length <= 24) { @@ -432,6 +448,7 @@ class MHTBlueToothController extends GetControllerEx { // ef.log("下发指令: ${Base64Tool.decode(commandData['data'])}"); } ef.log("全部指令"); + ef.log("wifi指令控制下发时间-->${DateTime.now()}"); commandData['wfr'] = false; commandData['type'] = 0; //0 base64数据 1 二进制数据 String serviceAddress = ServiceConstant.service_address; @@ -445,12 +462,15 @@ class MHTBlueToothController extends GetControllerEx { data: commandData, onSuccess: (res) { ef.log("下发指令成功: ${res.msg}"); + EasyDartModule.logger.info("下发wifi控制指令成功: $res"); }, onFailure: (res) { ef.log("下发指令失败: ${res.msg}"); + EasyDartModule.logger.info("下发wifi控制指令失败: $res"); throw "下发wifi控制指令失败: ${res.msg}"; }, ); + await Future.delayed(Duration(milliseconds: 100)); } //todo 解绑的时候删除自己所拥有的所有设备的睡眠习惯 @@ -472,3 +492,641 @@ void safeShowNotification(String msg) { // print("TopSlideNotification 显示异常: $e\n$stack"); } } + +// import 'dart:async'; +// import 'dart:convert'; + +// import 'package:EasyDartModule/EasyDartModule.dart'; +// import 'package:easydevice/src/app/thapp.dart'; +// import 'package:ef/ef.dart'; +// import 'package:flutter/src/widgets/framework.dart'; +// import 'package:json_annotation/json_annotation.dart'; +// import 'package:vbvs_app/common/color/ServiceConstant.dart'; +// import 'package:vbvs_app/common/color/app_uri_status.dart'; +// import 'package:vbvs_app/common/util/DailyLogUtils.dart'; +// import 'package:vbvs_app/common/util/FirmwareVersionService.dart'; +// import 'package:vbvs_app/common/util/MyUtils.dart'; +// import 'package:vbvs_app/common/util/base64Tool.dart'; +// import 'package:vbvs_app/common/util/requestWithLog.dart'; +// import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; +// import 'package:vbvs_app/model/api_response.dart'; +// import 'package:vbvs_app/pages/mh_page/device/model/BlueToothDataModel.dart'; +// import 'package:vbvs_app/pages/mh_page/user/controller/mht_register_controller.dart'; + +// part 'mht_bluetooth_controller.g.dart'; + +// @JsonSerializable() +// class MHTBlueToothModel { +// bool? bluetooth = false; //蓝牙开关 +// double? singal = -100; + +// @JsonKey(ignore: true) +// List? blueRawData = []; //蓝牙原始数据 +// @JsonKey(ignore: true) +// List? deviceDataStatus; //已经请求过状态的数据 + +// int? read = 0; +// int? deviceType; //绑定设备类型 + +// bool wifiPassShow = false; +// String? wifiPass; + +// MHTBlueToothModel(); + +// static MHTBlueToothModel fromJson(Map json) => +// _$MHTBlueToothModelFromJson(json); +// Map toJson() => _$MHTBlueToothModelToJson(this); +// } + +// class MHTBlueToothController extends GetControllerEx { +// MHTBlueToothController() { +// attr = GetModel(MHTBlueToothModel()).obs; +// } + +// Timer? _statusTimer; +// MHTRegisterController registerController = Get.find(); + +// RxString search = "".obs; + +// RxString currentDeviceMac = "".obs; //当前正在绑定的设备,用来显示loading + +// THapp? currentDevice; //当前连接的设备 +// BlueToothDataModel? currentFullDevice; //当前连接的设备 + +// RxInt blueConnectFlag = 0.obs; //当前蓝牙连接状态 0.正在连接 1.未连接 2.已连接 + +// RxBool bluetoothStatus = false.obs; //蓝牙开启状态 +// RxInt connectStatus = 0.obs; //当前wifi连接状态 0:未连接 1:已连接 +// RxInt netType = 0.obs; //当前网络类型 0.正在检测 1.wifi 2.4g设备 3.未知 +// RxInt wifiConnectStatus = 1.obs; //获取wifi状态 0.正在检测 1.已检测完 + +// RxMap selectWifi = {}.obs; //正在连接wifi信息 + +// int returnPage = 0; //0返回首页 1.返回设备列表 + +// RxInt wifiStatus = 0.obs; //wifi连接状态 0:未连接 1:已连接 +// RxList wifiList = [].obs; +// RxMap connect_wifi = {}.obs; +// RxString? cid = "".obs; + +// RxBool isScanning = false.obs; +// RxMap localUpgradeMac = {}.obs; //mac 进度 +// String? currentUpgradeVersion; //最新版本号 +// String? currentUpgradeName; //最新固件名 +// String? currentUpgradeUrl; //最新固件下载地址 +// List firmwareList = []; //固件版本列表 +// RxBool allSelect = false.obs; //升级是否全选 +// RxBool autoUpgrade = false.obs; //是否自动升级 + +// // wifi下发指令队列相关 +// final List> _commandQueue = []; // 指令队列 +// bool _isSendingCommand = false; // 是否正在发送指令 +// Timer? _commandTimeoutTimer; // 指令超时定时器 +// int _currentRetryCount = 0; // 当前重试次数 +// Map? _currentCommand; // 当前正在发送的指令 + +// // 最大重试次数 +// static const int _maxRetryCount = 3; +// // 超时时间(秒) +// static const int _commandTimeoutSeconds = 10; + +// void startStatusPolling() { +// updateDeviceStatus().then((res) { +// if (res.code == HttpStatusCodes.ok) { +// updateAll(); +// } else { +// safeShowNotification(res.msg ?? "获取设备状态异常".tr); +// EasyDartModule.logger.info("获取设备状态异常: $res"); +// DailyLogUtils.writeLog("获取设备状态异常: $res"); +// } +// }); + +// if (_statusTimer == null) { +// _statusTimer = Timer.periodic(Duration(seconds: 2), (timer) { +// updateDeviceStatus().then((res) { +// if (res.code == HttpStatusCodes.ok) { +// updateAll(); +// } else { +// safeShowNotification(res.msg ?? "获取设备状态异常".tr); +// EasyDartModule.logger.info("获取设备状态异常: $res"); +// DailyLogUtils.writeLog("获取设备状态异常: $res"); +// } +// }).catchError((e, stack) { +// print("updateDeviceStatus 执行异常: $e\n$stack"); +// safeShowNotification("设备状态请求失败".tr); +// EasyDartModule.logger.info("设备状态异常: $e"); +// DailyLogUtils.writeLog("设备状态异常: $e"); +// }); +// }); +// } +// } + +// void stopStatusPolling() { +// _statusTimer?.cancel(); +// _statusTimer = null; +// } + +// var shouldScan = true.obs; + +// void pauseScanning() { +// shouldScan.value = false; +// update(); +// } + +// // 恢复扫描 +// void resumeScanning() { +// shouldScan.value = true; +// update(); +// } + +// Future updateDeviceStatus() async { +// try { +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.get_bluetooth_device_status; +// String queryUrl = "$serviceAddress$serviceName$serviceApi"; + +// if (model.blueRawData != null && model.blueRawData!.isNotEmpty) { +// final macParams = model.blueRawData! +// .map((device) => +// "mac=${Uri.encodeQueryComponent(device.mac!.replaceAll(':', ''))}") +// .join("&"); + +// if (queryUrl.contains('?')) { +// queryUrl += '&$macParams'; +// } else { +// queryUrl += '?$macParams'; +// } + +// String? language = ""; +// if (mhLanguageController.selectLanguage != null) { +// language = mhLanguageController.selectLanguage.value!.language_code; +// } +// if (language != null && language.isNotEmpty) { +// if (queryUrl.contains("?")) { +// queryUrl += "&lang=$language"; +// } else { +// queryUrl += "?lang=$language"; +// } +// } + +// var response = await EasyDartModule.dio.get(queryUrl); +// var responseData = +// response.data is String ? jsonDecode(response.data) : response.data; +// ApiResponse res = +// ApiResponse.fromJson(responseData, (object) => object); +// if (res.code != HttpStatusCodes.ok) return res; + +// if (response.data['data'] != null && response.data['data'] is List) { +// List responseList = response.data['data']; + +// // 新建一个 Map,便于快速通过 mac 查找返回的设备状态 +// final Map responseMap = { +// for (var item in responseList) +// item['mac'.tr].toString().toUpperCase(): item +// }; + +// // 遍历 blueRawData,更新 bind 状态 +// for (var device in model.blueRawData!) { +// final macKey = device.mac!.replaceAll(':', '').toUpperCase(); +// if (responseMap.containsKey(macKey)) { +// var item = responseMap[macKey]; +// // 更新 device 绑定状态等信息 +// device.bind = item['bind'] ?? device.bind; +// device.mac = item['bindMac'] ?? device.mac; +// } +// } +// model.deviceDataStatus = +// List.from(model.blueRawData!); +// } else { +// model.deviceDataStatus = []; +// } + +// updateAll(); +// return res; +// } else { +// model.deviceDataStatus = []; +// return ApiResponse(code: 1, msg: "".tr); +// } +// } catch (e) { +// print("获取设备状态异常: $e"); +// EasyDartModule.logger.info("获取设备状态异常: $e"); +// DailyLogUtils.writeLog("获取设备状态异常: $e"); +// return ApiResponse(code: -1, msg: "请求失败".tr); +// } +// return ApiResponse(code: -1, msg: "未知错误".tr); +// } + +// bindDeviceAndMAC(BlueToothDataModel bleDevice, BuildContext context) async { +// try { +// if ((bleDevice.macA == null || bleDevice.macA.isEmpty) && +// (bleDevice.macB == null || bleDevice.macB.isEmpty)) { +// TopSlideNotification.show(context, +// text: "传感器mac读取失败".tr, textColor: themeController.currentColor.sc9); +// currentDeviceMac.value = ""; +// return; +// } +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.device_bind; +// String queryUrl = "$serviceAddress$serviceName$serviceApi"; +// var data = { +// "deviceType": model.deviceType, +// "mac".tr: bleDevice.mac, +// "macA": bleDevice.macA, +// if (bleDevice.macB != null && bleDevice.macB!.isNotEmpty) +// "macB": bleDevice.macB, +// if (bleDevice.name != null && bleDevice.name!.isNotEmpty) +// 'param': { +// 'name': bleDevice.name, +// }, +// }; +// EasyDartModule.logger.info("绑定传感器数据: $data"); +// var response = +// await EasyDartModule.dio.post(queryUrl, data: jsonEncode(data)); +// if (response != null) { +// var responseData = +// response.data is String ? jsonDecode(response.data) : response.data; +// ApiResponse res = +// ApiResponse.fromJson(responseData, (object) => object); +// MyUtils.formatResponse(res, "绑定成功".tr, "绑定成功".tr); +// if (res.code == HttpStatusCodes.ok) { +// // PersonController personController = Get.find(); +// // personController.currentPersonId.value = res.data['id']; +// //todo 绑定成功需要返回传感器id +// currentDeviceMac.value = ""; +// if (res.data != null) { +// if (currentFullDevice != null) { +// currentFullDevice!.macAID = res.data['macA']; +// currentFullDevice!.macBID = res.data['macB']; +// currentFullDevice!.deviceID = res.data['id']; +// } +// } +// return res; +// } else { +// return res; +// } +// } else { +// return ApiResponse(code: -1, msg: "服务器失败".tr); +// } +// } catch (e) { +// EasyDartModule.logger.info("绑定异常: $e"); +// DailyLogUtils.writeLog("蓝牙绑定: $e"); +// } +// return ApiResponse(code: -1, msg: "未知错误".tr); +// } + +// Future saveHabitData(sleepData) async { +// bool resFlag = false; +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.user_setting; +// String type = "sleep_habit_${sleepData['mac']}"; +// String queryUrl = "${serviceAddress}${serviceName}${serviceApi}"; +// var data = { +// "type": type, +// "mac".tr: sleepData['mac'.tr], +// "time": DateTime.now().millisecondsSinceEpoch, +// "data": sleepData, +// }; +// await requestWithLog( +// logTitle: "更新睡眠习惯".tr, +// method: MyHttpMethod.put, +// queryUrl: queryUrl, +// data: data, +// onSuccess: (res) { +// resFlag = true; +// }, +// onFailure: (res) { +// resFlag = false; +// }, +// ); +// return resFlag; +// } + +// Future loadHabitDataApi(String mac, {int time = 3}) async { +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.user_setting; +// String type = "sleep_habit_${mac}"; +// String queryUrl = +// "${serviceAddress}${serviceName}${serviceApi}?type=${type}"; + +// // 使用 Future 来等待异步操作完成 +// Map result = {}; + +// await requestWithLog( +// logTitle: "更新睡眠习惯".tr, +// method: MyHttpMethod.get, +// queryUrl: queryUrl, +// onSuccess: (res) { +// ef.log("加载睡眠习惯成功: ${res.data}"); +// result = res.data; // 将返回的数据存入 result +// }, +// onFailure: (res) { +// ef.log("加载睡眠习惯失败: ${res.msg}"); +// result = {}; // 如果失败,可以返回空的 Map +// }, +// ); + +// return result; // 在 requestWithLog 完成之后返回 result +// } + +// saveJiYiData(sleepData) async { +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.user_setting; +// String type = "sleep_jiyi_${sleepData['mac']}"; +// String queryUrl = "${serviceAddress}${serviceName}${serviceApi}"; +// var data = { +// "type": type, +// "mac": sleepData['mac'], +// "time": DateTime.now().millisecondsSinceEpoch, +// "data": sleepData, +// }; +// await requestWithLog( +// logTitle: "更新睡眠记忆", +// method: MyHttpMethod.put, +// queryUrl: queryUrl, +// data: data, +// ); +// } + +// loadJiYiData(String mac, {int time = 3}) async { +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.user_setting; +// String type = "sleep_jiyi_${mac}"; +// String queryUrl = +// "${serviceAddress}${serviceName}${serviceApi}?type=${type}"; +// // 使用 Future 来等待异步操作完成 +// Map result = {}; +// await requestWithLog( +// logTitle: "更新记忆", +// method: MyHttpMethod.get, +// queryUrl: queryUrl, +// onSuccess: (res) { +// ef.log("加载记忆成功: ${res.data}"); +// result = res.data; // 将返回的数据存入 result +// }, +// onFailure: (res) { +// ef.log("加载记忆失败: ${res.msg}"); +// result = {}; // 如果失败,可以返回空的 Map +// }, +// ); + +// return result; // 在 requestWithLog 完成之后返回 result +// } + +// saveMattressTimeData(sleepData) async { +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.user_setting; +// String type = "sleep_new_time_${sleepData['mac']}"; +// String queryUrl = "${serviceAddress}${serviceName}${serviceApi}"; +// var data = { +// "type": type, +// "mac": sleepData['mac'], +// "time": DateTime.now().millisecondsSinceEpoch, +// "data": sleepData, +// }; +// await requestWithLog( +// logTitle: "更新新版倒计时", +// method: MyHttpMethod.put, +// queryUrl: queryUrl, +// data: data, +// ); +// } + +// loadMattressTimeData(String mac, {int time = 3}) async { +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.user_setting; +// String type = "sleep_new_time_${mac}"; +// String queryUrl = +// "${serviceAddress}${serviceName}${serviceApi}?type=${type}"; + +// // 使用 Future 来等待异步操作完成 +// Map result = {}; + +// await requestWithLog( +// logTitle: "更新新版倒计时", +// method: MyHttpMethod.get, +// queryUrl: queryUrl, +// onSuccess: (res) { +// ef.log("更新新版倒计时成功: ${res.data}"); +// result = res.data; // 将返回的数据存入 result +// }, +// onFailure: (res) { +// ef.log("更新新版倒计时失败: ${res.msg}"); +// result = {}; // 如果失败,可以返回空的 Map +// }, +// ); + +// return result; // 在 requestWithLog 完成之后返回 result +// } + +// // 修改后的下发wifi指令方法(带队列、重试和超时机制) +// void sendCommand(Map commandData) { +// if (commandData == null || commandData.isEmpty) { +// throw "指令数据不能为空"; +// } + +// // 忽略特定的空指令 +// if (commandData['data'] == "FFFFFFFF00031000010014FD") { +// return; +// } + +// // 将指令添加到队列 +// _commandQueue.add(commandData); +// ef.log("指令已加入队列,当前队列长度: ${_commandQueue.length}"); + +// // 如果没有正在发送的指令,开始处理队列 +// if (!_isSendingCommand) { +// _processNextCommand(); +// } +// } + +// // 处理下一条指令 +// void _processNextCommand() { +// if (_commandQueue.isEmpty) { +// _isSendingCommand = false; +// _currentCommand = null; +// return; +// } + +// _isSendingCommand = true; +// _currentRetryCount = 0; +// _currentCommand = _commandQueue.removeAt(0); + +// _sendCommandWithRetry(); +// } + +// // 带重试机制的指令发送 +// void _sendCommandWithRetry() { +// if (_currentCommand == null) return; + +// ef.log( +// "开始发送指令,第 ${_currentRetryCount + 1} 次尝试,指令内容: ${_currentCommand!['data']}"); + +// // 设置超时定时器 +// _commandTimeoutTimer?.cancel(); +// _commandTimeoutTimer = Timer(Duration(seconds: _commandTimeoutSeconds), () { +// _handleCommandTimeout(); +// }); + +// // 复制指令数据,避免修改原始数据 +// Map commandToSend = Map.from(_currentCommand!); + +// // 调用原有的发送逻辑 +// _executeSendCommand(commandToSend); +// } + +// // 执行实际的指令发送 +// void _executeSendCommand(Map commandData) async { +// ef.log("下发指令内容为: ${commandData['data']}"); + +// // Base64编码处理 +// commandData['data'] = Base64Tool.encode(commandData['data']); + +// // 调试日志 +// if (Base64Tool.decode(commandData['data']) != "FFFFFFFF00031000010014FD" && +// Base64Tool.decode(commandData['data']).length <= 24) { +// ef.log("下发指令: ${Base64Tool.decode(commandData['data'])}"); +// } + +// ef.log("全部指令"); +// commandData['wfr'] = false; +// commandData['type'] = 0; //0 base64数据 1 二进制数据 + +// String serviceAddress = ServiceConstant.service_address; +// String serviceName = ServiceConstant.server_service; +// String serviceApi = ServiceConstant.sendWifiCommand; +// String queryUrl = "${serviceAddress}${serviceName}${serviceApi}"; +// ef.log("wifi指令控制下发时间-->${DateTime.now()}"); +// try { +// await requestWithLog( +// logTitle: "下发wifi控制指令", +// method: MyHttpMethod.post, +// queryUrl: queryUrl, +// data: commandData, +// onSuccess: (res) { +// // 取消超时定时器 +// _commandTimeoutTimer?.cancel(); +// _commandTimeoutTimer = null; + +// ef.log("下发指令成功: ${res.msg}"); +// EasyDartModule.logger.info("下发wifi控制指令成功: $res"); + +// // 指令发送成功,但还需要等待设备回复 +// // 注意:需要在收到设备回复时调用 onCommandResponse(true) +// // 如果设备回复通过其他方式通知,则在这里不处理,等待回调 + +// }, +// onFailure: (res) { +// // 取消超时定时器 +// _commandTimeoutTimer?.cancel(); +// _commandTimeoutTimer = null; + +// ef.log("下发指令失败: ${res.msg}"); +// EasyDartModule.logger.info("下发wifi指令控制失败: ${res.msg}"); + +// // 处理失败,进行重试 +// _handleCommandFailure("下发指令失败: ${res.msg}"); +// }, +// ); +// } catch (e) { +// // 捕获未知异常 +// _commandTimeoutTimer?.cancel(); +// _commandTimeoutTimer = null; + +// ef.log("下发指令异常: $e"); +// EasyDartModule.logger.info("下发wifi控制指令异常: $e"); + +// _handleCommandFailure("下发指令异常: $e"); +// } +// } + +// // 处理指令超时 +// void _handleCommandTimeout() { +// ef.log("指令发送超时"); +// EasyDartModule.logger.info("指令发送超时"); + +// _commandTimeoutTimer = null; +// _handleCommandFailure("指令发送超时"); +// } + +// // 处理指令失败(重试逻辑) +// void _handleCommandFailure(String error) { +// _currentRetryCount++; + +// if (_currentRetryCount < _maxRetryCount) { +// // 未达到最大重试次数,继续重试 +// ef.log("指令失败,准备第 ${_currentRetryCount + 1} 次重试,失败原因: $error"); +// _sendCommandWithRetry(); +// } else { +// // 已达到最大重试次数,放弃当前指令,处理下一条 +// ef.log("指令重试已达最大次数,放弃当前指令,失败原因: $error"); +// EasyDartModule.logger.info("指令发送失败,已重试 $_maxRetryCount 次: $error"); + +// // 可以在这里显示错误通知 +// safeShowNotification("指令发送失败: $error"); + +// // 继续处理下一条指令 +// _processNextCommand(); +// } +// } + +// // 设备回复回调(需要在收到设备回复时调用此方法) +// void onCommandResponse(bool success, {String? error}) { +// if (!_isSendingCommand || _currentCommand == null) { +// ef.log("收到设备回复,但当前没有正在发送的指令"); +// return; +// } + +// // 取消超时定时器 +// _commandTimeoutTimer?.cancel(); +// _commandTimeoutTimer = null; + +// if (success) { +// ef.log("设备回复成功,继续发送下一条指令"); +// // 指令成功,重置重试计数,处理下一条 +// _currentRetryCount = 0; +// _processNextCommand(); +// } else { +// ef.log("设备回复失败: $error"); +// // 设备回复失败,按失败处理(进行重试) +// _handleCommandFailure(error ?? "设备回复失败"); +// } +// } + +// // 清空指令队列(可用于页面退出时) +// void clearCommandQueue() { +// _commandQueue.clear(); +// _commandTimeoutTimer?.cancel(); +// _commandTimeoutTimer = null; +// _isSendingCommand = false; +// _currentCommand = null; +// _currentRetryCount = 0; +// ef.log("指令队列已清空"); +// } + +// //todo 解绑的时候删除自己所拥有的所有设备的睡眠习惯 +// } + +// void safeShowNotification(String msg) { +// try { +// final ctx = Get.context; +// if (ctx != null && ctx.mounted) { +// TopSlideNotification.show( +// ctx, +// text: msg, +// textColor: themeController.currentColor.sc9, +// ); +// } else { +// print("TopSlideNotification 未显示:context 不可用或未挂载".tr); +// } +// } catch (e, stack) { +// // print("TopSlideNotification 显示异常: $e\n$stack"); +// } +// } diff --git a/lib/pages/mh_page/test/WebviewTestModel.dart b/lib/pages/mh_page/test/WebviewTestModel.dart index f76ee19..eb88cd4 100644 --- a/lib/pages/mh_page/test/WebviewTestModel.dart +++ b/lib/pages/mh_page/test/WebviewTestModel.dart @@ -494,6 +494,9 @@ class WebviewTestController extends GetControllerEx { return; } if (tmp['data'] != null && tmp['data'] is Map) { + MHTBlueToothController mhtBlueToothController = Get.find(); + ef.log("wifi指令控制回复时间-->${DateTime.now()}"); + // mhtBlueToothController.onCommandResponse(true); var newData = tmp['data']; var mac = newData['mac']; String order = Base64Tool.decode(newData['data']); diff --git a/lib/pages/sleep_report/QcReportWidget.dart b/lib/pages/sleep_report/QcReportWidget.dart new file mode 100644 index 0000000..af27f5d --- /dev/null +++ b/lib/pages/sleep_report/QcReportWidget.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/QcBreatheStandardWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/QcDiseasePercentsWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/QcHeartHealthWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/QcHeartRateStandardWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/QcPiLaoZhiShuPercentWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/QcZiZhuShenJingPercentWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/qc_heart_change.dart'; +import 'package:vbvs_app/pages/sleep_report/qc_report/qc_heart_point.dart'; + +Widget QcReportWidget(Map data) { + List _buildSectionList() { + EdgeInsetsDirectional padding = + EdgeInsetsDirectional.fromSTEB(0.rpx, 0, 0.rpx, 0.rpx); + // Map data = { + // "_id": "69ae3ac13a075b8438083b0a", + // "person": { + // "name": "张三", + // "gender": "男", + // "age": 52, + // "weight": 37.5, + // "height": 165 + // }, + // "score": 80, + // "level": 2, + // "mac": "设备mac", + // "hr": { + // "avg": 89, + // "base": 85, + // "min": 68, + // "max": 96, + // "data": [ + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 69, + // 70, + // 70, + // 70, + // 70, + // 70, + // 88, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 70, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 71, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68, + // 68 + // ] + // }, + // "br": { + // "avg": 12, + // "base": 12, + // "min": 10, + // "max": 15, + // "data": [ + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 16, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 17, + // 20, + // 21, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 15, + // 11 + // ] + // }, + // "hrv": [ + // {"name": "心脏总能量", "val": 45, "range": "-"} + // ], + // "hrs": [ + // {"x": 1005, "y": 800} + // ], + // "xljk": [ + // {"name": "焦虑抑郁", "val": 15, "color": "00C1AA"} + // ], + // "mbzs": [ + // {"name": "心血管", "val": 70, "color": "00C1AA"} + // ], + // "plzs": {"level": 1}, + // "zzsjphzs": {"level": 2}, + // "ai": {}, + // "create_time": 1772690803969 + // }; + + return [ + QcHeartRateStandardWidget(reportData: data), + QcBreatheStandardWidget(reportData: data), + QcHeartChangeWidget(reportData: data), + QcHeartPointWidget(reportData: data), + QcHeartHealthWidget(reportData: data), + QcDiseasePercentsWidget(reportData: data), + QcPiLaoZhiShuPercentWidget(reportData: data), + QcZiZhuShenJingPercentWidget(reportData: data), + ] + .divide(SizedBox(height: 25.rpx)) + .map((widget) => Padding( + padding: padding, + child: SizedBox(width: double.infinity, child: widget), + )) + .toList(); + } + + return Column( + children: _buildSectionList(), + ); +} diff --git a/lib/pages/sleep_report/chart/HorizontalBarChart.dart b/lib/pages/sleep_report/chart/HorizontalBarChart.dart index c5506e3..3da41be 100644 --- a/lib/pages/sleep_report/chart/HorizontalBarChart.dart +++ b/lib/pages/sleep_report/chart/HorizontalBarChart.dart @@ -1,3 +1,307 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_svg/flutter_svg.dart'; +// import 'package:vbvs_app/common/color/appConstants.dart'; +// import 'package:vbvs_app/common/util/FitTool.dart'; +// import 'package:vbvs_app/common/util/MyUtils.dart'; +// import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +// import 'package:vbvs_app/enum/APPPackageType.dart'; +// import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; + +// class HorizontalBarChart extends StatelessWidget { +// final List> showLabel; +// final bool showPercent; + +// const HorizontalBarChart({ +// super.key, +// required this.showLabel, +// this.showPercent = true, +// }); + +// @override +// Widget build(BuildContext context) { +// final data = showLabel.map((item) { +// return BarData( +// label: item['name'], +// value: (item['percent'] ?? 0).toDouble(), +// color: item['color'] ?? Colors.grey, +// explain: item['explain'] ?? '', +// ); +// }).toList(); + +// final double labelWidth = (MediaQuery.of(context).size.width * 0.5).clamp( +// MediaQuery.of(context).size.width * 0.08, +// MediaQuery.of(context).size.width * 0.22); + +// final double barHeight = 24.0.rpx; +// final double barSpacing = 40.0.rpx; +// final totalHeight = data.length * (barHeight + barSpacing) + 30.rpx; + +// return SizedBox( +// height: totalHeight, +// child: Row( +// children: [ +// // 左侧标签列 +// SizedBox( +// width: labelWidth, +// child: ListView.builder( +// physics: const NeverScrollableScrollPhysics(), +// itemCount: data.length, +// itemBuilder: (context, index) { +// final bar = data[index]; +// return Container( +// height: barHeight + barSpacing, +// alignment: Alignment.centerRight, +// child: LabelWithSvg( +// label: bar.label, +// explain: bar.explain, +// ), +// ); +// }, +// ), +// ), +// SizedBox( +// width: 16.rpx, +// ), +// // 右侧柱状图区 +// Expanded( +// child: Stack( +// children: [ +// // 网格线背景层 +// CustomPaint( +// size: Size(double.infinity, totalHeight), +// painter: GridPainter(totalHeight: totalHeight), +// ), + +// // 柱状图列表 +// ListView.builder( +// physics: const NeverScrollableScrollPhysics(), +// itemCount: data.length, +// itemBuilder: (context, index) { +// final bar = data[index]; +// return SizedBox( +// height: barHeight + barSpacing, +// child: CustomPaint( +// painter: SingleBarPainter( +// value: bar.value, +// color: bar.color, +// showPercent: showPercent, +// ), +// ), +// ); +// }, +// ) +// ], +// ), +// ), +// ], +// ), +// ); +// } +// } + +// class BarData { +// final String label; +// final double value; +// final Color color; +// final String explain; + +// BarData({ +// required this.label, +// required this.value, +// required this.color, +// required this.explain, +// }); +// } + +// class LabelWithSvg extends StatelessWidget { +// final String label; +// final String explain; + +// const LabelWithSvg({ +// super.key, +// required this.label, +// required this.explain, +// }); + +// @override +// Widget build(BuildContext context) { +// final textStyle = +// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx); + +// return Row( +// mainAxisAlignment: MainAxisAlignment.end, +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// Flexible( +// child: Text( +// label, +// style: textStyle, +// overflow: TextOverflow.ellipsis, +// ), +// ), +// // SizedBox(width: 8.rpx), +// ClickableContainer( +// backgroundColor: Colors.transparent, +// highlightColor: Colors.white, +// padding: +// EdgeInsetsDirectional.fromSTEB(14.rpx, 14.rpx, 14.rpx, 14.rpx), +// borderRadius: 0.rpx, +// onTap: () { +// // Get.toNamed("/deviceShareListPage", arguments: explain); + +// if (AppConstants().ent_type == APPPackageType.MHT.code) { +// showTipDialog( +// context, +// Container( +// child: Text( +// explain, +// style: TextStyle(fontSize: 26.rpx, color: Colors.black), +// ), +// ), +// backgroundColor: Color(0xFFFFFFFF), +// colors: [ +// Color(0XFF1592AA), +// Color(0xFF0C83A7), +// Color(0xFF006FA3) +// ], +// ); +// } else { +// showTipDialog( +// context, +// Container( +// child: Text( +// explain, +// style: TextStyle( +// fontSize: 26.rpx, +// color: themeController.currentColor.sc3), +// ), +// ), +// backgroundColor: themeController.currentColor.sc17, +// colors: AppConstants().thNormalButton, +// ); +// } +// }, +// child: SizedBox( +// width: 17.rpx, +// height: 17.rpx, +// child: SvgPicture.asset( +// 'assets/img/icon/explain.svg', +// fit: BoxFit.cover, +// color: Colors.white, +// ), +// ), +// ), +// ], +// ); +// } +// } + +// class SingleBarPainter extends CustomPainter { +// final double value; +// final Color color; +// final bool showPercent; +// final double maxValue = 100; + +// SingleBarPainter({ +// required this.value, +// required this.color, +// required this.showPercent, +// }); + +// @override +// void paint(Canvas canvas, Size size) { +// final barHeight = 24.0.rpx; +// final rightPadding = 20.0.rpx; +// final chartWidth = size.width - rightPadding; + +// final left = 0.0; +// final right = left + (value / maxValue) * chartWidth; +// final rect = Rect.fromLTWH( +// left, (size.height - barHeight) / 2, right - left, barHeight); +// final paint = Paint()..color = color; +// canvas.drawRect(rect, paint); + +// if (showPercent) { +// final textStyle = +// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx); +// final textPainter = TextPainter(textDirection: TextDirection.ltr); +// textPainter.text = +// TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle); +// textPainter.layout(); +// canvas.save(); +// canvas.clipRect(Rect.fromLTWH( +// left, (size.height - barHeight) / 2, chartWidth, barHeight)); +// textPainter.paint( +// canvas, +// Offset(right + 4.0.rpx, (size.height - textPainter.height) / 2), +// ); +// canvas.restore(); +// } +// } + +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +// } + +// class GridPainter extends CustomPainter { +// final double totalHeight; +// final int gridCount = 5; +// final double maxValue = 100; +// final double bottomPadding = 30.0.rpx; + +// GridPainter({required this.totalHeight}); + +// @override +// void paint(Canvas canvas, Size size) { +// final Paint gridPaint = Paint() +// ..color = Colors.grey.withOpacity(0.3) +// ..strokeWidth = 1.0.rpx; + +// final double rightPadding = 20.0.rpx; +// final double chartWidth = size.width - rightPadding; +// final double chartHeight = totalHeight - bottomPadding; + +// for (int i = 0; i <= gridCount; i++) { +// double dx = (i / gridCount) * chartWidth; +// _drawDashedLine( +// canvas, Offset(dx, 0), Offset(dx, chartHeight), gridPaint); + +// final percent = (i / gridCount) * maxValue; +// final TextPainter textPainter = TextPainter( +// textDirection: TextDirection.ltr, +// text: TextSpan( +// text: '${percent.toInt()}', +// style: TextStyle(color: Colors.grey, fontSize: 18.rpx), +// ), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, Offset(dx - textPainter.width / 2, chartHeight + 4.0.rpx)); +// } +// } + +// void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) { +// const double dashWidth = 6.0; +// const double dashSpace = 4.0; + +// final double totalHeight = (end.dy - start.dy).abs(); +// double currentY = start.dy; + +// while (currentY < end.dy) { +// final double nextY = currentY + dashWidth; +// canvas.drawLine( +// Offset(start.dx, currentY), +// Offset(start.dx, nextY > end.dy ? end.dy : nextY), +// paint, +// ); +// currentY = nextY + dashSpace; +// } +// } + +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +// } + import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:vbvs_app/common/color/appConstants.dart'; @@ -10,11 +314,13 @@ import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; class HorizontalBarChart extends StatelessWidget { final List> showLabel; final bool showPercent; + final bool showRangeBackground; // 新增参数,控制是否显示区间背景 const HorizontalBarChart({ super.key, required this.showLabel, this.showPercent = true, + this.showRangeBackground = false, // 默认为false }); @override @@ -66,10 +372,13 @@ class HorizontalBarChart extends StatelessWidget { Expanded( child: Stack( children: [ - // 网格线背景层 + // 背景层 - 包含网格线和区间背景 CustomPaint( size: Size(double.infinity, totalHeight), - painter: GridPainter(totalHeight: totalHeight), + painter: GridPainter( + totalHeight: totalHeight, + showRangeBackground: showRangeBackground, + ), ), // 柱状图列表 @@ -240,7 +549,12 @@ class SingleBarPainter extends CustomPainter { } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; + bool shouldRepaint(covariant CustomPainter oldDelegate) { + if (oldDelegate is SingleBarPainter) { + return oldDelegate.value != value || oldDelegate.color != color; + } + return true; + } } class GridPainter extends CustomPainter { @@ -248,19 +562,58 @@ class GridPainter extends CustomPainter { final int gridCount = 5; final double maxValue = 100; final double bottomPadding = 30.0.rpx; + final bool showRangeBackground; // 新增参数 - GridPainter({required this.totalHeight}); + GridPainter({ + required this.totalHeight, + this.showRangeBackground = false, + }); @override void paint(Canvas canvas, Size size) { - final Paint gridPaint = Paint() - ..color = Colors.grey.withOpacity(0.3) - ..strokeWidth = 1.0.rpx; - final double rightPadding = 20.0.rpx; final double chartWidth = size.width - rightPadding; final double chartHeight = totalHeight - bottomPadding; + // 如果需要显示区间背景 + if (showRangeBackground) { + final double segmentWidth = chartWidth / gridCount; + + // 计算0-60对应的网格区间(60对应3格,因为每格20) + // 0-60: 第0-3格(0,20,40,60) + for (int i = 0; i < 3; i++) { + final rect = Rect.fromLTWH( + i * segmentWidth, + 0, + segmentWidth, + chartHeight, + ); + final paint = Paint() + ..color = themeController.currentColor.sc1.withOpacity(0.2) + ..style = PaintingStyle.fill; + canvas.drawRect(rect, paint); + } + + // 60-100: 第3-5格(60,80,100) + for (int i = 3; i < 5; i++) { + final rect = Rect.fromLTWH( + i * segmentWidth, + 0, + segmentWidth, + chartHeight, + ); + final paint = Paint() + ..color = themeController.currentColor.sc9.withOpacity(0.2) + ..style = PaintingStyle.fill; + canvas.drawRect(rect, paint); + } + } + + // 绘制网格线 + final Paint gridPaint = Paint() + ..color = Colors.grey.withOpacity(0.3) + ..strokeWidth = 1.0.rpx; + for (int i = 0; i <= gridCount; i++) { double dx = (i / gridCount) * chartWidth; _drawDashedLine( @@ -299,5 +652,10 @@ class GridPainter extends CustomPainter { } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + bool shouldRepaint(covariant CustomPainter oldDelegate) { + if (oldDelegate is GridPainter) { + return oldDelegate.showRangeBackground != showRangeBackground; + } + return true; + } } diff --git a/lib/pages/sleep_report/chart/QcTimeSeriesChart.dart b/lib/pages/sleep_report/chart/QcTimeSeriesChart.dart new file mode 100644 index 0000000..c222dc4 --- /dev/null +++ b/lib/pages/sleep_report/chart/QcTimeSeriesChart.dart @@ -0,0 +1,977 @@ +// import 'package:ef/ef.dart'; +// import 'package:fl_chart/fl_chart.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_svg/svg.dart'; +// import 'package:vbvs_app/common/color/appConstants.dart'; +// import 'package:vbvs_app/common/util/FitTool.dart'; +// import 'package:vbvs_app/common/util/MyUtils.dart'; + +// class QcTimeSeriesPoint { +// final double value; +// QcTimeSeriesPoint(this.value); +// } + +// class QcTimeSeriesChart extends StatelessWidget { +// final List dataPoints; +// final double yMin; +// final double yMax; +// final int xSegmentCount; +// final double? baseValue; // 基准值,可选 +// final String? baseLabel; // 基准值标签,可选 + +// const QcTimeSeriesChart({ +// Key? key, +// required this.yMin, +// required this.yMax, +// required this.dataPoints, +// this.xSegmentCount = 11, +// this.baseValue, +// this.baseLabel, +// }) : super(key: key); + +// int get _dataPointCount => dataPoints.length; + +// List _generateYAxisTicks() { +// if (yMin >= yMax) { +// return [0, 20, 40, 60, 80, 100]; +// } + +// double step = (yMax - yMin) / 5; +// List ticks = []; + +// for (int i = 0; i <= 5; i++) { +// ticks.add(yMin + (step * i)); +// } + +// return ticks; +// } + +// // 计算所有刻度位置对应的索引 +// List _getTickIndices() { +// List tickIndices = []; + +// // 确保至少显示两个刻度(起点和终点) +// if (_dataPointCount <= 1) { +// return [0]; +// } + +// // 计算合理的刻度间隔 +// double step = _dataPointCount / (xSegmentCount - 1); + +// for (int i = 0; i < xSegmentCount; i++) { +// int index = (i * step).round(); +// // 确保索引在有效范围内 +// index = index.clamp(0, _dataPointCount - 1); + +// // 避免重复的索引 +// if (tickIndices.isEmpty || index != tickIndices.last) { +// tickIndices.add(index); +// } +// } + +// // 确保最后一个点是最后一个数据点 +// if (tickIndices.last != _dataPointCount - 1) { +// tickIndices.add(_dataPointCount - 1); +// } + +// return tickIndices; +// } + +// // 查找最大值和最小值的索引和值 +// Map _findMinMax() { +// if (dataPoints.isEmpty) +// return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0}; + +// int minIndex = -1; +// int maxIndex = -1; +// double minValue = double.infinity; +// double maxValue = -double.infinity; + +// for (int i = 0; i < dataPoints.length; i++) { +// var point = dataPoints[i]; +// if (point.value != -1) { +// // 跳过无效数据 +// if (point.value < minValue) { +// minValue = point.value; +// minIndex = i; +// } +// if (point.value > maxValue) { +// maxValue = point.value; +// maxIndex = i; +// } +// } +// } + +// return { +// 'minIndex': minIndex, +// 'maxIndex': maxIndex, +// 'minValue': minValue, +// 'maxValue': maxValue +// }; +// } + +// @override +// Widget build(BuildContext context) { +// // final yTicks = _generateYAxisTicks(); +// final double xMax = _dataPointCount.toDouble(); +// final double yRange = yMax - yMin; +// final double yInterval = yRange > 0 ? yRange / 5 : 20.0; + +// final tickIndices = _getTickIndices(); +// final minMaxData = _findMinMax(); + +// // 将数据点分割成多个连续段 +// List> lineSegments = []; +// List currentSegment = []; + +// for (int i = 0; i < dataPoints.length; i++) { +// var point = dataPoints[i]; +// if (point.value != -1) { +// currentSegment.add(FlSpot( +// (i + 1).toDouble(), +// point.value, +// )); +// } else if (currentSegment.isNotEmpty) { +// lineSegments.add(currentSegment); +// currentSegment = []; +// } +// } + +// if (currentSegment.isNotEmpty) { +// lineSegments.add(currentSegment); +// } + +// // 创建渐变填充的线图数据 +// List lineBarsData = []; +// for (var segment in lineSegments) { +// if (segment.isEmpty) continue; + +// lineBarsData.add( +// LineChartBarData( +// spots: segment, +// isCurved: false, +// color: themeController.currentColor.sc2, +// barWidth: 2, +// dotData: FlDotData(show: false), +// preventCurveOverShooting: true, +// belowBarData: BarAreaData( +// show: true, +// gradient: LinearGradient( +// begin: Alignment.topCenter, +// end: Alignment.bottomCenter, +// colors: [ +// themeController.currentColor.sc2.withOpacity(0.3), +// themeController.currentColor.sc2.withOpacity(0.1), +// Colors.transparent, +// ], +// stops: const [0.0, 1, 1.0], +// ), +// applyCutOffY: true, +// cutOffY: 0, +// ), +// ), +// ); +// } + +// // 创建刻度点的数据(绿色小球) +// List tickSpots = []; +// for (int index in tickIndices) { +// if (index >= 0 && index < dataPoints.length) { +// var point = dataPoints[index]; +// if (point.value != -1) { +// tickSpots.add(FlSpot( +// (index + 1).toDouble(), +// point.value, +// )); +// } +// } +// } + +// // 准备水平线列表 +// List horizontalLines = [ +// HorizontalLine( +// y: 0, +// color: themeController.currentColor.sc4, +// strokeWidth: 1.rpx, +// ), +// ]; + +// // 如果有基准值,添加基准线 +// if (baseValue != null) { +// horizontalLines.add( +// HorizontalLine( +// y: baseValue!, +// color: themeController.currentColor.sc9, +// strokeWidth: 2.rpx, +// dashArray: [8, 4], // 虚线样式 +// label: HorizontalLineLabel( +// show: true, +// alignment: Alignment.topRight, +// labelResolver: (line) => +// "基准".tr + '${baseValue!.toStringAsFixed(0)}', +// style: TextStyle( +// color: themeController.currentColor.sc9, +// fontSize: 18.rpx, +// fontWeight: FontWeight.w500, +// ), +// ), +// ), +// ); +// } + +// return AspectRatio( +// aspectRatio: 2, +// child: LayoutBuilder( +// builder: (context, constraints) { +// return Stack( +// children: [ +// LineChart( +// LineChartData( +// minX: 1, +// maxX: xMax + 0.5, +// minY: yMin - 2, +// maxY: yMax + 2, +// gridData: FlGridData(show: false), +// extraLinesData: ExtraLinesData( +// horizontalLines: horizontalLines, +// ), +// titlesData: FlTitlesData( +// bottomTitles: AxisTitles( +// sideTitles: SideTitles( +// showTitles: true, +// reservedSize: 30, +// interval: 1, // 让 fl_chart 自动计算合适的间隔 +// getTitlesWidget: (value, meta) { +// // 只显示我们在 tickIndices 中定义的刻度 +// int index = value.round() - 1; +// if (index >= 0 && +// index < _dataPointCount && +// tickIndices.contains(index)) { +// String label; +// if (index == 0) { +// label = '0'; +// } else if (index == _dataPointCount - 1) { +// label = '${_dataPointCount}'; +// } else { +// label = '${index + 1}'; +// } + +// return Padding( +// padding: const EdgeInsets.only(top: 8.0), +// child: Text( +// label, +// style: TextStyle( +// color: themeController.currentColor.sc4, +// fontSize: 14.rpx, +// ), +// ), +// ); +// } + +// return const SizedBox.shrink(); +// }, +// ), +// ), +// leftTitles: AxisTitles( +// sideTitles: SideTitles( +// showTitles: true, +// reservedSize: 60.rpx, +// interval: yInterval, +// getTitlesWidget: (value, meta) { +// return Padding( +// padding: const EdgeInsets.only(right: 8.0), +// child: Text( +// value.toStringAsFixed(0), +// style: TextStyle( +// color: themeController.currentColor.sc4, +// fontSize: 16.rpx, +// ), +// textAlign: TextAlign.right, +// ), +// ); +// }, +// ), +// ), +// rightTitles: +// AxisTitles(sideTitles: SideTitles(showTitles: false)), +// topTitles: +// AxisTitles(sideTitles: SideTitles(showTitles: false)), +// ), +// borderData: FlBorderData( +// show: true, +// border: Border( +// bottom: BorderSide(color: Colors.grey.withOpacity(0.3)), +// left: BorderSide(color: Colors.grey.withOpacity(0.3)), +// right: BorderSide.none, +// top: BorderSide.none, +// ), +// ), +// lineBarsData: [ +// ...lineBarsData, +// // 添加绿色小球的线图数据 +// LineChartBarData( +// spots: tickSpots, +// isCurved: false, +// color: Colors.transparent, +// barWidth: 0, +// dotData: FlDotData( +// show: true, +// getDotPainter: (spot, percent, barData, index) { +// return FlDotCirclePainter( +// radius: 4.rpx, +// color: stringToColor("#5DD8C9"), +// strokeWidth: 1, +// strokeColor: stringToColor("#00C1AA"), +// ); +// }, +// ), +// preventCurveOverShooting: true, +// ), +// ], +// ), +// ), +// // 使用 Positioned 来绘制最大值和最小值的标签 +// if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1) +// _buildMinMaxLabels( +// context, constraints, minMaxData, xMax, yMin, yMax), +// ], +// ); +// }, +// ), +// ); +// } + +// Widget _buildMinMaxLabels( +// BuildContext context, +// BoxConstraints constraints, +// Map minMaxData, +// double xMax, +// double yMin, +// double yMax, +// ) { +// // 获取图表区域的实际绘制区域 +// // fl_chart 默认会有一些内边距,我们需要估算这些内边距 +// double leftPadding = 60.rpx; // 左侧留白,用于Y轴标签 +// double rightPadding = 10.rpx; // 右侧留白 +// double topPadding = 10.rpx; // 顶部留白 +// double bottomPadding = 30.rpx; // 底部留白,用于X轴标签 + +// double chartWidth = constraints.maxWidth - leftPadding - rightPadding; +// double chartHeight = constraints.maxHeight - topPadding - bottomPadding; + +// // X轴范围:1 到 xMax +// // Y轴范围:yMin - 2 到 yMax + 2 +// double xMin = 1; +// double xMaxValue = xMax; +// double yMinValue = yMin - 2; +// double yMaxValue = yMax + 2; + +// double xRange = xMaxValue - xMin; +// double yRange = yMaxValue - yMinValue; + +// List labels = []; + +// // 添加最小值标签 +// if (minMaxData['minIndex'] != -1) { +// double minX = (minMaxData['minIndex'] + 1).toDouble(); +// double minY = minMaxData['minValue']; + +// // 计算在图表中的相对位置 (0-1) +// double relativeX = (minX - xMin) / xRange; +// double relativeY = (minY - yMinValue) / yRange; + +// // 转换为像素位置 +// double left = leftPadding + (relativeX * chartWidth); +// double top = topPadding + ((1 - relativeY) * chartHeight); + +// // 标签尺寸 +// double labelWidth = 38.rpx; +// double labelHeight = 50.rpx; + +// labels.add( +// Positioned( +// left: left - labelWidth / 2, // 水平居中 +// top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距 +// child: SizedBox( +// width: labelWidth, +// height: labelHeight, +// child: Stack( +// alignment: Alignment.center, +// children: [ +// // SVG图片作为背景 +// SvgPicture.asset( +// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径 +// // width: 28.rpx, +// // height: 40.rpx, +// fit: BoxFit.contain, +// color: stringToColor("#d69dd2"), +// ), +// Padding( +// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离 +// child: Text( +// '${minMaxData['minValue'].toStringAsFixed(0)}', +// style: TextStyle( +// color: Colors.white, +// fontSize: AppConstants().smaller_text_fontSize, +// ), +// ), +// ), +// ], +// ), +// ), +// ), +// ); +// } + +// // 添加最大值标签 +// if (minMaxData['maxIndex'] != -1 && +// minMaxData['maxIndex'] != minMaxData['minIndex']) { +// double maxX = (minMaxData['maxIndex'] + 1).toDouble(); +// double maxY = minMaxData['maxValue']; + +// // 计算在图表中的相对位置 (0-1) +// double relativeX = (maxX - xMin) / xRange; +// double relativeY = (maxY - yMinValue) / yRange; + +// // 转换为像素位置 +// double left = leftPadding + (relativeX * chartWidth); +// double top = topPadding + ((1 - relativeY) * chartHeight); + +// // 标签尺寸 +// double labelWidth = 38.rpx; +// double labelHeight = 50.rpx; + +// labels.add( +// Positioned( +// left: left - labelWidth / 2, // 水平居中 +// top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距 +// child: SizedBox( +// width: labelWidth, +// height: labelHeight, +// child: Stack( +// alignment: Alignment.center, +// children: [ +// // SVG图片作为背景 +// SvgPicture.asset( +// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径 +// // width: 28.rpx, +// // height: 40.rpx, +// fit: BoxFit.contain, +// color: stringToColor("#FF9F66"), +// ), +// // 文字覆盖在SVG上 +// // Text( +// // '${minMaxData['maxValue'].toStringAsFixed(0)}', +// // style: TextStyle( +// // color: Colors.white, +// // fontSize: AppConstants().smaller_text_fontSize, +// // // fontWeight: FontWeight.bold, +// // ), +// // ), +// Padding( +// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离 +// child: Text( +// '${minMaxData['maxValue'].toStringAsFixed(0)}', +// style: TextStyle( +// color: Colors.white, +// fontSize: AppConstants().smaller_text_fontSize, +// ), +// ), +// ), +// ], +// ), +// ), +// ), +// ); +// } + +// return Stack( +// children: labels, +// ); +// } +// } + +import 'package:ef/ef.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; + +class QcTimeSeriesPoint { + final double value; + QcTimeSeriesPoint(this.value); +} + +class QcTimeSeriesChart extends StatelessWidget { + final List dataPoints; + final double yMin; + final double yMax; // 注意:这个值在使用时会自动加 padding + final int xSegmentCount; + final double? baseValue; // 基准值,可选 + final String? baseLabel; // 基准值标签,可选 + final double yAxisPadding; // Y轴顶部留白,默认为20 + + const QcTimeSeriesChart({ + Key? key, + required this.yMin, + required this.yMax, + required this.dataPoints, + this.xSegmentCount = 11, + this.baseValue, + this.baseLabel, + this.yAxisPadding = 20.0, // 默认为20 + }) : super(key: key); + + // 添加一个 getter 来获取实际使用的 yMax(自动加 padding) + double get _actualYMax => yMax + yAxisPadding; + + int get _dataPointCount => dataPoints.length; + + List _generateYAxisTicks() { + if (yMin >= _actualYMax) { + return [0, 20, 40, 60, 80, 100]; + } + + double step = (_actualYMax - yMin) / 5; + List ticks = []; + + for (int i = 0; i <= 5; i++) { + ticks.add(yMin + (step * i)); + } + + return ticks; + } + + // 计算所有刻度位置对应的索引 + List _getTickIndices() { + List tickIndices = []; + + // 确保至少显示两个刻度(起点和终点) + if (_dataPointCount <= 1) { + return [0]; + } + + // 计算合理的刻度间隔 + double step = _dataPointCount / (xSegmentCount - 1); + + for (int i = 0; i < xSegmentCount; i++) { + int index = (i * step).round(); + // 确保索引在有效范围内 + index = index.clamp(0, _dataPointCount - 1); + + // 避免重复的索引 + if (tickIndices.isEmpty || index != tickIndices.last) { + tickIndices.add(index); + } + } + + // 确保最后一个点是最后一个数据点 + if (tickIndices.last != _dataPointCount - 1) { + tickIndices.add(_dataPointCount - 1); + } + + return tickIndices; + } + + // 查找最大值和最小值的索引和值 + Map _findMinMax() { + if (dataPoints.isEmpty) + return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0}; + + int minIndex = -1; + int maxIndex = -1; + double minValue = double.infinity; + double maxValue = -double.infinity; + + for (int i = 0; i < dataPoints.length; i++) { + var point = dataPoints[i]; + if (point.value != -1) { + // 跳过无效数据 + if (point.value < minValue) { + minValue = point.value; + minIndex = i; + } + if (point.value > maxValue) { + maxValue = point.value; + maxIndex = i; + } + } + } + + return { + 'minIndex': minIndex, + 'maxIndex': maxIndex, + 'minValue': minValue, + 'maxValue': maxValue + }; + } + + @override + Widget build(BuildContext context) { + // final yTicks = _generateYAxisTicks(); + final double xMax = _dataPointCount.toDouble(); + final double yRange = _actualYMax - yMin; + final double yInterval = yRange > 0 ? yRange / 5 : 20.0; + + final tickIndices = _getTickIndices(); + final minMaxData = _findMinMax(); + + // 将数据点分割成多个连续段 + List> lineSegments = []; + List currentSegment = []; + + for (int i = 0; i < dataPoints.length; i++) { + var point = dataPoints[i]; + if (point.value != -1) { + currentSegment.add(FlSpot( + (i + 1).toDouble(), + point.value, + )); + } else if (currentSegment.isNotEmpty) { + lineSegments.add(currentSegment); + currentSegment = []; + } + } + + if (currentSegment.isNotEmpty) { + lineSegments.add(currentSegment); + } + + // 创建渐变填充的线图数据 + List lineBarsData = []; + for (var segment in lineSegments) { + if (segment.isEmpty) continue; + + lineBarsData.add( + LineChartBarData( + spots: segment, + isCurved: false, + color: themeController.currentColor.sc2, + barWidth: 2, + dotData: FlDotData(show: false), + preventCurveOverShooting: true, + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + themeController.currentColor.sc2.withOpacity(0.3), + themeController.currentColor.sc2.withOpacity(0.1), + Colors.transparent, + ], + stops: const [0.0, 1, 1.0], + ), + applyCutOffY: true, + cutOffY: 0, + ), + ), + ); + } + + // 创建刻度点的数据(绿色小球) + List tickSpots = []; + for (int index in tickIndices) { + if (index >= 0 && index < dataPoints.length) { + var point = dataPoints[index]; + if (point.value != -1) { + tickSpots.add(FlSpot( + (index + 1).toDouble(), + point.value, + )); + } + } + } + + // 准备水平线列表 + List horizontalLines = [ + HorizontalLine( + y: 0, + color: themeController.currentColor.sc4, + strokeWidth: 1.rpx, + ), + ]; + + // 如果有基准值,添加基准线 + if (baseValue != null) { + horizontalLines.add( + HorizontalLine( + y: baseValue!, + color: themeController.currentColor.sc9, + strokeWidth: 2.rpx, + dashArray: [8, 4], // 虚线样式 + label: HorizontalLineLabel( + show: true, + alignment: Alignment.topRight, + labelResolver: (line) => + "基准".tr + '${baseValue!.toStringAsFixed(0)}', + style: TextStyle( + color: themeController.currentColor.sc9, + fontSize: 18.rpx, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } + + return AspectRatio( + aspectRatio: 2, + child: LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + LineChart( + LineChartData( + minX: 1, + maxX: xMax + 0.5, + minY: yMin - 2, + maxY: _actualYMax + 2, + gridData: FlGridData(show: false), + extraLinesData: ExtraLinesData( + horizontalLines: horizontalLines, + ), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 1, // 让 fl_chart 自动计算合适的间隔 + getTitlesWidget: (value, meta) { + // 只显示我们在 tickIndices 中定义的刻度 + int index = value.round() - 1; + if (index >= 0 && + index < _dataPointCount && + tickIndices.contains(index)) { + String label; + if (index == 0) { + label = '0'; + } else if (index == _dataPointCount - 1) { + label = '${_dataPointCount}'; + } else { + label = '${index + 1}'; + } + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + label, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: 14.rpx, + ), + ), + ); + } + + return const SizedBox.shrink(); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 60.rpx, + interval: yInterval, + getTitlesWidget: (value, meta) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + value.toStringAsFixed(0), + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: 16.rpx, + ), + textAlign: TextAlign.right, + ), + ); + }, + ), + ), + rightTitles: + AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: + AxisTitles(sideTitles: SideTitles(showTitles: false)), + ), + borderData: FlBorderData( + show: true, + border: Border( + bottom: BorderSide(color: Colors.grey.withOpacity(0.3)), + left: BorderSide(color: Colors.grey.withOpacity(0.3)), + right: BorderSide.none, + top: BorderSide.none, + ), + ), + lineBarsData: [ + ...lineBarsData, + // 添加绿色小球的线图数据 + LineChartBarData( + spots: tickSpots, + isCurved: false, + color: Colors.transparent, + barWidth: 0, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4.rpx, + color: stringToColor("#5DD8C9"), + strokeWidth: 1, + strokeColor: stringToColor("#00C1AA"), + ); + }, + ), + preventCurveOverShooting: true, + ), + ], + ), + ), + // 使用 Positioned 来绘制最大值和最小值的标签 + if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1) + _buildMinMaxLabels( + context, constraints, minMaxData, xMax, yMin, _actualYMax), + ], + ); + }, + ), + ); + } + + Widget _buildMinMaxLabels( + BuildContext context, + BoxConstraints constraints, + Map minMaxData, + double xMax, + double yMin, + double yMax, + ) { + // 获取图表区域的实际绘制区域 + // fl_chart 默认会有一些内边距,我们需要估算这些内边距 + double leftPadding = 60.rpx; // 左侧留白,用于Y轴标签 + double rightPadding = 10.rpx; // 右侧留白 + double topPadding = 10.rpx; // 顶部留白 + double bottomPadding = 30.rpx; // 底部留白,用于X轴标签 + + double chartWidth = constraints.maxWidth - leftPadding - rightPadding; + double chartHeight = constraints.maxHeight - topPadding - bottomPadding; + + // X轴范围:1 到 xMax + // Y轴范围:yMin - 2 到 yMax + 2 + double xMin = 1; + double xMaxValue = xMax; + double yMinValue = yMin - 2; + double yMaxValue = yMax + 2; + + double xRange = xMaxValue - xMin; + double yRange = yMaxValue - yMinValue; + + List labels = []; + + // 添加最小值标签 + if (minMaxData['minIndex'] != -1) { + double minX = (minMaxData['minIndex'] + 1).toDouble(); + double minY = minMaxData['minValue']; + + // 计算在图表中的相对位置 (0-1) + double relativeX = (minX - xMin) / xRange; + double relativeY = (minY - yMinValue) / yRange; + + // 转换为像素位置 + double left = leftPadding + (relativeX * chartWidth); + double top = topPadding + ((1 - relativeY) * chartHeight); + + // 标签尺寸 + double labelWidth = 38.rpx; + double labelHeight = 50.rpx; + + labels.add( + Positioned( + left: left - labelWidth / 2, // 水平居中 + top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距 + child: SizedBox( + width: labelWidth, + height: labelHeight, + child: Stack( + alignment: Alignment.center, + children: [ + // SVG图片作为背景 + SvgPicture.asset( + 'assets/img/icon/location.svg', + fit: BoxFit.contain, + color: stringToColor("#d69dd2"), + ), + Padding( + padding: EdgeInsets.only(bottom: 12.rpx), + child: Text( + '${minMaxData['minValue'].toStringAsFixed(0)}', + style: TextStyle( + color: Colors.white, + fontSize: AppConstants().smaller_text_fontSize, + ), + ), + ), + ], + ), + ), + ), + ); + } + + // 添加最大值标签 + if (minMaxData['maxIndex'] != -1 && + minMaxData['maxIndex'] != minMaxData['minIndex']) { + double maxX = (minMaxData['maxIndex'] + 1).toDouble(); + double maxY = minMaxData['maxValue']; + + // 计算在图表中的相对位置 (0-1) + double relativeX = (maxX - xMin) / xRange; + double relativeY = (maxY - yMinValue) / yRange; + + // 转换为像素位置 + double left = leftPadding + (relativeX * chartWidth); + double top = topPadding + ((1 - relativeY) * chartHeight); + + // 标签尺寸 + double labelWidth = 38.rpx; + double labelHeight = 50.rpx; + + labels.add( + Positioned( + left: left - labelWidth / 2, // 水平居中 + top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距 + child: SizedBox( + width: labelWidth, + height: labelHeight, + child: Stack( + alignment: Alignment.center, + children: [ + // SVG图片作为背景 + SvgPicture.asset( + 'assets/img/icon/location.svg', + fit: BoxFit.contain, + color: stringToColor("#FF9F66"), + ), + Padding( + padding: EdgeInsets.only(bottom: 12.rpx), + child: Text( + '${minMaxData['maxValue'].toStringAsFixed(0)}', + style: TextStyle( + color: Colors.white, + fontSize: AppConstants().smaller_text_fontSize, + ), + ), + ), + ], + ), + ), + ), + ); + } + + return Stack( + children: labels, + ); + } +} diff --git a/lib/pages/sleep_report/chart/StatusBarWithIndicator.dart b/lib/pages/sleep_report/chart/StatusBarWithIndicator.dart index 4632c38..2b28162 100644 --- a/lib/pages/sleep_report/chart/StatusBarWithIndicator.dart +++ b/lib/pages/sleep_report/chart/StatusBarWithIndicator.dart @@ -1,11 +1,188 @@ +// import 'package:flutter/material.dart'; +// import 'package:vbvs_app/common/color/appConstants.dart'; +// import 'package:vbvs_app/common/util/FitTool.dart'; +// import 'package:vbvs_app/common/util/MyUtils.dart'; + +// class StatusBarWithIndicator extends StatelessWidget { +// final int selectKey; +// final List> showLabel; +// final IconData icon; +// final double gap; // 每段之间的间距 +// final bool showCurrentValue; // 新增参数,控制是否显示当前值 +// final String? currentValueText; // 可选的当前值文字,如果不提供则使用选中的name + +// const StatusBarWithIndicator({ +// super.key, +// required this.selectKey, +// required this.showLabel, +// this.icon = Icons.favorite, +// this.gap = 8.0, // 默认 8.rpx 间距 +// this.showCurrentValue = false, // 默认为false +// this.currentValueText, +// }); + +// @override +// Widget build(BuildContext context) { +// return LayoutBuilder(builder: (context, constraints) { +// final totalWidth = constraints.maxWidth; +// final itemCount = showLabel.length; + +// // 每条线的宽度 = (总宽度 - 总间隔)/ 项数 +// final totalGap = (itemCount - 1) * gap.rpx; +// final itemWidth = (totalWidth - totalGap) / itemCount; + +// // 找到选中项的 index 和对应的数据 +// final selectedIndex = showLabel.indexWhere((e) => e['key'] == selectKey); +// final selectedItem = selectedIndex >= 0 ? showLabel[selectedIndex] : null; + +// final iconLeft = selectedIndex >= 0 +// ? selectedIndex * (itemWidth + gap.rpx) + itemWidth / 2 +// : 0.0; + +// // 确定要显示的当前值文字 +// String displayValue = ''; +// if (showCurrentValue) { +// if (currentValueText != null) { +// displayValue = currentValueText!; +// } else if (selectedItem != null) { +// displayValue = selectedItem['name'] ?? ''; +// } +// } + +// return SizedBox( +// width: double.infinity, +// child: Stack( +// clipBehavior: Clip.none, +// children: [ +// if (selectedIndex >= 0) ...[ +// // 如果显示当前值,在箭头上方添加文字 +// if (showCurrentValue && displayValue.isNotEmpty) +// Positioned( +// left: iconLeft, +// top: -50.rpx, // 调整位置,给文字留出空间 +// child: Transform.translate( +// offset: Offset(-45.rpx, 0), // 图片宽度 45.rpx,居中偏移 +// child: Container( +// // padding: EdgeInsets.symmetric( +// // horizontal: 8.rpx, +// // vertical: 4.rpx, +// // ), +// // decoration: BoxDecoration( +// // color: selectedItem?['color'] ?? Colors.blue, +// // borderRadius: BorderRadius.circular(4.rpx), +// // boxShadow: [ +// // BoxShadow( +// // color: Colors.black.withOpacity(0.1), +// // blurRadius: 4.rpx, +// // offset: Offset(0, 2.rpx), +// // ), +// // ], +// // ), +// child: Text( +// displayValue, +// style: TextStyle( +// fontSize: AppConstants().small_an_text_fontSize, +// color: themeController.currentColor.sc9, +// fontWeight: FontWeight.w500, +// ), +// ), +// ), +// ), +// ), + +// // 箭头图片 +// Positioned( +// left: iconLeft, +// top: showCurrentValue ? -20.rpx : -20.rpx, // 如果有文字,箭头位置稍下移 +// child: Transform.translate( +// offset: Offset(-22.5.rpx, 0), // 图片宽度 45.rpx,居中偏移 +// child: Container( +// width: 45.rpx, +// height: 76.rpx, +// decoration: BoxDecoration( +// image: DecorationImage( +// image: AssetImage('assets/img/tip_arrow.gif'), +// fit: BoxFit.cover, +// ), +// ), +// ), +// ), +// ), +// ], + +// // 条形图和文字标签 +// Padding( +// padding: EdgeInsets.only(top: showCurrentValue ? 70.rpx : 50.rpx), +// child: Column( +// children: [ +// // 条形段(带间距) +// Row( +// children: showLabel.asMap().entries.map((entry) { +// int index = entry.key; +// var item = entry.value; + +// return Container( +// width: itemWidth, +// height: 15.rpx, +// margin: EdgeInsets.only( +// left: index == 0 ? 0 : gap.rpx, +// ), +// decoration: BoxDecoration( +// color: item['color'], +// borderRadius: BorderRadius.circular(0.rpx), +// ), +// ); +// }).toList(), +// ), +// SizedBox(height: 12.rpx), +// // 名称文字 +// Row( +// children: showLabel.asMap().entries.map((entry) { +// int index = entry.key; +// var item = entry.value; + +// return Container( +// width: itemWidth, +// margin: EdgeInsets.only( +// left: index == 0 ? 0 : gap.rpx, +// ), +// alignment: Alignment.center, +// child: Text( +// item['name'], +// style: TextStyle( +// fontSize: 24.rpx, +// color: Colors.white, +// ), +// textAlign: TextAlign.center, +// ), +// ); +// }).toList(), +// ), +// ], +// ), +// ), +// ], +// ), +// ); +// }); +// } +// } + import 'package:flutter/material.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; class StatusBarWithIndicator extends StatelessWidget { final int selectKey; final List> showLabel; final IconData icon; final double gap; // 每段之间的间距 + final bool showCurrentValue; // 控制是否显示当前值 + final String? currentValueText; // 可选的当前值文字,如果不提供则使用选中的name + final bool showRange; // 新增参数,控制是否显示范围 + final String? Function(Map item)? + rangeTextBuilder; // 可选的范围文字构建器 const StatusBarWithIndicator({ super.key, @@ -13,6 +190,10 @@ class StatusBarWithIndicator extends StatelessWidget { required this.showLabel, this.icon = Icons.favorite, this.gap = 8.0, // 默认 8.rpx 间距 + this.showCurrentValue = false, // 默认为false + this.currentValueText, + this.showRange = false, // 默认为false,不显示范围 + this.rangeTextBuilder, // 自定义范围文字构建器 }); @override @@ -25,23 +206,63 @@ class StatusBarWithIndicator extends StatelessWidget { final totalGap = (itemCount - 1) * gap.rpx; final itemWidth = (totalWidth - totalGap) / itemCount; - // 找到选中项的 index + // 找到选中项的 index 和对应的数据 final selectedIndex = showLabel.indexWhere((e) => e['key'] == selectKey); + final selectedItem = selectedIndex >= 0 ? showLabel[selectedIndex] : null; + final iconLeft = selectedIndex >= 0 ? selectedIndex * (itemWidth + gap.rpx) + itemWidth / 2 : 0.0; + // 确定要显示的当前值文字 + String displayValue = ''; + if (showCurrentValue) { + if (currentValueText != null) { + displayValue = currentValueText!; + } else if (selectedItem != null) { + displayValue = selectedItem['name'] ?? ''; + } + } + + // 获取范围文字 + String getRangeText(Map item) { + if (rangeTextBuilder != null) { + return rangeTextBuilder!(item) ?? ''; + } + // 默认返回空字符串,需要外部传入范围数据 + return item['range']?.toString() ?? ''; + } + return SizedBox( width: double.infinity, child: Stack( clipBehavior: Clip.none, children: [ - if (selectedIndex >= 0) + if (selectedIndex >= 0) ...[ + // 如果显示当前值,在箭头上方添加文字 + if (showCurrentValue && displayValue.isNotEmpty) + Positioned( + left: iconLeft, + top: -50.rpx, + child: Transform.translate( + offset: Offset(-45.rpx, 0), + child: Text( + displayValue, + style: TextStyle( + fontSize: AppConstants().small_an_text_fontSize, + color: themeController.currentColor.sc9, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + + // 箭头图片 Positioned( left: iconLeft, - top: -20.rpx, + top: showCurrentValue ? -20.rpx : -20.rpx, child: Transform.translate( - offset: Offset(-22.5.rpx, 0), // 图片宽度 45.rpx,居中偏移 + offset: Offset(-22.5.rpx, 0), child: Container( width: 45.rpx, height: 76.rpx, @@ -54,8 +275,11 @@ class StatusBarWithIndicator extends StatelessWidget { ), ), ), + ], + + // 条形图和文字标签 Padding( - padding: EdgeInsets.only(top: 50.rpx), + padding: EdgeInsets.only(top: showCurrentValue ? 70.rpx : 50.rpx), child: Column( children: [ // 条形段(带间距) @@ -78,25 +302,46 @@ class StatusBarWithIndicator extends StatelessWidget { }).toList(), ), SizedBox(height: 12.rpx), - // 名称文字 + + // 名称文字和范围(每个下面显示对应的范围) Row( + crossAxisAlignment: CrossAxisAlignment.start, children: showLabel.asMap().entries.map((entry) { int index = entry.key; var item = entry.value; - return Container( - width: itemWidth, - margin: EdgeInsets.only( - left: index == 0 ? 0 : gap.rpx, - ), - alignment: Alignment.center, - child: Text( - item['name'], - style: TextStyle( - fontSize: 24.rpx, - color: Colors.white, + return Expanded( + child: Container( + margin: EdgeInsets.only( + left: index == 0 ? 0 : gap.rpx, + ), + child: Column( + children: [ + // 名称文字 + Text( + item['name'], + style: TextStyle( + fontSize: 24.rpx, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + + // 范围文字(如果显示) + if (showRange) + Padding( + padding: EdgeInsets.only(top: 4.rpx), + child: Text( + "(" + getRangeText(item) + ")", + style: TextStyle( + fontSize: 20.rpx, + color: Colors.white.withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + ), + ], ), - textAlign: TextAlign.center, ), ); }).toList(), diff --git a/lib/pages/sleep_report/component/HeartPointWidget.dart b/lib/pages/sleep_report/component/HeartPointWidget.dart index ec7b242..dff4199 100644 --- a/lib/pages/sleep_report/component/HeartPointWidget.dart +++ b/lib/pages/sleep_report/component/HeartPointWidget.dart @@ -1,5 +1,4 @@ -import 'dart:math'; - +import 'package:EasyDartModule/EasyDartModule.dart' as es; import 'package:ef/ef.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; @@ -8,12 +7,10 @@ import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:vbvs_app/common/color/appConstants.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; -import 'package:vbvs_app/component/base/THFlutterFlowDropDown.dart'; import 'package:vbvs_app/component/tool/ClickableContainer.dart'; import 'package:vbvs_app/enum/APPPackageType.dart'; import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; import 'package:vbvs_app/pages/sleep_report/chart/ScatterPlotChart.dart'; -import 'package:EasyDartModule/EasyDartModule.dart' as es; class HeartPointWidget extends StatefulWidget { var sleepReport; diff --git a/lib/pages/sleep_report/qc_report/QcAIAdviceWidget.dart b/lib/pages/sleep_report/qc_report/QcAIAdviceWidget.dart new file mode 100644 index 0000000..f027684 --- /dev/null +++ b/lib/pages/sleep_report/qc_report/QcAIAdviceWidget.dart @@ -0,0 +1,136 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/AdviceComponnetWidget.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; + +class QcAIAdviceWidget extends StatefulWidget { + var sleepReport; + QcAIAdviceWidget({super.key, required this.sleepReport}); + + @override + State createState() => _QcAIAdviceWidgetState(); +} + +class _QcAIAdviceWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.sleepReport == null || + widget.sleepReport['sugges'] == null || + widget.sleepReport['sugges'].isEmpty) { + return Container(); + } + List advices = widget.sleepReport['sugges']; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular( + AppConstants().normal_container_radius), // 你可以按需调整圆角半径 + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "AI分析".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx),// + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + // "AI分析介绍".tr, + "AI分析是指利用人工智能技术对用户的睡眠数据进行自动化处理与规律提取,对用户的异常睡眠数据给到一定的辅助决策或者解决问题。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + }, + child: Container( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部 + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 31.rpx, + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Column( + children: advices.map((advice) { + return AdviceComponnetWidget( + title: advice["q"], + description: advice["s"], + ).paddingOnly(bottom: 0.rpx); // 在每个组件下方添加间隔 + }).toList(), + ), + ) + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("AI分析绘制异常${e}"); + return Container(); + } + } +} diff --git a/lib/pages/sleep_report/qc_report/QcBreatheStandardWidget.dart b/lib/pages/sleep_report/qc_report/QcBreatheStandardWidget.dart new file mode 100644 index 0000000..4c718b6 --- /dev/null +++ b/lib/pages/sleep_report/qc_report/QcBreatheStandardWidget.dart @@ -0,0 +1,355 @@ +import 'package:EasyDartModule/EasyDartModule.dart' as es; +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/QcTimeSeriesChart.dart'; + +class QcBreatheStandardWidget extends StatefulWidget { + var reportData; + QcBreatheStandardWidget({super.key, required this.reportData}); + + @override + State createState() => + _QcBreatheStandardWidgetState(); +} + +class _QcBreatheStandardWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + // 计算y轴的最大最小值 + (double, double) _calculateYMinMax(List dataPoints) { + if (dataPoints.isEmpty) { + return (8.0, 20.0); + } + + // 过滤掉无效数据点(值为-1的) + final validPoints = dataPoints.where((point) => point.value >= 0).toList(); + + if (validPoints.isEmpty) { + return (8.0, 20.0); + } + + // 找出数据中的实际最小值和最大值 + double dataMin = + validPoints.map((point) => point.value).reduce((a, b) => a < b ? a : b); + double dataMax = + validPoints.map((point) => point.value).reduce((a, b) => a > b ? a : b); + + // 计算最小值(向下取整到5的倍数) + double yMin = (dataMin / 5).floor() * 5.0; + // 如果最小值小于0,设为0 + if (yMin < 0) yMin = 0; + + // 计算最大值(向上取整到5的倍数) + double yMax = (dataMax / 5).ceil() * 5.0; + + // 确保至少有10的差值 + if (yMax - yMin < 10) { + yMax = yMin + 10; + } + + return (yMin, yMax); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null || widget.reportData is! Map) { + return Container(); + } + + // 从reportData中获取br数据 + Map brData = widget.reportData['br'] ?? {}; + if (brData.isEmpty) { + return Container(); + } + + // 获取呼吸数据点 + List dataList = brData['data'] ?? []; + List dataPoints = []; + + // 构建数据点(只保留值,不需要时间戳) + for (int i = 0; i < dataList.length; i++) { + dynamic value = dataList[i]; + if (value == null || value == '') { + dataPoints.add(QcTimeSeriesPoint(-1)); + } else { + double y = (value as num).toDouble(); + dataPoints.add(QcTimeSeriesPoint(y)); + } + } + + // 计算动态的y轴范围 + final (yMin, yMax) = _calculateYMinMax(dataPoints); + + // 构建呼吸统计数据 + Map avgBreath = { + 'name': '平均呼吸'.tr, + 'value': brData['avg'].toInt() ?? 0, + 'unit': '次/分', + }; + + Map baseBreath = { + 'name': '基础呼吸', + 'value': brData['base'].toInt() ?? 0, + 'unit': '次/分', + }; + + Map minBreath = { + 'name': '最低呼吸', + 'value': brData['min'].toInt() ?? 0, + 'unit': '次/分', + }; + + Map maxBreath = { + 'name': '最高呼吸', + 'value': brData['max'].toInt() ?? 0, + 'unit': '次/分', + }; + + // 构建正常范围字符串 + String range = ''; + if (baseBreath['value'] != 0) { + int baseValue = baseBreath['value']; + range = '${baseValue - 3}~${baseValue + 3}'; + } else { + range = '12~20'; + } + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: + BorderRadius.circular(AppConstants().normal_container_radius), + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "呼吸数据".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), + borderRadius: 0.rpx, + onTap: () { + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + "呼吸数据是指用户在睡眠过程中呼吸的基本数据,是评估睡眠呼吸质量、筛查睡眠呼吸障碍的核心指标。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "呼吸数据是指用户在睡眠过程中呼吸的基本数据,是评估睡眠呼吸质量、筛查睡眠呼吸障碍的核心指标。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: + EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0.rpx, 0), + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 31.rpx, + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: 14.rpx, + height: 14.rpx, + decoration: BoxDecoration( + color: themeController.currentColor.sc2, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 15.rpx), + Text( + '正常范围 '.tr + range, + style: TextStyle( + fontSize: AppConstants().smaller_text_fontSize, + color: themeController.currentColor.sc3, + ), + ), + ], + ), + Container( + width: double.infinity, + child: QcTimeSeriesChart( + yMin: yMin, + yMax: yMax, + dataPoints: dataPoints, + xSegmentCount: 11, + baseValue: brData['base'].toDouble(), // 传入基准值 + // baseValue: 16, // 传入基准值 + baseLabel: '基准', // 可选的自定义标签 + yAxisPadding: 0, + ), + ), + ].divide(SizedBox( + height: 18.rpx, + )), + ), + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx), + child: Row( + children: [ + _buildBreathItem(avgBreath), + _buildBreathItem(baseBreath), + _buildBreathItem(minBreath), + _buildBreathItem(maxBreath), + ], + ), + ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("呼吸基准监测绘制异常${e}"); + return Container(); + } + } + + Widget _buildBreathItem(Map data) { + return Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.rpx, vertical: 4.rpx), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Text( + "${data['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().normal_text_fontSize, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 4.rpx), + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + fit: FlexFit.loose, + child: Text( + "${data['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants().normal_text_fontSize, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + SizedBox(width: 4.rpx), + Flexible( + fit: FlexFit.loose, + child: Text( + "${data['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().small_text_fontSize, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/sleep_report/qc_report/QcDiseasePercentsWidget.dart b/lib/pages/sleep_report/qc_report/QcDiseasePercentsWidget.dart new file mode 100644 index 0000000..5d9229f --- /dev/null +++ b/lib/pages/sleep_report/qc_report/QcDiseasePercentsWidget.dart @@ -0,0 +1,208 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/HorizontalBarChart.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; + +class QcDiseasePercentsWidget extends StatefulWidget { + var reportData; // 改为更通用的名称 + QcDiseasePercentsWidget({super.key, required this.reportData}); + + @override + State createState() => + _QcDiseasePercentsWidgetState(); +} + +class _QcDiseasePercentsWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null) { + return Container(); + } + + // 从reportData中获取mbzs数据 + List mbzsData = widget.reportData['mbzs'] ?? []; + if (mbzsData.isEmpty) { + return Container(); + } + + var showLabel = convertDiseaseData(mbzsData); + + // 如果没有有效数据,不显示组件 + if (showLabel.isEmpty) { + return Container(); + } + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular( + AppConstants().normal_container_radius), // 你可以按需调整圆角半径 + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "慢性病风险指数".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + // 你的点击逻辑 + + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + // "心理健康评估介绍".tr, + "慢性病风险指数是通过整合个体的生理指标、生活方式等多维度数据,构建的量化评估模型,用于预测用户未来患慢性非传染性疾病(如高血压、糖尿病、冠心病、癌症等)的风险概率。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "慢性病风险指数是通过整合个体的生理指标、生活方式等多维度数据,构建的量化评估模型,用于预测用户未来患慢性非传染性疾病(如高血压、糖尿病、冠心病、癌症等)的风险概率。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部 + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 34.rpx, + ), + Container( + child: HorizontalBarChart( + showLabel: showLabel, + showPercent: true, + showRangeBackground: true), + ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("疾病绘制异常${e}"); + return Container(); + } + } + + List> convertDiseaseData(List data) { + return data.asMap().entries.map>((entry) { + final index = entry.key; + final item = entry.value; + + // 根据名称生成一个简单的ID(如果没有id字段) + String id = 'disease_$index'; + if (item['name'] != null) { + // 根据名称生成一个简单的哈希值作为id + id = '${item['name']}_$index'; + } + + // 根据值的大小决定颜色深浅 + Color barColor = _getColorByValue(item['val'] ?? 0); + + return { + "key": id, + "name": item["name"] ?? '未知指标', + "color": barColor, + "percent": item["val"] ?? 0, // 注意这里从val取值 + "explain": + (item["tips"] != null && (item["tips"] as String).trim().isNotEmpty) + ? item["tips"] + : '${item["val"] ?? 0}%的风险概率', + }; + }).toList(); + } + + // 根据风险值获取对应的颜色 + Color _getColorByValue(int value) { + if (value < 30) { + return stringToColor("#00C1AA"); // 低风险 - 绿色 + } else if (value < 60) { + return stringToColor("#FFB800"); // 中风险 - 黄色 + } else if (value < 80) { + return stringToColor("#FF7159"); // 高风险 - 橙色 + } else { + return stringToColor("#FF4D4F"); // 极高风险 - 红色 + } + } +} diff --git a/lib/pages/sleep_report/qc_report/QcHeartHealthWidget.dart b/lib/pages/sleep_report/qc_report/QcHeartHealthWidget.dart new file mode 100644 index 0000000..9291a35 --- /dev/null +++ b/lib/pages/sleep_report/qc_report/QcHeartHealthWidget.dart @@ -0,0 +1,215 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/FatigueCircleIndicator.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; + +class QcHeartHealthWidget extends StatefulWidget { + var reportData; // 改为更通用的名称 + QcHeartHealthWidget({super.key, required this.reportData}); + + @override + State createState() => _QcHeartHealthWidgetState(); +} + +class _QcHeartHealthWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null) { + return Container(); + } + + // 从reportData中获取xljk数据 + List xljkData = widget.reportData['xljk'] ?? []; + if (xljkData.isEmpty) { + return Container(); + } + + var showLabel = convertMentalHealthData(xljkData); + + // 如果没有有效数据,不显示组件 + if (showLabel.isEmpty) { + return Container(); + } + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular( + AppConstants().normal_container_radius), // 你可以按需调整圆角半径 + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 0.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "心理健康评估".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + "心率健康评估主要通过用户睡眠报告中的时间点、体征数据及HRV数据等信息,来判断其心理健康水平、疲劳程度。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "心率健康评估主要通过用户睡眠报告中的时间点、体征数据及HRV数据等信息,来判断其心理健康水平、疲劳程度。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部 + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 104.rpx, + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (showLabel.length > 0) + Flexible( + flex: 1, + child: FatigueCircleIndicator( + data: showLabel[0], + ), + ), + if (showLabel.length > 1) + Flexible( + flex: 1, + child: FatigueCircleIndicator( + data: showLabel[1], + ), + ), + ].divide(SizedBox( + width: 110.rpx, + )), + ), + ), + SizedBox( + height: 72.rpx, + ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("心理健康绘制异常${e}"); + return Container(); + } + } + + List> convertMentalHealthData(List data) { + return data.map>((item) { + final String? colorStr = item['color']; + final int value = item['val'].toInt() ?? 0; // 注意这里从val取值 + final String explain = + (item['tips'] != null && (item['tips'] as String).trim().isNotEmpty) + ? item['tips'] + : '未知数据'.tr; + + // 根据value值确定level描述 + String level = ''; + if (value <= 30) { + level = '较低'; + } else if (value <= 60) { + level = '正常'; + } else if (value <= 80) { + level = '偏高'; + } else { + level = '严重'; + } + + return { + 'name': item['name'] ?? '未知指标', + 'color': colorStr != null && colorStr.isNotEmpty + ? stringToColor(colorStr) + : stringToColor("#00C1AA"), // 默认颜色 + 'percent': value, + 'explain': level, // 使用计算出的level + "bottomColor": stringToColor("#393D49"), + }; + }).toList(); + } +} diff --git a/lib/pages/sleep_report/qc_report/QcHeartRateStandardWidget.dart b/lib/pages/sleep_report/qc_report/QcHeartRateStandardWidget.dart new file mode 100644 index 0000000..87ed6c4 --- /dev/null +++ b/lib/pages/sleep_report/qc_report/QcHeartRateStandardWidget.dart @@ -0,0 +1,364 @@ +import 'package:EasyDartModule/EasyDartModule.dart' as es; +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/QcTimeSeriesChart.dart'; + +//心率基准 +class QcHeartRateStandardWidget extends StatefulWidget { + var reportData; + QcHeartRateStandardWidget({super.key, required this.reportData}); + + @override + State createState() => + _QcHeartRateStandardWidgetState(); +} + +class _QcHeartRateStandardWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + // 计算y轴的最大最小值 + (double, double) _calculateYMinMax(List dataPoints) { + if (dataPoints.isEmpty) { + return (50.0, 150.0); + } + + // 过滤掉无效数据点(值为-1的) + final validPoints = dataPoints.where((point) => point.value >= 0).toList(); + + if (validPoints.isEmpty) { + return (50.0, 150.0); + } + + // 找出数据中的实际最小值和最大值 + double dataMin = + validPoints.map((point) => point.value).reduce((a, b) => a < b ? a : b); + double dataMax = + validPoints.map((point) => point.value).reduce((a, b) => a > b ? a : b); + + // 计算最小值(向下取整到10的倍数) + double yMin = (dataMin / 10).floor() * 10.0; + // 如果最小值小于0,设为0 + if (yMin < 0) yMin = 0; + + // 计算最大值(向上取整到10的倍数) + double yMax = (dataMax / 10).ceil() * 10.0; + + // 确保至少有20的差值 + if (yMax - yMin < 20) { + yMax = yMin + 20; + } + + return (yMin, yMax); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null || widget.reportData is! Map) { + return Container(); + } + + // 从reportData中获取hr数据 + Map hrData = widget.reportData['hr'] ?? {}; + if (hrData.isEmpty) { + return Container(); + } + + // 获取心率数据点 + List dataList = hrData['data'] ?? []; + List dataPoints = []; + + // 构建数据点(只保留值,不需要时间戳) + for (int i = 0; i < dataList.length; i++) { + dynamic value = dataList[i]; + if (value == null || value == '') { + dataPoints.add(QcTimeSeriesPoint(-1)); + } else { + double y = (value as num).toDouble(); + dataPoints.add(QcTimeSeriesPoint(y)); + } + } + + // 计算动态的y轴范围 + final (yMin, yMax) = _calculateYMinMax(dataPoints); + + // 构建心率统计数据 + Map avgHeartRate = { + 'name': '平均心率'.tr, + 'value': hrData['avg'].toInt() ?? 0, + 'unit': '次/分'.tr, + }; + + Map baseHeartRate = { + 'name': '基准心率'.tr, + 'value': hrData['base'].toInt() ?? 0, + 'unit': '次/分'.tr, + }; + + Map minHeartRate = { + 'name': '最低心率'.tr, + 'value': hrData['min'].toInt() ?? 0, + 'unit': '次/分'.tr, + }; + + Map maxHeartRate = { + 'name': '最高心率'.tr, + 'value': hrData['max'].toInt() ?? 0, + 'unit': '次/分'.tr, + }; + + // 构建正常范围字符串 + String range = ''; + if (baseHeartRate['value'] != 0) { + int baseValue = baseHeartRate['value']; + range = '${baseValue - 10}~${baseValue + 10}'; + } else { + range = '60~100'; + } + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: + BorderRadius.circular(AppConstants().normal_container_radius), + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "心率数据".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), + borderRadius: 0.rpx, + onTap: () { + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + "心率数据是指用户在睡眠过程中基本心率数据,可初步判断睡眠中的心血管负荷及自主神经功能状态,为睡眠健康评估提供重要依据。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "心率数据是指用户在睡眠过程中基本心率数据,可初步判断睡眠中的心血管负荷及自主神经功能状态,为睡眠健康评估提供重要依据。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: + EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0.rpx, 0), + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 31.rpx, + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx), + child: Column( + children: [ + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: 14.rpx, + height: 14.rpx, + decoration: BoxDecoration( + color: themeController.currentColor.sc2, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 15.rpx), + Text( + '正常范围 '.tr + range, + style: TextStyle( + fontSize: AppConstants().smaller_text_fontSize, + color: themeController.currentColor.sc3, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Container( + width: double.infinity, + child: QcTimeSeriesChart( + yMin: yMin, + yMax: yMax, + dataPoints: dataPoints, + xSegmentCount: 11, + baseValue: hrData['base'].toDouble(), // 传入基准值 + // baseValue: 65, // 传入基准值 + baseLabel: '基准', // 可选的自定义标签 + yAxisPadding: 20, + ), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0.rpx, 0.rpx, 0.rpx, 0.rpx), + child: Row( + children: [ + _buildHeartRateItem(avgHeartRate), + _buildHeartRateItem(baseHeartRate), + _buildHeartRateItem(minHeartRate), + _buildHeartRateItem(maxHeartRate), + ], + ), + ), + ].divide(SizedBox( + height: 18.rpx, + )), + ), + ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("心率基准绘制异常${e}"); + return Container(); + } + } + + Widget _buildHeartRateItem(Map data) { + return Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.rpx, vertical: 4.rpx), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Text( + "${data['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().normal_text_fontSize, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 4.rpx), + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + fit: FlexFit.loose, + child: Text( + "${data['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants().normal_text_fontSize, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + SizedBox(width: 6.rpx), + Flexible( + fit: FlexFit.loose, + child: Text( + "${data['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().small_text_fontSize, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/sleep_report/qc_report/QcPiLaoZhiShuPercentWidget.dart b/lib/pages/sleep_report/qc_report/QcPiLaoZhiShuPercentWidget.dart new file mode 100644 index 0000000..683d9ec --- /dev/null +++ b/lib/pages/sleep_report/qc_report/QcPiLaoZhiShuPercentWidget.dart @@ -0,0 +1,238 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/StatusBarWithIndicator.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; + +class QcPiLaoZhiShuPercentWidget extends StatefulWidget { + var reportData; // 改为更通用的名称 + QcPiLaoZhiShuPercentWidget({super.key, required this.reportData}); + + @override + State createState() => + _PiLaoZhiShuPercentWidgetState(); +} + +class _PiLaoZhiShuPercentWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null) { + return Container(); + } + + // 从reportData中获取plzs数据 + Map plzsData = widget.reportData['plzs'] ?? {}; + if (plzsData.isEmpty) { + return Container(); + } + + int level = plzsData['level'] ?? 0; + + // 根据level值确定显示内容和颜色 + String levelText = ''; + Color levelColor = themeController.currentColor.sc3; + + switch (level) { + case 0: + levelText = '正常'; + levelColor = Colors.green; + break; + case 1: + levelText = '轻度疲劳'; + levelColor = Colors.orange; + break; + case 2: + levelText = '中度疲劳'; + levelColor = Colors.red; + break; + case 3: + levelText = '重度疲劳'; + levelColor = Colors.red; + break; + default: + levelText = '未知'; + levelColor = Colors.grey; + } + + // 构建StatusBarWithIndicator需要的数据格式 + List> showLabel = [ + { + 'key': 0, + 'name': '正常', + 'color': Colors.green, + }, + { + 'key': 1, + 'name': '轻度疲劳', + 'color': Colors.orange, + }, + { + 'key': 2, + 'name': '中度疲劳', + 'color': Colors.red, + }, + { + 'key': 3, + 'name': '重度疲劳', + 'color': Colors.red, + }, + ]; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: + BorderRadius.circular(AppConstants().normal_container_radius), + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "疲劳指数".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), + borderRadius: 0.rpx, + onTap: () { + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + "疲劳指数是评估人体疲劳程度的重要指标,反映身体和精神状态的疲劳水平。".tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "疲劳指数是评估人体疲劳程度的重要指标,反映身体和精神状态的疲劳水平。".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: + EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0.rpx, 0), + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 83.rpx, + ), + // 显示level值的文本 + // Padding( + // padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 30.rpx), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Text( + // '当前状态:', + // style: TextStyle( + // fontSize: 30.rpx, + // color: themeController.currentColor.sc4, + // ), + // ), + // Text( + // levelText, + // style: TextStyle( + // fontSize: 36.rpx, + // fontWeight: FontWeight.bold, + // color: levelColor, + // ), + // ), + // ], + // ), + // ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + child: StatusBarWithIndicator( + selectKey: level, // 使用level值作为选中的key + showLabel: showLabel, + currentValueText: "当前属于".tr, + showCurrentValue: true, + ), + ), + SizedBox( + height: 56.rpx, + ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("疲劳指数绘制异常${e}"); + return Container(); + } + } +} diff --git a/lib/pages/sleep_report/qc_report/QcZiZhuShenJingPercentWidget.dart b/lib/pages/sleep_report/qc_report/QcZiZhuShenJingPercentWidget.dart new file mode 100644 index 0000000..b6e8c83 --- /dev/null +++ b/lib/pages/sleep_report/qc_report/QcZiZhuShenJingPercentWidget.dart @@ -0,0 +1,242 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/StatusBarWithIndicator.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; + +class QcZiZhuShenJingPercentWidget extends StatefulWidget { + var reportData; // 改为更通用的名称 + QcZiZhuShenJingPercentWidget({super.key, required this.reportData}); + + @override + State createState() => + _ZiZhuShenJingPercentWidgetState(); +} + +class _ZiZhuShenJingPercentWidgetState + extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null) { + return Container(); + } + + // 从reportData中获取zzsjphzs数据 + Map zzsjphzsData = widget.reportData['zzsjphzs'] ?? {}; + if (zzsjphzsData.isEmpty) { + return Container(); + } + + int level = zzsjphzsData['level'] ?? 0; + + // 根据level值确定显示内容和颜色 + String levelText = ''; + Color levelColor = themeController.currentColor.sc3; + + switch (level) { + case 0: + levelText = '正常'; + levelColor = Colors.green; + break; + case 1: + levelText = '轻度失衡'; + levelColor = Colors.orange; + break; + case 2: + levelText = '中度失衡'; + levelColor = Colors.red; + break; + case 3: + levelText = '重度失衡'; + levelColor = Colors.red; + break; + default: + levelText = '未知'; + levelColor = Colors.grey; + } + + // 构建StatusBarWithIndicator需要的数据格式 + List> showLabel = [ + { + 'key': 0, + 'name': '正常', + 'color': Colors.green, + }, + { + 'key': 1, + 'name': '轻度失衡', + 'color': Colors.orange, + }, + { + 'key': 2, + 'name': '中度失衡', + 'color': Colors.red, + }, + { + 'key': 3, + 'name': '重度失衡', + 'color': Colors.red, + }, + ]; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular( + AppConstants().normal_container_radius), // 你可以按需调整圆角半径 + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "自主神经平衡指数".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + // "心理健康评估介绍".tr, + "自主神经平衡指数 是评估人体自主神经系统(ANS)功能状态的重要指标,主要反映交感神经和副交感神经的活性平衡关系。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "自主神经平衡指数 是评估人体自主神经系统(ANS)功能状态的重要指标,主要反映交感神经和副交感神经的活性平衡关系。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部 + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 83.rpx, + ), + // 显示level值的文本 + // Padding( + // padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 30.rpx), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Text( + // '当前状态:', + // style: TextStyle( + // fontSize: 30.rpx, + // color: themeController.currentColor.sc4, + // ), + // ), + // Text( + // levelText, + // style: TextStyle( + // fontSize: 36.rpx, + // fontWeight: FontWeight.bold, + // color: levelColor, + // ), + // ), + // ], + // ), + // ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + child: StatusBarWithIndicator( + selectKey: level, // 使用level值作为选中的key + showLabel: showLabel, + currentValueText: "当前属于".tr, + showCurrentValue: true, + ), + ), + SizedBox( + height: 56.rpx, + ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("自主神经平衡指数绘制异常${e}"); + return Container(); + } + } +} diff --git a/lib/pages/sleep_report/qc_report/qc_heart_change.dart b/lib/pages/sleep_report/qc_report/qc_heart_change.dart new file mode 100644 index 0000000..71ec5cd --- /dev/null +++ b/lib/pages/sleep_report/qc_report/qc_heart_change.dart @@ -0,0 +1,380 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/DataShowWidget.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; + +class QcHeartChangeWidget extends StatefulWidget { + var reportData; // 改为更通用的名称 + QcHeartChangeWidget({super.key, required this.reportData}); + + @override + State createState() => _QcHeartChangeWidgetState(); +} + +class _QcHeartChangeWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null) { + return Container(); + } + + // 从reportData中获取hrv数据 + List hrvData = widget.reportData['hrv'] ?? []; + if (hrvData.isEmpty) { + return Container(); + } + + List> data = transformHrvData(hrvData); + + // 获取人员姓名用于显示 + String personName = widget.reportData['person']?['name'] ?? '--'; + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular( + AppConstants().normal_container_radius), // 你可以按需调整圆角半径 + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(30.rpx, 29.rpx, 30.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "心率变异性(HRV)".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + // "心率变异性(HRV)介绍".tr, + "心率变异性(HRV)是指心脏每次跳动间隔时间的差异程度,反映自主神经系统(交感神经和副交感神经)对心脏的调节能力,是评估心血管健康和压力状态的重要指标。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "心率变异性(HRV)是指心脏每次跳动间隔时间的差异程度,反映自主神经系统(交感神经和副交感神经)对心脏的调节能力,是评估心血管健康和压力状态的重要指标。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部 + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 31.rpx, + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx), + child: Column( + children: [ + DataShowWidget( + alignment: MainAxisAlignment.center, + widget1: Text( + "名称".tr, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget2: Text( + "测量值".tr, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget3: Text( + "参考范围".tr, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget4: Text( + "趋势".tr, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + ), + Column( + children: data.map((data) { + return DataShowWidget( + alignment: MainAxisAlignment.center, + widget1: Row( + children: [ + Flexible( + child: Text( + '${data['name']}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize, + ), + ), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 14.rpx, 14.rpx, 14.rpx), + borderRadius: 0.rpx, + onTap: () { + // Get.toNamed("/deviceShareListPage", arguments: explain); + if (AppConstants().ent_type == + APPPackageType.MHT) { + showTipDialog( + context, + Container( + child: Text( + '${data['desc']}', + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + '${data['desc']}', + style: TextStyle( + fontSize: 26.rpx, + color: themeController + .currentColor.sc3, + ), + ), + ), + backgroundColor: + themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: SizedBox( + width: 17.rpx, + height: 17.rpx, + child: SvgPicture.asset( + 'assets/img/icon/question.svg', + fit: BoxFit.cover, + color: Colors.white, + ), + ), + ), + ], + ), + widget2: Text( + '${data['value']}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: + getColorByRange(data['value'], data['range']), + // color: themeController.currentColor.sc3, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget3: Text( + '${data['range']}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget4: data['change'] == 0 + ? Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0, 0), + child: Container( + width: 22.rpx, + height: 22.rpx, + decoration: BoxDecoration(), + child: SvgPicture.asset( + 'assets/img/icon/score_up.svg', + // fit: BoxFit.cover, + ), + ), + ) + : data['change'] == 1 + ? Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0, 0), + child: Container( + width: 22.rpx, + height: 22.rpx, + decoration: BoxDecoration(), + child: SvgPicture.asset( + 'assets/img/icon/score_down.svg', + // fit: BoxFit.cover, + ), + ), + ) + : Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0, 0), + child: Container( + width: 22.rpx, + height: 22.rpx, + decoration: BoxDecoration(), + child: SvgPicture.asset( + 'assets/img/icon/score_equal.svg', + // fit: BoxFit.fill, + ), + ), + ), + ).paddingOnly(bottom: 0.rpx); // 在每个组件下方添加间隔 + }).toList(), + ), + ], + ), + ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("心率变化绘制异常${e}"); + return Container(); + } + } + + List> transformHrvData(List originalData) { + return originalData.map((item) { + return { + "name": item["name"] ?? "未知指标".tr, + "value": + (item["val"] is num) ? item["val"].toDouble() : 0.0, // 注意这里从val取值 + "range": item["range"] ?? "0-0", + "change": item["trend"] ?? 0, // 如果没有trend字段,默认为0 + "desc": item["tips"]?.toString().isNotEmpty == true + ? item["tips"].toString() + : "心率变异性(HRV)是指心脏每次跳动间隔时间的差异程度,反映自主神经系统对心脏的调节能力。".tr + }; + }).toList(); + } + + getColorByRange(double value, String? range) { + try { + // 1. 空、"-"、null 都直接返回默认颜色 + if (range == null || range.toString().isEmpty || range == "-") { + return themeController.currentColor.sc3; + } + // 2. 拆分范围,例如 "70-150" + final parts = range.split('-'); + if (parts.length != 2) { + return themeController.currentColor.sc3; // 格式不对直接默认 + } + + final min = int.parse(parts[0].trim()); + final max = int.parse(parts[1].trim()); + + // 3. 判断是否在范围内 + if (value < min || value > max) { + // 不在范围 → 返回 danger 色 + return themeController.currentColor.sc9; + } + + // 在范围 → 正常色 + return themeController.currentColor.sc3; + } catch (e) { + // 任意解析错误都回默认 + return themeController.currentColor.sc3; + } + } +} diff --git a/lib/pages/sleep_report/qc_report/qc_heart_point.dart b/lib/pages/sleep_report/qc_report/qc_heart_point.dart new file mode 100644 index 0000000..ecf94e7 --- /dev/null +++ b/lib/pages/sleep_report/qc_report/qc_heart_point.dart @@ -0,0 +1,323 @@ +import 'package:EasyDartModule/EasyDartModule.dart' as es; +import 'package:ef/ef.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/enum/APPPackageType.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/ScatterPlotChart.dart'; + +class QcHeartPointWidget extends StatefulWidget { + var reportData; // 改为更通用的名称 + QcHeartPointWidget({super.key, required this.reportData}); + + @override + State createState() => _QcHeartPointWidgetState(); +} + +class _QcHeartPointWidgetState extends State { + @override + void setState(VoidCallback callback) { + super.setState(callback); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + try { + if (widget.reportData == null) { + return Container(); + } + + // 从reportData中获取hrs数据 + List hrsData = widget.reportData['hrs'] ?? []; + if (hrsData.isEmpty) { + return Container(); + } + + double maxX = 0; + double maxY = 0; + + List data = []; + + try { + data = hrsData.map((item) { + double x = (item['x'] ?? 0).toDouble(); // 注意这里从x取值 + double y = (item['y'] ?? 0).toDouble(); // 注意这里从y取值 + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + return ScatterSpot(x, y); + }).toList(); + } catch (e) { + print(e); + } + + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular( + AppConstants().normal_container_radius), // 你可以按需调整圆角半径 + ), + child: Padding( + padding: + EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "心率散点图".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 10.rpx, 14.rpx, 10.rpx), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + if (AppConstants().ent_type == + APPPackageType.MHT.code) { + showTipDialog( + context, + Container( + child: Text( + // "心率散点图介绍".tr, + "心电散点图是用非线性的图形方法描记的连续心冲击图的RR间期图,因图形由散点组成,又称散点图。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: Colors.black, + ), + ), + ), + backgroundColor: Color(0xFFFFFFFF), + colors: [ + Color(0XFF1592AA), + Color(0xFF0C83A7), + Color(0xFF006FA3) + ], + ); + } else { + showTipDialog( + context, + Container( + child: Text( + "心电散点图是用非线性的图形方法描记的连续心冲击图的RR间期图,因图形由散点组成,又称散点图。" + .tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + backgroundColor: themeController.currentColor.sc17, + colors: AppConstants().thNormalButton, + ); + } + }, + child: Container( + padding: EdgeInsetsDirectional.fromSTEB( + 0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部 + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc4, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 31.rpx, + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Container( + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.width * 0.7, + constraints: BoxConstraints( + minWidth: 430.rpx, + minHeight: 430.rpx, + ), + child: ScatterPlotChart( + points: data, + // 根据实际数据动态设置最大最小值 + xMax: maxX > 0 ? maxX.toInt() + 100 : 3000, + yMax: maxY > 0 ? maxY.toInt() + 100 : 3000, + xMin: 0, + yMin: 0, + pointColor: stringToColor("#00C1AA"), // 点的颜色 + // divisions: 7, // 刻度分割数量 + ), + ), + ), + SizedBox( + height: 31.rpx, + ), + // Row( + // children: [ + // Text( + // "图形参考".tr, + // style: TextStyle( + // color: themeController.currentColor.sc3, + // fontSize: AppConstants().middler_text_fontSize), + // ), + // ], + // ), + // SizedBox( + // height: 31.rpx, + // ), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // // 第 1 个 + // SizedBox( + // width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4, + // child: Column( + // children: [ + // Image.asset( + // "assets/img/heartPic1.png", + // width: 120.rpx, + // height: 120.rpx, + // ), + // SizedBox(height: 10), + // SizedBox( + // height: 60.rpx, // 👈 固定说明文字高度 + // child: Text( + // '正常心率窦性图'.tr, + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.center, + // style: TextStyle( + // fontSize: AppConstants().small_text_fontSize, + // color: themeController.currentColor.sc3, + // ), + // ), + // ), + // ], + // ), + // ), + + // // 第 2 个 + // SizedBox( + // width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4, + // child: Column( + // children: [ + // Image.asset( + // "assets/img/heartPic2.png", + // width: 120.rpx, + // height: 120.rpx, + // ), + // SizedBox(height: 10), + // SizedBox( + // height: 60.rpx, + // child: Text( + // '窦性心律不齐图'.tr, + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.center, + // style: TextStyle( + // fontSize: AppConstants().small_text_fontSize, + // color: themeController.currentColor.sc3, + // ), + // ), + // ), + // ], + // ), + // ), + + // // 第 3 个 + // SizedBox( + // width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4, + // child: Column( + // children: [ + // Image.asset( + // "assets/img/heartPic3.png", + // width: 120.rpx, + // height: 120.rpx, + // ), + // SizedBox(height: 10), + // SizedBox( + // height: 60.rpx, + // child: Text( + // '持续性房颤图'.tr, + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.center, + // style: TextStyle( + // fontSize: AppConstants().small_text_fontSize, + // color: themeController.currentColor.sc3, + // ), + // ), + // ), + // ], + // ), + // ), + + // // 第 4 个 + // SizedBox( + // width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4, + // child: Column( + // children: [ + // Image.asset( + // "assets/img/heartPic4.png", + // width: 120.rpx, + // height: 120.rpx, + // ), + // SizedBox(height: 10), + // SizedBox( + // height: 60.rpx, + // child: Text( + // '阵法性房颤图'.tr, + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.center, + // style: TextStyle( + // fontSize: AppConstants().small_text_fontSize, + // color: themeController.currentColor.sc3, + // ), + // ), + // ), + // ], + // ), + // ), + // ].divide(SizedBox(width: 20.rpx)), + // ), + ], + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("心率点绘制异常${e}"); + return Container(); + } + } +} diff --git a/lib/pages/sleep_report/quick_health_report_page.dart b/lib/pages/sleep_report/quick_health_report_page.dart new file mode 100644 index 0000000..c04ddd1 --- /dev/null +++ b/lib/pages/sleep_report/quick_health_report_page.dart @@ -0,0 +1,527 @@ +import 'package:ef/ef.dart'; +import 'package:flutter/material.dart'; +import 'package:flutterflow_ui/flutterflow_ui.dart'; +import 'package:vbvs_app/common/color/ServiceConstant.dart'; +import 'package:vbvs_app/common/color/appConstants.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/common/util/requestWithLog.dart'; +import 'package:vbvs_app/component/NullDataComponentWidget.dart'; +import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; +import 'package:vbvs_app/controller/date/CalendarController.dart'; +import 'package:vbvs_app/controller/sleep/sleep_report_controller.dart'; +import 'package:vbvs_app/controller/sleep/sleep_time_convert.dart'; +import 'package:vbvs_app/pages/sleep_report/QcReportWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/StatusBarWithIndicator.dart'; + +class QuickHealthReportPage extends StatefulWidget { + var data; + QuickHealthReportPage({super.key, required this.data}) {} + + @override + State createState() => _QuickHealthReportPageState(); +} + +class _QuickHealthReportPageState extends State { + late SleepReportController sleepReportController; + CalendarController calendarController = Get.find(); + _QuickHealthReportPageState() {} + final GlobalKey sleepCardKey = GlobalKey(); + final GlobalKey heartRateCardKey = GlobalKey(); + final GlobalKey breatheCardKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + final RxBool isRightLimit = false.obs; + + @override + void didUpdateWidget(QuickHealthReportPage oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + super.dispose(); + Future.microtask(() { + if (Get.isRegistered()) { + sleepReportController.isLoading.value = false; + sleepReportController.model.type = 1; + } + }); + } + + @override + void initState() { + super.initState(); + + if (!Get.isRegistered(tag: widget.data["tag"])) { + Get.put(tag: widget.data["tag"], SleepReportController()); + } + sleepReportController = Get.find(tag: widget.data["tag"]); + // sleepReportController.sleepReport.value = {}; + + if (widget.data['date'] == null) { + widget.data['date'] = DateTime.now().millisecondsSinceEpoch; + } + } + + @override + Widget build(BuildContext context) { + // Future.microtask(() { + // _initSleepReportData(); + // }); + final healthLevelInfo = getHealthLevelInfo(widget.data); + + double lineWidth = 150.rpx; + return LayoutBuilder( + builder: (context, bodySize) => GestureDetector( + // onTap: () => FocusScope.of(context).unfocus(),, + child: Container( + decoration: BoxDecoration( + image: (widget.data['noBackImg'] != null && + widget.data['noBackImg'] == true) + ? null // ✅ 不要背景图 + : DecorationImage( + image: (widget.data['backgroundImg'] != null && + widget.data['backgroundImg'].toString().isNotEmpty) + ? AssetImage(widget.data['backgroundImg']) + : AssetImage(getBackgroundImageNoImage()) + as ImageProvider, + fit: BoxFit.fill, + ), + ), + child: Scaffold( + backgroundColor: Colors.transparent, // 背景透明 + appBar: (widget.data['arrow'] != null && + widget.data['arrow'] == false) + ? null + : AppBar( + backgroundColor: widget.data['backgroundColor'] != null + ? widget.data['backgroundColor'].withOpacity(0.8) + : themeController.currentColor.sc5, + 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( + '快检报告'.tr, + style: TextStyle( + fontFamily: 'Readex Pro', + color: themeController.currentColor.sc3, + letterSpacing: 0, + fontSize: 30.rpx, + ), + ), + + /// 左边返回按钮 + if (widget.data['arrow'] == null || + widget.data['arrow'] == true) + Positioned( + left: 0, + child: returnIconButtomNew(), + ), + ], + ), + ), + ), + body: SafeArea( + top: true, + child: Obx(() { + try { + var sleepReport = sleepReportController.sleepReport.value; + if (sleepReport == null || sleepReport.isEmpty) { + return SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB( + 30.rpx, 30.rpx, 30.rpx, 26.rpx), + child: Container( + width: double.infinity, + constraints: BoxConstraints( + minHeight: 90.rpx, // 最小高度 + ), + decoration: BoxDecoration( + // color: themeController.currentColor.sc5, // 背景色 + ), + child: Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0.rpx, 0.rpx, 0.rpx, 0.rpx), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧图标 + Container( + margin: EdgeInsets.only(right: 7.rpx), + child: Icon( + Icons.info_outline, + size: 26.rpx, + color: themeController.currentColor.sc8, + ), + ), + + // 中间可换行文字 + Expanded( + child: Container( + margin: EdgeInsets.only(right: 69.rpx), + child: Text( + '5分钟快速检测可能受到用户当前状态、过程中说话动作、精神紧张等情况影响,数据仅供参考。' + .tr, + style: TextStyle( + fontFamily: 'Inter', + fontSize: AppConstants() + .smaller_text_fontSize, + color: themeController + .currentColor.sc8, + height: 1.4, + ), + softWrap: true, + ), + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(30.rpx, 0, 30.rpx, 0), + child: Container( + decoration: BoxDecoration( + color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular(16.rpx), + ), + child: Column( + children: [ + // 第一个容器 - 包含Row布局 + Container( + width: double.infinity, + padding: EdgeInsets.all(30.rpx), + decoration: BoxDecoration( + // color: themeController.currentColor.sc5, + borderRadius: + BorderRadius.circular(16.rpx), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + // 左边的Column,包含五行文字 + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + '${widget.data['person']['name']} (${widget.data['person']['gender']} ${widget.data['person']['age'].toInt()}岁)', + style: TextStyle( + fontSize: AppConstants() + .bigger_text_fontSize, + color: themeController + .currentColor.sc3, + // fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 18.rpx), + Text( + "快检得分:".tr + + '${widget.data['score']['score'].toInt()} 分', + style: TextStyle( + fontSize: AppConstants() + .small_an_text_fontSize, + color: themeController + .currentColor.sc3, + ), + ), + SizedBox(height: 18.rpx), + Row( + children: [ + Text( + "健康等级:".tr, + style: TextStyle( + fontSize: AppConstants() + .small_an_text_fontSize, + color: themeController + .currentColor.sc3, + ), + ), + Text( + healthLevelInfo['text'], + style: TextStyle( + fontSize: AppConstants() + .small_an_text_fontSize, + color: healthLevelInfo[ + 'color'], + ), + ), + ], + ), + SizedBox(height: 18.rpx), + Text( + "设备ID:".tr + + '${widget.data['mac']}', + style: TextStyle( + fontSize: AppConstants() + .small_an_text_fontSize, + color: themeController + .currentColor.sc3, + ), + ), + SizedBox(height: 18.rpx), + Text( + "快检时间:".tr + + '${MyUtils.timestampToDateString(widget.data['create_time'].toInt())}', + style: TextStyle( + fontSize: AppConstants() + .small_an_text_fontSize, + color: themeController + .currentColor.sc3, + ), + ), + ], + ), + ), + // SizedBox(width: 20.rpx), // 左边文字和右边图片的间距 + + // 右边的本地图片 + Container( + width: 120.rpx, + height: 190.rpx, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(12.rpx), + image: DecorationImage( + image: AssetImage( + 'assets/img/black_body_still.png'), // 替换为您的本地图片路径 + fit: BoxFit.cover, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 34.rpx), // 两个容器之间的间距 + + // 第二个容器 - 包含StatusBarWithIndicator todo接入报告数据 + Container( + width: double.infinity, + // height: 220.rpx, + decoration: BoxDecoration( + // color: themeController.currentColor.sc5, + borderRadius: + BorderRadius.circular(16.rpx), + ), + child: Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 20.rpx, 30.rpx, 20.rpx), + child: StatusBarWithIndicator( + selectKey: widget.data['score']['level'] + .toInt(), // 这里传入对应的 level 值 + showLabel: (widget.data['type']['score'] + as List) + .map>((item) { + return { + 'key': item['level']?.toInt() ?? + 0, // 使用 level 作为 key + 'name': item['name'] ?? '', + 'color': stringToColor( + item['color'] ?? + ''), // 将颜色字符串转换为 Color + 'range': + item['range'] ?? '', // 添加范围数据 + }; + }).toList(), + showCurrentValue: true, // 显示当前值 + currentValueText: + '当前属于'.tr, // 或者根据实际值动态生成 + showRange: true, // 显示范围 + ), + ), + ), + + SizedBox(height: 25.rpx), // 两个容器之间的间距 + ], + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(30.rpx, 0, 30.rpx, 0), + child: Container( + width: double.infinity, + // padding: EdgeInsets.all(30.rpx), + decoration: BoxDecoration( + // color: themeController.currentColor.sc5, + borderRadius: BorderRadius.circular(16.rpx), + ), + child: QcReportWidget(widget.data), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(46.rpx, 0, 46.rpx, 0), + child: Column( + children: [ + // Text( + // "MAC号".tr + + // ": ${widget.data['mac'] ?? '未知数据'.tr}", + // style: TextStyle( + // color: themeController.currentColor.sc4, + // fontSize: + // AppConstants().smaller_text_fontSize, + // ), + // ), + SizedBox( + height: 4.rpx, + ), + Text( + "注:本页报告是基于心率、呼吸等体征波形数据,通过AI算法模型分析完成,其结果仅供参考;其中报告未见数据异常部分,并不代表没有潜在性的疾病风险,如有不适请及时就医。" + .tr, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: + AppConstants().smaller_text_fontSize, + ), + ), + ], + ), + ), + SizedBox( + height: 40.rpx, + ), + ] + .divide(SizedBox( + height: 25.rpx, + )) + .addToEnd(SizedBox(height: 25.rpx)), + ), + ); + } else { + return Container(); + } + } catch (e, s) { + ef.log('ERROR:$e$s'); + } + return NullDataWidget(); + }), + ), + ), + ), + ), + ); + } + + Future loadSleepReport(int type) async { + if (sleepReportController.isLoading.value) return; + sleepReportController.model.type = type; + sleepReportController.isLoading.value = true; + String data = MyUtils.formatDate(calendarController.selectedDate.value!); + + String serviceAddress = ServiceConstant.service_address; + String serviceName = ServiceConstant.server_service; + String serviceApi = ServiceConstant.sleep_report; + String queryUrl = + "$serviceAddress$serviceName$serviceApi?mac=${widget.data['mac']}&time=$data&type=$type&sleepType=2"; + await requestWithLog( + logTitle: "查询睡眠报告", + method: MyHttpMethod.get, + // queryUrl: + // "https://sleepdata.he-info.com/api/analysis/sleep/analysis?mac=${widget.data['mac']}&time=$data&type=$type", + queryUrl: queryUrl, + onSuccess: (res) { + final sleepData = Map.from(res.data); + // 处理时区转换 + applyTimezoneConversion(sleepData); + sleepReportController.sleepReport.value = sleepData; + }, + onFailure: (res) { + TopSlideNotification.show( + context, + text: res.msg!, + textColor: themeController.currentColor.sc9, + ); + sleepReportController.sleepReport.value = {}; + }, + ); + sleepReportController.isLoading.value = false; + sleepReportController.updateAll(); + } + +// 获取健康等级信息的方法 + Map getHealthLevelInfo(Map data) { + // 默认值 + String defaultText = '未知'; + Color defaultColor = themeController.currentColor.sc3; + + try { + // 获取score的level + double? level; + if (data['score'] is Map) { + level = data['score']['level']?.toDouble(); + } else if (data['score'] is num) { + level = data['score'].toDouble(); + } + + if (level == null) { + return {'text': defaultText, 'color': defaultColor}; + } + + // 获取type中的score等级配置 + var scoreTypes = data['type']?['score']; + if (scoreTypes == null || scoreTypes is! List) { + return {'text': defaultText, 'color': defaultColor}; + } + + // 查找匹配的等级 + for (var type in scoreTypes) { + if (type['level']?.toDouble() == level) { + String name = type['name'] ?? defaultText; + String colorStr = type['color'] ?? ''; + + // 转换颜色字符串为Color对象 + Color textColor = stringToColor(colorStr); + + return {'text': '$name', 'color': textColor}; + } + } + } catch (e) { + print('获取健康等级信息出错:$e'); + } + + return {'text': defaultText, 'color': defaultColor}; + } + +// 辅助方法:将颜色字符串转换为Color对象 + Color _getColorFromString(String colorStr, Color defaultColor) { + if (colorStr.isEmpty) return defaultColor; + + try { + // 处理不同格式的颜色字符串 + String hexColor = colorStr.toUpperCase().replaceAll('#', ''); + + // 如果是6位颜色代码 + if (hexColor.length == 6) { + return Color(int.parse('FF$hexColor', radix: 16)); + } + // 如果是8位颜色代码(包含透明度) + else if (hexColor.length == 8) { + return Color(int.parse(hexColor, radix: 16)); + } + // 如果是3位颜色代码(简写) + else if (hexColor.length == 3) { + String r = hexColor[0] * 2; + String g = hexColor[1] * 2; + String b = hexColor[2] * 2; + return Color(int.parse('FF$r$g$b', radix: 16)); + } + } catch (e) { + print('颜色转换出错:$e'); + } + + return defaultColor; + } +} diff --git a/lib/pages/user/setting_page.dart b/lib/pages/user/setting_page.dart index 378ac3c..298db3e 100644 --- a/lib/pages/user/setting_page.dart +++ b/lib/pages/user/setting_page.dart @@ -613,6 +613,7 @@ class _SettingPageState extends State { ), ), ), + ), Padding( padding: EdgeInsetsDirectional.fromSTEB( diff --git a/lib/routers/routers.dart b/lib/routers/routers.dart index 886f71a..37ff831 100644 --- a/lib/routers/routers.dart +++ b/lib/routers/routers.dart @@ -7,6 +7,8 @@ import 'package:vbvs_app/pages/device/BodyDeviceWidget.dart'; import 'package:vbvs_app/pages/device/component/MessageSetting.dart'; import 'package:vbvs_app/pages/device/component/SingleMessageSetting.dart'; import 'package:vbvs_app/pages/device/device_detail.dart'; +import 'package:vbvs_app/pages/device/health_experience.dart'; +import 'package:vbvs_app/pages/device/health_experience_history.dart'; import 'package:vbvs_app/pages/device/instant_body_page.dart'; import 'package:vbvs_app/pages/device/message_review_page.dart'; import 'package:vbvs_app/pages/device_bind/MobileScannerTestPage.dart'; @@ -48,6 +50,7 @@ import 'package:vbvs_app/pages/setting/Theme_setting.dart'; import 'package:vbvs_app/pages/setting/common_mesaage_setting.dart'; import 'package:vbvs_app/pages/setting/language_setting.dart'; import 'package:vbvs_app/pages/sleep_report/new_sleep_report_page.dart'; +import 'package:vbvs_app/pages/sleep_report/quick_health_report_page.dart'; import 'package:vbvs_app/pages/sleep_report/sleep_report_page.dart'; import 'package:vbvs_app/pages/user/about_us_page.dart'; import 'package:vbvs_app/pages/user/privacy_scheme_page.dart'; @@ -131,8 +134,7 @@ var routes = { "/updateUserTelPage": (contxt) => UpdateUserTelPage(), "/updateUserEmailPage": (contxt) => UpdateUserEmailPage(), "/singleMessageSetting": (context, {arguments}) => - SingleMessageSetting( - data: arguments), + SingleMessageSetting(data: arguments), // "/thShareDeviceDetailWidget": (context, {arguments}) => // THShareDeviceDetailWidget( // data: arguments), @@ -140,6 +142,12 @@ var routes = { THShareDeviceDetailWidget( data: arguments['data'], // 从arguments中获取data device: arguments['device']), // 从arguments中获取device + "/healthCheckPage": (contxt, {arguments}) => + HealthCheckPage(personInfo: arguments), + "/healthExperienceHistory": (contxt, {arguments}) => + HealthExperienceHistory(data: arguments), + "/healthQuickCheckReportPage": (contxt, {arguments}) => + QuickHealthReportPage(data: arguments), }; var onGenerateRoute = (RouteSettings settings) {