This commit is contained in:
wyf
2025-05-20 14:00:44 +08:00
parent 75ddfca402
commit 0a8cffa4c6
48 changed files with 5221 additions and 1733 deletions

View File

@@ -0,0 +1,408 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
class DotBarChart extends StatefulWidget {
final List<Map<String, dynamic>> showLabel;
final int threshold;
final int startTime;
final int endTime;
const DotBarChart({
Key? key,
required this.showLabel,
required this.threshold,
required this.startTime,
required this.endTime,
}) : super(key: key);
@override
_DotBarChartState createState() => _DotBarChartState();
}
class _DotBarChartState extends State<DotBarChart> {
int? selectedIndex;
OverlayEntry? _overlayEntry;
@override
void dispose() {
_removeOverlay();
super.dispose();
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
void _showTooltip(BuildContext context, Map<String, dynamic> data,
Offset position, double dotY) {
_removeOverlay();
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final Offset globalPosition = renderBox.localToGlobal(position);
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
left: globalPosition.dx - 60.rpx,
top: globalPosition.dy - 80.rpx,
child: Material(
color: Colors.transparent,
child: Container(
padding: EdgeInsets.all(12.rpx),
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(8.rpx),
boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.6),
// blurRadius: 10.rpx,
// offset: Offset(0, 4.rpx),
// ),
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 12.rpx,
spreadRadius: 2.rpx,
offset: Offset(0, 6.rpx),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'时间: ${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(data['time']))}',
style: TextStyle(
fontSize: 20.rpx,
color: themeController.currentColor.sc3,
),
),
SizedBox(height: 4.rpx),
Text(
'时长: ${data['times']}',
style: TextStyle(
fontSize: 20.rpx,
color: themeController.currentColor.sc3,
),
),
],
),
),
),
),
);
Overlay.of(context)?.insert(_overlayEntry!);
}
@override
Widget build(BuildContext context) {
if (widget.showLabel.isEmpty) return const SizedBox();
int maxTimes = widget.showLabel
.map((e) => e['times'] ?? 0)
.reduce((a, b) => a > b ? a : b);
int yMax = (maxTimes / 10).ceil() * 10;
if (yMax == 0) yMax = 10;
int maxSteps = 6;
int step = (yMax / maxSteps).ceil();
step = ((step + 9) ~/ 10) * 10;
int displayMax = step * maxSteps;
List<int> yLabels = List.generate(maxSteps + 1, (index) => step * index);
DateFormat fullFormat = DateFormat('HH:mm');
DateFormat hourFormat = DateFormat('H');
int maxXLabels = 11;
int totalPoints = widget.showLabel.length;
List<int> xLabelIndices = [];
if (totalPoints <= maxXLabels) {
xLabelIndices = List.generate(totalPoints, (i) => i);
} else {
xLabelIndices.add(0);
int middleCount = maxXLabels - 2;
double stepX = (totalPoints - 1) / (middleCount + 1);
for (int i = 1; i <= middleCount; i++) {
xLabelIndices.add((stepX * i).round());
}
xLabelIndices.add(totalPoints - 1);
}
double yAxisWidth = 36.rpx;
double xAxisHeight = 40.rpx;
double chartHeight = 491.rpx;
double bottomPadding = 10.rpx;
return GestureDetector(
onTap: () {
setState(() {
selectedIndex = null;
_removeOverlay();
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: chartHeight,
child: Row(
children: [
SizedBox(
width: yAxisWidth,
height: chartHeight + bottomPadding,
child: Stack(
children: [
Positioned(
top: 0,
right: 6.rpx,
child: Text(
'',
style: TextStyle(
fontSize: 18.rpx,
color: themeController.currentColor.sc4,
),
),
),
...List.generate(yLabels.length, (index) {
double yLabelsAreaHeight =
chartHeight - (30.rpx + 18.rpx) - bottomPadding;
double y = (30.rpx + 18.rpx) +
index * (yLabelsAreaHeight / (yLabels.length - 1));
double textHeight = 18.rpx;
double topPos = y - textHeight / 2;
if (index == yLabels.length - 1) {
topPos -= bottomPadding / 2;
}
return Positioned(
right: 6.rpx,
top: topPos,
child: Text(
'${yLabels.reversed.toList()[index]}',
style: TextStyle(
fontSize: 18.rpx,
color: themeController.currentColor.sc4,
),
),
);
}),
],
),
),
Expanded(
child: CustomPaint(
size: Size(double.infinity, chartHeight),
painter: _DotBarChartPainter(
data: widget.showLabel,
yMax: displayMax,
threshold: widget.threshold,
yLabelsCount: yLabels.length,
yAxisTopPadding: 30.rpx + 18.rpx,
horizontalPadding: 20.rpx,
selectedIndex: selectedIndex,
),
child: LayoutBuilder(
builder: (context, constraints) {
final double chartWidth =
constraints.maxWidth - 2 * 20.rpx;
final double xStep = widget.showLabel.length > 1
? chartWidth / (widget.showLabel.length - 1)
: 0;
final double drawableHeight =
chartHeight - (30.rpx + 18.rpx);
return Stack(
children:
widget.showLabel.asMap().entries.map((entry) {
int index = entry.key;
Map<String, dynamic> data = entry.value;
int times = data['times'] ?? 0;
double x = 20.rpx + index * xStep;
double y = (30.rpx + 18.rpx) +
drawableHeight * (1 - times / displayMax);
return Positioned(
left: x - 20.rpx, // Increase touch area
top: y - 20.rpx, // Increase touch area
child: GestureDetector(
onTap: () {
setState(() {
selectedIndex = index;
});
_showTooltip(
context,
data,
Offset(x, y),
y,
);
},
child: Container(
width: 40.rpx,
height: 40.rpx,
color: Colors.transparent,
),
),
);
}).toList(),
);
},
),
),
),
],
),
),
Padding(
padding: EdgeInsets.only(left: yAxisWidth),
child: SizedBox(
height: xAxisHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(widget.showLabel.length, (index) {
String label = '';
if (xLabelIndices.contains(index)) {
DateTime dt = DateTime.fromMillisecondsSinceEpoch(
widget.showLabel[index]['time']);
if (index == 0 || index == totalPoints - 1) {
label = fullFormat.format(dt);
} else {
label = dt.hour.toString();
}
}
return Expanded(
child: Padding(
padding: EdgeInsets.only(top: 14.rpx),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.rpx,
color: themeController.currentColor.sc4,
),
),
),
);
}),
),
),
),
],
),
);
}
}
class _DotBarChartPainter extends CustomPainter {
final List<Map<String, dynamic>> data;
final int yMax;
final int threshold;
final int yLabelsCount;
final double yAxisTopPadding;
final double horizontalPadding;
final int? selectedIndex;
_DotBarChartPainter({
required this.data,
required this.yMax,
required this.threshold,
required this.yLabelsCount,
required this.yAxisTopPadding,
required this.horizontalPadding,
this.selectedIndex,
});
@override
void paint(Canvas canvas, Size size) {
final double chartWidth = size.width - 2 * horizontalPadding;
final double xStep = data.length > 1 ? chartWidth / (data.length - 1) : 0;
final double chartHeight = size.height;
final double drawableHeight = chartHeight - yAxisTopPadding;
final Paint thresholdPaint = Paint()
..style = PaintingStyle.stroke
..color = stringToColor("#FF7159")
..strokeWidth = 1.rpx;
double thresholdY =
yAxisTopPadding + drawableHeight * (1 - threshold / yMax);
drawDashedLine(
canvas,
Offset(0, thresholdY),
Offset(size.width, thresholdY),
thresholdPaint,
8.rpx,
6.rpx,
);
final double dotRadius = 13.rpx;
for (int i = 0; i < data.length; i++) {
int times = data[i]['times'] ?? 0;
double x = horizontalPadding + i * xStep;
double y = yAxisTopPadding + drawableHeight * (1 - times / yMax);
Paint dotPaint = Paint()
..style = PaintingStyle.fill
..color = times >= threshold
? stringToColor("#FF7159")
: stringToColor("#00C1AA");
// Draw a larger circle for selected dot
if (i == selectedIndex) {
Paint borderPaint = Paint()
..style = PaintingStyle.stroke
..color = Colors.white
..strokeWidth = 3.rpx;
canvas.drawCircle(Offset(x, y), dotRadius + 1.rpx, borderPaint);
}
canvas.drawCircle(Offset(x, y), dotRadius, dotPaint);
}
final Paint solidLinePaint = Paint()
..color = Colors.grey.withOpacity(0.7)
..strokeWidth = 1.rpx
..style = PaintingStyle.stroke;
final Paint dashedLinePaint = Paint()
..color = Colors.grey.withOpacity(0.4)
..strokeWidth = 1.rpx
..style = PaintingStyle.stroke;
for (int i = 0; i < yLabelsCount; i++) {
double y = yAxisTopPadding + i * (drawableHeight / (yLabelsCount - 1));
if (i == yLabelsCount - 1) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), solidLinePaint);
} else {
drawDashedLine(
canvas,
Offset(0, y),
Offset(size.width, y),
dashedLinePaint,
8.rpx,
6.rpx,
);
}
}
}
void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint,
double dashWidth, double gapWidth) {
double dx = start.dx;
final double y = start.dy;
while (dx < end.dx) {
final double nextDx = (dx + dashWidth).clamp(start.dx, end.dx);
canvas.drawLine(Offset(dx, y), Offset(nextDx, y), paint);
dx = nextDx + gapWidth;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
class FatigueCircleIndicator extends StatelessWidget {
final Map<String, dynamic> data;
const FatigueCircleIndicator({super.key, required this.data});
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
final double radius = (screenWidth * 0.127).clamp(95.rpx, double.infinity);
final double strokeWidth = 14.rpx;
final double backgroundStrokeWidth = 8.rpx;
final String name = data["name"];
final Color color = data["color"];
final int percent = data["percent"];
final String explain = data["explain"];
final Color bottomColor = data["bottomColor"];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: radius * 2,
height: radius * 2,
child: Stack(
alignment: Alignment.center,
children: [
// 合并绘制背景与进度
CustomPaint(
size: Size(radius * 2, radius * 2),
painter: _CirclePainter(
percent: percent,
color: color,
bottomColor: bottomColor,
progressStrokeWidth: strokeWidth,
backgroundStrokeWidth: backgroundStrokeWidth,
),
),
// 中心文本
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$percent%',
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: percent > 60
? themeController.currentColor.sc9
: themeController.currentColor.sc3,
),
),
SizedBox(height: 4.rpx),
Text(
explain,
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: percent > 60
? themeController.currentColor.sc9
: themeController.currentColor.sc3,
),
),
],
),
],
),
),
SizedBox(height: 40.rpx),
Text(
name,
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: themeController.currentColor.sc3,
),
),
],
);
}
}
class _CirclePainter extends CustomPainter {
final int percent;
final Color color;
final Color bottomColor;
final double progressStrokeWidth;
final double backgroundStrokeWidth;
_CirclePainter({
required this.percent,
required this.color,
required this.bottomColor,
required this.progressStrokeWidth,
required this.backgroundStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width - progressStrokeWidth) / 2;
// 背景环(底色,细)
final backgroundPaint = Paint()
..color = bottomColor
..style = PaintingStyle.stroke
..strokeWidth = backgroundStrokeWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
90 * 3.1415926 / 180,
2 * 3.1415926,
false,
backgroundPaint,
);
// 进度环(进度色,粗)
final progressPaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = progressStrokeWidth
..strokeCap = StrokeCap.butt;
final sweepAngle = 2 * 3.1415926 * (percent / 100);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
90 * 3.1415926 / 180,
sweepAngle,
false,
progressPaint,
);
}
@override
bool shouldRepaint(_CirclePainter oldDelegate) {
return oldDelegate.percent != percent ||
oldDelegate.color != color ||
oldDelegate.bottomColor != bottomColor ||
oldDelegate.progressStrokeWidth != progressStrokeWidth ||
oldDelegate.backgroundStrokeWidth != backgroundStrokeWidth;
}
}

