import 'package:flutter/material.dart'; import 'dart:math'; class ScoreProgressChart extends StatelessWidget { // 历史成绩列表,按时间排序 final List> historyScores; final double height; final Color lineColor; final Color fillColor; const ScoreProgressChart({ super.key, required this.historyScores, this.height = 60.0, this.lineColor = Colors.blue, this.fillColor = Colors.blueAccent, }); @override Widget build(BuildContext context) { if (historyScores.isEmpty) { return SizedBox(height: height); } // 提取达成率数据 (achievement / 10000) // 假设 historyScores 已经按时间正序排列 final List achievements = historyScores.map((s) { int ach = s['achievement'] ?? 0; return ach / 10000.0; }).toList(); // 找出最小值和最大值以确定 Y 轴范围,为了美观,稍微扩大一点范围 double minVal = achievements.reduce(min); double maxVal = achievements.reduce(max); // 如果所有分数一样,给一个默认范围 if (minVal == maxVal) { minVal = max(0, minVal - 1.0); maxVal = min(101.0, maxVal + 1.0); } else { // 增加一点上下边距 double range = maxVal - minVal; minVal = max(0, minVal - range * 0.1); maxVal = min(101.0, maxVal + range * 0.1); } return SizedBox( height: height, child: CustomPaint( size: Size.infinite, painter: _ChartPainter( achievements: achievements, minVal: minVal, maxVal: maxVal, lineColor: lineColor, fillColor: fillColor.withOpacity(0.2), ), ), ); } } class _ChartPainter extends CustomPainter { final List achievements; final double minVal; final double maxVal; final Color lineColor; final Color fillColor; _ChartPainter({ required this.achievements, required this.minVal, required this.maxVal, required this.lineColor, required this.fillColor, }); @override void paint(Canvas canvas, Size size) { // ✅ 修复1:数据不足直接返回 if (achievements.length < 1) return; final paint = Paint() ..color = lineColor ..strokeWidth = 2.0 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; final fillPaint = Paint() ..color = fillColor ..style = PaintingStyle.fill; final path = Path(); final fillPath = Path(); double width = size.width; double height = size.height; double range = maxVal - minVal; if (range == 0) range = 1; // 防止除以0 // 计算每个点的坐标 List points = []; for (int i = 0; i < achievements.length; i++) { double x; if (achievements.length == 1) { x = width / 2; // ✅ 修复2:只有1个点时,放中间 } else { x = (i / (achievements.length - 1)) * width; } // Y轴翻转,因为Canvas原点在左上角 double normalizedY = (achievements[i] - minVal) / range; double y = height - (normalizedY * height); // ✅ 修复3:防止 NaN / 无限大 if (x.isNaN || x.isInfinite) x = 0; if (y.isNaN || y.isInfinite) y = height / 2; points.add(Offset(x, y)); } // 构建折线路径 path.moveTo(points.first.dx, points.first.dy); fillPath.moveTo(points.first.dx, height); // 填充从底部开始 fillPath.lineTo(points.first.dx, points.first.dy); for (int i = 1; i < points.length; i++) { path.lineTo(points[i].dx, points[i].dy); fillPath.lineTo(points[i].dx, points[i].dy); } // 闭合填充路径 fillPath.lineTo(points.last.dx, height); fillPath.close(); // 绘制填充 canvas.drawPath(fillPath, fillPaint); // 绘制折线 canvas.drawPath(path, paint); // 可选:绘制最后一个点的高亮圆点 final dotPaint = Paint()..color = lineColor; final lastPoint = points.last; // ✅ 修复4:绘制前校验,彻底杜绝NaN崩溃 if (!lastPoint.dx.isNaN && !lastPoint.dy.isNaN) { canvas.drawCircle(lastPoint, 3.0, dotPaint); } } @override bool shouldRepaint(covariant _ChartPainter oldDelegate) { return oldDelegate.achievements != achievements || oldDelegate.minVal != minVal || oldDelegate.maxVal != maxVal; } }