更新快检功能

This commit is contained in:
wyf
2026-03-10 12:01:00 +08:00
parent d0ae2a9f76
commit 6472baf993
48 changed files with 8046 additions and 120 deletions

View File

@@ -0,0 +1,247 @@
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/QcBreatheStandardWidget.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/QcDiseasePercentsWidget.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/QcHeartHealthWidget.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/QcHeartRateStandardWidget.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/QcPiLaoZhiShuPercentWidget.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/QcZiZhuShenJingPercentWidget.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/qc_heart_change.dart';
import 'package:vbvs_app/pages/sleep_report/qc_report/qc_heart_point.dart';
Widget QcReportWidget(Map data) {
List<Widget> _buildSectionList() {
EdgeInsetsDirectional padding =
EdgeInsetsDirectional.fromSTEB(0.rpx, 0, 0.rpx, 0.rpx);
// Map data = {
// "_id": "69ae3ac13a075b8438083b0a",
// "person": {
// "name": "张三",
// "gender": "男",
// "age": 52,
// "weight": 37.5,
// "height": 165
// },
// "score": 80,
// "level": 2,
// "mac": "设备mac",
// "hr": {
// "avg": 89,
// "base": 85,
// "min": 68,
// "max": 96,
// "data": [
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 69,
// 70,
// 70,
// 70,
// 70,
// 70,
// 88,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 70,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 71,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68,
// 68
// ]
// },
// "br": {
// "avg": 12,
// "base": 12,
// "min": 10,
// "max": 15,
// "data": [
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 16,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 17,
// 20,
// 21,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 15,
// 11
// ]
// },
// "hrv": [
// {"name": "心脏总能量", "val": 45, "range": "-"}
// ],
// "hrs": [
// {"x": 1005, "y": 800}
// ],
// "xljk": [
// {"name": "焦虑抑郁", "val": 15, "color": "00C1AA"}
// ],
// "mbzs": [
// {"name": "心血管", "val": 70, "color": "00C1AA"}
// ],
// "plzs": {"level": 1},
// "zzsjphzs": {"level": 2},
// "ai": {},
// "create_time": 1772690803969
// };
return [
QcHeartRateStandardWidget(reportData: data),
QcBreatheStandardWidget(reportData: data),
QcHeartChangeWidget(reportData: data),
QcHeartPointWidget(reportData: data),
QcHeartHealthWidget(reportData: data),
QcDiseasePercentsWidget(reportData: data),
QcPiLaoZhiShuPercentWidget(reportData: data),
QcZiZhuShenJingPercentWidget(reportData: data),
]
.divide(SizedBox(height: 25.rpx))
.map((widget) => Padding(
padding: padding,
child: SizedBox(width: double.infinity, child: widget),
))
.toList();
}
return Column(
children: _buildSectionList(),
);
}

View File

