日报周报月报
This commit is contained in:
73
lib/pages/sleep_report/component/DailyDataWidget.dart
Normal file
73
lib/pages/sleep_report/component/DailyDataWidget.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/AIAdviceWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/BreatheCard.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/BreathePauseNewWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/BreatheStandardWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/CompareSleepWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/DiseasePercentsWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartChangeWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartHealthWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartPointWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartRateCard.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartRateStandardWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SkinPercentWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SleepCard.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SleepScoreWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SleepView.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SnoreViewWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/ZiZhuShenJingPercentWidget.dart';
|
||||
|
||||
Widget DailyDataWidget(
|
||||
Map<dynamic, dynamic> sleepReport,
|
||||
GlobalKey sleepCardKey,
|
||||
GlobalKey heartRateCardKey,
|
||||
GlobalKey breatheCardKey,
|
||||
dynamic data,
|
||||
) {
|
||||
List<Widget> _buildSectionList() {
|
||||
EdgeInsetsDirectional padding =
|
||||
EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 25.rpx);
|
||||
|
||||
return [
|
||||
SleepScoreWidget(sleepReport: sleepReport),
|
||||
SleepViewWidget(sleepReport: sleepReport),
|
||||
SleepCard(
|
||||
key: sleepCardKey,
|
||||
sleepReport: sleepReport,
|
||||
highlightItem: data['itemName'],
|
||||
),
|
||||
CompareSleepWidget(sleepReport: sleepReport),
|
||||
HeartPointWidget(sleepReport: sleepReport),
|
||||
AIAdviceWidget(sleepReport: sleepReport),
|
||||
HeartRateStandardWidget(sleepReport: sleepReport),
|
||||
HeartRateCard(
|
||||
key: heartRateCardKey,
|
||||
sleepReport: sleepReport,
|
||||
highlightItem: data['itemName'],
|
||||
),
|
||||
HeartChangeWidget(sleepReport: sleepReport),//心率变异性
|
||||
BreatheStandardWidget(sleepReport: sleepReport),
|
||||
BreatheCard(
|
||||
key: breatheCardKey,
|
||||
sleepReport: sleepReport,
|
||||
highlightItem: data['itemName'],
|
||||
),
|
||||
SnoreViewWidgetWidget(sleepReport: sleepReport),
|
||||
BreathePauseNewWidget(sleepReport: sleepReport),
|
||||
HeartHealthWidget(sleepReport: sleepReport),
|
||||
DiseasePercentsWidget(sleepReport: sleepReport),
|
||||
ZiZhuShenJingPercentWidget(sleepReport: sleepReport),
|
||||
SkinPercentWidget(sleepReport: sleepReport),
|
||||
]
|
||||
.map((widget) => Padding(
|
||||
padding: padding,
|
||||
child: SizedBox(width: double.infinity, child: widget),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: _buildSectionList(),
|
||||
);
|
||||
}
|
||||
757
lib/pages/sleep_report/component/MonthDataWidget.dart
Normal file
757
lib/pages/sleep_report/component/MonthDataWidget.dart
Normal file
@@ -0,0 +1,757 @@
|
||||
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 - 1(0 起始)
|
||||
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;
|
||||
}
|
||||
114
lib/pages/sleep_report/component/SleepChartWidget.dart
Normal file
114
lib/pages/sleep_report/component/SleepChartWidget.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
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/GradientLine.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/SnoreWaveform.dart';
|
||||
import 'package:EasyDartModule/EasyDartModule.dart' as es;
|
||||
|
||||
class SleepChartContainer extends StatelessWidget {
|
||||
final Map sleepReport;
|
||||
final String title;
|
||||
final String tipText;
|
||||
final Widget? chartContent;
|
||||
final List<dynamic> showLabel;
|
||||
const SleepChartContainer(
|
||||
{Key? key,
|
||||
required this.sleepReport,
|
||||
required this.title,
|
||||
required this.tipText,
|
||||
required this.chartContent,
|
||||
required this.showLabel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc5,
|
||||
borderRadius: BorderRadius.circular(25.rpx),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 30.rpx,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
tipText,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 30.rpx,
|
||||
height: 30.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
color: Color(0xFFD3D3D3),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
child: chartContent ?? Container(),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 32.rpx,
|
||||
runSpacing: 20.rpx,
|
||||
children: showLabel.map<Widget>((item) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 20.rpx,
|
||||
height: 20.rpx,
|
||||
decoration: BoxDecoration(
|
||||
color: stringToColor("${item["color"]}"),
|
||||
borderRadius: BorderRadius.circular(10.rpx),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 17.rpx),
|
||||
Text(
|
||||
item["name"],
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24.rpx,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -33,166 +33,263 @@ class _SleepScoreWidgetState extends State<SleepScoreWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
try {
|
||||
if (widget.sleepReport == null ||
|
||||
widget.sleepReport is! Map ||
|
||||
widget.sleepReport.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
List showLabel = widget.sleepReport['score']['type'];
|
||||
List stages = widget.sleepReport['score']['stages'];
|
||||
List<SegmentData> segments = parseSegments(widget.sleepReport);
|
||||
widget.sleepReport is! Map ||
|
||||
widget.sleepReport.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
// decoration: BoxDecoration(color: Colors.green),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150.rpx),
|
||||
child: Text(
|
||||
'30天平均分'.tr,
|
||||
style: TextStyle(
|
||||
color: Color(0xFFD3D3D3),
|
||||
fontSize: 26.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
maxLines: 1,
|
||||
List showLabel = widget.sleepReport['score']['type'];
|
||||
List stages = widget.sleepReport['score']['stages'];
|
||||
List<SegmentData> segments = parseSegments(widget.sleepReport);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
// decoration: BoxDecoration(color: Colors.green),
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150.rpx),
|
||||
child: Text(
|
||||
'30天平均分'.tr,
|
||||
style: TextStyle(
|
||||
color: Color(0xFFD3D3D3),
|
||||
fontSize: 26.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${widget.sleepReport['score']?['avg']}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 48.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(height: 56.rpx)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${widget.sleepReport['score']?['avg']}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 48.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(height: 56.rpx)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(),
|
||||
child: SegmentedCircleWithCenterWidget(
|
||||
// segments: [
|
||||
// SegmentData(color: Colors.red, value: 30),
|
||||
// SegmentData(color: Colors.green, value: 20),
|
||||
// SegmentData(color: Colors.blue, value: 40),
|
||||
// SegmentData(color: Colors.orange, value: 10),
|
||||
// ],
|
||||
segments: segments,
|
||||
strokeWidth: 8,
|
||||
gapAngle: 8,
|
||||
centerWidget: Container(
|
||||
child: Column(
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"睡眠评分".tr,
|
||||
'睡眠等级'.tr,
|
||||
style: TextStyle(
|
||||
color: stringToColor("#FFFFFF"),
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
color: Color(0xFFD3D3D3),
|
||||
fontSize: 26.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${widget.sleepReport['score']?['score']}',
|
||||
'${getSleepLevel(widget.sleepReport)}',
|
||||
style: TextStyle(
|
||||
color: stringToColor("#FF9F66"),
|
||||
fontSize: 100.rpx),
|
||||
color: stringToColor("#FF9F66"),
|
||||
fontSize: 48.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
].divide(SizedBox(height: 56.rpx)),
|
||||
),
|
||||
),
|
||||
trend: widget.sleepReport['score']?['trend'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'睡眠等级'.tr,
|
||||
style: TextStyle(
|
||||
color: Color(0xFFD3D3D3),
|
||||
fontSize: 26.rpx,
|
||||
letterSpacing: 0.0,
|
||||
SizedBox(
|
||||
width: 256.rpx,
|
||||
height: 256.rpx,
|
||||
child: SegmentedCircleWithCenterWidget(
|
||||
// segments: [
|
||||
// SegmentData(color: Colors.red, value: 30),
|
||||
// SegmentData(color: Colors.green, value: 20),
|
||||
// SegmentData(color: Colors.blue, value: 40),
|
||||
// SegmentData(color: Colors.orange, value: 10),
|
||||
// ],
|
||||
segments: segments,
|
||||
strokeWidth: 6.rpx,
|
||||
gapAngle: 8,
|
||||
centerWidget: Container(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"睡眠评分".tr,
|
||||
style: TextStyle(
|
||||
color: stringToColor("#FFFFFF"),
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
),
|
||||
Text(
|
||||
'${widget.sleepReport['score']?['score']}',
|
||||
style: TextStyle(
|
||||
color: stringToColor("#FF9F66"),
|
||||
fontSize: 100.rpx),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trend: widget.sleepReport['score']?['trend'],
|
||||
),
|
||||
Text(
|
||||
'${getSleepLevel(widget.sleepReport)}',
|
||||
style: TextStyle(
|
||||
color: stringToColor("#FF9F66"),
|
||||
fontSize: 48.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(height: 56.rpx)),
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 33.rpx,
|
||||
)),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 32.rpx,
|
||||
runSpacing: 20.rpx,
|
||||
children: showLabel.map<Widget>((item) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(5.rpx),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 20.rpx,
|
||||
height: 20.rpx,
|
||||
decoration: BoxDecoration(
|
||||
color: stringToColor("${item["color"]}"),
|
||||
borderRadius: BorderRadius.circular(10.rpx),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 17.rpx),
|
||||
Text(
|
||||
item["name"] + '(' + item['range'] + ")",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24.rpx,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
// Row(
|
||||
// mainAxisSize: MainAxisSize.max,
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: BoxDecoration(),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.max,
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Container(
|
||||
// constraints: BoxConstraints(maxWidth: 150.rpx),
|
||||
// child: Text(
|
||||
// '30天平均分'.tr,
|
||||
// style: TextStyle(
|
||||
// color: Color(0xFFD3D3D3),
|
||||
// fontSize: 26.rpx,
|
||||
// letterSpacing: 0.0,
|
||||
// ),
|
||||
// maxLines: 1,
|
||||
// ),
|
||||
// ),
|
||||
// Text(
|
||||
// '${widget.sleepReport['score']?['avg']}',
|
||||
// style: TextStyle(
|
||||
// color: Colors.white,
|
||||
// fontSize: 48.rpx,
|
||||
// letterSpacing: 0.0,
|
||||
// ),
|
||||
// ),
|
||||
// ].divide(SizedBox(height: 56.rpx)),
|
||||
// ),
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(),
|
||||
// child: SegmentedCircleWithCenterWidget(
|
||||
// // segments: [
|
||||
// // SegmentData(color: Colors.red, value: 30),
|
||||
// // SegmentData(color: Colors.green, value: 20),
|
||||
// // SegmentData(color: Colors.blue, value: 40),
|
||||
// // SegmentData(color: Colors.orange, value: 10),
|
||||
// // ],
|
||||
// segments: segments,
|
||||
// strokeWidth: 8,
|
||||
// gapAngle: 8,
|
||||
// centerWidget: Container(
|
||||
// child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Text(
|
||||
// "睡眠评分".tr,
|
||||
// style: TextStyle(
|
||||
// color: stringToColor("#FFFFFF"),
|
||||
// fontSize:
|
||||
// AppConstants().normal_text_fontSize),
|
||||
// ),
|
||||
// Text(
|
||||
// '${widget.sleepReport['score']?['score']}',
|
||||
// style: TextStyle(
|
||||
// color: stringToColor("#FF9F66"),
|
||||
// fontSize: 100.rpx),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// trend: widget.sleepReport['score']?['trend'],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Container(
|
||||
// decoration: BoxDecoration(),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.max,
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Text(
|
||||
// '睡眠等级'.tr,
|
||||
// style: TextStyle(
|
||||
// color: Color(0xFFD3D3D3),
|
||||
// fontSize: 26.rpx,
|
||||
// letterSpacing: 0.0,
|
||||
// ),
|
||||
// ),
|
||||
// Text(
|
||||
// '${getSleepLevel(widget.sleepReport)}',
|
||||
// style: TextStyle(
|
||||
// color: stringToColor("#FF9F66"),
|
||||
// fontSize: 48.rpx,
|
||||
// letterSpacing: 0.0,
|
||||
// ),
|
||||
// ),
|
||||
// ].divide(SizedBox(height: 56.rpx)),
|
||||
// ),
|
||||
// ),
|
||||
// ].divide(SizedBox(
|
||||
// width: 33.rpx,
|
||||
// )),
|
||||
// ),
|
||||
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 50.rpx),
|
||||
Wrap(
|
||||
spacing: 32.rpx,
|
||||
runSpacing: 20.rpx,
|
||||
children: showLabel.map<Widget>((item) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(5.rpx),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 20.rpx,
|
||||
height: 20.rpx,
|
||||
decoration: BoxDecoration(
|
||||
color: stringToColor("${item["color"]}"),
|
||||
borderRadius: BorderRadius.circular(10.rpx),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 17.rpx),
|
||||
Text(
|
||||
item["name"] + '(' + item['range'] + ")",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24.rpx,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
);
|
||||
} catch (e) {
|
||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
||||
return Container();
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
//获取睡眠等级
|
||||
|
||||
@@ -100,7 +100,7 @@ class _SleepViewWidgetState extends State<SleepViewWidget> {
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
"睡眠规律性介绍。",
|
||||
"睡眠规律性介绍",
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
|
||||
78
lib/pages/sleep_report/component/TrendDataTablePage.dart
Normal file
78
lib/pages/sleep_report/component/TrendDataTablePage.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
|
||||
class TrendDataTablePage extends StatefulWidget {
|
||||
final String title;
|
||||
final String tipText;
|
||||
final Widget? chartContent;
|
||||
final double? padding;
|
||||
|
||||
const TrendDataTablePage({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.tipText,
|
||||
required this.chartContent,
|
||||
required this.padding,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TrendDataTablePage> createState() => _TrendDataTablePageState();
|
||||
}
|
||||
|
||||
class _TrendDataTablePageState extends State<TrendDataTablePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc5,
|
||||
borderRadius: BorderRadius.circular(25.rpx)),
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(26.rpx, 29.rpx, 26.rpx, widget.padding ?? 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 30.rpx,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
widget.tipText,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 30.rpx,
|
||||
height: 30.rpx,
|
||||
child: SvgPicture.asset('assets/img/icon/explain.svg',
|
||||
color: Color(0xFFD3D3D3)),
|
||||
)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 47.rpx),
|
||||
widget.chartContent ?? Container(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
123
lib/pages/sleep_report/component/TrendDataTextPage.dart
Normal file
123
lib/pages/sleep_report/component/TrendDataTextPage.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
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/DataShowWidget.dart';
|
||||
import 'package:EasyDartModule/EasyDartModule.dart' as es;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class IndicatorCompareCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String? tooltip;
|
||||
final List<String> headers; // 表头 ["名称", "测量值", "参考范围", "趋势"]
|
||||
final List<List<Widget>> rows; // 每一行的 4 个 widget
|
||||
final double spacing;
|
||||
|
||||
const IndicatorCompareCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.tooltip,
|
||||
required this.headers,
|
||||
required this.rows,
|
||||
this.spacing = 31,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// 标题行
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title.tr,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().title_text_fontSize,
|
||||
),
|
||||
),
|
||||
if (tooltip != null)
|
||||
ClickableContainer(
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor: Colors.white,
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(14.rpx, 0, 14.rpx, 0),
|
||||
borderRadius: 0.rpx,
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Text(
|
||||
tooltip!.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 30.rpx,
|
||||
height: 30.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
color: Color(0xFFD3D3D3),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: spacing.rpx),
|
||||
|
||||
/// 表头
|
||||
DataShowWidget(
|
||||
alignment: MainAxisAlignment.center,
|
||||
widget1: _buildHeader(headers, 0),
|
||||
widget2: _buildHeader(headers, 1),
|
||||
widget3: _buildHeader(headers, 2),
|
||||
widget4: _buildHeader(headers, 3),
|
||||
),
|
||||
|
||||
/// 数据行
|
||||
Column(
|
||||
children: rows.map((row) {
|
||||
return DataShowWidget(
|
||||
alignment: MainAxisAlignment.center,
|
||||
widget1: row[0],
|
||||
widget2: row[1],
|
||||
widget3: row[2],
|
||||
widget4: row[3],
|
||||
).paddingOnly(bottom: 0.rpx);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(List<String> headers, int index) {
|
||||
return Text(
|
||||
headers.length > index ? headers[index].tr : "",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc4,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
852
lib/pages/sleep_report/component/WeekDataWidget.dart
Normal file
852
lib/pages/sleep_report/component/WeekDataWidget.dart
Normal file
@@ -0,0 +1,852 @@
|
||||
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 WeekDataWidget(
|
||||
Map<dynamic, dynamic> sleepReport,
|
||||
dynamic data,
|
||||
) {
|
||||
List<Widget> _buildSectionList() {
|
||||
EdgeInsetsDirectional padding =
|
||||
EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 25.rpx);
|
||||
|
||||
return [
|
||||
AvgSleepScoreWidget(
|
||||
sleepReport: sleepReport,
|
||||
), //睡眠评分
|
||||
SleepChartContainer(
|
||||
title: "每日得分",
|
||||
tipText: "睡眠规律性介绍",
|
||||
sleepReport: sleepReport,
|
||||
chartContent: LineView(
|
||||
xLabels: [
|
||||
ChartLables(
|
||||
//标签系列1
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ChartLables(
|
||||
//X轴标签系列2
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置
|
||||
ondrawer: (canvas, offset, index, label, align, style) {
|
||||
ChartLables.drawText(
|
||||
canvas,
|
||||
label,
|
||||
offset,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.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),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
xCount: 7,
|
||||
yCount: sleepReport['scoreList']['yLable'].length,
|
||||
points: buildWeekDatesAndPoints(sleepReport['scoreList'])['points'],
|
||||
displayMode: ChartDisplayMode.bar,
|
||||
barColors:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['colors'],
|
||||
tips: buildValueTexts(sleepReport['scoreList']['data'], '分', 1),
|
||||
xUnit: sleepReport['scoreList']['yUnit'],
|
||||
barWidth: 0.1,
|
||||
),
|
||||
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: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ChartLables(
|
||||
//X轴标签系列2
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置
|
||||
ondrawer: (canvas, offset, index, label, align, style) {
|
||||
ChartLables.drawText(
|
||||
canvas,
|
||||
label,
|
||||
offset,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
yLabels: [
|
||||
ChartLables(
|
||||
min: sleepReport['csd']['yRange']['min'] ?? 0.0,
|
||||
max: sleepReport['csd']['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),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
xCount: 7,
|
||||
yCount: sleepReport['csd']['yLable'].length,
|
||||
points: [],
|
||||
dualBarPoints: buildTripleBarData(sleepReport['csd']),
|
||||
displayMode: ChartDisplayMode.dualBar,
|
||||
xUnit: sleepReport['csd']['yUnit'],
|
||||
tips: buildSleepValueTexts(sleepReport['csd']['data'], '小时', 1),
|
||||
barWidth: 0.1,
|
||||
),
|
||||
showLabel: sleepReport['csd']['type'],
|
||||
),
|
||||
TrendDataTablePage(
|
||||
title: sleepReport['dysp'][0]['name'],
|
||||
tipText: "入睡时间趋势提示",
|
||||
chartContent: LineView(
|
||||
xLabels: [
|
||||
ChartLables(
|
||||
//标签系列1
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ChartLables(
|
||||
//X轴标签系列2
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置
|
||||
ondrawer: (canvas, offset, index, label, align, style) {
|
||||
ChartLables.drawText(
|
||||
canvas,
|
||||
label,
|
||||
offset,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.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: 7,
|
||||
yCount: sleepReport['dysp'][0]['yLable'].length,
|
||||
points: buildGeneralPoints(sleepReport['dysp'][0]),
|
||||
displayMode: ChartDisplayMode.line,
|
||||
xUnit: sleepReport['dysp'][0]['yUnit'],
|
||||
barWidth: 0.1,
|
||||
bottomPadding: 50,
|
||||
),
|
||||
padding: 45.rpx),
|
||||
TrendDataTablePage(
|
||||
title: sleepReport['dysp'][1]['name'],
|
||||
tipText: "起床时间趋势提示",
|
||||
chartContent: LineView(
|
||||
xLabels: [
|
||||
ChartLables(
|
||||
//标签系列1
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ChartLables(
|
||||
//X轴标签系列2
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置
|
||||
ondrawer: (canvas, offset, index, label, align, style) {
|
||||
ChartLables.drawText(
|
||||
canvas,
|
||||
label,
|
||||
offset,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.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: 7,
|
||||
yCount: sleepReport['dysp'][1]['yLable'].length,
|
||||
points: buildGeneralPoints(sleepReport['dysp'][1]),
|
||||
displayMode: ChartDisplayMode.line,
|
||||
xUnit: sleepReport['dysp'][1]['yUnit'],
|
||||
barWidth: 0.1,
|
||||
bottomPadding: 50,
|
||||
),
|
||||
padding: 45.rpx),
|
||||
TrendDataTablePage(
|
||||
title: sleepReport['dysp'][2]['name'],
|
||||
tipText: "心率基准趋势提示",
|
||||
chartContent: LineView(
|
||||
xLabels: [
|
||||
ChartLables(
|
||||
//标签系列1
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ChartLables(
|
||||
//X轴标签系列2
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置
|
||||
ondrawer: (canvas, offset, index, label, align, style) {
|
||||
ChartLables.drawText(
|
||||
canvas,
|
||||
label,
|
||||
offset,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.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: 7,
|
||||
yCount: sleepReport['dysp'][2]['yLable'].length,
|
||||
points: buildGeneralPoints(sleepReport['dysp'][2]),
|
||||
displayMode: ChartDisplayMode.line,
|
||||
xUnit: sleepReport['dysp'][2]['yUnit'],
|
||||
barWidth: 0.1,
|
||||
bottomPadding: 50,
|
||||
),
|
||||
padding: 45.rpx),
|
||||
TrendDataTablePage(
|
||||
title: sleepReport['dysp'][3]['name'],
|
||||
tipText: "心率变异性趋势提示",
|
||||
chartContent: LineView(
|
||||
xLabels: [
|
||||
ChartLables(
|
||||
//标签系列1
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ChartLables(
|
||||
//X轴标签系列2
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置
|
||||
ondrawer: (canvas, offset, index, label, align, style) {
|
||||
ChartLables.drawText(
|
||||
canvas,
|
||||
label,
|
||||
offset,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.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: 7,
|
||||
yCount: sleepReport['dysp'][3]['yLable'].length,
|
||||
points: buildGeneralPoints(sleepReport['dysp'][3]),
|
||||
displayMode: ChartDisplayMode.line,
|
||||
xUnit: sleepReport['dysp'][3]['yUnit'],
|
||||
barWidth: 0.1,
|
||||
bottomPadding: 50,
|
||||
),
|
||||
padding: 45.rpx),
|
||||
TrendDataTablePage(
|
||||
title: sleepReport['dysp'][4]['name'],
|
||||
tipText: "呼吸基准趋势提示",
|
||||
chartContent: LineView(
|
||||
xLabels: [
|
||||
ChartLables(
|
||||
//标签系列1
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels:
|
||||
buildWeekDatesAndPoints(sleepReport['scoreList'])['dates'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ChartLables(
|
||||
//X轴标签系列2
|
||||
min: 0, //最小值0
|
||||
max: 7, //最大值30
|
||||
q: 6, //labels第一个与最后一个的真实距离,也就是30-1 = 29
|
||||
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
indexs: [0, 1, 2, 3, 4, 5, 6], //每一个标签的对应在X轴的真实位置
|
||||
offset: Offset(0, -50.rpx), //标签相对于原点的偏移,下方60像素位置
|
||||
ondrawer: (canvas, offset, index, label, align, style) {
|
||||
ChartLables.drawText(
|
||||
canvas,
|
||||
label,
|
||||
offset,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Color(0XFFFFFFFF), fontSize: 26.rpx),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
yLabels: [
|
||||
ChartLables(
|
||||
min: 0,
|
||||
max: 5,
|
||||
q: 5,
|
||||
labels: List<String>.from(
|
||||
sleepReport['dysp'][4]['yLable']
|
||||
.map((e) => e['name'].toString()),
|
||||
),
|
||||
offset: Offset(-20.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: 7,
|
||||
yCount: sleepReport['dysp'][4]['yLable'].length,
|
||||
points: buildGeneralPoints(sleepReport['dysp'][4]),
|
||||
displayMode: ChartDisplayMode.line,
|
||||
xUnit: sleepReport['dysp'][4]['yUnit'],
|
||||
barWidth: 0.1,
|
||||
bottomPadding: 50,
|
||||
),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
// 计算非均匀标签的 y 坐标
|
||||
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<String> buildValueTexts(
|
||||
List<dynamic> data,
|
||||
String unit,
|
||||
int direction, // 0 表示左侧,1 表示右侧
|
||||
) {
|
||||
if (data.isEmpty) return [];
|
||||
|
||||
return data.where((item) => item.containsKey('value')).map((item) {
|
||||
final val = item['value'].toString();
|
||||
return direction == 1 ? "$val$unit" : "$unit$val";
|
||||
}).toList();
|
||||
}
|
||||
|
||||
//多个关键点标签
|
||||
List<String> buildSleepValueTexts(
|
||||
List<dynamic> data,
|
||||
String unit,
|
||||
int direction, // 0 左侧,1 右侧
|
||||
) {
|
||||
if (data.isEmpty) return [];
|
||||
|
||||
return data.map((item) {
|
||||
final dst = (item['dst'] ?? 0).toString();
|
||||
final lst = (item['lst'] ?? 0).toString();
|
||||
final slt = (item['slt'] ?? 0).toString();
|
||||
|
||||
final prefix = direction == 1 ? '' : unit;
|
||||
final suffix = direction == 1 ? unit : '';
|
||||
|
||||
var q = [
|
||||
"睡眠时长:$prefix$slt$suffix",
|
||||
"深睡:$prefix$dst$suffix",
|
||||
"浅睡:$prefix$lst$suffix",
|
||||
].join("\n");
|
||||
print(q);
|
||||
return q;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
int getWeekdayX(DateTime current, DateTime first) {
|
||||
final firstMonday = first.subtract(Duration(days: first.weekday - 1));
|
||||
final currentMonday = current.subtract(Duration(days: current.weekday - 1));
|
||||
final weekOffset = currentMonday.difference(firstMonday).inDays ~/ 7;
|
||||
return weekOffset * 7 + (current.weekday - 1);
|
||||
}
|
||||
|
||||
Map<String, dynamic> buildWeekDatesAndPoints(Map<String, dynamic> scoreList) {
|
||||
if (!scoreList.containsKey('data') || (scoreList['data'] as List).isEmpty) {
|
||||
return {
|
||||
'dates': [],
|
||||
'points': [],
|
||||
'colors': [],
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> data = (scoreList['data'] as List)
|
||||
.where((item) => item is Map<String, dynamic>)
|
||||
.cast<Map<String, dynamic>>()
|
||||
.toList();
|
||||
|
||||
// 提取 level -> color 映射表
|
||||
Map<int, String> levelColorMap = {};
|
||||
if (scoreList.containsKey('type')) {
|
||||
List typeList = scoreList['type'];
|
||||
for (var item in typeList) {
|
||||
if (item is Map &&
|
||||
item.containsKey('level') &&
|
||||
item.containsKey('color')) {
|
||||
levelColorMap[item['level']] = item['color'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DateTime baseDate = DateTime.fromMillisecondsSinceEpoch(data.first['st']);
|
||||
int weekday = baseDate.weekday;
|
||||
DateTime monday = baseDate.subtract(Duration(days: weekday - 1));
|
||||
List<String> dates = List.generate(7, (i) {
|
||||
DateTime d = monday.add(Duration(days: i));
|
||||
String month = d.month.toString().padLeft(2, '0');
|
||||
String day = d.day.toString().padLeft(2, '0');
|
||||
return "$month/$day";
|
||||
});
|
||||
|
||||
List<Offset> points = [];
|
||||
List<String> colors = [];
|
||||
|
||||
for (var item in data) {
|
||||
DateTime dt = DateTime.fromMillisecondsSinceEpoch(item['st']);
|
||||
double x = getWeekdayX(dt, baseDate).toDouble();
|
||||
double y = (item['value'] as num?)?.toDouble() ?? 0;
|
||||
int level = item['level'];
|
||||
String color = levelColorMap[level] ?? "#FFFF00";
|
||||
|
||||
points.add(Offset(x, y));
|
||||
colors.add(color);
|
||||
}
|
||||
|
||||
return {
|
||||
'dates': dates,
|
||||
'points': points,
|
||||
'colors': colors,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DateTime baseDate = DateTime.fromMillisecondsSinceEpoch(values.first['st']);
|
||||
List<Offset> points = [];
|
||||
|
||||
for (var item in values) {
|
||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
|
||||
double x = getWeekdayX(date, baseDate).toDouble();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
List<List<Offset>> buildTripleBarData(Map<String, dynamic> csd) {
|
||||
final data =
|
||||
(csd['data'] as List?)?.whereType<Map<String, dynamic>>().toList() ?? [];
|
||||
final 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;
|
||||
|
||||
DateTime firstDate = DateTime.fromMillisecondsSinceEpoch(data.first['st']);
|
||||
|
||||
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;
|
||||
|
||||
dst = dst.clamp(minY, maxY);
|
||||
lst = lst.clamp(minY, maxY);
|
||||
slt = slt.clamp(minY, maxY);
|
||||
|
||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(item['st']);
|
||||
double x = getWeekdayX(date, firstDate).toDouble();
|
||||
|
||||
points.add([
|
||||
Offset(x, dst),
|
||||
Offset(x, dst + lst),
|
||||
Offset(x, slt),
|
||||
]);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
217
lib/pages/sleep_report/component/WeekSleepScoreWidget.dart
Normal file
217
lib/pages/sleep_report/component/WeekSleepScoreWidget.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.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/pages/sleep_report/chart/SegmentedCirclePainter.dart';
|
||||
import 'package:EasyDartModule/EasyDartModule.dart' as es;
|
||||
|
||||
class AvgSleepScoreWidget extends StatelessWidget {
|
||||
final Map sleepReport;
|
||||
final String leftLabel;
|
||||
final String mediumLabel;
|
||||
final String rightLabel;
|
||||
final String unknownText;
|
||||
|
||||
const AvgSleepScoreWidget({
|
||||
Key? key,
|
||||
required this.sleepReport,
|
||||
this.leftLabel = '最低分',
|
||||
this.mediumLabel = '本周评分',
|
||||
this.rightLabel = '最高分',
|
||||
this.unknownText = '未知数据',
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
if (sleepReport == null || sleepReport is! Map || sleepReport.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
List showLabel = sleepReport['score']['type'];
|
||||
List<SegmentData> segments = parseSegments(sleepReport);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
// height: 300.rpx, // 自定义一个高度,确保有空间放置中间内容
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 左侧 min
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150.rpx),
|
||||
child: Text(
|
||||
leftLabel.tr,
|
||||
style: TextStyle(
|
||||
color: Color(0xFFD3D3D3),
|
||||
fontSize: 26.rpx,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sleepReport['score']?['min']?.toString() ?? '--',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 48.rpx,
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(height: 56.rpx)),
|
||||
),
|
||||
|
||||
// 右侧 max
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
rightLabel.tr,
|
||||
style: TextStyle(
|
||||
color: Color(0xFFD3D3D3),
|
||||
fontSize: 26.rpx,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sleepReport['score']?['max']?.toString() ?? '--',
|
||||
style: TextStyle(
|
||||
color: stringToColor("#FF9F66"),
|
||||
fontSize: 48.rpx,
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(height: 56.rpx)),
|
||||
),
|
||||
].divide(SizedBox(width: 33.rpx)),
|
||||
),
|
||||
|
||||
// 中间圆环绝对居中
|
||||
SizedBox(
|
||||
width: 256.rpx,
|
||||
height: 256.rpx,
|
||||
child: SegmentedCircleWithCenterWidget(
|
||||
segments: segments,
|
||||
strokeWidth: 6.rpx,
|
||||
gapAngle: 0,
|
||||
centerWidget: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
mediumLabel.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sleepReport['score']?['avg']?.toString() ?? '--',
|
||||
style: TextStyle(
|
||||
color: segments[0].color,
|
||||
fontSize: 99.rpx,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trend: sleepReport['score']?['trend'],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 50.rpx),
|
||||
Wrap(
|
||||
spacing: 32.rpx,
|
||||
runSpacing: 20.rpx,
|
||||
children: showLabel.map<Widget>((item) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 20.rpx,
|
||||
height: 20.rpx,
|
||||
decoration: BoxDecoration(
|
||||
color: stringToColor("${item["color"]}"),
|
||||
borderRadius: BorderRadius.circular(10.rpx),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 17.rpx),
|
||||
Text(
|
||||
item["name"] + '(' + item['range'] + ")",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24.rpx,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
print("SleepScoreWidget Error: $e");
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
String getSleepLevel(Map sleepReport) {
|
||||
if (sleepReport['score'] == null ||
|
||||
sleepReport['score']['level'] == null ||
|
||||
sleepReport['score']['type'] == null) {
|
||||
return unknownText.tr;
|
||||
}
|
||||
|
||||
int level = sleepReport['score']['level'];
|
||||
List<dynamic> typeList = sleepReport['score']['type'];
|
||||
|
||||
var matched = typeList.firstWhere(
|
||||
(item) => item['level'] == level,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (matched != null && matched['name'] != null) {
|
||||
return matched['name'];
|
||||
}
|
||||
return unknownText.tr;
|
||||
}
|
||||
|
||||
List<SegmentData> parseSegments(Map sleepReport) {
|
||||
try {
|
||||
final score = sleepReport['score'] ?? {};
|
||||
final int level = score['level'] ?? 4;
|
||||
final typeList = score['type'] ?? [];
|
||||
|
||||
// 尝试获取匹配 level 的颜色
|
||||
final matchedType = typeList.firstWhere(
|
||||
(item) => item['level'] == level,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
final color = matchedType != null && matchedType['color'] != null
|
||||
? stringToColor(matchedType['color'])
|
||||
: Colors.grey;
|
||||
|
||||
return [SegmentData(color: color, value: 100)];
|
||||
} catch (e, st) {
|
||||
print('parseSegments error: $e\n$st');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user