From 7a816922fa10dde2cf70946374014fb193c9fb96 Mon Sep 17 00:00:00 2001 From: wyf <494641114@qq.com> Date: Thu, 29 May 2025 20:20:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=9D=A1=E7=9C=A0=E6=8A=A5?= =?UTF-8?q?=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/img/icon/triangle.svg | 1 + assets/img/xiaoe.png | Bin 0 -> 60230 bytes assets/langs/en_US.json | 4 +- assets/langs/zh_CN.json | 5 +- .../home_page/DynamicReportDetailWidget.dart | 5 +- .../home_page/SleepDataModuleWidget.dart | 387 +++++--- .../device/blueteeth_bind_controller.dart | 3 +- .../weather/weather_controller.dart | 3 + lib/main.dart | 4 +- .../component/DeviceDataComponentWidget.dart | 45 +- lib/pages/device/instant_body_page.dart | 4 + .../device_bind/blueteeth_device_page.dart | 49 +- .../device_bind/componnet/bind_dialog.dart | 3 +- lib/pages/device_bind/wifi_page.dart | 79 +- lib/pages/main_bottom/home_page.dart | 13 +- lib/pages/main_bottom/mine_page.dart | 2 +- .../chart/FatigueCircleIndicator.dart | 8 +- .../sleep_report/chart/LineChartByRange.dart | 614 ++++++++++-- lib/pages/sleep_report/chart/SnoreChart.dart | 199 ++++ .../sleep_report/chart/SnoreWaveform.dart | 145 ++- .../component/AIAdviceWidget.dart | 155 +-- .../component/BreathPauseWidget.dart | 178 ++-- .../sleep_report/component/BreatheCard.dart | 149 ++- .../component/BreathePauseNewWidget.dart | 194 ++-- .../component/BreatheStandardWidget.dart | 615 ++++++------ .../component/CompareSleepWidget.dart | 26 +- .../component/DiseasePercentsWidget.dart | 55 +- .../component/HeartChangeWidget.dart | 513 +++++----- .../component/HeartHealthWidget.dart | 177 ++-- .../component/HeartPointWidget.dart | 214 ++-- .../sleep_report/component/HeartRateCard.dart | 152 ++- .../component/HeartRateStandardWidget.dart | 631 ++++++------ .../sleep_report/component/HrvWidget.dart | 151 +-- .../component/SkinPercentWidget.dart | 9 +- .../sleep_report/component/SleepCard.dart | 155 ++- .../component/SleepScoreWidget.dart | 16 +- .../sleep_report/component/SleepView.dart | 527 +++++----- .../component/SnoreViewWidget.dart | 317 ++++-- .../component/ZiZhuShenJingPercentWidget.dart | 189 ++-- .../new_sleep_report_page copy.dart | 937 ++++++++++++++++++ .../sleep_report/new_sleep_report_page.dart | 65 +- 41 files changed, 4604 insertions(+), 2394 deletions(-) create mode 100644 assets/img/icon/triangle.svg create mode 100644 assets/img/xiaoe.png create mode 100644 lib/pages/sleep_report/chart/SnoreChart.dart create mode 100644 lib/pages/sleep_report/new_sleep_report_page copy.dart diff --git a/assets/img/icon/triangle.svg b/assets/img/icon/triangle.svg new file mode 100644 index 0000000..bfd6690 --- /dev/null +++ b/assets/img/icon/triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/img/xiaoe.png b/assets/img/xiaoe.png new file mode 100644 index 0000000000000000000000000000000000000000..42541427dcf6382eb7898f47e1f7933c5e495ff4 GIT binary patch literal 60230 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