@@ -1,3 +1,307 @@
// import 'package:flutter/material.dart';
// import 'package:flutter_svg/flutter_svg.dart';
// import 'package:vbvs_app/common/color/appConstants.dart';
// import 'package:vbvs_app/common/util/FitTool.dart';
// import 'package:vbvs_app/common/util/MyUtils.dart';
// import 'package:vbvs_app/component/tool/ClickableContainer.dart';
// import 'package:vbvs_app/enum/APPPackageType.dart';
// import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
// class HorizontalBarChart extends StatelessWidget {
// final List<Map<String, dynamic>> showLabel;
// final bool showPercent;
// const HorizontalBarChart({
// super.key,
// required this.showLabel,
// this.showPercent = true,
// });
// @override
// Widget build(BuildContext context) {
// final data = showLabel.map((item) {
// return BarData(
// label: item['name'],
// value: (item['percent'] ?? 0).toDouble(),
// color: item['color'] ?? Colors.grey,
// explain: item['explain'] ?? '',
// );
// }).toList();
// final double labelWidth = (MediaQuery.of(context).size.width * 0.5).clamp(
// MediaQuery.of(context).size.width * 0.08,
// MediaQuery.of(context).size.width * 0.22);
// final double barHeight = 24.0.rpx;
// final double barSpacing = 40.0.rpx;
// final totalHeight = data.length * (barHeight + barSpacing) + 30.rpx;
// return SizedBox(
// height: totalHeight,
// child: Row(
// children: [
// // 左侧标签列
// SizedBox(
// width: labelWidth,
// child: ListView.builder(
// physics: const NeverScrollableScrollPhysics(),
// itemCount: data.length,
// itemBuilder: (context, index) {
// final bar = data[index];
// return Container(
// height: barHeight + barSpacing,
// alignment: Alignment.centerRight,
// child: LabelWithSvg(
// label: bar.label,
// explain: bar.explain,
// ),
// );
// },
// ),
// ),
// SizedBox(
// width: 16.rpx,
// ),
// // 右侧柱状图区
// Expanded(
// child: Stack(
// children: [
// // 网格线背景层
// CustomPaint(
// size: Size(double.infinity, totalHeight),
// painter: GridPainter(totalHeight: totalHeight),
// ),
// // 柱状图列表
// ListView.builder(
// physics: const NeverScrollableScrollPhysics(),
// itemCount: data.length,
// itemBuilder: (context, index) {
// final bar = data[index];
// return SizedBox(
// height: barHeight + barSpacing,
// child: CustomPaint(
// painter: SingleBarPainter(
// value: bar.value,
// color: bar.color,
// showPercent: showPercent,
// ),
// ),
// );
// },
// )
// ],
// ),
// ),
// ],
// ),
// );
// }
// }
// class BarData {
// final String label;
// final double value;
// final Color color;
// final String explain;
// BarData({
// required this.label,
// required this.value,
// required this.color,
// required this.explain,
// });
// }
// class LabelWithSvg extends StatelessWidget {
// final String label;
// final String explain;
// const LabelWithSvg({
// super.key,
// required this.label,
// required this.explain,
// });
// @override
// Widget build(BuildContext context) {
// final textStyle =
// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
// return Row(
// mainAxisAlignment: MainAxisAlignment.end,
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// Flexible(
// child: Text(
// label,
// style: textStyle,
// overflow: TextOverflow.ellipsis,
// ),
// ),
// // SizedBox(width: 8.rpx),
// ClickableContainer(
// backgroundColor: Colors.transparent,
// highlightColor: Colors.white,
// padding:
// EdgeInsetsDirectional.fromSTEB(14.rpx, 14.rpx, 14.rpx, 14.rpx),
// borderRadius: 0.rpx,
// onTap: () {
// // Get.toNamed("/deviceShareListPage", arguments: explain);
// if (AppConstants().ent_type == APPPackageType.MHT.code) {
// showTipDialog(
// context,
// Container(
// child: Text(
// explain,
// style: TextStyle(fontSize: 26.rpx, color: Colors.black),
// ),
// ),
// backgroundColor: Color(0xFFFFFFFF),
// colors: [
// Color(0XFF1592AA),
// Color(0xFF0C83A7),
// Color(0xFF006FA3)
// ],
// );
// } else {
// showTipDialog(
// context,
// Container(
// child: Text(
// explain,
// style: TextStyle(
// fontSize: 26.rpx,
// color: themeController.currentColor.sc3),
// ),
// ),
// backgroundColor: themeController.currentColor.sc17,
// colors: AppConstants().thNormalButton,
// );
// }
// },
// child: SizedBox(
// width: 17.rpx,
// height: 17.rpx,
// child: SvgPicture.asset(
// 'assets/img/icon/explain.svg',
// fit: BoxFit.cover,
// color: Colors.white,
// ),
// ),
// ),
// ],
// );
// }
// }
// class SingleBarPainter extends CustomPainter {
// final double value;
// final Color color;
// final bool showPercent;
// final double maxValue = 100;
// SingleBarPainter({
// required this.value,
// required this.color,
// required this.showPercent,
// });
// @override
// void paint(Canvas canvas, Size size) {
// final barHeight = 24.0.rpx;
// final rightPadding = 20.0.rpx;
// final chartWidth = size.width - rightPadding;
// final left = 0.0;
// final right = left + (value / maxValue) * chartWidth;
// final rect = Rect.fromLTWH(
// left, (size.height - barHeight) / 2, right - left, barHeight);
// final paint = Paint()..color = color;
// canvas.drawRect(rect, paint);
// if (showPercent) {
// final textStyle =
// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
// final textPainter = TextPainter(textDirection: TextDirection.ltr);
// textPainter.text =
// TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle);
// textPainter.layout();
// canvas.save();
// canvas.clipRect(Rect.fromLTWH(
// left, (size.height - barHeight) / 2, chartWidth, barHeight));
// textPainter.paint(
// canvas,
// Offset(right + 4.0.rpx, (size.height - textPainter.height) / 2),
// );
// canvas.restore();
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// }
// class GridPainter extends CustomPainter {
// final double totalHeight;
// final int gridCount = 5;
// final double maxValue = 100;
// final double bottomPadding = 30.0.rpx;
// GridPainter({required this.totalHeight});
// @override
// void paint(Canvas canvas, Size size) {
// final Paint gridPaint = Paint()
// ..color = Colors.grey.withOpacity(0.3)
// ..strokeWidth = 1.0.rpx;
// final double rightPadding = 20.0.rpx;
// final double chartWidth = size.width - rightPadding;
// final double chartHeight = totalHeight - bottomPadding;
// for (int i = 0; i <= gridCount; i++) {
// double dx = (i / gridCount) * chartWidth;
// _drawDashedLine(
// canvas, Offset(dx, 0), Offset(dx, chartHeight), gridPaint);
// final percent = (i / gridCount) * maxValue;
// final TextPainter textPainter = TextPainter(
// textDirection: TextDirection.ltr,
// text: TextSpan(
// text: '${percent.toInt()}',
// style: TextStyle(color: Colors.grey, fontSize: 18.rpx),
// ),
// );
// textPainter.layout();
// textPainter.paint(
// canvas, Offset(dx - textPainter.width / 2, chartHeight + 4.0.rpx));
// }
// }
// void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
// const double dashWidth = 6.0;
// const double dashSpace = 4.0;
// final double totalHeight = (end.dy - start.dy).abs();
// double currentY = start.dy;
// while (currentY < end.dy) {
// final double nextY = currentY + dashWidth;
// canvas.drawLine(
// Offset(start.dx, currentY),
// Offset(start.dx, nextY > end.dy ? end.dy : nextY),
// paint,
// );
// currentY = nextY + dashSpace;
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
// }
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
@@ -10,11 +314,13 @@ import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
class HorizontalBarChart extends StatelessWidget {
final List<Map<String, dynamic>> showLabel;
final bool showPercent;
final bool showRangeBackground; // 新增参数,控制是否显示区间背景
const HorizontalBarChart({
super.key,
required this.showLabel,
this.showPercent = true,
this.showRangeBackground = false, // 默认为false
});
@override
@@ -66,10 +372,13 @@ class HorizontalBarChart extends StatelessWidget {
Expanded(
child: Stack(
children: [
// 网格线背景
// 背景层 - 包含网格线和区间背景
CustomPaint(
size: Size(double.infinity, totalHeight),
painter: GridPainter(totalHeight: totalHeight),
painter: GridPainter(
totalHeight: totalHeight,
showRangeBackground: showRangeBackground,
),
),
// 柱状图列表
@@ -240,7 +549,12 @@ class SingleBarPainter extends CustomPainter {
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
bool shouldRepaint(covariant CustomPainter oldDelegate) {
if (oldDelegate is SingleBarPainter) {
return oldDelegate.value != value || oldDelegate.color != color;
}
return true;
}
}
class GridPainter extends CustomPainter {
@@ -248,19 +562,58 @@ class GridPainter extends CustomPainter {
final int gridCount = 5;
final double maxValue = 100;
final double bottomPadding = 30.0.rpx;
final bool showRangeBackground; // 新增参数
GridPainter({required this.totalHeight});
GridPainter({
required this.totalHeight,
this.showRangeBackground = false,
});
@override
void paint(Canvas canvas, Size size) {
final Paint gridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0.rpx;
final double rightPadding = 20.0.rpx;
final double chartWidth = size.width - rightPadding;
final double chartHeight = totalHeight - bottomPadding;
// 如果需要显示区间背景
if (showRangeBackground) {
final double segmentWidth = chartWidth / gridCount;
// 计算0-60对应的网格区间60对应3格因为每格20
// 0-60: 第0-3格0,20,40,60
for (int i = 0; i < 3; i++) {
final rect = Rect.fromLTWH(
i * segmentWidth,
0,
segmentWidth,
chartHeight,
);
final paint = Paint()
..color = themeController.currentColor.sc1.withOpacity(0.2)
..style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
}
// 60-100: 第3-5格60,80,100
for (int i = 3; i < 5; i++) {
final rect = Rect.fromLTWH(
i * segmentWidth,
0,
segmentWidth,
chartHeight,
);
final paint = Paint()
..color = themeController.currentColor.sc9.withOpacity(0.2)
..style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
}
}
// 绘制网格线
final Paint gridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0.rpx;
for (int i = 0; i <= gridCount; i++) {
double dx = (i / gridCount) * chartWidth;
_drawDashedLine(
@@ -299,5 +652,10 @@ class GridPainter extends CustomPainter {
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
bool shouldRepaint(covariant CustomPainter oldDelegate) {
if (oldDelegate is GridPainter) {
return oldDelegate.showRangeBackground != showRangeBackground;
}
return true;
}
}

View File

@@ -0,0 +1,977 @@
// import 'package:ef/ef.dart';
// import 'package:fl_chart/fl_chart.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_svg/svg.dart';
// import 'package:vbvs_app/common/color/appConstants.dart';
// import 'package:vbvs_app/common/util/FitTool.dart';
// import 'package:vbvs_app/common/util/MyUtils.dart';
// class QcTimeSeriesPoint {
// final double value;
// QcTimeSeriesPoint(this.value);
// }
// class QcTimeSeriesChart extends StatelessWidget {
// final List<QcTimeSeriesPoint> dataPoints;
// final double yMin;
// final double yMax;
// final int xSegmentCount;
// final double? baseValue; // 基准值,可选
// final String? baseLabel; // 基准值标签,可选
// const QcTimeSeriesChart({
// Key? key,
// required this.yMin,
// required this.yMax,
// required this.dataPoints,
// this.xSegmentCount = 11,
// this.baseValue,
// this.baseLabel,
// }) : super(key: key);
// int get _dataPointCount => dataPoints.length;
// List<double> _generateYAxisTicks() {
// if (yMin >= yMax) {
// return [0, 20, 40, 60, 80, 100];
// }
// double step = (yMax - yMin) / 5;
// List<double> ticks = [];
// for (int i = 0; i <= 5; i++) {
// ticks.add(yMin + (step * i));
// }
// return ticks;
// }
// // 计算所有刻度位置对应的索引
// List<int> _getTickIndices() {
// List<int> tickIndices = [];
// // 确保至少显示两个刻度(起点和终点)
// if (_dataPointCount <= 1) {
// return [0];
// }
// // 计算合理的刻度间隔
// double step = _dataPointCount / (xSegmentCount - 1);
// for (int i = 0; i < xSegmentCount; i++) {
// int index = (i * step).round();
// // 确保索引在有效范围内
// index = index.clamp(0, _dataPointCount - 1);
// // 避免重复的索引
// if (tickIndices.isEmpty || index != tickIndices.last) {
// tickIndices.add(index);
// }
// }
// // 确保最后一个点是最后一个数据点
// if (tickIndices.last != _dataPointCount - 1) {
// tickIndices.add(_dataPointCount - 1);
// }
// return tickIndices;
// }
// // 查找最大值和最小值的索引和值
// Map<String, dynamic> _findMinMax() {
// if (dataPoints.isEmpty)
// return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0};
// int minIndex = -1;
// int maxIndex = -1;
// double minValue = double.infinity;
// double maxValue = -double.infinity;
// for (int i = 0; i < dataPoints.length; i++) {
// var point = dataPoints[i];
// if (point.value != -1) {
// // 跳过无效数据
// if (point.value < minValue) {
// minValue = point.value;
// minIndex = i;
// }
// if (point.value > maxValue) {
// maxValue = point.value;
// maxIndex = i;
// }
// }
// }
// return {
// 'minIndex': minIndex,
// 'maxIndex': maxIndex,
// 'minValue': minValue,
// 'maxValue': maxValue
// };
// }
// @override
// Widget build(BuildContext context) {
// // final yTicks = _generateYAxisTicks();
// final double xMax = _dataPointCount.toDouble();
// final double yRange = yMax - yMin;
// final double yInterval = yRange > 0 ? yRange / 5 : 20.0;
// final tickIndices = _getTickIndices();
// final minMaxData = _findMinMax();
// // 将数据点分割成多个连续段
// List<List<FlSpot>> lineSegments = [];
// List<FlSpot> currentSegment = [];
// for (int i = 0; i < dataPoints.length; i++) {
// var point = dataPoints[i];
// if (point.value != -1) {
// currentSegment.add(FlSpot(
// (i + 1).toDouble(),
// point.value,
// ));
// } else if (currentSegment.isNotEmpty) {
// lineSegments.add(currentSegment);
// currentSegment = [];
// }
// }
// if (currentSegment.isNotEmpty) {
// lineSegments.add(currentSegment);
// }
// // 创建渐变填充的线图数据
// List<LineChartBarData> lineBarsData = [];
// for (var segment in lineSegments) {
// if (segment.isEmpty) continue;
// lineBarsData.add(
// LineChartBarData(
// spots: segment,
// isCurved: false,
// color: themeController.currentColor.sc2,
// barWidth: 2,
// dotData: FlDotData(show: false),
// preventCurveOverShooting: true,
// belowBarData: BarAreaData(
// show: true,
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// colors: [
// themeController.currentColor.sc2.withOpacity(0.3),
// themeController.currentColor.sc2.withOpacity(0.1),
// Colors.transparent,
// ],
// stops: const [0.0, 1, 1.0],
// ),
// applyCutOffY: true,
// cutOffY: 0,
// ),
// ),
// );
// }
// // 创建刻度点的数据(绿色小球)
// List<FlSpot> tickSpots = [];
// for (int index in tickIndices) {
// if (index >= 0 && index < dataPoints.length) {
// var point = dataPoints[index];
// if (point.value != -1) {
// tickSpots.add(FlSpot(
// (index + 1).toDouble(),
// point.value,
// ));
// }
// }
// }
// // 准备水平线列表
// List<HorizontalLine> horizontalLines = [
// HorizontalLine(
// y: 0,
// color: themeController.currentColor.sc4,
// strokeWidth: 1.rpx,
// ),
// ];
// // 如果有基准值,添加基准线
// if (baseValue != null) {
// horizontalLines.add(
// HorizontalLine(
// y: baseValue!,
// color: themeController.currentColor.sc9,
// strokeWidth: 2.rpx,
// dashArray: [8, 4], // 虚线样式
// label: HorizontalLineLabel(
// show: true,
// alignment: Alignment.topRight,
// labelResolver: (line) =>
// "基准".tr + '${baseValue!.toStringAsFixed(0)}',
// style: TextStyle(
// color: themeController.currentColor.sc9,
// fontSize: 18.rpx,
// fontWeight: FontWeight.w500,
// ),
// ),
// ),
// );
// }
// return AspectRatio(
// aspectRatio: 2,
// child: LayoutBuilder(
// builder: (context, constraints) {
// return Stack(
// children: [
// LineChart(
// LineChartData(
// minX: 1,
// maxX: xMax + 0.5,
// minY: yMin - 2,
// maxY: yMax + 2,
// gridData: FlGridData(show: false),
// extraLinesData: ExtraLinesData(
// horizontalLines: horizontalLines,
// ),
// titlesData: FlTitlesData(
// bottomTitles: AxisTitles(
// sideTitles: SideTitles(
// showTitles: true,
// reservedSize: 30,
// interval: 1, // 让 fl_chart 自动计算合适的间隔
// getTitlesWidget: (value, meta) {
// // 只显示我们在 tickIndices 中定义的刻度
// int index = value.round() - 1;
// if (index >= 0 &&
// index < _dataPointCount &&
// tickIndices.contains(index)) {
// String label;
// if (index == 0) {
// label = '0';
// } else if (index == _dataPointCount - 1) {
// label = '${_dataPointCount}';
// } else {
// label = '${index + 1}';
// }
// return Padding(
// padding: const EdgeInsets.only(top: 8.0),
// child: Text(
// label,
// style: TextStyle(
// color: themeController.currentColor.sc4,
// fontSize: 14.rpx,
// ),
// ),
// );
// }
// return const SizedBox.shrink();
// },
// ),
// ),
// leftTitles: AxisTitles(
// sideTitles: SideTitles(
// showTitles: true,
// reservedSize: 60.rpx,
// interval: yInterval,
// getTitlesWidget: (value, meta) {
// return Padding(
// padding: const EdgeInsets.only(right: 8.0),
// child: Text(
// value.toStringAsFixed(0),
// style: TextStyle(
// color: themeController.currentColor.sc4,
// fontSize: 16.rpx,
// ),
// textAlign: TextAlign.right,
// ),
// );
// },
// ),
// ),
// rightTitles:
// AxisTitles(sideTitles: SideTitles(showTitles: false)),
// topTitles:
// AxisTitles(sideTitles: SideTitles(showTitles: false)),
// ),
// borderData: FlBorderData(
// show: true,
// border: Border(
// bottom: BorderSide(color: Colors.grey.withOpacity(0.3)),
// left: BorderSide(color: Colors.grey.withOpacity(0.3)),
// right: BorderSide.none,
// top: BorderSide.none,
// ),
// ),
// lineBarsData: [
// ...lineBarsData,
// // 添加绿色小球的线图数据
// LineChartBarData(
// spots: tickSpots,
// isCurved: false,
// color: Colors.transparent,
// barWidth: 0,
// dotData: FlDotData(
// show: true,
// getDotPainter: (spot, percent, barData, index) {
// return FlDotCirclePainter(
// radius: 4.rpx,
// color: stringToColor("#5DD8C9"),
// strokeWidth: 1,
// strokeColor: stringToColor("#00C1AA"),
// );
// },
// ),
// preventCurveOverShooting: true,
// ),
// ],
// ),
// ),
// // 使用 Positioned 来绘制最大值和最小值的标签
// if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1)
// _buildMinMaxLabels(
// context, constraints, minMaxData, xMax, yMin, yMax),
// ],
// );
// },
// ),
// );
// }
// Widget _buildMinMaxLabels(
// BuildContext context,
// BoxConstraints constraints,
// Map<String, dynamic> minMaxData,
// double xMax,
// double yMin,
// double yMax,
// ) {
// // 获取图表区域的实际绘制区域
// // fl_chart 默认会有一些内边距,我们需要估算这些内边距
// double leftPadding = 60.rpx; // 左侧留白用于Y轴标签
// double rightPadding = 10.rpx; // 右侧留白
// double topPadding = 10.rpx; // 顶部留白
// double bottomPadding = 30.rpx; // 底部留白用于X轴标签
// double chartWidth = constraints.maxWidth - leftPadding - rightPadding;
// double chartHeight = constraints.maxHeight - topPadding - bottomPadding;
// // X轴范围1 到 xMax
// // Y轴范围yMin - 2 到 yMax + 2
// double xMin = 1;
// double xMaxValue = xMax;
// double yMinValue = yMin - 2;
// double yMaxValue = yMax + 2;
// double xRange = xMaxValue - xMin;
// double yRange = yMaxValue - yMinValue;
// List<Widget> labels = [];
// // 添加最小值标签
// if (minMaxData['minIndex'] != -1) {
// double minX = (minMaxData['minIndex'] + 1).toDouble();
// double minY = minMaxData['minValue'];
// // 计算在图表中的相对位置 (0-1)
// double relativeX = (minX - xMin) / xRange;
// double relativeY = (minY - yMinValue) / yRange;
// // 转换为像素位置
// double left = leftPadding + (relativeX * chartWidth);
// double top = topPadding + ((1 - relativeY) * chartHeight);
// // 标签尺寸
// double labelWidth = 38.rpx;
// double labelHeight = 50.rpx;
// labels.add(
// Positioned(
// left: left - labelWidth / 2, // 水平居中
// top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
// child: SizedBox(
// width: labelWidth,
// height: labelHeight,
// child: Stack(
// alignment: Alignment.center,
// children: [
// // SVG图片作为背景
// SvgPicture.asset(
// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径
// // width: 28.rpx,
// // height: 40.rpx,
// fit: BoxFit.contain,
// color: stringToColor("#d69dd2"),
// ),
// Padding(
// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离
// child: Text(
// '${minMaxData['minValue'].toStringAsFixed(0)}',
// style: TextStyle(
// color: Colors.white,
// fontSize: AppConstants().smaller_text_fontSize,
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// // 添加最大值标签
// if (minMaxData['maxIndex'] != -1 &&
// minMaxData['maxIndex'] != minMaxData['minIndex']) {
// double maxX = (minMaxData['maxIndex'] + 1).toDouble();
// double maxY = minMaxData['maxValue'];
// // 计算在图表中的相对位置 (0-1)
// double relativeX = (maxX - xMin) / xRange;
// double relativeY = (maxY - yMinValue) / yRange;
// // 转换为像素位置
// double left = leftPadding + (relativeX * chartWidth);
// double top = topPadding + ((1 - relativeY) * chartHeight);
// // 标签尺寸
// double labelWidth = 38.rpx;
// double labelHeight = 50.rpx;
// labels.add(
// Positioned(
// left: left - labelWidth / 2, // 水平居中
// top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
// child: SizedBox(
// width: labelWidth,
// height: labelHeight,
// child: Stack(
// alignment: Alignment.center,
// children: [
// // SVG图片作为背景
// SvgPicture.asset(
// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径
// // width: 28.rpx,
// // height: 40.rpx,
// fit: BoxFit.contain,
// color: stringToColor("#FF9F66"),
// ),
// // 文字覆盖在SVG上
// // Text(
// // '${minMaxData['maxValue'].toStringAsFixed(0)}',
// // style: TextStyle(
// // color: Colors.white,
// // fontSize: AppConstants().smaller_text_fontSize,
// // // fontWeight: FontWeight.bold,
// // ),
// // ),
// Padding(
// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离
// child: Text(
// '${minMaxData['maxValue'].toStringAsFixed(0)}',
// style: TextStyle(
// color: Colors.white,
// fontSize: AppConstants().smaller_text_fontSize,
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// return Stack(
// children: labels,
// );
// }
// }
import 'package:ef/ef.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
class QcTimeSeriesPoint {
final double value;
QcTimeSeriesPoint(this.value);
}
class QcTimeSeriesChart extends StatelessWidget {
final List<QcTimeSeriesPoint> dataPoints;
final double yMin;
final double yMax; // 注意:这个值在使用时会自动加 padding
final int xSegmentCount;
final double? baseValue; // 基准值,可选
final String? baseLabel; // 基准值标签,可选
final double yAxisPadding; // Y轴顶部留白默认为20
const QcTimeSeriesChart({
Key? key,
required this.yMin,
required this.yMax,
required this.dataPoints,
this.xSegmentCount = 11,
this.baseValue,
this.baseLabel,
this.yAxisPadding = 20.0, // 默认为20
}) : super(key: key);
// 添加一个 getter 来获取实际使用的 yMax自动加 padding
double get _actualYMax => yMax + yAxisPadding;
int get _dataPointCount => dataPoints.length;
List<double> _generateYAxisTicks() {
if (yMin >= _actualYMax) {
return [0, 20, 40, 60, 80, 100];
}
double step = (_actualYMax - yMin) / 5;
List<double> ticks = [];
for (int i = 0; i <= 5; i++) {
ticks.add(yMin + (step * i));
}
return ticks;
}
// 计算所有刻度位置对应的索引
List<int> _getTickIndices() {
List<int> tickIndices = [];
// 确保至少显示两个刻度(起点和终点)
if (_dataPointCount <= 1) {
return [0];
}
// 计算合理的刻度间隔
double step = _dataPointCount / (xSegmentCount - 1);
for (int i = 0; i < xSegmentCount; i++) {
int index = (i * step).round();
// 确保索引在有效范围内
index = index.clamp(0, _dataPointCount - 1);
// 避免重复的索引
if (tickIndices.isEmpty || index != tickIndices.last) {
tickIndices.add(index);
}
}
// 确保最后一个点是最后一个数据点
if (tickIndices.last != _dataPointCount - 1) {
tickIndices.add(_dataPointCount - 1);
}
return tickIndices;
}
// 查找最大值和最小值的索引和值
Map<String, dynamic> _findMinMax() {
if (dataPoints.isEmpty)
return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0};
int minIndex = -1;
int maxIndex = -1;
double minValue = double.infinity;
double maxValue = -double.infinity;
for (int i = 0; i < dataPoints.length; i++) {
var point = dataPoints[i];
if (point.value != -1) {
// 跳过无效数据
if (point.value < minValue) {
minValue = point.value;
minIndex = i;
}
if (point.value > maxValue) {
maxValue = point.value;
maxIndex = i;
}
}
}
return {
'minIndex': minIndex,
'maxIndex': maxIndex,
'minValue': minValue,
'maxValue': maxValue
};
}
@override
Widget build(BuildContext context) {
// final yTicks = _generateYAxisTicks();
final double xMax = _dataPointCount.toDouble();
final double yRange = _actualYMax - yMin;
final double yInterval = yRange > 0 ? yRange / 5 : 20.0;
final tickIndices = _getTickIndices();
final minMaxData = _findMinMax();
// 将数据点分割成多个连续段
List<List<FlSpot>> lineSegments = [];
List<FlSpot> currentSegment = [];
for (int i = 0; i < dataPoints.length; i++) {
var point = dataPoints[i];
if (point.value != -1) {
currentSegment.add(FlSpot(
(i + 1).toDouble(),
point.value,
));
} else if (currentSegment.isNotEmpty) {
lineSegments.add(currentSegment);
currentSegment = [];
}
}
if (currentSegment.isNotEmpty) {
lineSegments.add(currentSegment);
}
// 创建渐变填充的线图数据
List<LineChartBarData> lineBarsData = [];
for (var segment in lineSegments) {
if (segment.isEmpty) continue;
lineBarsData.add(
LineChartBarData(
spots: segment,
isCurved: false,
color: themeController.currentColor.sc2,
barWidth: 2,
dotData: FlDotData(show: false),
preventCurveOverShooting: true,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
themeController.currentColor.sc2.withOpacity(0.3),
themeController.currentColor.sc2.withOpacity(0.1),
Colors.transparent,
],
stops: const [0.0, 1, 1.0],
),
applyCutOffY: true,
cutOffY: 0,
),
),
);
}
// 创建刻度点的数据(绿色小球)
List<FlSpot> tickSpots = [];
for (int index in tickIndices) {
if (index >= 0 && index < dataPoints.length) {
var point = dataPoints[index];
if (point.value != -1) {
tickSpots.add(FlSpot(
(index + 1).toDouble(),
point.value,
));
}
}
}
// 准备水平线列表
List<HorizontalLine> horizontalLines = [
HorizontalLine(
y: 0,
color: themeController.currentColor.sc4,
strokeWidth: 1.rpx,
),
];
// 如果有基准值,添加基准线
if (baseValue != null) {
horizontalLines.add(
HorizontalLine(
y: baseValue!,
color: themeController.currentColor.sc9,
strokeWidth: 2.rpx,
dashArray: [8, 4], // 虚线样式
label: HorizontalLineLabel(
show: true,
alignment: Alignment.topRight,
labelResolver: (line) =>
"基准".tr + '${baseValue!.toStringAsFixed(0)}',
style: TextStyle(
color: themeController.currentColor.sc9,
fontSize: 18.rpx,
fontWeight: FontWeight.w500,
),
),
),
);
}
return AspectRatio(
aspectRatio: 2,
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
LineChart(
LineChartData(
minX: 1,
maxX: xMax + 0.5,
minY: yMin - 2,
maxY: _actualYMax + 2,
gridData: FlGridData(show: false),
extraLinesData: ExtraLinesData(
horizontalLines: horizontalLines,
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1, // 让 fl_chart 自动计算合适的间隔
getTitlesWidget: (value, meta) {
// 只显示我们在 tickIndices 中定义的刻度
int index = value.round() - 1;
if (index >= 0 &&
index < _dataPointCount &&
tickIndices.contains(index)) {
String label;
if (index == 0) {
label = '0';
} else if (index == _dataPointCount - 1) {
label = '${_dataPointCount}';
} else {
label = '${index + 1}';
}
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
label,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: 14.rpx,
),
),
);
}
return const SizedBox.shrink();
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 60.rpx,
interval: yInterval,
getTitlesWidget: (value, meta) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
value.toStringAsFixed(0),
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: 16.rpx,
),
textAlign: TextAlign.right,
),
);
},
),
),
rightTitles:
AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles:
AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(color: Colors.grey.withOpacity(0.3)),
left: BorderSide(color: Colors.grey.withOpacity(0.3)),
right: BorderSide.none,
top: BorderSide.none,
),
),
lineBarsData: [
...lineBarsData,
// 添加绿色小球的线图数据
LineChartBarData(
spots: tickSpots,
isCurved: false,
color: Colors.transparent,
barWidth: 0,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4.rpx,
color: stringToColor("#5DD8C9"),
strokeWidth: 1,
strokeColor: stringToColor("#00C1AA"),
);
},
),
preventCurveOverShooting: true,
),
],
),
),
// 使用 Positioned 来绘制最大值和最小值的标签
if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1)
_buildMinMaxLabels(
context, constraints, minMaxData, xMax, yMin, _actualYMax),
],
);
},
),
);
}
Widget _buildMinMaxLabels(
BuildContext context,
BoxConstraints constraints,
Map<String, dynamic> minMaxData,
double xMax,
double yMin,
double yMax,
) {
// 获取图表区域的实际绘制区域
// fl_chart 默认会有一些内边距,我们需要估算这些内边距
double leftPadding = 60.rpx; // 左侧留白用于Y轴标签
double rightPadding = 10.rpx; // 右侧留白
double topPadding = 10.rpx; // 顶部留白
double bottomPadding = 30.rpx; // 底部留白用于X轴标签
double chartWidth = constraints.maxWidth - leftPadding - rightPadding;
double chartHeight = constraints.maxHeight - topPadding - bottomPadding;
// X轴范围1 到 xMax
// Y轴范围yMin - 2 到 yMax + 2
double xMin = 1;
double xMaxValue = xMax;
double yMinValue = yMin - 2;
double yMaxValue = yMax + 2;
double xRange = xMaxValue - xMin;
double yRange = yMaxValue - yMinValue;
List<Widget> labels = [];
// 添加最小值标签
if (minMaxData['minIndex'] != -1) {
double minX = (minMaxData['minIndex'] + 1).toDouble();
double minY = minMaxData['minValue'];
// 计算在图表中的相对位置 (0-1)
double relativeX = (minX - xMin) / xRange;
double relativeY = (minY - yMinValue) / yRange;
// 转换为像素位置
double left = leftPadding + (relativeX * chartWidth);
double top = topPadding + ((1 - relativeY) * chartHeight);
// 标签尺寸
double labelWidth = 38.rpx;
double labelHeight = 50.rpx;
labels.add(
Positioned(
left: left - labelWidth / 2, // 水平居中
top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
child: SizedBox(
width: labelWidth,
height: labelHeight,
child: Stack(
alignment: Alignment.center,
children: [
// SVG图片作为背景
SvgPicture.asset(
'assets/img/icon/location.svg',
fit: BoxFit.contain,
color: stringToColor("#d69dd2"),
),
Padding(
padding: EdgeInsets.only(bottom: 12.rpx),
child: Text(
'${minMaxData['minValue'].toStringAsFixed(0)}',
style: TextStyle(
color: Colors.white,
fontSize: AppConstants().smaller_text_fontSize,
),
),
),
],
),
),
),
);
}
// 添加最大值标签
if (minMaxData['maxIndex'] != -1 &&
minMaxData['maxIndex'] != minMaxData['minIndex']) {
double maxX = (minMaxData['maxIndex'] + 1).toDouble();
double maxY = minMaxData['maxValue'];
// 计算在图表中的相对位置 (0-1)
double relativeX = (maxX - xMin) / xRange;
double relativeY = (maxY - yMinValue) / yRange;
// 转换为像素位置
double left = leftPadding + (relativeX * chartWidth);
double top = topPadding + ((1 - relativeY) * chartHeight);
// 标签尺寸
double labelWidth = 38.rpx;
double labelHeight = 50.rpx;
labels.add(
Positioned(
left: left - labelWidth / 2, // 水平居中
top: top - labelHeight - 8.rpx, // 显示在点的正上方留出8rpx间距
child: SizedBox(
width: labelWidth,
height: labelHeight,
child: Stack(
alignment: Alignment.center,
children: [
// SVG图片作为背景
SvgPicture.asset(
'assets/img/icon/location.svg',
fit: BoxFit.contain,
color: stringToColor("#FF9F66"),
),
Padding(
padding: EdgeInsets.only(bottom: 12.rpx),
child: Text(
'${minMaxData['maxValue'].toStringAsFixed(0)}',
style: TextStyle(
color: Colors.white,
fontSize: AppConstants().smaller_text_fontSize,
),
),
),
],
),
),
),
);
}
return Stack(
children: labels,
);
}
}