View File

@@ -0,0 +1,281 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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';
class HorizontalBarChart extends StatelessWidget {
final List<Map<String, dynamic>> showLabel;
final bool showPercent;
const HorizontalBarChart({
super.key,
required this.showLabel,
this.showPercent = true,
});
@override
Widget build(BuildContext context) {
final data = showLabel.map((item) {
return BarData(
label: item['name'],
value: (item['percent'] ?? 0).toDouble(),
color: item['color'] ?? Colors.grey,
explain: item['explain'] ?? '',
);
}).toList();
final double labelWidth = (MediaQuery.of(context).size.width * 0.5).clamp(
MediaQuery.of(context).size.width * 0.08,
MediaQuery.of(context).size.width * 0.22);
final double barHeight = 24.0.rpx;
final double barSpacing = 40.0.rpx;
final totalHeight = data.length * (barHeight + barSpacing) + 30.rpx;
return SizedBox(
height: totalHeight,
child: Row(
children: [
// 左侧标签列
SizedBox(
width: labelWidth,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: data.length,
itemBuilder: (context, index) {
final bar = data[index];
return Container(
height: barHeight + barSpacing,
alignment: Alignment.centerRight,
child: LabelWithSvg(
label: bar.label,
explain: bar.explain,
),
);
},
),
),
SizedBox(
width: 16.rpx,
),
// 右侧柱状图区
Expanded(
child: Stack(
children: [
// 网格线背景层
CustomPaint(
size: Size(double.infinity, totalHeight),
painter: GridPainter(totalHeight: totalHeight),
),
// 柱状图列表
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: data.length,
itemBuilder: (context, index) {
final bar = data[index];
return SizedBox(
height: barHeight + barSpacing,
child: CustomPaint(
painter: SingleBarPainter(
value: bar.value,
color: bar.color,
showPercent: showPercent,
),
),
);
},
)
],
),
),
],
),
);
}
}
class BarData {
final String label;
final double value;
final Color color;
final String explain;
BarData({
required this.label,
required this.value,
required this.color,
required this.explain,
});
}
class LabelWithSvg extends StatelessWidget {
final String label;
final String explain;
const LabelWithSvg({
super.key,
required this.label,
required this.explain,
});
@override
Widget build(BuildContext context) {
final textStyle =
TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Text(
label,
style: textStyle,
overflow: TextOverflow.ellipsis,
),
),
// SizedBox(width: 8.rpx),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white,
padding:
EdgeInsetsDirectional.fromSTEB(14.rpx, 14.rpx, 14.rpx, 14.rpx),
borderRadius: 0.rpx,
onTap: () {
// Get.toNamed("/deviceShareListPage", arguments: explain);
showTipDialog(
context,
Container(
child: Text(
explain,
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: SizedBox(
width: 17.rpx,
height: 17.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: Colors.white,
),
),
),
],
);
}
}
class SingleBarPainter extends CustomPainter {
final double value;
final Color color;
final bool showPercent;
final double maxValue = 100;
SingleBarPainter({
required this.value,
required this.color,
required this.showPercent,
});
@override
void paint(Canvas canvas, Size size) {
final barHeight = 24.0.rpx;
final rightPadding = 20.0.rpx;
final chartWidth = size.width - rightPadding;
final left = 0.0;
final right = left + (value / maxValue) * chartWidth;
final rect = Rect.fromLTWH(
left, (size.height - barHeight) / 2, right - left, barHeight);
final paint = Paint()..color = color;
canvas.drawRect(rect, paint);
if (showPercent) {
final textStyle =
TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
final textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text =
TextSpan(text: '${value.toStringAsFixed(0)}%', style: textStyle);
textPainter.layout();
canvas.save();
canvas.clipRect(Rect.fromLTWH(
left, (size.height - barHeight) / 2, chartWidth, barHeight));
textPainter.paint(
canvas,
Offset(right + 4.0.rpx, (size.height - textPainter.height) / 2),
);
canvas.restore();
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
class GridPainter extends CustomPainter {
final double totalHeight;
final int gridCount = 5;
final double maxValue = 100;
final double bottomPadding = 30.0.rpx;
GridPainter({required this.totalHeight});
@override
void paint(Canvas canvas, Size size) {
final Paint gridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0.rpx;
final double rightPadding = 20.0.rpx;
final double chartWidth = size.width - rightPadding;
final double chartHeight = totalHeight - bottomPadding;
for (int i = 0; i <= gridCount; i++) {
double dx = (i / gridCount) * chartWidth;
_drawDashedLine(
canvas, Offset(dx, 0), Offset(dx, chartHeight), gridPaint);
final percent = (i / gridCount) * maxValue;
final TextPainter textPainter = TextPainter(
textDirection: TextDirection.ltr,
text: TextSpan(
text: '${percent.toInt()}',
style: TextStyle(color: Colors.grey, fontSize: 18.rpx),
),
);
textPainter.layout();
textPainter.paint(
canvas, Offset(dx - textPainter.width / 2, chartHeight + 4.0.rpx));
}
}
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
const double dashWidth = 6.0;
const double dashSpace = 4.0;
final double totalHeight = (end.dy - start.dy).abs();
double currentY = start.dy;
while (currentY < end.dy) {
final double nextY = currentY + dashWidth;
canvas.drawLine(
Offset(start.dx, currentY),
Offset(start.dx, nextY > end.dy ? end.dy : nextY),
paint,
);
currentY = nextY + dashSpace;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'dart:ui' as ui;
import 'dart:math';
class LineChartByRange extends StatelessWidget {
final List<Map<String, dynamic>> showLabel;
final int startTime;
final int endTime;
const LineChartByRange({
Key? key,
required this.showLabel,
required this.startTime,
required this.endTime,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (showLabel.isEmpty) return const SizedBox();
int maxTimes =
showLabel.map((e) => e['times'] ?? 0).reduce((a, b) => a > b ? a : b);
int yMax = (maxTimes / 10).ceil() * 10;
if (yMax == 0) yMax = 10;
DateTime minTime = DateTime.fromMillisecondsSinceEpoch(startTime);
DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(endTime);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 500.rpx,
child: CustomPaint(
size: Size(double.infinity, 500.rpx),
painter: _LineChartByRangePainter(
data: showLabel,
yMax: yMax,
minTime: minTime,
maxTime: maxTime,
),
),
),
],
);
}
}
class _LineChartByRangePainter extends CustomPainter {
final List<Map<String, dynamic>> data;
final int yMax;
final DateTime minTime;
final DateTime maxTime;
_LineChartByRangePainter({
required this.data,
required this.yMax,
required this.minTime,
required this.maxTime,
});
@override
void paint(Canvas canvas, Size size) {
double padding = 20.rpx;
double labelInset = 12.rpx; // X轴标签缩进距离
// 绘图X轴起止点考虑内缩labelInset
final double xStart = padding + labelInset;
final double xEnd = size.width - padding - labelInset;
final double chartWidth = xEnd - xStart;
double chartHeight = size.height - 30.rpx;
int totalDuration =
maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
if (totalDuration <= 0) return;
Paint linePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.rpx
..color = stringToColor("#00C1AA")
..strokeCap = StrokeCap.round;
Paint fillCirclePaint = Paint()
..style = PaintingStyle.fill
..color = stringToColor("#00C1AA");
// 1. 先绘制数据线段及起止点圆点
for (var item in data) {
int start = item['startTime'];
int end = item['endTime'];
int times = item['times'];
double startX = xStart +
chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
double endX = xStart +
chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
double y = chartHeight * (1 - times / yMax);
// 画线段
canvas.drawLine(Offset(startX, y), Offset(endX, y), linePaint);
// 画起点圆点
canvas.drawCircle(Offset(startX, y), 4.rpx, fillCirclePaint);
// 画终点圆点
canvas.drawCircle(Offset(endX, y), 4.rpx, fillCirclePaint);
}
// 2. Y轴辅助线及文字
Paint axisPaint = Paint()
..color = Colors.grey.withOpacity(0.4)
..strokeWidth = 1.rpx;
for (int i = 0; i <= 6; i++) {
double y = chartHeight * i / 6;
if (i == 6) {
// 实线
canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint);
} else {
// 虚线
drawDashedLine(
canvas,
Offset(xStart, y),
Offset(xEnd, y),
axisPaint,
dashWidth: 8.rpx,
dashSpace: 6.rpx,
);
}
// Y轴文字
TextPainter tp = TextPainter(
text: TextSpan(
text: '${yMax - (yMax * i / 6).round()}',
style: TextStyle(
fontSize: 18.rpx, color: themeController.currentColor.sc4),
),
textDirection: ui.TextDirection.ltr,
);
tp.layout();
tp.paint(canvas, Offset(0, y - tp.height / 2));
}
// 3. X轴线
canvas.drawLine(
Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint);
// 4. 画X轴时间点对应的垂直虚线辅助线
int totalHours = maxTime.difference(minTime).inHours;
int startHour = minTime.hour;
for (int i = 1; i < totalHours; i++) {
double x = xStart + chartWidth * i / totalHours;
// 垂直虚线
drawDashedLine(
canvas,
Offset(x, 0),
Offset(x, chartHeight),
axisPaint,
dashWidth: 4.rpx,
dashSpace: 4.rpx,
);
}
// 5. 画左侧完整时分 (HH:mm),往内缩 labelInset
String leftLabel = DateFormat('HH:mm').format(minTime);
TextPainter leftTp = TextPainter(
text: TextSpan(
text: leftLabel,
style: TextStyle(
fontSize: 18.rpx,
color: themeController.currentColor.sc4,
),
),
textDirection: ui.TextDirection.ltr,
);
leftTp.layout();
leftTp.paint(canvas,
Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx));
// 6. 画右侧完整时分 (HH:mm),往内缩 labelInset
String rightLabel = DateFormat('HH:mm').format(maxTime);
TextPainter rightTp = TextPainter(
text: TextSpan(
text: rightLabel,
style: TextStyle(
fontSize: 18.rpx,
color: themeController.currentColor.sc4,
),
),
textDirection: ui.TextDirection.ltr,
);
rightTp.layout();
rightTp.paint(
canvas,
Offset(size.width - padding - labelInset - rightTp.width / 2,
chartHeight + 8.rpx));
// 7. 中间小时数字(23, 0, 1, 2, ...)
for (int i = 1; i < totalHours; i++) {
double x = xStart + chartWidth * i / totalHours;
int hourLabelNum = (startHour + i) % 24;
String hourLabel = '$hourLabelNum';
TextPainter tp = TextPainter(
text: TextSpan(
text: hourLabel,
style: TextStyle(
fontSize: 18.rpx,
color: themeController.currentColor.sc4,
),
),
textDirection: ui.TextDirection.ltr,
);
tp.layout();
tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
void drawDashedLine(
Canvas canvas,
Offset start,
Offset end,
Paint paint, {
required double dashWidth,
required double dashSpace,
}) {
final dx = end.dx - start.dx;
final dy = end.dy - start.dy;
final distance = sqrt(dx * dx + dy * dy);
final direction = Offset(dx / distance, dy / distance);
double drawn = 0;
while (drawn < distance) {
final from = start + direction * drawn;
final to = start + direction * (drawn + dashWidth).clamp(0, distance);
canvas.drawLine(from, to, paint);
drawn += dashWidth + dashSpace;
}
}
}

View File

@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
class StatusBarWithIndicator extends StatelessWidget {
final int selectKey;
final List<Map<String, dynamic>> showLabel;
final IconData icon;
final double gap; // 每段之间的间距
const StatusBarWithIndicator({
super.key,
required this.selectKey,
required this.showLabel,
this.icon = Icons.favorite,
this.gap = 8.0, // 默认 8.rpx 间距
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final totalWidth = constraints.maxWidth;
final itemCount = showLabel.length;
// 每条线的宽度 = (总宽度 - 总间隔)/ 项数
final totalGap = (itemCount - 1) * gap.rpx;
final itemWidth = (totalWidth - totalGap) / itemCount;
// 找到选中项的 index
final selectedIndex = showLabel.indexWhere((e) => e['key'] == selectKey);
final iconLeft = selectedIndex >= 0
? selectedIndex * (itemWidth + gap.rpx) + itemWidth / 2
: 0.0;
return SizedBox(
width: double.infinity,
child: Stack(
clipBehavior: Clip.none,
children: [
if (selectedIndex >= 0)
Positioned(
left: iconLeft,
top: -20.rpx,
child: Transform.translate(
offset: Offset(-22.5.rpx, 0), // 图片宽度 45.rpx居中偏移
child: Container(
width: 45.rpx,
height: 76.rpx,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/img/tip_arrow.gif'),
fit: BoxFit.cover,
),
),
),
),
),
Padding(
padding: EdgeInsets.only(top: 50.rpx),
child: Column(
children: [
// 条形段(带间距)
Row(
children: showLabel.asMap().entries.map((entry) {
int index = entry.key;
var item = entry.value;
return Container(
width: itemWidth,
height: 15.rpx,
margin: EdgeInsets.only(
left: index == 0 ? 0 : gap.rpx,
),
decoration: BoxDecoration(
color: item['color'],
borderRadius: BorderRadius.circular(0.rpx),
),
);
}).toList(),
),
SizedBox(height: 12.rpx),
// 名称文字
Row(
children: showLabel.asMap().entries.map((entry) {
int index = entry.key;
var item = entry.value;
return Container(
width: itemWidth,
margin: EdgeInsets.only(
left: index == 0 ? 0 : gap.rpx,
),
alignment: Alignment.center,
child: Text(
item['name'],
style: TextStyle(
fontSize: 24.rpx,
color: Colors.white,
),
textAlign: TextAlign.center,
),
);
}).toList(),
),
],
),
),
],
),
);
});
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
class VerticalBarList extends StatelessWidget {
final List<Map<String, dynamic>> showLabel;
final double maxBarHeight;
final double barWidth;
const VerticalBarList({
super.key,
required this.showLabel,
this.maxBarHeight = 100.0, // 柱子的最大高度
this.barWidth = 20.0, // 每根柱子的宽度
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: showLabel.map((item) {
final percent = item['percent'] ?? 0;
final color = item['color'] ?? Colors.grey;
final name = item['name'] ?? '';
final barHeight = maxBarHeight * (percent / 100.0);
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.rpx),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 柱子
Container(
width: barWidth.rpx,
height: barHeight.rpx,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4.rpx),
),
),
SizedBox(height: 8.rpx),
// 文字竖排
Text(
name,
style: TextStyle(
fontSize: 24.rpx,
color: Colors.white,
),
textAlign: TextAlign.center,
),
],
),
);
}).toList(),
);
}
}

