1. 新增报告计算进度圆环、科目卡片、完成弹窗组件 2. 重构report_computation_page,替换旧的自定义组件为新封装组件 3. 新增student_info_page的主题资源和UI重构,适配Flutter屏幕适配 4. 新增临时调试入口用于开发测试
236 lines
7.5 KiB
Dart
236 lines
7.5 KiB
Dart
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;
|
||
}
|