View File

@@ -1,11 +1,188 @@
// import 'package:flutter/material.dart';
// import 'package:vbvs_app/common/color/appConstants.dart';
// import 'package:vbvs_app/common/util/FitTool.dart';
// import 'package:vbvs_app/common/util/MyUtils.dart';
// class StatusBarWithIndicator extends StatelessWidget {
// final int selectKey;
// final List<Map<String, dynamic>> showLabel;
// final IconData icon;
// final double gap; // 每段之间的间距
// final bool showCurrentValue; // 新增参数,控制是否显示当前值
// final String? currentValueText; // 可选的当前值文字如果不提供则使用选中的name
// const StatusBarWithIndicator({
// super.key,
// required this.selectKey,
// required this.showLabel,
// this.icon = Icons.favorite,
// this.gap = 8.0, // 默认 8.rpx 间距
// this.showCurrentValue = false, // 默认为false
// this.currentValueText,
// });
// @override
// Widget build(BuildContext context) {
// return LayoutBuilder(builder: (context, constraints) {
// final totalWidth = constraints.maxWidth;
// final itemCount = showLabel.length;
// // 每条线的宽度 = (总宽度 - 总间隔)/ 项数
// final totalGap = (itemCount - 1) * gap.rpx;
// final itemWidth = (totalWidth - totalGap) / itemCount;
// // 找到选中项的 index 和对应的数据
// final selectedIndex = showLabel.indexWhere((e) => e['key'] == selectKey);
// final selectedItem = selectedIndex >= 0 ? showLabel[selectedIndex] : null;
// final iconLeft = selectedIndex >= 0
// ? selectedIndex * (itemWidth + gap.rpx) + itemWidth / 2
// : 0.0;
// // 确定要显示的当前值文字
// String displayValue = '';
// if (showCurrentValue) {
// if (currentValueText != null) {
// displayValue = currentValueText!;
// } else if (selectedItem != null) {
// displayValue = selectedItem['name'] ?? '';
// }
// }
// return SizedBox(
// width: double.infinity,
// child: Stack(
// clipBehavior: Clip.none,
// children: [
// if (selectedIndex >= 0) ...[
// // 如果显示当前值,在箭头上方添加文字
// if (showCurrentValue && displayValue.isNotEmpty)
// Positioned(
// left: iconLeft,
// top: -50.rpx, // 调整位置,给文字留出空间
// child: Transform.translate(
// offset: Offset(-45.rpx, 0), // 图片宽度 45.rpx居中偏移
// child: Container(
// // padding: EdgeInsets.symmetric(
// // horizontal: 8.rpx,
// // vertical: 4.rpx,
// // ),
// // decoration: BoxDecoration(
// // color: selectedItem?['color'] ?? Colors.blue,
// // borderRadius: BorderRadius.circular(4.rpx),
// // boxShadow: [
// // BoxShadow(
// // color: Colors.black.withOpacity(0.1),
// // blurRadius: 4.rpx,
// // offset: Offset(0, 2.rpx),
// // ),
// // ],
// // ),
// child: Text(
// displayValue,
// style: TextStyle(
// fontSize: AppConstants().small_an_text_fontSize,
// color: themeController.currentColor.sc9,
// fontWeight: FontWeight.w500,
// ),
// ),
// ),
// ),
// ),
// // 箭头图片
// Positioned(
// left: iconLeft,
// top: showCurrentValue ? -20.rpx : -20.rpx, // 如果有文字,箭头位置稍下移
// child: Transform.translate(
// offset: Offset(-22.5.rpx, 0), // 图片宽度 45.rpx居中偏移
// child: Container(
// width: 45.rpx,
// height: 76.rpx,
// decoration: BoxDecoration(
// image: DecorationImage(
// image: AssetImage('assets/img/tip_arrow.gif'),
// fit: BoxFit.cover,
// ),
// ),
// ),
// ),
// ),
// ],
// // 条形图和文字标签
// Padding(
// padding: EdgeInsets.only(top: showCurrentValue ? 70.rpx : 50.rpx),
// child: Column(
// children: [
// // 条形段(带间距)
// Row(
// children: showLabel.asMap().entries.map((entry) {
// int index = entry.key;
// var item = entry.value;
// return Container(
// width: itemWidth,
// height: 15.rpx,
// margin: EdgeInsets.only(
// left: index == 0 ? 0 : gap.rpx,
// ),
// decoration: BoxDecoration(
// color: item['color'],
// borderRadius: BorderRadius.circular(0.rpx),
// ),
// );
// }).toList(),
// ),
// SizedBox(height: 12.rpx),
// // 名称文字
// Row(
// children: showLabel.asMap().entries.map((entry) {
// int index = entry.key;
// var item = entry.value;
// return Container(
// width: itemWidth,
// margin: EdgeInsets.only(
// left: index == 0 ? 0 : gap.rpx,
// ),
// alignment: Alignment.center,
// child: Text(
// item['name'],
// style: TextStyle(
// fontSize: 24.rpx,
// color: Colors.white,
// ),
// textAlign: TextAlign.center,
// ),
// );
// }).toList(),
// ),
// ],
// ),
// ),
// ],
// ),
// );
// });
// }
// }
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
class StatusBarWithIndicator extends StatelessWidget {
final int selectKey;
final List<Map<String, dynamic>> showLabel;
final IconData icon;
final double gap; // 每段之间的间距
final bool showCurrentValue; // 控制是否显示当前值
final String? currentValueText; // 可选的当前值文字如果不提供则使用选中的name
final bool showRange; // 新增参数,控制是否显示范围
final String? Function(Map<String, dynamic> item)?
rangeTextBuilder; // 可选的范围文字构建器
const StatusBarWithIndicator({
super.key,
@@ -13,6 +190,10 @@ class StatusBarWithIndicator extends StatelessWidget {
required this.showLabel,
this.icon = Icons.favorite,
this.gap = 8.0, // 默认 8.rpx 间距
this.showCurrentValue = false, // 默认为false
this.currentValueText,
this.showRange = false, // 默认为false不显示范围
this.rangeTextBuilder, // 自定义范围文字构建器
});
@override
@@ -25,23 +206,63 @@ class StatusBarWithIndicator extends StatelessWidget {
final totalGap = (itemCount - 1) * gap.rpx;
final itemWidth = (totalWidth - totalGap) / itemCount;
// 找到选中项的 index
// 找到选中项的 index 和对应的数据
final selectedIndex = showLabel.indexWhere((e) => e['key'] == selectKey);
final selectedItem = selectedIndex >= 0 ? showLabel[selectedIndex] : null;
final iconLeft = selectedIndex >= 0
? selectedIndex * (itemWidth + gap.rpx) + itemWidth / 2
: 0.0;
// 确定要显示的当前值文字
String displayValue = '';
if (showCurrentValue) {
if (currentValueText != null) {
displayValue = currentValueText!;
} else if (selectedItem != null) {
displayValue = selectedItem['name'] ?? '';
}
}
// 获取范围文字
String getRangeText(Map<String, dynamic> item) {
if (rangeTextBuilder != null) {
return rangeTextBuilder!(item) ?? '';
}
// 默认返回空字符串,需要外部传入范围数据
return item['range']?.toString() ?? '';
}
return SizedBox(
width: double.infinity,
child: Stack(
clipBehavior: Clip.none,
children: [
if (selectedIndex >= 0)
if (selectedIndex >= 0) ...[
// 如果显示当前值,在箭头上方添加文字
if (showCurrentValue && displayValue.isNotEmpty)
Positioned(
left: iconLeft,
top: -50.rpx,
child: Transform.translate(
offset: Offset(-45.rpx, 0),
child: Text(
displayValue,
style: TextStyle(
fontSize: AppConstants().small_an_text_fontSize,
color: themeController.currentColor.sc9,
fontWeight: FontWeight.w500,
),
),
),
),
// 箭头图片
Positioned(
left: iconLeft,
top: -20.rpx,
top: showCurrentValue ? -20.rpx : -20.rpx,
child: Transform.translate(
offset: Offset(-22.5.rpx, 0), // 图片宽度 45.rpx居中偏移
offset: Offset(-22.5.rpx, 0),
child: Container(
width: 45.rpx,
height: 76.rpx,
@@ -54,8 +275,11 @@ class StatusBarWithIndicator extends StatelessWidget {
),
),
),
],
// 条形图和文字标签
Padding(
padding: EdgeInsets.only(top: 50.rpx),
padding: EdgeInsets.only(top: showCurrentValue ? 70.rpx : 50.rpx),
child: Column(
children: [
// 条形段(带间距)
@@ -78,25 +302,46 @@ class StatusBarWithIndicator extends StatelessWidget {
}).toList(),
),
SizedBox(height: 12.rpx),
// 名称文字
// 名称文字和范围(每个下面显示对应的范围)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: showLabel.asMap().entries.map((entry) {
int index = entry.key;
var item = entry.value;
return Container(
width: itemWidth,
margin: EdgeInsets.only(
left: index == 0 ? 0 : gap.rpx,
),
alignment: Alignment.center,
child: Text(
item['name'],
style: TextStyle(
fontSize: 24.rpx,
color: Colors.white,
return Expanded(
child: Container(
margin: EdgeInsets.only(
left: index == 0 ? 0 : gap.rpx,
),
child: Column(
children: [
// 名称文字
Text(
item['name'],
style: TextStyle(
fontSize: 24.rpx,
color: Colors.white,
),
textAlign: TextAlign.center,
),
// 范围文字(如果显示)
if (showRange)
Padding(
padding: EdgeInsets.only(top: 4.rpx),
child: Text(
"(" + getRangeText(item) + ")",
style: TextStyle(
fontSize: 20.rpx,
color: Colors.white.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
),
],
),
textAlign: TextAlign.center,
),
);
}).toList(),

View File

@@ -1,5 +1,4 @@
import 'dart:math';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
import 'package:ef/ef.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
@@ -8,12 +7,10 @@ import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/base/THFlutterFlowDropDown.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/ScatterPlotChart.dart';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
class HeartPointWidget extends StatefulWidget {
var sleepReport;

View File

@@ -0,0 +1,136 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/AdviceComponnetWidget.dart';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
class QcAIAdviceWidget extends StatefulWidget {
var sleepReport;
QcAIAdviceWidget({super.key, required this.sleepReport});
@override
State<QcAIAdviceWidget> createState() => _QcAIAdviceWidgetState();
}
class _QcAIAdviceWidgetState extends State<QcAIAdviceWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
if (widget.sleepReport == null ||
widget.sleepReport['sugges'] == null ||
widget.sleepReport['sugges'].isEmpty) {
return Container();
}
List advices = widget.sleepReport['sugges'];
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"AI分析".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx),//
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
// "AI分析介绍".tr,
"AI分析是指利用人工智能技术对用户的睡眠数据进行自动化处理与规律提取对用户的异常睡眠数据给到一定的辅助决策或者解决问题。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 31.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx),
child: Column(
children: advices.map<Widget>((advice) {
return AdviceComponnetWidget(
title: advice["q"],
description: advice["s"],
).paddingOnly(bottom: 0.rpx); // 在每个组件下方添加间隔
}).toList(),
),
)
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("AI分析绘制异常${e}");
return Container();
}
}
}

View File

@@ -0,0 +1,355 @@
import 'package:EasyDartModule/EasyDartModule.dart' as es;
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/QcTimeSeriesChart.dart';
class QcBreatheStandardWidget extends StatefulWidget {
var reportData;
QcBreatheStandardWidget({super.key, required this.reportData});
@override
State<QcBreatheStandardWidget> createState() =>
_QcBreatheStandardWidgetState();
}
class _QcBreatheStandardWidgetState extends State<QcBreatheStandardWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
// 计算y轴的最大最小值
(double, double) _calculateYMinMax(List<QcTimeSeriesPoint> dataPoints) {
if (dataPoints.isEmpty) {
return (8.0, 20.0);
}
// 过滤掉无效数据点(值为-1的
final validPoints = dataPoints.where((point) => point.value >= 0).toList();
if (validPoints.isEmpty) {
return (8.0, 20.0);
}
// 找出数据中的实际最小值和最大值
double dataMin =
validPoints.map((point) => point.value).reduce((a, b) => a < b ? a : b);
double dataMax =
validPoints.map((point) => point.value).reduce((a, b) => a > b ? a : b);
// 计算最小值向下取整到5的倍数
double yMin = (dataMin / 5).floor() * 5.0;
// 如果最小值小于0设为0
if (yMin < 0) yMin = 0;
// 计算最大值向上取整到5的倍数
double yMax = (dataMax / 5).ceil() * 5.0;
// 确保至少有10的差值
if (yMax - yMin < 10) {
yMax = yMin + 10;
}
return (yMin, yMax);
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null || widget.reportData is! Map) {
return Container();
}
// 从reportData中获取br数据
Map<String, dynamic> brData = widget.reportData['br'] ?? {};
if (brData.isEmpty) {
return Container();
}
// 获取呼吸数据点
List<dynamic> dataList = brData['data'] ?? [];
List<QcTimeSeriesPoint> dataPoints = [];
// 构建数据点(只保留值,不需要时间戳)
for (int i = 0; i < dataList.length; i++) {
dynamic value = dataList[i];
if (value == null || value == '') {
dataPoints.add(QcTimeSeriesPoint(-1));
} else {
double y = (value as num).toDouble();
dataPoints.add(QcTimeSeriesPoint(y));
}
}
// 计算动态的y轴范围
final (yMin, yMax) = _calculateYMinMax(dataPoints);
// 构建呼吸统计数据
Map<String, dynamic> avgBreath = {
'name': '平均呼吸'.tr,
'value': brData['avg'].toInt() ?? 0,
'unit': '次/分',
};
Map<String, dynamic> baseBreath = {
'name': '基础呼吸',
'value': brData['base'].toInt() ?? 0,
'unit': '次/分',
};
Map<String, dynamic> minBreath = {
'name': '最低呼吸',
'value': brData['min'].toInt() ?? 0,
'unit': '次/分',
};
Map<String, dynamic> maxBreath = {
'name': '最高呼吸',
'value': brData['max'].toInt() ?? 0,
'unit': '次/分',
};
// 构建正常范围字符串
String range = '';
if (baseBreath['value'] != 0) {
int baseValue = baseBreath['value'];
range = '${baseValue - 3}~${baseValue + 3}';
} else {
range = '12~20';
}
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius:
BorderRadius.circular(AppConstants().normal_container_radius),
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"呼吸数据".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white,
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx),
borderRadius: 0.rpx,
onTap: () {
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
"呼吸数据是指用户在睡眠过程中呼吸的基本数据,是评估睡眠呼吸质量、筛查睡眠呼吸障碍的核心指标。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"呼吸数据是指用户在睡眠过程中呼吸的基本数据,是评估睡眠呼吸质量、筛查睡眠呼吸障碍的核心指标。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding:
EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0.rpx, 0),
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 31.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
width: 14.rpx,
height: 14.rpx,
decoration: BoxDecoration(
color: themeController.currentColor.sc2,
shape: BoxShape.circle,
),
),
SizedBox(width: 15.rpx),
Text(
'正常范围 '.tr + range,
style: TextStyle(
fontSize: AppConstants().smaller_text_fontSize,
color: themeController.currentColor.sc3,
),
),
],
),
Container(
width: double.infinity,
child: QcTimeSeriesChart(
yMin: yMin,
yMax: yMax,
dataPoints: dataPoints,
xSegmentCount: 11,
baseValue: brData['base'].toDouble(), // 传入基准值
// baseValue: 16, // 传入基准值
baseLabel: '基准', // 可选的自定义标签
yAxisPadding: 0,
),
),
].divide(SizedBox(
height: 18.rpx,
)),
),
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx),
child: Row(
children: [
_buildBreathItem(avgBreath),
_buildBreathItem(baseBreath),
_buildBreathItem(minBreath),
_buildBreathItem(maxBreath),
],
),
),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("呼吸基准监测绘制异常${e}");
return Container();
}
}
Widget _buildBreathItem(Map<String, dynamic> data) {
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 4.rpx, vertical: 4.rpx),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Text(
"${data['name']}",
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().normal_text_fontSize,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
SizedBox(height: 4.rpx),
Flexible(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
fit: FlexFit.loose,
child: Text(
"${data['value']}",
style: TextStyle(
color: themeController.currentColor.sc2,
fontSize: AppConstants().normal_text_fontSize,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
SizedBox(width: 4.rpx),
Flexible(
fit: FlexFit.loose,
child: Text(
"${data['unit']}",
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().small_text_fontSize,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,208 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/HorizontalBarChart.dart';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
class QcDiseasePercentsWidget extends StatefulWidget {
var reportData; // 改为更通用的名称
QcDiseasePercentsWidget({super.key, required this.reportData});
@override
State<QcDiseasePercentsWidget> createState() =>
_QcDiseasePercentsWidgetState();
}
class _QcDiseasePercentsWidgetState extends State<QcDiseasePercentsWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null) {
return Container();
}
// 从reportData中获取mbzs数据
List mbzsData = widget.reportData['mbzs'] ?? [];
if (mbzsData.isEmpty) {
return Container();
}
var showLabel = convertDiseaseData(mbzsData);
// 如果没有有效数据,不显示组件
if (showLabel.isEmpty) {
return Container();
}
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"慢性病风险指数".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
// 你的点击逻辑
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
// "心理健康评估介绍".tr,
"慢性病风险指数是通过整合个体的生理指标、生活方式等多维度数据,构建的量化评估模型,用于预测用户未来患慢性非传染性疾病(如高血压、糖尿病、冠心病、癌症等)的风险概率。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"慢性病风险指数是通过整合个体的生理指标、生活方式等多维度数据,构建的量化评估模型,用于预测用户未来患慢性非传染性疾病(如高血压、糖尿病、冠心病、癌症等)的风险概率。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 34.rpx,
),
Container(
child: HorizontalBarChart(
showLabel: showLabel,
showPercent: true,
showRangeBackground: true),
),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("疾病绘制异常${e}");
return Container();
}
}
List<Map<String, dynamic>> convertDiseaseData(List data) {
return data.asMap().entries.map<Map<String, dynamic>>((entry) {
final index = entry.key;
final item = entry.value;
// 根据名称生成一个简单的ID如果没有id字段
String id = 'disease_$index';
if (item['name'] != null) {
// 根据名称生成一个简单的哈希值作为id
id = '${item['name']}_$index';
}
// 根据值的大小决定颜色深浅
Color barColor = _getColorByValue(item['val'] ?? 0);
return {
"key": id,
"name": item["name"] ?? '未知指标',
"color": barColor,
"percent": item["val"] ?? 0, // 注意这里从val取值
"explain":
(item["tips"] != null && (item["tips"] as String).trim().isNotEmpty)
? item["tips"]
: '${item["val"] ?? 0}%的风险概率',
};
}).toList();
}
// 根据风险值获取对应的颜色
Color _getColorByValue(int value) {
if (value < 30) {
return stringToColor("#00C1AA"); // 低风险 - 绿色
} else if (value < 60) {
return stringToColor("#FFB800"); // 中风险 - 黄色
} else if (value < 80) {
return stringToColor("#FF7159"); // 高风险 - 橙色
} else {
return stringToColor("#FF4D4F"); // 极高风险 - 红色
}
}
}

View File

@@ -0,0 +1,215 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/FatigueCircleIndicator.dart';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
class QcHeartHealthWidget extends StatefulWidget {
var reportData; // 改为更通用的名称
QcHeartHealthWidget({super.key, required this.reportData});
@override
State<QcHeartHealthWidget> createState() => _QcHeartHealthWidgetState();
}
class _QcHeartHealthWidgetState extends State<QcHeartHealthWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null) {
return Container();
}
// 从reportData中获取xljk数据
List xljkData = widget.reportData['xljk'] ?? [];
if (xljkData.isEmpty) {
return Container();
}
var showLabel = convertMentalHealthData(xljkData);
// 如果没有有效数据,不显示组件
if (showLabel.isEmpty) {
return Container();
}
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 0.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"心理健康评估".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
"心率健康评估主要通过用户睡眠报告中的时间点、体征数据及HRV数据等信息来判断其心理健康水平、疲劳程度。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"心率健康评估主要通过用户睡眠报告中的时间点、体征数据及HRV数据等信息来判断其心理健康水平、疲劳程度。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 104.rpx,
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (showLabel.length > 0)
Flexible(
flex: 1,
child: FatigueCircleIndicator(
data: showLabel[0],
),
),
if (showLabel.length > 1)
Flexible(
flex: 1,
child: FatigueCircleIndicator(
data: showLabel[1],
),
),
].divide(SizedBox(
width: 110.rpx,
)),
),
),
SizedBox(
height: 72.rpx,
),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("心理健康绘制异常${e}");
return Container();
}
}
List<Map<String, dynamic>> convertMentalHealthData(List data) {
return data.map<Map<String, dynamic>>((item) {
final String? colorStr = item['color'];
final int value = item['val'].toInt() ?? 0; // 注意这里从val取值
final String explain =
(item['tips'] != null && (item['tips'] as String).trim().isNotEmpty)
? item['tips']
: '未知数据'.tr;
// 根据value值确定level描述
String level = '';
if (value <= 30) {
level = '较低';
} else if (value <= 60) {
level = '正常';
} else if (value <= 80) {
level = '偏高';
} else {
level = '严重';
}
return {
'name': item['name'] ?? '未知指标',
'color': colorStr != null && colorStr.isNotEmpty
? stringToColor(colorStr)
: stringToColor("#00C1AA"), // 默认颜色
'percent': value,
'explain': level, // 使用计算出的level
"bottomColor": stringToColor("#393D49"),
};
}).toList();
}
}