View File

@@ -0,0 +1,141 @@
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/DotBarChart.dart';
class BreathPauseWidget extends StatefulWidget {
BreathPauseWidget({super.key});
@override
State<BreathPauseWidget> createState() => _BreathPauseWidgetState();
}
class _BreathPauseWidgetState extends State<BreathPauseWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
// var showLabel = [
// {"time": 1744547251000, "times": 20},
// {"time": 1744550851000, "times": 50},
// {"time": 1744554451000, "times": 20},
// {"time": 1744558051000, "times": 30},
// {"time": 1744561651000, "times": 20},
// {"time": 1744565251000, "times": 10},
// {"time": 1744568851000, "times": 20},
// {"time": 1744572451000, "times": 20},
// {"time": 1744583251000, "times": 100},
// {"time": 1744586851000, "times": 20},
// ];
var showLabel = [
{"time": 1744547251000, "times": 25},
{"time": 1744550851000, "times": 27},
{"time": 1744554451000, "times": 40},
{"time": 1744558051000, "times": 28},
{"time": 1744561651000, "times": 15},
{"time": 1744565251000, "times": 48},
{"time": 1744568851000, "times": 25},
{"time": 1744572451000, "times": 17},
{"time": 1744583251000, "times": 35},
{"time": 1744586851000, "times": 40},
];
var threshold = 40;
var startTime = 1744641151000;
var endTime = 1744677151000;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 0.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"呼吸暂停监测".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
"呼吸暂停监测介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 32.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx),
child: DotBarChart(
showLabel: showLabel,
threshold: threshold,
startTime: startTime,
endTime: endTime,
),
),
SizedBox(
height: 52.rpx,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,152 @@
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/HorizontalBarChart.dart';
class DiseasePercentsWidget extends StatefulWidget {
DiseasePercentsWidget({super.key});
@override
State<DiseasePercentsWidget> createState() => _DiseasePercentsWidgetState();
}
class _DiseasePercentsWidgetState extends State<DiseasePercentsWidget> {
var showLabel = [
{
"key": 1,
"name": "心脏病",
"color": stringToColor("#00C1AA"),
"percent": 45,
"explain": "心脏病是指心脏的结构或功能异常,可能导致心脏无法有效地泵血。"
},
{
"key": 2,
"name": "高血压",
"color": stringToColor("#00C1AA"),
"percent": 32,
"explain": "高血压是指血液在动脉中流动时对血管壁施加的压力过高。"
},
{
"key": 3,
"name": "糖尿病",
"color": stringToColor("#00C1AA"),
"percent": 50,
"explain": "糖尿病是一种代谢性疾病,导致血糖水平异常升高。"
},
{
"key": 4,
"name": "甲亢",
"color": stringToColor("#FF7159"),
"percent": 80,
"explain": "甲亢是指甲状腺分泌过多的甲状腺激素,导致新陈代谢加速。"
},
{
"key": 5,
"name": "消化系统",
"color": stringToColor("#00C1AA"),
"percent": 12,
"explain": "消化系统是身体中处理食物的机构,是造成疾病和疾病症状的来源。",
},
{
"key": 6,
"name": "呼吸系统",
"color": stringToColor("#00C1AA"),
"percent": 62,
"explain": "呼吸系统是负责气体交换的器官系统,包括鼻、喉、气管和肺等。",
},
];
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@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(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"慢性病风险指数".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
// 你的点击逻辑
showTipDialog(
context,
Container(
child: Text(
"慢性病风险指数介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 34.rpx,
),
Container(
child: HorizontalBarChart(
showLabel: showLabel,
showPercent: true,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,148 @@
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
import 'package:vbvs_app/pages/sleep_report/chart/FatigueCircleIndicator.dart';
class HeartHealthWidget extends StatefulWidget {
HeartHealthWidget({super.key});
@override
State<HeartHealthWidget> createState() => _HeartHealthWidgetState();
}
class _HeartHealthWidgetState extends State<HeartHealthWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
var showLabel = [
{
"name": "焦虑抑郁",
"color": Color(0xFF4CAF50),
"percent": "7%",
"explain": "低风险"
},
{
"name": "过度疲劳",
"color": stringToColor("#FF7159"),
"percent": "69%",
"explain": "高风险"
},
];
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 0.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"心理健康评估".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
"心理健康评估介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 104.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FatigueCircleIndicator(
data: {
"name": "焦虑抑郁",
"color": stringToColor("#00C1AA"),
"percent": 7,
"explain": "低风险",
"bottomColor": Colors.grey,
},
),
FatigueCircleIndicator(
data: {
"name": "过度疲劳",
"color": stringToColor("#FF7159"),
"percent": 69,
"explain": "高风险",
"bottomColor": Colors.grey,
},
),
].divide(SizedBox(
width: 110.rpx,
)),
),
),
SizedBox(
height: 72.rpx,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,117 @@
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/StatusBarWithIndicator.dart';
class HeartPointWidget extends StatefulWidget {
HeartPointWidget({super.key});
@override
State<HeartPointWidget> createState() => _HeartPointWidgetState();
}
class _HeartPointWidgetState extends State<HeartPointWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@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(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"心率散点图".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
"心率散点图介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 83.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: StatusBarWithIndicator(
selectKey: 2,
showLabel: [
{"key": 1, "name": "正常", "color": Color(0xFF4CAF50)},
{"key": 2, "name": "一般", "color": Color(0xFF8BC34A)},
{"key": 3, "name": "注意", "color": Color(0xFFFFC107)},
{"key": 4, "name": "警告", "color": Color(0xFFF44336)},
],
),
),
SizedBox(
height: 56.rpx,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,117 @@
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/StatusBarWithIndicator.dart';
class HrvWidget extends StatefulWidget {
HrvWidget({super.key});
@override
State<HrvWidget> createState() => _HrvWidgetState();
}
class _HrvWidgetState extends State<HrvWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@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(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"心率变异性HRV".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
"心率变异性HRV介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 83.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: StatusBarWithIndicator(
selectKey: 2,
showLabel: [
{"key": 1, "name": "正常", "color": Color(0xFF4CAF50)},
{"key": 2, "name": "一般", "color": Color(0xFF8BC34A)},
{"key": 3, "name": "注意", "color": Color(0xFFFFC107)},
{"key": 4, "name": "警告", "color": Color(0xFFF44336)},
],
),
),
SizedBox(
height: 56.rpx,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,117 @@
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/StatusBarWithIndicator.dart';
class SkinPercentWidget extends StatefulWidget {
SkinPercentWidget({super.key});
@override
State<SkinPercentWidget> createState() => _SkinPercentWidgetState();
}
class _SkinPercentWidgetState extends State<SkinPercentWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@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(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"皮肤指数".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
"皮肤指数介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 83.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: StatusBarWithIndicator(
selectKey: 2,
showLabel: [
{"key": 1, "name": "正常", "color": Color(0xFF4CAF50)},
{"key": 2, "name": "一般", "color": Color(0xFF8BC34A)},
{"key": 3, "name": "注意", "color": Color(0xFFFFC107)},
{"key": 4, "name": "警告", "color": Color(0xFFF44336)},
],
),
),
SizedBox(
height: 56.rpx,
),
],
),
),
);
}
}

