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 sleepReport, dynamic data, ) { List _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.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.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>((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.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.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: buildSleepValueTexts(sleepReport['csd']['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: sleepReport['dysp'][0]['tips'], 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.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.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: sleepReport['dysp'][1]['tips'], 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.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.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: sleepReport['dysp'][2]['tips'], 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.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.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: sleepReport['dysp'][3]['tips'], 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.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.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: sleepReport['dysp'][4]['tips'], 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.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.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 buildValueTexts( List 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(); } List buildSleepValueTexts( List 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 : ''; // 格式化日期(不带时间) String dateStr = ''; if (item['st'] != null) { final dt = DateTime.fromMillisecondsSinceEpoch(item['st']); dateStr = "${dt.year}年${dt.month.toString().padLeft(2, '0')}月${dt.day.toString().padLeft(2, '0')}日"; } var q = [ "睡眠时长:$prefix$slt$suffix", "深睡:$prefix$dst$suffix", "浅睡:$prefix$lst$suffix", dateStr, ].join("\n"); print(q); return q; }).toList(); } Map buildMonthlyChartData(Map dyspData) { final List> data = (dyspData['data'] as List?)?.whereType>().toList() ?? []; final List> yLabels = (dyspData['yLable'] as List?) ?.whereType>() .toList() ?? []; final List> types = (dyspData['type'] as List?)?.whereType>().toList() ?? []; if (data.isEmpty || yLabels.length < 2) { return { 'points': [], 'colors': [], 'daysInMonth': 0.0, }; } // 解析 yLabel 为数值 List 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() - 1; // 👈 改为 double List points = []; List 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 buildGeneralPoints(Map dyspData) { final values = (dyspData['value'] as List?)?.whereType>().toList(); final yLabels = (dyspData['yLable'] as List?)?.whereType>().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 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 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 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> buildTripleBarData(Map csd) { final List> data = (csd['data'] as List?)?.whereType>().toList() ?? []; final List> yLabels = (csd['yLable'] as List?)?.whereType>().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> 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; }