View File

@@ -0,0 +1,364 @@
import 'package:EasyDartModule/EasyDartModule.dart' as es;
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/QcTimeSeriesChart.dart';
//心率基准
class QcHeartRateStandardWidget extends StatefulWidget {
var reportData;
QcHeartRateStandardWidget({super.key, required this.reportData});
@override
State<QcHeartRateStandardWidget> createState() =>
_QcHeartRateStandardWidgetState();
}
class _QcHeartRateStandardWidgetState extends State<QcHeartRateStandardWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
// 计算y轴的最大最小值
(double, double) _calculateYMinMax(List<QcTimeSeriesPoint> dataPoints) {
if (dataPoints.isEmpty) {
return (50.0, 150.0);
}
// 过滤掉无效数据点(值为-1的
final validPoints = dataPoints.where((point) => point.value >= 0).toList();
if (validPoints.isEmpty) {
return (50.0, 150.0);
}
// 找出数据中的实际最小值和最大值
double dataMin =
validPoints.map((point) => point.value).reduce((a, b) => a < b ? a : b);
double dataMax =
validPoints.map((point) => point.value).reduce((a, b) => a > b ? a : b);
// 计算最小值向下取整到10的倍数
double yMin = (dataMin / 10).floor() * 10.0;
// 如果最小值小于0设为0
if (yMin < 0) yMin = 0;
// 计算最大值向上取整到10的倍数
double yMax = (dataMax / 10).ceil() * 10.0;
// 确保至少有20的差值
if (yMax - yMin < 20) {
yMax = yMin + 20;
}
return (yMin, yMax);
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null || widget.reportData is! Map) {
return Container();
}
// 从reportData中获取hr数据
Map<String, dynamic> hrData = widget.reportData['hr'] ?? {};
if (hrData.isEmpty) {
return Container();
}
// 获取心率数据点
List<dynamic> dataList = hrData['data'] ?? [];
List<QcTimeSeriesPoint> dataPoints = [];
// 构建数据点(只保留值,不需要时间戳)
for (int i = 0; i < dataList.length; i++) {
dynamic value = dataList[i];
if (value == null || value == '') {
dataPoints.add(QcTimeSeriesPoint(-1));
} else {
double y = (value as num).toDouble();
dataPoints.add(QcTimeSeriesPoint(y));
}
}
// 计算动态的y轴范围
final (yMin, yMax) = _calculateYMinMax(dataPoints);
// 构建心率统计数据
Map<String, dynamic> avgHeartRate = {
'name': '平均心率'.tr,
'value': hrData['avg'].toInt() ?? 0,
'unit': '次/分'.tr,
};
Map<String, dynamic> baseHeartRate = {
'name': '基准心率'.tr,
'value': hrData['base'].toInt() ?? 0,
'unit': '次/分'.tr,
};
Map<String, dynamic> minHeartRate = {
'name': '最低心率'.tr,
'value': hrData['min'].toInt() ?? 0,
'unit': '次/分'.tr,
};
Map<String, dynamic> maxHeartRate = {
'name': '最高心率'.tr,
'value': hrData['max'].toInt() ?? 0,
'unit': '次/分'.tr,
};
// 构建正常范围字符串
String range = '';
if (baseHeartRate['value'] != 0) {
int baseValue = baseHeartRate['value'];
range = '${baseValue - 10}~${baseValue + 10}';
} else {
range = '60~100';
}
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius:
BorderRadius.circular(AppConstants().normal_container_radius),
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"心率数据".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white,
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx),
borderRadius: 0.rpx,
onTap: () {
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
"心率数据是指用户在睡眠过程中基本心率数据,可初步判断睡眠中的心血管负荷及自主神经功能状态,为睡眠健康评估提供重要依据。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"心率数据是指用户在睡眠过程中基本心率数据,可初步判断睡眠中的心血管负荷及自主神经功能状态,为睡眠健康评估提供重要依据。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding:
EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0.rpx, 0),
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 31.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx),
child: Column(
children: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0.rpx, 0.rpx, 30.rpx, 0.rpx),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
width: 14.rpx,
height: 14.rpx,
decoration: BoxDecoration(
color: themeController.currentColor.sc2,
shape: BoxShape.circle,
),
),
SizedBox(width: 15.rpx),
Text(
'正常范围 '.tr + range,
style: TextStyle(
fontSize: AppConstants().smaller_text_fontSize,
color: themeController.currentColor.sc3,
),
),
],
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0.rpx, 0.rpx, 30.rpx, 0.rpx),
child: Container(
width: double.infinity,
child: QcTimeSeriesChart(
yMin: yMin,
yMax: yMax,
dataPoints: dataPoints,
xSegmentCount: 11,
baseValue: hrData['base'].toDouble(), // 传入基准值
// baseValue: 65, // 传入基准值
baseLabel: '基准', // 可选的自定义标签
yAxisPadding: 20,
),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0.rpx, 0.rpx, 0.rpx, 0.rpx),
child: Row(
children: [
_buildHeartRateItem(avgHeartRate),
_buildHeartRateItem(baseHeartRate),
_buildHeartRateItem(minHeartRate),
_buildHeartRateItem(maxHeartRate),
],
),
),
].divide(SizedBox(
height: 18.rpx,
)),
),
),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("心率基准绘制异常${e}");
return Container();
}
}
Widget _buildHeartRateItem(Map<String, dynamic> data) {
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 4.rpx, vertical: 4.rpx),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Text(
"${data['name']}",
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().normal_text_fontSize,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
SizedBox(height: 4.rpx),
Flexible(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
fit: FlexFit.loose,
child: Text(
"${data['value']}",
style: TextStyle(
color: themeController.currentColor.sc2,
fontSize: AppConstants().normal_text_fontSize,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
SizedBox(width: 6.rpx),
Flexible(
fit: FlexFit.loose,
child: Text(
"${data['unit']}",
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().small_text_fontSize,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,238 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/StatusBarWithIndicator.dart';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
class QcPiLaoZhiShuPercentWidget extends StatefulWidget {
var reportData; // 改为更通用的名称
QcPiLaoZhiShuPercentWidget({super.key, required this.reportData});
@override
State<QcPiLaoZhiShuPercentWidget> createState() =>
_PiLaoZhiShuPercentWidgetState();
}
class _PiLaoZhiShuPercentWidgetState extends State<QcPiLaoZhiShuPercentWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null) {
return Container();
}
// 从reportData中获取plzs数据
Map<String, dynamic> plzsData = widget.reportData['plzs'] ?? {};
if (plzsData.isEmpty) {
return Container();
}
int level = plzsData['level'] ?? 0;
// 根据level值确定显示内容和颜色
String levelText = '';
Color levelColor = themeController.currentColor.sc3;
switch (level) {
case 0:
levelText = '正常';
levelColor = Colors.green;
break;
case 1:
levelText = '轻度疲劳';
levelColor = Colors.orange;
break;
case 2:
levelText = '中度疲劳';
levelColor = Colors.red;
break;
case 3:
levelText = '重度疲劳';
levelColor = Colors.red;
break;
default:
levelText = '未知';
levelColor = Colors.grey;
}
// 构建StatusBarWithIndicator需要的数据格式
List<Map<String, dynamic>> showLabel = [
{
'key': 0,
'name': '正常',
'color': Colors.green,
},
{
'key': 1,
'name': '轻度疲劳',
'color': Colors.orange,
},
{
'key': 2,
'name': '中度疲劳',
'color': Colors.red,
},
{
'key': 3,
'name': '重度疲劳',
'color': Colors.red,
},
];
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius:
BorderRadius.circular(AppConstants().normal_container_radius),
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"疲劳指数".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white,
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx),
borderRadius: 0.rpx,
onTap: () {
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
"疲劳指数是评估人体疲劳程度的重要指标,反映身体和精神状态的疲劳水平。".tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"疲劳指数是评估人体疲劳程度的重要指标,反映身体和精神状态的疲劳水平。".tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding:
EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0.rpx, 0),
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 83.rpx,
),
// 显示level值的文本
// Padding(
// padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 30.rpx),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(
// '当前状态:',
// style: TextStyle(
// fontSize: 30.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// Text(
// levelText,
// style: TextStyle(
// fontSize: 36.rpx,
// fontWeight: FontWeight.bold,
// color: levelColor,
// ),
// ),
// ],
// ),
// ),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: StatusBarWithIndicator(
selectKey: level, // 使用level值作为选中的key
showLabel: showLabel,
currentValueText: "当前属于".tr,
showCurrentValue: true,
),
),
SizedBox(
height: 56.rpx,
),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("疲劳指数绘制异常${e}");
return Container();
}
}
}

