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 createState() => _ComputationProgressRingState(); } class _ComputationProgressRingState extends State 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; }