import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:new_learn/core/api/auth/api.dart'; import 'package:new_learn/core/api/report/api.dart'; import 'package:new_learn/core/api/report/model.dart'; import 'package:new_learn/core/logger/app_talker.dart'; import 'package:new_learn/core/providers/user_provider.dart'; /// ============================================================ /// 【ReportComputationPage】AI 加载动画页 —— 对标 Vue reportComputation.vue /// /// 入门测 H5 壳子看完所有科目报告 → navigateTo reportComputation → /// 圆环进度 + 每个科目依次"AI计算中"动画 → finishedPopup 弹窗 → /// 调 ChangeShowReport → refresh userProvider → switchTab 首页 /// ============================================================ class ReportComputationPage extends ConsumerStatefulWidget { const ReportComputationPage({super.key}); @override ConsumerState createState() => _ReportComputationPageState(); } class _ReportComputationPageState extends ConsumerState { List<_SubjectData> _subjectList = []; int _ringProgress = 0; bool _showFinishedPopup = false; int _currentIndex = -1; Timer? _animTimer; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _loadAndStart()); } @override void dispose() { _animTimer?.cancel(); super.dispose(); } Future _loadAndStart() async { try { final results = await Future.wait([ ReportApi.instance.getCurrentReport(), ReportApi.instance.getCurrentReportKnow(), ]); final report = results[0] as UserReportType; final knowList = results[1] as List; if (report.subjectReports.isEmpty) return; final itemTime = (15 / report.subjectReports.length).floor(); setState(() { _subjectList = report.subjectReports.map((item) { final knowItem = knowList.isNotEmpty ? knowList.firstWhere( (k) => k.subjectName == item.subjectName, orElse: () => const UserReportKnowType(), ) : const UserReportKnowType(); return _SubjectData( subjectReport: item, knowCount: knowItem.knowCount, time: itemTime, ); }).toList(); }); _startSubjects(0); } catch (e) { talker.error('reportComputation 加载失败', e); } } void _startSubjects(int index) { if (index >= _subjectList.length) { setState(() { _ringProgress = 100; _showFinishedPopup = true; }); return; } setState(() { _currentIndex = index; _subjectList[index].status = _SubjectStatus.inProgress; _subjectList[index].progress = 0; }); final totalTimeMs = _subjectList[index].time * 1000; final startTime = DateTime.now().millisecondsSinceEpoch; _animTimer = Timer.periodic(const Duration(milliseconds: 30), (timer) { final elapsed = DateTime.now().millisecondsSinceEpoch - startTime; final progress = (elapsed / totalTimeMs * 100).clamp(0, 100).toInt(); setState(() { _subjectList[index].progress = progress; _ringProgress = ((index * 100) + progress) ~/ _subjectList.length; }); if (elapsed >= totalTimeMs) { timer.cancel(); _animTimer = null; setState(() { _subjectList[index].progress = 100; _subjectList[index].status = _SubjectStatus.finished; }); _startSubjects(index + 1); } }); } /// finishedPopup 的"立即开启学习蝶变"按钮 Future _onFinish() async { try { await UserApi.changeShowReport(); talker.debug('changeShowReport 成功'); await ref.read(currentUserProvider.notifier).refresh(); } catch (e) { talker.error('changeShowReport 失败', e); } if (!mounted) return; context.go('/'); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF0F1A2E), body: Stack( children: [ SafeArea( child: Column( children: [ const SizedBox(height: 16), const Text( 'AI智能生成学习计划', style: TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 24), // 圆环进度 _ProgressRing(progress: _ringProgress), const SizedBox(height: 20), // 科目列表 Expanded( child: ListView.separated( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), itemCount: _subjectList.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (_, index) { return _SubjectItem(data: _subjectList[index]); }, ), ), ], ), ), // finishedPopup 弹窗 if (_showFinishedPopup) Positioned.fill( child: Container( color: Colors.black54, child: Center( child: Container( width: 320, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check_circle, size: 48, color: Color(0xFF10C79C)), const SizedBox(height: 12), const Text( '报告生成完成', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1D2B47), ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: _onFinish, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF3D7DF5), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), elevation: 0, ), child: const Text('立即开启学习蝶变'), ), ), ], ), ), ), ), ), ], ), ); } } // ==================== 内部数据类 ==================== enum _SubjectStatus { notStarted, inProgress, finished } class _SubjectData { final SubjectReportType subjectReport; int progress; _SubjectStatus status; int time; int knowCount; _SubjectData({ required this.subjectReport, required this.time, required this.knowCount, this.progress = 0, this.status = _SubjectStatus.notStarted, }); } // ==================== 圆环进度组件 ==================== class _ProgressRing extends StatelessWidget { const _ProgressRing({required this.progress}); final int progress; @override Widget build(BuildContext context) { return SizedBox( width: 262, height: 262, child: Stack( alignment: Alignment.center, children: [ // 背景圈 SizedBox( width: 240, height: 240, child: CircularProgressIndicator( value: 1.0, strokeWidth: 8, backgroundColor: Colors.white.withValues(alpha: 0.1), valueColor: const AlwaysStoppedAnimation( Color(0xFF1D2B47), ), ), ), // 进度圈 SizedBox( width: 240, height: 240, child: CircularProgressIndicator( value: progress / 100, strokeWidth: 8, backgroundColor: Colors.transparent, valueColor: const AlwaysStoppedAnimation( Color(0xFF3D7DF5), ), ), ), // 中心文字 Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$progress%', style: const TextStyle( color: Colors.white, fontSize: 40, fontWeight: FontWeight.w700, ), ), const Text( 'AI加载中', style: TextStyle( color: Color(0xFF687FAD), fontSize: 14, fontWeight: FontWeight.w700, ), ), ], ), ], ), ); } } // ==================== 科目卡片组件 ==================== class _SubjectItem extends StatelessWidget { const _SubjectItem({required this.data}); final _SubjectData data; @override Widget build(BuildContext context) { final isFinished = data.status == _SubjectStatus.finished; final isInProgress = data.status == _SubjectStatus.inProgress; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: isFinished ? const [Color(0xFFE3FCF4), Color(0xFFF7FCFA), Colors.white] : const [Color(0xFFE3F7FC), Color(0xFFF7FBFC), Colors.white], ), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 6, offset: Offset(0, 2), ), ], ), child: Row( children: [ // 状态徽章 Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isFinished ? const Color(0xFF19CE92) : isInProgress ? const Color(0xFF3D7DF5) : const Color(0xFFC0C4CC), borderRadius: BorderRadius.circular(10), ), child: Text( '${data.progress}%', style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), const SizedBox(width: 12), // 科目图标 ClipRRect( borderRadius: BorderRadius.circular(8), child: data.subjectReport.icon.isNotEmpty ? Image.network( data.subjectReport.icon, width: 26, height: 26, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon( Icons.menu_book, size: 26, color: Color(0xFF3D7DF5), ), ) : const Icon( Icons.menu_book, size: 26, color: Color(0xFF3D7DF5), ), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( data.subjectReport.subjectName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1D2B47), ), ), const SizedBox(height: 6), // 科目内部进度条 ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: data.progress / 100, backgroundColor: const Color(0xFFE1EAFA), valueColor: AlwaysStoppedAnimation( isFinished ? const Color(0xFF19CE92) : const Color(0xFF3D7DF5), ), minHeight: 4, ), ), ], ), ), const SizedBox(width: 8), // 知识点数量 Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: const Color(0xFFE1EAFA), borderRadius: BorderRadius.circular(4), ), child: Text( '${data.knowCount}', style: const TextStyle( fontSize: 12, color: Color(0xFF3D7DF5), ), ), ), ], ), ); } }