Files
App.K12.Student.Miniapp_Flu…/lib/features/report/widgets/computation_progress_ring.dart
T
崔宝新 0fefc786bb feat(report): 实现AI学习报告计算页及相关组件
1. 新增报告计算进度圆环、科目卡片、完成弹窗组件
2. 重构report_computation_page,替换旧的自定义组件为新封装组件
3. 新增student_info_page的主题资源和UI重构,适配Flutter屏幕适配
4. 新增临时调试入口用于开发测试
2026-09-10 11:57:18 +08:00

236 lines
7.5 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 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:new_learn/core/utils/img_url.dart';
/// ============================================================
/// 【ComputationProgressRing】AI 计算圆环 —— 对标 Vue ProgressRing.vue
///
/// 由外到内:
/// 1. 虚线刻度图(700rpx,6s 顺时针慢转)
/// 2. 外圈光环球图(524rpx,1.5s 反向快转)
/// 3. 外层渐变光圈 ellipse-wai(452rpx)
/// 4. 渐变进度弧(370rpx,线宽 20rpx,#8BFFF5 → #006CFF 纵向渐变)
/// 5. 白色内圆 ellipse-nei(300rpx)+ 渐变百分比文字 + 副标题
///
/// Vue 用 canvas 画弧;Flutter 用 CustomPaint,渐变方向与 Vue
/// createLinearGradient(center,0 → center,size) 保持一致(纵向)。
/// ============================================================
class ComputationProgressRing extends StatefulWidget {
const ComputationProgressRing({super.key, required this.progress});
/// 进度值 0-100
final double progress;
/// 外圈光环球图(Vue props.outerImg 默认值)
static const String outerImg =
'/StaticResource/2026-06-11/outer1781143064766.png';
/// 虚线刻度图(Vue props.dashedLineImg 默认值)
static const String dashedImg =
'/StaticResource/2026-06-11/dashedLine1781143064766.png';
@override
State<ComputationProgressRing> createState() =>
_ComputationProgressRingState();
}
class _ComputationProgressRingState extends State<ComputationProgressRing>
with TickerProviderStateMixin {
/// 外圈:Vue rotate-cw 0 → -360deg / 1.5s
late final AnimationController _outerCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
/// 虚线:Vue rotate-ccw 0 → 360deg / 6s
late final AnimationController _dashedCtrl = AnimationController(
vsync: this,
duration: const Duration(seconds: 6),
)..repeat();
@override
void dispose() {
_outerCtrl.dispose();
_dashedCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = 262.w; // 524rpx
return SizedBox(
width: size,
height: size,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
// 虚线刻度(700rpx,超出 524rpx 容器,允许溢出绘制)
RepaintBoundary(
child: RotationTransition(
turns: _dashedCtrl,
child: _ringImage(ComputationProgressRing.dashedImg, 350.w),
),
),
// 外圈光球
RepaintBoundary(
child: RotationTransition(
turns: ReverseAnimation(_outerCtrl),
child: _ringImage(ComputationProgressRing.outerImg, 262.w),
),
),
// ellipse-wai:87.42deg 紫色渐变 + 内发光(内阴影用外发光近似)
Container(
width: 226.w,
height: 226.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
stops: [0.1133, 1.0],
colors: [Color(0x6B5B5DFF), Color(0x0000FFF2)],
),
border: Border.all(color: const Color(0x33FFFFFF), width: 1.w),
boxShadow: const [
BoxShadow(color: Color(0x33FFFFFF), blurRadius: 25),
],
),
),
// 进度弧(canvas)
SizedBox(
width: 185.w,
height: 185.w,
child: CustomPaint(painter: _RingArcPainter(widget.progress)),
),
// ellipse-nei:内圆
Container(
width: 150.w,
height: 150.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment(-0.6, -0.8),
end: Alignment(0.87, 0.49),
stops: [0.42, 0.82],
colors: [Color(0xFFB1DFFF), Color(0xFFFFFFFF)],
),
border: Border.all(color: const Color(0x80FFFFFF)),
boxShadow: const [
BoxShadow(
color: Color(0x0D1D2B47),
blurRadius: 28,
offset: Offset(0, 14),
),
BoxShadow(color: Color(0x80FFFFFF), blurRadius: 26),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_gradientText(
widget.progress.floor().toString(),
40.spMin,
height: 1.0,
),
Padding(
padding: EdgeInsets.only(left: 2.w, bottom: 6.h),
child: _gradientText('%', 14.5.spMin),
),
],
),
SizedBox(height: 2.h),
Text(
'AI加载中',
style: TextStyle(
fontSize: 14.5.spMin,
fontWeight: FontWeight.bold,
color: const Color(0xFF0F9AFF),
),
),
],
),
),
],
),
);
}
Widget _ringImage(String path, double size) {
return CachedNetworkImage(
imageUrl: ImgUrl.build(path),
width: size,
height: size,
fit: BoxFit.contain,
errorWidget: (_, _, _) => SizedBox(width: size, height: size),
);
}
/// 渐变文字:linear-gradient(180deg, #006cff → #529bff)
Widget _gradientText(String text, double fontSize, {double? height}) {
return ShaderMask(
shaderCallback: (rect) => ui.Gradient.linear(
rect.topCenter,
rect.bottomCenter,
const [Color(0xFF006CFF), Color(0xFF529BFF)],
),
child: Text(
text,
style: TextStyle(
fontSize: fontSize,
height: height,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
);
}
}
/// 进度弧画笔(对标 Vue canvas arc + 纵向线性渐变 + round 线帽)
class _RingArcPainter extends CustomPainter {
_RingArcPainter(this.progress);
final double progress;
@override
void paint(Canvas canvas, Size size) {
final p = progress.clamp(0.0, 100.0);
if (p <= 0) return;
final stroke = 10.w; // 20rpx
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width - stroke) / 2;
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = stroke
..strokeCap = StrokeCap.round
..shader = ui.Gradient.linear(
Offset(center.dx, center.dy - radius),
Offset(center.dx, center.dy + radius),
const [Color(0xFF8BFFF5), Color(0xFF006CFF)],
);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2, // 从 12 点方向开始
2 * math.pi * p / 100,
false,
paint,
);
}
@override
bool shouldRepaint(covariant _RingArcPainter oldDelegate) =>
oldDelegate.progress != progress;
}