fix: 7处修复 - 溢出/黑屏/趋势图/欢迎卡片/抽屉
This commit is contained in:
@@ -94,30 +94,36 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
final data = <Map<String, dynamic>>[];
|
||||
final rng = Random(42);
|
||||
|
||||
// 7天模式全部有数据;30天/90天模式随机约70%的天数有数据
|
||||
final hasDataChance = _period <= 7 ? 1.0 : 0.7;
|
||||
|
||||
for (int i = _period - 1; i >= 0; i--) {
|
||||
final date = now.subtract(Duration(days: i));
|
||||
num value;
|
||||
num? value;
|
||||
num? value2;
|
||||
|
||||
switch (widget.metricType) {
|
||||
case 'blood_pressure':
|
||||
value = 110 + rng.nextInt(40);
|
||||
value2 = 70 + rng.nextInt(25);
|
||||
break;
|
||||
case 'heart_rate':
|
||||
value = 65 + rng.nextInt(35);
|
||||
break;
|
||||
case 'glucose':
|
||||
value = 4.0 + rng.nextDouble() * 3.0;
|
||||
break;
|
||||
case 'spo2':
|
||||
value = 95 + rng.nextDouble() * 5;
|
||||
break;
|
||||
case 'weight':
|
||||
value = 58 + rng.nextDouble() * 15;
|
||||
break;
|
||||
default:
|
||||
value = 100 + rng.nextInt(50);
|
||||
final measured = rng.nextDouble() < hasDataChance;
|
||||
if (measured) {
|
||||
switch (widget.metricType) {
|
||||
case 'blood_pressure':
|
||||
value = 110 + rng.nextInt(40);
|
||||
value2 = 70 + rng.nextInt(25);
|
||||
break;
|
||||
case 'heart_rate':
|
||||
value = 65 + rng.nextInt(35);
|
||||
break;
|
||||
case 'glucose':
|
||||
value = 4.0 + rng.nextDouble() * 3.0;
|
||||
break;
|
||||
case 'spo2':
|
||||
value = 95 + rng.nextDouble() * 5;
|
||||
break;
|
||||
case 'weight':
|
||||
value = 58 + rng.nextDouble() * 15;
|
||||
break;
|
||||
default:
|
||||
value = 100 + rng.nextInt(50);
|
||||
}
|
||||
}
|
||||
|
||||
data.add({
|
||||
@@ -166,14 +172,18 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
({double min, double max, double step}) _calcYRange() {
|
||||
if (_data.isEmpty) return (min: 0, max: 100, step: 20);
|
||||
|
||||
final validData = _data.where((e) => e['value'] != null).toList();
|
||||
if (validData.isEmpty) return (min: 0, max: 100, step: 20);
|
||||
|
||||
num allMin, allMax;
|
||||
if (_isDualLine) {
|
||||
final values1 = _data.map((e) => e['value'] as num);
|
||||
final values2 = _data.map((e) => e['value2'] as num);
|
||||
allMin = min(values1.reduce(min), values2.reduce(min));
|
||||
allMax = max(values1.reduce(max), values2.reduce(max));
|
||||
final values1 = validData.map((e) => e['value'] as num);
|
||||
final validDual = validData.where((e) => e.containsKey('value2') && e['value2'] != null).toList();
|
||||
final values2 = validDual.map((e) => e['value2'] as num);
|
||||
allMin = min(values1.reduce(min), values2.isEmpty ? values1.reduce(min) : values2.reduce(min));
|
||||
allMax = max(values1.reduce(max), values2.isEmpty ? values1.reduce(max) : values2.reduce(max));
|
||||
} else {
|
||||
final values = _data.map((e) => e['value'] as num);
|
||||
final values = validData.map((e) => e['value'] as num);
|
||||
allMin = values.reduce(min);
|
||||
allMax = values.reduce(max);
|
||||
}
|
||||
@@ -210,8 +220,9 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _calcStats() {
|
||||
if (_data.isEmpty) return {};
|
||||
final values = _data.map((e) => e['value'] as num).toList();
|
||||
final validData = _data.where((e) => e['value'] != null).toList();
|
||||
if (validData.isEmpty) return {};
|
||||
final values = validData.map((e) => e['value'] as num).toList();
|
||||
|
||||
final maxVal = values.reduce(max);
|
||||
final minVal = values.reduce(min);
|
||||
@@ -219,7 +230,16 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
|
||||
String avgStr;
|
||||
if (_isDualLine) {
|
||||
final values2 = _data.map((e) => e['value2'] as num).toList();
|
||||
final validDual = validData.where((e) => e.containsKey('value2') && e['value2'] != null).toList();
|
||||
if (validDual.isEmpty) {
|
||||
return {
|
||||
'max': _formatValue(maxVal),
|
||||
'min': _formatValue(minVal),
|
||||
'avg': _formatValue(avgVal),
|
||||
'count': validData.length,
|
||||
};
|
||||
}
|
||||
final values2 = validDual.map((e) => e['value2'] as num).toList();
|
||||
final maxVal2 = values2.reduce(max);
|
||||
final minVal2 = values2.reduce(min);
|
||||
final avgVal2 = values2.reduce((a, b) => a + b) / values2.length;
|
||||
@@ -228,7 +248,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
'max': '${_formatValue(maxVal)} / ${_formatValue(maxVal2)}',
|
||||
'min': '${_formatValue(minVal)} / ${_formatValue(minVal2)}',
|
||||
'avg': avgStr,
|
||||
'count': _data.length,
|
||||
'count': validData.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -236,7 +256,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
'max': _formatValue(maxVal),
|
||||
'min': _formatValue(minVal),
|
||||
'avg': _formatValue(avgVal),
|
||||
'count': _data.length,
|
||||
'count': validData.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -309,7 +329,9 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
// ==================== 当前值卡片 ====================
|
||||
List<Widget> _buildCurrentValueCard() {
|
||||
if (_data.isEmpty) return [];
|
||||
final latest = _data.last;
|
||||
// 找到最新的一条有数据的记录
|
||||
final latest = _data.reversed.where((e) => e['value'] != null).firstOrNull;
|
||||
if (latest == null) return [];
|
||||
final val = latest['value'];
|
||||
final val2 = latest['value2'];
|
||||
final status = _getStatus(val as num, value2: val2 as num?);
|
||||
@@ -491,7 +513,8 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
Widget _buildRecordList() {
|
||||
if (_data.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final displayList = _showAllRecords ? _data : _data.reversed.take(5).toList().reversed.toList();
|
||||
final validRecords = _data.where((e) => e['value'] != null).toList().reversed.toList();
|
||||
final displayList = _showAllRecords ? validRecords : validRecords.take(5).toList();
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@@ -515,12 +538,12 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('数据记录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
|
||||
Text('${_data.length} 条', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
Text('${validRecords.length} 条', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
],
|
||||
),
|
||||
),
|
||||
...displayList.map((item) => _buildRecordRow(item)),
|
||||
if (_data.length > 5)
|
||||
if (validRecords.length > 5)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Center(
|
||||
@@ -530,7 +553,7 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
),
|
||||
child: Text(
|
||||
_showAllRecords ? '收起' : '查看全部 (${_data.length} 条)',
|
||||
_showAllRecords ? '收起' : '查看全部 (${validRecords.length} 条)',
|
||||
style: const TextStyle(fontSize: 13, color: Color(0xFF635BFF)),
|
||||
),
|
||||
),
|
||||
@@ -587,10 +610,11 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
const rightPadding = 8.0;
|
||||
final chartW = renderBox.size.width - leftPadding - rightPadding;
|
||||
|
||||
// 找到 x 方向最近的数据点
|
||||
int nearestIndex = 0;
|
||||
// 找到 x 方向最近的有数据点
|
||||
int? nearestIndex;
|
||||
double minDist = double.infinity;
|
||||
for (int i = 0; i < _data.length; i++) {
|
||||
if (_data[i]['value'] == null) continue;
|
||||
final pointX = leftPadding + (chartW * i / (_data.length - 1));
|
||||
final dist = (localPosition.dx - pointX).abs();
|
||||
if (dist < minDist) {
|
||||
@@ -599,8 +623,8 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// 点击偏离数据点太远则不响应
|
||||
if (minDist > 40) return;
|
||||
// 点击偏离数据点太远或没有有效数据点则不响应
|
||||
if (nearestIndex == null || minDist > 40) return;
|
||||
|
||||
final item = _data[nearestIndex];
|
||||
final date = item['date'] as DateTime;
|
||||
@@ -730,6 +754,12 @@ class _StatItem extends StatelessWidget {
|
||||
// 趋势图 CustomPainter
|
||||
// ============================================================
|
||||
|
||||
class _Segment {
|
||||
final int start;
|
||||
final int end;
|
||||
const _Segment({required this.start, required this.end});
|
||||
}
|
||||
|
||||
class _TrendChartPainter extends CustomPainter {
|
||||
final List<Map<String, dynamic>> data;
|
||||
final String metricType;
|
||||
@@ -774,36 +804,92 @@ class _TrendChartPainter extends CustomPainter {
|
||||
// ---- 1. 网格线 & Y轴刻度 ----
|
||||
_drawGridAndYAxis(canvas, plotLeft, plotTop, plotRight, plotBottom, chartH);
|
||||
|
||||
// ---- 2. 数据点坐标计算 ----
|
||||
// ---- 2. 数据点坐标计算(跳过 null 值)----
|
||||
final points1 = <Offset>[];
|
||||
final points2 = <Offset>[];
|
||||
// 记录每条线的分段信息:每个段是连续非null的点的索引范围
|
||||
final segments1 = <_Segment>[];
|
||||
final segments2 = <_Segment>[];
|
||||
|
||||
int? segStart1;
|
||||
int? segStart2;
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
final x = plotLeft + (chartW * i / (data.length - 1));
|
||||
final val = (data[i]['value'] as num).toDouble();
|
||||
final y = plotTop + chartH - ((val - yMin) / (yMax - yMin)) * chartH;
|
||||
points1.add(Offset(x, y.clamp(plotTop, plotBottom)));
|
||||
final val = data[i]['value'] as num?;
|
||||
|
||||
if (val != null) {
|
||||
final y = plotTop + chartH - ((val.toDouble() - yMin) / (yMax - yMin)) * chartH;
|
||||
points1.add(Offset(x, y.clamp(plotTop, plotBottom)));
|
||||
segStart1 ??= points1.length - 1;
|
||||
} else {
|
||||
if (segStart1 != null && points1.length > segStart1 + 1) {
|
||||
segments1.add(_Segment(start: segStart1, end: points1.length - 1));
|
||||
}
|
||||
segStart1 = null;
|
||||
}
|
||||
|
||||
if (isDualLine && data[i].containsKey('value2')) {
|
||||
final val2 = (data[i]['value2'] as num).toDouble();
|
||||
final y2 = plotTop + chartH - ((val2 - yMin) / (yMax - yMin)) * chartH;
|
||||
points2.add(Offset(x, y2.clamp(plotTop, plotBottom)));
|
||||
final val2 = data[i]['value2'] as num?;
|
||||
if (val2 != null) {
|
||||
final y2 = plotTop + chartH - ((val2.toDouble() - yMin) / (yMax - yMin)) * chartH;
|
||||
points2.add(Offset(x, y2.clamp(plotTop, plotBottom)));
|
||||
segStart2 ??= points2.length - 1;
|
||||
} else {
|
||||
if (segStart2 != null && points2.length > segStart2 + 1) {
|
||||
segments2.add(_Segment(start: segStart2, end: points2.length - 1));
|
||||
}
|
||||
segStart2 = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 3. 填充区域(主线) ----
|
||||
_drawFill(canvas, points1, plotBottom, _primaryColor);
|
||||
|
||||
// ---- 4. 填充区域(副线,血压) ----
|
||||
if (isDualLine && points2.isNotEmpty) {
|
||||
_drawFill(canvas, points2, plotBottom, _secondaryColor);
|
||||
// 收尾最后一个段
|
||||
if (segStart1 != null && points1.length > segStart1 + 1) {
|
||||
segments1.add(_Segment(start: segStart1, end: points1.length - 1));
|
||||
}
|
||||
if (segStart2 != null && points2.length > segStart2 + 1) {
|
||||
segments2.add(_Segment(start: segStart2, end: points2.length - 1));
|
||||
}
|
||||
|
||||
// ---- 5. 数据线(主线) ----
|
||||
_drawSmoothLine(canvas, points1, _primaryColor);
|
||||
// ---- 3. 填充区域(主线,分段绘制)----
|
||||
if (segments1.isNotEmpty) {
|
||||
for (final seg in segments1) {
|
||||
_drawFill(canvas, points1.sublist(seg.start, seg.end + 1), plotBottom, _primaryColor);
|
||||
}
|
||||
} else if (points1.length >= 2) {
|
||||
_drawFill(canvas, points1, plotBottom, _primaryColor);
|
||||
}
|
||||
|
||||
// ---- 6. 数据线(副线) ----
|
||||
if (isDualLine && points2.isNotEmpty) {
|
||||
_drawSmoothLine(canvas, points2, _secondaryColor);
|
||||
// ---- 4. 填充区域(副线,血压,分段绘制)----
|
||||
if (isDualLine) {
|
||||
if (segments2.isNotEmpty) {
|
||||
for (final seg in segments2) {
|
||||
_drawFill(canvas, points2.sublist(seg.start, seg.end + 1), plotBottom, _secondaryColor);
|
||||
}
|
||||
} else if (points2.length >= 2) {
|
||||
_drawFill(canvas, points2, plotBottom, _secondaryColor);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 5. 数据线(主线,分段绘制)----
|
||||
if (segments1.isNotEmpty) {
|
||||
for (final seg in segments1) {
|
||||
_drawLine(canvas, points1.sublist(seg.start, seg.end + 1), _primaryColor);
|
||||
}
|
||||
} else if (points1.length >= 2) {
|
||||
_drawLine(canvas, points1, _primaryColor);
|
||||
}
|
||||
|
||||
// ---- 6. 数据线(副线,分段绘制)----
|
||||
if (isDualLine) {
|
||||
if (segments2.isNotEmpty) {
|
||||
for (final seg in segments2) {
|
||||
_drawLine(canvas, points2.sublist(seg.start, seg.end + 1), _secondaryColor);
|
||||
}
|
||||
} else if (points2.length >= 2) {
|
||||
_drawLine(canvas, points2, _secondaryColor);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 7. 数据点 ----
|
||||
@@ -864,16 +950,14 @@ class _TrendChartPainter extends CustomPainter {
|
||||
..moveTo(points.first.dx, plotBottom)
|
||||
..lineTo(points.first.dx, points.first.dy);
|
||||
for (int i = 1; i < points.length; i++) {
|
||||
final midX = (points[i - 1].dx + points[i].dx) / 2;
|
||||
fillPath.quadraticBezierTo(midX, points[i - 1].dy, midX, (points[i - 1].dy + points[i].dy) / 2);
|
||||
fillPath.lineTo(points[i].dx, points[i].dy);
|
||||
}
|
||||
fillPath.lineTo(points.last.dx, points.last.dy);
|
||||
fillPath.lineTo(points.last.dx, plotBottom);
|
||||
fillPath.close();
|
||||
canvas.drawPath(fillPath, fillPaint);
|
||||
}
|
||||
|
||||
void _drawSmoothLine(Canvas canvas, List<Offset> points, Color color) {
|
||||
void _drawLine(Canvas canvas, List<Offset> points, Color color) {
|
||||
if (points.length < 2) return;
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
@@ -882,12 +966,10 @@ class _TrendChartPainter extends CustomPainter {
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
|
||||
final path = Path()..moveTo(points[0].dx, points[0].dy);
|
||||
final path = Path()..moveTo(points.first.dx, points.first.dy);
|
||||
for (int i = 1; i < points.length; i++) {
|
||||
final cpx = (points[i - 1].dx + points[i].dx) / 2;
|
||||
path.quadraticBezierTo(cpx, points[i - 1].dy, (cpx + points[i].dx) / 2, (points[i - 1].dy + points[i].dy) / 2);
|
||||
path.lineTo(points[i].dx, points[i].dy);
|
||||
}
|
||||
path.lineTo(points.last.dx, points.last.dy);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user