Files
UnionApp/lib/widgets/score_progress_chart.dart
spasolreisa 00bd43dc7f 0418 0222
更新2
2026-04-18 02:11:45 +08:00

156 lines
4.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'dart:math';
class ScoreProgressChart extends StatelessWidget {
// 历史成绩列表,按时间排序
final List<Map<String, dynamic>> 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<double> 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<double> 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<Offset> 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;
}
}