From 1dcef1090dc1a053190bc1fcd14b0ab38588490b Mon Sep 17 00:00:00 2001 From: wyf <494641114@qq.com> Date: Wed, 24 Dec 2025 14:56:35 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=9B=B4=E6=96=B0=E6=B6=88=E6=81=AF=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E6=97=B6=E9=97=B4=202.=E4=BF=AE=E5=A4=8D=E9=85=8D?= =?UTF-8?q?=E7=BD=AEwifi=E6=97=B6wifi=E5=90=8D=E7=A7=B0=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E7=A9=BA=E6=A0=BC=E6=97=B6=E9=85=8D=E7=BD=AE=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=203.=E4=BF=AE=E5=A4=8D=E9=97=A8=E5=BA=97=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=88=B7=E6=96=B0=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/util/Dio.dart | 54 +- lib/common/util/requestWithLog.dart | 2 +- lib/component/tool/cmd.dart | 23 +- lib/controller/home/Dio.dart | 46 -- .../experience_store_list_page.dart | 7 +- lib/main.dart | 5 +- .../component/DeviceComponentWidget.dart | 4 +- lib/pages/mh_page/device/mht_people_info.dart | 2 + lib/pages/mh_page/new_settingPage.dart | 2 +- lib/pages/person/select_city.dart | 5 +- .../sleep_report/chart/SnoreWaveform.dart | 480 ++++++++++++++++-- 11 files changed, 490 insertions(+), 140 deletions(-) delete mode 100644 lib/controller/home/Dio.dart diff --git a/lib/common/util/Dio.dart b/lib/common/util/Dio.dart index 8f82702..486ed40 100644 --- a/lib/common/util/Dio.dart +++ b/lib/common/util/Dio.dart @@ -1,9 +1,6 @@ import 'package:dio/dio.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; +import 'package:ef/ef.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; -import 'package:vbvs_app/controller/login/login_controller.dart'; -import 'package:vbvs_app/pages/common/selectDialog.dart'; class ApiService { static Dio dio = Dio(); @@ -15,32 +12,35 @@ class ApiService { static Dio reservation = Dio(); static void init() { - reservationInit(); } static void reservationInit() { - reservation.options.baseUrl = "https://crm-api.swes.com.cn"; - reservation.options.connectTimeout = const Duration(seconds: 15); - reservation.options.receiveTimeout = const Duration(seconds: 10); - reservation.options.sendTimeout = const Duration(seconds: 10); - reservation.interceptors.add(InterceptorsWrapper( - onRequest: (options, handler) { - print("---> ${options.method}, ${options.path}, ${options.data}"); - options.headers['token'] = - "bMAQVR1x48t66u8EDYSftAJGo17r0rIB3z15JgyyoGz1rAEZHs1htHOCorYFJ2RT"; - return handler.next(options); - }, - onResponse: (response, ResponseInterceptorHandler handler) { - print("<--- ${response.statusCode} ${response.data}"); - return handler.next(response); - }, - onError: (e, handler) { - // 错误处理,例如打印错误信息 - showToast("网络异常或服务连接异常,请稍候再试", color: color_error); - print("DioError: $e"); - return handler.reject(e); - }, - )); + try { + reservation.options.baseUrl = "https://crm-api.swes.com.cn"; + reservation.options.connectTimeout = const Duration(seconds: 15); + reservation.options.receiveTimeout = const Duration(seconds: 10); + reservation.options.sendTimeout = const Duration(seconds: 10); + reservation.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) { + print("---> ${options.method}, ${options.path}, ${options.data}"); + options.headers['token'] = + "bMAQVR1x48t66u8EDYSftAJGo17r0rIB3z15JgyyoGz1rAEZHs1htHOCorYFJ2RT"; + return handler.next(options); + }, + onResponse: (response, ResponseInterceptorHandler handler) { + print("<--- ${response.statusCode} ${response.data}"); + return handler.next(response); + }, + onError: (e, handler) { + // 错误处理,例如打印错误信息 + showToast("网络异常或服务连接异常,请稍候再试", color: color_error); + print("DioError: $e"); + return handler.reject(e); + }, + )); + } catch (e) { + ef.log("$e"); + } } } diff --git a/lib/common/util/requestWithLog.dart b/lib/common/util/requestWithLog.dart index 6eddf34..a487ebe 100644 --- a/lib/common/util/requestWithLog.dart +++ b/lib/common/util/requestWithLog.dart @@ -92,8 +92,8 @@ Future requestWithLog({ } catch (e) { EasyDartModule.logger.error("$logTitle 失败->$e"); DailyLogUtils.writeError("$logTitle 失败->$e"); - onFailure?.call(apiResponse); apiResponse.msg = e.toString(); + onFailure?.call(apiResponse); return apiResponse; } } diff --git a/lib/component/tool/cmd.dart b/lib/component/tool/cmd.dart index 66b6099..194ea9b 100644 --- a/lib/component/tool/cmd.dart +++ b/lib/component/tool/cmd.dart @@ -10,7 +10,7 @@ import 'package:vbvs_app/common/util/DailyLogUtils.dart'; // wifi列表指令 getWifiList(THapp tHapp) async { try { - await openDlog(tHapp); + await openDlog(tHapp); print("wscan scan"); edm.EasyDartModule.logger.info("发送请求网络列表指令"); DailyLogUtils.writeLog("发送请求网络列表指令"); @@ -54,7 +54,7 @@ getWifiList(THapp tHapp) async { } getWifiStatus(THapp tHapp) async { - await openDlog(tHapp); + await openDlog(tHapp); edm.EasyDartModule.logger.info("发送请求设备网络状态指令"); DailyLogUtils.writeLog("发送请求设备网络状态指令"); var result = await tHapp.send( @@ -85,7 +85,7 @@ getWifiStatus(THapp tHapp) async { Future sendWifiSetting(wifiItem, String password, THapp tHapp) async { try { - await openDlog(tHapp); + await openDlog(tHapp); edm.EasyDartModule.logger.info("发送wifi配置指令"); DailyLogUtils.writeLog("发送wifi配置指令->"); // String cmd = "vtouch save update -a -i .wifi.sta.auth=${wifiItem['auth']} " @@ -132,7 +132,7 @@ getDeviceWifiStatus( edm.EasyDartModule.logger.info("[aaa:配置开始]发送请求设备已配置网络状态指令:${currenttIme}"); ef.log("[aaa:配置开始]:${currenttIme}"); var stream = tHapp.logingStream.listen((data) { - ef.log("[设备日志]:${data}"); + ef.log("[设备日志1]:${data}"); }); try { var result = await tHapp.send("at+system info", true, (ss) { @@ -153,8 +153,11 @@ getDeviceWifiStatus( // 如果设备连接状态是 "connect",继续检测 if (status.contains('connect')) { // 匹配 Wi-Fi 连接信息 + // final wifiInfoMatch = RegExp( + // r'WIFI CONNECTED INFO:SSID=([^\s]+),RSSI=([-0-9]+),AUTH=([0-9]+),CH=([0-9]+),BSSID=([A-F0-9]+)') + // .firstMatch(log); final wifiInfoMatch = RegExp( - r'WIFI CONNECTED INFO:SSID=([^\s]+),RSSI=([-0-9]+),AUTH=([0-9]+),CH=([0-9]+),BSSID=([A-F0-9]+)') + r'WIFI CONNECTED INFO:SSID=(.+?),RSSI=([-0-9]+),AUTH=([0-9]+),CH=([0-9]+),BSSID=([A-F0-9]+)') .firstMatch(log); if (wifiInfoMatch != null) { final ssid = wifiInfoMatch.group(1); @@ -207,7 +210,7 @@ getDeviceWifiStatus( //只有4g并且没有wifi Future getDeviceNetVersion(THapp tHapp, int times) async { - await openDlog(tHapp); + await openDlog(tHapp); edm.EasyDartModule.logger.info("发送请求设备的网络信息"); DailyLogUtils.writeLog("发送请求设备的网络信息"); print("ls /root/mnt"); @@ -245,7 +248,7 @@ Future getDeviceNetVersion(THapp tHapp, int times) async { /// 发送关闭blog日志与开启业务日志的指令 Future sendBlogAndDlogCommands(THapp tHapp) async { - await openDlog(tHapp); + await openDlog(tHapp); edm.EasyDartModule.logger.info("发送 blog disable 与 dlog on business 指令"); DailyLogUtils.writeLog("发送 blog disable 与 dlog on business 指令"); try { @@ -299,7 +302,7 @@ Future sendBlogAndDlogCommands(THapp tHapp) async { /// 执行 wscan close -> 延迟 1s -> wscan open Future sendCloseAndOpenWscanCommand(THapp tHapp) async { - await openDlog(tHapp); + await openDlog(tHapp); edm.EasyDartModule.logger.info("执行 wscan close -> wscan open 指令"); DailyLogUtils.writeLog("执行 wscan close -> wscan open 指令开始"); @@ -352,7 +355,7 @@ Future sendCloseAndOpenWscanCommand(THapp tHapp) async { /// 若匹配到“关键内存剩余:xxxx 字节”,其中 xxxx < 20000 返回 false, /// 否则返回 true。 Future queryMemory(THapp tHapp, {int times = 1}) async { - await openDlog(tHapp); + await openDlog(tHapp); edm.EasyDartModule.logger.info("发送查询内存状态指令 (free all)"); DailyLogUtils.writeLog("发送查询内存状态指令 (free all)"); @@ -401,7 +404,7 @@ Future queryMemory(THapp tHapp, {int times = 1}) async { /// 发送 "reboot" 命令,若响应中包含 reboot / restart / ok / success 等关键字 /// 视为执行成功,返回 true;否则返回 false。 Future rebootDevice(THapp tHapp) async { - await openDlog(tHapp); + await openDlog(tHapp); edm.EasyDartModule.logger.info("发送设备重启指令 (reboot)"); DailyLogUtils.writeLog("发送设备重启指令 (reboot)"); diff --git a/lib/controller/home/Dio.dart b/lib/controller/home/Dio.dart deleted file mode 100644 index 8f82702..0000000 --- a/lib/controller/home/Dio.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:vbvs_app/common/util/MyUtils.dart'; -import 'package:vbvs_app/controller/login/login_controller.dart'; -import 'package:vbvs_app/pages/common/selectDialog.dart'; - -class ApiService { - static Dio dio = Dio(); - - static Dio request = Dio(); - static Dio requestNoInfo = Dio(); - static Dio requestNoError = Dio(); //不处理错误的请求,用于设备操作上报 - - static Dio reservation = Dio(); - - static void init() { - - reservationInit(); - } - - static void reservationInit() { - reservation.options.baseUrl = "https://crm-api.swes.com.cn"; - reservation.options.connectTimeout = const Duration(seconds: 15); - reservation.options.receiveTimeout = const Duration(seconds: 10); - reservation.options.sendTimeout = const Duration(seconds: 10); - reservation.interceptors.add(InterceptorsWrapper( - onRequest: (options, handler) { - print("---> ${options.method}, ${options.path}, ${options.data}"); - options.headers['token'] = - "bMAQVR1x48t66u8EDYSftAJGo17r0rIB3z15JgyyoGz1rAEZHs1htHOCorYFJ2RT"; - return handler.next(options); - }, - onResponse: (response, ResponseInterceptorHandler handler) { - print("<--- ${response.statusCode} ${response.data}"); - return handler.next(response); - }, - onError: (e, handler) { - // 错误处理,例如打印错误信息 - showToast("网络异常或服务连接异常,请稍候再试", color: color_error); - print("DioError: $e"); - return handler.reject(e); - }, - )); - } -} diff --git a/lib/controller/mh_controller/experience_store_list_page.dart b/lib/controller/mh_controller/experience_store_list_page.dart index 1e75df4..1c38fde 100644 --- a/lib/controller/mh_controller/experience_store_list_page.dart +++ b/lib/controller/mh_controller/experience_store_list_page.dart @@ -57,7 +57,7 @@ class ExperienceStoreListController // if (page == 0) { // position = await weatherModelController.determinePosition(); // } - + lock = true; int page_ = page; ApiService.reservation @@ -74,6 +74,7 @@ class ExperienceStoreListController .indexWhere((item2) => item2["id"] == item["id"]) == -1) { model.experienceStoreModelList.add(item); + attr.refresh(); } else { model.experienceStoreModelList[index] = item; } @@ -82,10 +83,10 @@ class ExperienceStoreListController } page = page_ + 1; total = d.data["total"]; - updateAll(); + // updateAll(); + attr.refresh(); }).catchError((d) { lock = false; }); } - } diff --git a/lib/main.dart b/lib/main.dart index 3304064..056719a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,7 @@ import 'package:vbvs_app/common/pojo/city.dart'; import 'package:vbvs_app/common/util/CheckNetwork.dart'; import 'package:vbvs_app/common/util/CommonVariables.dart'; import 'package:vbvs_app/common/util/DailyLogUtils.dart'; -// import 'package:vbvs_app/common/util/Dio.dart'; +import 'package:vbvs_app/common/util/Dio.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/JPushUtil.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; @@ -32,7 +32,6 @@ import 'package:vbvs_app/controller/device/device_calibration_controller.dart'; import 'package:vbvs_app/controller/device/device_share_controller.dart'; import 'package:vbvs_app/controller/device/device_share_list_controller.dart'; import 'package:vbvs_app/controller/device/device_type_controller.dart'; -import 'package:vbvs_app/controller/home/Dio.dart'; import 'package:vbvs_app/controller/home/home_controller.dart'; import 'package:vbvs_app/controller/login/login_controller.dart'; import 'package:vbvs_app/controller/main_bottom/global_controller.dart'; @@ -486,7 +485,7 @@ Future startMessagePolling(int ent_type) async { } } - _messageTimer = Timer.periodic(Duration(seconds: 1), (timer) async { + _messageTimer = Timer.periodic(Duration(seconds: 5), (timer) async { try { if (ent_type == APPPackageType.MHT.code) { if (Get.isRegistered()) { diff --git a/lib/pages/mh_page/device/component/DeviceComponentWidget.dart b/lib/pages/mh_page/device/component/DeviceComponentWidget.dart index ab5971d..e4815a4 100644 --- a/lib/pages/mh_page/device/component/DeviceComponentWidget.dart +++ b/lib/pages/mh_page/device/component/DeviceComponentWidget.dart @@ -508,9 +508,10 @@ class _DeviceComponentWidgetState extends State { const int maxRetries = 2; const Duration timeout = Duration(seconds: 5); String? macAddress; + THapp bledevice = THapp(device: device.scanResult.device); try { // 连接设备 - THapp bledevice = THapp(device: device.scanResult.device); + await bledevice.connect(); var res2 = bledevice.isConnected; if (!res2) { @@ -545,6 +546,7 @@ class _DeviceComponentWidgetState extends State { print('MAC地址: $macAddress'); return macAddress; } catch (e) { + bledevice.disconnect(); blueteethBindController.currentDeviceMac.value = ""; edm.EasyDartModule.logger.error("蓝牙获取MAC失败:$e"); DailyLogUtils.printLog("蓝牙获取MAC失败:$e"); diff --git a/lib/pages/mh_page/device/mht_people_info.dart b/lib/pages/mh_page/device/mht_people_info.dart index ab34586..565bc75 100644 --- a/lib/pages/mh_page/device/mht_people_info.dart +++ b/lib/pages/mh_page/device/mht_people_info.dart @@ -870,6 +870,8 @@ class _MHTPeopleInfoPageState extends State { stringToColor("#84F5FF"), selectedCityColor: stringToColor("#84F5FF"), + selectedTextColor: + stringToColor("#011D33"), ), ); }); diff --git a/lib/pages/mh_page/new_settingPage.dart b/lib/pages/mh_page/new_settingPage.dart index d42e3b1..541a1a3 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('SWES2025.12.20', + Text('SWES2025.12.24', style: TextStyle( color: Colors.white, fontSize: 26.rpx, diff --git a/lib/pages/person/select_city.dart b/lib/pages/person/select_city.dart index 107cd85..ec96512 100644 --- a/lib/pages/person/select_city.dart +++ b/lib/pages/person/select_city.dart @@ -627,6 +627,9 @@ Widget _buildCityPickerContent( PersonController personController = Get.find(); personController.cityModel = fullCityData; personController.updateAll(); + if (onCityChanged != null) { + onCityChanged(fullCityData); + } Navigator.of(context).pop(); } }, @@ -809,4 +812,4 @@ Widget _buildErrorBottomSheet( ], ), ); -} \ No newline at end of file +} diff --git a/lib/pages/sleep_report/chart/SnoreWaveform.dart b/lib/pages/sleep_report/chart/SnoreWaveform.dart index e19a5ef..b491e1e 100644 --- a/lib/pages/sleep_report/chart/SnoreWaveform.dart +++ b/lib/pages/sleep_report/chart/SnoreWaveform.dart @@ -1,3 +1,343 @@ +// 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) { +// // barData.add({ +// // "type": 6, +// // "et": 1765291778963, +// // "st": 1765289341000, +// // }); +// 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: CustomPaint( +// size: Size(double.infinity, 150), +// 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; +// final double centerY = height / 2; +// final double totalDuration = (endTime - startTime).toDouble(); +// final double pixelPerMs = width / totalDuration; + +// final Paint wavePaint = Paint() +// ..color = stringToColor("#8E7DEF").withOpacity(0.8) +// ..strokeWidth = 1.5 +// ..style = PaintingStyle.stroke; + +// final Path upperPath = Path(); +// final Path lowerPath = Path(); + +// double maxValue = snoreValues.fold(0, (prev, e) { +// final value = e["value"]?.toDouble() ?? 0; +// return value > prev ? value : prev; +// }); + +// final double maxWaveHeight = height * 1; +// final double scaleY = maxValue > 0 ? (maxWaveHeight / maxValue) : 1; + +// for (int i = 0; i < snoreValues.length; i++) { +// final timestamp = snoreValues[i]["st"]; +// final value = snoreValues[i]["value"]?.toDouble() ?? 0; + +// final x = (timestamp - startTime) * pixelPerMs; +// final y = centerY - value * scaleY; +// final yMirror = centerY + value * scaleY; + +// if (i == 0) { +// upperPath.moveTo(x, y); +// lowerPath.moveTo(x, yMirror); +// } else { +// upperPath.lineTo(x, y); +// lowerPath.lineTo(x, yMirror); +// } +// } + +// canvas.drawPath(upperPath, wavePaint); +// canvas.drawPath(lowerPath, wavePaint); + +// final Paint axisPaint = Paint() +// ..color = Colors.grey.withOpacity(0.6) +// ..strokeWidth = 0.5; +// canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint); + +// final textPainter = TextPainter( +// textAlign: TextAlign.center, +// textDirection: ui.TextDirection.ltr, +// ); + +// final int hourMs = 60 * 60 * 1000; +// final int totalHours = (endTime - startTime) ~/ hourMs; + +// // 1. 始终显示开始时间 +// double x = 0; +// DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime); +// 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(x - textPainter.width / 2, height + 2), +// ); + +// // 2. 决定显示策略 +// if (totalHours <= 8) { +// // 小时间段:显示所有整点小时 +// for (int t = startTime + hourMs; t < endTime; t += hourMs) { +// x = (t - startTime) * pixelPerMs; +// DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); + +// // 判断是否接近边界(30分钟内不显示) +// if (t - startTime < 30 * 60 * 1000 || endTime - t < 30 * 60 * 1000) { +// continue; +// } + +// label = dt.hour == 0 ? "0" : "${dt.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), +// ); +// } +// } else { +// // 长时间段:使用自适应间隔 +// int labelInterval = (totalHours / 6).ceil(); + +// // 计算第一个标签位置(对齐整点) +// int firstLabelMs = +// ((startTime ~/ (labelInterval * hourMs))) * labelInterval * hourMs; +// if (firstLabelMs <= startTime) { +// firstLabelMs += labelInterval * hourMs; +// } + +// // 绘制中间标签 +// for (int t = firstLabelMs; t < endTime; t += labelInterval * hourMs) { +// // 跳过太接近边界的时间点(1小时内不显示) +// if (t - startTime < hourMs || endTime - t < hourMs) continue; + +// x = (t - startTime) * pixelPerMs; +// DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); +// label = dt.hour == 0 ? "0" : "${dt.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), +// ); +// } +// } + +// // 3. 始终显示结束时间 +// x = (endTime - startTime) * pixelPerMs; +// DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime); +// label = DateFormat('HH:mm').format(endDt); + +// textPainter.text = TextSpan( +// text: label, +// style: TextStyle(fontSize: 10, color: Colors.grey), +// ); +// textPainter.layout(); +// textPainter.paint( +// canvas, +// Offset(x - textPainter.width / 2, height + 2), +// ); +// } + +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +// } + import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; @@ -23,11 +363,6 @@ class SnoreChartContainer extends StatelessWidget { @override Widget build(BuildContext context) { - // barData.add({ - // "type": 6, - // "et": 1765291778963, - // "st": 1765289341000, - // }); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -167,13 +502,17 @@ class SnoreWaveform extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( height: 150, - child: CustomPaint( - size: Size(double.infinity, 150), - painter: SnoreWaveformPainter( - snoreValues: snoreValues, - startTime: startTime, - endTime: endTime, - ), + child: LayoutBuilder( + builder: (context, constraints) { + return CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: SnoreWaveformPainter( + snoreValues: snoreValues, + startTime: startTime, + endTime: endTime, + ), + ); + }, ), ); } @@ -194,51 +533,94 @@ class SnoreWaveformPainter extends CustomPainter { void paint(Canvas canvas, Size size) { final double width = size.width; final double height = size.height; - final double centerY = height / 2; + + if (width <= 0 || height <= 0) return; + final double totalDuration = (endTime - startTime).toDouble(); + if (totalDuration <= 0) return; + final double pixelPerMs = width / totalDuration; - final Paint wavePaint = Paint() - ..color = stringToColor("#8E7DEF").withOpacity(0.8) - ..strokeWidth = 1.5 - ..style = PaintingStyle.stroke; + // 过滤在时间范围内的有效事件 + final validEvents = snoreValues.where((e) { + final int st = e['st']; + final int et = e['et']; + // 事件与时间范围有重叠 + return !(et <= startTime || st >= endTime); + }).toList(); - final Path upperPath = Path(); - final Path lowerPath = Path(); - - double maxValue = snoreValues.fold(0, (prev, e) { - final value = e["value"]?.toDouble() ?? 0; - return value > prev ? value : prev; - }); - - final double maxWaveHeight = height * 1; - final double scaleY = maxValue > 0 ? (maxWaveHeight / maxValue) : 1; - - for (int i = 0; i < snoreValues.length; i++) { - final timestamp = snoreValues[i]["st"]; - final value = snoreValues[i]["value"]?.toDouble() ?? 0; - - final x = (timestamp - startTime) * pixelPerMs; - final y = centerY - value * scaleY; - final yMirror = centerY + value * scaleY; - - if (i == 0) { - upperPath.moveTo(x, y); - lowerPath.moveTo(x, yMirror); - } else { - upperPath.lineTo(x, y); - lowerPath.lineTo(x, yMirror); - } + if (validEvents.isEmpty) { + // 绘制无数据提示 + final textPainter = TextPainter( + text: TextSpan( + text: '无打鼾数据', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + textDirection: ui.TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint( + canvas, Offset(width / 2 - textPainter.width / 2, height / 2 - 10)); + return; } - canvas.drawPath(upperPath, wavePaint); - canvas.drawPath(lowerPath, wavePaint); + // 计算中心线位置 + 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.6) + ..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, @@ -335,5 +717,9 @@ class SnoreWaveformPainter extends CustomPainter { } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; + bool shouldRepaint(covariant SnoreWaveformPainter oldDelegate) { + return oldDelegate.snoreValues != snoreValues || + oldDelegate.startTime != startTime || + oldDelegate.endTime != endTime; + } }