View File

@@ -3,7 +3,7 @@ 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/component/SegmentedCirclePainter.dart';
import 'package:vbvs_app/pages/sleep_report/chart/SegmentedCirclePainter.dart';
class SleepScoreWidget extends StatefulWidget {
const SleepScoreWidget({super.key});

View File

@@ -0,0 +1,124 @@
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/LineChartByRange.dart';
class SnoreViewWidgetWidget extends StatefulWidget {
SnoreViewWidgetWidget({super.key});
@override
State<SnoreViewWidgetWidget> createState() => _SnoreViewWidgetWidgetState();
}
class _SnoreViewWidgetWidgetState extends State<SnoreViewWidgetWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
var showLabel = [
{"startTime": 1744644751000, "endTime": 1744648351000, "times": 25},
{"startTime": 1744650031000, "endTime": 1744653631000, "times": 27},
{"startTime": 1744655011000, "endTime": 1744662211000, "times": 60},
// {"startTime": 1744657291000, "endTime": 1744657411000, "times": 28},
// {"startTime": 1744661011000, "endTime": 1744661131000, "times": 15},
// {"startTime": 1744668331000, "endTime": 1744668511000, "times": 48},
// {"startTime": 1744673431000, "endTime": 1744673551000, "times": 25},
];
var startTime = 1744641151000;
var endTime = 1744677151000;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: themeController.currentColor.sc5,
borderRadius: BorderRadius.circular(
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 0.rpx),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"呼吸暂停监测".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
"呼吸暂停监测介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 32.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx),
child: LineChartByRange(
showLabel: showLabel,
startTime: startTime,
endTime: endTime,
),
),
SizedBox(
height: 52.rpx,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,119 @@
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/StatusBarWithIndicator.dart';
class ZiZhuShenJingPercentWidget extends StatefulWidget {
ZiZhuShenJingPercentWidget({super.key});
@override
State<ZiZhuShenJingPercentWidget> createState() =>
_ZiZhuShenJingPercentWidgetState();
}
class _ZiZhuShenJingPercentWidgetState
extends State<ZiZhuShenJingPercentWidget> {
@override
void setState(VoidCallback callback) {
super.setState(callback);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@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(
mainAxisSize: MainAxisSize.max,
children: [
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"自主神经平衡指数".tr,
style: TextStyle(
color: themeController.currentColor.sc3,
fontSize: AppConstants().title_text_fontSize),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
padding: EdgeInsetsDirectional.fromSTEB(
14.rpx, 0.rpx, 14.rpx, 0), //
borderRadius: 0.rpx, // 圆形点击区域
onTap: () {
showTipDialog(
context,
Container(
child: Text(
"自主神经平衡指数监测介绍。",
style: TextStyle(
fontSize: 26.rpx,
color: themeController.currentColor.sc3,
),
),
),
);
},
child: Container(
padding: EdgeInsetsDirectional.fromSTEB(
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/explain.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
),
),
),
],
),
),
SizedBox(
height: 83.rpx,
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
child: StatusBarWithIndicator(
selectKey: 3,
showLabel: [
{"key": 1, "name": "正常", "color": Color(0xFF4CAF50)},
{"key": 2, "name": "一般", "color": Color(0xFF8BC34A)},
{"key": 3, "name": "注意", "color": Color(0xFFFFC107)},
{"key": 4, "name": "警告", "color": Color(0xFFF44336)},
],
),
),
SizedBox(
height: 56.rpx,
),
],
),
),
);
}
}