View File

@@ -0,0 +1,242 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/StatusBarWithIndicator.dart';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
class QcZiZhuShenJingPercentWidget extends StatefulWidget {
var reportData; // 改为更通用的名称
QcZiZhuShenJingPercentWidget({super.key, required this.reportData});
@override
State<QcZiZhuShenJingPercentWidget> createState() =>
_ZiZhuShenJingPercentWidgetState();
}
class _ZiZhuShenJingPercentWidgetState
extends State<QcZiZhuShenJingPercentWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null) {
return Container();
}
// 从reportData中获取zzsjphzs数据
Map<String, dynamic> zzsjphzsData = widget.reportData['zzsjphzs'] ?? {};
if (zzsjphzsData.isEmpty) {
return Container();
}
int level = zzsjphzsData['level'] ?? 0;
// 根据level值确定显示内容和颜色
String levelText = '';
Color levelColor = themeController.currentColor.sc3;
switch (level) {
case 0:
levelText = '正常';
levelColor = Colors.green;
break;
case 1:
levelText = '轻度失衡';
levelColor = Colors.orange;
break;
case 2:
levelText = '中度失衡';
levelColor = Colors.red;
break;
case 3:
levelText = '重度失衡';
levelColor = Colors.red;
break;
default:
levelText = '未知';
levelColor = Colors.grey;
}
// 构建StatusBarWithIndicator需要的数据格式
List<Map<String, dynamic>> showLabel = [
{
'key': 0,
'name': '正常',
'color': Colors.green,
},
{
'key': 1,
'name': '轻度失衡',
'color': Colors.orange,
},
{
'key': 2,
'name': '中度失衡',
'color': Colors.red,
},
{
'key': 3,
'name': '重度失衡',
'color': Colors.red,
},
];
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"自主神经平衡指数".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
// "心理健康评估介绍".tr,
"自主神经平衡指数 是评估人体自主神经系统ANS功能状态的重要指标主要反映交感神经和副交感神经的活性平衡关系。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"自主神经平衡指数 是评估人体自主神经系统ANS功能状态的重要指标主要反映交感神经和副交感神经的活性平衡关系。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 83.rpx,
),
// 显示level值的文本
// Padding(
// padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 30.rpx),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(
// '当前状态:',
// style: TextStyle(
// fontSize: 30.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// Text(
// levelText,
// style: TextStyle(
// fontSize: 36.rpx,
// fontWeight: FontWeight.bold,
// color: levelColor,
// ),
// ),
// ],
// ),
// ),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: StatusBarWithIndicator(
selectKey: level, // 使用level值作为选中的key
showLabel: showLabel,
currentValueText: "当前属于".tr,
showCurrentValue: true,
),
),
SizedBox(
height: 56.rpx,
),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("自主神经平衡指数绘制异常${e}");
return Container();
}
}
}

