From 2991deb8b3104489e42e2f6ab88e493699ba21c8 Mon Sep 17 00:00:00 2001 From: wyf <494641114@qq.com> Date: Thu, 8 Jan 2026 11:56:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A5=E6=8A=A5=E4=B8=AD?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E6=97=B6=E9=97=B4=E8=BD=B4=E9=94=99=E4=B9=B1?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home_page/DynamicReportDetailWidget.dart | 1019 ++++++++++++++--- lib/pages/device/deviceCopy.dart | 903 +++++++++++++++ lib/pages/main_bottom/mine_page.dart | 2 +- lib/pages/mh_page/new_settingPage.dart | 2 +- lib/pages/policy/privacy_policy.dart | 2 +- .../sleep_report/chart/LineChartByRange.dart | 492 +++++++- lib/pages/sleep_report/chart/SnoreChart.dart | 839 ++++++++------ .../sleep_report/chart/SnoreWaveform.dart | 407 ------- .../sleep_report/chart/TimeSeriesChart.dart | 189 +-- .../component/AIAdviceWidget.dart | 2 +- .../component/BreathPauseWidget.dart | 2 +- .../component/BreathePauseNewWidget.dart | 2 +- .../component/BreatheStandardWidget.dart | 2 +- .../component/DiseasePercentsWidget.dart | 2 +- .../component/HeartChangeWidget.dart | 2 +- .../component/HeartHealthWidget.dart | 2 +- .../component/HeartPointWidget.dart | 2 +- .../sleep_report/component/HrvWidget.dart | 2 +- .../component/SkinPercentWidget.dart | 2 +- .../component/SleepScoreWidget.dart | 2 +- .../component/new_sleep_view.dart | 2 +- 21 files changed, 2824 insertions(+), 1055 deletions(-) create mode 100644 lib/pages/device/deviceCopy.dart diff --git a/lib/component/home_page/DynamicReportDetailWidget.dart b/lib/component/home_page/DynamicReportDetailWidget.dart index 65c089d..f0f1d98 100644 --- a/lib/component/home_page/DynamicReportDetailWidget.dart +++ b/lib/component/home_page/DynamicReportDetailWidget.dart @@ -1,3 +1,483 @@ +// import 'dart:async'; + +// import 'package:ef/ef.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_svg/svg.dart'; +// import 'package:flutterflow_ui/flutterflow_ui.dart'; +// import 'package:vbvs_app/common/color/appConstants.dart'; +// import 'package:vbvs_app/common/util/FitTool.dart'; +// import 'package:vbvs_app/common/util/MyUtils.dart'; +// import 'package:vbvs_app/component/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/device/body_device_controller.dart'; +// import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; +// import 'package:vbvs_app/enum/DataStatus.dart'; + +// class DynamicReportDetailWidget extends StatefulWidget { +// final List sleepDateWidgets; +// final List sleepDataModuleWidgets; +// final Map targetDevice; + +// const DynamicReportDetailWidget({ +// Key? key, +// required this.sleepDateWidgets, +// required this.sleepDataModuleWidgets, +// required this.targetDevice, +// }) : super(key: key); + +// @override +// State createState() => +// _DynamicReportDetailWidgetState(); +// } + +// class _DynamicReportDetailWidgetState extends State { +// final ThemeController themeController = Get.find(); +// final ScrollController _scrollController = ScrollController(); +// bool _hasScrolled = false; +// BodyDeviceController bodyDeviceController = Get.find(); + +// @override +// void initState() { +// super.initState(); + +// WidgetsBinding.instance.addPostFrameCallback((_) { +// Future.delayed(Duration(milliseconds: 1000), () { +// if (!_hasScrolled && _scrollController.hasClients +// // && _scrollController.position.maxScrollExtent > 0 +// ) { +// _scrollController.animateTo( +// _scrollController.position.maxScrollExtent, +// duration: Duration(milliseconds: 300), +// curve: Curves.easeOut, +// ); +// _hasScrolled = true; +// } +// }); +// }); +// } + +// @override +// Widget build(BuildContext context) { +// return Padding( +// padding: EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0, 0.rpx), +// child: Container( +// width: double.infinity, +// decoration: BoxDecoration( +// color: themeController.currentColor.sc5, +// borderRadius: +// BorderRadius.circular(AppConstants().normal_container_radius), +// ), +// child: Padding( +// padding: +// EdgeInsetsDirectional.fromSTEB(30.rpx, 30.rpx, 30.rpx, 30.rpx), +// child: Column( +// mainAxisSize: MainAxisSize.max, +// children: [ +// _buildHeader(context, widget.targetDevice), +// SizedBox(height: 33.rpx), +// _buildSleepDateWidgets(), +// SizedBox(height: 20.rpx), +// if (!AppConstants.is_test_account) +// Obx(() { +// return _buildSleepDataModuleWidgets(); +// }), +// ], +// ), +// ), +// ), +// ); +// } + +// Widget _buildHeader(BuildContext context, Map targetDevice) { +// return Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// ClickableContainer( +// backgroundColor: Colors.transparent, +// highlightColor: themeController.currentColor.sc3.withOpacity(0.2), +// borderRadius: 0, +// padding: EdgeInsets.zero, +// onTap: () async { +// await Get.toNamed("/bodyDevice", arguments: targetDevice); +// }, +// child: Container( +// constraints: BoxConstraints( +// maxWidth: MediaQuery.sizeOf(context).width * 0.5, +// ), +// child: Text( +// '${targetDevice['person']?['name'] == null || targetDevice['person']?['name'].isEmpty ? '体征监测设备'.tr : targetDevice['person']['name']}', +// style: TextStyle( +// fontFamily: 'Inter', +// fontSize: 30.rpx, +// letterSpacing: 0.0, +// color: themeController.currentColor.sc3, +// ), +// maxLines: 1, +// overflow: TextOverflow.ellipsis, +// ), +// ), +// ), +// if (!AppConstants.is_test_account) +// ClickableContainer( +// backgroundColor: Colors.transparent, +// highlightColor: themeController.currentColor.sc3, +// borderRadius: 0, +// padding: EdgeInsets.zero, +// onTap: () { +// String mac = targetDevice['mac']; +// List selectedWidgets = widget.sleepDateWidgets +// .where((w) => w.isSelected == true) +// .toList(); +// if (selectedWidgets.isNotEmpty) { +// DateTime dateTime = DateTime.fromMillisecondsSinceEpoch( +// int.parse(selectedWidgets[0].time!)); +// String time = MyUtils.formatBindTime(dateTime); +// // String sleepReportUrl = +// // "${ServiceConstant.sleep_report_url}?mac=$mac&token=${ServiceConstant.sleep_token}&date=$time"; +// // Get.toNamed("/sleepReportPage", arguments: sleepReportUrl); +// Get.toNamed("/newSleepReportPage", arguments: { +// 'date': dateTime != null +// ? dateTime.millisecondsSinceEpoch +// : DateTime.now().millisecondsSinceEpoch, +// "mac": mac, +// 'type': 1, +// 'name': 'sleep', //'sleep', 'heartRate' 或 'breathe' +// // 'itemName': widget.data['id'], +// 'person': widget.targetDevice['person'], +// }); +// } else { +// TopSlideNotification.show(context, +// text: "当前暂无数据".tr, +// textColor: themeController.currentColor.sc9); +// } +// }, +// child: Row( +// children: [ +// Text( +// '首页.报告详情'.tr, +// style: TextStyle( +// fontFamily: 'Inter', +// fontSize: 26.rpx, +// letterSpacing: 0.0, +// color: themeController.currentColor.sc2, +// ), +// ), +// Padding( +// padding: EdgeInsetsDirectional.fromSTEB(0, 6.rpx, 0, 0.rpx), +// child: SvgPicture.asset( +// 'assets/img/icon/arrow_right.svg', +// width: 14.rpx, +// height: 14.rpx, +// color: themeController.currentColor.sc3, +// ), +// ), +// ].divide(SizedBox(width: 22.rpx)), +// ), +// ), +// ], +// ); +// } + +// Widget _buildSleepDateWidgets() { +// return Container( +// width: double.infinity, +// child: SingleChildScrollView( +// controller: _scrollController, +// scrollDirection: Axis.horizontal, +// child: Row( +// children: widget.sleepDateWidgets +// .map((widget) => widget) +// .toList() +// .divide(SizedBox(width: 20.rpx)), +// ), +// ), +// ); +// } + +// // Widget _buildSleepDataModuleWidgets() { +// // // homePageSleepFlag +// // //widget.targetDevice['mac'] +// // if (bodyDeviceController.homePageSleepFlag[widget.targetDevice['mac']] == +// // DataStatus.Loading.code) { +// // return Container( +// // height: 200.rpx, +// // alignment: Alignment.center, +// // child: CircularProgressIndicator( +// // color: themeController.currentColor.sc1, +// // ), +// // ); +// // } +// // if (widget.sleepDataModuleWidgets.isEmpty) { +// // return Container( +// // height: 200.rpx, +// // alignment: Alignment.center, +// // child: Text( +// // '暂无数据'.tr, +// // style: TextStyle( +// // fontFamily: 'Inter', +// // fontSize: 28.rpx, +// // color: themeController.currentColor.sc4, +// // ), +// // ), +// // ); +// // } +// // // if (widget.sleepDataModuleWidgets.isEmpty) { +// // // return Container( +// // // height: 200.rpx, +// // // alignment: Alignment.center, +// // // child: CircularProgressIndicator( +// // // color: themeController.currentColor.sc1, +// // // ), +// // // ); +// // // } + +// // // return Container( +// // // width: double.infinity, +// // // height: 200.rpx, +// // // child: SingleChildScrollView( +// // // scrollDirection: Axis.horizontal, +// // // child: Row( +// // // children: widget.sleepDataModuleWidgets +// // // .map((widget) => widget) +// // // .toList() +// // // .divide(SizedBox(width: 14.rpx)), +// // // ), +// // // ), +// // // ); +// // var aa = widget.sleepDataModuleWidgets +// // // 过滤:当 data 中存在 'show' 且其为 false 时排除该元素 +// // .where((item) => item.data?['show'] != false) +// // // 保持元素本身(SleepDataModuleWidget) +// // .map((item) => item) +// // .toList(); +// // return Container( +// // width: double.infinity, +// // height: 200.rpx, +// // child: SingleChildScrollView( +// // scrollDirection: Axis.horizontal, +// // child: Row( +// // children: +// // // 保留你原来的 divide 间隔处理 +// // aa.divide(SizedBox(width: 14.rpx)), +// // ), +// // ), +// // ); +// // } + +// Widget _buildSleepDataModuleWidgets() { +// if (bodyDeviceController.homePageSleepFlag[widget.targetDevice['mac']] == +// DataStatus.Loading.code) { +// return Container( +// height: 200.rpx, +// alignment: Alignment.center, +// child: CircularProgressIndicator( +// color: themeController.currentColor.sc1, +// ), +// ); +// } + +// var items = widget.sleepDataModuleWidgets +// .where((item) => item.data?['show'] != false) +// .toList(); + +// if (items.isEmpty) { +// return Container( +// height: 200.rpx, +// alignment: Alignment.center, +// child: Text( +// '暂无数据'.tr, +// style: TextStyle( +// fontFamily: 'Inter', +// fontSize: 28.rpx, +// color: themeController.currentColor.sc4, +// ), +// ), +// ); +// } + +// return Container( +// height: 200.rpx, +// child: WidgetMarquee( +// items: items, +// itemWidth: 240.rpx, +// spacing: 14.rpx, +// speed: 30.0, +// ), +// ); +// } + +// void resetScroll() { +// _hasScrolled = false; +// if (_scrollController.hasClients) { +// _scrollController.jumpTo(0); +// } +// } +// } + +// class WidgetMarquee extends StatefulWidget { +// final List items; +// final double itemWidth; +// final double spacing; +// final double speed; // 像素/秒 + +// const WidgetMarquee({ +// Key? key, +// required this.items, +// required this.itemWidth, +// this.spacing = 14, +// this.speed = 30, +// }) : super(key: key); + +// @override +// _WidgetMarqueeState createState() => _WidgetMarqueeState(); +// } + +// class _WidgetMarqueeState extends State { +// final ScrollController _scrollController = ScrollController(); +// Timer? _timer; +// double _scrollOffset = 0; +// bool _needsMarquee = false; +// double _containerWidth = 0; +// double _totalWidth = 0; + +// @override +// void initState() { +// super.initState(); +// WidgetsBinding.instance.addPostFrameCallback((_) { +// _checkIfNeedsMarquee(); +// _startMarquee(); +// }); +// } + +// void _checkIfNeedsMarquee() { +// if (!mounted) return; + +// final containerWidth = context.size?.width ?? 0; +// _totalWidth = widget.items.length * widget.itemWidth + +// (widget.items.length - 1) * widget.spacing; + +// setState(() { +// _containerWidth = containerWidth; +// _needsMarquee = _totalWidth > containerWidth; +// }); +// } + +// void _startMarquee() { +// if (!_needsMarquee) return; + +// // 延迟开始滚动,让用户有时间查看初始内容 +// Future.delayed(Duration(seconds: 0), () { +// if (!mounted) return; + +// _timer = Timer.periodic(Duration(milliseconds: 16), (timer) { +// if (!mounted || !_scrollController.hasClients) { +// timer.cancel(); +// return; +// } + +// setState(() { +// _scrollOffset += widget.speed / 60; // 每帧移动的距离 + +// // 当滚动到第一组内容的末尾时,重置偏移量 +// if (_scrollOffset >= _totalWidth) { +// _scrollOffset -= _totalWidth; +// } + +// _scrollController.jumpTo(_scrollOffset); +// }); +// }); +// }); +// } + +// @override +// void dispose() { +// _timer?.cancel(); +// _scrollController.dispose(); +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// if (widget.items.isEmpty) { +// return Container(); +// } + +// if (!_needsMarquee) { +// // 不需要滚动,显示单行 +// return SingleChildScrollView( +// scrollDirection: Axis.horizontal, +// child: Row( +// children: widget.items +// .asMap() +// .entries +// .map((entry) => Padding( +// padding: EdgeInsets.only( +// right: entry.key < widget.items.length - 1 +// ? widget.spacing +// : 0, +// ), +// child: Container( +// width: widget.itemWidth, +// child: entry.value, +// ), +// )) +// .toList(), +// ), +// ); +// } + +// // 需要滚动,使用两倍内容实现无缝滚动 +// return SingleChildScrollView( +// controller: _scrollController, +// scrollDirection: Axis.horizontal, +// physics: const NeverScrollableScrollPhysics(), +// child: Row( +// children: [ +// // 第一组 +// Row( +// children: widget.items +// .asMap() +// .entries +// .map((entry) => Padding( +// padding: EdgeInsets.only( +// right: entry.key < widget.items.length - 1 +// ? widget.spacing +// : 0, +// ), +// child: Container( +// width: widget.itemWidth, +// child: entry.value, +// ), +// )) +// .toList(), +// ), +// Row( +// children: widget.items +// .asMap() +// .entries +// .map((entry) => Padding( +// padding: EdgeInsets.only( +// right: entry.key < widget.items.length - 1 +// ? widget.spacing +// : 0, +// ), +// child: Container( +// width: widget.itemWidth, +// child: entry.value, +// ), +// )) +// .toList(), +// ), +// ], +// ), +// ); +// } +// } + +import 'dart:async'; + import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -39,15 +519,12 @@ class _DynamicReportDetailWidgetState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - Future.delayed(Duration(milliseconds: 1000), () { - if (!_hasScrolled && _scrollController.hasClients - // && _scrollController.position.maxScrollExtent > 0 - ) { + Future.delayed(const Duration(milliseconds: 1000), () { + if (!_hasScrolled && _scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, - duration: Duration(milliseconds: 300), + duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); _hasScrolled = true; @@ -61,7 +538,6 @@ class _DynamicReportDetailWidgetState extends State { return Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0, 0.rpx), child: Container( - width: double.infinity, decoration: BoxDecoration( color: themeController.currentColor.sc5, borderRadius: @@ -71,16 +547,13 @@ class _DynamicReportDetailWidgetState extends State { padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 30.rpx, 30.rpx, 30.rpx), child: Column( - mainAxisSize: MainAxisSize.max, children: [ _buildHeader(context, widget.targetDevice), SizedBox(height: 33.rpx), _buildSleepDateWidgets(), SizedBox(height: 20.rpx), if (!AppConstants.is_test_account) - Obx(() { - return _buildSleepDataModuleWidgets(); - }), + Obx(() => _buildSleepDataModuleWidgets()), ], ), ), @@ -95,179 +568,419 @@ class _DynamicReportDetailWidgetState extends State { ClickableContainer( backgroundColor: Colors.transparent, highlightColor: themeController.currentColor.sc3.withOpacity(0.2), - borderRadius: 0, padding: EdgeInsets.zero, onTap: () async { await Get.toNamed("/bodyDevice", arguments: targetDevice); }, - child: Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.sizeOf(context).width * 0.5, - ), - child: Text( - '${targetDevice['person']?['name'] == null ? '未命名'.tr : targetDevice['person']['name']}', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 30.rpx, - letterSpacing: 0.0, - color: themeController.currentColor.sc3, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Text( + targetDevice['person']?['name']?.isNotEmpty == true + ? targetDevice['person']['name'] + : '体征监测设备'.tr, + style: TextStyle( + fontSize: 30.rpx, + color: themeController.currentColor.sc3, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - if (!AppConstants.is_test_account) - ClickableContainer( - backgroundColor: Colors.transparent, - highlightColor: themeController.currentColor.sc3, - borderRadius: 0, - padding: EdgeInsets.zero, - onTap: () { - String mac = targetDevice['mac']; - List selectedWidgets = widget.sleepDateWidgets - .where((w) => w.isSelected == true) - .toList(); - if (selectedWidgets.isNotEmpty) { - DateTime dateTime = DateTime.fromMillisecondsSinceEpoch( - int.parse(selectedWidgets[0].time!)); - String time = MyUtils.formatBindTime(dateTime); - // String sleepReportUrl = - // "${ServiceConstant.sleep_report_url}?mac=$mac&token=${ServiceConstant.sleep_token}&date=$time"; - // Get.toNamed("/sleepReportPage", arguments: sleepReportUrl); - Get.toNamed("/newSleepReportPage", arguments: { - 'date': dateTime != null - ? dateTime.millisecondsSinceEpoch - : DateTime.now().millisecondsSinceEpoch, - "mac": mac, - 'type': 1, - 'name': 'sleep', //'sleep', 'heartRate' 或 'breathe' - // 'itemName': widget.data['id'], - 'person': widget.targetDevice['person'], - }); - } else { - TopSlideNotification.show(context, - text: "当前暂无数据".tr, - textColor: themeController.currentColor.sc9); - } - }, - child: Row( - children: [ - Text( - '首页.报告详情'.tr, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 26.rpx, - letterSpacing: 0.0, - color: themeController.currentColor.sc2, - ), - ), - Padding( - padding: EdgeInsetsDirectional.fromSTEB(0, 6.rpx, 0, 0.rpx), - child: SvgPicture.asset( - 'assets/img/icon/arrow_right.svg', - width: 14.rpx, - height: 14.rpx, - color: themeController.currentColor.sc3, - ), - ), - ].divide(SizedBox(width: 22.rpx)), - ), - ), ], ); } Widget _buildSleepDateWidgets() { - return Container( - width: double.infinity, - child: SingleChildScrollView( - controller: _scrollController, - scrollDirection: Axis.horizontal, - child: Row( - children: widget.sleepDateWidgets - .map((widget) => widget) - .toList() - .divide(SizedBox(width: 20.rpx)), - ), + return SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + child: Row( + children: widget.sleepDateWidgets + .map((e) => e) + .toList() + .divide(SizedBox(width: 20.rpx)), ), ); } Widget _buildSleepDataModuleWidgets() { - // homePageSleepFlag - //widget.targetDevice['mac'] if (bodyDeviceController.homePageSleepFlag[widget.targetDevice['mac']] == DataStatus.Loading.code) { - return Container( + return SizedBox( height: 200.rpx, - alignment: Alignment.center, - child: CircularProgressIndicator( - color: themeController.currentColor.sc1, - ), - ); - } - if (widget.sleepDataModuleWidgets.isEmpty) { - return Container( - height: 200.rpx, - alignment: Alignment.center, - child: Text( - '暂无数据'.tr, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 28.rpx, - color: themeController.currentColor.sc4, + child: Center( + child: CircularProgressIndicator( + color: themeController.currentColor.sc1, ), ), ); } - // if (widget.sleepDataModuleWidgets.isEmpty) { - // return Container( - // height: 200.rpx, - // alignment: Alignment.center, - // child: CircularProgressIndicator( - // color: themeController.currentColor.sc1, - // ), - // ); - // } - // return Container( - // width: double.infinity, - // height: 200.rpx, - // child: SingleChildScrollView( - // scrollDirection: Axis.horizontal, - // child: Row( - // children: widget.sleepDataModuleWidgets - // .map((widget) => widget) - // .toList() - // .divide(SizedBox(width: 14.rpx)), - // ), - // ), - // ); - var aa = widget.sleepDataModuleWidgets - // 过滤:当 data 中存在 'show' 且其为 false 时排除该元素 - .where((item) => item.data?['show'] != false) - // 保持元素本身(SleepDataModuleWidget) - .map((item) => item) + final items = widget.sleepDataModuleWidgets + .where((e) => e.data?['show'] != false) .toList(); - return Container( - width: double.infinity, - height: 200.rpx, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: - // 保留你原来的 divide 间隔处理 - aa.divide(SizedBox(width: 14.rpx)), + + if (items.isEmpty) { + return SizedBox( + height: 200.rpx, + child: Center( + child: Text( + '暂无数据'.tr, + style: TextStyle( + fontSize: 28.rpx, + color: themeController.currentColor.sc4, + ), + ), ), + ); + } + + return SizedBox( + height: 200.rpx, + child: WidgetMarquee( + items: items, + itemWidth: 240.rpx, + spacing: 14.rpx, + speed: 30, ), ); } +} - void resetScroll() { - _hasScrolled = false; - if (_scrollController.hasClients) { - _scrollController.jumpTo(0); +/* -------------------------------------------------------------------------- */ +/* WidgetMarquee */ +/* -------------------------------------------------------------------------- */ + +// class WidgetMarquee extends StatefulWidget { +// final List items; +// final double itemWidth; +// final double spacing; +// final double speed; + +// const WidgetMarquee({ +// Key? key, +// required this.items, +// required this.itemWidth, +// this.spacing = 14, +// this.speed = 30, +// }) : super(key: key); + +// @override +// State createState() => _WidgetMarqueeState(); +// } + +// class _WidgetMarqueeState extends State { +// final ScrollController _scrollController = ScrollController(); + +// Timer? _timer; +// Timer? _resumeTimer; + +// double _scrollOffset = 0; +// double _totalWidth = 0; +// bool _needsMarquee = false; +// bool _isUserInteracting = false; + +// @override +// void initState() { +// super.initState(); +// WidgetsBinding.instance.addPostFrameCallback((_) { +// _calc(); +// _start(); +// }); +// } + +// void _calc() { +// final containerWidth = context.size?.width ?? 0; +// _totalWidth = widget.items.length * widget.itemWidth + +// (widget.items.length - 1) * widget.spacing; +// _needsMarquee = _totalWidth > containerWidth; +// } + +// /// ⭐ 核心:同步真实滚动位置,防止跳帧 +// void _syncScrollOffset() { +// if (!_scrollController.hasClients) return; + +// double current = _scrollController.position.pixels; + +// if (current >= _totalWidth) { +// current -= _totalWidth; +// _scrollController.jumpTo(current); +// } + +// _scrollOffset = current; +// } + +// void _start() { +// if (!_needsMarquee || _timer != null) return; + +// _timer = Timer.periodic(const Duration(milliseconds: 16), (_) { +// if (!_scrollController.hasClients || _isUserInteracting) return; + +// _scrollOffset += widget.speed / 60; + +// if (_scrollOffset >= _totalWidth) { +// _scrollOffset -= _totalWidth; +// } + +// _scrollController.jumpTo(_scrollOffset); +// }); +// } + +// void _stop() { +// _timer?.cancel(); +// _timer = null; +// } + +// void _resumeLater() { +// _resumeTimer?.cancel(); +// _resumeTimer = Timer(const Duration(seconds: 2), _start); +// } + +// Widget _buildRow() { +// return Row( +// children: widget.items +// .asMap() +// .entries +// .map((e) => Padding( +// padding: EdgeInsets.only( +// right: e.key < widget.items.length - 1 +// ? widget.spacing +// : 0, +// ), +// child: SizedBox( +// width: widget.itemWidth, +// child: e.value, +// ), +// )) +// .toList(), +// ); +// } + +// @override +// void dispose() { +// _stop(); +// _resumeTimer?.cancel(); +// _scrollController.dispose(); +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// if (!_needsMarquee) { +// return SingleChildScrollView( +// scrollDirection: Axis.horizontal, +// child: _buildRow(), +// ); +// } + +// return Listener( +// onPointerDown: (_) { +// _isUserInteracting = true; +// _stop(); +// }, +// onPointerUp: (_) { +// _isUserInteracting = false; +// _syncScrollOffset(); // ⭐ 防跳帧关键 +// _resumeLater(); +// }, +// onPointerCancel: (_) { +// _isUserInteracting = false; +// _syncScrollOffset(); +// _resumeLater(); +// }, +// child: SingleChildScrollView( +// controller: _scrollController, +// scrollDirection: Axis.horizontal, +// physics: const BouncingScrollPhysics(), +// child: Row( +// children: [ +// _buildRow(), +// _buildRow(), // 第二份用于无缝循环 +// ], +// ), +// ), +// ); +// } + +// } + +class WidgetMarquee extends StatefulWidget { + final List items; + final double itemWidth; + final double spacing; + final double speed; // px / second + + const WidgetMarquee({ + Key? key, + required this.items, + required this.itemWidth, + this.spacing = 14, + this.speed = 30, + }) : super(key: key); + + @override + State createState() => _WidgetMarqueeState(); +} + +class _WidgetMarqueeState extends State { + final ScrollController _scrollController = ScrollController(); + + Timer? _timer; + Timer? _resumeTimer; + + double _scrollOffset = 0; + double _totalWidth = 0; + + bool _needsMarquee = false; + bool _isUserInteracting = false; + + // ====================== + // 自动滚动控制 + // ====================== + + void _startMarquee() { + if (_timer != null || !_needsMarquee) return; + + _timer = Timer.periodic(const Duration(milliseconds: 16), (_) { + if (!_scrollController.hasClients || _isUserInteracting) return; + + _scrollOffset += widget.speed / 60; + + if (_scrollOffset >= _totalWidth) { + _scrollOffset -= _totalWidth; + } + + _scrollController.jumpTo(_scrollOffset); + }); + } + + void _stopMarquee() { + _timer?.cancel(); + _timer = null; + } + + void _resumeMarqueeWithDelay() { + _resumeTimer?.cancel(); + _resumeTimer = Timer(const Duration(seconds: 1), () { + if (!mounted) return; + _startMarquee(); + }); + } + + // ====================== + // 关键:同步真实滚动位置 + // ====================== + + void _syncScrollOffset() { + if (!_scrollController.hasClients) return; + + double current = _scrollController.position.pixels; + + if (current >= _totalWidth) { + current -= _totalWidth; + _scrollController.jumpTo(current); } + + _scrollOffset = current; + } + + // ====================== + // UI + // ====================== + + Widget _buildRow() { + return Row( + children: widget.items + .asMap() + .entries + .map( + (entry) => Padding( + padding: EdgeInsets.only( + right: entry.key < widget.items.length - 1 ? widget.spacing : 0, + ), + child: SizedBox( + width: widget.itemWidth, + child: entry.value, + ), + ), + ) + .toList(), + ); + } + + @override + void dispose() { + _timer?.cancel(); + _resumeTimer?.cancel(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.items.isEmpty) { + return const SizedBox.shrink(); + } + + return LayoutBuilder( + builder: (context, constraints) { + final containerWidth = constraints.maxWidth; + + _totalWidth = widget.items.length * widget.itemWidth + + (widget.items.length - 1) * widget.spacing; + + final needsMarquee = _totalWidth > containerWidth; + + /// ⭐ 第一次满足条件时立即启动自动滚动 + if (needsMarquee && !_needsMarquee) { + _needsMarquee = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _startMarquee(); + }); + } + + /// 不需要滚动 + if (!needsMarquee) { + _needsMarquee = false; + _stopMarquee(); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: _buildRow(), + ); + } + + /// 需要滚动(支持手势) + return Listener( + onPointerDown: (_) { + _isUserInteracting = true; + _stopMarquee(); + }, + onPointerUp: (_) { + _isUserInteracting = false; + _syncScrollOffset(); + _resumeMarqueeWithDelay(); + }, + onPointerCancel: (_) { + _isUserInteracting = false; + _syncScrollOffset(); + _resumeMarqueeWithDelay(); + }, + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + children: [ + _buildRow(), + SizedBox( + width: 20.rpx, + ), + _buildRow(), // 无缝循环 + ], + ), + ), + ); + }, + ); } } diff --git a/lib/pages/device/deviceCopy.dart b/lib/pages/device/deviceCopy.dart new file mode 100644 index 0000000..4d16d31 --- /dev/null +++ b/lib/pages/device/deviceCopy.dart @@ -0,0 +1,903 @@ +import 'dart:async'; +import 'dart:math'; +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/color/app_uri_status.dart'; +import 'package:vbvs_app/common/util/EventBus.dart'; +import 'package:vbvs_app/common/util/FitTool.dart'; +import 'package:vbvs_app/common/util/MyUtils.dart'; +import 'package:vbvs_app/common/util/eventType.dart'; +import 'package:vbvs_app/component/NullDataComponentWidget.dart'; +import 'package:vbvs_app/component/tool/ClickableContainer.dart'; +import 'package:vbvs_app/component/tool/CustomCard.dart'; +import 'package:vbvs_app/component/tool/TopSlideNotification.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/home/home_controller.dart'; +import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; +import 'package:vbvs_app/pages/device/component/DeviceDataComponentWidget.dart'; + +// 滚动通知事件类 +class ScrollNotificationEvent {} + +class BodyDeviceWidgetCopy extends StatefulWidget { + var type; + BodyDeviceWidgetCopy({super.key, required this.type}); + + @override + State createState() => _BodyDevicePageState(); +} + +class _BodyDevicePageState extends State + with SingleTickerProviderStateMixin { + final ThemeController themeController = Get.find(); + final BodyDeviceController bodyDeviceController = Get.find(); + HomeController homeController = Get.find(); + final GlobalKey addIconKey = GlobalKey(); + final ScrollController _myDeviceScrollController = ScrollController(); + final ScrollController _cloudDeviceScrollController = ScrollController(); + OverlayEntry? _popupEntry; + Timer? _timer; + late PageController _pageController; + + // 飘动文字的变量(简化版) + late AnimationController _floatController; + double _floatX = 0.0; // 水平位置(-1到1之间,表示屏幕宽度的百分比) + double _floatY = 0.0; // 垂直位置(-1到1之间,表示屏幕高度的百分比) + double _speedX = 0.005; // X方向速度(向右) + double _speedY = -0.005; // Y方向速度(负值表示向上,45度方向) + + void _showPopup() { + final renderBox = + addIconKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null) return; + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + double popupWidth = 190.rpx; + + _popupEntry?.remove(); + + _popupEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + ModalBarrier( + dismissible: true, + color: Colors.transparent, + onDismiss: _hidePopup, + ), + Positioned( + top: position.dy + size.height + 26.rpx, + left: position.dx + size.width - popupWidth - 40.rpx, + child: Material( + color: Colors.transparent, + child: Container( + width: popupWidth, + padding: EdgeInsets.all(20.rpx), + decoration: BoxDecoration( + color: themeController.currentColor.sc17, + borderRadius: BorderRadius.circular(12.rpx), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.5), + blurRadius: 12.rpx, + spreadRadius: 2.rpx, + offset: Offset(0, 6.rpx), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 11.rpx), + ClickableContainer( + padding: EdgeInsets.symmetric(vertical: 10.rpx), + backgroundColor: Colors.transparent, + highlightColor: + themeController.currentColor.sc16.withOpacity(0.1), + borderRadius: 0.rpx, + onTap: () { + _hidePopup(); + BlueteethBindController blueteethBindController = + Get.find(); + blueteethBindController.returnPage = 1; + Get.toNamed("/deviceType"); + }, + child: Container( + width: double.infinity, + child: Center( + child: Text( + '蓝牙绑定.标题'.tr, + style: TextStyle( + fontSize: AppConstants().normal_text_fontSize, + color: themeController.currentColor.sc3, + ), + ), + ), + ), + ), + SizedBox(height: 13.rpx), + ], + ), + ), + ), + ), + ], + ), + ); + + Overlay.of(context)!.insert(_popupEntry!); + } + + void _hidePopup() { + _popupEntry?.remove(); + _popupEntry = null; + } + + @override + void initState() { + bodyDeviceController.keyWord.value = ""; + super.initState(); + + // 初始化PageController,根据当前类型设置初始页面 + _pageController = PageController( + initialPage: bodyDeviceController.model.type == 1 ? 0 : 1); + + // 处理传入的type参数 + if (widget.type != null && widget.type is Map) { + final bindType = widget.type['bind_type']; + final mac = widget.type['mac']; + + if (bindType != null) { + bodyDeviceController.model.type = bindType; + homeController.model.type = bindType; + // 更新PageController到正确的位置 + _pageController = PageController( + initialPage: bodyDeviceController.model.type == 1 ? 0 : 1); + } + + _fetchDeviceList().then((_) { + if (mac != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToDeviceWithMac(mac); + }); + } + }); + } else { + _fetchDeviceList(); + } + + _timer = Timer.periodic(Duration(seconds: 5), (timer) { + _fetchDeviceList(); + }); + + // 初始化飘动动画 + _floatController = AnimationController( + duration: Duration(milliseconds: 16), // 约60帧/秒 + vsync: this, + )..repeat(); + + // 监听动画更新 + _floatController.addListener(_updateFloatPosition); + } + + // 更新飘动文字位置(简化版) + void _updateFloatPosition() { + if (!mounted) return; + + // 更新位置 + setState(() { + _floatX += _speedX; + _floatY += _speedY; + }); + + // 边界检测和反弹 + final screenWidth = MediaQuery.of(Get.context!).size.width; + final screenHeight = MediaQuery.of(Get.context!).size.height; + + // 文字尺寸 + final textWidth = 100.rpx; + final textHeight = 30.rpx; + + // 转换为实际像素位置 + final actualX = (_floatX + 1) / 2 * (screenWidth - textWidth); + final actualY = (_floatY + 1) / 2 * (screenHeight - textHeight); + + // 碰到右边界,向左反弹 + if (actualX >= screenWidth - textWidth - 10) { + setState(() { + _speedX = -_speedX.abs(); // 向左 + }); + } + // 碰到左边界,向右反弹 + else if (actualX <= 10) { + setState(() { + _speedX = _speedX.abs(); // 向右 + }); + } + + // 碰到下边界,向上反弹 + if (actualY >= screenHeight - textHeight - 80) { + // 留出底部空间 + setState(() { + _speedY = -_speedY.abs(); // 向上 + }); + } + // 碰到上边界(避开AppBar),向下反弹 + else if (actualY <= 180) { + // 留出AppBar和标签栏空间 + setState(() { + _speedY = _speedY.abs(); // 向下 + }); + } + } + + // 构建飘动文字组件(简化版) + Widget _buildFloatingText() { + return Positioned.fill( + child: IgnorePointer( + // 忽略点击,避免干扰其他交互 + child: Transform.translate( + offset: Offset( + (_floatX + 1) / 2 * (MediaQuery.of(context).size.width - 100.rpx), + (_floatY + 1) / 2 * (MediaQuery.of(context).size.height - 30.rpx), + ), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.rpx, + vertical: 6.rpx, + ), + decoration: BoxDecoration( + // color: themeController.currentColor.sc2.withOpacity(0.15), + color: Colors.transparent, + borderRadius: BorderRadius.circular(15.rpx), + // border: Border.all( + // color: themeController.currentColor.sc2.withOpacity(0.3), + // width: 1.rpx, + // ), + ), + child: Text( + '太和e护', + style: TextStyle( + fontSize: 20.rpx, + fontWeight: FontWeight.w500, + color: themeController.currentColor.sc2, + ), + ), + ), + ), + ), + ); + } + + Future _fetchDeviceList() async { + await bodyDeviceController + .getDeviceList(key: bodyDeviceController.keyWord.value) + .then((apiResponse) { + if (apiResponse.code != HttpStatusCodes.ok) { + TopSlideNotification.show( + Get.context!, + text: apiResponse.msg!, + textColor: themeController.currentColor.sc9, + ); + } + }); + } + + void _scrollToDeviceWithMac(String mac) { + final deviceList = bodyDeviceController.deviceList.value; + final index = deviceList.indexWhere((device) => device['mac'] == mac); + + if (index != -1) { + final screenHeight = MediaQuery.of(Get.context!).size.height; + final dynamicItemHeight = (screenHeight * 0.266).rpx; + final itemHeight = + dynamicItemHeight < 501.rpx ? 501.rpx : dynamicItemHeight; + final spacing = 25.rpx; + final targetPosition = index * (itemHeight + spacing); + + // 根据当前类型选择对应的ScrollController + final currentScrollController = bodyDeviceController.model.type == 1 + ? _myDeviceScrollController + : _cloudDeviceScrollController; + + if (currentScrollController.hasClients) { + currentScrollController.animateTo( + targetPosition, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } + } + } + + // 标签切换回调 + Future _onTabChanged(int index) async { + _pageController.animateToPage(index, + duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + BodyDeviceController deviceController = Get.find(); + + if (index == 0) { + deviceController.model.type = 1; + homeController.model.type = 1; + } else if (index == 1) { + deviceController.model.type = 2; + homeController.model.type = 2; + } + deviceController.updateAll(); + await deviceController.getDeviceList(); + await deviceController.getSleepReport(); + } + + // 页面切换回调 + void _onPageChanged(int index) { + int newType = index == 0 ? 1 : 2; + if (bodyDeviceController.model.type != newType) { + bodyDeviceController.model.type = newType; + homeController.model.type = newType; + bodyDeviceController.updateAll(); + homeController.updateAll(); + _fetchDeviceList(); + } + } + + @override + void dispose() { + _timer?.cancel(); + _myDeviceScrollController.dispose(); + _cloudDeviceScrollController.dispose(); + _pageController.dispose(); + _floatController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, bodysize) => GestureDetector( + onTap: () { + _hidePopup(); + FocusScope.of(context).unfocus(); + }, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/img/bgNoImg.png'), + fit: BoxFit.fill, + ), + ), + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: themeController.currentColor.sc17, + automaticallyImplyLeading: false, + iconTheme: IconThemeData( + color: themeController.currentColor.sc3, + ), + titleSpacing: 0, + title: Container( + width: double.infinity, + height: 180.rpx, + child: Stack( + alignment: Alignment.center, + children: [ + Text( + '体征检测设备.标题'.tr, + style: TextStyle( + fontFamily: 'ReadexPro', + color: themeController.currentColor.sc3, + letterSpacing: 0, + fontSize: 30.rpx, + ), + ), + Positioned( + left: 0, + child: returnIconButtomAddCallback(() { + bodyDeviceController.getDeviceNum(); + bodyDeviceController.getDeviceList(); + bodyDeviceController.updateAll(); + }), + ), + Positioned( + right: 20.rpx, + child: ClickableContainer( + key: addIconKey, + backgroundColor: Colors.transparent, + highlightColor: themeController.currentColor.sc16, + padding: EdgeInsets.all(8.rpx), + onTap: () { + if (_popupEntry == null) { + _showPopup(); + } else { + _hidePopup(); + } + }, + child: SvgPicture.asset( + 'assets/img/icon/add.svg', + width: 39.rpx, + height: 39.rpx, + color: themeController.currentColor.sc16, + ), + ), + ), + ], + ), + ), + actions: [], + centerTitle: false, + ), + body: GestureDetector( + onTap: _hidePopup, + child: SafeArea( + top: true, + child: Stack( + children: [ + Padding( + padding: + EdgeInsetsDirectional.fromSTEB(0.rpx, 0, 0.rpx, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + 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: [ + Obx(() { + return ClickableContainer( + backgroundColor: + Colors.transparent, + highlightColor: themeController + .currentColor.sc3, + borderRadius: 8.rpx, + padding: EdgeInsets.all(0), + onTap: () => _onTabChanged(0), + child: Column( + mainAxisSize: + MainAxisSize.max, + children: [ + Container( + width: 180.rpx, + alignment: + Alignment.center, + child: Text( + '体征检测设备.我的e护'.tr, + style: TextStyle( + fontFamily: 'Inter', + fontSize: AppConstants() + .title_text_fontSize, + letterSpacing: 0.0, + color: bodyDeviceController + .model + .type == + 2 + ? themeController + .currentColor + .sc3 + : themeController + .currentColor + .sc2, + ), + ), + ), + SizedBox(height: 10.rpx), + ], + ), + ); + }), + Obx(() { + return ClickableContainer( + backgroundColor: + Colors.transparent, + highlightColor: themeController + .currentColor.sc3, + borderRadius: 8.rpx, + padding: EdgeInsets.all(0), + onTap: () => _onTabChanged(1), + child: Column( + mainAxisSize: + MainAxisSize.max, + children: [ + Container( + width: 180.rpx, + alignment: + Alignment.center, + child: Text( + '体征检测设备.云关爱'.tr, + style: TextStyle( + fontFamily: 'Inter', + fontSize: AppConstants() + .title_text_fontSize, + letterSpacing: 0.0, + color: bodyDeviceController + .model + .type == + 1 + ? themeController + .currentColor + .sc3 + : themeController + .currentColor + .sc2, + ), + overflow: TextOverflow + .ellipsis, + maxLines: 1, + ), + ), + SizedBox(height: 10.rpx), + ], + ), + ); + }), + ], + ), + Obx(() { + // 保持原有的横线宽度180.rpx + double lineWidth = 180.rpx; + return AnimatedPositioned( + duration: + Duration(milliseconds: 300), + curve: Curves.easeInOut, + bottom: 0, + left: bodyDeviceController + .model.type == + 1 + ? 0 + : 180.rpx, + child: Container( + width: lineWidth, + height: 4.rpx, + decoration: BoxDecoration( + color: themeController + .currentColor.sc2, + borderRadius: + BorderRadius.circular( + 2.rpx), + ), + ), + ); + }), + ], + ), + // 搜索框部分保持不变 + Container( + width: MediaQuery.sizeOf(context).width * + 0.38, + constraints: BoxConstraints( + minWidth: 285.rpx, + ), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: + BorderRadius.circular(20.rpx), + ), + child: Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 0.rpx, 0, 20.rpx, 0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Container( + height: 80.rpx, + child: Align( + alignment: + AlignmentDirectional( + -1, 0), + child: TextFormField( + onChanged: (value) { + bodyDeviceController + .keyWord + .value = value; + }, + autofocus: false, + obscureText: false, + decoration: InputDecoration( + contentPadding: + EdgeInsets.fromLTRB( + 12.rpx, + 0, + 0.rpx, + 0), + isDense: true, + labelStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + ), + hintText: + '体征检测设备.输入关键词'.tr, + hintStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc4, + ), + enabledBorder: + OutlineInputBorder( + borderSide: BorderSide( + color: + Color(0x00000000), + width: 1.rpx, + ), + borderRadius: + BorderRadius + .circular( + 8.rpx), + ), + focusedBorder: + OutlineInputBorder( + borderSide: BorderSide( + color: + Color(0x00000000), + width: 1.rpx, + ), + borderRadius: + BorderRadius + .circular( + 8.rpx), + ), + errorBorder: + OutlineInputBorder( + borderSide: BorderSide( + color: Colors.red, + width: 1.rpx, + ), + borderRadius: + BorderRadius + .circular( + 8.rpx), + ), + focusedErrorBorder: + OutlineInputBorder( + borderSide: BorderSide( + color: Colors.red, + width: 1.rpx, + ), + borderRadius: + BorderRadius + .circular( + 8.rpx), + ), + filled: false, + fillColor: Colors.white, + ), + style: TextStyle( + fontFamily: 'Inter', + fontSize: 26.rpx, + letterSpacing: 0.0, + color: themeController + .currentColor.sc3, + ), + cursorColor: themeController + .currentColor.sc3, + ), + ), + ), + ), + Padding( + padding: EdgeInsetsDirectional + .fromSTEB(26.rpx, 0, 0, 0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + height: 40.rpx, + child: VerticalDivider( + thickness: 2.rpx, + color: themeController + .currentColor.sc2, + ), + ), + ClickableContainer( + backgroundColor: + Colors.transparent, + highlightColor: + themeController + .currentColor.sc5, + borderRadius: 6.rpx, + padding: EdgeInsets.zero, + onTap: () async { + await bodyDeviceController + .getDeviceList( + key: + bodyDeviceController + .keyWord + .value); + bodyDeviceController + .updateAll(); + }, + child: Text( + '体征检测设备.搜索'.tr, + style: TextStyle( + fontFamily: 'Inter', + fontSize: AppConstants() + .normal_text_fontSize, + letterSpacing: 0.0, + color: themeController + .currentColor.sc2, + ), + ), + ), + ].divide( + SizedBox(width: 14.rpx)), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + // 使用PageView替换原来的单一列表 + Expanded( + child: NotificationListener( + onNotification: + (ScrollNotification notification) { + if (notification is ScrollStartNotification || + notification is ScrollUpdateNotification) { + // 发送全局滚动事件 + EventBus().emit(ScrollNotificationEvent()); + } + return false; + }, + child: PageView( + controller: _pageController, + onPageChanged: _onPageChanged, + children: [ + // 我的e护页面 + Obx(() { + final myDeviceList = bodyDeviceController + .deviceList.value + .where((device) => + device['type'] == 1 || + device['bind_type'] == 1) + .toList(); + return myDeviceList.isEmpty + ? NullDataWidget() + : Padding( + padding: + EdgeInsetsDirectional.fromSTEB( + 30.rpx, 26.rpx, 30.rpx, 0), + child: SingleChildScrollView( + controller: + _myDeviceScrollController, + child: Column( + mainAxisSize: MainAxisSize.max, + children: myDeviceList + .map((device) => + DeviceDataComponentWidget( + device: device)) + .toList() + .divide(SizedBox( + height: 25.rpx)), + ), + ), + ); + }), + // 云关爱页面 + Obx(() { + final cloudDeviceList = bodyDeviceController + .deviceList.value + .where((device) => + device['type'] == 2 || + device['bind_type'] == 2) + .toList(); + return cloudDeviceList.isEmpty + ? NullDataWidget() + : Padding( + padding: + EdgeInsetsDirectional.fromSTEB( + 30.rpx, 26.rpx, 30.rpx, 0), + child: SingleChildScrollView( + controller: + _cloudDeviceScrollController, + child: Column( + mainAxisSize: MainAxisSize.max, + children: cloudDeviceList + .map((device) => + DeviceDataComponentWidget( + device: device)) + .toList() + .divide(SizedBox( + height: 25.rpx)), + ), + ), + ); + }), + ], + ), + ), + ), + ], + ), + ), + // 添加飘动文字(简化版) + _buildFloatingText(), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildDeviceCard(BuildContext context, + {required String title, required String imageUrl, required String type}) { + return CustomCard( + borderRadius: 20.rpx, + onTap: () { + if (type != null) { + if (type == '1') { + Get.toNamed("/blueteethDevice"); + } + } + }, + colors: [themeController.currentColor.sc17], + child: Container( + width: double.infinity, + height: MediaQuery.sizeOf(context).height * 0.135, + constraints: BoxConstraints( + minHeight: 220.rpx, + ), + padding: EdgeInsetsDirectional.fromSTEB(77.rpx, 0, 21.rpx, 0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: TextStyle( + fontFamily: 'Inter', + color: const Color(0xFFC2CED7), + fontSize: 30.rpx, + letterSpacing: 0.0, + ), + ), + ClipRRect( + borderRadius: BorderRadius.circular(8.rpx), + child: Image.asset( + imageUrl, + width: 212.rpx, + height: 168.rpx, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/main_bottom/mine_page.dart b/lib/pages/main_bottom/mine_page.dart index 65b82f5..e1f742e 100644 --- a/lib/pages/main_bottom/mine_page.dart +++ b/lib/pages/main_bottom/mine_page.dart @@ -703,7 +703,7 @@ class _MinePageState extends State { mainAxisSize: MainAxisSize.max, children: [ Text( - 'V1.0.2601.06', + 'V1.0.2601.08', style: TextStyle( fontFamily: 'Inter', // color: Color(0xFFD9E3EB), diff --git a/lib/pages/mh_page/new_settingPage.dart b/lib/pages/mh_page/new_settingPage.dart index c49f6e3..fd9f261 100644 --- a/lib/pages/mh_page/new_settingPage.dart +++ b/lib/pages/mh_page/new_settingPage.dart @@ -253,7 +253,7 @@ class _SettingPageState extends State { ), ].divide(SizedBox(width: 22.rpx)), ), - Text('SWES2026.1.5', + Text('SWES2026.1.8', style: TextStyle( color: Colors.white, fontSize: 26.rpx, diff --git a/lib/pages/policy/privacy_policy.dart b/lib/pages/policy/privacy_policy.dart index 260a445..543b41e 100644 --- a/lib/pages/policy/privacy_policy.dart +++ b/lib/pages/policy/privacy_policy.dart @@ -153,7 +153,7 @@ class _PrivacyPolicyNewPageState extends State { return Stack( children: [ Padding( - padding: EdgeInsets.fromLTRB(0, 0, 30.rpx, 0), + padding: EdgeInsets.fromLTRB(0, 0, 0.rpx, 0), child: InAppWebView( key: UniqueKey(), initialUrlRequest: URLRequest(url: WebUri(widget.sleepUri)), diff --git a/lib/pages/sleep_report/chart/LineChartByRange.dart b/lib/pages/sleep_report/chart/LineChartByRange.dart index 7f55960..d6af96c 100644 --- a/lib/pages/sleep_report/chart/LineChartByRange.dart +++ b/lib/pages/sleep_report/chart/LineChartByRange.dart @@ -1,3 +1,361 @@ +// import 'dart:math'; +// import 'dart:ui' as ui; + +// import 'package:ef/ef.dart'; +// 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'; + +// 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 (widget.showLabel.isEmpty) return const SizedBox(); + +// 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 / 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' +// "时长" +// .tr + +// ' ${selectedData!['times']}', +// style: TextStyle( +// fontSize: 18.rpx, +// color: Colors.white, +// ), +// ), +// ), +// ), +// ], +// ), +// ); +// } +// } + +// class _LineChartByRangePainter extends CustomPainter { +// final List> data; +// final int maxY; +// final DateTime minTime; +// final DateTime maxTime; +// final int? threshold; +// final int ySegments; + +// _LineChartByRangePainter({ +// required this.data, +// 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; + +// 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 axisPaint = Paint() +// ..color = Colors.grey.withOpacity(0.4) +// ..strokeWidth = 1.rpx; + +// Paint thresholdPaint = Paint() +// ..color = themeController.currentColor.sc9 +// ..strokeWidth = 1.rpx; + +// // 阈值虚线(红色) +// 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']; +// // int times = item['times']; +// int times = item['times']; + +// double startX = xStart * 2 + +// chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration; +// double endX = xStart * 2 + +// chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration; +// 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), dynamicLinePaint); + +// // 画起点和终点圆点 +// canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint); +// canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint); +// } + +// // Y轴辅助线和文字 +// for (int i = 0; i <= ySegments; i++) { +// double y = chartHeight * i / ySegments; + +// if (i == ySegments) { +// 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: '${maxY - (maxY * i / ySegments).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)); +// } + +// // X轴主线 +// canvas.drawLine( +// Offset(xStart, chartHeight), +// Offset(xEnd, chartHeight), +// axisPaint, +// ); + +// // 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)); + +// // 中间小时刻度 +// 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 'dart:math'; import 'dart:ui' as ui; @@ -199,7 +557,6 @@ class _LineChartByRangePainter extends CustomPainter { for (var item in data) { int start = item['startTime']; int end = item['endTime']; - // int times = item['times']; int times = item['times']; double startX = xStart * 2 + @@ -305,28 +662,121 @@ class _LineChartByRangePainter extends CustomPainter { Offset(size.width - padding - labelInset - rightTp.width / 2, chartHeight + 8.rpx)); - // 中间小时刻度 - int totalHours = maxTime.difference(minTime).inHours + 1; - int startHour = minTime.hour; + // 中间小时刻度 - 使用实际时间比例 + final int hourMs = 60 * 60 * 1000; + final int totalHours = + (maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch) ~/ + hourMs; - 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, + if (totalHours <= 8) { + // 显示每个整点 + DateTime currentHour = DateTime( + minTime.year, + minTime.month, + minTime.day, + minTime.hour, ); - tp.layout(); - tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx)); + + // 如果起始时间不是整点,从下一个整点开始 + if (minTime.minute > 0 || minTime.second > 0 || minTime.millisecond > 0) { + currentHour = currentHour.add(Duration(hours: 1)); + } + + while ( + currentHour.millisecondsSinceEpoch < maxTime.millisecondsSinceEpoch) { + final int timeMs = currentHour.millisecondsSinceEpoch; + + if (timeMs > minTime.millisecondsSinceEpoch && + timeMs < maxTime.millisecondsSinceEpoch) { + // 检查是否太靠近边界 + if (timeMs - minTime.millisecondsSinceEpoch < 10 * 60 * 1000 || + maxTime.millisecondsSinceEpoch - timeMs < 10 * 60 * 1000) { + currentHour = currentHour.add(Duration(hours: 1)); + continue; + } + + // 计算从起始时间到当前整点的位置 + final double x = xStart + + chartWidth * + (timeMs - minTime.millisecondsSinceEpoch) / + totalDuration; + + final hourLabel = '${currentHour.hour}'; + + 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), + ); + } + + currentHour = currentHour.add(Duration(hours: 1)); + } + } else { + // 超过8小时,跳着显示 + final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签 + + DateTime firstLabelHour = DateTime( + minTime.year, + minTime.month, + minTime.day, + minTime.hour + (labelInterval - (minTime.hour % labelInterval)), + 0, + 0, + 0, + 0, + ); + + // 确保第一个标签在开始时间之后 + if (firstLabelHour.millisecondsSinceEpoch <= + minTime.millisecondsSinceEpoch) { + firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval)); + } + + DateTime currentHour = firstLabelHour; + while ( + currentHour.millisecondsSinceEpoch < maxTime.millisecondsSinceEpoch) { + final int timeMs = currentHour.millisecondsSinceEpoch; + + // 确保标签离边界足够远 + if (timeMs - minTime.millisecondsSinceEpoch >= hourMs && + maxTime.millisecondsSinceEpoch - timeMs >= hourMs) { + final double x = xStart + + chartWidth * + (timeMs - minTime.millisecondsSinceEpoch) / + totalDuration; + + final hourLabel = '${currentHour.hour}'; + + 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), + ); + } + + currentHour = currentHour.add(Duration(hours: labelInterval)); + } } } diff --git a/lib/pages/sleep_report/chart/SnoreChart.dart b/lib/pages/sleep_report/chart/SnoreChart.dart index 0df02d6..02ad47c 100644 --- a/lib/pages/sleep_report/chart/SnoreChart.dart +++ b/lib/pages/sleep_report/chart/SnoreChart.dart @@ -1,3 +1,364 @@ +// 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 StatefulWidget { +// 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 +// State createState() => _BarChartWidgetState(); +// } + +// class _BarChartWidgetState extends State { +// BarData? selectedBar; + +// void _handleTapOrDrag(Offset localPosition, Size size) { +// final chartWidth = size.width - 30.rpx; +// final totalDuration = widget.endTime - widget.startTime; + +// for (final d in widget.data) { +// final left = +// ((d.st - widget.startTime) / totalDuration) * chartWidth + 30.rpx; +// final right = +// ((d.et - widget.startTime) / totalDuration) * chartWidth + 30.rpx; +// if (localPosition.dx >= left && localPosition.dx <= right) { +// setState(() { +// selectedBar = d; +// }); +// return; +// } +// } + +// setState(() { +// selectedBar = null; +// }); +// } + +// @override +// Widget build(BuildContext context) { +// return LayoutBuilder(builder: (context, constraints) { +// return GestureDetector( +// behavior: HitTestBehavior.opaque, +// onPanDown: (details) => +// _handleTapOrDrag(details.localPosition, constraints.biggest), +// onPanUpdate: (details) => +// _handleTapOrDrag(details.localPosition, constraints.biggest), +// onTapDown: (details) => +// _handleTapOrDrag(details.localPosition, constraints.biggest), +// child: CustomPaint( +// size: Size(constraints.maxWidth, 250.rpx), // 使用约束的最大宽度 +// painter: BarChartPainter( +// widget.data, +// widget.startTime, +// widget.endTime, +// maxYValue: widget.maxYValue, +// yStepCount: widget.yStepCount, +// selectedBar: selectedBar, +// ), +// ), +// ); +// }); +// } +// } + +// class BarChartPainter extends CustomPainter { +// final List data; +// final int startTime; +// final int endTime; +// final double maxYValue; +// final int yStepCount; +// final BarData? selectedBar; + +// final double topPadding = 0; +// final double bottomPadding = 0; +// final double leftPadding = 30.rpx; + +// BarChartPainter( +// this.data, +// this.startTime, +// this.endTime, { +// required this.maxYValue, +// this.yStepCount = 5, +// this.selectedBar, +// }); + +// @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; + +// // final dashPaint = Paint() +// // ..color = Colors.grey.withOpacity(0.4) +// // ..strokeWidth = 1.rpx; + +// // drawDashedLine( +// // canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); + +// // textPainter.text = TextSpan( +// // text: value.toStringAsFixed(0), +// // style: TextStyle( +// // fontSize: 18.rpx, +// // color: themeController.currentColor.sc4, +// // ), +// // ); +// // textPainter.layout(); +// // textPainter.paint( +// // canvas, +// // Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), +// // ); +// // } +// // Y轴刻度 +// for (int i = 0; i <= yStepCount; i++) { +// final value = stepValue * i; +// final y = topPadding + chartHeight - (value / maxYValue) * chartHeight; + +// // 判断是否是基线(i == 0) +// final bool isBaseline = i == 0; + +// if (isBaseline) { +// // 基线画实线 +// final baselinePaint = Paint() +// ..color = Colors.grey.withOpacity(0.6) +// ..strokeWidth = 1.rpx +// ..style = PaintingStyle.stroke; + +// canvas.drawLine( +// Offset(leftPadding, y), +// Offset(size.width, y), +// baselinePaint, +// ); +// } else { +// // 其他刻度画虚线 +// final dashPaint = Paint() +// ..color = Colors.grey.withOpacity(0.4) +// ..strokeWidth = 1.rpx; + +// drawDashedLine( +// canvas, +// Offset(leftPadding, y), +// Offset(size.width, y), +// dashPaint, +// ); +// } + +// // 绘制刻度文字 +// textPainter.text = TextSpan( +// text: value.toStringAsFixed(0), +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, +// Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), +// ); +// } + +// // X轴刻度 - 参考横线图的24小时制 +// final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); +// final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); +// final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标 + +// // 计算总小时数 +// final totalHours = endDate.difference(startDate).inHours + 1; +// final startHour = startDate.hour; + +// // 绘制X轴主线(实线) +// // final xAxisPaint = Paint() +// // ..color = Colors.grey.withOpacity(0.4) +// // ..strokeWidth = 1.rpx; +// // canvas.drawLine( +// // Offset(leftPadding, xAxisY), +// // Offset(size.width, xAxisY), +// // xAxisPaint, +// // ); + +// // 绘制左右两侧时间标签(HH:mm格式) +// final leftLabel = DateFormat('HH:mm').format(startDate); +// textPainter.text = TextSpan( +// text: leftLabel, +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx)); + +// final rightLabel = DateFormat('HH:mm').format(endDate); +// textPainter.text = TextSpan( +// text: rightLabel, +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx)); + +// // 绘制中间小时刻度(只显示小时数字) +// for (int i = 1; i < totalHours; i++) { +// final double x = leftPadding + chartWidth * i / totalHours; + +// int hourLabelNum = (startHour + i) % 24; +// final hourLabel = '$hourLabelNum'; + +// textPainter.text = TextSpan( +// text: hourLabel, +// style: TextStyle( +// fontSize: 18.rpx, +// color: themeController.currentColor.sc4, +// ), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx)); +// } + +// // 柱子绘制 & 提示信息缓存 +// Offset? tipOffset; +// Size? tipSize; +// String? tipText; +// Color tipColor = Colors.black; + +// 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, +// ); + +// // 缓存 tip 信息 +// if (selectedBar == d) { +// tipText = +// '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}'; + +// final tp = TextPainter( +// text: TextSpan( +// text: tipText, +// style: TextStyle(fontSize: 16.rpx, color: Colors.white), +// ), +// textAlign: TextAlign.center, +// textDirection: ui.TextDirection.ltr, +// ); +// tp.layout(); + +// final tipWidth = tp.width + 20.rpx; +// final tipHeight = tp.height + 10.rpx; +// final tipLeft = left + (right - left) / 2 - tipWidth / 2; +// final tipTop = top - tipHeight - 10.rpx; + +// // 确保tip不会超出画布顶部 +// final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop; + +// tipOffset = Offset(tipLeft, adjustedTipTop); +// tipSize = Size(tipWidth, tipHeight); +// tipColor = Colors.black.withOpacity(0.8); +// } +// } + +// // 绘制 tip(在柱子之上) +// if (tipText != null && tipOffset != null && tipSize != null) { +// final rect = RRect.fromRectAndRadius( +// Rect.fromLTWH( +// tipOffset.dx, tipOffset.dy, tipSize.width, tipSize.height), +// Radius.circular(8.rpx), +// ); +// final tipBgPaint = Paint()..color = tipColor; +// canvas.drawRRect(rect, tipBgPaint); + +// final tipTextPainter = TextPainter( +// text: TextSpan( +// text: tipText, +// style: TextStyle(fontSize: 16.rpx, color: Colors.white), +// ), +// textAlign: TextAlign.center, +// textDirection: ui.TextDirection.ltr, +// ); +// tipTextPainter.layout(); +// tipTextPainter.paint( +// canvas, +// Offset(tipOffset.dx + 10.rpx, tipOffset.dy + 5.rpx), +// ); +// } +// } + +// @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; +// } +// } +// } + import 'dart:ui' as ui; //柱形图显示 @@ -127,31 +488,6 @@ class BarChartPainter extends CustomPainter { 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; - - // final dashPaint = Paint() - // ..color = Colors.grey.withOpacity(0.4) - // ..strokeWidth = 1.rpx; - - // drawDashedLine( - // canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); - - // textPainter.text = TextSpan( - // text: value.toStringAsFixed(0), - // style: TextStyle( - // fontSize: 18.rpx, - // color: themeController.currentColor.sc4, - // ), - // ); - // textPainter.layout(); - // textPainter.paint( - // canvas, - // Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), - // ); - // } // Y轴刻度 for (int i = 0; i <= yStepCount; i++) { final value = stepValue * i; @@ -201,24 +537,14 @@ class BarChartPainter extends CustomPainter { ); } - // X轴刻度 - 参考横线图的24小时制 + // X轴刻度 - 基于实际时间而不是均匀分布 final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标 // 计算总小时数 - final totalHours = endDate.difference(startDate).inHours + 1; - final startHour = startDate.hour; - - // 绘制X轴主线(实线) - // final xAxisPaint = Paint() - // ..color = Colors.grey.withOpacity(0.4) - // ..strokeWidth = 1.rpx; - // canvas.drawLine( - // Offset(leftPadding, xAxisY), - // Offset(size.width, xAxisY), - // xAxisPaint, - // ); + final int hourMs = 60 * 60 * 1000; + final int totalHours = (endTime - startTime) ~/ hourMs; // 绘制左右两侧时间标签(HH:mm格式) final leftLabel = DateFormat('HH:mm').format(startDate); @@ -245,23 +571,101 @@ class BarChartPainter extends CustomPainter { textPainter.paint( canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx)); - // 绘制中间小时刻度(只显示小时数字) - for (int i = 1; i < totalHours; i++) { - final double x = leftPadding + chartWidth * i / totalHours; - - int hourLabelNum = (startHour + i) % 24; - final hourLabel = '$hourLabelNum'; - - textPainter.text = TextSpan( - text: hourLabel, - style: TextStyle( - fontSize: 18.rpx, - color: themeController.currentColor.sc4, - ), + // 绘制中间小时刻度 - 参考SnoreChartContainer的逻辑 + if (totalHours <= 8) { + // 显示每个整点 + DateTime currentHour = DateTime( + startDate.year, + startDate.month, + startDate.day, + startDate.hour, ); - textPainter.layout(); - textPainter.paint( - canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx)); + + // 如果起始时间不是整点,从下一个整点开始 + if (startDate.minute > 0 || + startDate.second > 0 || + startDate.millisecond > 0) { + currentHour = currentHour.add(Duration(hours: 1)); + } + + while (currentHour.millisecondsSinceEpoch < endTime) { + final int timeMs = currentHour.millisecondsSinceEpoch; + + if (timeMs > startTime && timeMs < endTime) { + // 检查是否太靠近边界 + if (timeMs - startTime < 10 * 60 * 1000 || + endTime - timeMs < 10 * 60 * 1000) { + currentHour = currentHour.add(Duration(hours: 1)); + continue; + } + + // 计算从起始时间到当前整点的分钟数 + final double x = + leftPadding + ((timeMs - startTime) / totalDuration) * chartWidth; + final hourLabel = '${currentHour.hour}'; + + textPainter.text = TextSpan( + text: hourLabel, + style: TextStyle( + fontSize: 18.rpx, + color: themeController.currentColor.sc4, + ), + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset(x - textPainter.width / 2, xAxisY + 8.rpx), + ); + } + + currentHour = currentHour.add(Duration(hours: 1)); + } + } else { + // 超过8小时,跳着显示 + final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签 + + DateTime firstLabelHour = DateTime( + startDate.year, + startDate.month, + startDate.day, + startDate.hour + (labelInterval - (startDate.hour % labelInterval)), + 0, + 0, + 0, + 0, + ); + + // 确保第一个标签在开始时间之后 + if (firstLabelHour.millisecondsSinceEpoch <= startTime) { + firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval)); + } + + DateTime currentHour = firstLabelHour; + while (currentHour.millisecondsSinceEpoch < endTime) { + final int timeMs = currentHour.millisecondsSinceEpoch; + + // 确保标签离边界足够远 + if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) { + final double x = + leftPadding + ((timeMs - startTime) / totalDuration) * chartWidth; + final hourLabel = '${currentHour.hour}'; + + textPainter.text = TextSpan( + text: hourLabel, + style: TextStyle( + fontSize: 18.rpx, + color: themeController.currentColor.sc4, + ), + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset(x - textPainter.width / 2, xAxisY + 8.rpx), + ); + } + + currentHour = currentHour.add(Duration(hours: labelInterval)); + } } // 柱子绘制 & 提示信息缓存 @@ -358,330 +762,3 @@ class BarChartPainter extends CustomPainter { } } } - -//横线显示 -// import 'dart:math'; -// import 'dart:ui' as ui; - -// 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'; - -// 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 StatefulWidget { -// 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 -// State createState() => _BarChartWidgetState(); -// } - -// class _BarChartWidgetState extends State { -// BarData? selectedBar; - -// void _handleTapOrDrag(Offset localPosition, Size size) { -// print('点击位置: $localPosition, 画布大小: $size'); -// final chartWidth = size.width - 30.rpx; -// final totalDuration = widget.endTime - widget.startTime; - -// // 使用与绘制相同的chartHeight计算方式 -// final double topPadding = 0; -// final double bottomPadding = 0; -// final double leftPadding = 30.rpx; -// final double chartHeight = size.height - topPadding - bottomPadding; - -// for (final d in widget.data) { -// final left = ((d.st - widget.startTime) / totalDuration) * chartWidth + -// leftPadding; -// final right = ((d.et - widget.startTime) / totalDuration) * chartWidth + -// leftPadding; - -// // 使用与绘制相同的Y坐标计算方式 -// final y = topPadding + chartHeight * (1 - d.value / widget.maxYValue); - -// // 判断点击是否在横线附近(增加容差范围) -// if (localPosition.dx >= left - 5.rpx && -// localPosition.dx <= right + 5.rpx && -// (localPosition.dy - y).abs() < 15.rpx) { -// setState(() { -// selectedBar = d; -// }); -// return; -// } -// } - -// setState(() { -// selectedBar = null; -// }); -// } - -// @override -// Widget build(BuildContext context) { -// return LayoutBuilder(builder: (context, constraints) { -// return GestureDetector( -// behavior: HitTestBehavior.opaque, -// onPanDown: (details) => -// _handleTapOrDrag(details.localPosition, constraints.biggest), -// onPanUpdate: (details) => -// _handleTapOrDrag(details.localPosition, constraints.biggest), -// onTapDown: (details) => -// _handleTapOrDrag(details.localPosition, constraints.biggest), -// child: CustomPaint( -// // size: Size(double.infinity, 500.rpx), -// size: Size(constraints.maxWidth, 500.rpx), // 使用约束的最大宽度 -// painter: BarChartPainter( -// widget.data, -// widget.startTime, -// widget.endTime, -// maxYValue: widget.maxYValue, -// yStepCount: widget.yStepCount, -// selectedBar: selectedBar, -// ), -// ), -// ); -// }); -// } -// } - -// class BarChartPainter extends CustomPainter { -// final List data; -// final int startTime; -// final int endTime; -// final double maxYValue; -// final int yStepCount; -// final BarData? selectedBar; - -// final double topPadding = 0; -// final double bottomPadding = 0; -// final double leftPadding = 30.rpx; - -// BarChartPainter( -// this.data, -// this.startTime, -// this.endTime, { -// required this.maxYValue, -// this.yStepCount = 5, -// this.selectedBar, -// }); - -// @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 = maxYValue - (stepValue * i); // 从最大值开始递减 -// final y = topPadding + chartHeight * i / yStepCount; - -// final dashPaint = Paint() -// ..color = Colors.grey.withOpacity(0.4) -// ..strokeWidth = 1.rpx; - -// // 最上面的线(i == ySegments)是实线 -// if (i == yStepCount) { -// canvas.drawLine( -// Offset(leftPadding, y), Offset(size.width, y), dashPaint); -// } else { -// // 其他线都是虚线 -// drawDashedLine( -// canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); -// } - -// textPainter.text = TextSpan( -// text: value.toStringAsFixed(0), -// style: TextStyle( -// fontSize: 18.rpx, -// color: themeController.currentColor.sc4, -// ), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, -// Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), -// ); -// } - -// // X轴刻度 - 24小时制 -// final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); -// final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); -// final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标 - -// // 计算总小时数 -// final totalHours = endDate.difference(startDate).inHours + 1; -// final startHour = startDate.hour; - -// // 绘制X轴主线(实线) -// final xAxisPaint = Paint() -// ..color = Colors.grey.withOpacity(0.4) -// ..strokeWidth = 1.rpx; -// canvas.drawLine( -// Offset(leftPadding, xAxisY), -// Offset(size.width, xAxisY), -// xAxisPaint, -// ); - -// // 绘制左右两侧时间标签 -// final leftLabel = DateFormat('HH:mm').format(startDate); -// textPainter.text = TextSpan( -// text: leftLabel, -// style: TextStyle( -// fontSize: 18.rpx, -// color: themeController.currentColor.sc4, -// ), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx)); - -// final rightLabel = DateFormat('HH:mm').format(endDate); -// textPainter.text = TextSpan( -// text: rightLabel, -// style: TextStyle( -// fontSize: 18.rpx, -// color: themeController.currentColor.sc4, -// ), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx)); - -// // 绘制中间小时刻度 -// for (int i = 1; i < totalHours; i++) { -// final double x = leftPadding + chartWidth * i / totalHours; - -// int hourLabelNum = (startHour + i) % 24; -// final hourLabel = '$hourLabelNum'; - -// textPainter.text = TextSpan( -// text: hourLabel, -// style: TextStyle( -// fontSize: 18.rpx, -// color: themeController.currentColor.sc4, -// ), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx)); -// } - -// // 绘制数据横线(根据数据值绘制水平线段) -// for (final d in data) { -// final left = -// ((d.st - startTime) / totalDuration) * chartWidth + leftPadding; -// final right = -// ((d.et - startTime) / totalDuration) * chartWidth + leftPadding; - -// // 根据value计算Y位置(0在底部,maxYValue在顶部) -// final y = topPadding + chartHeight * (1 - d.value / maxYValue); - -// final linePaint = Paint() -// ..style = PaintingStyle.stroke -// ..strokeWidth = 3.rpx -// ..color = d.color -// ..strokeCap = StrokeCap.round; - -// // 画水平线段 -// canvas.drawLine(Offset(left, y), Offset(right, y), linePaint); -// } - -// // 如果选中了某条横线,显示提示信息 -// // 如果选中了某条横线,显示提示信息 -// if (selectedBar != null) { -// final d = selectedBar!; -// final left = -// ((d.st - startTime) / totalDuration) * chartWidth + leftPadding; -// final right = -// ((d.et - startTime) / totalDuration) * chartWidth + leftPadding; -// final y = topPadding + chartHeight * (1 - d.value / maxYValue); - -// final tipText = -// '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}'; - -// final tp = TextPainter( -// text: TextSpan( -// text: tipText, -// style: TextStyle(fontSize: 16.rpx, color: Colors.white), -// ), -// textAlign: TextAlign.center, -// textDirection: ui.TextDirection.ltr, -// ); -// tp.layout(); - -// final tipWidth = tp.width + 20.rpx; -// final tipHeight = tp.height + 10.rpx; -// final tipLeft = left + (right - left) / 2 - tipWidth / 2; -// final tipTop = y - tipHeight - 10.rpx; - -// // 确保tip不会超出画布顶部 - 明确转换为double -// final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop; - -// // 绘制tip背景 -// final rect = RRect.fromRectAndRadius( -// Rect.fromLTWH(tipLeft, adjustedTipTop, tipWidth, tipHeight), -// Radius.circular(8.rpx), -// ); -// final tipBgPaint = Paint()..color = Colors.black.withOpacity(0.8); -// canvas.drawRRect(rect, tipBgPaint); - -// // 绘制tip文字 -// tp.paint( -// canvas, -// Offset(tipLeft + 10.rpx, adjustedTipTop + 5.rpx), -// ); -// } -// } - -// @override -// bool shouldRepaint(covariant CustomPainter oldDelegate) => true; - -// void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint, -// {double dashWidth = 4, double dashSpace = 3}) { -// 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; -// } -// } -// } diff --git a/lib/pages/sleep_report/chart/SnoreWaveform.dart b/lib/pages/sleep_report/chart/SnoreWaveform.dart index 8993fdd..d635dac 100644 --- a/lib/pages/sleep_report/chart/SnoreWaveform.dart +++ b/lib/pages/sleep_report/chart/SnoreWaveform.dart @@ -1,410 +1,3 @@ -// import 'dart:ui' as ui; -// import 'package:flutter/material.dart'; -// import 'package:flutterflow_ui/flutterflow_ui.dart'; -// import 'package:vbvs_app/common/util/FitTool.dart'; -// import 'package:vbvs_app/common/util/MyUtils.dart'; -// import 'package:intl/intl.dart'; - -// class SnoreChartContainer extends StatelessWidget { -// final List snoreValues; -// final List barData; -// final List showLabel; -// final int startTime; -// final int endTime; - -// const SnoreChartContainer({ -// required this.snoreValues, -// required this.barData, -// required this.showLabel, -// required this.startTime, -// required this.endTime, -// super.key, -// }); - -// @override -// Widget build(BuildContext context) { -// return Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// SnoreBarOverlay( -// barData: barData, -// showLabel: showLabel, -// startTime: startTime, -// endTime: endTime, -// ), -// Container(height: 32.rpx), -// Container( -// height: 23.rpx, -// child: SnoreWaveform( -// snoreValues: snoreValues, -// startTime: startTime, -// endTime: endTime, -// ), -// ), -// ], -// ); -// } -// } - -// class SnoreBarOverlay extends StatelessWidget { -// final List barData; -// final List showLabel; -// final int startTime; -// final int endTime; - -// const SnoreBarOverlay({ -// required this.barData, -// required this.showLabel, -// required this.startTime, -// required this.endTime, -// super.key, -// }); - -// @override -// Widget build(BuildContext context) { -// const double barHeight = 50; -// return SizedBox( -// height: barHeight, -// child: CustomPaint( -// size: Size(double.infinity, barHeight), -// painter: SnoreBarPainter( -// barData: barData, -// showLabel: showLabel, -// startTime: startTime, -// endTime: endTime, -// ), -// ), -// ); -// } -// } - -// class SnoreBarPainter extends CustomPainter { -// final List barData; -// final List showLabel; -// final int startTime; -// final int endTime; - -// SnoreBarPainter({ -// required this.barData, -// required this.showLabel, -// 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 pixelPerMs = width / (endTime - startTime); - -// for (var item in barData) { -// final int st = item['st']; -// final int et = item['et']; -// final int type = item['type']; -// int heightInit = 1; - -// final match = showLabel.firstWhere( -// (e) => e['type'] == type, -// orElse: () => null, -// ); - -// Color barColor = Colors.transparent; -// if (match != null) { -// final dynamic colorStr = match['color']; -// if (colorStr != null && colorStr.toString().isNotEmpty) { -// barColor = stringToColor(colorStr); -// } -// } - -// final Paint barPaint = Paint() -// ..color = barColor -// ..style = PaintingStyle.fill; - -// final double leftX = (st - startTime) * pixelPerMs; -// final double rightX = (et - startTime) * pixelPerMs; -// final double barWidth = rightX - leftX; - -// //rem 深睡 中 -// //浅睡 低 -// //其他 高 -// if (type == 1) { -// heightInit = 1; -// } else if (type == 2 || type == 6) { -// heightInit = 2; -// } else { -// heightInit = 3; -// } -// final double barHeight = (heightInit + 5).toDouble() * 8; -// final double top = height - barHeight; - -// final rect = Rect.fromLTWH(leftX, top, barWidth, barHeight); -// canvas.drawRect(rect, barPaint); -// } -// } - -// @override -// bool shouldRepaint(covariant CustomPainter oldDelegate) => true; -// } - -// class SnoreWaveform extends StatelessWidget { -// final List snoreValues; -// final int startTime; -// final int endTime; - -// const SnoreWaveform({ -// required this.snoreValues, -// required this.startTime, -// required this.endTime, -// super.key, -// }); - -// @override -// Widget build(BuildContext context) { -// return SizedBox( -// height: 150, -// child: LayoutBuilder( -// builder: (context, constraints) { -// return CustomPaint( -// size: Size(constraints.maxWidth, constraints.maxHeight), -// painter: SnoreWaveformPainter( -// snoreValues: snoreValues, -// startTime: startTime, -// endTime: endTime, -// ), -// ); -// }, -// ), -// ); -// } -// } - -// 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; - -// if (width <= 0 || height <= 0) return; - -// final double totalDuration = (endTime - startTime).toDouble(); -// if (totalDuration <= 0) return; - -// final double pixelPerMs = width / totalDuration; - -// // 过滤在时间范围内的有效事件 -// final validEvents = snoreValues.where((e) { -// final int st = e['st']; -// final int et = e['et']; -// // 事件与时间范围有重叠 -// return !(et <= startTime || st >= endTime); -// }).toList(); - -// // 计算中心线位置 -// final double centerY = height / 2; - -// // 统一使用一个颜色(打鼾颜色) -// final Color snoreColor = stringToColor("#8E7DEF").withOpacity(0.8); -// final Paint barPaint = Paint() -// ..color = snoreColor -// ..style = PaintingStyle.fill; - -// final Paint borderPaint = Paint() -// ..color = snoreColor.withOpacity(0.9) -// ..style = PaintingStyle.stroke -// ..strokeWidth = 0.5; - -// // 固定高度(上下对称) -// final double fixedBarHeight = height * 0.3; // 固定为画布高度的30% - -// // 绘制每个打鼾事件(上下对称的柱状图) -// for (final event in validEvents) { -// final int st = event['st']; -// final int et = event['et']; - -// // 计算绘制位置(裁剪到可视范围内) -// final double startX = (st - startTime) * pixelPerMs; -// final double endX = (et - startTime) * pixelPerMs; - -// // 确保在画布范围内 -// if (endX < 0 || startX > width) continue; - -// final double drawStartX = startX.clamp(0, width); -// final double drawEndX = endX.clamp(0, width); -// final double drawWidth = drawEndX - drawStartX; - -// if (drawWidth <= 0) continue; - -// // 绘制上方的柱状图 -// final double topBarTop = centerY - fixedBarHeight; -// final Rect topRect = -// Rect.fromLTWH(drawStartX, topBarTop, drawWidth, fixedBarHeight); -// canvas.drawRect(topRect, barPaint); -// canvas.drawRect(topRect, borderPaint); - -// // 绘制下方的柱状图(对称) -// final double bottomBarTop = centerY; -// final Rect bottomRect = -// Rect.fromLTWH(drawStartX, bottomBarTop, drawWidth, fixedBarHeight); -// canvas.drawRect(bottomRect, barPaint); -// canvas.drawRect(bottomRect, borderPaint); -// } - -// // 绘制中心线 -// final Paint axisPaint = Paint() -// ..color = Colors.grey.withOpacity(0.3) -// ..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; -// final int totalHours = (endTime - startTime) ~/ hourMs; - -// // 创建开始和结束时间的DateTime对象 -// final DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime); -// final DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime); - -// // 1. 始终显示开始时间 -// String label = DateFormat('HH:mm').format(startDt); -// textPainter.text = TextSpan( -// text: label, -// style: TextStyle(fontSize: 10, color: Colors.grey), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, -// Offset(0 - textPainter.width / 2, height + 2), -// ); - -// // 2. 决定显示策略 -// if (totalHours <= 8) { -// // 小时间段:显示所有整点小时(基于实际时间) -// DateTime currentHour = DateTime( -// startDt.year, -// startDt.month, -// startDt.day, -// startDt.hour + 1, // 从下一个整点开始 -// 0, -// 0, -// 0, -// 0); - -// // 如果开始时间本身就是整点,需要调整 -// if (startDt.minute == 0 && -// startDt.second == 0 && -// startDt.millisecond == 0) { -// currentHour = startDt; -// } - -// while (currentHour.millisecondsSinceEpoch < endTime) { -// int timeMs = currentHour.millisecondsSinceEpoch; - -// // 确保时间在范围内 -// if (timeMs > startTime && timeMs < endTime) { -// double x = (timeMs - startTime) * pixelPerMs; - -// // 跳过太接近边界的时间点(30分钟内不显示) -// if (timeMs - startTime < 10 * 60 * 1000 || -// endTime - timeMs < 10 * 60 * 1000) { -// currentHour = currentHour.add(Duration(hours: 1)); -// continue; -// } - -// label = "${currentHour.hour}"; - -// textPainter.text = TextSpan( -// text: label, -// style: TextStyle(fontSize: 10, color: Colors.grey), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, -// Offset(x - textPainter.width / 2, height + 2), -// ); -// } - -// currentHour = currentHour.add(Duration(hours: 1)); -// } -// } else { -// // 长时间段:使用自适应间隔 -// int labelInterval = (totalHours / 6).ceil(); - -// // 计算第一个整点标签(对齐整点小时) -// DateTime firstLabelHour = DateTime( -// startDt.year, -// startDt.month, -// startDt.day, -// startDt.hour + (labelInterval - (startDt.hour % labelInterval)), -// 0, -// 0, -// 0, -// 0); - -// // 如果第一个标签在开始时间之前,调整到下一个间隔 -// if (firstLabelHour.millisecondsSinceEpoch <= startTime) { -// firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval)); -// } - -// // 绘制中间标签 -// DateTime currentHour = firstLabelHour; -// while (currentHour.millisecondsSinceEpoch < endTime) { -// int timeMs = currentHour.millisecondsSinceEpoch; - -// // 跳过太接近边界的时间点(1小时内不显示) -// if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) { -// double x = (timeMs - startTime) * pixelPerMs; -// label = "${currentHour.hour}"; - -// textPainter.text = TextSpan( -// text: label, -// style: TextStyle(fontSize: 10, color: Colors.grey), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, -// Offset(x - textPainter.width / 2, height + 2), -// ); -// } - -// currentHour = currentHour.add(Duration(hours: labelInterval)); -// } -// } - -// // 3. 始终显示结束时间 -// label = DateFormat('HH:mm').format(endDt); -// textPainter.text = TextSpan( -// text: label, -// style: TextStyle(fontSize: 10, color: Colors.grey), -// ); -// textPainter.layout(); -// textPainter.paint( -// canvas, -// Offset(width - textPainter.width / 2, height + 2), -// ); -// } - -// @override -// bool shouldRepaint(covariant SnoreWaveformPainter oldDelegate) { -// return oldDelegate.snoreValues != snoreValues || -// oldDelegate.startTime != startTime || -// oldDelegate.endTime != endTime; -// } -// } - import 'dart:math' as math; import 'dart:ui' as ui; import 'package:ef/ef.dart'; diff --git a/lib/pages/sleep_report/chart/TimeSeriesChart.dart b/lib/pages/sleep_report/chart/TimeSeriesChart.dart index ebb3ba6..be00d0f 100644 --- a/lib/pages/sleep_report/chart/TimeSeriesChart.dart +++ b/lib/pages/sleep_report/chart/TimeSeriesChart.dart @@ -25,63 +25,113 @@ class TimeSeriesChart extends StatelessWidget { required this.dataPoints, }) : super(key: key); - // X轴刻度数据 - List _generateXLabels() { - final labels = []; + // 计算总分钟数 + double get _totalMinutes { + return (endTime - startTime) / (1000 * 60); + } + + // 生成X轴刻度标签 + Map _generateXLabels() { + final labels = {}; final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); - // 第一个刻度,原始 startTime,HH:mm格式 - labels.add(XLabel( - time: startTime, - label: - '${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}', - )); + // 0分钟位置:起始时间 + labels[0.0] = + '${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}'; - // 生成中间整点小时刻度,注意起点向上取整一个小时 - DateTime current = DateTime( - startDate.year, - startDate.month, - startDate.day, - startDate.hour, - ); - if (startDate.minute > 0 || - startDate.second > 0 || - startDate.millisecond > 0) { - // 如果 startTime 不是整点,跳到下一个整点小时 - current = current.add(Duration(hours: 1)); + // 计算总小时数 + final int hourMs = 60 * 60 * 1000; + final int totalHours = (endTime - startTime) ~/ hourMs; + + // 按照参考代码的逻辑,当小时数超过8时,跳着显示 + if (totalHours <= 8) { + // 显示每个整点 + DateTime currentHour = DateTime( + startDate.year, + startDate.month, + startDate.day, + startDate.hour, + ); + + // 如果起始时间不是整点,从下一个整点开始 + if (startDate.minute > 0 || + startDate.second > 0 || + startDate.millisecond > 0) { + currentHour = currentHour.add(Duration(hours: 1)); + } + + while (currentHour.millisecondsSinceEpoch < endTime) { + final int timeMs = currentHour.millisecondsSinceEpoch; + + if (timeMs > startTime && timeMs < endTime) { + // 检查是否太靠近边界 + if (timeMs - startTime < 10 * 60 * 1000 || + endTime - timeMs < 10 * 60 * 1000) { + currentHour = currentHour.add(Duration(hours: 1)); + continue; + } + + // 计算从起始时间到当前整点的分钟数 + final minutesFromStart = + (currentHour.millisecondsSinceEpoch - startTime) / (1000 * 60); + labels[minutesFromStart] = '${currentHour.hour}'; + } + + currentHour = currentHour.add(Duration(hours: 1)); + } + } else { + // 超过8小时,跳着显示 + final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签 + + DateTime firstLabelHour = DateTime( + startDate.year, + startDate.month, + startDate.day, + startDate.hour + (labelInterval - (startDate.hour % labelInterval)), + 0, + 0, + 0, + 0, + ); + + // 确保第一个标签在开始时间之后 + if (firstLabelHour.millisecondsSinceEpoch <= startTime) { + firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval)); + } + + DateTime currentHour = firstLabelHour; + while (currentHour.millisecondsSinceEpoch < endTime) { + final int timeMs = currentHour.millisecondsSinceEpoch; + + // 确保标签离边界足够远 + if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) { + final minutesFromStart = + (currentHour.millisecondsSinceEpoch - startTime) / (1000 * 60); + labels[minutesFromStart] = '${currentHour.hour}'; + } + + currentHour = currentHour.add(Duration(hours: labelInterval)); + } } - while (current.isBefore(endDate)) { - labels.add(XLabel( - time: current.millisecondsSinceEpoch, - label: current.hour.toString(), - )); - current = current.add(Duration(hours: 1)); - } - - // 最后一个刻度,原始 endTime,HH:mm格式 - labels.add(XLabel( - time: endTime, - label: - '${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}', - )); + // 最后位置:结束时间 + labels[_totalMinutes] = + '${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}'; return labels; } - // 时间戳映射到0~(labels.length-1)之间 - double _timeToX(double timestamp, List labels) { - int start = labels.first.time; - int end = labels.last.time; - double total = (end - start).toDouble(); - double pos = (timestamp - start).clamp(0, total).toDouble(); - return pos / total * (labels.length - 1); + // 时间戳映射到X坐标(分钟数) + double _timeToX(double timestamp) { + final minutesFromStart = (timestamp - startTime) / (1000 * 60); + return minutesFromStart.clamp(0.0, _totalMinutes); } @override Widget build(BuildContext context) { final xLabels = _generateXLabels(); + final labelPositions = xLabels.keys.toList()..sort(); final midY = (yMin + yMax) / 2; // 将数据点分割成多个连续段,遇到value=-1时断开 @@ -92,7 +142,7 @@ class TimeSeriesChart extends StatelessWidget { if (point.value != -1) { // 有效数据点,添加到当前段 currentSegment.add(FlSpot( - _timeToX(point.timestamp.toDouble(), xLabels), + _timeToX(point.timestamp.toDouble()), point.value, )); } else if (currentSegment.isNotEmpty) { @@ -112,7 +162,7 @@ class TimeSeriesChart extends StatelessWidget { child: LineChart( LineChartData( minX: 0, - maxX: (xLabels.length - 1).toDouble(), + maxX: _totalMinutes, minY: yMin < 0 ? yMin : 0, maxY: yMax, gridData: FlGridData(show: false), @@ -144,37 +194,26 @@ class TimeSeriesChart extends StatelessWidget { sideTitles: SideTitles( showTitles: true, reservedSize: 30, - interval: 1, + interval: 1, // 现在每个单位是1分钟 getTitlesWidget: (value, meta) { - int index = value.toInt(); - if (index < 0 || index >= xLabels.length) - return const SizedBox.shrink(); + // 四舍五入到最接近的整数 + final roundedValue = value.roundToDouble(); - final dateTime = - DateTime.fromMillisecondsSinceEpoch(xLabels[index].time); - - if (index == 0 || index == xLabels.length - 1) { - // 开始和结束显示 HH:mm - final formatted = - '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(formatted, - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: 16.rpx)), - ); - } else { - // 中间显示小时H,24小时制,不补零 - final formatted = '${dateTime.hour}'; - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(formatted, - style: TextStyle( - color: themeController.currentColor.sc4, - fontSize: 16.rpx)), - ); + // 检查是否在标签位置 + for (var position in labelPositions) { + if ((position - roundedValue).abs() < 0.5) { + final label = xLabels[position] ?? ''; + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(label, + style: TextStyle( + color: themeController.currentColor.sc4, + fontSize: 16.rpx)), + ); + } } + + return const SizedBox.shrink(); }, ), ), @@ -238,9 +277,3 @@ class TimeSeriesChart extends StatelessWidget { ); } } - -class XLabel { - final int time; - final String label; - XLabel({required this.time, required this.label}); -} diff --git a/lib/pages/sleep_report/component/AIAdviceWidget.dart b/lib/pages/sleep_report/component/AIAdviceWidget.dart index a15feb9..e6c3e72 100644 --- a/lib/pages/sleep_report/component/AIAdviceWidget.dart +++ b/lib/pages/sleep_report/component/AIAdviceWidget.dart @@ -129,7 +129,7 @@ class _AIAdviceWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("AI分析绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/BreathPauseWidget.dart b/lib/pages/sleep_report/component/BreathPauseWidget.dart index d623f63..da0c4d9 100644 --- a/lib/pages/sleep_report/component/BreathPauseWidget.dart +++ b/lib/pages/sleep_report/component/BreathPauseWidget.dart @@ -126,7 +126,7 @@ class _BreathPauseWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("呼吸暂停监测绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/BreathePauseNewWidget.dart b/lib/pages/sleep_report/component/BreathePauseNewWidget.dart index 3fdc614..3848a57 100644 --- a/lib/pages/sleep_report/component/BreathePauseNewWidget.dart +++ b/lib/pages/sleep_report/component/BreathePauseNewWidget.dart @@ -193,7 +193,7 @@ class _SnoreViewWidgetWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("呼吸监测绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/BreatheStandardWidget.dart b/lib/pages/sleep_report/component/BreatheStandardWidget.dart index 26f0c2e..b9339f2 100644 --- a/lib/pages/sleep_report/component/BreatheStandardWidget.dart +++ b/lib/pages/sleep_report/component/BreatheStandardWidget.dart @@ -416,7 +416,7 @@ class _BreatheStandardWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${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 89d99ae..99f4aaf 100644 --- a/lib/pages/sleep_report/component/DiseasePercentsWidget.dart +++ b/lib/pages/sleep_report/component/DiseasePercentsWidget.dart @@ -144,7 +144,7 @@ class _DiseasePercentsWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("疾病绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/HeartChangeWidget.dart b/lib/pages/sleep_report/component/HeartChangeWidget.dart index 7d5b605..3c97d85 100644 --- a/lib/pages/sleep_report/component/HeartChangeWidget.dart +++ b/lib/pages/sleep_report/component/HeartChangeWidget.dart @@ -319,7 +319,7 @@ class _HeartChangeWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("心率变化绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/HeartHealthWidget.dart b/lib/pages/sleep_report/component/HeartHealthWidget.dart index 6a85c6a..f20488a 100644 --- a/lib/pages/sleep_report/component/HeartHealthWidget.dart +++ b/lib/pages/sleep_report/component/HeartHealthWidget.dart @@ -159,7 +159,7 @@ class _HeartHealthWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("心理健康绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/HeartPointWidget.dart b/lib/pages/sleep_report/component/HeartPointWidget.dart index 21f334c..f97c8a7 100644 --- a/lib/pages/sleep_report/component/HeartPointWidget.dart +++ b/lib/pages/sleep_report/component/HeartPointWidget.dart @@ -397,7 +397,7 @@ class _HeartPointWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${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 020f893..2d48a9a 100644 --- a/lib/pages/sleep_report/component/HrvWidget.dart +++ b/lib/pages/sleep_report/component/HrvWidget.dart @@ -117,7 +117,7 @@ class _HrvWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("hrv绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/SkinPercentWidget.dart b/lib/pages/sleep_report/component/SkinPercentWidget.dart index 2ef122e..2f9af60 100644 --- a/lib/pages/sleep_report/component/SkinPercentWidget.dart +++ b/lib/pages/sleep_report/component/SkinPercentWidget.dart @@ -165,7 +165,7 @@ class _SkinPercentWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("皮肤检测绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/SleepScoreWidget.dart b/lib/pages/sleep_report/component/SleepScoreWidget.dart index 5ca56fe..1ed1583 100644 --- a/lib/pages/sleep_report/component/SleepScoreWidget.dart +++ b/lib/pages/sleep_report/component/SleepScoreWidget.dart @@ -219,7 +219,7 @@ class _SleepScoreWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("睡眠评分绘制异常${e}"); return Container(); } } diff --git a/lib/pages/sleep_report/component/new_sleep_view.dart b/lib/pages/sleep_report/component/new_sleep_view.dart index 410d0f5..5bd64df 100644 --- a/lib/pages/sleep_report/component/new_sleep_view.dart +++ b/lib/pages/sleep_report/component/new_sleep_view.dart @@ -299,7 +299,7 @@ class _NewSleepViewWidgetState extends State { ), ); } catch (e) { - es.EasyDartModule.logger.error("打鼾监测绘制异常${e}"); + es.EasyDartModule.logger.error("睡眠规律性绘制异常${e}"); return Container(); } }