View File

@@ -9,7 +9,14 @@ import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/controller/date/CalendarController.dart';
import 'package:vbvs_app/controller/sleep/sleep_report_controller.dart';
import 'package:vbvs_app/pages/common/selectDialog.dart';
import 'package:vbvs_app/pages/sleep_report/component/BreathPauseWidget.dart';
import 'package:vbvs_app/pages/sleep_report/component/DiseasePercentsWidget.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/SkinPercentWidget.dart';
import 'package:vbvs_app/pages/sleep_report/component/SleepScoreWidget.dart';
import 'package:vbvs_app/pages/sleep_report/component/SnoreViewWidget.dart';
import 'package:vbvs_app/pages/sleep_report/component/ZiZhuShenJingPercentWidget.dart';
class NewSleepReportPage extends StatefulWidget {
var date;
@@ -32,6 +39,7 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
DateTime.fromMillisecondsSinceEpoch(widget.date);
sleepReportController.selectedDate.value =
DateTime.fromMillisecondsSinceEpoch(widget.date);
sleepReportController.model.type = 1;
super.initState();
}
@@ -309,7 +317,7 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
width: double.infinity,
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 57.rpx, 30.rpx, 57.rpx),
30.rpx, 32.rpx, 30.rpx, 32.rpx),
child: Obx(() {
var date = sleepReportController.selectedDate;
return getTimeWidget();
@@ -497,7 +505,65 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
child: SleepScoreWidget(),
),
),
],
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0),
child: Container(
width: double.infinity,
child: HeartPointWidget(),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0),
child: Container(
width: double.infinity,
child: SnoreViewWidgetWidget(),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0),
child: Container(
width: double.infinity,
child: BreathPauseWidget(),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0),
child: Container(
width: double.infinity,
child: HeartHealthWidget(),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0),
child: Container(
width: double.infinity,
child: DiseasePercentsWidget(),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0),
child: Container(
width: double.infinity,
child: SkinPercentWidget(),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
30.rpx, 0.rpx, 30.rpx, 0),
child: Container(
width: double.infinity,
child: ZiZhuShenJingPercentWidget(),
),
),
].divide(SizedBox(
height: 25.rpx,
)),
),
),
),
@@ -508,124 +574,144 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
}
Widget getTimeWidget() {
if (sleepReportController.model.type == 1) {
//日报
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 28.rpx,
height: 28.rpx,
// width: double.infinity,
decoration: BoxDecoration(),
),
Container(
child: Row(
children: [
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: themeController.currentColor.sc3,
padding: EdgeInsets.all(10.rpx), // 增加点击热区
borderRadius: 8.rpx,
onTap: () {
sleepReportController.selectedDate.value =
sleepReportController.selectedDate.value!
.subtract(const Duration(days: 1));
calendarController.selectedDate.value =
sleepReportController.selectedDate.value;
sleepReportController.updateAll();
calendarController.updateAll();
},
child: SizedBox(
width: 9.rpx,
height: 14.rpx,
child: SvgPicture.asset(
'assets/img/icon/arrow_left.svg',
color: themeController.currentColor.sc3,
),
),
final selectedDate = sleepReportController.selectedDate.value!;
final type = sleepReportController.model.type;
String displayText = '';
if (type == 1) {
// 日报
displayText =
MyUtils.getFormatChineseTime(selectedDate.millisecondsSinceEpoch);
} else if (type == 2) {
// 周报
final startOfWeek =
selectedDate.subtract(Duration(days: selectedDate.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 6));
displayText =
'${MyUtils.getFormatChineseTime(startOfWeek.millisecondsSinceEpoch, showWeekday: false)}-${MyUtils.getFormatChineseTime(endOfWeek.millisecondsSinceEpoch, showWeekday: false)}';
} else if (type == 3) {
// 月报
displayText =
'${selectedDate.year}${selectedDate.month.toString().padLeft(2, '0')}';
}
void onLeftArrowTap() {
if (type == 1) {
sleepReportController.selectedDate.value =
selectedDate.subtract(const Duration(days: 1));
} else if (type == 2) {
sleepReportController.selectedDate.value =
selectedDate.subtract(const Duration(days: 7));
} else if (type == 3) {
sleepReportController.selectedDate.value = DateTime(
selectedDate.year,
selectedDate.month - 1,
selectedDate.day,
);
}
calendarController.selectedDate.value =
sleepReportController.selectedDate.value;
sleepReportController.updateAll();
calendarController.updateAll();
}
void onRightArrowTap() {
if (type == 1) {
sleepReportController.selectedDate.value =
selectedDate.add(const Duration(days: 1));
} else if (type == 2) {
sleepReportController.selectedDate.value =
selectedDate.add(const Duration(days: 7));
} else if (type == 3) {
sleepReportController.selectedDate.value = DateTime(
selectedDate.year,
selectedDate.month + 1,
selectedDate.day,
);
}
calendarController.selectedDate.value =
sleepReportController.selectedDate.value;
sleepReportController.updateAll();
calendarController.updateAll();
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(width: 28.rpx, height: 28.rpx), // 占位
Row(
children: [
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: themeController.currentColor.sc3,
padding: EdgeInsets.all(10.rpx),
borderRadius: 8.rpx,
onTap: onLeftArrowTap,
child: SizedBox(
width: 9.rpx,
height: 14.rpx,
child: SvgPicture.asset(
'assets/img/icon/arrow_left.svg',
color: themeController.currentColor.sc3,
),
Container(
child: Text(
MyUtils.getFormatChineseTime(sleepReportController
.selectedDate.value!.millisecondsSinceEpoch),
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: themeController.currentColor.sc3,
),
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: themeController.currentColor.sc3,
padding: EdgeInsets.all(10.rpx), // 增加点击热区
borderRadius: 8.rpx,
onTap: () {
sleepReportController.selectedDate.value =
sleepReportController.selectedDate.value!
.subtract(const Duration(days: -1));
calendarController.selectedDate.value =
sleepReportController.selectedDate.value;
sleepReportController.updateAll();
calendarController.updateAll();
},
child: SizedBox(
width: 9.rpx,
height: 14.rpx,
child: SvgPicture.asset(
'assets/img/icon/arrow_right.svg',
color: themeController.currentColor.sc3,
),
),
),
].divide(SizedBox(
width: 26.rpx,
)),
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: themeController.currentColor.sc3,
padding: EdgeInsetsDirectional.fromSTEB(
0.rpx,
0.rpx,
0.rpx,
0.rpx,
),
borderRadius: 0,
onTap: () {
showSleepCalendarBottomSheet(
timestamp: sleepReportController
.selectedDate.value!.millisecondsSinceEpoch,
context: context,
onDateSelected: (selectedDate) {
print("选中日期:");
print(selectedDate);
sleepReportController.selectedDate.value = selectedDate;
calendarController.selectedDate.value = selectedDate;
sleepReportController.updateAll();
calendarController.updateAll();
});
},
child: SizedBox(
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/share.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc3,
),
),
)
],
);
}
if (sleepReportController.model.type == 2) {
//周报
}
if (sleepReportController.model.type == 3) {
//月报
}
return Container();
Padding(
padding: EdgeInsets.symmetric(horizontal: 26.rpx),
child: Text(
displayText,
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: themeController.currentColor.sc3,
),
),
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: themeController.currentColor.sc3,
padding: EdgeInsets.all(10.rpx),
borderRadius: 8.rpx,
onTap: onRightArrowTap,
child: SizedBox(
width: 9.rpx,
height: 14.rpx,
child: SvgPicture.asset(
'assets/img/icon/arrow_right.svg',
color: themeController.currentColor.sc3,
),
),
),
],
),
ClickableContainer(
backgroundColor: Colors.transparent,
highlightColor: themeController.currentColor.sc3,
padding: EdgeInsets.zero,
borderRadius: 0,
onTap: () {
showSleepCalendarBottomSheet(
type: sleepReportController.model.type,
timestamp: selectedDate.millisecondsSinceEpoch,
context: context,
onDateSelected: (newDate) {
sleepReportController.selectedDate.value = newDate;
calendarController.selectedDate.value = newDate;
sleepReportController.updateAll();
calendarController.updateAll();
},
);
},
child: SizedBox(
width: 28.rpx,
height: 28.rpx,
child: SvgPicture.asset(
'assets/img/icon/share.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc3,
),
),
),
],
);
}
}