View File

@@ -0,0 +1,380 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/DataShowWidget.dart';
import 'package:EasyDartModule/EasyDartModule.dart' as es;
class QcHeartChangeWidget extends StatefulWidget {
var reportData; // 改为更通用的名称
QcHeartChangeWidget({super.key, required this.reportData});
@override
State<QcHeartChangeWidget> createState() => _QcHeartChangeWidgetState();
}
class _QcHeartChangeWidgetState extends State<QcHeartChangeWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null) {
return Container();
}
// 从reportData中获取hrv数据
List hrvData = widget.reportData['hrv'] ?? [];
if (hrvData.isEmpty) {
return Container();
}
List<Map<String, dynamic>> data = transformHrvData(hrvData);
// 获取人员姓名用于显示
String personName = widget.reportData['person']?['name'] ?? '--';
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(30.rpx, 29.rpx, 30.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"心率变异性HRV".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
// "心率变异性HRV介绍".tr,
"心率变异性HRV是指心脏每次跳动间隔时间的差异程度反映自主神经系统交感神经和副交感神经对心脏的调节能力是评估心血管健康和压力状态的重要指标。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"心率变异性HRV是指心脏每次跳动间隔时间的差异程度反映自主神经系统交感神经和副交感神经对心脏的调节能力是评估心血管健康和压力状态的重要指标。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 31.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx),
child: Column(
children: [
DataShowWidget(
alignment: MainAxisAlignment.center,
widget1: Text(
"名称".tr,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: AppConstants().normal_text_fontSize,
),
),
widget2: Text(
"测量值".tr,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: AppConstants().normal_text_fontSize,
),
),
widget3: Text(
"参考范围".tr,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: AppConstants().normal_text_fontSize,
),
),
widget4: Text(
"趋势".tr,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize: AppConstants().normal_text_fontSize,
),
),
),
Column(
children: data.map<Widget>((data) {
return DataShowWidget(
alignment: MainAxisAlignment.center,
widget1: Row(
children: [
Flexible(
child: Text(
'${data['name']}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize:
AppConstants().normal_text_fontSize,
),
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white,
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 14.rpx, 14.rpx, 14.rpx),
borderRadius: 0.rpx,
onTap: () {
// Get.toNamed("/deviceShareListPage", arguments: explain);
if (AppConstants().ent_type ==
APPPackageType.MHT) {
showTipDialog(
context,
Container(
child: Text(
'${data['desc']}',
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
'${data['desc']}',
style: TextStyle(
fontSize: 26.rpx,
color: themeController
.currentColor.sc3,
),
),
),
backgroundColor:
themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: SizedBox(
width: 17.rpx,
height: 17.rpx,
child: SvgPicture.asset(
'assets/img/icon/question.svg',
fit: BoxFit.cover,
color: Colors.white,
),
),
),
],
),
widget2: Text(
'${data['value']}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color:
getColorByRange(data['value'], data['range']),
// color: themeController.currentColor.sc3,
fontSize: AppConstants().normal_text_fontSize,
),
),
widget3: Text(
'${data['range']}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().normal_text_fontSize,
),
),
widget4: data['change'] == 0
? Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0, 0),
child: Container(
width: 22.rpx,
height: 22.rpx,
decoration: BoxDecoration(),
child: SvgPicture.asset(
'assets/img/icon/score_up.svg',
// fit: BoxFit.cover,
),
),
)
: data['change'] == 1
? Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0, 0),
child: Container(
width: 22.rpx,
height: 22.rpx,
decoration: BoxDecoration(),
child: SvgPicture.asset(
'assets/img/icon/score_down.svg',
// fit: BoxFit.cover,
),
),
)
: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0, 0),
child: Container(
width: 22.rpx,
height: 22.rpx,
decoration: BoxDecoration(),
child: SvgPicture.asset(
'assets/img/icon/score_equal.svg',
// fit: BoxFit.fill,
),
),
),
).paddingOnly(bottom: 0.rpx); // 在每个组件下方添加间隔
}).toList(),
),
],
),
),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("心率变化绘制异常${e}");
return Container();
}
}
List<Map<String, dynamic>> transformHrvData(List<dynamic> originalData) {
return originalData.map((item) {
return {
"name": item["name"] ?? "未知指标".tr,
"value":
(item["val"] is num) ? item["val"].toDouble() : 0.0, // 注意这里从val取值
"range": item["range"] ?? "0-0",
"change": item["trend"] ?? 0, // 如果没有trend字段默认为0
"desc": item["tips"]?.toString().isNotEmpty == true
? item["tips"].toString()
: "心率变异性HRV是指心脏每次跳动间隔时间的差异程度反映自主神经系统对心脏的调节能力。".tr
};
}).toList();
}
getColorByRange(double value, String? range) {
try {
// 1. 空、"-"、null 都直接返回默认颜色
if (range == null || range.toString().isEmpty || range == "-") {
return themeController.currentColor.sc3;
}
// 2. 拆分范围,例如 "70-150"
final parts = range.split('-');
if (parts.length != 2) {
return themeController.currentColor.sc3; // 格式不对直接默认
}
final min = int.parse(parts[0].trim());
final max = int.parse(parts[1].trim());
// 3. 判断是否在范围内
if (value < min || value > max) {
// 不在范围 → 返回 danger 色
return themeController.currentColor.sc9;
}
// 在范围 → 正常色
return themeController.currentColor.sc3;
} catch (e) {
// 任意解析错误都回默认
return themeController.currentColor.sc3;
}
}
}

