Files
tuiche/lib/pages/sleep_report/component/MonthDataWidget.dart
2025-07-07 14:58:54 +08:00

758 lines
28 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:ef/base/chart/drawer.dart';
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/pages/mh_page/component/easychart.dart';
import 'package:vbvs_app/pages/sleep_report/component/SleepCard.dart';
import 'package:vbvs_app/pages/sleep_report/component/SleepChartWidget.dart';
import 'package:vbvs_app/pages/sleep_report/component/TrendDataTablePage.dart';
import 'package:vbvs_app/pages/sleep_report/component/TrendDataTextPage.dart';
import 'package:vbvs_app/pages/sleep_report/component/WeekSleepScoreWidget.dart';
Widget MonthDataWidget(
Map<dynamic, dynamic> sleepReport,
dynamic data,
) {
List<Widget> _buildSectionList() {
EdgeInsetsDirectional padding =
EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 25.rpx);
return [
AvgSleepScoreWidget(
sleepReport: sleepReport,
mediumLabel: "本月平均分",
), //睡眠评分
SleepChartContainer(
title: "每日得分",
tipText: "每日得分介绍",
sleepReport: sleepReport,
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: sleepReport['scoreList']['yRange']['min'] ?? 0.0,
max: sleepReport['scoreList']['yRange']['max'] ?? 100.0,
q: 100,
labels: List<String>.from(
sleepReport['scoreList']['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['scoreList']['data'], '', 1),
xCount: buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['scoreList']['yLable'].length,
points: buildMonthlyChartData(sleepReport['scoreList'])['points'],
displayMode: ChartDisplayMode.bar,
barColors: buildMonthlyChartData(sleepReport['scoreList'])['colors'],
xUnit: sleepReport['scoreList']['yUnit'],
barWidth: 0.5,
),
showLabel: sleepReport['scoreList']['type'],
),
SleepCard(
sleepReport: sleepReport,
highlightItem: data['itemName'],
),
IndicatorCompareCard(
title: "与上月对比",
headers: ["名称", "上月", "本月", "参考范围"],
tooltip: "与上月对比提示",
rows: (sleepReport['cwl'] ?? []).map<List<Widget>>((item) {
return [
Text(
item['name']?.toString() ?? '-',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 26.rpx),
),
Text(
item['lastCurr']?.toString() ?? '-',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 26.rpx),
),
Text(
item['curr']?.toString() ?? '-',
style: TextStyle(
color: item["currColor"] ?? Color(0xFFFFFFFF),
fontSize: 26.rpx,
),
),
Text(
item['range']?.toString() ?? '-',
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 26.rpx),
),
];
}).toList(),
),
SleepChartContainer(
title: "本月睡眠时长",
tipText: "每日得分介绍",
sleepReport: sleepReport,
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: sleepReport['scoreList']['yRange']['min'] ?? 0.0,
max: sleepReport['scoreList']['yRange']['max'] ?? 10.0,
q: 10,
labels: List<String>.from(
sleepReport['csd']['yLable'].map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['scoreList']['data'], '小时', 1),
xCount: buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['csd']['yLable'].length,
points: [],
dualBarPoints: buildTripleBarData(sleepReport['csd']),
displayMode: ChartDisplayMode.dualBar,
xUnit: sleepReport['csd']['yUnit'],
barWidth: 0.5,
),
showLabel: sleepReport['csd']['type'],
),
TrendDataTablePage(
title: sleepReport['dysp'][0]['name'],
tipText: "入睡时间趋势提示",
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 5,
q: 5,
labels: List<String>.from(
sleepReport['dysp'][0]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-30.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][0]['value'], '入睡时间:', 0),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][0]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][0]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][0]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][1]['name'],
tipText: "起床时间趋势提示",
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 4,
q: 4,
labels: List<String>.from(
sleepReport['dysp'][1]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-30.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][1]['value'], '起床时间:', 0),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][1]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][1]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][1]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][2]['name'],
tipText: "心率基准趋势提示",
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 4,
q: 4,
labels: List<String>.from(
sleepReport['dysp'][2]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][2]['value'], '', 1),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][2]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][2]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][2]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][3]['name'],
tipText: "心率变异性趋势提示",
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 5,
q: 5,
labels: List<String>.from(
sleepReport['dysp'][3]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][3]['value'], '毫秒', 1),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][3]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][3]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][3]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
TrendDataTablePage(
title: sleepReport['dysp'][4]['name'],
tipText: "呼吸基准趋势提示",
chartContent: LineView(
xLabels: [
ChartLables(
//标签系列1
min: 0, //最小值0
max: buildMonthlyChartData(
sleepReport['scoreList'])['daysInMonth'], //最大值30
q: buildMonthlyChartData(sleepReport['scoreList'])[
'daysInMonth'], //labels第一个与最后一个的真实距离也就是30-1 = 29
labels: List<String>.from(
sleepReport['scoreList']['xLable'].map((e) => e['name']),
),
indexs: (sleepReport['scoreList']['xLable'] as List)
.map((e) => e['index'] as int)
.toList(),
offset: Offset(0, -16.rpx), //标签相对于原点的偏移下方20像素位置
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style:
TextStyle(color: Color(0XffD3D3D3), fontSize: 18.rpx),
);
},
),
],
yLabels: [
ChartLables(
min: 0,
max: 5,
q: 5,
labels: List<String>.from(
sleepReport['dysp'][4]['yLable']
.map((e) => e['name'].toString()),
),
offset: Offset(-15.rpx, 0),
ondrawer: (canvas, offset, index, label, align, style) {
ChartLables.drawText(
canvas,
label,
offset,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFFFFFF).withOpacity(0.6),
fontSize: 18.rpx),
);
},
),
],
tips: buildValueTexts(sleepReport['dysp'][4]['value'], '次/分', 1),
xCount:
buildMonthlyChartData(sleepReport['scoreList'])['daysInMonth']
.toInt(),
yCount: sleepReport['dysp'][4]['yLable'].length,
points: buildGeneralPoints(sleepReport['dysp'][4]),
displayMode: ChartDisplayMode.line,
xUnit: sleepReport['dysp'][4]['yUnit'],
barWidth: 0.5,
bottomPadding: 16,
),
padding: 45.rpx),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("MAC号${data['mac']}",
style: TextStyle(
color: Color(0xFFD3D3D3).withOpacity(0.2), fontSize: 18.rpx))
],
),
]
.map((widget) => Padding(
padding: padding,
child: SizedBox(width: double.infinity, child: widget),
))
.toList();
}
return Column(
children: _buildSectionList(),
);
}
List<String> buildValueTexts(
List<dynamic> data,
String unit,
int direction, // 0 表示左侧1 表示右侧
) {
if (data.isEmpty) return [];
return data
.where((item) => item.containsKey('value') && item.containsKey('st'))
.map((item) {
final val = item['value'].toString();
// 单位位置
final prefix = direction == 1 ? '' : unit;
final suffix = direction == 1 ? unit : '';
// 中文年月日格式
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
final dateStr = "${date.year}${date.month}${date.day}";
return "$prefix$val$suffix\n$dateStr";
}).toList();
}
Map<String, dynamic> buildMonthlyChartData(Map<String, dynamic> dyspData) {
final List<Map<String, dynamic>> data =
(dyspData['data'] as List?)?.whereType<Map<String, dynamic>>().toList() ??
[];
final List<Map<String, dynamic>> yLabels = (dyspData['yLable'] as List?)
?.whereType<Map<String, dynamic>>()
.toList() ??
[];
final List<Map<String, dynamic>> types =
(dyspData['type'] as List?)?.whereType<Map<String, dynamic>>().toList() ??
[];
if (data.isEmpty || yLabels.length < 2) {
return {
'points': <Offset>[],
'colors': <String>[],
'daysInMonth': 0.0,
};
}
// 解析 yLabel 为数值
List<double> yValues =
yLabels.map((e) => double.tryParse(e['name'].toString()) ?? 0).toList();
double minY = yValues.first;
double maxY = yValues.last;
double yRange = maxY - minY;
double yStepCount = (yValues.length - 1).toDouble(); // 👈 用 double
// 获取当前月的总天数(以第一条数据为准)
DateTime baseDate = DateTime.fromMillisecondsSinceEpoch(data.first['st']);
double daysInMonth = DateTime(baseDate.year, baseDate.month + 1, 0)
.day
.toDouble(); // 👈 改为 double
List<Offset> points = [];
List<String> colors = [];
for (var item in data) {
final int st = item['st'];
final int level = item['level'];
final double value = (item['value'] as num).toDouble();
DateTime date = DateTime.fromMillisecondsSinceEpoch(st);
double day = date.day.toDouble();
// x: day - 10 起始)
double x = day - 1;
// // y: 归一化
// double y = ((value - minY) / yRange) * yStepCount;
// 获取 color
String color = types.firstWhere(
(e) => e['level'] == level,
orElse: () => {'color': '#CCCCCC'},
)['color'];
points.add(Offset(x, value));
colors.add(color);
}
return {
'points': points,
'colors': colors,
'daysInMonth': daysInMonth,
};
}
List<Offset> buildGeneralPoints(Map<String, dynamic> dyspData) {
final values =
(dyspData['value'] as List?)?.whereType<Map<String, dynamic>>().toList();
final yLabels =
(dyspData['yLable'] as List?)?.whereType<Map<String, dynamic>>().toList();
if (values == null || values.isEmpty || yLabels == null || yLabels.length < 2)
return [];
bool isTimeAxis = yLabels.first['name'].toString().contains(":");
double labelToValue(String name) {
if (isTimeAxis) {
final parts = name.split(':');
int hour = int.parse(parts[0]);
int minute = int.parse(parts[1]);
return hour * 60 + minute * 1.0;
} else {
return double.tryParse(name) ?? 0;
}
}
List<double> labelValues =
yLabels.map((e) => labelToValue(e['name'])).toList();
// 处理时间轴跨午夜
bool crossMidnight = false;
if (isTimeAxis && labelValues.last < labelValues.first) {
crossMidnight = true;
double base = labelValues.first;
for (int i = 1; i < labelValues.length; i++) {
if (labelValues[i] < base) {
labelValues[i] += 1440; // +24小时单位分钟
}
}
}
List<Offset> points = [];
for (var item in values) {
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
double x = (date.day - 1).toDouble(); // 👈 按照“日”定位 X 坐标
dynamic rawValue = item['value'];
double valueMinBased;
if (isTimeAxis && rawValue is String) {
final parts = rawValue.split(':');
int hour = int.parse(parts[0]);
int minute = int.parse(parts[1]);
valueMinBased = hour * 60 + minute * 1.0;
if (crossMidnight && valueMinBased < labelValues.first) {
valueMinBased += 1440;
}
} else if (!isTimeAxis && rawValue is num) {
valueMinBased = rawValue.toDouble();
} else {
continue;
}
if (valueMinBased < labelValues.first) valueMinBased = labelValues.first;
if (valueMinBased > labelValues.last) valueMinBased = labelValues.last;
double y = getYPositionBySegmentedLabels(labelValues, valueMinBased);
points.add(Offset(x, y));
}
return points;
}
double getYPositionBySegmentedLabels(List<double> labels, double value) {
if (value <= labels.first) return 0.0;
if (value >= labels.last) return labels.length - 1.0;
for (int i = 0; i < labels.length - 1; i++) {
double low = labels[i];
double high = labels[i + 1];
if (value >= low && value <= high) {
double ratio = (value - low) / (high - low);
return i + ratio;
}
}
// 不应该走到这里默认返回0
return 0.0;
}
//多色柱状图坐标
List<List<Offset>> buildTripleBarData(Map<String, dynamic> csd) {
final List<Map<String, dynamic>> data =
(csd['data'] as List?)?.whereType<Map<String, dynamic>>().toList() ?? [];
final List<Map<String, dynamic>> yLabels =
(csd['yLable'] as List?)?.whereType<Map<String, dynamic>>().toList() ??
[];
if (data.isEmpty || yLabels.isEmpty) return [];
double minY = double.tryParse(yLabels.first['name'].toString()) ?? 0;
double maxY = double.tryParse(yLabels.last['name'].toString()) ?? 10;
List<List<Offset>> points = [];
for (var item in data) {
double dst = (item['dst'] as num?)?.toDouble() ?? 0;
double lst = (item['lst'] as num?)?.toDouble() ?? 0;
double slt = (item['slt'] as num?)?.toDouble() ?? 0;
// 限制 y 值范围
dst = dst.clamp(minY, maxY);
lst = lst.clamp(minY, maxY);
slt = slt.clamp(minY, maxY);
// 计算 X本月第几天从 0 开始)
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
int dayOfMonth = date.day; // 1 ~ 31
double x = (dayOfMonth - 1).toDouble(); // 转为从 0 开始
points.add([
Offset(x, dst), // 深睡终点
Offset(x, dst + lst), // 浅睡+深睡终点
Offset(x, slt), // 总睡眠终点(第三段)
]);
}
return points;
}