L+v#az>Sv>#!QWqqUVzpJt=c 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 literal 0 HcmV?d00001 diff --git a/assets/langs/en_US.json b/assets/langs/en_US.json index 43ac062..4399497 100644 --- a/assets/langs/en_US.json +++ b/assets/langs/en_US.json @@ -321,7 +321,7 @@ "ios启用网络提示":"", "设备报修":"Device repair", "联系人":"Contact", - "手机号":"Phone number", + "手机号":"Phone", "名称输入提示":"Enter contact name", "手机号输入提示":"Enter contact phone number", "提交":"Submit", @@ -398,7 +398,7 @@ "全部消息":"All messages", "请先在设置里的消息通知打开全部消息配置":"Please enable all message notifications in settings first", "请先打开全部消息配置":"Please enable all message notifications first", - "正常值":"Normal range:", + "正常值":"range:", "今日":"Today", "深色":"Dark", "皮肤指数":"Skin index", diff --git a/assets/langs/zh_CN.json b/assets/langs/zh_CN.json index 5d6ea38..4a86f2f 100644 --- a/assets/langs/zh_CN.json +++ b/assets/langs/zh_CN.json @@ -421,7 +421,8 @@ "心率散点图介绍":"心电散点图是用非线性的图形方法描记的连续心冲击图的RR间期图,因图形由散点组成,又称散点图。", "今日数据":"今日数据", "昨日数据":"昨日数据", - "次":"次", - "秒":"秒" + "次":"频次", + "秒":"秒", + "当前暂无数据":"当前暂无数据" } \ No newline at end of file diff --git a/lib/component/home_page/DynamicReportDetailWidget.dart b/lib/component/home_page/DynamicReportDetailWidget.dart index 54336c8..4cb025f 100644 --- a/lib/component/home_page/DynamicReportDetailWidget.dart +++ b/lib/component/home_page/DynamicReportDetailWidget.dart @@ -9,6 +9,7 @@ import 'package:vbvs_app/common/util/MyUtils.dart'; import 'package:vbvs_app/component/home_page/SleepDataModuleWidget.dart'; import 'package:vbvs_app/component/home_page/SleepDateWidget.dart'; import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; class DynamicReportDetailWidget extends StatefulWidget { @@ -38,7 +39,7 @@ class _DynamicReportDetailWidgetState extends State { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - Future.delayed(Duration(milliseconds: 100), () { + Future.delayed(Duration(milliseconds: 500), () { if (!_hasScrolled && _scrollController.hasClients && _scrollController.position.maxScrollExtent > 0) { @@ -121,6 +122,8 @@ class _DynamicReportDetailWidgetState extends State { String sleepReportUrl = "${ServiceConstant.sleep_report_url}?mac=$mac&token=${ServiceConstant.sleep_token}&date=$time"; Get.toNamed("/sleepReportPage", arguments: sleepReportUrl); + } else { + TopSlideNotification.show(context,text: "当前暂无数据".tr,textColor: themeController.currentColor.sc9); } }, child: Row( diff --git a/lib/component/home_page/SleepDataModuleWidget.dart b/lib/component/home_page/SleepDataModuleWidget.dart index e3a0505..5414058 100644 --- a/lib/component/home_page/SleepDataModuleWidget.dart +++ b/lib/component/home_page/SleepDataModuleWidget.dart @@ -79,156 +79,181 @@ class _SleepDataModuleWidgetState extends State { showTipDialog( backgroundColor: stringToColor("#FFFFFF"), context, - Column( - children: [ - Text( - "${widget.data['name']}", - style: TextStyle( - color: stringToColor("#333333"), - fontSize: 36.rpx, - ), - ), - SizedBox( - height: 17.rpx, - ), - Text( - (widget.data['tips']?.toString().trim().isNotEmpty ?? false) - ? widget.data['tips'].toString() - : "未知数据".tr, - style: TextStyle( - color: stringToColor("#C8CBD2"), - fontSize: 26.rpx, - ), - ), - SizedBox( - height: 37.rpx, - ), - Text( - "${widget.data['value']}", - style: TextStyle( - color: stringToColor("${widget.data['color']}"), - fontSize: 60.rpx, - ), - ), - SizedBox( - height: 81.rpx, - ), - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (int i = 0; i < levelGroups.length; i++) ...[ - // 每个 levelGroup 区域 - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Level 名称 - Text( - levelGroups[i]['levelName'], - style: TextStyle( - fontSize: 30.rpx, - color: stringToColor("#333333"), - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 38.rpx), + Container( + constraints: BoxConstraints( + maxHeight: 700.rpx, + ), + child: SingleChildScrollView( + child: Column( + children: [ + Text( + "${widget.data['name']}", + style: TextStyle( + color: stringToColor("#333333"), + fontSize: 36.rpx, + ), + ), + SizedBox( + height: 17.rpx, + ), + Text( + (widget.data['tips']?.toString().trim().isNotEmpty ?? + false) + ? widget.data['tips'].toString() + : "未知数据".tr, + style: TextStyle( + color: stringToColor("#C8CBD2"), + fontSize: 26.rpx, + ), + ), + SizedBox( + height: 37.rpx, + ), + Text( + "${widget.data['value']}" + + ((widget.data['unit'] == null || + widget.data['unit'].toString().isEmpty) + ? '' + : widget.data['unit']), + style: TextStyle( + color: stringToColor("${widget.data['color']}"), + fontSize: 60.rpx, + ), + ), + SizedBox( + height: 81.rpx, + ), + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (int i = 0; i < levelGroups.length; i++) ...[ + // 每个 levelGroup 区域 + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Level 名称 + Text( + levelGroups[i]['levelName'], + style: TextStyle( + fontSize: 30.rpx, + color: stringToColor("#333333"), + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 38.rpx), - // 颜色圆点 + key(包一层,和 svg 分离) - Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - crossAxisAlignment: - CrossAxisAlignment.start, // 上对齐,避免撑高分割线 - children: - levelGroups[i]['items'].map((item) { - final bool isSelected = - (item['key'] == itemLevel); - return Column( - children: [ - // 颜色圆点 + key(参与分割线高度) - Column( + // 颜色圆点 + key(包一层,和 svg 分离) + Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + crossAxisAlignment: + CrossAxisAlignment.start, // 上对齐,避免撑高分割线 + children: levelGroups[i]['items'] + .map((item) { + final bool isSelected = + (item['key'] == itemLevel); + return Column( children: [ - Container( - width: 20.rpx, - height: 20.rpx, - decoration: BoxDecoration( - color: - stringToColor(item['color']), - shape: BoxShape.circle, - ), - ), - SizedBox(height: 30.rpx), - Text( - item['key'], - style: TextStyle( - color: stringToColor("#333333"), - fontSize: 20.rpx, - ), + // 颜色圆点 + key(参与分割线高度) + Column( + children: [ + Container( + width: 20.rpx, + height: 20.rpx, + decoration: BoxDecoration( + color: stringToColor( + item['color']), + shape: BoxShape.circle, + ), + ), + SizedBox(height: 30.rpx), + Text( + item['key'], + style: TextStyle( + color: + stringToColor("#333333"), + fontSize: 20.rpx, + ), + ), + ], ), + + // svg 箭头(不影响分割线高度) + SizedBox(height: 20.rpx), + isSelected + ? SvgPicture.asset( + 'assets/img/icon/triangle.svg', + width: 18.rpx, + height: 18.rpx, + color: themeController + .currentColor.sc9, + ) + : SizedBox(height: 18.rpx), ], - ), - - // svg 箭头(不影响分割线高度) - SizedBox(height: 20.rpx), - isSelected - ? SvgPicture.asset( - 'assets/img/icon/triangle.svg', - width: 18.rpx, - height: 18.rpx, - color: themeController - .currentColor.sc9, - ) - : SizedBox(height: 18.rpx), - ], - ); - }).toList(), + ); + }).toList(), + ), + ], ), - ], - ), - ), + ), - // 分割线(只和主要内容等高) - if (i != levelGroups.length - 1) - Container( - width: 1.rpx, - color: stringToColor("${widget.data['color']}"), - margin: EdgeInsets.symmetric(horizontal: 10.rpx), + // 分割线(只和主要内容等高) + if (i != levelGroups.length - 1) + Container( + width: 1.rpx, + color: Colors.grey.withOpacity(0.5), + margin: + EdgeInsets.symmetric(horizontal: 10.rpx), + ), + ] + ], + ), + ), + SizedBox( + height: 71.rpx, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "当前属于".tr, // 第一部分文本 + style: TextStyle( + color: Colors.black, // 你想要的样式 + fontSize: 30.rpx, + ), ), - ] - ], - ), - ), - SizedBox( - height: 71.rpx, - ), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: "当前属于".tr, // 第一部分文本 - style: TextStyle( - color: Colors.black, // 你想要的样式 - fontSize: 30.rpx, - ), + TextSpan( + text: itemLevel, // 第二部分文本 + style: TextStyle( + color: stringToColor("${widget.data['color']}"), + fontSize: 30.rpx, + ), + ), + ], ), - TextSpan( - text: itemLevel, // 第二部分文本 - style: TextStyle( - color: stringToColor("${widget.data['color']}"), - fontSize: 30.rpx, - ), - ), - ], - ), + ), + ], ), - ], + ), ), ); } + if (widget.data['onto'] != null && widget.data['onto'] == true) { + //跳转睡眠报告 + Get.toNamed("/newSleepReportPage", arguments: { + 'date': widget.data['time'] != null + ? int.parse(widget.data['time'].toString()) + : DateTime.now().millisecondsSinceEpoch, + "mac": 'aaaaaaeeeeeq', + 'type': 1, + 'name': 'sleep', //'sleep', 'heartRate' 或 'breathe' + 'itemName': widget.data['id'], + }); + } }, child: Container( - // width: MediaQuery.sizeOf(context).width * 0.267, width: MediaQuery.sizeOf(context).width * 0.267, constraints: BoxConstraints( minWidth: 200.rpx, @@ -251,40 +276,80 @@ class _SleepDataModuleWidgetState extends State { overflow: TextOverflow.ellipsis, ), Row( - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${widget.data['value']}', - style: FlutterFlowTheme.of(context).bodyMedium.override( + // Expanded( + // child: Row( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.end, + // children: [ + // Text( + // '${widget.data['value']}', + // style: FlutterFlowTheme.of(context).bodyMedium.override( + // fontFamily: 'Inter', + // fontSize: 36.rpx, + // letterSpacing: 0.0, + // color: themeController.currentColor.sc3, + // ), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // Padding( + // padding: + // EdgeInsetsDirectional.fromSTEB(0, 0, 0, 10.rpx), + // child: Text( + // '${widget.data['unit'] ?? ''}', + // style: FlutterFlowTheme.of(context) + // .bodyMedium + // .override( + // fontFamily: 'Inter', + // fontSize: AppConstants().small_text_fontSize, + // letterSpacing: 0.0, + // color: themeController.currentColor.sc3, + // ), + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ], + // ), + // ), + Expanded( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${widget.data['value']}', + style: TextStyle( fontFamily: 'Inter', fontSize: 36.rpx, letterSpacing: 0.0, color: themeController.currentColor.sc3, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + ), + WidgetSpan(child: SizedBox(width: 2.rpx)), // 可选间距 + TextSpan( + text: widget.data['unit'] != null + ? '${widget.data['unit']}' + : '', + style: TextStyle( + fontFamily: 'Inter', + fontSize: AppConstants().small_text_fontSize, + letterSpacing: 0.0, + color: themeController.currentColor.sc3, + ), + ), + ], ), - Padding( - padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 10.rpx), - child: Text( - '${widget.data['unit'] ?? ''}', - style: FlutterFlowTheme.of(context).bodyMedium.override( - fontFamily: 'Inter', - fontSize: AppConstants().small_text_fontSize, - letterSpacing: 0.0, - color: themeController.currentColor.sc3, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + maxLines: 1, + style: TextStyle( + color: + themeController.currentColor.sc3), // 强制 ellipsis 颜色 + overflow: TextOverflow.ellipsis, + ), ), + if (widget.data['level'] != null) ClickableContainer( backgroundColor: (widget.data['color'] == null || diff --git a/lib/controller/device/blueteeth_bind_controller.dart b/lib/controller/device/blueteeth_bind_controller.dart index f7886ec..4b27a5f 100644 --- a/lib/controller/device/blueteeth_bind_controller.dart +++ b/lib/controller/device/blueteeth_bind_controller.dart @@ -71,7 +71,8 @@ class BlueteethBindController extends GetControllerEx { RxString search = "".obs; //搜索关键字 - RxInt connectStatus = 0.obs; + RxInt connectStatus = 0.obs;//当前wifi连接状态 0:未连接 1:已连接 + RxInt blueConnectFlag = 0.obs;//当前蓝牙连接状态 0.正在连接 1.为连接 2.已连接 RxMap selectWifi = {}.obs; //正在连接wifi信息 diff --git a/lib/controller/weather/weather_controller.dart b/lib/controller/weather/weather_controller.dart index b5d91e3..6fa1397 100644 --- a/lib/controller/weather/weather_controller.dart +++ b/lib/controller/weather/weather_controller.dart @@ -256,6 +256,9 @@ class WeatherModelController extends GetControllerEx { Future _getCurrentLocation() async { try { Position position = await _determinePosition(); + if (position == null) { + throw Exception("获取位置失败"); + } String? language = "zh_CN"; if (languageController.selectLanguage != null) { diff --git a/lib/main.dart b/lib/main.dart index 43503b0..a2be13c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -103,13 +103,13 @@ void startMessagePolling() { // 避免重复启动 _messageTimer?.cancel(); - _messageTimer = Timer.periodic(Duration(seconds: 5), (timer) async { + _messageTimer = Timer.periodic(Duration(seconds: 10), (timer) async { try { MessageController messageController = Get.find(); messageController.getMessageStatus(); // print("轮询消息状态成功"); } catch (e) { - print("轮询消息状态失败: $e"); + print("轮询消息状态失败: $e"); } }); } diff --git a/lib/pages/device/component/DeviceDataComponentWidget.dart b/lib/pages/device/component/DeviceDataComponentWidget.dart index ce61e34..1109440 100644 --- a/lib/pages/device/component/DeviceDataComponentWidget.dart +++ b/lib/pages/device/component/DeviceDataComponentWidget.dart @@ -1,9 +1,9 @@ import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' as ui; -import 'package:easydevice/easydevice.dart'; import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:vbvs_app/common/color/ServiceConstant.dart'; @@ -15,18 +15,12 @@ import 'package:vbvs_app/component/tool/ClickableContainer.dart'; import 'package:vbvs_app/component/tool/CustomCard.dart'; import 'package:vbvs_app/component/tool/ToggleColorContainer.dart'; import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; -import 'package:vbvs_app/component/tool/cmd.dart'; import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart'; import 'package:vbvs_app/controller/device/body_device_controller.dart'; import 'package:vbvs_app/controller/person/person_controller.dart'; -import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; import 'package:vbvs_app/enum/BindType.dart'; -import 'package:vbvs_app/model/BleDeviceData.dart'; import 'package:vbvs_app/model/api_response.dart'; -import 'package:vbvs_app/pages/device_bind/blueteeth_device_page.dart'; import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; -import 'dart:math' as math; -import 'dart:ui' as ui; class DeviceDataComponentWidget extends StatefulWidget { final Map device; @@ -1040,7 +1034,7 @@ class _DeviceDataComponentWidgetState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Expanded( + Expanded( child: CustomCard( borderRadius: AppConstants().button_container_radius, onTap: () async { @@ -1052,11 +1046,17 @@ class _DeviceDataComponentWidgetState extends State { personController.gender.value = widget.device['person']['gender'] ?? 1; personController.weight?.value = - widget.device['person']['weight'].toString() ?? - ""; + widget.device['person']['weight'] == null + ? '' + : widget.device['person']['weight'] + .toString(); + personController.height.value = - widget.device['person']['height'].toString() ?? - ""; + widget.device['person']['height'] == null + ? '' + : widget.device['person']['height'] + .toString(); + personController.selectedDiseaseIds.value = widget.device['person']['disease'] ?? []; personController.birthday.value = @@ -1165,7 +1165,8 @@ class _DeviceDataComponentWidgetState extends State { borderRadius: AppConstants().button_container_radius, onTap: () { // TopSlideNotification.show(context, text: "待开发功能".tr); - Get.toNamed("/messageReviewPage",arguments: widget.device); + Get.toNamed("/messageReviewPage", + arguments: widget.device); }, colors: [ themeController.currentColor.sc1, @@ -1463,19 +1464,3 @@ class _DeviceDataComponentWidgetState extends State { } } } - -_showBluetoothNotEnabledDialog() async { - await showDialog( - context: Get.context!, - builder: (_) => AlertDialog( - title: Text("蓝牙未开启"), - content: Text("请先打开蓝牙再进行WIFI配置"), - actions: [ - TextButton( - onPressed: () => Navigator.of(Get.context!).pop(), - child: Text("知道了"), - ), - ], - ), - ); -} diff --git a/lib/pages/device/instant_body_page.dart b/lib/pages/device/instant_body_page.dart index f280997..5f35174 100644 --- a/lib/pages/device/instant_body_page.dart +++ b/lib/pages/device/instant_body_page.dart @@ -325,6 +325,7 @@ class _InstantBodyPageState extends State { ), ].divide(SizedBox(height: 34.rpx)), ), + Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -342,6 +343,8 @@ class _InstantBodyPageState extends State { color: themeController .currentColor.sc3, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), Text( '${device['person']?['weight'] ?? '未知数据'.tr}kg', @@ -358,6 +361,7 @@ class _InstantBodyPageState extends State { ), ].divide(SizedBox(height: 34.rpx)), ), + ] .divide(SizedBox(width: 33.rpx)) .addToStart(SizedBox(width: 37.rpx)), diff --git a/lib/pages/device_bind/blueteeth_device_page.dart b/lib/pages/device_bind/blueteeth_device_page.dart index ab7f719..3e95083 100644 --- a/lib/pages/device_bind/blueteeth_device_page.dart +++ b/lib/pages/device_bind/blueteeth_device_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:permission_handler/permission_handler.dart'; // 引入permission_handler +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'; @@ -15,6 +16,7 @@ import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; import 'package:vbvs_app/controller/user_info_controller.dart'; import 'package:vbvs_app/model/BleDeviceData.dart'; import 'package:vbvs_app/pages/device_bind/componnet/SingleBlueteethDeviceCompoentWidget.dart'; +import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart'; class BlueteethDevicePage extends StatefulWidget { int tid = -1; @@ -730,19 +732,40 @@ class _BlueteethDevicePageState extends State { } _showBluetoothNotEnabledDialog() async { - await showDialog( - context: context, - builder: (_) => AlertDialog( - title: Text("蓝牙未开启"), - content: Text("请先打开蓝牙再进行设备扫描"), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("知道了"), - ), - ], - ), - ); + await showTipDialog( + context, + Column( + children: [ + Text( + "蓝牙未开启".tr, + style: TextStyle( + fontSize: AppConstants().title_text_fontSize, + color: themeController.currentColor.sc3), + ), + SizedBox( + height: 20.rpx, + ), + Text( + "请先打开蓝牙在进行设备扫描".tr, + style: TextStyle( + fontSize: AppConstants().normal_text_fontSize, + color: themeController.currentColor.sc3), + ), + ], + )); + // await showDialog( + // context: context, + // builder: (_) => AlertDialog( + // title: Text("蓝牙未开启"), + // content: Text("请先打开蓝牙再进行设备扫描"), + // actions: [ + // TextButton( + // onPressed: () => Navigator.of(context).pop(), + // child: Text("知道了"), + // ), + // ], + // ), + // ); } } diff --git a/lib/pages/device_bind/componnet/bind_dialog.dart b/lib/pages/device_bind/componnet/bind_dialog.dart index 786020d..07f4e8d 100644 --- a/lib/pages/device_bind/componnet/bind_dialog.dart +++ b/lib/pages/device_bind/componnet/bind_dialog.dart @@ -860,7 +860,7 @@ void showWifiDialog( ); } -void showTipDialog( +String showTipDialog( BuildContext context, Widget widget, { Color? backgroundColor, // 新增可选参数 @@ -990,4 +990,5 @@ void showTipDialog( ); }, ); + return ""; } diff --git a/lib/pages/device_bind/wifi_page.dart b/lib/pages/device_bind/wifi_page.dart index ef96d7d..e4b6a82 100644 --- a/lib/pages/device_bind/wifi_page.dart +++ b/lib/pages/device_bind/wifi_page.dart @@ -62,6 +62,7 @@ class _WifiPageState extends State { // textColor: themeController.currentColor.sc2, // ); // }); + blueteethBindController.blueConnectFlag.value = 2; blueteethBindController.currentDevice = bledevice; if (lisObj != null) { lisObj!.cancel(); @@ -116,6 +117,8 @@ class _WifiPageState extends State { } }); } else { + blueteethBindController.blueConnectFlag.value = 0; + blueteethBindController.updateAll(); dealWifi(widget.type['mac']).then((aa) { print("object"); }); @@ -175,21 +178,50 @@ class _WifiPageState extends State { SizedBox( width: 14.rpx, ), - Obx(() { - if (blueteethBindController.connectStatus.value == - 0) { - return SizedBox( - width: 24.rpx, - height: 24.rpx, - child: CircularProgressIndicator( - strokeWidth: 1, - valueColor: - AlwaysStoppedAnimation(Colors.white), - ), - ); - } - return Container(); - }), + if (widget.type == null) + Obx(() { + if (blueteethBindController.connectStatus.value == + 0) { + return SizedBox( + width: 24.rpx, + height: 24.rpx, + child: CircularProgressIndicator( + strokeWidth: 1, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ); + } + return Container(); + }), + if (widget.type != null) + Obx(() { + if (blueteethBindController.blueConnectFlag.value == + 0) { + return SizedBox( + width: 24.rpx, + height: 24.rpx, + child: CircularProgressIndicator( + strokeWidth: 1, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ); + } + if (blueteethBindController.connectStatus.value == + 0&&blueteethBindController.blueConnectFlag.value ==0) { + return SizedBox( + width: 24.rpx, + height: 24.rpx, + child: CircularProgressIndicator( + strokeWidth: 1, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ); + } + return Container(); + }), ], ), @@ -717,6 +749,20 @@ class _WifiPageState extends State { horizontal: 20.rpx, vertical: 10.rpx), borderRadius: 20.rpx, onTap: () async { + if ((blueteethBindController + .blueConnectFlag == + 0 || + blueteethBindController + .blueConnectFlag == + 1) && + widget.type != null) { + blueteethBindController + .blueConnectFlag.value = 0; + dealWifi(widget.type['mac']).then((aa) { + print("object"); + }); + return; + } blueteethBindController .connectStatus.value = 0; blueteethBindController.updateAll(); @@ -957,6 +1003,8 @@ class _WifiPageState extends State { timeoutTimer = Timer(Duration(seconds: 20), () { try { if (!isConnected) { + blueteethBindController.blueConnectFlag.value = 1; + blueteethBindController.updateAll(); // Navigator.of(context).pop(); // 先关闭 dialog WidgetsBinding.instance.addPostFrameCallback((_) { TopSlideNotification.show( @@ -1005,6 +1053,7 @@ class _WifiPageState extends State { var res2 = bledevice.isConnected; if (res2) { // Navigator.pop(context); + blueteethBindController.blueConnectFlag.value = 2; TopSlideNotification.show( context, text: "蓝牙绑定.连接成功".tr, diff --git a/lib/pages/main_bottom/home_page.dart b/lib/pages/main_bottom/home_page.dart index 5c32355..e60b380 100644 --- a/lib/pages/main_bottom/home_page.dart +++ b/lib/pages/main_bottom/home_page.dart @@ -1013,7 +1013,7 @@ class _HomePageState extends State { (device) => device['mac'] == mac, ); List stateModule = []; - + String currentTime = ""; return DynamicReportDetailWidget( targetDevice: targetDevice!, sleepDateWidgets: List.generate( @@ -1030,6 +1030,7 @@ class _HomePageState extends State { dayData['selected'] == true && dayData['state'] != null) { stateModule = dayData['state']; + currentTime = dayData['time']; } return SleepDateWidget( mac: mac, @@ -1053,9 +1054,13 @@ class _HomePageState extends State { stateModule.isNotEmpty ? List.generate( stateModule.length, - (j) => SleepDataModuleWidget( - data: stateModule[j], - ), + (j) { + stateModule[j]['onto'] = true; + stateModule[j]['time'] = currentTime; + return SleepDataModuleWidget( + data: stateModule[j], + ); + }, ) : [], ); diff --git a/lib/pages/main_bottom/mine_page.dart b/lib/pages/main_bottom/mine_page.dart index 916074d..5e0a95d 100644 --- a/lib/pages/main_bottom/mine_page.dart +++ b/lib/pages/main_bottom/mine_page.dart @@ -693,7 +693,7 @@ class _MinePageState extends State { mainAxisSize: MainAxisSize.max, children: [ Text( - 'V1.0.2505.28', + 'V1.0.2505.29', style: FlutterFlowTheme.of(context) .bodyMedium diff --git a/lib/pages/sleep_report/chart/FatigueCircleIndicator.dart b/lib/pages/sleep_report/chart/FatigueCircleIndicator.dart index 73778b5..b713d07 100644 --- a/lib/pages/sleep_report/chart/FatigueCircleIndicator.dart +++ b/lib/pages/sleep_report/chart/FatigueCircleIndicator.dart @@ -50,9 +50,7 @@ class FatigueCircleIndicator extends StatelessWidget { '$percent%', style: TextStyle( fontSize: AppConstants().normal_text_fontSize, - color: percent > 60 - ? themeController.currentColor.sc9 - : themeController.currentColor.sc3, + color: color, ), ), SizedBox(height: 4.rpx), @@ -60,9 +58,7 @@ class FatigueCircleIndicator extends StatelessWidget { explain, style: TextStyle( fontSize: AppConstants().normal_text_fontSize, - color: percent > 60 - ? themeController.currentColor.sc9 - : themeController.currentColor.sc3, + color: color, ), ), ], diff --git a/lib/pages/sleep_report/chart/LineChartByRange.dart b/lib/pages/sleep_report/chart/LineChartByRange.dart index 024e608..37ffa44 100644 --- a/lib/pages/sleep_report/chart/LineChartByRange.dart +++ b/lib/pages/sleep_report/chart/LineChartByRange.dart @@ -5,69 +5,517 @@ import 'package:vbvs_app/common/util/MyUtils.dart'; import 'dart:ui' as ui; import 'dart:math'; -class LineChartByRange extends StatelessWidget { +//根据数据自定义 +// class LineChartByRange extends StatefulWidget { +// final List> showLabel; +// final int startTime; +// final int endTime; +// final int? threshold; + +// const LineChartByRange({ +// Key? key, +// required this.showLabel, +// required this.startTime, +// required this.endTime, +// this.threshold, // 新增 +// }) : super(key: key); + +// @override +// State createState() => _LineChartByRangeState(); +// } + +// class _LineChartByRangeState extends State { +// Offset? selectedOffset; +// Map? selectedData; + +// @override +// Widget build(BuildContext context) { +// if (widget.showLabel.isEmpty) return const SizedBox(); + +// int maxTimes = widget.showLabel +// .map((e) => e['times'] ?? 0) +// .reduce((a, b) => a > b ? a : b); +// int yMax = (maxTimes / 10).ceil() * 10; +// if (yMax == 0) yMax = 10; + +// DateTime minTime = DateTime.fromMillisecondsSinceEpoch(widget.startTime); +// DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime); + +// return GestureDetector( +// onTapDown: (details) { +// RenderBox box = context.findRenderObject() as RenderBox; +// final localPosition = box.globalToLocal(details.globalPosition); + +// // 查找是否点击到某个点 +// for (var item in widget.showLabel) { +// int start = item['startTime']; +// int end = item['endTime']; +// int times = item['times']; + +// double chartWidth = box.size.width - 40.rpx; // 与 painter 内一致处理 +// double chartHeight = box.size.height - 30.rpx; +// double xStart = 20.rpx + 12.rpx; + +// int totalDuration = +// maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch; + +// double startX = xStart + +// chartWidth * +// (start - minTime.millisecondsSinceEpoch) / +// totalDuration; +// double y = chartHeight * (1 - times / yMax); + +// // 判断点击范围(圆点半径±6.rpx范围) +// if ((localPosition - Offset(startX, y)).distance < 10.rpx) { +// setState(() { +// selectedOffset = Offset(startX, y); +// selectedData = item; +// }); +// return; +// } + +// double endX = xStart + +// chartWidth * +// (end - minTime.millisecondsSinceEpoch) / +// totalDuration; +// if ((localPosition - Offset(endX, y)).distance < 10.rpx) { +// setState(() { +// selectedOffset = Offset(endX, y); +// selectedData = item; +// }); +// return; +// } +// } + +// // 没点到,清除选中 +// setState(() { +// selectedOffset = null; +// selectedData = null; +// }); +// }, +// child: Stack( +// children: [ +// SizedBox( +// height: 500.rpx, +// child: CustomPaint( +// size: Size(double.infinity, 500.rpx), +// painter: _LineChartByRangePainter( +// data: widget.showLabel, +// yMax: yMax, +// minTime: minTime, +// maxTime: maxTime, +// threshold: widget.threshold, // 新增 +// ), +// ), +// ), +// if (selectedOffset != null && selectedData != null) +// Positioned( +// left: selectedOffset!.dx - 60.rpx, +// top: selectedOffset!.dy - 50.rpx, +// child: Container( +// padding: +// EdgeInsets.symmetric(horizontal: 12.rpx, vertical: 8.rpx), +// decoration: BoxDecoration( +// color: Colors.black.withOpacity(0.3), +// borderRadius: BorderRadius.circular(10.rpx), +// ), +// child: Text( +// '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - ' +// '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n' +// '次数: ${selectedData!['times']}', +// style: TextStyle( +// fontSize: 18.rpx, +// color: Colors.white, +// ), +// ), +// ), +// ), +// ], +// ), +// ); +// } +// } + +// class _LineChartByRangePainter extends CustomPainter { +// final List> data; +// final int yMax; +// final DateTime minTime; +// final DateTime maxTime; +// final int? threshold; + +// _LineChartByRangePainter({ +// required this.data, +// required this.yMax, +// required this.minTime, +// required this.maxTime, +// this.threshold, +// }); + +// @override +// void paint(Canvas canvas, Size size) { +// double padding = 20.rpx; +// double labelInset = 12.rpx; + +// final double xStart = padding + labelInset; +// final double xEnd = size.width - padding - labelInset; +// final double chartWidth = xEnd - xStart; + +// double chartHeight = size.height - 30.rpx; + +// int totalDuration = +// maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch; +// if (totalDuration <= 0) return; + +// Paint linePaint = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = 3.rpx +// ..color = stringToColor("#00C1AA") +// ..strokeCap = StrokeCap.round; + +// Paint axisPaint = Paint() +// ..color = Colors.grey.withOpacity(0.4) +// ..strokeWidth = 1.rpx; + +// Paint thresholdPaint = Paint() +// ..color = themeController.currentColor.sc9 +// ..strokeWidth = 1.rpx; + +// // 1. 阈值虚线(红色) +// if (threshold != null && threshold! >= 0 && threshold! <= yMax) { +// double yThreshold = chartHeight * (1 - threshold! / yMax); +// drawDashedLine( +// canvas, +// Offset(xStart, yThreshold), +// Offset(xEnd, yThreshold), +// thresholdPaint, +// dashWidth: 8.rpx, +// dashSpace: 6.rpx, +// ); +// } + +// // 2. 绘制数据线段和圆点 +// for (var item in data) { +// int start = item['startTime']; +// int end = item['endTime']; +// int times = item['times']; + +// double startX = xStart + +// chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration; +// double endX = xStart + +// chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration; +// double y = chartHeight * (1 - times / yMax); + +// // 设置颜色(根据 threshold 判断) +// Color pointColor; +// if (threshold != null && times >= threshold!) { +// pointColor = themeController.currentColor.sc9; +// } else { +// pointColor = stringToColor("#00C1AA"); +// } + +// Paint dynamicLinePaint = Paint() +// ..style = PaintingStyle.stroke +// ..strokeWidth = 3.rpx +// ..color = pointColor +// ..strokeCap = StrokeCap.round; + +// Paint dynamicCirclePaint = Paint() +// ..style = PaintingStyle.fill +// ..color = pointColor; + +// // 画线段 +// canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint); + +// // 画起点和终点圆点 +// canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint); +// canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint); +// } + +// // 3. Y轴辅助线和文字 +// for (int i = 0; i <= 6; i++) { +// double y = chartHeight * i / 6; + +// if (i == 6) { +// canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint); +// } else { +// drawDashedLine( +// canvas, +// Offset(xStart, y), +// Offset(xEnd, y), +// axisPaint, +// dashWidth: 8.rpx, +// dashSpace: 6.rpx, +// ); +// } + +// TextPainter tp = TextPainter( +// text: TextSpan( +// text: '${yMax - (yMax * i / 6).round()}', +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ), +// textDirection: ui.TextDirection.ltr, +// ); +// tp.layout(); +// tp.paint(canvas, Offset(0, y - tp.height / 2)); +// } + +// // 4. X轴主线 +// canvas.drawLine( +// Offset(xStart, chartHeight), +// Offset(xEnd, chartHeight), +// axisPaint, +// ); + +// // 5. X轴时间文字(左右两侧) +// String leftLabel = DateFormat('HH:mm').format(minTime); +// TextPainter leftTp = TextPainter( +// text: TextSpan( +// text: leftLabel, +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ), +// textDirection: ui.TextDirection.ltr, +// ); +// leftTp.layout(); +// leftTp.paint(canvas, +// Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx)); + +// String rightLabel = DateFormat('HH:mm').format(maxTime); +// TextPainter rightTp = TextPainter( +// text: TextSpan( +// text: rightLabel, +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ), +// textDirection: ui.TextDirection.ltr, +// ); +// rightTp.layout(); +// rightTp.paint( +// canvas, +// Offset(size.width - padding - labelInset - rightTp.width / 2, +// chartHeight + 8.rpx)); + +// // 6. 中间小时刻度 +// int totalHours = maxTime.difference(minTime).inHours + 1; +// int startHour = minTime.hour; + +// for (int i = 1; i < totalHours; i++) { +// double x = xStart + chartWidth * i / totalHours; + +// int hourLabelNum = (startHour + i) % 24; +// String hourLabel = '$hourLabelNum'; + +// TextPainter tp = TextPainter( +// text: TextSpan( +// text: hourLabel, +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ), +// textDirection: ui.TextDirection.ltr, +// ); +// tp.layout(); +// tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx)); +// } +// } + +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => true; + +// void drawDashedLine( +// Canvas canvas, +// Offset start, +// Offset end, +// Paint paint, { +// required double dashWidth, +// required double dashSpace, +// }) { +// final dx = end.dx - start.dx; +// final dy = end.dy - start.dy; +// final distance = sqrt(dx * dx + dy * dy); +// final direction = Offset(dx / distance, dy / distance); + +// double drawn = 0; +// while (drawn < distance) { +// final from = start + direction * drawn; +// final to = start + direction * (drawn + dashWidth).clamp(0, distance); +// canvas.drawLine(from, to, paint); +// drawn += dashWidth + dashSpace; +// } +// } +// } + + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'dart:ui' as ui; +import 'dart:math'; + +class LineChartByRange extends StatefulWidget { final List> showLabel; final int startTime; final int endTime; + final int? threshold; + + /// 新增外部指定的 Y 轴最大值 + final int maxY; + + /// Y 轴分段数,默认6段 + final int ySegments; const LineChartByRange({ Key? key, required this.showLabel, required this.startTime, required this.endTime, + required this.maxY, + this.threshold, + this.ySegments = 6, }) : super(key: key); + @override + State createState() => _LineChartByRangeState(); +} + +class _LineChartByRangeState extends State { + Offset? selectedOffset; + Map? selectedData; + @override Widget build(BuildContext context) { - if (showLabel.isEmpty) return const SizedBox(); + if (widget.showLabel.isEmpty) return const SizedBox(); - int maxTimes = - showLabel.map((e) => e['times'] ?? 0).reduce((a, b) => a > b ? a : b); - int yMax = (maxTimes / 10).ceil() * 10; - if (yMax == 0) yMax = 10; + DateTime minTime = DateTime.fromMillisecondsSinceEpoch(widget.startTime); + DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime); - DateTime minTime = DateTime.fromMillisecondsSinceEpoch(startTime); - DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(endTime); + return GestureDetector( + onTapDown: (details) { + RenderBox box = context.findRenderObject() as RenderBox; + final localPosition = box.globalToLocal(details.globalPosition); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 500.rpx, - child: CustomPaint( - size: Size(double.infinity, 500.rpx), - painter: _LineChartByRangePainter( - data: showLabel, - yMax: yMax, - minTime: minTime, - maxTime: maxTime, + // 查找是否点击到某个点 + for (var item in widget.showLabel) { + int start = item['startTime']; + int end = item['endTime']; + int times = item['times']; + + double chartWidth = box.size.width - 40.rpx; // 与 painter 内一致处理 + double chartHeight = box.size.height - 30.rpx; + double xStart = 20.rpx + 12.rpx; + + int totalDuration = + maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch; + + double startX = xStart + + chartWidth * + (start - minTime.millisecondsSinceEpoch) / + totalDuration; + double y = chartHeight * (1 - times / widget.maxY); + + // 判断点击范围(圆点半径±6.rpx范围) + if ((localPosition - Offset(startX, y)).distance < 10.rpx) { + setState(() { + selectedOffset = Offset(startX, y); + selectedData = item; + }); + return; + } + + double endX = xStart + + chartWidth * + (end - minTime.millisecondsSinceEpoch) / + totalDuration; + if ((localPosition - Offset(endX, y)).distance < 10.rpx) { + setState(() { + selectedOffset = Offset(endX, y); + selectedData = item; + }); + return; + } + } + + // 没点到,清除选中 + setState(() { + selectedOffset = null; + selectedData = null; + }); + }, + child: Stack( + children: [ + SizedBox( + height: 500.rpx, + child: CustomPaint( + size: Size(double.infinity, 500.rpx), + painter: _LineChartByRangePainter( + data: widget.showLabel, + maxY: widget.maxY, + minTime: minTime, + maxTime: maxTime, + threshold: widget.threshold, + ySegments: widget.ySegments, + ), ), ), - ), - ], + if (selectedOffset != null && selectedData != null) + Positioned( + left: selectedOffset!.dx - 60.rpx, + top: selectedOffset!.dy - 50.rpx, + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.rpx, vertical: 8.rpx), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: BorderRadius.circular(10.rpx), + ), + child: Text( + '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - ' + '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n' + '次数: ${selectedData!['times']}', + style: TextStyle( + fontSize: 18.rpx, + color: Colors.white, + ), + ), + ), + ), + ], + ), ); } } class _LineChartByRangePainter extends CustomPainter { final List> data; - final int yMax; + final int maxY; final DateTime minTime; final DateTime maxTime; + final int? threshold; + final int ySegments; _LineChartByRangePainter({ required this.data, - required this.yMax, + required this.maxY, required this.minTime, required this.maxTime, + this.threshold, + this.ySegments = 6, }); @override void paint(Canvas canvas, Size size) { double padding = 20.rpx; - double labelInset = 12.rpx; // X轴标签缩进距离 + double labelInset = 12.rpx; - // 绘图X轴起止点,考虑内缩labelInset final double xStart = padding + labelInset; final double xEnd = size.width - padding - labelInset; final double chartWidth = xEnd - xStart; @@ -78,17 +526,28 @@ class _LineChartByRangePainter extends CustomPainter { maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch; if (totalDuration <= 0) return; - Paint linePaint = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 3.rpx - ..color = stringToColor("#00C1AA") - ..strokeCap = StrokeCap.round; + Paint axisPaint = Paint() + ..color = Colors.grey.withOpacity(0.4) + ..strokeWidth = 1.rpx; - Paint fillCirclePaint = Paint() - ..style = PaintingStyle.fill - ..color = stringToColor("#00C1AA"); + Paint thresholdPaint = Paint() + ..color = themeController.currentColor.sc9 + ..strokeWidth = 1.rpx; - // 1. 先绘制数据线段及起止点圆点 + // 阈值虚线(红色) + if (threshold != null && threshold! >= 0 && threshold! <= maxY) { + double yThreshold = chartHeight * (1 - threshold! / maxY); + drawDashedLine( + canvas, + Offset(xStart, yThreshold), + Offset(xEnd, yThreshold), + thresholdPaint, + dashWidth: 8.rpx, + dashSpace: 6.rpx, + ); + } + + // 绘制数据线段和圆点 for (var item in data) { int start = item['startTime']; int end = item['endTime']; @@ -98,31 +557,41 @@ class _LineChartByRangePainter extends CustomPainter { chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration; double endX = xStart + chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration; - double y = chartHeight * (1 - times / yMax); + double y = chartHeight * (1 - times / maxY); + + // 设置颜色(根据 threshold 判断) + Color pointColor; + if (threshold != null && times >= threshold!) { + pointColor = themeController.currentColor.sc9; + } else { + pointColor = stringToColor("#00C1AA"); + } + + Paint dynamicLinePaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3.rpx + ..color = pointColor + ..strokeCap = StrokeCap.round; + + Paint dynamicCirclePaint = Paint() + ..style = PaintingStyle.fill + ..color = pointColor; // 画线段 - canvas.drawLine(Offset(startX, y), Offset(endX, y), linePaint); + canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint); - // 画起点圆点 - canvas.drawCircle(Offset(startX, y), 4.rpx, fillCirclePaint); - - // 画终点圆点 - canvas.drawCircle(Offset(endX, y), 4.rpx, fillCirclePaint); + // 画起点和终点圆点 + canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint); + canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint); } - // 2. Y轴辅助线及文字 - Paint axisPaint = Paint() - ..color = Colors.grey.withOpacity(0.4) - ..strokeWidth = 1.rpx; + // Y轴辅助线和文字 + for (int i = 0; i <= ySegments; i++) { + double y = chartHeight * i / ySegments; - for (int i = 0; i <= 6; i++) { - double y = chartHeight * i / 6; - - if (i == 6) { - // 实线 + if (i == ySegments) { canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint); } else { - // 虚线 drawDashedLine( canvas, Offset(xStart, y), @@ -133,12 +602,13 @@ class _LineChartByRangePainter extends CustomPainter { ); } - // Y轴文字 TextPainter tp = TextPainter( text: TextSpan( - text: '${yMax - (yMax * i / 6).round()}', + text: '${maxY - (maxY * i / ySegments).round()}', style: TextStyle( - fontSize: 18.rpx, color: themeController.currentColor.sc4), + fontSize: 18.rpx, + color: themeController.currentColor.sc4, + ), ), textDirection: ui.TextDirection.ltr, ); @@ -146,29 +616,14 @@ class _LineChartByRangePainter extends CustomPainter { tp.paint(canvas, Offset(0, y - tp.height / 2)); } - // 3. X轴线 + // X轴主线 canvas.drawLine( - Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint); + Offset(xStart, chartHeight), + Offset(xEnd, chartHeight), + axisPaint, + ); - // 4. 画X轴时间点对应的垂直虚线辅助线 - int totalHours = maxTime.difference(minTime).inHours; - int startHour = minTime.hour; - - // for (int i = 1; i < totalHours; i++) { - // double x = xStart + chartWidth * i / totalHours; - - // // 垂直虚线 - // drawDashedLine( - // canvas, - // Offset(x, 0), - // Offset(x, chartHeight), - // axisPaint, - // dashWidth: 4.rpx, - // dashSpace: 4.rpx, - // ); - // } - - // 5. 画左侧完整时分 (HH:mm),往内缩 labelInset + // X轴时间文字(左右两侧) String leftLabel = DateFormat('HH:mm').format(minTime); TextPainter leftTp = TextPainter( text: TextSpan( @@ -184,7 +639,6 @@ class _LineChartByRangePainter extends CustomPainter { leftTp.paint(canvas, Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx)); - // 6. 画右侧完整时分 (HH:mm),往内缩 labelInset String rightLabel = DateFormat('HH:mm').format(maxTime); TextPainter rightTp = TextPainter( text: TextSpan( @@ -202,7 +656,10 @@ class _LineChartByRangePainter extends CustomPainter { Offset(size.width - padding - labelInset - rightTp.width / 2, chartHeight + 8.rpx)); - // 7. 中间小时数字(23, 0, 1, 2, ...) + // 中间小时刻度 + int totalHours = maxTime.difference(minTime).inHours + 1; + int startHour = minTime.hour; + for (int i = 1; i < totalHours; i++) { double x = xStart + chartWidth * i / totalHours; @@ -220,7 +677,6 @@ class _LineChartByRangePainter extends CustomPainter { textDirection: ui.TextDirection.ltr, ); tp.layout(); - tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx)); } } diff --git a/lib/pages/sleep_report/chart/SnoreChart.dart b/lib/pages/sleep_report/chart/SnoreChart.dart new file mode 100644 index 0000000..f6295ce --- /dev/null +++ b/lib/pages/sleep_report/chart/SnoreChart.dart @@ -0,0 +1,199 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.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 BarData { + final int st; // 起始时间(毫秒) + final int et; // 结束时间(毫秒) + final double value; // 柱子高度 + final int id; + final String name; + final Color color; + + BarData({ + required this.st, + required this.et, + required this.value, + required this.id, + required this.name, + required this.color, + }); +} + +class BarChartWidget extends StatelessWidget { + final List data; + final int startTime; // 毫秒时间戳 + final int endTime; // 毫秒时间戳 + final double maxYValue; // Y轴最大值 + final int yStepCount; // Y轴分段数 + + const BarChartWidget({ + super.key, + required this.data, + required this.startTime, + required this.endTime, + required this.maxYValue, + this.yStepCount = 5, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(double.infinity, 500.rpx), + painter: BarChartPainter( + data, + startTime, + endTime, + maxYValue: maxYValue, + yStepCount: yStepCount, + ), + ); + } +} + +class BarChartPainter extends CustomPainter { + final List data; + final int startTime; + final int endTime; + final double maxYValue; + final int yStepCount; + + final double topPadding = 0; // 控制顶部间距 + final double bottomPadding = 0; // 控制底部间距 + final double leftPadding = 30.rpx; + // final double labelHeight = 50.rpx; + + BarChartPainter( + this.data, + this.startTime, + this.endTime, { + required this.maxYValue, + this.yStepCount = 5, + }); + + @override + void paint(Canvas canvas, Size size) { + final chartWidth = size.width - leftPadding; + final chartHeight = size.height - topPadding - bottomPadding; + final totalDuration = endTime - startTime; + + final textPainter = TextPainter(textDirection: ui.TextDirection.ltr); + final stepValue = maxYValue / yStepCount; + + // 绘制 Y 轴刻度线和文字 + for (int i = 0; i <= yStepCount; i++) { + final value = stepValue * i; + final y = topPadding + chartHeight - (value / maxYValue) * chartHeight; + + // 横线 + // canvas.drawLine( + // Offset(leftPadding, y), + // Offset(size.width, y), + // Paint() + // ..color = Colors.grey.withOpacity(0.3) + // ..strokeWidth = 0.5, + // ); + final dashPaint = Paint() + ..color = Colors.grey.withOpacity(0.5) + ..strokeWidth = 0.5; + + drawDashedLine( + canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); + + // Y轴刻度文字 + textPainter.text = TextSpan( + text: value.toStringAsFixed(0), + style: TextStyle( + fontSize: 20.rpx, + color: themeController.currentColor.sc4, + ), + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + leftPadding - textPainter.width - 4, y - textPainter.height / 2)); + } + + // X轴时间刻度 + final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); + final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); + final hourStep = const Duration(hours: 1); + final xPaint = Paint()..color = Colors.grey; + + final xAxisY = topPadding + chartHeight; + + // 绘制整点小时刻度 + for (DateTime t = startDate; t.isBefore(endDate); t = t.add(hourStep)) { + final x = ((t.millisecondsSinceEpoch - startTime) / totalDuration) * + chartWidth + + leftPadding; + + final timeLabel = (t == startDate || t == endDate) + ? DateFormat('HH:mm').format(t) + : DateFormat('h').format(t); + + textPainter.text = TextSpan( + text: timeLabel, + style: TextStyle( + fontSize: AppConstants().smaller_text_fontSize, + color: themeController.currentColor.sc4, + ), + ); + textPainter.layout(); + textPainter.paint(canvas, Offset(x - textPainter.width / 2, xAxisY + 4)); + } + + // ✅ 强制绘制结束时间刻度(确保显示) + final endX = + ((endTime - startTime) / totalDuration) * chartWidth + leftPadding; + final endLabel = DateFormat('HH:mm').format(endDate); + textPainter.text = TextSpan( + text: endLabel, + style: TextStyle( + fontSize: AppConstants().smaller_text_fontSize, + color: themeController.currentColor.sc4, + ), + ); + textPainter.layout(); + textPainter.paint(canvas, Offset(endX - textPainter.width / 2, xAxisY + 4)); + + // 绘制柱子 + for (final d in data) { + final left = + ((d.st - startTime) / totalDuration) * chartWidth + leftPadding; + final right = + ((d.et - startTime) / totalDuration) * chartWidth + leftPadding; + final barHeight = (d.value / maxYValue) * chartHeight; + final top = topPadding + chartHeight - barHeight; + + final barPaint = Paint()..color = d.color; + + canvas.drawRect( + Rect.fromLTRB(left, top, right, topPadding + chartHeight), barPaint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; + + void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint, + {double dashWidth = 5, double dashSpace = 3}) { + double totalLength = (end.dx - start.dx).abs(); + double dashCount = (totalLength / (dashWidth + dashSpace)).floorToDouble(); + + double dx = start.dx; + final dy = start.dy; + + for (int i = 0; i < dashCount; i++) { + final from = Offset(dx, dy); + final to = Offset(dx + dashWidth, dy); + canvas.drawLine(from, to, paint); + dx += dashWidth + dashSpace; + } + } +} diff --git a/lib/pages/sleep_report/chart/SnoreWaveform.dart b/lib/pages/sleep_report/chart/SnoreWaveform.dart index 8320ea6..999e08f 100644 --- a/lib/pages/sleep_report/chart/SnoreWaveform.dart +++ b/lib/pages/sleep_report/chart/SnoreWaveform.dart @@ -164,6 +164,114 @@ class SnoreWaveform extends StatelessWidget { } } +// class SnoreWaveformPainter extends CustomPainter { +// final List snoreValues; +// final int startTime; +// final int endTime; + +// SnoreWaveformPainter({ +// required this.snoreValues, +// required this.startTime, +// required this.endTime, +// }); + +// @override +// void paint(Canvas canvas, Size size) { +// final double width = size.width; +// final double height = size.height; +// final double centerY = height / 2; +// final double totalDuration = (endTime - startTime).toDouble(); +// final double pixelPerMs = width / totalDuration; + +// final Paint wavePaint = Paint() +// ..color = stringToColor("#8E7DEF") +// ..strokeWidth = 1.5 +// ..style = PaintingStyle.stroke; + +// final Path upperPath = Path(); +// final Path lowerPath = Path(); +// const double scaleY = 0.5; //波形图比例 + +// for (int i = 0; i < snoreValues.length; i++) { +// final timestamp = snoreValues[i]["st"]; +// final value = snoreValues[i]["value"]?.toDouble() ?? 0; + +// final x = (timestamp - startTime) * pixelPerMs; +// final y = centerY - value * scaleY; +// final yMirror = centerY + value * scaleY; + +// if (i == 0) { +// upperPath.moveTo(x, y); +// lowerPath.moveTo(x, yMirror); +// } else { +// upperPath.lineTo(x, y); +// lowerPath.lineTo(x, yMirror); +// } +// } + +// canvas.drawPath(upperPath, wavePaint); +// canvas.drawPath(lowerPath, wavePaint); + +// final Paint axisPaint = Paint() +// ..color = Colors.grey +// ..strokeWidth = 0.5; + +// // 画中心线 +// canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint); + +// // 时间刻度绘制 +// final textPainter = TextPainter( +// textAlign: TextAlign.center, +// textDirection: ui.TextDirection.ltr, +// ); + +// final int hourMs = 60 * 60 * 1000; + +// // 循环绘制整点小时标签(不包含终点) +// for (int t = startTime; t < endTime; t += hourMs) { +// double x = (t - startTime) * pixelPerMs; + +// DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); +// String label; +// if (t == startTime) { +// label = DateFormat('HH:mm').format(dt); // 起点显示 HH:mm +// } else { +// label = DateFormat('h').format(dt); // 中间显示小时,不带前导0 +// } + +// textPainter.text = TextSpan( +// text: label, +// style: TextStyle(fontSize: 10, color: Colors.grey), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, +// Offset(x - textPainter.width / 2, height + 20.rpx), +// ); +// } + +// // 单独绘制终点时间标签,确保显示具体时分 +// { +// double x = (endTime - startTime) * pixelPerMs; +// DateTime dt = DateTime.fromMillisecondsSinceEpoch(endTime); +// String label = DateFormat('HH:mm').format(dt); + +// textPainter.text = TextSpan( +// text: label, +// style: TextStyle(fontSize: 10, color: Colors.grey), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, +// Offset(x - textPainter.width / 2, height + 20.rpx), +// ); +// } +// } + +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +// } + class SnoreWaveformPainter extends CustomPainter { final List snoreValues; final int startTime; @@ -184,13 +292,22 @@ class SnoreWaveformPainter extends CustomPainter { final double pixelPerMs = width / totalDuration; final Paint wavePaint = Paint() - ..color = stringToColor("#8E7DEF") + ..color = stringToColor("#8E7DEF").withOpacity(0.8) ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; final Path upperPath = Path(); final Path lowerPath = Path(); - const double scaleY = 0.5; //波形图比例 + + // ✅ 获取最大值用于自适应比例 + double maxValue = snoreValues.fold(0, (prev, e) { + final value = e["value"]?.toDouble() ?? 0; + return value > prev ? value : prev; + }); + + // ✅ 自适应缩放比例,限制波形最大高度为 height * 0.45 + final double maxWaveHeight = height * 1; + final double scaleY = maxValue > 0 ? (maxWaveHeight / maxValue) : 1; for (int i = 0; i < snoreValues.length; i++) { final timestamp = snoreValues[i]["st"]; @@ -212,32 +329,26 @@ class SnoreWaveformPainter extends CustomPainter { canvas.drawPath(upperPath, wavePaint); canvas.drawPath(lowerPath, wavePaint); + // ✅ 最后绘制中心线,防止被覆盖 final Paint axisPaint = Paint() - ..color = Colors.grey + ..color = Colors.grey.withOpacity(0.6) ..strokeWidth = 0.5; - - // 画中心线 canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint); - // 时间刻度绘制 + // ✅ 时间刻度绘制 final textPainter = TextPainter( textAlign: TextAlign.center, textDirection: ui.TextDirection.ltr, ); final int hourMs = 60 * 60 * 1000; - - // 循环绘制整点小时标签(不包含终点) for (int t = startTime; t < endTime; t += hourMs) { double x = (t - startTime) * pixelPerMs; DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); - String label; - if (t == startTime) { - label = DateFormat('HH:mm').format(dt); // 起点显示 HH:mm - } else { - label = DateFormat('h').format(dt); // 中间显示小时,不带前导0 - } + String label = t == startTime + ? DateFormat('HH:mm').format(dt) + : DateFormat('h').format(dt); // 12小时制 textPainter.text = TextSpan( text: label, @@ -246,11 +357,11 @@ class SnoreWaveformPainter extends CustomPainter { textPainter.layout(); textPainter.paint( canvas, - Offset(x - textPainter.width / 2, height + 20.rpx), + Offset(x - textPainter.width / 2, height + 2), // 标签显示在底部 ); } - // 单独绘制终点时间标签,确保显示具体时分 + // ✅ 画终点时间 { double x = (endTime - startTime) * pixelPerMs; DateTime dt = DateTime.fromMillisecondsSinceEpoch(endTime); @@ -263,7 +374,7 @@ class SnoreWaveformPainter extends CustomPainter { textPainter.layout(); textPainter.paint( canvas, - Offset(x - textPainter.width / 2, height + 20.rpx), + Offset(x - textPainter.width / 2, height + 2), ); } } diff --git a/lib/pages/sleep_report/component/AIAdviceWidget.dart b/lib/pages/sleep_report/component/AIAdviceWidget.dart index 1e5d83d..4aef03d 100644 --- a/lib/pages/sleep_report/component/AIAdviceWidget.dart +++ b/lib/pages/sleep_report/component/AIAdviceWidget.dart @@ -7,6 +7,7 @@ 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 AIAdviceWidget extends StatefulWidget { var sleepReport; @@ -34,88 +35,94 @@ class _AIAdviceWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport['sugges'] == null || - widget.sleepReport['sugges'].isEmpty) { - return Container(); - } - List advices = widget.sleepReport['sugges']; + 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "AI分析介绍".tr, - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "AI分析介绍".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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(), + 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("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/BreathPauseWidget.dart b/lib/pages/sleep_report/component/BreathPauseWidget.dart index 6231075..03a6b9d 100644 --- a/lib/pages/sleep_report/component/BreathPauseWidget.dart +++ b/lib/pages/sleep_report/component/BreathPauseWidget.dart @@ -7,6 +7,7 @@ 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/DotBarChart.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class BreathPauseWidget extends StatefulWidget { var sleepReport; @@ -34,107 +35,100 @@ class _BreathPauseWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport['asp'] == null || - widget.sleepReport['asp'].isEmpty) { - return Container(); - } + try { + if (widget.sleepReport == null || + widget.sleepReport['asp'] == null || + widget.sleepReport['asp'].isEmpty) { + return Container(); + } - // List data = widget.sleepReport['asp']; - // var showLabel = [ - // {"time": 1744547251000, "times": 25}, - // {"time": 1744550851000, "times": 27}, - // {"time": 1744554451000, "times": 40}, - // {"time": 1744558051000, "times": 28}, - // {"time": 1744561651000, "times": 15}, - // {"time": 1744565251000, "times": 48}, - // {"time": 1744568851000, "times": 25}, - // {"time": 1744572451000, "times": 17}, - // {"time": 1744583251000, "times": 35}, - // {"time": 1744586851000, "times": 40}, - // ]; - List data = widget.sleepReport['asp']; - var showLabel = convertAspData(data); - var threshold = 30; - var startTime = widget.sleepReport['startTime']; - var endTime = widget.sleepReport['endTime']; - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "呼吸暂停监测介绍。", - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + List data = widget.sleepReport['asp']; + var showLabel = convertAspData(data); + var threshold = 30; + var startTime = widget.sleepReport['startTime']; + var endTime = widget.sleepReport['endTime']; + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "呼吸暂停监测介绍。", + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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: 32.rpx, - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx), - child: DotBarChart( - showLabel: showLabel, - threshold: threshold, - startTime: startTime, - endTime: endTime, + SizedBox( + height: 32.rpx, ), - ), - SizedBox( - height: 52.rpx, - ), - ], + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx), + child: DotBarChart( + showLabel: showLabel, + threshold: threshold, + startTime: startTime, + endTime: endTime, + ), + ), + SizedBox( + height: 52.rpx, + ), + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } List> convertAspData(List data) { diff --git a/lib/pages/sleep_report/component/BreatheCard.dart b/lib/pages/sleep_report/component/BreatheCard.dart index cdd6144..5b640f2 100644 --- a/lib/pages/sleep_report/component/BreatheCard.dart +++ b/lib/pages/sleep_report/component/BreatheCard.dart @@ -3,68 +3,137 @@ 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/home_page/SleepDataModuleWidget.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class BreatheCard extends StatefulWidget { var sleepReport; - BreatheCard({super.key, required this.sleepReport}); + final int? highlightItem; + BreatheCard({super.key, required this.sleepReport, this.highlightItem}); @override State createState() => _BreatheCardState(); } -class _BreatheCardState extends State { - @override - void setState(VoidCallback callback) { - super.setState(callback); - } +class _BreatheCardState extends State + with TickerProviderStateMixin { + final GlobalKey _highlightKey = GlobalKey(); + AnimationController? _animationController; + bool _shouldAnimate = false; + int? _highlightedId; + int _flashCount = 0; @override void initState() { super.initState(); + if (widget.highlightItem != null) { + _highlightedId = widget.highlightItem; + _shouldAnimate = true; + _initAnimation(); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.highlightItem != null && + _highlightKey.currentContext != null) { + Scrollable.ensureVisible( + _highlightKey.currentContext!, + duration: Duration(milliseconds: 500), + curve: Curves.easeInOut, + alignment: 0.3, + ); + } + }); + } + + void _initAnimation() { + _animationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 300), + )..addStatusListener((status) { + if (status == AnimationStatus.completed) { + _animationController!.reverse(); + } else if (status == AnimationStatus.dismissed) { + _flashCount++; + if (_flashCount >= 3) { + _animationController!.dispose(); + setState(() { + _shouldAnimate = false; + _highlightedId = null; + }); + } else { + _animationController!.forward(); + } + } + }); + + _animationController!.forward(); } @override void dispose() { + _animationController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } + + List data = widget.sleepReport['brs'] ?? []; + + 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: Wrap( + spacing: 23.rpx, + runSpacing: 25.rpx, + children: List.generate(data.length, (index) { + final item = data[index]; + item['showTip'] = true; + final bool isHighlighted = + _shouldAnimate && item['id'] == _highlightedId; + + return SizedBox( + width: (MediaQuery.of(context).size.width - 160.rpx) / 3, + child: AnimatedBuilder( + animation: _animationController ?? AlwaysStoppedAnimation(0), + builder: (context, child) { + return Container( + key: isHighlighted ? _highlightKey : null, + decoration: isHighlighted + ? BoxDecoration( + border: Border.all( + color: themeController.currentColor.sc2 + .withOpacity( + _animationController?.value ?? 0), + width: 1.rpx, + ), + borderRadius: BorderRadius.circular(8), + ) + : null, + child: SleepDataModuleWidget(data: item), + ); + }, + ), + ); + }), + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("呼吸监测绘制异常${e}"); return Container(); } - - List data = widget.sleepReport['brs'] ?? []; - - 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: Wrap( - spacing: 23.rpx, // 横向间距(左右间距如需加可设置) - runSpacing: 25.rpx, // 每行之间的垂直间距 - children: List.generate(data.length, (index) { - final item = data[index]; - item['showTip'] = true; - return SizedBox( - width: (MediaQuery.of(context).size.width - 160.rpx) / 3, - child: SleepDataModuleWidget(data: item), - // child: Container( - // width: 20, - // height: 20, - // color: Colors.red, - // ), - ); - }), - ), - ), - ); } } diff --git a/lib/pages/sleep_report/component/BreathePauseNewWidget.dart b/lib/pages/sleep_report/component/BreathePauseNewWidget.dart index abc8f07..6c3d597 100644 --- a/lib/pages/sleep_report/component/BreathePauseNewWidget.dart +++ b/lib/pages/sleep_report/component/BreathePauseNewWidget.dart @@ -7,6 +7,7 @@ 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/LineChartByRange.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class BreathePauseNewWidget extends StatefulWidget { var sleepReport; @@ -34,102 +35,127 @@ class _SnoreViewWidgetWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { - return Container(); - } + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } - List> data = - (widget.sleepReport['asp'] as List).cast>(); - List> showLabel = convertToShowLabel(data); - var startTime = widget.sleepReport['startTime']; - var endTime = widget.sleepReport['endTime']; - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "呼吸暂停监测介绍。", - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + List standard = widget.sleepReport['brs'] ?? []; + final Map? result = standard.cast().firstWhere( + (element) => element['id'] == 302, + orElse: () => {}, + ); + + int threshold = 0; + if (result != null && result.isNotEmpty) { + final rangeValue = result['range']; + if (rangeValue is int) { + threshold = rangeValue; + } else if (rangeValue is String) { + threshold = int.tryParse(rangeValue) ?? 0; + } + } + + List> data = + (widget.sleepReport['asp'] as List).cast>(); + List> showLabel = convertToShowLabel(data); + var startTime = widget.sleepReport['startTime']; + var endTime = widget.sleepReport['endTime']; + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "呼吸暂停监测介绍。", + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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: 32.rpx, + ), + Row( + children: [ + Text( + "秒".tr, + style: TextStyle( + color: stringToColor("#FFFFFF"), fontSize: 18.rpx), ), ], ), - ), - SizedBox( - height: 32.rpx, - ), - Row( - children: [ - Text( - "秒".tr, - style: TextStyle( - color: stringToColor("#FFFFFF"), fontSize: 18.rpx), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 40.rpx, 0.rpx, 0.rpx), + child: LineChartByRange( + showLabel: showLabel, + startTime: startTime, + endTime: endTime, + threshold: threshold != 0 ? threshold : null, + maxY: threshold == 0 ? threshold + 10 : 70, + ySegments: 7, ), - ], - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(0.rpx, 40.rpx, 0.rpx, 0.rpx), - child: LineChartByRange( - showLabel: showLabel, - startTime: startTime, - endTime: endTime, ), - ), - SizedBox( - height: 52.rpx, - ), - ], + SizedBox( + height: 52.rpx, + ), + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } List> convertToShowLabel( diff --git a/lib/pages/sleep_report/component/BreatheStandardWidget.dart b/lib/pages/sleep_report/component/BreatheStandardWidget.dart index 2eaa3c0..32ffadb 100644 --- a/lib/pages/sleep_report/component/BreatheStandardWidget.dart +++ b/lib/pages/sleep_report/component/BreatheStandardWidget.dart @@ -8,6 +8,7 @@ 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/TimeSeriesChart.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class BreatheStandardWidget extends StatefulWidget { var sleepReport; @@ -35,338 +36,336 @@ class _BreatheStandardWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { - return Container(); - } + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } - final startTime = widget.sleepReport['startTime']; - final endTime = widget.sleepReport['endTime']; - List> data = - (widget.sleepReport['brbc'] as List).cast>(); - // final dataPoints = [ - // TimeSeriesPoint(12121, 50), - // TimeSeriesPoint(1212, 120), - // TimeSeriesPoint(121, 80), - // TimeSeriesPoint(1212, 180), - // TimeSeriesPoint(1212, 30), - // TimeSeriesPoint(1212, 150), - // ]; - final dataPoints = data.map((item) { - final x = item['st'] as int; - final y = (item['value'] as num).toDouble(); // 安全地转换为 double - return TimeSeriesPoint(x, y); - }).toList(); + final startTime = widget.sleepReport['startTime']; + final endTime = widget.sleepReport['endTime']; + List> data = + (widget.sleepReport['brbc'] as List).cast>(); + final dataPoints = data.map((item) { + final x = item['st'] as int; + final y = (item['value'] as num).toDouble(); // 安全地转换为 double + return TimeSeriesPoint(x, y); + }).toList(); - List> brs = - (widget.sleepReport['brs'] as List).cast>(); - //307 平均呼吸 - //305 基准呼吸 - //308 最低呼吸 - //309 最高呼吸 - // 307 平均呼吸 - Map? avgBreath = brs.firstWhere( - (element) => element['id'] == 307, - orElse: () => {}, - ); + List> brs = + (widget.sleepReport['brs'] as List).cast>(); + //307 平均呼吸 + //305 基准呼吸 + //308 最低呼吸 + //309 最高呼吸 + // 307 平均呼吸 + Map? avgBreath = brs.firstWhere( + (element) => element['id'] == 307, + orElse: () => {}, + ); // 305 基准呼吸 - Map? baseBreath = brs.firstWhere( - (element) => element['id'] == 305, - orElse: () => {}, - ); + Map? baseBreath = brs.firstWhere( + (element) => element['id'] == 305, + orElse: () => {}, + ); // 308 最低呼吸 - Map? minBreath = brs.firstWhere( - (element) => element['id'] == 308, - orElse: () => {}, - ); + Map? minBreath = brs.firstWhere( + (element) => element['id'] == 308, + orElse: () => {}, + ); // 309 最高呼吸 - Map? maxBreath = brs.firstWhere( - (element) => element['id'] == 309, - orElse: () => {}, - ); + Map? maxBreath = brs.firstWhere( + (element) => element['id'] == 309, + orElse: () => {}, + ); - String range = baseBreath['range'] ?? ''; - int min = 0; - int max = 0; + String range = baseBreath['range'] ?? ''; + int min = 0; + int max = 0; - if (range.isNotEmpty && range.contains('~')) { - List parts = range.split('~'); - if (parts.length == 2) { - min = int.tryParse(parts[0]) ?? 0; - max = int.tryParse(parts[1]) ?? 0; + if (range.isNotEmpty && range.contains('~')) { + List parts = range.split('~'); + if (parts.length == 2) { + min = int.tryParse(parts[0]) ?? 0; + max = int.tryParse(parts[1]) ?? 0; + } } - } - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "呼吸数据介绍".tr, - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "呼吸数据介绍".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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: [ - 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( - // color: Colors.red, - width: double.infinity, - // height: 300.rpx, - child: TimeSeriesChart( - startTime: startTime, - endTime: endTime, - yMin: 50, - yMax: 150, - dataPoints: dataPoints, - actYMax: max.toDouble(), - actYMin: min.toDouble(), - ), - ), - Padding( - padding: EdgeInsetsDirectional.fromSTEB( - 30.rpx, 0.rpx, 0.rpx, 0.rpx), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + SizedBox( + height: 31.rpx, + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Column( - children: [ - Text( - "${avgBreath['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${avgBreath['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - ), - Text( - "${avgBreath['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], + // 圆形小球容器 + Container( + width: 14.rpx, // 圆球的直径 + height: 14.rpx, + decoration: BoxDecoration( + color: themeController.currentColor.sc2, // 小球的颜色 + shape: BoxShape.circle, // 设置为圆形 + ), ), - Column( - children: [ - Text( - "${baseBreath['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${baseBreath['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - "${baseBreath['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], - ), - Column( - children: [ - Text( - "${minBreath['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${minBreath['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - "${minBreath['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], - ), - Column( - children: [ - Text( - "${maxBreath['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${maxBreath['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - "${maxBreath['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], + SizedBox(width: 15.rpx), // 圆球和文字之间的间隔 + // 文字 + Text( + '正常范围'.tr + "${range}", + style: TextStyle( + fontSize: + AppConstants().smaller_text_fontSize, // 文字的大小 + color: themeController.currentColor.sc3, // 文字颜色 + ), ), ], ), - ), - ].divide(SizedBox( - height: 18.rpx, - )), + Container( + // color: Colors.red, + width: double.infinity, + // height: 300.rpx, + child: TimeSeriesChart( + startTime: startTime, + endTime: endTime, + yMin: 50, + yMax: 150, + dataPoints: dataPoints, + actYMax: max.toDouble(), + actYMin: min.toDouble(), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 0.rpx, 0.rpx), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + Text( + "${avgBreath['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${avgBreath['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + ), + Text( + "${avgBreath['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + Column( + children: [ + Text( + "${baseBreath['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${baseBreath['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + "${baseBreath['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + Column( + children: [ + Text( + "${minBreath['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${minBreath['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + "${minBreath['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + Column( + children: [ + Text( + "${maxBreath['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${maxBreath['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + "${maxBreath['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + ], + ), + ), + ].divide(SizedBox( + height: 18.rpx, + )), + ), ), - ), - ], + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/CompareSleepWidget.dart b/lib/pages/sleep_report/component/CompareSleepWidget.dart index 455ac6d..f53aa91 100644 --- a/lib/pages/sleep_report/component/CompareSleepWidget.dart +++ b/lib/pages/sleep_report/component/CompareSleepWidget.dart @@ -8,6 +8,7 @@ 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/SleepRadarChart.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class CompareSleepWidget extends StatefulWidget { var sleepReport; @@ -35,29 +36,15 @@ class _CompareSleepWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || + try { + if (widget.sleepReport == null || widget.sleepReport['yc'] == null || widget.sleepReport['yc'].isEmpty) { return Container(); } List> data = (widget.sleepReport['yc'] as List) .map((e) => e as Map) - .toList(); - - // var today = { - // "type1": 40.0, - // "type2": 80.0, - // "type3": 60.0, - // "type4": 70.0, - // "type5": 100.0 - // }; - // var yesterday = { - // "type1": 40.0, - // "type2": 90.0, - // "type3": 50.0, - // "type4": 70.0, - // "type5": 30.0 - // }; + .toList(); Map today = {}; Map yesterday = {}; @@ -210,5 +197,10 @@ class _CompareSleepWidgetState extends State { ), ), ); + + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/DiseasePercentsWidget.dart b/lib/pages/sleep_report/component/DiseasePercentsWidget.dart index d8ac881..1f51c0c 100644 --- a/lib/pages/sleep_report/component/DiseasePercentsWidget.dart +++ b/lib/pages/sleep_report/component/DiseasePercentsWidget.dart @@ -7,6 +7,7 @@ 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/HorizontalBarChart.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class DiseasePercentsWidget extends StatefulWidget { var sleepReport; @@ -17,51 +18,7 @@ class DiseasePercentsWidget extends StatefulWidget { } class _DiseasePercentsWidgetState extends State { - // var showLabel = [ - // { - // "key": 1, - // "name": "心脏病", - // "color": stringToColor("#00C1AA"), - // "percent": 45, - // "explain": "心脏病是指心脏的结构或功能异常,可能导致心脏无法有效地泵血。" - // }, - // { - // "key": 2, - // "name": "高血压", - // "color": stringToColor("#00C1AA"), - // "percent": 32, - // "explain": "高血压是指血液在动脉中流动时对血管壁施加的压力过高。" - // }, - // { - // "key": 3, - // "name": "糖尿病", - // "color": stringToColor("#00C1AA"), - // "percent": 50, - // "explain": "糖尿病是一种代谢性疾病,导致血糖水平异常升高。" - // }, - // { - // "key": 4, - // "name": "甲亢", - // "color": stringToColor("#FF7159"), - // "percent": 80, - // "explain": "甲亢是指甲状腺分泌过多的甲状腺激素,导致新陈代谢加速。" - // }, - // { - // "key": 5, - // "name": "消化系统", - // "color": stringToColor("#00C1AA"), - // "percent": 12, - // "explain": "消化系统是身体中处理食物的机构,是造成疾病和疾病症状的来源。", - // }, - // { - // "key": 6, - // "name": "呼吸系统", - // "color": stringToColor("#00C1AA"), - // "percent": 62, - // "explain": "呼吸系统是负责气体交换的器官系统,包括鼻、喉、气管和肺等。", - // }, - // ]; - + @override void setState(VoidCallback callback) { super.setState(callback); @@ -79,7 +36,8 @@ class _DiseasePercentsWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || + try { + if (widget.sleepReport == null || widget.sleepReport['cdri'] == null || widget.sleepReport['cdri'].isEmpty) { return Container(); @@ -157,6 +115,11 @@ class _DiseasePercentsWidgetState extends State { ), ), ); + + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } List> convertDiseaseData(List data) { diff --git a/lib/pages/sleep_report/component/HeartChangeWidget.dart b/lib/pages/sleep_report/component/HeartChangeWidget.dart index 57a0423..7e4b92c 100644 --- a/lib/pages/sleep_report/component/HeartChangeWidget.dart +++ b/lib/pages/sleep_report/component/HeartChangeWidget.dart @@ -7,6 +7,7 @@ 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/DataShowWidget.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class HeartChangeWidget extends StatefulWidget { var sleepReport; @@ -34,293 +35,235 @@ class _HeartChangeWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport['hrvs'] == null || - widget.sleepReport['hrvs'].isEmpty) { + try { + if (widget.sleepReport == null || + widget.sleepReport['hrvs'] == null || + widget.sleepReport['hrvs'].isEmpty) { + return Container(); + } + + List dataList = widget.sleepReport['hrvs']; + + List> data = transformHrvData(dataList); + 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( + "心率变异性(HRV)".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "心率变异性(HRV)介绍".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + ); + }, + 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( + "名称", + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget2: Text( + "测量值", + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget3: Text( + "参考范围", + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + widget4: Text( + "趋势", + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + ), + ), + Column( + children: data.map((data) { + return DataShowWidget( + alignment: MainAxisAlignment.center, + widget1: Row( + children: [ + 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); + showTipDialog( + context, + Container( + child: Text( + '${data['desc']}', + style: TextStyle( + fontSize: 26.rpx, + color: + themeController.currentColor.sc3, + ), + ), + ), + ); + }, + child: SizedBox( + width: 17.rpx, + height: 17.rpx, + child: SvgPicture.asset( + 'assets/img/icon/explain.svg', + fit: BoxFit.cover, + color: Colors.white, + ), + ), + ), + ], + ), + widget2: Text( + '${data['value']}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + 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 dataList = widget.sleepReport['hrvs']; - //0上升 1下降 2持平 - // List data = [ - // { - // "name": "心脏总能量", - // "value": 5262, - // "range": "2055-6000", - // "change": 0, - // "desc": "心脏总能量介绍" - // }, - // { - // "name": "心率减速力", - // "value": 5262, - // "range": "2055-6000", - // "change": 1, - // "desc": "心率减速力介绍" - // }, - // { - // "name": "迷走神经张力指数", - // "value": 5262, - // "range": "2055-6000", - // "change": 2, - // "desc": "迷走神经张力指数介绍" - // }, - // { - // "name": "交感神经张力指数", - // "value": 5262, - // "range": "2055-6000", - // "change": 0, - // "desc": "交感神经张力指数介绍" - // }, - // { - // "name": "自主神经张力指数", - // "value": 5262, - // "range": "2055-6000", - // "change": 2, - // "desc": "自主神经张力指数介绍" - // }, - // { - // "name": "血管舒张指数", - // "value": 5262, - // "range": "2055-6000", - // "change": 1, - // "desc": "血管舒张指数介绍" - // }, - // { - // "name": "SDNN", - // "value": 5262, - // "range": "2055-6000", - // "change": 0, - // "desc": "SDNN介绍" - // }, - // { - // "name": "PNN50", - // "value": 5262, - // "range": "2055-6000", - // "change": 1, - // "desc": "PNN50介绍" - // }, - // { - // "name": "RMSSD", - // "value": 5262, - // "range": "2055-6000", - // "change": 2, - // "desc": "RMSSD介绍" - // }, - // ]; - List> data = transformHrvData(dataList); - 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( - "心率变异性(HRV)".tr, - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: AppConstants().title_text_fontSize), - ), - ClickableContainer( - backgroundColor: Colors.transparent, - highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 - padding: EdgeInsetsDirectional.fromSTEB( - 14.rpx, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "心率变异性(HRV)介绍".tr, - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, - ), - ), - ), - ); - }, - 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( - "名称", - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: AppConstants().normal_text_fontSize, - ), - ), - widget2: Text( - "测量值", - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: AppConstants().normal_text_fontSize, - ), - ), - widget3: Text( - "参考范围", - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: AppConstants().normal_text_fontSize, - ), - ), - widget4: Text( - "趋势", - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: AppConstants().normal_text_fontSize, - ), - ), - ), - Column( - children: data.map((data) { - return DataShowWidget( - alignment: MainAxisAlignment.center, - widget1: Row( - children: [ - 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); - showTipDialog( - context, - Container( - child: Text( - '${data['desc']}', - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, - ), - ), - ), - ); - }, - child: SizedBox( - width: 17.rpx, - height: 17.rpx, - child: SvgPicture.asset( - 'assets/img/icon/explain.svg', - fit: BoxFit.cover, - color: Colors.white, - ), - ), - ), - ], - ), - widget2: Text( - '${data['value']}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - 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(), - ), - ], - ), - ), - ], - ), - ), - ); } List> transformHrvData(List originalData) { diff --git a/lib/pages/sleep_report/component/HeartHealthWidget.dart b/lib/pages/sleep_report/component/HeartHealthWidget.dart index 8e2b42c..59cbda0 100644 --- a/lib/pages/sleep_report/component/HeartHealthWidget.dart +++ b/lib/pages/sleep_report/component/HeartHealthWidget.dart @@ -8,6 +8,7 @@ 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/FatigueCircleIndicator.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class HeartHealthWidget extends StatefulWidget { var sleepReport; @@ -35,98 +36,104 @@ class _HeartHealthWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport['mha'] == null || - widget.sleepReport['mha'].isEmpty) { - return Container(); - } + try { + if (widget.sleepReport == null || + widget.sleepReport['mha'] == null || + widget.sleepReport['mha'].isEmpty) { + return Container(); + } - List data = widget.sleepReport['mha']; - var showLabel = convertMentalHealthData(data); - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "心理健康评估介绍".tr, - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + List data = widget.sleepReport['mha']; + var showLabel = convertMentalHealthData(data); + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "心理健康评估介绍".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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: [ - FatigueCircleIndicator( - data: showLabel[0], - ), - FatigueCircleIndicator( - data: showLabel[1], - ), - ].divide(SizedBox( - width: 110.rpx, - )), + SizedBox( + height: 104.rpx, ), - ), - SizedBox( - height: 72.rpx, - ), - ], + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FatigueCircleIndicator( + data: showLabel[0], + ), + 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) { @@ -136,7 +143,7 @@ class _HeartHealthWidgetState extends State { final String explain = (item['tips'] != null && (item['tips'] as String).trim().isNotEmpty) ? item['tips'] - : '高风险'; + : '未知数据'.tr; return { 'name': item['name'], @@ -144,7 +151,7 @@ class _HeartHealthWidgetState extends State { ? stringToColor(colorStr) : stringToColor("#00C1AA"), // 默认红色 'percent': value, - 'explain': explain, + 'explain': item['level'], "bottomColor": Colors.grey, }; }).toList(); diff --git a/lib/pages/sleep_report/component/HeartPointWidget.dart b/lib/pages/sleep_report/component/HeartPointWidget.dart index f381056..64e620f 100644 --- a/lib/pages/sleep_report/component/HeartPointWidget.dart +++ b/lib/pages/sleep_report/component/HeartPointWidget.dart @@ -10,6 +10,7 @@ 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/ScatterPlotChart.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class HeartPointWidget extends StatefulWidget { var sleepReport; @@ -37,125 +38,120 @@ class _HeartPointWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport['hrsp'] == null || - widget.sleepReport['hrsp'].isEmpty) { - return Container(); - } - // List rawData = widget.sleepReport['hrsp']; - // List data = List.generate(200, (index) { - // // 随机生成 x 和 y 值,范围都在 0-1400 之间 - // double x = Random().nextDouble() * 1400; // x 值在 0-1400 范围 - // double y = Random().nextDouble() * 1400; // y 值也在 0-1400 范围 - - // // 返回 ScatterSpot,使用圆点绘制器自定义大小和颜色 - // return ScatterSpot( - // x, - // y, - // ); - // }); - double maxX = 0; - double maxY = 0; - - List data = []; - List rawData = widget.sleepReport['hrsp']; try { - data = rawData.map((item) { - double x = (item['st'] ?? 0).toDouble(); - double y = (item['value'] ?? 0).toDouble(); - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - return ScatterSpot(x, y); - }).toList(); - } catch (e) { - print(e); - } + if (widget.sleepReport == null || + widget.sleepReport['hrsp'] == null || + widget.sleepReport['hrsp'].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: [ - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "心率散点图介绍".tr, - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + double maxX = 0; + double maxY = 0; + + List data = []; + List rawData = widget.sleepReport['hrsp']; + try { + data = rawData.map((item) { + double x = (item['st'] ?? 0).toDouble(); + double y = (item['value'] ?? 0).toDouble(); + 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: [ + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "心率散点图介绍".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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( + 30.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.toInt(), // x轴最大值 + yMax: maxY.toInt(), // y轴最大值 + pointColor: stringToColor("#00C1AA"), // 点的颜色 + divisions: 7, // 刻度分割数量 ), - ], - ), - ), - SizedBox( - height: 31.rpx, - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(30.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.toInt(), // x轴最大值 - yMax: maxY.toInt(), // y轴最大值 - pointColor: stringToColor("#00C1AA"), // 点的颜色 - divisions: 7, // 刻度分割数量 ), ), - ), - // SizedBox( - // height: 31.rpx, - // ), - ], + // SizedBox( + // height: 31.rpx, + // ), + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/HeartRateCard.dart b/lib/pages/sleep_report/component/HeartRateCard.dart index 753190e..2da6d13 100644 --- a/lib/pages/sleep_report/component/HeartRateCard.dart +++ b/lib/pages/sleep_report/component/HeartRateCard.dart @@ -3,68 +3,138 @@ 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/home_page/SleepDataModuleWidget.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class HeartRateCard extends StatefulWidget { var sleepReport; - HeartRateCard({super.key, required this.sleepReport}); + final int? highlightItem; + HeartRateCard({super.key, required this.sleepReport, this.highlightItem}); @override State createState() => _HeartRateCardState(); } -class _HeartRateCardState extends State { - @override - void setState(VoidCallback callback) { - super.setState(callback); - } +class _HeartRateCardState extends State with TickerProviderStateMixin { + final GlobalKey _highlightKey = GlobalKey(); + AnimationController? _animationController; + bool _shouldAnimate = false; + int? _highlightedId; + int _flashCount = 0; // 用于跟踪闪烁次数 @override void initState() { super.initState(); + if (widget.highlightItem != null) { + _highlightedId = widget.highlightItem; + _shouldAnimate = true; + _initAnimation(); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.highlightItem != null && _highlightKey.currentContext != null) { + Scrollable.ensureVisible( + _highlightKey.currentContext!, + duration: Duration(milliseconds: 500), + curve: Curves.easeInOut, + alignment: 0.3, + ); + } + }); + } + + void _initAnimation() { + _animationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 300), + )..addStatusListener((status) { + if (status == AnimationStatus.completed) { + // 正向动画完成,开始反向动画 + _animationController!.reverse(); + } else if (status == AnimationStatus.dismissed) { + // 反向动画完成,增加计数 + _flashCount++; + // 闪烁3次后停止 + if (_flashCount >= 5) { + _animationController!.dispose(); + setState(() { + _shouldAnimate = false; + _highlightedId = null; + }); + } else { + // 继续下一次闪烁 + _animationController!.forward(); + } + } + }); + + _animationController!.forward(); } @override void dispose() { + _animationController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } + + List data = widget.sleepReport['hrs'] ?? []; + + 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: Wrap( + spacing: 23.rpx, + runSpacing: 25.rpx, + children: List.generate(data.length, (index) { + final item = data[index]; + item['showTip'] = true; + final bool isHighlighted = _shouldAnimate && + item['id'] == _highlightedId; + + return SizedBox( + width: (MediaQuery.of(context).size.width - 160.rpx) / 3, + child: AnimatedBuilder( + animation: _animationController ?? AlwaysStoppedAnimation(0), + builder: (context, child) { + return Container( + key: isHighlighted ? _highlightKey : null, + decoration: isHighlighted + ? BoxDecoration( + border: Border.all( + color: themeController.currentColor.sc2 + .withOpacity(_animationController?.value ?? 0), + width: 1.rpx, + ), + borderRadius: BorderRadius.circular(8), + ) + : null, + child: SleepDataModuleWidget(data: item), + ); + }, + ), + ); + }), + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("心率监测绘制异常${e}"); return Container(); } - - List data = widget.sleepReport['hrs'] ?? []; - - 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: Wrap( - spacing: 23.rpx, // 横向间距(左右间距如需加可设置) - runSpacing: 25.rpx, // 每行之间的垂直间距 - children: List.generate(data.length, (index) { - final item = data[index]; - item['showTip'] = true; - return SizedBox( - width: (MediaQuery.of(context).size.width - 160.rpx) / 3, - child: SleepDataModuleWidget(data: item), - // child: Container( - // width: 20, - // height: 20, - // color: Colors.red, - // ), - ); - }), - ), - ), - ); } -} +} \ No newline at end of file diff --git a/lib/pages/sleep_report/component/HeartRateStandardWidget.dart b/lib/pages/sleep_report/component/HeartRateStandardWidget.dart index daa38d1..159fc8a 100644 --- a/lib/pages/sleep_report/component/HeartRateStandardWidget.dart +++ b/lib/pages/sleep_report/component/HeartRateStandardWidget.dart @@ -8,6 +8,7 @@ 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/TimeSeriesChart.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class HeartRateStandardWidget extends StatefulWidget { var sleepReport; @@ -36,340 +37,346 @@ class _HeartRateStandardWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { - return Container(); - } + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } - final startTime = widget.sleepReport['startTime']; - final endTime = widget.sleepReport['endTime']; - List> data = - (widget.sleepReport['hrbc'] as List).cast>(); - final dataPoints = data.map((item) { - final x = item['st'] as int; - final y = (item['value'] as num).toDouble(); // 安全地转换为 double - return TimeSeriesPoint(x, y); - }).toList(); + final startTime = widget.sleepReport['startTime']; + final endTime = widget.sleepReport['endTime']; + List> data = + (widget.sleepReport['hrbc'] as List).cast>(); + final dataPoints = data.map((item) { + final x = item['st'] as int; + final y = (item['value'] as num).toDouble(); // 安全地转换为 double + return TimeSeriesPoint(x, y); + }).toList(); - List> hrs = - (widget.sleepReport['hrs'] as List).cast>(); - //206 平均心率 - //202 基准心率 - //207 最低心率 - //208 最高心率 - // 找 id == 206 的元素(平均心率) - Map? avgHeartRate = hrs.firstWhere( - (element) => element['id'] == 206, - orElse: () => {}, - ); + List> hrs = + (widget.sleepReport['hrs'] as List).cast>(); + //206 平均心率 + //202 基准心率 + //207 最低心率 + //208 最高心率 + // 找 id == 206 的元素(平均心率) + Map? avgHeartRate = hrs.firstWhere( + (element) => element['id'] == 206, + orElse: () => {}, + ); // 找 id == 202 的元素(基准心率) - Map? baseHeartRate = hrs.firstWhere( - (element) => element['id'] == 202, - orElse: () => {}, - ); + Map? baseHeartRate = hrs.firstWhere( + (element) => element['id'] == 202, + orElse: () => {}, + ); // 找 id == 207 的元素(最低心率) - Map? minHeartRate = hrs.firstWhere( - (element) => element['id'] == 207, - orElse: () => {}, - ); + Map? minHeartRate = hrs.firstWhere( + (element) => element['id'] == 207, + orElse: () => {}, + ); // 找 id == 208 的元素(最高心率) - Map? maxHeartRate = hrs.firstWhere( - (element) => element['id'] == 208, - orElse: () => {}, - ); - String range = baseHeartRate['range'] ?? ''; - int min = 0; - int max = 0; + Map? maxHeartRate = hrs.firstWhere( + (element) => element['id'] == 208, + orElse: () => {}, + ); + String range = baseHeartRate['range'] ?? ''; + int min = 0; + int max = 0; - if (range.isNotEmpty && range.contains('~')) { - List parts = range.split('~'); - if (parts.length == 2) { - min = int.tryParse(parts[0]) ?? 0; - max = int.tryParse(parts[1]) ?? 0; + if (range.isNotEmpty && range.contains('~')) { + List parts = range.split('~'); + if (parts.length == 2) { + min = int.tryParse(parts[0]) ?? 0; + max = int.tryParse(parts[1]) ?? 0; + } } - } - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "心率数据介绍".tr, - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "心率数据介绍".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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: [ - 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( - // // color: Colors.red, - // width: double.infinity, - // // height: 300.rpx, - // child: TimeSeriesChart( - // startTime: startTime, - // endTime: endTime, - // yMin: min.toDouble(), - // yMax: max.toDouble(), - // dataPoints: dataPoints, - // ), - // ), - Container( - // color: Colors.red, - width: double.infinity, - // height: 300.rpx, - child: TimeSeriesChart( - startTime: startTime, - endTime: endTime, - yMin: 50, - yMax: 150, - dataPoints: dataPoints, - actYMin: min.toDouble(), - actYMax: max.toDouble(), - ), - ), - Padding( - padding: EdgeInsetsDirectional.fromSTEB( - 30.rpx, 0.rpx, 0.rpx, 0.rpx), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + SizedBox( + height: 31.rpx, + ), + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Column( - children: [ - Text( - "${avgHeartRate['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${avgHeartRate['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - ), - Text( - "${avgHeartRate['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], + // 圆形小球容器 + Container( + width: 14.rpx, // 圆球的直径 + height: 14.rpx, + decoration: BoxDecoration( + color: themeController.currentColor.sc2, // 小球的颜色 + shape: BoxShape.circle, // 设置为圆形 + ), ), - Column( - children: [ - Text( - "${baseHeartRate['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${baseHeartRate['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - "${baseHeartRate['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], - ), - Column( - children: [ - Text( - "${minHeartRate['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${minHeartRate['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - "${minHeartRate['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], - ), - Column( - children: [ - Text( - "${maxHeartRate['name']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "${maxHeartRate['value']}", - style: TextStyle( - color: themeController.currentColor.sc2, - fontSize: - AppConstants().normal_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - "${maxHeartRate['unit']}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: - AppConstants().small_text_fontSize), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ].divide(SizedBox( - width: 6.rpx, - )), - ), - ], + SizedBox(width: 15.rpx), // 圆球和文字之间的间隔 + // 文字 + Text( + '正常范围'.tr + "${range}", + style: TextStyle( + fontSize: + AppConstants().smaller_text_fontSize, // 文字的大小 + color: themeController.currentColor.sc3, // 文字颜色 + ), ), ], ), - ), - ].divide(SizedBox( - height: 18.rpx, - )), + // Container( + // // color: Colors.red, + // width: double.infinity, + // // height: 300.rpx, + // child: TimeSeriesChart( + // startTime: startTime, + // endTime: endTime, + // yMin: min.toDouble(), + // yMax: max.toDouble(), + // dataPoints: dataPoints, + // ), + // ), + Container( + // color: Colors.red, + width: double.infinity, + // height: 300.rpx, + child: TimeSeriesChart( + startTime: startTime, + endTime: endTime, + yMin: 50, + yMax: 150, + dataPoints: dataPoints, + actYMin: min.toDouble(), + actYMax: max.toDouble(), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 0.rpx, 0.rpx), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + Text( + "${avgHeartRate['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${avgHeartRate['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + ), + Text( + "${avgHeartRate['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + Column( + children: [ + Text( + "${baseHeartRate['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${baseHeartRate['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + "${baseHeartRate['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + Column( + children: [ + Text( + "${minHeartRate['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${minHeartRate['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + "${minHeartRate['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + Column( + children: [ + Text( + "${maxHeartRate['name']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${maxHeartRate['value']}", + style: TextStyle( + color: themeController.currentColor.sc2, + fontSize: AppConstants() + .normal_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + "${maxHeartRate['unit']}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().small_text_fontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ].divide(SizedBox( + width: 6.rpx, + )), + ), + ], + ), + ], + ), + ), + ].divide(SizedBox( + height: 18.rpx, + )), + ), ), - ), - ], + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/HrvWidget.dart b/lib/pages/sleep_report/component/HrvWidget.dart index 338768f..020f893 100644 --- a/lib/pages/sleep_report/component/HrvWidget.dart +++ b/lib/pages/sleep_report/component/HrvWidget.dart @@ -7,6 +7,7 @@ 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/StatusBarWithIndicator.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class HrvWidget extends StatefulWidget { HrvWidget({super.key}); @@ -33,85 +34,91 @@ class _HrvWidgetState extends State { @override Widget build(BuildContext context) { - 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( - "心率变异性(HRV)".tr, - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: AppConstants().title_text_fontSize), - ), - ClickableContainer( - backgroundColor: Colors.transparent, - highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 - padding: EdgeInsetsDirectional.fromSTEB( - 14.rpx, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "心率变异性(HRV)介绍。", - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + try { + 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( + "心率变异性(HRV)".tr, + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().title_text_fontSize), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: Colors.white, // 或设置为你需要的水波纹颜色 + padding: EdgeInsetsDirectional.fromSTEB( + 14.rpx, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "心率变异性(HRV)介绍。", + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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, - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx), - child: StatusBarWithIndicator( - selectKey: 2, - showLabel: [ - {"key": 1, "name": "正常", "color": Color(0xFF4CAF50)}, - {"key": 2, "name": "一般", "color": Color(0xFF8BC34A)}, - {"key": 3, "name": "注意", "color": Color(0xFFFFC107)}, - {"key": 4, "name": "警告", "color": Color(0xFFF44336)}, - ], + SizedBox( + height: 83.rpx, ), - ), - SizedBox( - height: 56.rpx, - ), - ], + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + child: StatusBarWithIndicator( + selectKey: 2, + showLabel: [ + {"key": 1, "name": "正常", "color": Color(0xFF4CAF50)}, + {"key": 2, "name": "一般", "color": Color(0xFF8BC34A)}, + {"key": 3, "name": "注意", "color": Color(0xFFFFC107)}, + {"key": 4, "name": "警告", "color": Color(0xFFF44336)}, + ], + ), + ), + SizedBox( + height: 56.rpx, + ), + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/SkinPercentWidget.dart b/lib/pages/sleep_report/component/SkinPercentWidget.dart index 1196741..f7b3fee 100644 --- a/lib/pages/sleep_report/component/SkinPercentWidget.dart +++ b/lib/pages/sleep_report/component/SkinPercentWidget.dart @@ -7,6 +7,7 @@ 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/StatusBarWithIndicator.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class SkinPercentWidget extends StatefulWidget { var sleepReport; @@ -34,7 +35,8 @@ class _SkinPercentWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || + try { + if (widget.sleepReport == null || widget.sleepReport['sicp'] == null || widget.sleepReport['sicp'].isEmpty) { return Container(); @@ -134,5 +136,10 @@ class _SkinPercentWidgetState extends State { ), ), ); + + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/SleepCard.dart b/lib/pages/sleep_report/component/SleepCard.dart index 1dc905b..05bff0a 100644 --- a/lib/pages/sleep_report/component/SleepCard.dart +++ b/lib/pages/sleep_report/component/SleepCard.dart @@ -3,68 +3,139 @@ 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/home_page/SleepDataModuleWidget.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class SleepCard extends StatefulWidget { - var sleepReport; - SleepCard({super.key, required this.sleepReport}); + final dynamic sleepReport; + final int? highlightItem; + + SleepCard({super.key, required this.sleepReport, this.highlightItem}); @override State createState() => _SleepCardState(); } -class _SleepCardState extends State { - @override - void setState(VoidCallback callback) { - super.setState(callback); - } +class _SleepCardState extends State with TickerProviderStateMixin { + final GlobalKey _highlightKey = GlobalKey(); + AnimationController? _animationController; + bool _shouldAnimate = false; + int? _highlightedId; + int _flashCount = 0; // 用于跟踪闪烁次数 @override void initState() { super.initState(); + if (widget.highlightItem != null) { + _highlightedId = widget.highlightItem; + _shouldAnimate = true; + _initAnimation(); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.highlightItem != null && _highlightKey.currentContext != null) { + Scrollable.ensureVisible( + _highlightKey.currentContext!, + duration: Duration(milliseconds: 500), + curve: Curves.easeInOut, + alignment: 0.3, + ); + } + }); + } + + void _initAnimation() { + _animationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 300), + )..addStatusListener((status) { + if (status == AnimationStatus.completed) { + // 正向动画完成,开始反向动画 + _animationController!.reverse(); + } else if (status == AnimationStatus.dismissed) { + // 反向动画完成,增加计数 + _flashCount++; + // 闪烁3次后停止 + if (_flashCount >= 5) { + _animationController!.dispose(); + setState(() { + _shouldAnimate = false; + _highlightedId = null; + }); + } else { + // 继续下一次闪烁 + _animationController!.forward(); + } + } + }); + + _animationController!.forward(); } @override void dispose() { + _animationController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } + + List data = widget.sleepReport['bs'] ?? []; + + 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: Wrap( + spacing: 23.rpx, + runSpacing: 25.rpx, + children: List.generate(data.length, (index) { + final item = data[index]; + item['showTip'] = true; + final bool isHighlighted = _shouldAnimate && + item['id'] == _highlightedId; + + return SizedBox( + width: (MediaQuery.of(context).size.width - 160.rpx) / 3, + child: AnimatedBuilder( + animation: _animationController ?? AlwaysStoppedAnimation(0), + builder: (context, child) { + return Container( + key: isHighlighted ? _highlightKey : null, + decoration: isHighlighted + ? BoxDecoration( + border: Border.all( + color: themeController.currentColor.sc2 + .withOpacity(_animationController?.value ?? 0), + width: 1.rpx, + ), + borderRadius: BorderRadius.circular(8), + ) + : null, + child: SleepDataModuleWidget(data: item), + ); + }, + ), + ); + }), + ), + ), + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); return Container(); } - - List data = widget.sleepReport['bs'] ?? []; - - 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: Wrap( - spacing: 23.rpx, // 横向间距(左右间距如需加可设置) - runSpacing: 25.rpx, // 每行之间的垂直间距 - children: List.generate(data.length, (index) { - final item = data[index]; - item['showTip'] = true; - return SizedBox( - width: (MediaQuery.of(context).size.width - 160.rpx) / 3, - child: SleepDataModuleWidget(data: item), - // child: Container( - // width: 20, - // height: 20, - // color: Colors.red, - // ), - ); - }), - ), - ), - ); } -} +} \ No newline at end of file diff --git a/lib/pages/sleep_report/component/SleepScoreWidget.dart b/lib/pages/sleep_report/component/SleepScoreWidget.dart index dbf9d6c..32c4479 100644 --- a/lib/pages/sleep_report/component/SleepScoreWidget.dart +++ b/lib/pages/sleep_report/component/SleepScoreWidget.dart @@ -5,6 +5,7 @@ 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/pages/sleep_report/chart/SegmentedCirclePainter.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class SleepScoreWidget extends StatefulWidget { var sleepReport; @@ -32,17 +33,13 @@ class _SleepScoreWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || + try { + if (widget.sleepReport == null || widget.sleepReport is! Map || widget.sleepReport.isEmpty) { return Container(); } - // List> showLabel = [ - // {"level": 1, "name": "优秀(≥85)", "color": Color(0xFF4CAF50)}, - // {"level": 2, "name": "良好(75~84)", "color": Color(0xFF8BC34A)}, - // {"level": 3, "name": "合格(60~74)", "color": Color(0xFFFFC107)}, - // {"level": 4, "name": "注意(<60)", "color": Color(0xFFF44336)}, - // ]; + List showLabel = widget.sleepReport['score']['type']; List stages = widget.sleepReport['score']['stages']; List segments = parseSegments(widget.sleepReport); @@ -191,6 +188,11 @@ class _SleepScoreWidgetState extends State { ), ), ); + + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } //获取睡眠等级 diff --git a/lib/pages/sleep_report/component/SleepView.dart b/lib/pages/sleep_report/component/SleepView.dart index 6b97e9b..c71e906 100644 --- a/lib/pages/sleep_report/component/SleepView.dart +++ b/lib/pages/sleep_report/component/SleepView.dart @@ -8,6 +8,7 @@ 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/GradientLine.dart'; import 'package:vbvs_app/pages/sleep_report/chart/SnoreWaveform.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; //睡眠规律性 class SleepViewWidget extends StatefulWidget { @@ -36,183 +37,126 @@ class _SleepViewWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { - return Container(); - } + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } - // List showLabel = widget.sleepReport['sleepData']['type']; - List showLabel = widget.sleepReport['sleepData']['type'] - .where((item) => item['show'] != false) - .toList(); + // List showLabel = widget.sleepReport['sleepData']['type']; + List showLabel = widget.sleepReport['sleepData']['type'] + .where((item) => item['show'] != false) + .toList(); - - List snoreValues = widget.sleepReport['ssp']; - Map time = MyUtils.diffHoursMinutesMap( - widget.sleepReport['startTime'], widget.sleepReport['endTime']); - int hour = time['hours']; - int minutes = time['minutes']; - - List stages = widget.sleepReport['sleepData']['stages']; - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, + List snoreValues = []; + List lightSnore = widget.sleepReport['ssp']['data'][0]; + List heavySnore = widget.sleepReport['ssp']['data'][1]; + + + snoreValues = [...lightSnore, ...heavySnore]; + + + snoreValues.sort((a, b) { + return a['st'].compareTo(b['st']); + }); + Map time = MyUtils.diffHoursMinutesMap( + widget.sleepReport['startTime'], widget.sleepReport['endTime']); + int hour = time['hours']; + int minutes = time['minutes']; + + List stages = widget.sleepReport['sleepData']['stages']; + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "睡眠规律性介绍。", + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), + ), + ), + ); + }, + 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, + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // 左侧 - 入睡时间 + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + "assets/img/moon.png", + width: 43.rpx, + height: 43.rpx, + fit: BoxFit.cover, + ), + SizedBox(height: 33.rpx), Container( + height: 40.rpx, child: Text( - "睡眠规律性介绍。", + "${MyUtils.formatToHHmm(widget.sleepReport['startTime'])}", style: TextStyle( - fontSize: 26.rpx, color: themeController.currentColor.sc3, + fontSize: AppConstants().normal_text_fontSize, ), ), ), - ); - }, - 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, - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // 左侧 - 入睡时间 - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - "assets/img/moon.png", - width: 43.rpx, - height: 43.rpx, - fit: BoxFit.cover, - ), - SizedBox(height: 33.rpx), - Container( - height: 40.rpx, - child: Text( - "${MyUtils.formatToHHmm(widget.sleepReport['startTime'])}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: AppConstants().normal_text_fontSize, - ), - ), - ), - SizedBox(height: 20.rpx), - Text( - "入睡时间".tr, - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: AppConstants().normal_text_fontSize, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - - // 中间 - 渐变线 + 时间 - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 43.rpx, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GradientLine( - height: 1, - color: Colors.white60, - ), - ], - )), - SizedBox(height: 33.rpx), // 与图标对齐 - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "$hour", - style: TextStyle( - color: themeController - .currentColor.sc2, // 小时数字颜色 - fontSize: 36.rpx, // 小时数字字号 - ), - ), - TextSpan( - text: "小时", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: AppConstants().title_text_fontSize, - ), - ), - TextSpan( - text: "$minutes", - style: TextStyle( - color: themeController - .currentColor.sc2, // 小时数字颜色 - fontSize: 36.rpx, // 分钟数字字号 - ), - ), - TextSpan( - text: "分钟", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: AppConstants().title_text_fontSize, - ), - ), - ], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), SizedBox(height: 20.rpx), Text( - "睡眠时长".tr, + "入睡时间".tr, style: TextStyle( color: themeController.currentColor.sc4, fontSize: AppConstants().normal_text_fontSize, @@ -222,111 +166,170 @@ class _SleepViewWidgetState extends State { ), ], ), - ), - // 右侧 - 起床时间图标 - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - "assets/img/sun.png", - width: 43.rpx, - height: 43.rpx, - fit: BoxFit.cover, + // 中间 - 渐变线 + 时间 + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 43.rpx, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GradientLine( + height: 1, + color: Colors.white60, + ), + ], + )), + SizedBox(height: 33.rpx), // 与图标对齐 + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "$hour", + style: TextStyle( + color: themeController + .currentColor.sc2, // 小时数字颜色 + fontSize: 36.rpx, // 小时数字字号 + ), + ), + TextSpan( + text: "小时", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().title_text_fontSize, + ), + ), + TextSpan( + text: "$minutes", + style: TextStyle( + color: themeController + .currentColor.sc2, // 小时数字颜色 + fontSize: 36.rpx, // 分钟数字字号 + ), + ), + TextSpan( + text: "分钟", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: + AppConstants().title_text_fontSize, + ), + ), + ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 20.rpx), + Text( + "睡眠时长".tr, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), - SizedBox(height: 33.rpx), - Container( - height: 40.rpx, - child: Text( - "${MyUtils.formatToHHmm(widget.sleepReport['endTime'])}", - style: TextStyle( - color: themeController.currentColor.sc3, - fontSize: AppConstants().normal_text_fontSize, + ), + + // 右侧 - 起床时间图标 + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + "assets/img/sun.png", + width: 43.rpx, + height: 43.rpx, + fit: BoxFit.cover, + ), + SizedBox(height: 33.rpx), + Container( + height: 40.rpx, + child: Text( + "${MyUtils.formatToHHmm(widget.sleepReport['endTime'])}", + style: TextStyle( + color: themeController.currentColor.sc3, + fontSize: AppConstants().normal_text_fontSize, + ), ), ), - ), - SizedBox(height: 20.rpx), - Text( - "起床时间".tr, - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: AppConstants().normal_text_fontSize, + SizedBox(height: 20.rpx), + Text( + "起床时间".tr, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: AppConstants().normal_text_fontSize, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - SizedBox( - height: 49.rpx, - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(26.rpx, 0.rpx, 26.rpx, 0.rpx), - child: SnoreChartContainer( - snoreValues: snoreValues, - barData: stages, - startTime: widget.sleepReport['startTime'], - endTime: widget.sleepReport['endTime'], - showLabel: showLabel, + SizedBox( + height: 49.rpx, ), - ), - SizedBox( - height: 70.rpx, - ), - Wrap( - spacing: 55.rpx, - runSpacing: 20.rpx, - children: showLabel.map((item) { - return Container( - padding: EdgeInsets.all(5.rpx), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 20.rpx, - height: 20.rpx, - decoration: BoxDecoration( - color: item["color"] == null || item["color"] == "" - ? Colors.transparent - : stringToColor(item["color"]), - borderRadius: BorderRadius.circular(10.rpx), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 26.rpx, 0.rpx, 26.rpx, 0.rpx), + child: SnoreChartContainer( + snoreValues: snoreValues, + barData: stages, + startTime: widget.sleepReport['startTime'], + endTime: widget.sleepReport['endTime'], + showLabel: showLabel, + ), + ), + SizedBox( + height: 70.rpx, + ), + Wrap( + spacing: 55.rpx, + runSpacing: 20.rpx, + children: showLabel.map((item) { + return Container( + padding: EdgeInsets.all(5.rpx), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 20.rpx, + height: 20.rpx, + decoration: BoxDecoration( + color: item["color"] == null || item["color"] == "" + ? Colors.transparent + : stringToColor(item["color"]), + borderRadius: BorderRadius.circular(10.rpx), + ), ), - ), - SizedBox(width: 17.rpx), - Text( - item["name"], - style: TextStyle( - color: Colors.white, - fontSize: 24.rpx, + SizedBox(width: 17.rpx), + Text( + item["name"], + style: TextStyle( + color: Colors.white, + fontSize: 24.rpx, + ), ), - ), - ], - ), - ); - }).toList(), - ), - ], + ], + ), + ); + }).toList(), + ), + ], + ), ), - ), - ); - } - - List> generateSnoreValues(int startTime, int endTime) { - final int count = 100; - final int interval = ((endTime - startTime) / count).floor(); - - return List.generate(count, (index) { - final timestamp = startTime + interval * index; - final value = - (2 + 1 * (index % 7) / 6.0) * (index % 2 == 0 ? 1 : -1); // 示例上下对称波动 - return { - "timestamp": timestamp, - "value": double.parse(value.toStringAsFixed(2)), - }; - }); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/component/SnoreViewWidget.dart b/lib/pages/sleep_report/component/SnoreViewWidget.dart index 3943370..e38d2ab 100644 --- a/lib/pages/sleep_report/component/SnoreViewWidget.dart +++ b/lib/pages/sleep_report/component/SnoreViewWidget.dart @@ -1,3 +1,6 @@ +import 'dart:math'; + +import 'package:EasyDartModule/EasyDartModule.dart' as es; import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -7,6 +10,7 @@ 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/LineChartByRange.dart'; +import 'package:vbvs_app/pages/sleep_report/chart/SnoreChart.dart'; class SnoreViewWidgetWidget extends StatefulWidget { var sleepReport; @@ -34,106 +38,243 @@ class _SnoreViewWidgetWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport is! Map || - widget.sleepReport.isEmpty) { - return Container(); - } + try { + if (widget.sleepReport == null || + widget.sleepReport is! Map || + widget.sleepReport.isEmpty) { + return Container(); + } + double maxY = 60; + var startTime = widget.sleepReport['startTime']; + var endTime = widget.sleepReport['endTime']; + List snoreValues = []; - List> data = - (widget.sleepReport['ssp'] as List).cast>(); - List> showLabel = convertToShowLabel(data); - // List> showLabel = [ - // // {'startTime': 1748275800344, "endTime": 1748283000000, "times": 4}, - // {'startTime': 1748293800000, "endTime": 1748294400000, "times": 7}, - // ]; - var startTime = widget.sleepReport['startTime']; - var endTime = widget.sleepReport['endTime']; - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "打鼾监测介绍。", - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + List type = widget.sleepReport['ssp']['type']; + List lightSnore = widget.sleepReport['ssp']['data'][0]; + List heavySnore = widget.sleepReport['ssp']['data'][1]; + + List processedLightSnore = lightSnore.map((item) { + return { + ...item, + 'id': type[0]['id'], + 'name': type[0]['name'], + 'color': type[0]['color'], + }; + }).toList(); + + List processedHeavySnore = heavySnore.map((item) { + return { + ...item, + 'id': type[1]['id'], + 'name': type[1]['name'], + 'color': type[1]['color'], + }; + }).toList(); + + snoreValues = [...processedLightSnore, ...processedHeavySnore]; + snoreValues.sort((a, b) => a['st'].compareTo(b['st'])); + print(snoreValues); + List barDataList = snoreValues.map((item) { + return BarData( + st: item['st'], + et: item['et'], + value: (item['value'] as num).toDouble() > maxY + ? maxY + 3 + : (item['value'] as num).toDouble(), + id: item['id'], + name: item['name'], + color: (item['color'] == null || item['color'].isEmpty) + ? (item['id'] == 1 ? Colors.green : Colors.red) + : stringToColor(item['color']), + ); + }).toList(); + + // List> data = + // (widget.sleepReport['ssp'] as List).cast>(); + // List> showLabel = convertToShowLabel(data); + + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "打鼾监测介绍。", + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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: 32.rpx, + ), + Row( + children: [ + Text( + "次".tr, + style: TextStyle( + color: stringToColor("#FFFFFF"), fontSize: 18.rpx), ), ], ), - ), - SizedBox( - height: 32.rpx, - ), - Row( - children: [ - Text( - "次".tr, - style: TextStyle( - color: stringToColor("#FFFFFF"), fontSize: 18.rpx), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0.rpx, 40.rpx, 20.rpx, 0.rpx), + // child: LineChartByRange( + // showLabel: showLabel, + // startTime: startTime, + // endTime: endTime, + // ), + child: BarChartWidget( + data: barDataList, + startTime: startTime, + endTime: endTime, + maxYValue: maxY, // 最大值可自定义 + yStepCount: 6, // 分4段(0, 5, 10, 15, 20) ), - ], - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(0.rpx, 40.rpx, 0.rpx, 0.rpx), - child: LineChartByRange( - showLabel: showLabel, - startTime: startTime, - endTime: endTime, ), - ), - SizedBox( - height: 52.rpx, - ), - ], + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 52.rpx, 0.rpx, 0.rpx), + child: Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, // 左对齐 + children: [ + Row( + children: [ + // 小圆球 + Container( + width: 14.rpx, + height: 14.rpx, + decoration: BoxDecoration( + color: (type[0]?['color'] == null || + type[0]?['color'].isEmpty) + ? Colors.green + : stringToColor( + "${type[0]['color']}"), // 你想要的颜色 + shape: BoxShape.circle, + ), + ), + SizedBox(width: 12.rpx), // 小圆球和文字间距 + Text( + '${type[0]?['name']}', + style: TextStyle( + fontSize: + AppConstants().normal_text_fontSize, + color: themeController.currentColor.sc3), + ), + ], + ), + SizedBox(height: 16.rpx), // 两行文字间距 + Text( + '${(type[0]?['value'] == null || type[0]['value'].toString().isEmpty) ? '未知数据'.tr : '${type[0]?['value']}${(type[0]?['unit'] == null || type[0]['unit'].toString().isEmpty) ? '' : type[0]?['unit']}'}', + style: TextStyle( + fontSize: AppConstants().small_text_fontSize, + color: themeController.currentColor.sc4), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, // 左对齐 + children: [ + Row( + children: [ + // 小圆球 + Container( + width: 14.rpx, + height: 14.rpx, + decoration: BoxDecoration( + color: (type[1]?['color'] == null || + type[1]?['color'].isEmpty) + ? Colors.red + : stringToColor( + "${type[1]['color']}"), // 你想要的颜色 + shape: BoxShape.circle, + ), + ), + SizedBox(width: 12.rpx), // 小圆球和文字间距 + Text( + '${type[1]?['name']}', + style: TextStyle( + fontSize: + AppConstants().normal_text_fontSize, + color: themeController.currentColor.sc3), + ), + ], + ), + SizedBox(height: 16.rpx), // 两行文字间距 + Text( + '${(type[1]?['value'] == null || type[1]['value'].toString().isEmpty) ? '未知数据'.tr : '${type[1]?['value']}${(type[1]?['unit'] == null || type[1]['unit'].toString().isEmpty) ? '' : type[1]?['unit']}'}', + style: TextStyle( + fontSize: AppConstants().small_text_fontSize, + color: themeController.currentColor.sc4), + ), + ], + ), + ], + ), + ), + ), + SizedBox( + height: 52.rpx, + ), + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } List> convertToShowLabel( diff --git a/lib/pages/sleep_report/component/ZiZhuShenJingPercentWidget.dart b/lib/pages/sleep_report/component/ZiZhuShenJingPercentWidget.dart index 59d652c..3815908 100644 --- a/lib/pages/sleep_report/component/ZiZhuShenJingPercentWidget.dart +++ b/lib/pages/sleep_report/component/ZiZhuShenJingPercentWidget.dart @@ -7,6 +7,7 @@ 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/StatusBarWithIndicator.dart'; +import 'package:EasyDartModule/EasyDartModule.dart' as es; class ZiZhuShenJingPercentWidget extends StatefulWidget { var sleepReport; @@ -36,106 +37,112 @@ class _ZiZhuShenJingPercentWidgetState @override Widget build(BuildContext context) { - if (widget.sleepReport == null || - widget.sleepReport['sicp'] == null || - widget.sleepReport['sicp'].isEmpty) { - return Container(); - } - int id = 100000; - List data = widget.sleepReport['sicp']; - final target = data.firstWhere( - (element) => element['id'] == id, - orElse: () => null, // 如果没有找到,返回 null(你也可以抛异常或用 {} 替代) - ); + try { + if (widget.sleepReport == null || + widget.sleepReport['sicp'] == null || + widget.sleepReport['sicp'].isEmpty) { + return Container(); + } + int id = 100000; + List data = widget.sleepReport['sicp']; + final target = data.firstWhere( + (element) => element['id'] == id, + orElse: () => null, // 如果没有找到,返回 null(你也可以抛异常或用 {} 替代) + ); - if (target == null) { - return Container(); - } - List> showLabel = []; - if (target != null && target['type'] is List) { - showLabel = (target['type'] as List).map>((item) { - return { - 'key': item['type'], - 'name': item['name'], - 'color': stringToColor(item['color']), - }; - }).toList(); - } + if (target == null) { + return Container(); + } + List> showLabel = []; + if (target != null && target['type'] is List) { + showLabel = (target['type'] as List).map>((item) { + return { + 'key': item['type'], + 'name': item['name'], + 'color': stringToColor(item['color']), + }; + }).toList(); + } - 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, 0.rpx, 14.rpx, 0), // - borderRadius: 0.rpx, // 圆形点击区域 - onTap: () { - showTipDialog( - context, - Container( - child: Text( - "自主神经平衡指数监测介绍".tr, - style: TextStyle( - fontSize: 26.rpx, - color: themeController.currentColor.sc3, + 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, 0.rpx, 14.rpx, 0), // + borderRadius: 0.rpx, // 圆形点击区域 + onTap: () { + showTipDialog( + context, + Container( + child: Text( + "自主神经平衡指数监测介绍".tr, + style: TextStyle( + fontSize: 26.rpx, + color: themeController.currentColor.sc3, + ), ), ), + ); + }, + 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, ), - ); - }, - 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, - ), - Padding( - padding: - EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx), - child: StatusBarWithIndicator( - selectKey: target['value'], - showLabel: showLabel, + SizedBox( + height: 83.rpx, ), - ), - SizedBox( - height: 56.rpx, - ), - ], + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0.rpx), + child: StatusBarWithIndicator( + selectKey: target['value'], + showLabel: showLabel, + ), + ), + SizedBox( + height: 56.rpx, + ), + ], + ), ), - ), - ); + ); + } catch (e) { + es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + return Container(); + } } } diff --git a/lib/pages/sleep_report/new_sleep_report_page copy.dart b/lib/pages/sleep_report/new_sleep_report_page copy.dart new file mode 100644 index 0000000..ae490df --- /dev/null +++ b/lib/pages/sleep_report/new_sleep_report_page copy.dart @@ -0,0 +1,937 @@ +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/common/util/requestWithLog.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.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/language/AppLanguage.dart'; +import 'package:vbvs_app/pages/common/selectDialog.dart'; +import 'package:vbvs_app/pages/sleep_report/component/AIAdviceWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/BreatheCard.dart'; +import 'package:vbvs_app/pages/sleep_report/component/BreathePauseNewWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/BreatheStandardWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/CompareSleepWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/DiseasePercentsWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/HeartChangeWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/HeartHealthWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/HeartPointWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/HeartRateCard.dart'; +import 'package:vbvs_app/pages/sleep_report/component/HeartRateStandardWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/SkinPercentWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/SleepCard.dart'; +import 'package:vbvs_app/pages/sleep_report/component/SleepScoreWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/SleepView.dart'; +import 'package:vbvs_app/pages/sleep_report/component/SnoreViewWidget.dart'; +import 'package:vbvs_app/pages/sleep_report/component/ZiZhuShenJingPercentWidget.dart'; + +class NewSleepReportPage extends StatefulWidget { + var data; + NewSleepReportPage({super.key, required this.data}); + + @override + State createState() => _NewSleepReportPageState(); +} + +class _NewSleepReportPageState extends State { + SleepReportController sleepReportController = Get.find(); + CalendarController calendarController = Get.find(); + + @override + void initState() { + if (widget.data['date'] == null) { + widget.data['date'] = DateTime.now(); + } + calendarController.selectedDate.value = + DateTime.fromMillisecondsSinceEpoch(widget.data['date']); + sleepReportController.selectedDate.value = + DateTime.fromMillisecondsSinceEpoch(widget.data['date']); + if (widget.data['type'] != null) { + sleepReportController.model.type = widget.data['type']; + } else { + sleepReportController.model.type = 1; + } + String date = MyUtils.formatToDate(widget.data['date']); + // String date = '2025-5-27'; + requestWithLog( + logTitle: "查询睡眠报告", + method: MyHttpMethod.get, + queryUrl: + "https://sleepdata.he-info.com/api/analysis/sleep/analysis?mac=${widget.data['mac']}&time=${date}&type=${sleepReportController.model.type}", + onSuccess: (res) { + print(res); + sleepReportController.sleepReport.value = res.data; + sleepReportController.updateAll(); + }, + onFailure: (res) { + TopSlideNotification.show(context, + text: res.msg!, textColor: themeController.currentColor.sc9); + sleepReportController.sleepReport.value = {}; + sleepReportController.updateAll(); + print(res); + }); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + double lineWidth = 115.rpx; + return LayoutBuilder( + builder: (context, bodySize) => GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/img/bgNoImg.png'), // 本地图片 + 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: FlutterFlowTheme.of(context).bodyMedium.override( + fontFamily: 'Readex Pro', + color: themeController.currentColor.sc3, + letterSpacing: 0, + fontSize: 30.rpx, + ), + ), + + /// 左边返回按钮 + Positioned( + left: 0, + child: returnIconButtom, + ), + ], + ), + ), + ), + body: SafeArea( + top: true, + child: SingleChildScrollView( + child: Obx(() { + var sleepReport = sleepReportController.sleepReport; + print(sleepReport); + return Column( + children: [ + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0, 30.rpx, 0, 0), + child: Container( + width: double.infinity, + constraints: BoxConstraints( + minHeight: 90.rpx, + ), + decoration: BoxDecoration( + color: themeController.currentColor.sc5), + child: Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 15.rpx, 30.rpx, 15.rpx), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Stack( + alignment: Alignment.bottomLeft, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: + themeController.currentColor.sc3, + borderRadius: 8.rpx, + padding: EdgeInsets.all(0), + onTap: () async { + sleepReportController.model.type = + 1; + sleepReportController.updateAll(); + }, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + width: 115.rpx, // 固定宽度为 160.rpx + alignment: + Alignment.center, // 文字居中 + child: Text( + '日报'.tr, + style: FlutterFlowTheme.of( + context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: AppConstants() + .title_text_fontSize, + letterSpacing: 0.0, + color: + sleepReportController + .model + .type == + 1 + ? themeController + .currentColor + .sc2 + : themeController + .currentColor + .sc3, + ), + ), + ), + SizedBox(height: 10.rpx), + ], + ), + ), + + // Obx(() { + // return ClickableContainer( + // backgroundColor: Colors.transparent, + // highlightColor: + // themeController.currentColor.sc3, + // borderRadius: 8.rpx, + // padding: EdgeInsets.all(0), + // onTap: () async { + // sleepReportController.model.type = + // 2; + // sleepReportController.updateAll(); + // }, + // child: Column( + // mainAxisSize: MainAxisSize.max, + // children: [ + // Container( + // width: 115.rpx, // 固定宽度为 160.rpx + // alignment: + // Alignment.center, // 文字居中 + // child: Text( + // '周报'.tr, + // style: FlutterFlowTheme.of( + // context) + // .bodyMedium + // .override( + // fontFamily: 'Inter', + // fontSize: AppConstants() + // .title_text_fontSize, + // letterSpacing: 0.0, + // color: + // sleepReportController + // .model + // .type == + // 2 + // ? themeController + // .currentColor + // .sc2 + // : themeController + // .currentColor + // .sc3, + // ), + // ), + // ), + // SizedBox(height: 10.rpx), + // ], + // ), + // ); + // }), + // Obx(() { + // return ClickableContainer( + // backgroundColor: Colors.transparent, + // highlightColor: + // themeController.currentColor.sc3, + // borderRadius: 8.rpx, + // padding: EdgeInsets.all(0), + // onTap: () async { + // sleepReportController.model.type = + // 3; + // sleepReportController.updateAll(); + // }, + // child: Column( + // mainAxisSize: MainAxisSize.max, + // children: [ + // Container( + // width: 115.rpx, // 固定宽度为 160.rpx + // alignment: + // Alignment.center, // 文字居中 + // child: Text( + // '月报'.tr, + // style: FlutterFlowTheme.of( + // context) + // .bodyMedium + // .override( + // fontFamily: 'Inter', + // fontSize: AppConstants() + // .title_text_fontSize, + // letterSpacing: 0.0, + // color: + // sleepReportController + // .model + // .type == + // 3 + // ? themeController + // .currentColor + // .sc2 + // : themeController + // .currentColor + // .sc3, + // ), + // ), + // ), + // SizedBox(height: 10.rpx), + // ], + // ), + // ); + // }), + ], + ), + AnimatedPositioned( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + bottom: 0, + left: sleepReportController.model.type == + 1 + ? 0 + : sleepReportController.model.type == + 2 + ? 115.rpx + : 230.rpx, + child: Container( + width: lineWidth, + height: 4.rpx, + decoration: BoxDecoration( + color: + themeController.currentColor.sc2, + borderRadius: + BorderRadius.circular(2.rpx), + ), + ), + ), + ], + ), + // Padding( + // padding: EdgeInsetsDirectional.fromSTEB( + // 0, 0.rpx, 0.rpx, 0), + // child: Container( + // width: 28.rpx, + // height: 28.rpx, + // // width: double.infinity, + // decoration: BoxDecoration(), + // child: SvgPicture.asset( + // 'assets/img/icon/share.svg', + // fit: BoxFit.cover, + // color: themeController.currentColor.sc3, + // ), + // ), + // ), + ], + ), + ), + ), + ), + Container( + width: double.infinity, + child: Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 32.rpx, 30.rpx, 32.rpx), + child: getTimeWidget(), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 58.rpx), + child: ClickableContainer( + backgroundColor: themeController.currentColor.sc5, + highlightColor: + themeController.currentColor.sc5, // 或你希望的点击水波纹颜色 + borderRadius: AppConstants() + .normal_container_radius, // 如果你想加圆角可以设置 eg. 12.rpx + padding: EdgeInsets.zero, + onTap: () {}, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + flex: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + '实时体征.姓名'.tr, + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc4, + ), + ), + Text( + '实时体征.年龄'.tr, + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc4, + ), + ), + ].divide(SizedBox(height: 34.rpx)), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + '传感器1号', + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc3, + ), + ), + Text( + '69', + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc3, + ), + ), + ].divide(SizedBox(height: 34.rpx)), + ), + ] + .divide(SizedBox(width: 33.rpx)) + .addToStart(SizedBox(width: 37.rpx)), + ), + ] + .addToStart(SizedBox(height: 36.rpx)) + .addToEnd(SizedBox(height: 36.rpx)), + ), + ), + Expanded( + flex: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + '实时体征.设备ID'.tr, + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc4, + ), + ), + Text( + '实时体征.体重'.tr, + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc4, + ), + ), + ].divide(SizedBox(height: 34.rpx)), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + '1231212', + // "D11250300003", + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc3, + ), + ), + Text( + '55kg', + style: + FlutterFlowTheme.of(context) + .bodyMedium + .override( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc3, + ), + ), + ].divide(SizedBox(height: 34.rpx)), + ), + ] + .divide(SizedBox(width: 33.rpx)) + .addToStart(SizedBox(width: 37.rpx)), + ), + ] + .addToStart(SizedBox(height: 36.rpx)) + .addToEnd(SizedBox(height: 36.rpx)), + ), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: + SleepScoreWidget(sleepReport: sleepReport.value), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: SleepViewWidget( + sleepReport: sleepReport, + ), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: SleepCard( + sleepReport: sleepReport, + ), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: CompareSleepWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: HeartPointWidget( + sleepReport: sleepReport, + ), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: AIAdviceWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: + HeartRateStandardWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: HeartRateCard(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: HeartChangeWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: + BreatheStandardWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: BreatheCard(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: + SnoreViewWidgetWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: + BreathePauseNewWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: HeartHealthWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: + DiseasePercentsWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: SkinPercentWidget(sleepReport: sleepReport), + ), + ), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 30.rpx, 0.rpx, 30.rpx, 0), + child: Container( + width: double.infinity, + child: ZiZhuShenJingPercentWidget( + sleepReport: sleepReport), + ), + ), + ].divide(SizedBox( + height: 25.rpx, + )), + ); + }), + ), + ), + ), + ), + ), + ); + } + + Widget getTimeWidget() { + final selectedDate = sleepReportController.selectedDate.value!; + final type = sleepReportController.model.type; + bool isChinese = AppLanguage().isChinese(); + String displayText = ''; + if (isChinese) { + if (type == 1) { + // 日报 + displayText = + MyUtils.getFormatChineseTime(selectedDate.millisecondsSinceEpoch); + } else if (type == 2) { + // 周报 + final startOfWeek = + selectedDate.subtract(Duration(days: selectedDate.weekday - 1)); + final endOfWeek = startOfWeek.add(const Duration(days: 6)); + displayText = + '${MyUtils.getFormatChineseTime(startOfWeek.millisecondsSinceEpoch, showWeekday: false)}-${MyUtils.getFormatChineseTime(endOfWeek.millisecondsSinceEpoch, showWeekday: false)}'; + } else if (type == 3) { + // 月报 + displayText = + '${selectedDate.year}年${selectedDate.month.toString().padLeft(2, '0')}月'; + } + } else { + if (type == 1) { + // Daily Report + displayText = + MyUtils.getFormatEnglishDate(selectedDate.millisecondsSinceEpoch); + } else if (type == 2) { + // Weekly Report + final startOfWeek = + selectedDate.subtract(Duration(days: selectedDate.weekday - 1)); + final endOfWeek = startOfWeek.add(const Duration(days: 6)); + displayText = + '${MyUtils.getFormatEnglishDate(startOfWeek.millisecondsSinceEpoch)} - ${MyUtils.getFormatEnglishDate(endOfWeek.millisecondsSinceEpoch)}'; + } else if (type == 3) { + // Monthly Report + displayText = + '${_getEnglishMonthName(selectedDate.month)} ${selectedDate.year}'; + } + } + + void onLeftArrowTap() { + if (type == 1) { + sleepReportController.selectedDate.value = + selectedDate.subtract(const Duration(days: 1)); + } else if (type == 2) { + sleepReportController.selectedDate.value = + selectedDate.subtract(const Duration(days: 7)); + } else if (type == 3) { + sleepReportController.selectedDate.value = DateTime( + selectedDate.year, + selectedDate.month - 1, + selectedDate.day, + ); + } + calendarController.selectedDate.value = + sleepReportController.selectedDate.value; + // String date = MyUtils.formatToDate(widget.data['date']); + String data = MyUtils.formatDate(calendarController.selectedDate.value!); + requestWithLog( + logTitle: "查询睡眠报告", + method: MyHttpMethod.get, + queryUrl: + "https://sleepdata.he-info.com/api/analysis/sleep/analysis?mac=${widget.data['mac']}&time=${data}&type=${sleepReportController.model.type}", + onSuccess: (res) { + print(res); + sleepReportController.sleepReport.value = res.data; + sleepReportController.updateAll(); + }, + onFailure: (res) { + TopSlideNotification.show(context, + text: res.msg!, textColor: themeController.currentColor.sc9); + sleepReportController.sleepReport.value = {}; + sleepReportController.updateAll(); + print(res); + }); + + sleepReportController.updateAll(); + calendarController.updateAll(); + } + + void onRightArrowTap() { + if (type == 1) { + sleepReportController.selectedDate.value = + selectedDate.add(const Duration(days: 1)); + } else if (type == 2) { + sleepReportController.selectedDate.value = + selectedDate.add(const Duration(days: 7)); + } else if (type == 3) { + sleepReportController.selectedDate.value = DateTime( + selectedDate.year, + selectedDate.month + 1, + selectedDate.day, + ); + } + calendarController.selectedDate.value = + sleepReportController.selectedDate.value; + String data = MyUtils.formatDate(calendarController.selectedDate.value!); + requestWithLog( + logTitle: "查询睡眠报告", + method: MyHttpMethod.get, + queryUrl: + "https://sleepdata.he-info.com/api/analysis/sleep/analysis?mac=${widget.data['mac']}&time=${data}&type=${sleepReportController.model.type}", + onSuccess: (res) { + print(res); + sleepReportController.sleepReport.value = res.data; + sleepReportController.updateAll(); + }, + onFailure: (res) { + TopSlideNotification.show(context, + text: res.msg!, textColor: themeController.currentColor.sc9); + sleepReportController.sleepReport.value = {}; + sleepReportController.updateAll(); + print(res); + }); + sleepReportController.updateAll(); + calendarController.updateAll(); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 28.rpx, height: 28.rpx), // 占位 + Row( + children: [ + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: themeController.currentColor.sc3, + padding: EdgeInsets.all(10.rpx), + borderRadius: 8.rpx, + onTap: onLeftArrowTap, + child: SizedBox( + width: 9.rpx, + height: 14.rpx, + child: SvgPicture.asset( + 'assets/img/icon/arrow_left.svg', + color: themeController.currentColor.sc3, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 26.rpx), + child: Text( + displayText, + style: TextStyle( + fontSize: AppConstants().normal_text_fontSize, + color: themeController.currentColor.sc3, + ), + ), + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: themeController.currentColor.sc3, + padding: EdgeInsets.all(10.rpx), + borderRadius: 8.rpx, + onTap: onRightArrowTap, + child: SizedBox( + width: 9.rpx, + height: 14.rpx, + child: SvgPicture.asset( + 'assets/img/icon/arrow_right.svg', + color: themeController.currentColor.sc3, + ), + ), + ), + ], + ), + ClickableContainer( + backgroundColor: Colors.transparent, + highlightColor: themeController.currentColor.sc3, + padding: EdgeInsets.zero, + borderRadius: 8, + onTap: () { + showSleepCalendarBottomSheet( + type: sleepReportController.model.type, + timestamp: selectedDate.millisecondsSinceEpoch, + context: context, + onDateSelected: (newDate) { + sleepReportController.selectedDate.value = newDate; + calendarController.selectedDate.value = newDate; + String data = + MyUtils.formatDate(calendarController.selectedDate.value!); + requestWithLog( + logTitle: "查询睡眠报告", + method: MyHttpMethod.get, + queryUrl: + "https://sleepdata.he-info.com/api/analysis/sleep/analysis?mac=${widget.data['mac']}&time=${data}&type=${sleepReportController.model.type}", + onSuccess: (res) { + print(res); + sleepReportController.sleepReport.value = res.data; + sleepReportController.updateAll(); + }, + onFailure: (res) { + TopSlideNotification.show(context, + text: res.msg!, + textColor: themeController.currentColor.sc9); + sleepReportController.sleepReport.value = {}; + sleepReportController.updateAll(); + print(res); + }); + sleepReportController.updateAll(); + calendarController.updateAll(); + }, + ); + }, + child: SizedBox( + width: 28.rpx, + height: 28.rpx, + child: SvgPicture.asset( + 'assets/img/icon/calendar.svg', + fit: BoxFit.cover, + color: themeController.currentColor.sc3, + ), + ), + ), + ], + ); + } + + static String _getEnglishMonthName(int month) { + const monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + return monthNames[month - 1]; + } +} diff --git a/lib/pages/sleep_report/new_sleep_report_page.dart b/lib/pages/sleep_report/new_sleep_report_page.dart index 3e53689..353d50c 100644 --- a/lib/pages/sleep_report/new_sleep_report_page.dart +++ b/lib/pages/sleep_report/new_sleep_report_page.dart @@ -6,7 +6,6 @@ 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/ClickableContainer.dart'; import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; import 'package:vbvs_app/controller/date/CalendarController.dart'; @@ -43,8 +42,23 @@ class _NewSleepReportPageState extends State { SleepReportController sleepReportController = Get.find(); CalendarController calendarController = Get.find(); + final GlobalKey sleepCardKey = GlobalKey(); + final GlobalKey heartRateCardKey = GlobalKey(); + final GlobalKey breatheCardKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + @override + void didUpdateWidget(NewSleepReportPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.data['name'] != oldWidget.data['name'] || + widget.data['itemId'] != oldWidget.data['itemId']) { + _scrollToTargetComponent(sleepReportController.sleepReport); + } + } + @override void initState() { + sleepReportController.sleepReport.value = {}; if (widget.data['date'] == null) { widget.data['date'] = DateTime.now(); } @@ -68,6 +82,7 @@ class _NewSleepReportPageState extends State { print(res); sleepReportController.sleepReport.value = res.data; sleepReportController.updateAll(); + _scrollToTargetComponent(sleepReportController.sleepReport); }, onFailure: (res) { TopSlideNotification.show(context, @@ -560,9 +575,11 @@ class _NewSleepReportPageState extends State { padding: EdgeInsetsDirectional.fromSTEB( 30.rpx, 0.rpx, 30.rpx, 0), child: Container( + key: sleepCardKey, width: double.infinity, child: SleepCard( sleepReport: sleepReport, + highlightItem: widget.data['itemName'] ?? null, ), ), ), @@ -605,8 +622,12 @@ class _NewSleepReportPageState extends State { padding: EdgeInsetsDirectional.fromSTEB( 30.rpx, 0.rpx, 30.rpx, 0), child: Container( + key: heartRateCardKey, width: double.infinity, - child: HeartRateCard(sleepReport: sleepReport), + child: HeartRateCard( + sleepReport: sleepReport, + highlightItem: widget.data['itemName'] ?? null, + ), ), ), Padding( @@ -630,8 +651,12 @@ class _NewSleepReportPageState extends State { padding: EdgeInsetsDirectional.fromSTEB( 30.rpx, 0.rpx, 30.rpx, 0), child: Container( + key: breatheCardKey, width: double.infinity, - child: BreatheCard(sleepReport: sleepReport), + child: BreatheCard( + sleepReport: sleepReport, + highlightItem: widget.data['itemName'] ?? null, + ), ), ), Padding( @@ -935,4 +960,38 @@ class _NewSleepReportPageState extends State { ]; return monthNames[month - 1]; } + + void _scrollToTargetComponent(RxMap sleepReport) { + if (sleepReport.isEmpty) { + return; + } + WidgetsBinding.instance.addPostFrameCallback((_) { + final targetName = widget.data['name']; + final targetID = widget.data['itemName']; + GlobalKey? targetKey; + + List sleepdata = sleepReport['bs'] ?? []; // 睡眠数据 + List heartdata = sleepReport['hrs'] ?? []; // 心率数据 + List breathedata = sleepReport['brs'] ?? []; // 呼吸数据 + + if (sleepdata.any((e) => e['id'] == targetID)) { + targetKey = sleepCardKey; + } else if (heartdata.any((e) => e['id'] == targetID)) { + targetKey = heartRateCardKey; + } else if (breathedata.any((e) => e['id'] == targetID)) { + targetKey = breatheCardKey; + } else { + return; + } + + if (targetKey?.currentContext != null) { + Scrollable.ensureVisible( + targetKey!.currentContext!, + duration: Duration(milliseconds: 500), + curve: Curves.easeInOut, + alignment: 0.1, + ); + } + }); + } }