View File

@@ -0,0 +1,323 @@
import 'package:EasyDartModule/EasyDartModule.dart' as es;
import 'package:ef/ef.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/ScatterPlotChart.dart';
class QcHeartPointWidget extends StatefulWidget {
var reportData; // 改为更通用的名称
QcHeartPointWidget({super.key, required this.reportData});
@override
State<QcHeartPointWidget> createState() => _QcHeartPointWidgetState();
}
class _QcHeartPointWidgetState extends State<QcHeartPointWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
if (widget.reportData == null) {
return Container();
}
// 从reportData中获取hrs数据
List hrsData = widget.reportData['hrs'] ?? [];
if (hrsData.isEmpty) {
return Container();
}
double maxX = 0;
double maxY = 0;
List<ScatterSpot> data = [];
try {
data = hrsData.map<ScatterSpot>((item) {
double x = (item['x'] ?? 0).toDouble(); // 注意这里从x取值
double y = (item['y'] ?? 0).toDouble(); // 注意这里从y取值
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
return ScatterSpot(x, y);
}).toList();
} catch (e) {
print(e);
}
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding:
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"心率散点图".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 10.rpx, 14.rpx, 10.rpx), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
if (AppConstants().ent_type ==
APPPackageType.MHT.code) {
showTipDialog(
context,
Container(
child: Text(
// "心率散点图介绍".tr,
"心电散点图是用非线性的图形方法描记的连续心冲击图的RR间期图因图形由散点组成又称散点图。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: Colors.black,
),
),
),
backgroundColor: Color(0xFFFFFFFF),
colors: [
Color(0XFF1592AA),
Color(0xFF0C83A7),
Color(0xFF006FA3)
],
);
} else {
showTipDialog(
context,
Container(
child: Text(
"心电散点图是用非线性的图形方法描记的连续心冲击图的RR间期图因图形由散点组成又称散点图。"
.tr,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
backgroundColor: themeController.currentColor.sc17,
colors: AppConstants().thNormalButton,
);
}
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 31.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx),
child: Container(
width: MediaQuery.of(context).size.width * 0.7,
height: MediaQuery.of(context).size.width * 0.7,
constraints: BoxConstraints(
minWidth: 430.rpx,
minHeight: 430.rpx,
),
child: ScatterPlotChart(
points: data,
// 根据实际数据动态设置最大最小值
xMax: maxX > 0 ? maxX.toInt() + 100 : 3000,
yMax: maxY > 0 ? maxY.toInt() + 100 : 3000,
xMin: 0,
yMin: 0,
pointColor: stringToColor("#00C1AA"), // 点的颜色
// divisions: 7, // 刻度分割数量
),
),
),
SizedBox(
height: 31.rpx,
),
// Row(
// children: [
// Text(
// "图形参考".tr,
// style: TextStyle(
// color: themeController.currentColor.sc3,
// fontSize: AppConstants().middler_text_fontSize),
// ),
// ],
// ),
// SizedBox(
// height: 31.rpx,
// ),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// // 第 1 个
// SizedBox(
// width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4,
// child: Column(
// children: [
// Image.asset(
// "assets/img/heartPic1.png",
// width: 120.rpx,
// height: 120.rpx,
// ),
// SizedBox(height: 10),
// SizedBox(
// height: 60.rpx, // 👈 固定说明文字高度
// child: Text(
// '正常心率窦性图'.tr,
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// textAlign: TextAlign.center,
// style: TextStyle(
// fontSize: AppConstants().small_text_fontSize,
// color: themeController.currentColor.sc3,
// ),
// ),
// ),
// ],
// ),
// ),
// // 第 2 个
// SizedBox(
// width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4,
// child: Column(
// children: [
// Image.asset(
// "assets/img/heartPic2.png",
// width: 120.rpx,
// height: 120.rpx,
// ),
// SizedBox(height: 10),
// SizedBox(
// height: 60.rpx,
// child: Text(
// '窦性心律不齐图'.tr,
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// textAlign: TextAlign.center,
// style: TextStyle(
// fontSize: AppConstants().small_text_fontSize,
// color: themeController.currentColor.sc3,
// ),
// ),
// ),
// ],
// ),
// ),
// // 第 3 个
// SizedBox(
// width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4,
// child: Column(
// children: [
// Image.asset(
// "assets/img/heartPic3.png",
// width: 120.rpx,
// height: 120.rpx,
// ),
// SizedBox(height: 10),
// SizedBox(
// height: 60.rpx,
// child: Text(
// '持续性房颤图'.tr,
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// textAlign: TextAlign.center,
// style: TextStyle(
// fontSize: AppConstants().small_text_fontSize,
// color: themeController.currentColor.sc3,
// ),
// ),
// ),
// ],
// ),
// ),
// // 第 4 个
// SizedBox(
// width: (MediaQuery.sizeOf(context).width - 60.rpx * 3) / 4,
// child: Column(
// children: [
// Image.asset(
// "assets/img/heartPic4.png",
// width: 120.rpx,
// height: 120.rpx,
// ),
// SizedBox(height: 10),
// SizedBox(
// height: 60.rpx,
// child: Text(
// '阵法性房颤图'.tr,
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// textAlign: TextAlign.center,
// style: TextStyle(
// fontSize: AppConstants().small_text_fontSize,
// color: themeController.currentColor.sc3,
// ),
// ),
// ),
// ],
// ),
// ),
// ].divide(SizedBox(width: 20.rpx)),
// ),
],
),
),
);
} catch (e) {
es.EasyDartModule.logger.error("心率点绘制异常${e}");
return Container();
}
}
}

View File

