Files
tuiche/lib/pages/sleep_report/chart/SnoreWaveform.dart
wyf 1dcef1090d 1.更新消息阅读时间
2.修复配置wifi时wifi名称存在空格时配置失败
3.修复门店体验列表刷新失败
2025-12-24 14:56:35 +08:00

726 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// import 'dart: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<dynamic> snoreValues;
// final List<dynamic> barData;
// final List<dynamic> 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<dynamic> barData;
// final List<dynamic> 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<dynamic> barData;
// final List<dynamic> 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<dynamic> 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<dynamic> 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<double>(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';
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<dynamic> snoreValues;
final List<dynamic> barData;
final List<dynamic> 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<dynamic> barData;
final List<dynamic> 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<dynamic> barData;
final List<dynamic> 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<dynamic> 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<dynamic> 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();
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;
}
// 计算中心线位置
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;
// 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 SnoreWaveformPainter oldDelegate) {
return oldDelegate.snoreValues != snoreValues ||
oldDelegate.startTime != startTime ||
oldDelegate.endTime != endTime;
}
}