更新快检功能
This commit is contained in:
@@ -1,3 +1,307 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_svg/flutter_svg.dart';
|
||||
// import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
// import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
// import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
// import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
// import 'package:vbvs_app/enum/APPPackageType.dart';
|
||||
// import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
|
||||
// class HorizontalBarChart extends StatelessWidget {
|
||||
// final List<Map<String, dynamic>> showLabel;
|
||||
// final bool showPercent;
|
||||
|
||||
// const HorizontalBarChart({
|
||||
// super.key,
|
||||
// required this.showLabel,
|
||||
// this.showPercent = true,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final data = showLabel.map((item) {
|
||||
// return BarData(
|
||||
// label: item['name'],
|
||||
// value: (item['percent'] ?? 0).toDouble(),
|
||||
// color: item['color'] ?? Colors.grey,
|
||||
// explain: item['explain'] ?? '',
|
||||
// );
|
||||
// }).toList();
|
||||
|
||||
// final double labelWidth = (MediaQuery.of(context).size.width * 0.5).clamp(
|
||||
// MediaQuery.of(context).size.width * 0.08,
|
||||
// MediaQuery.of(context).size.width * 0.22);
|
||||
|
||||
// final double barHeight = 24.0.rpx;
|
||||
// final double barSpacing = 40.0.rpx;
|
||||
// final totalHeight = data.length * (barHeight + barSpacing) + 30.rpx;
|
||||
|
||||
// return SizedBox(
|
||||
// height: totalHeight,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// // 左侧标签列
|
||||
// SizedBox(
|
||||
// width: labelWidth,
|
||||
// child: ListView.builder(
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// itemCount: data.length,
|
||||
// itemBuilder: (context, index) {
|
||||
// final bar = data[index];
|
||||
// return Container(
|
||||
// height: barHeight + barSpacing,
|
||||
// alignment: Alignment.centerRight,
|
||||
// child: LabelWithSvg(
|
||||
// label: bar.label,
|
||||
// explain: bar.explain,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 16.rpx,
|
||||
// ),
|
||||
// // 右侧柱状图区
|
||||
// Expanded(
|
||||
// child: Stack(
|
||||
// children: [
|
||||
// // 网格线背景层
|
||||
// CustomPaint(
|
||||
// size: Size(double.infinity, totalHeight),
|
||||
// painter: GridPainter(totalHeight: totalHeight),
|
||||
// ),
|
||||
|
||||
// // 柱状图列表
|
||||
// ListView.builder(
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// itemCount: data.length,
|
||||
// itemBuilder: (context, index) {
|
||||
// final bar = data[index];
|
||||
// return SizedBox(
|
||||
// height: barHeight + barSpacing,
|
||||
// child: CustomPaint(
|
||||
// painter: SingleBarPainter(
|
||||
// value: bar.value,
|
||||
// color: bar.color,
|
||||
// showPercent: showPercent,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class BarData {
|
||||
// final String label;
|
||||
// final double value;
|
||||
// final Color color;
|
||||
// final String explain;
|
||||
|
||||
// BarData({
|
||||
// required this.label,
|
||||
// required this.value,
|
||||
// required this.color,
|
||||
// required this.explain,
|
||||
// });
|
||||
// }
|
||||
|
||||
// class LabelWithSvg extends StatelessWidget {
|
||||
// final String label;
|
||||
// final String explain;
|
||||
|
||||
// const LabelWithSvg({
|
||||
// super.key,
|
||||
// required this.label,
|
||||
// required this.explain,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final textStyle =
|
||||
// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
|
||||
|
||||
// return Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// children: [
|
||||
// Flexible(
|
||||
// child: Text(
|
||||
// label,
|
||||
// style: textStyle,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// ),
|
||||
// // SizedBox(width: 8.rpx),
|
||||
// ClickableContainer(
|
||||
// backgroundColor: Colors.transparent,
|
||||
// highlightColor: Colors.white,
|
||||
// padding:
|
||||
// EdgeInsetsDirectional.fromSTEB(14.rpx, 14.rpx, 14.rpx, 14.rpx),
|
||||
// borderRadius: 0.rpx,
|
||||
// onTap: () {
|
||||
// // Get.toNamed("/deviceShareListPage", arguments: explain);
|
||||
|
||||
// if (AppConstants().ent_type == APPPackageType.MHT.code) {
|
||||
// showTipDialog(
|
||||
// context,
|
||||
// Container(
|
||||
// child: Text(
|
||||
// explain,
|
||||
// style: TextStyle(fontSize: 26.rpx, color: Colors.black),
|
||||
// ),
|
||||
// ),
|
||||
// backgroundColor: Color(0xFFFFFFFF),
|
||||
// colors: [
|
||||
// Color(0XFF1592AA),
|
||||
// Color(0xFF0C83A7),
|
||||
// Color(0xFF006FA3)
|
||||
// ],
|
||||
// );
|
||||
// } else {
|
||||
// showTipDialog(
|
||||
// context,
|
||||
// Container(
|
||||
// child: Text(
|
||||
// explain,
|
||||
// style: TextStyle(
|
||||
// fontSize: 26.rpx,
|
||||
// color: themeController.currentColor.sc3),
|
||||
// ),
|
||||
// ),
|
||||
// backgroundColor: themeController.currentColor.sc17,
|
||||
// colors: AppConstants().thNormalButton,
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// child: SizedBox(
|
||||
// width: 17.rpx,
|
||||
// height: 17.rpx,
|
||||
// child: SvgPicture.asset(
|
||||
// 'assets/img/icon/explain.svg',
|
||||
// fit: BoxFit.cover,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class SingleBarPainter extends CustomPainter {
|
||||
// final double value;
|
||||
// final Color color;
|
||||
// final bool showPercent;
|
||||
// final double maxValue = 100;
|
||||
|
||||
// SingleBarPainter({
|
||||
// required this.value,
|
||||
// required this.color,
|
||||
// required this.showPercent,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// void paint(Canvas canvas, Size size) {
|
||||
// final barHeight = 24.0.rpx;
|
||||
// final rightPadding = 20.0.rpx;
|
||||
// final chartWidth = size.width - rightPadding;
|
||||
|
||||
// final left = 0.0;
|
||||
// final right = left + (value / maxValue) * chartWidth;
|
||||
// final rect = Rect.fromLTWH(
|
||||
// left, (size.height - barHeight) / 2, right - left, barHeight);
|
||||
// final paint = Paint()..color = color;
|
||||
// canvas.drawRect(rect, paint);
|
||||
|
||||
// if (showPercent) {
|
||||
// final textStyle =
|
||||
// TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
|
||||
// final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
// textPainter.text =
|
||||
// TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle);
|
||||
// textPainter.layout();
|
||||
// canvas.save();
|
||||
// canvas.clipRect(Rect.fromLTWH(
|
||||
// left, (size.height - barHeight) / 2, chartWidth, barHeight));
|
||||
// textPainter.paint(
|
||||
// canvas,
|
||||
// Offset(right + 4.0.rpx, (size.height - textPainter.height) / 2),
|
||||
// );
|
||||
// canvas.restore();
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
// }
|
||||
|
||||
// class GridPainter extends CustomPainter {
|
||||
// final double totalHeight;
|
||||
// final int gridCount = 5;
|
||||
// final double maxValue = 100;
|
||||
// final double bottomPadding = 30.0.rpx;
|
||||
|
||||
// GridPainter({required this.totalHeight});
|
||||
|
||||
// @override
|
||||
// void paint(Canvas canvas, Size size) {
|
||||
// final Paint gridPaint = Paint()
|
||||
// ..color = Colors.grey.withOpacity(0.3)
|
||||
// ..strokeWidth = 1.0.rpx;
|
||||
|
||||
// final double rightPadding = 20.0.rpx;
|
||||
// final double chartWidth = size.width - rightPadding;
|
||||
// final double chartHeight = totalHeight - bottomPadding;
|
||||
|
||||
// for (int i = 0; i <= gridCount; i++) {
|
||||
// double dx = (i / gridCount) * chartWidth;
|
||||
// _drawDashedLine(
|
||||
// canvas, Offset(dx, 0), Offset(dx, chartHeight), gridPaint);
|
||||
|
||||
// final percent = (i / gridCount) * maxValue;
|
||||
// final TextPainter textPainter = TextPainter(
|
||||
// textDirection: TextDirection.ltr,
|
||||
// text: TextSpan(
|
||||
// text: '${percent.toInt()}',
|
||||
// style: TextStyle(color: Colors.grey, fontSize: 18.rpx),
|
||||
// ),
|
||||
// );
|
||||
// textPainter.layout();
|
||||
// textPainter.paint(
|
||||
// canvas, Offset(dx - textPainter.width / 2, chartHeight + 4.0.rpx));
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
|
||||
// const double dashWidth = 6.0;
|
||||
// const double dashSpace = 4.0;
|
||||
|
||||
// final double totalHeight = (end.dy - start.dy).abs();
|
||||
// double currentY = start.dy;
|
||||
|
||||
// while (currentY < end.dy) {
|
||||
// final double nextY = currentY + dashWidth;
|
||||
// canvas.drawLine(
|
||||
// Offset(start.dx, currentY),
|
||||
// Offset(start.dx, nextY > end.dy ? end.dy : nextY),
|
||||
// paint,
|
||||
// );
|
||||
// currentY = nextY + dashSpace;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
// }
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
@@ -10,11 +314,13 @@ import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
class HorizontalBarChart extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> showLabel;
|
||||
final bool showPercent;
|
||||
final bool showRangeBackground; // 新增参数,控制是否显示区间背景
|
||||
|
||||
const HorizontalBarChart({
|
||||
super.key,
|
||||
required this.showLabel,
|
||||
this.showPercent = true,
|
||||
this.showRangeBackground = false, // 默认为false
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -66,10 +372,13 @@ class HorizontalBarChart extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
// 网格线背景层
|
||||
// 背景层 - 包含网格线和区间背景
|
||||
CustomPaint(
|
||||
size: Size(double.infinity, totalHeight),
|
||||
painter: GridPainter(totalHeight: totalHeight),
|
||||
painter: GridPainter(
|
||||
totalHeight: totalHeight,
|
||||
showRangeBackground: showRangeBackground,
|
||||
),
|
||||
),
|
||||
|
||||
// 柱状图列表
|
||||
@@ -240,7 +549,12 @@ class SingleBarPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
if (oldDelegate is SingleBarPainter) {
|
||||
return oldDelegate.value != value || oldDelegate.color != color;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class GridPainter extends CustomPainter {
|
||||
@@ -248,19 +562,58 @@ class GridPainter extends CustomPainter {
|
||||
final int gridCount = 5;
|
||||
final double maxValue = 100;
|
||||
final double bottomPadding = 30.0.rpx;
|
||||
final bool showRangeBackground; // 新增参数
|
||||
|
||||
GridPainter({required this.totalHeight});
|
||||
GridPainter({
|
||||
required this.totalHeight,
|
||||
this.showRangeBackground = false,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint gridPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.3)
|
||||
..strokeWidth = 1.0.rpx;
|
||||
|
||||
final double rightPadding = 20.0.rpx;
|
||||
final double chartWidth = size.width - rightPadding;
|
||||
final double chartHeight = totalHeight - bottomPadding;
|
||||
|
||||
// 如果需要显示区间背景
|
||||
if (showRangeBackground) {
|
||||
final double segmentWidth = chartWidth / gridCount;
|
||||
|
||||
// 计算0-60对应的网格区间(60对应3格,因为每格20)
|
||||
// 0-60: 第0-3格(0,20,40,60)
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final rect = Rect.fromLTWH(
|
||||
i * segmentWidth,
|
||||
0,
|
||||
segmentWidth,
|
||||
chartHeight,
|
||||
);
|
||||
final paint = Paint()
|
||||
..color = themeController.currentColor.sc1.withOpacity(0.2)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
|
||||
// 60-100: 第3-5格(60,80,100)
|
||||
for (int i = 3; i < 5; i++) {
|
||||
final rect = Rect.fromLTWH(
|
||||
i * segmentWidth,
|
||||
0,
|
||||
segmentWidth,
|
||||
chartHeight,
|
||||
);
|
||||
final paint = Paint()
|
||||
..color = themeController.currentColor.sc9.withOpacity(0.2)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制网格线
|
||||
final Paint gridPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.3)
|
||||
..strokeWidth = 1.0.rpx;
|
||||
|
||||
for (int i = 0; i <= gridCount; i++) {
|
||||
double dx = (i / gridCount) * chartWidth;
|
||||
_drawDashedLine(
|
||||
@@ -299,5 +652,10 @@ class GridPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
if (oldDelegate is GridPainter) {
|
||||
return oldDelegate.showRangeBackground != showRangeBackground;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
977
lib/pages/sleep_report/chart/QcTimeSeriesChart.dart
Normal file
977
lib/pages/sleep_report/chart/QcTimeSeriesChart.dart
Normal file
@@ -0,0 +1,977 @@
|
||||
// import 'package:ef/ef.dart';
|
||||
// import 'package:fl_chart/fl_chart.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_svg/svg.dart';
|
||||
// import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
// import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
// import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
// class QcTimeSeriesPoint {
|
||||
// final double value;
|
||||
// QcTimeSeriesPoint(this.value);
|
||||
// }
|
||||
|
||||
// class QcTimeSeriesChart extends StatelessWidget {
|
||||
// final List<QcTimeSeriesPoint> dataPoints;
|
||||
// final double yMin;
|
||||
// final double yMax;
|
||||
// final int xSegmentCount;
|
||||
// final double? baseValue; // 基准值,可选
|
||||
// final String? baseLabel; // 基准值标签,可选
|
||||
|
||||
// const QcTimeSeriesChart({
|
||||
// Key? key,
|
||||
// required this.yMin,
|
||||
// required this.yMax,
|
||||
// required this.dataPoints,
|
||||
// this.xSegmentCount = 11,
|
||||
// this.baseValue,
|
||||
// this.baseLabel,
|
||||
// }) : super(key: key);
|
||||
|
||||
// int get _dataPointCount => dataPoints.length;
|
||||
|
||||
// List<double> _generateYAxisTicks() {
|
||||
// if (yMin >= yMax) {
|
||||
// return [0, 20, 40, 60, 80, 100];
|
||||
// }
|
||||
|
||||
// double step = (yMax - yMin) / 5;
|
||||
// List<double> ticks = [];
|
||||
|
||||
// for (int i = 0; i <= 5; i++) {
|
||||
// ticks.add(yMin + (step * i));
|
||||
// }
|
||||
|
||||
// return ticks;
|
||||
// }
|
||||
|
||||
// // 计算所有刻度位置对应的索引
|
||||
// List<int> _getTickIndices() {
|
||||
// List<int> tickIndices = [];
|
||||
|
||||
// // 确保至少显示两个刻度(起点和终点)
|
||||
// if (_dataPointCount <= 1) {
|
||||
// return [0];
|
||||
// }
|
||||
|
||||
// // 计算合理的刻度间隔
|
||||
// double step = _dataPointCount / (xSegmentCount - 1);
|
||||
|
||||
// for (int i = 0; i < xSegmentCount; i++) {
|
||||
// int index = (i * step).round();
|
||||
// // 确保索引在有效范围内
|
||||
// index = index.clamp(0, _dataPointCount - 1);
|
||||
|
||||
// // 避免重复的索引
|
||||
// if (tickIndices.isEmpty || index != tickIndices.last) {
|
||||
// tickIndices.add(index);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 确保最后一个点是最后一个数据点
|
||||
// if (tickIndices.last != _dataPointCount - 1) {
|
||||
// tickIndices.add(_dataPointCount - 1);
|
||||
// }
|
||||
|
||||
// return tickIndices;
|
||||
// }
|
||||
|
||||
// // 查找最大值和最小值的索引和值
|
||||
// Map<String, dynamic> _findMinMax() {
|
||||
// if (dataPoints.isEmpty)
|
||||
// return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0};
|
||||
|
||||
// int minIndex = -1;
|
||||
// int maxIndex = -1;
|
||||
// double minValue = double.infinity;
|
||||
// double maxValue = -double.infinity;
|
||||
|
||||
// for (int i = 0; i < dataPoints.length; i++) {
|
||||
// var point = dataPoints[i];
|
||||
// if (point.value != -1) {
|
||||
// // 跳过无效数据
|
||||
// if (point.value < minValue) {
|
||||
// minValue = point.value;
|
||||
// minIndex = i;
|
||||
// }
|
||||
// if (point.value > maxValue) {
|
||||
// maxValue = point.value;
|
||||
// maxIndex = i;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// 'minIndex': minIndex,
|
||||
// 'maxIndex': maxIndex,
|
||||
// 'minValue': minValue,
|
||||
// 'maxValue': maxValue
|
||||
// };
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// // final yTicks = _generateYAxisTicks();
|
||||
// final double xMax = _dataPointCount.toDouble();
|
||||
// final double yRange = yMax - yMin;
|
||||
// final double yInterval = yRange > 0 ? yRange / 5 : 20.0;
|
||||
|
||||
// final tickIndices = _getTickIndices();
|
||||
// final minMaxData = _findMinMax();
|
||||
|
||||
// // 将数据点分割成多个连续段
|
||||
// List<List<FlSpot>> lineSegments = [];
|
||||
// List<FlSpot> currentSegment = [];
|
||||
|
||||
// for (int i = 0; i < dataPoints.length; i++) {
|
||||
// var point = dataPoints[i];
|
||||
// if (point.value != -1) {
|
||||
// currentSegment.add(FlSpot(
|
||||
// (i + 1).toDouble(),
|
||||
// point.value,
|
||||
// ));
|
||||
// } else if (currentSegment.isNotEmpty) {
|
||||
// lineSegments.add(currentSegment);
|
||||
// currentSegment = [];
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (currentSegment.isNotEmpty) {
|
||||
// lineSegments.add(currentSegment);
|
||||
// }
|
||||
|
||||
// // 创建渐变填充的线图数据
|
||||
// List<LineChartBarData> lineBarsData = [];
|
||||
// for (var segment in lineSegments) {
|
||||
// if (segment.isEmpty) continue;
|
||||
|
||||
// lineBarsData.add(
|
||||
// LineChartBarData(
|
||||
// spots: segment,
|
||||
// isCurved: false,
|
||||
// color: themeController.currentColor.sc2,
|
||||
// barWidth: 2,
|
||||
// dotData: FlDotData(show: false),
|
||||
// preventCurveOverShooting: true,
|
||||
// belowBarData: BarAreaData(
|
||||
// show: true,
|
||||
// gradient: LinearGradient(
|
||||
// begin: Alignment.topCenter,
|
||||
// end: Alignment.bottomCenter,
|
||||
// colors: [
|
||||
// themeController.currentColor.sc2.withOpacity(0.3),
|
||||
// themeController.currentColor.sc2.withOpacity(0.1),
|
||||
// Colors.transparent,
|
||||
// ],
|
||||
// stops: const [0.0, 1, 1.0],
|
||||
// ),
|
||||
// applyCutOffY: true,
|
||||
// cutOffY: 0,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// // 创建刻度点的数据(绿色小球)
|
||||
// List<FlSpot> tickSpots = [];
|
||||
// for (int index in tickIndices) {
|
||||
// if (index >= 0 && index < dataPoints.length) {
|
||||
// var point = dataPoints[index];
|
||||
// if (point.value != -1) {
|
||||
// tickSpots.add(FlSpot(
|
||||
// (index + 1).toDouble(),
|
||||
// point.value,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 准备水平线列表
|
||||
// List<HorizontalLine> horizontalLines = [
|
||||
// HorizontalLine(
|
||||
// y: 0,
|
||||
// color: themeController.currentColor.sc4,
|
||||
// strokeWidth: 1.rpx,
|
||||
// ),
|
||||
// ];
|
||||
|
||||
// // 如果有基准值,添加基准线
|
||||
// if (baseValue != null) {
|
||||
// horizontalLines.add(
|
||||
// HorizontalLine(
|
||||
// y: baseValue!,
|
||||
// color: themeController.currentColor.sc9,
|
||||
// strokeWidth: 2.rpx,
|
||||
// dashArray: [8, 4], // 虚线样式
|
||||
// label: HorizontalLineLabel(
|
||||
// show: true,
|
||||
// alignment: Alignment.topRight,
|
||||
// labelResolver: (line) =>
|
||||
// "基准".tr + '${baseValue!.toStringAsFixed(0)}',
|
||||
// style: TextStyle(
|
||||
// color: themeController.currentColor.sc9,
|
||||
// fontSize: 18.rpx,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// return AspectRatio(
|
||||
// aspectRatio: 2,
|
||||
// child: LayoutBuilder(
|
||||
// builder: (context, constraints) {
|
||||
// return Stack(
|
||||
// children: [
|
||||
// LineChart(
|
||||
// LineChartData(
|
||||
// minX: 1,
|
||||
// maxX: xMax + 0.5,
|
||||
// minY: yMin - 2,
|
||||
// maxY: yMax + 2,
|
||||
// gridData: FlGridData(show: false),
|
||||
// extraLinesData: ExtraLinesData(
|
||||
// horizontalLines: horizontalLines,
|
||||
// ),
|
||||
// titlesData: FlTitlesData(
|
||||
// bottomTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(
|
||||
// showTitles: true,
|
||||
// reservedSize: 30,
|
||||
// interval: 1, // 让 fl_chart 自动计算合适的间隔
|
||||
// getTitlesWidget: (value, meta) {
|
||||
// // 只显示我们在 tickIndices 中定义的刻度
|
||||
// int index = value.round() - 1;
|
||||
// if (index >= 0 &&
|
||||
// index < _dataPointCount &&
|
||||
// tickIndices.contains(index)) {
|
||||
// String label;
|
||||
// if (index == 0) {
|
||||
// label = '0';
|
||||
// } else if (index == _dataPointCount - 1) {
|
||||
// label = '${_dataPointCount}';
|
||||
// } else {
|
||||
// label = '${index + 1}';
|
||||
// }
|
||||
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(top: 8.0),
|
||||
// child: Text(
|
||||
// label,
|
||||
// style: TextStyle(
|
||||
// color: themeController.currentColor.sc4,
|
||||
// fontSize: 14.rpx,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// return const SizedBox.shrink();
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// leftTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(
|
||||
// showTitles: true,
|
||||
// reservedSize: 60.rpx,
|
||||
// interval: yInterval,
|
||||
// getTitlesWidget: (value, meta) {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(right: 8.0),
|
||||
// child: Text(
|
||||
// value.toStringAsFixed(0),
|
||||
// style: TextStyle(
|
||||
// color: themeController.currentColor.sc4,
|
||||
// fontSize: 16.rpx,
|
||||
// ),
|
||||
// textAlign: TextAlign.right,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// rightTitles:
|
||||
// AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
// topTitles:
|
||||
// AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
// ),
|
||||
// borderData: FlBorderData(
|
||||
// show: true,
|
||||
// border: Border(
|
||||
// bottom: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
// left: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
// right: BorderSide.none,
|
||||
// top: BorderSide.none,
|
||||
// ),
|
||||
// ),
|
||||
// lineBarsData: [
|
||||
// ...lineBarsData,
|
||||
// // 添加绿色小球的线图数据
|
||||
// LineChartBarData(
|
||||
// spots: tickSpots,
|
||||
// isCurved: false,
|
||||
// color: Colors.transparent,
|
||||
// barWidth: 0,
|
||||
// dotData: FlDotData(
|
||||
// show: true,
|
||||
// getDotPainter: (spot, percent, barData, index) {
|
||||
// return FlDotCirclePainter(
|
||||
// radius: 4.rpx,
|
||||
// color: stringToColor("#5DD8C9"),
|
||||
// strokeWidth: 1,
|
||||
// strokeColor: stringToColor("#00C1AA"),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// preventCurveOverShooting: true,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// // 使用 Positioned 来绘制最大值和最小值的标签
|
||||
// if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1)
|
||||
// _buildMinMaxLabels(
|
||||
// context, constraints, minMaxData, xMax, yMin, yMax),
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// Widget _buildMinMaxLabels(
|
||||
// BuildContext context,
|
||||
// BoxConstraints constraints,
|
||||
// Map<String, dynamic> minMaxData,
|
||||
// double xMax,
|
||||
// double yMin,
|
||||
// double yMax,
|
||||
// ) {
|
||||
// // 获取图表区域的实际绘制区域
|
||||
// // fl_chart 默认会有一些内边距,我们需要估算这些内边距
|
||||
// double leftPadding = 60.rpx; // 左侧留白,用于Y轴标签
|
||||
// double rightPadding = 10.rpx; // 右侧留白
|
||||
// double topPadding = 10.rpx; // 顶部留白
|
||||
// double bottomPadding = 30.rpx; // 底部留白,用于X轴标签
|
||||
|
||||
// double chartWidth = constraints.maxWidth - leftPadding - rightPadding;
|
||||
// double chartHeight = constraints.maxHeight - topPadding - bottomPadding;
|
||||
|
||||
// // X轴范围:1 到 xMax
|
||||
// // Y轴范围:yMin - 2 到 yMax + 2
|
||||
// double xMin = 1;
|
||||
// double xMaxValue = xMax;
|
||||
// double yMinValue = yMin - 2;
|
||||
// double yMaxValue = yMax + 2;
|
||||
|
||||
// double xRange = xMaxValue - xMin;
|
||||
// double yRange = yMaxValue - yMinValue;
|
||||
|
||||
// List<Widget> labels = [];
|
||||
|
||||
// // 添加最小值标签
|
||||
// if (minMaxData['minIndex'] != -1) {
|
||||
// double minX = (minMaxData['minIndex'] + 1).toDouble();
|
||||
// double minY = minMaxData['minValue'];
|
||||
|
||||
// // 计算在图表中的相对位置 (0-1)
|
||||
// double relativeX = (minX - xMin) / xRange;
|
||||
// double relativeY = (minY - yMinValue) / yRange;
|
||||
|
||||
// // 转换为像素位置
|
||||
// double left = leftPadding + (relativeX * chartWidth);
|
||||
// double top = topPadding + ((1 - relativeY) * chartHeight);
|
||||
|
||||
// // 标签尺寸
|
||||
// double labelWidth = 38.rpx;
|
||||
// double labelHeight = 50.rpx;
|
||||
|
||||
// labels.add(
|
||||
// Positioned(
|
||||
// left: left - labelWidth / 2, // 水平居中
|
||||
// top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距
|
||||
// child: SizedBox(
|
||||
// width: labelWidth,
|
||||
// height: labelHeight,
|
||||
// child: Stack(
|
||||
// alignment: Alignment.center,
|
||||
// children: [
|
||||
// // SVG图片作为背景
|
||||
// SvgPicture.asset(
|
||||
// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径
|
||||
// // width: 28.rpx,
|
||||
// // height: 40.rpx,
|
||||
// fit: BoxFit.contain,
|
||||
// color: stringToColor("#d69dd2"),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离
|
||||
// child: Text(
|
||||
// '${minMaxData['minValue'].toStringAsFixed(0)}',
|
||||
// style: TextStyle(
|
||||
// color: Colors.white,
|
||||
// fontSize: AppConstants().smaller_text_fontSize,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// // 添加最大值标签
|
||||
// if (minMaxData['maxIndex'] != -1 &&
|
||||
// minMaxData['maxIndex'] != minMaxData['minIndex']) {
|
||||
// double maxX = (minMaxData['maxIndex'] + 1).toDouble();
|
||||
// double maxY = minMaxData['maxValue'];
|
||||
|
||||
// // 计算在图表中的相对位置 (0-1)
|
||||
// double relativeX = (maxX - xMin) / xRange;
|
||||
// double relativeY = (maxY - yMinValue) / yRange;
|
||||
|
||||
// // 转换为像素位置
|
||||
// double left = leftPadding + (relativeX * chartWidth);
|
||||
// double top = topPadding + ((1 - relativeY) * chartHeight);
|
||||
|
||||
// // 标签尺寸
|
||||
// double labelWidth = 38.rpx;
|
||||
// double labelHeight = 50.rpx;
|
||||
|
||||
// labels.add(
|
||||
// Positioned(
|
||||
// left: left - labelWidth / 2, // 水平居中
|
||||
// top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距
|
||||
// child: SizedBox(
|
||||
// width: labelWidth,
|
||||
// height: labelHeight,
|
||||
// child: Stack(
|
||||
// alignment: Alignment.center,
|
||||
// children: [
|
||||
// // SVG图片作为背景
|
||||
// SvgPicture.asset(
|
||||
// 'assets/img/icon/location.svg', // 替换为你的SVG图片路径
|
||||
// // width: 28.rpx,
|
||||
// // height: 40.rpx,
|
||||
// fit: BoxFit.contain,
|
||||
// color: stringToColor("#FF9F66"),
|
||||
// ),
|
||||
// // 文字覆盖在SVG上
|
||||
// // Text(
|
||||
// // '${minMaxData['maxValue'].toStringAsFixed(0)}',
|
||||
// // style: TextStyle(
|
||||
// // color: Colors.white,
|
||||
// // fontSize: AppConstants().smaller_text_fontSize,
|
||||
// // // fontWeight: FontWeight.bold,
|
||||
// // ),
|
||||
// // ),
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(bottom: 12.rpx), // 调整这个值来控制向上移动的距离
|
||||
// child: Text(
|
||||
// '${minMaxData['maxValue'].toStringAsFixed(0)}',
|
||||
// style: TextStyle(
|
||||
// color: Colors.white,
|
||||
// fontSize: AppConstants().smaller_text_fontSize,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// return Stack(
|
||||
// children: labels,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class QcTimeSeriesPoint {
|
||||
final double value;
|
||||
QcTimeSeriesPoint(this.value);
|
||||
}
|
||||
|
||||
class QcTimeSeriesChart extends StatelessWidget {
|
||||
final List<QcTimeSeriesPoint> dataPoints;
|
||||
final double yMin;
|
||||
final double yMax; // 注意:这个值在使用时会自动加 padding
|
||||
final int xSegmentCount;
|
||||
final double? baseValue; // 基准值,可选
|
||||
final String? baseLabel; // 基准值标签,可选
|
||||
final double yAxisPadding; // Y轴顶部留白,默认为20
|
||||
|
||||
const QcTimeSeriesChart({
|
||||
Key? key,
|
||||
required this.yMin,
|
||||
required this.yMax,
|
||||
required this.dataPoints,
|
||||
this.xSegmentCount = 11,
|
||||
this.baseValue,
|
||||
this.baseLabel,
|
||||
this.yAxisPadding = 20.0, // 默认为20
|
||||
}) : super(key: key);
|
||||
|
||||
// 添加一个 getter 来获取实际使用的 yMax(自动加 padding)
|
||||
double get _actualYMax => yMax + yAxisPadding;
|
||||
|
||||
int get _dataPointCount => dataPoints.length;
|
||||
|
||||
List<double> _generateYAxisTicks() {
|
||||
if (yMin >= _actualYMax) {
|
||||
return [0, 20, 40, 60, 80, 100];
|
||||
}
|
||||
|
||||
double step = (_actualYMax - yMin) / 5;
|
||||
List<double> ticks = [];
|
||||
|
||||
for (int i = 0; i <= 5; i++) {
|
||||
ticks.add(yMin + (step * i));
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
// 计算所有刻度位置对应的索引
|
||||
List<int> _getTickIndices() {
|
||||
List<int> tickIndices = [];
|
||||
|
||||
// 确保至少显示两个刻度(起点和终点)
|
||||
if (_dataPointCount <= 1) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
// 计算合理的刻度间隔
|
||||
double step = _dataPointCount / (xSegmentCount - 1);
|
||||
|
||||
for (int i = 0; i < xSegmentCount; i++) {
|
||||
int index = (i * step).round();
|
||||
// 确保索引在有效范围内
|
||||
index = index.clamp(0, _dataPointCount - 1);
|
||||
|
||||
// 避免重复的索引
|
||||
if (tickIndices.isEmpty || index != tickIndices.last) {
|
||||
tickIndices.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保最后一个点是最后一个数据点
|
||||
if (tickIndices.last != _dataPointCount - 1) {
|
||||
tickIndices.add(_dataPointCount - 1);
|
||||
}
|
||||
|
||||
return tickIndices;
|
||||
}
|
||||
|
||||
// 查找最大值和最小值的索引和值
|
||||
Map<String, dynamic> _findMinMax() {
|
||||
if (dataPoints.isEmpty)
|
||||
return {'minIndex': -1, 'maxIndex': -1, 'minValue': 0, 'maxValue': 0};
|
||||
|
||||
int minIndex = -1;
|
||||
int maxIndex = -1;
|
||||
double minValue = double.infinity;
|
||||
double maxValue = -double.infinity;
|
||||
|
||||
for (int i = 0; i < dataPoints.length; i++) {
|
||||
var point = dataPoints[i];
|
||||
if (point.value != -1) {
|
||||
// 跳过无效数据
|
||||
if (point.value < minValue) {
|
||||
minValue = point.value;
|
||||
minIndex = i;
|
||||
}
|
||||
if (point.value > maxValue) {
|
||||
maxValue = point.value;
|
||||
maxIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'minIndex': minIndex,
|
||||
'maxIndex': maxIndex,
|
||||
'minValue': minValue,
|
||||
'maxValue': maxValue
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// final yTicks = _generateYAxisTicks();
|
||||
final double xMax = _dataPointCount.toDouble();
|
||||
final double yRange = _actualYMax - yMin;
|
||||
final double yInterval = yRange > 0 ? yRange / 5 : 20.0;
|
||||
|
||||
final tickIndices = _getTickIndices();
|
||||
final minMaxData = _findMinMax();
|
||||
|
||||
// 将数据点分割成多个连续段
|
||||
List<List<FlSpot>> lineSegments = [];
|
||||
List<FlSpot> currentSegment = [];
|
||||
|
||||
for (int i = 0; i < dataPoints.length; i++) {
|
||||
var point = dataPoints[i];
|
||||
if (point.value != -1) {
|
||||
currentSegment.add(FlSpot(
|
||||
(i + 1).toDouble(),
|
||||
point.value,
|
||||
));
|
||||
} else if (currentSegment.isNotEmpty) {
|
||||
lineSegments.add(currentSegment);
|
||||
currentSegment = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSegment.isNotEmpty) {
|
||||
lineSegments.add(currentSegment);
|
||||
}
|
||||
|
||||
// 创建渐变填充的线图数据
|
||||
List<LineChartBarData> lineBarsData = [];
|
||||
for (var segment in lineSegments) {
|
||||
if (segment.isEmpty) continue;
|
||||
|
||||
lineBarsData.add(
|
||||
LineChartBarData(
|
||||
spots: segment,
|
||||
isCurved: false,
|
||||
color: themeController.currentColor.sc2,
|
||||
barWidth: 2,
|
||||
dotData: FlDotData(show: false),
|
||||
preventCurveOverShooting: true,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
themeController.currentColor.sc2.withOpacity(0.3),
|
||||
themeController.currentColor.sc2.withOpacity(0.1),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0.0, 1, 1.0],
|
||||
),
|
||||
applyCutOffY: true,
|
||||
cutOffY: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 创建刻度点的数据(绿色小球)
|
||||
List<FlSpot> tickSpots = [];
|
||||
for (int index in tickIndices) {
|
||||
if (index >= 0 && index < dataPoints.length) {
|
||||
var point = dataPoints[index];
|
||||
if (point.value != -1) {
|
||||
tickSpots.add(FlSpot(
|
||||
(index + 1).toDouble(),
|
||||
point.value,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 准备水平线列表
|
||||
List<HorizontalLine> horizontalLines = [
|
||||
HorizontalLine(
|
||||
y: 0,
|
||||
color: themeController.currentColor.sc4,
|
||||
strokeWidth: 1.rpx,
|
||||
),
|
||||
];
|
||||
|
||||
// 如果有基准值,添加基准线
|
||||
if (baseValue != null) {
|
||||
horizontalLines.add(
|
||||
HorizontalLine(
|
||||
y: baseValue!,
|
||||
color: themeController.currentColor.sc9,
|
||||
strokeWidth: 2.rpx,
|
||||
dashArray: [8, 4], // 虚线样式
|
||||
label: HorizontalLineLabel(
|
||||
show: true,
|
||||
alignment: Alignment.topRight,
|
||||
labelResolver: (line) =>
|
||||
"基准".tr + '${baseValue!.toStringAsFixed(0)}',
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc9,
|
||||
fontSize: 18.rpx,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: 2,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
LineChart(
|
||||
LineChartData(
|
||||
minX: 1,
|
||||
maxX: xMax + 0.5,
|
||||
minY: yMin - 2,
|
||||
maxY: _actualYMax + 2,
|
||||
gridData: FlGridData(show: false),
|
||||
extraLinesData: ExtraLinesData(
|
||||
horizontalLines: horizontalLines,
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
interval: 1, // 让 fl_chart 自动计算合适的间隔
|
||||
getTitlesWidget: (value, meta) {
|
||||
// 只显示我们在 tickIndices 中定义的刻度
|
||||
int index = value.round() - 1;
|
||||
if (index >= 0 &&
|
||||
index < _dataPointCount &&
|
||||
tickIndices.contains(index)) {
|
||||
String label;
|
||||
if (index == 0) {
|
||||
label = '0';
|
||||
} else if (index == _dataPointCount - 1) {
|
||||
label = '${_dataPointCount}';
|
||||
} else {
|
||||
label = '${index + 1}';
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc4,
|
||||
fontSize: 14.rpx,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 60.rpx,
|
||||
interval: yInterval,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(
|
||||
value.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc4,
|
||||
fontSize: 16.rpx,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles:
|
||||
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles:
|
||||
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
left: BorderSide(color: Colors.grey.withOpacity(0.3)),
|
||||
right: BorderSide.none,
|
||||
top: BorderSide.none,
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
...lineBarsData,
|
||||
// 添加绿色小球的线图数据
|
||||
LineChartBarData(
|
||||
spots: tickSpots,
|
||||
isCurved: false,
|
||||
color: Colors.transparent,
|
||||
barWidth: 0,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, percent, barData, index) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 4.rpx,
|
||||
color: stringToColor("#5DD8C9"),
|
||||
strokeWidth: 1,
|
||||
strokeColor: stringToColor("#00C1AA"),
|
||||
);
|
||||
},
|
||||
),
|
||||
preventCurveOverShooting: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 使用 Positioned 来绘制最大值和最小值的标签
|
||||
if (minMaxData['minIndex'] != -1 || minMaxData['maxIndex'] != -1)
|
||||
_buildMinMaxLabels(
|
||||
context, constraints, minMaxData, xMax, yMin, _actualYMax),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMinMaxLabels(
|
||||
BuildContext context,
|
||||
BoxConstraints constraints,
|
||||
Map<String, dynamic> minMaxData,
|
||||
double xMax,
|
||||
double yMin,
|
||||
double yMax,
|
||||
) {
|
||||
// 获取图表区域的实际绘制区域
|
||||
// fl_chart 默认会有一些内边距,我们需要估算这些内边距
|
||||
double leftPadding = 60.rpx; // 左侧留白,用于Y轴标签
|
||||
double rightPadding = 10.rpx; // 右侧留白
|
||||
double topPadding = 10.rpx; // 顶部留白
|
||||
double bottomPadding = 30.rpx; // 底部留白,用于X轴标签
|
||||
|
||||
double chartWidth = constraints.maxWidth - leftPadding - rightPadding;
|
||||
double chartHeight = constraints.maxHeight - topPadding - bottomPadding;
|
||||
|
||||
// X轴范围:1 到 xMax
|
||||
// Y轴范围:yMin - 2 到 yMax + 2
|
||||
double xMin = 1;
|
||||
double xMaxValue = xMax;
|
||||
double yMinValue = yMin - 2;
|
||||
double yMaxValue = yMax + 2;
|
||||
|
||||
double xRange = xMaxValue - xMin;
|
||||
double yRange = yMaxValue - yMinValue;
|
||||
|
||||
List<Widget> labels = [];
|
||||
|
||||
// 添加最小值标签
|
||||
if (minMaxData['minIndex'] != -1) {
|
||||
double minX = (minMaxData['minIndex'] + 1).toDouble();
|
||||
double minY = minMaxData['minValue'];
|
||||
|
||||
// 计算在图表中的相对位置 (0-1)
|
||||
double relativeX = (minX - xMin) / xRange;
|
||||
double relativeY = (minY - yMinValue) / yRange;
|
||||
|
||||
// 转换为像素位置
|
||||
double left = leftPadding + (relativeX * chartWidth);
|
||||
double top = topPadding + ((1 - relativeY) * chartHeight);
|
||||
|
||||
// 标签尺寸
|
||||
double labelWidth = 38.rpx;
|
||||
double labelHeight = 50.rpx;
|
||||
|
||||
labels.add(
|
||||
Positioned(
|
||||
left: left - labelWidth / 2, // 水平居中
|
||||
top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距
|
||||
child: SizedBox(
|
||||
width: labelWidth,
|
||||
height: labelHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// SVG图片作为背景
|
||||
SvgPicture.asset(
|
||||
'assets/img/icon/location.svg',
|
||||
fit: BoxFit.contain,
|
||||
color: stringToColor("#d69dd2"),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 12.rpx),
|
||||
child: Text(
|
||||
'${minMaxData['minValue'].toStringAsFixed(0)}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: AppConstants().smaller_text_fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 添加最大值标签
|
||||
if (minMaxData['maxIndex'] != -1 &&
|
||||
minMaxData['maxIndex'] != minMaxData['minIndex']) {
|
||||
double maxX = (minMaxData['maxIndex'] + 1).toDouble();
|
||||
double maxY = minMaxData['maxValue'];
|
||||
|
||||
// 计算在图表中的相对位置 (0-1)
|
||||
double relativeX = (maxX - xMin) / xRange;
|
||||
double relativeY = (maxY - yMinValue) / yRange;
|
||||
|
||||
// 转换为像素位置
|
||||
double left = leftPadding + (relativeX * chartWidth);
|
||||
double top = topPadding + ((1 - relativeY) * chartHeight);
|
||||
|
||||
// 标签尺寸
|
||||
double labelWidth = 38.rpx;
|
||||
double labelHeight = 50.rpx;
|
||||
|
||||
labels.add(
|
||||
Positioned(
|
||||
left: left - labelWidth / 2, // 水平居中
|
||||
top: top - labelHeight - 8.rpx, // 显示在点的正上方,留出8rpx间距
|
||||
child: SizedBox(
|
||||
width: labelWidth,
|
||||
height: labelHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// SVG图片作为背景
|
||||
SvgPicture.asset(
|
||||
'assets/img/icon/location.svg',
|
||||
fit: BoxFit.contain,
|
||||
color: stringToColor("#FF9F66"),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 12.rpx),
|
||||
child: Text(
|
||||
'${minMaxData['maxValue'].toStringAsFixed(0)}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: AppConstants().smaller_text_fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: labels,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,188 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
// import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
// import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
// class StatusBarWithIndicator extends StatelessWidget {
|
||||
// final int selectKey;
|
||||
// final List<Map<String, dynamic>> showLabel;
|
||||
// final IconData icon;
|
||||
// final double gap; // 每段之间的间距
|
||||
// final bool showCurrentValue; // 新增参数,控制是否显示当前值
|
||||
// final String? currentValueText; // 可选的当前值文字,如果不提供则使用选中的name
|
||||
|
||||
// const StatusBarWithIndicator({
|
||||
// super.key,
|
||||
// required this.selectKey,
|
||||
// required this.showLabel,
|
||||
// this.icon = Icons.favorite,
|
||||
// this.gap = 8.0, // 默认 8.rpx 间距
|
||||
// this.showCurrentValue = false, // 默认为false
|
||||
// this.currentValueText,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return LayoutBuilder(builder: (context, constraints) {
|
||||
// final totalWidth = constraints.maxWidth;
|
||||
// final itemCount = showLabel.length;
|
||||
|
||||
// // 每条线的宽度 = (总宽度 - 总间隔)/ 项数
|
||||
// final totalGap = (itemCount - 1) * gap.rpx;
|
||||
// final itemWidth = (totalWidth - totalGap) / itemCount;
|
||||
|
||||
// // 找到选中项的 index 和对应的数据
|
||||
// final selectedIndex = showLabel.indexWhere((e) => e['key'] == selectKey);
|
||||
// final selectedItem = selectedIndex >= 0 ? showLabel[selectedIndex] : null;
|
||||
|
||||
// final iconLeft = selectedIndex >= 0
|
||||
// ? selectedIndex * (itemWidth + gap.rpx) + itemWidth / 2
|
||||
// : 0.0;
|
||||
|
||||
// // 确定要显示的当前值文字
|
||||
// String displayValue = '';
|
||||
// if (showCurrentValue) {
|
||||
// if (currentValueText != null) {
|
||||
// displayValue = currentValueText!;
|
||||
// } else if (selectedItem != null) {
|
||||
// displayValue = selectedItem['name'] ?? '';
|
||||
// }
|
||||
// }
|
||||
|
||||
// return SizedBox(
|
||||
// width: double.infinity,
|
||||
// child: Stack(
|
||||
// clipBehavior: Clip.none,
|
||||
// children: [
|
||||
// if (selectedIndex >= 0) ...[
|
||||
// // 如果显示当前值,在箭头上方添加文字
|
||||
// if (showCurrentValue && displayValue.isNotEmpty)
|
||||
// Positioned(
|
||||
// left: iconLeft,
|
||||
// top: -50.rpx, // 调整位置,给文字留出空间
|
||||
// child: Transform.translate(
|
||||
// offset: Offset(-45.rpx, 0), // 图片宽度 45.rpx,居中偏移
|
||||
// child: Container(
|
||||
// // padding: EdgeInsets.symmetric(
|
||||
// // horizontal: 8.rpx,
|
||||
// // vertical: 4.rpx,
|
||||
// // ),
|
||||
// // decoration: BoxDecoration(
|
||||
// // color: selectedItem?['color'] ?? Colors.blue,
|
||||
// // borderRadius: BorderRadius.circular(4.rpx),
|
||||
// // boxShadow: [
|
||||
// // BoxShadow(
|
||||
// // color: Colors.black.withOpacity(0.1),
|
||||
// // blurRadius: 4.rpx,
|
||||
// // offset: Offset(0, 2.rpx),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// child: Text(
|
||||
// displayValue,
|
||||
// style: TextStyle(
|
||||
// fontSize: AppConstants().small_an_text_fontSize,
|
||||
// color: themeController.currentColor.sc9,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// // 箭头图片
|
||||
// Positioned(
|
||||
// left: iconLeft,
|
||||
// top: showCurrentValue ? -20.rpx : -20.rpx, // 如果有文字,箭头位置稍下移
|
||||
// child: Transform.translate(
|
||||
// offset: Offset(-22.5.rpx, 0), // 图片宽度 45.rpx,居中偏移
|
||||
// child: Container(
|
||||
// width: 45.rpx,
|
||||
// height: 76.rpx,
|
||||
// decoration: BoxDecoration(
|
||||
// image: DecorationImage(
|
||||
// image: AssetImage('assets/img/tip_arrow.gif'),
|
||||
// fit: BoxFit.cover,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
|
||||
// // 条形图和文字标签
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(top: showCurrentValue ? 70.rpx : 50.rpx),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// // 条形段(带间距)
|
||||
// Row(
|
||||
// children: showLabel.asMap().entries.map((entry) {
|
||||
// int index = entry.key;
|
||||
// var item = entry.value;
|
||||
|
||||
// return Container(
|
||||
// width: itemWidth,
|
||||
// height: 15.rpx,
|
||||
// margin: EdgeInsets.only(
|
||||
// left: index == 0 ? 0 : gap.rpx,
|
||||
// ),
|
||||
// decoration: BoxDecoration(
|
||||
// color: item['color'],
|
||||
// borderRadius: BorderRadius.circular(0.rpx),
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
// ),
|
||||
// SizedBox(height: 12.rpx),
|
||||
// // 名称文字
|
||||
// Row(
|
||||
// children: showLabel.asMap().entries.map((entry) {
|
||||
// int index = entry.key;
|
||||
// var item = entry.value;
|
||||
|
||||
// return Container(
|
||||
// width: itemWidth,
|
||||
// margin: EdgeInsets.only(
|
||||
// left: index == 0 ? 0 : gap.rpx,
|
||||
// ),
|
||||
// alignment: Alignment.center,
|
||||
// child: Text(
|
||||
// item['name'],
|
||||
// style: TextStyle(
|
||||
// fontSize: 24.rpx,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class StatusBarWithIndicator extends StatelessWidget {
|
||||
final int selectKey;
|
||||
final List<Map<String, dynamic>> showLabel;
|
||||
final IconData icon;
|
||||
final double gap; // 每段之间的间距
|
||||
final bool showCurrentValue; // 控制是否显示当前值
|
||||
final String? currentValueText; // 可选的当前值文字,如果不提供则使用选中的name
|
||||
final bool showRange; // 新增参数,控制是否显示范围
|
||||
final String? Function(Map<String, dynamic> item)?
|
||||
rangeTextBuilder; // 可选的范围文字构建器
|
||||
|
||||
const StatusBarWithIndicator({
|
||||
super.key,
|
||||
@@ -13,6 +190,10 @@ class StatusBarWithIndicator extends StatelessWidget {
|
||||
required this.showLabel,
|
||||
this.icon = Icons.favorite,
|
||||
this.gap = 8.0, // 默认 8.rpx 间距
|
||||
this.showCurrentValue = false, // 默认为false
|
||||
this.currentValueText,
|
||||
this.showRange = false, // 默认为false,不显示范围
|
||||
this.rangeTextBuilder, // 自定义范围文字构建器
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -25,23 +206,63 @@ class StatusBarWithIndicator extends StatelessWidget {
|
||||
final totalGap = (itemCount - 1) * gap.rpx;
|
||||
final itemWidth = (totalWidth - totalGap) / itemCount;
|
||||
|
||||
// 找到选中项的 index
|
||||
// 找到选中项的 index 和对应的数据
|
||||
final selectedIndex = showLabel.indexWhere((e) => e['key'] == selectKey);
|
||||
final selectedItem = selectedIndex >= 0 ? showLabel[selectedIndex] : null;
|
||||
|
||||
final iconLeft = selectedIndex >= 0
|
||||
? selectedIndex * (itemWidth + gap.rpx) + itemWidth / 2
|
||||
: 0.0;
|
||||
|
||||
// 确定要显示的当前值文字
|
||||
String displayValue = '';
|
||||
if (showCurrentValue) {
|
||||
if (currentValueText != null) {
|
||||
displayValue = currentValueText!;
|
||||
} else if (selectedItem != null) {
|
||||
displayValue = selectedItem['name'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取范围文字
|
||||
String getRangeText(Map<String, dynamic> item) {
|
||||
if (rangeTextBuilder != null) {
|
||||
return rangeTextBuilder!(item) ?? '';
|
||||
}
|
||||
// 默认返回空字符串,需要外部传入范围数据
|
||||
return item['range']?.toString() ?? '';
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
if (selectedIndex >= 0)
|
||||
if (selectedIndex >= 0) ...[
|
||||
// 如果显示当前值,在箭头上方添加文字
|
||||
if (showCurrentValue && displayValue.isNotEmpty)
|
||||
Positioned(
|
||||
left: iconLeft,
|
||||
top: -50.rpx,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-45.rpx, 0),
|
||||
child: Text(
|
||||
displayValue,
|
||||
style: TextStyle(
|
||||
fontSize: AppConstants().small_an_text_fontSize,
|
||||
color: themeController.currentColor.sc9,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 箭头图片
|
||||
Positioned(
|
||||
left: iconLeft,
|
||||
top: -20.rpx,
|
||||
top: showCurrentValue ? -20.rpx : -20.rpx,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-22.5.rpx, 0), // 图片宽度 45.rpx,居中偏移
|
||||
offset: Offset(-22.5.rpx, 0),
|
||||
child: Container(
|
||||
width: 45.rpx,
|
||||
height: 76.rpx,
|
||||
@@ -54,8 +275,11 @@ class StatusBarWithIndicator extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 条形图和文字标签
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 50.rpx),
|
||||
padding: EdgeInsets.only(top: showCurrentValue ? 70.rpx : 50.rpx),
|
||||
child: Column(
|
||||
children: [
|
||||
// 条形段(带间距)
|
||||
@@ -78,25 +302,46 @@ class StatusBarWithIndicator extends StatelessWidget {
|
||||
}).toList(),
|
||||
),
|
||||
SizedBox(height: 12.rpx),
|
||||
// 名称文字
|
||||
|
||||
// 名称文字和范围(每个下面显示对应的范围)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: showLabel.asMap().entries.map((entry) {
|
||||
int index = entry.key;
|
||||
var item = entry.value;
|
||||
|
||||
return Container(
|
||||
width: itemWidth,
|
||||
margin: EdgeInsets.only(
|
||||
left: index == 0 ? 0 : gap.rpx,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
item['name'],
|
||||
style: TextStyle(
|
||||
fontSize: 24.rpx,
|
||||
color: Colors.white,
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: index == 0 ? 0 : gap.rpx,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 名称文字
|
||||
Text(
|
||||
item['name'],
|
||||
style: TextStyle(
|
||||
fontSize: 24.rpx,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
// 范围文字(如果显示)
|
||||
if (showRange)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 4.rpx),
|
||||
child: Text(
|
||||
"(" + getRangeText(item) + ")",
|
||||
style: TextStyle(
|
||||
fontSize: 20.rpx,
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
Reference in New Issue
Block a user