@@ -0,0 +1,527 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/ServiceConstant.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/common/util/requestWithLog.dart';
import 'package:vbvs_app/component/NullDataComponentWidget.dart';
import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
import 'package:vbvs_app/controller/date/CalendarController.dart';
import 'package:vbvs_app/controller/sleep/sleep_report_controller.dart';
import 'package:vbvs_app/controller/sleep/sleep_time_convert.dart';
import 'package:vbvs_app/pages/sleep_report/QcReportWidget.dart';
import 'package:vbvs_app/pages/sleep_report/chart/StatusBarWithIndicator.dart';
class QuickHealthReportPage extends StatefulWidget {
var data;
QuickHealthReportPage({super.key, required this.data}) {}
@override
State<QuickHealthReportPage> createState() => _QuickHealthReportPageState();
}
class _QuickHealthReportPageState extends State<QuickHealthReportPage> {
late SleepReportController sleepReportController;
CalendarController calendarController = Get.find();
_QuickHealthReportPageState() {}
final GlobalKey sleepCardKey = GlobalKey();
final GlobalKey heartRateCardKey = GlobalKey();
final GlobalKey breatheCardKey = GlobalKey();
final ScrollController _scrollController = ScrollController();
final RxBool isRightLimit = false.obs;
@override
void didUpdateWidget(QuickHealthReportPage oldWidget) {
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
super.dispose();
Future.microtask(() {
if (Get.isRegistered<SleepReportController>()) {
sleepReportController.isLoading.value = false;
sleepReportController.model.type = 1;
}
});
}
@override
void initState() {
super.initState();
if (!Get.isRegistered<SleepReportController>(tag: widget.data["tag"])) {
Get.put(tag: widget.data["tag"], SleepReportController());
}
sleepReportController = Get.find(tag: widget.data["tag"]);
// sleepReportController.sleepReport.value = {};
if (widget.data['date'] == null) {
widget.data['date'] = DateTime.now().millisecondsSinceEpoch;
}
}
@override
Widget build(BuildContext context) {
// Future.microtask(() {
// _initSleepReportData();
// });
final healthLevelInfo = getHealthLevelInfo(widget.data);
double lineWidth = 150.rpx;
return LayoutBuilder(
builder: (context, bodySize) => GestureDetector(
// onTap: () => FocusScope.of(context).unfocus(),,
child: Container(
decoration: BoxDecoration(
image: (widget.data['noBackImg'] != null &&
widget.data['noBackImg'] == true)
? null // ✅ 不要背景图
: DecorationImage(
image: (widget.data['backgroundImg'] != null &&
widget.data['backgroundImg'].toString().isNotEmpty)
? AssetImage(widget.data['backgroundImg'])
: AssetImage(getBackgroundImageNoImage())
as ImageProvider,
fit: BoxFit.fill,
),
),
child: Scaffold(
backgroundColor: Colors.transparent, // 背景透明
appBar: (widget.data['arrow'] != null &&
widget.data['arrow'] == false)
? null
: AppBar(
backgroundColor: widget.data['backgroundColor'] != null
? widget.data['backgroundColor'].withOpacity(0.8)
: themeController.currentColor.sc5,
automaticallyImplyLeading: false,
iconTheme:
IconThemeData(color: themeController.currentColor.sc3),
titleSpacing: 0,
title: Container(
width: double.infinity,
height: 180.rpx,
child: Stack(
alignment: Alignment.center,
children: [
/// 居中标题
Text(
'快检报告'.tr,
style: TextStyle(
fontFamily: 'Readex Pro',
color: themeController.currentColor.sc3,
letterSpacing: 0,
fontSize: 30.rpx,
),
),
/// 左边返回按钮
if (widget.data['arrow'] == null ||
widget.data['arrow'] == true)
Positioned(
left: 0,
child: returnIconButtomNew(),
),
],
),
),
),
body: SafeArea(
top: true,
child: Obx(() {
try {
var sleepReport = sleepReportController.sleepReport.value;
if (sleepReport == null || sleepReport.isEmpty) {
return SingleChildScrollView(
child: Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(
30.rpx, 30.rpx, 30.rpx, 26.rpx),
child: Container(
width: double.infinity,
constraints: BoxConstraints(
minHeight: 90.rpx, // 最小高度
),
decoration: BoxDecoration(
// color: themeController.currentColor.sc5, // 背景色
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0.rpx, 0.rpx, 0.rpx, 0.rpx),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 左侧图标
Container(
margin: EdgeInsets.only(right: 7.rpx),
child: Icon(
Icons.info_outline,
size: 26.rpx,
color: themeController.currentColor.sc8,
),
),
// 中间可换行文字
Expanded(
child: Container(
margin: EdgeInsets.only(right: 69.rpx),
child: Text(
'5分钟快速检测可能受到用户当前状态、过程中说话动作、精神紧张等情况影响数据仅供参考。'
.tr,
style: TextStyle(
fontFamily: 'Inter',
fontSize: AppConstants()
.smaller_text_fontSize,
color: themeController
.currentColor.sc8,
height: 1.4,
),
softWrap: true,
),
),
),
],
),
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(30.rpx, 0, 30.rpx, 0),
child: Container(
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(16.rpx),
),
child: Column(
children: [
// 第一个容器 - 包含Row布局
Container(
width: double.infinity,
padding: EdgeInsets.all(30.rpx),
decoration: BoxDecoration(
// color: themeController.currentColor.sc5,
borderRadius:
BorderRadius.circular(16.rpx),
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
// 左边的Column包含五行文字
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'${widget.data['person']['name']} (${widget.data['person']['gender']} ${widget.data['person']['age'].toInt()}岁)',
style: TextStyle(
fontSize: AppConstants()
.bigger_text_fontSize,
color: themeController
.currentColor.sc3,
// fontWeight: FontWeight.bold,
),
),
SizedBox(height: 18.rpx),
Text(
"快检得分:".tr +
'${widget.data['score']['score'].toInt()}',
style: TextStyle(
fontSize: AppConstants()
.small_an_text_fontSize,
color: themeController
.currentColor.sc3,
),
),
SizedBox(height: 18.rpx),
Row(
children: [
Text(
"健康等级:".tr,
style: TextStyle(
fontSize: AppConstants()
.small_an_text_fontSize,
color: themeController
.currentColor.sc3,
),
),
Text(
healthLevelInfo['text'],
style: TextStyle(
fontSize: AppConstants()
.small_an_text_fontSize,
color: healthLevelInfo[
'color'],
),
),
],
),
SizedBox(height: 18.rpx),
Text(
"设备ID".tr +
'${widget.data['mac']}',
style: TextStyle(
fontSize: AppConstants()
.small_an_text_fontSize,
color: themeController
.currentColor.sc3,
),
),
SizedBox(height: 18.rpx),
Text(
"快检时间:".tr +
'${MyUtils.timestampToDateString(widget.data['create_time'].toInt())}',
style: TextStyle(
fontSize: AppConstants()
.small_an_text_fontSize,
color: themeController
.currentColor.sc3,
),
),
],
),
),
// SizedBox(width: 20.rpx), // 左边文字和右边图片的间距
// 右边的本地图片
Container(
width: 120.rpx,
height: 190.rpx,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(12.rpx),
image: DecorationImage(
image: AssetImage(
'assets/img/black_body_still.png'), // 替换为您的本地图片路径
fit: BoxFit.cover,
),
),
),
],
),
),
SizedBox(height: 34.rpx), // 两个容器之间的间距
// 第二个容器 - 包含StatusBarWithIndicator todo接入报告数据
Container(
width: double.infinity,
// height: 220.rpx,
decoration: BoxDecoration(
// color: themeController.currentColor.sc5,
borderRadius:
BorderRadius.circular(16.rpx),
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 20.rpx, 30.rpx, 20.rpx),
child: StatusBarWithIndicator(
selectKey: widget.data['score']['level']
.toInt(), // 这里传入对应的 level 值
showLabel: (widget.data['type']['score']
as List)
.map<Map<String, dynamic>>((item) {
return {
'key': item['level']?.toInt() ??
0, // 使用 level 作为 key
'name': item['name'] ?? '',
'color': stringToColor(
item['color'] ??
''), // 将颜色字符串转换为 Color
'range':
item['range'] ?? '', // 添加范围数据
};
}).toList(),
showCurrentValue: true, // 显示当前值
currentValueText:
'当前属于'.tr, // 或者根据实际值动态生成
showRange: true, // 显示范围
),
),
),
SizedBox(height: 25.rpx), // 两个容器之间的间距
],
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(30.rpx, 0, 30.rpx, 0),
child: Container(
width: double.infinity,
// padding: EdgeInsets.all(30.rpx),
decoration: BoxDecoration(
// color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(16.rpx),
),
child: QcReportWidget(widget.data),
),
),
Padding(
padding: EdgeInsets.fromLTRB(46.rpx, 0, 46.rpx, 0),
child: Column(
children: [
// Text(
// "MAC号".tr +
// ": ${widget.data['mac'] ?? '未知数据'.tr}",
// style: TextStyle(
// color: themeController.currentColor.sc4,
// fontSize:
// AppConstants().smaller_text_fontSize,
// ),
// ),
SizedBox(
height: 4.rpx,
),
Text(
"本页报告是基于心率、呼吸等体征波形数据通过AI算法模型分析完成其结果仅供参考其中报告未见数据异常部分并不代表没有潜在性的疾病风险如有不适请及时就医。"
.tr,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize:
AppConstants().smaller_text_fontSize,
),
),
],
),
),
SizedBox(
height: 40.rpx,
),
]
.divide(SizedBox(
height: 25.rpx,
))
.addToEnd(SizedBox(height: 25.rpx)),
),
);
} else {
return Container();
}
} catch (e, s) {
ef.log('ERROR:$e$s');
}
return NullDataWidget();
}),
),
),
),
),
);
}
Future<void> loadSleepReport(int type) async {
if (sleepReportController.isLoading.value) return;
sleepReportController.model.type = type;
sleepReportController.isLoading.value = true;
String data = MyUtils.formatDate(calendarController.selectedDate.value!);
String serviceAddress = ServiceConstant.service_address;
String serviceName = ServiceConstant.server_service;
String serviceApi = ServiceConstant.sleep_report;
String queryUrl =
"$serviceAddress$serviceName$serviceApi?mac=${widget.data['mac']}&time=$data&type=$type&sleepType=2";
await requestWithLog(
logTitle: "查询睡眠报告",
method: MyHttpMethod.get,
// queryUrl:
// "https://sleepdata.he-info.com/api/analysis/sleep/analysis?mac=${widget.data['mac']}&time=$data&type=$type",
queryUrl: queryUrl,
onSuccess: (res) {
final sleepData = Map<String, dynamic>.from(res.data);
// 处理时区转换
applyTimezoneConversion(sleepData);
sleepReportController.sleepReport.value = sleepData;
},
onFailure: (res) {
TopSlideNotification.show(
context,
text: res.msg!,
textColor: themeController.currentColor.sc9,
);
sleepReportController.sleepReport.value = {};
},
);
sleepReportController.isLoading.value = false;
sleepReportController.updateAll();
}
// 获取健康等级信息的方法
Map<String, dynamic> getHealthLevelInfo(Map data) {
// 默认值
String defaultText = '未知';
Color defaultColor = themeController.currentColor.sc3;
try {
// 获取score的level
double? level;
if (data['score'] is Map) {
level = data['score']['level']?.toDouble();
} else if (data['score'] is num) {
level = data['score'].toDouble();
}
if (level == null) {
return {'text': defaultText, 'color': defaultColor};
}
// 获取type中的score等级配置
var scoreTypes = data['type']?['score'];
if (scoreTypes == null || scoreTypes is! List) {
return {'text': defaultText, 'color': defaultColor};
}
// 查找匹配的等级
for (var type in scoreTypes) {
if (type['level']?.toDouble() == level) {
String name = type['name'] ?? defaultText;
String colorStr = type['color'] ?? '';
// 转换颜色字符串为Color对象
Color textColor = stringToColor(colorStr);
return {'text': '$name', 'color': textColor};
}
}
} catch (e) {
print('获取健康等级信息出错:$e');
}
return {'text': defaultText, 'color': defaultColor};
}
// 辅助方法将颜色字符串转换为Color对象
Color _getColorFromString(String colorStr, Color defaultColor) {
if (colorStr.isEmpty) return defaultColor;
try {
// 处理不同格式的颜色字符串
String hexColor = colorStr.toUpperCase().replaceAll('#', '');
// 如果是6位颜色代码
if (hexColor.length == 6) {
return Color(int.parse('FF$hexColor', radix: 16));
}
// 如果是8位颜色代码包含透明度
else if (hexColor.length == 8) {
return Color(int.parse(hexColor, radix: 16));
}
// 如果是3位颜色代码简写
else if (hexColor.length == 3) {
String r = hexColor[0] * 2;
String g = hexColor[1] * 2;
String b = hexColor[2] * 2;
return Color(int.parse('FF$r$g$b', radix: 16));
}
} catch (e) {
print('颜色转换出错:$e');
}
return defaultColor;
}
}