import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.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'; import 'package:new_learn/core/utils/img_url.dart'; import 'package:new_learn/core/utils/screen_adapt_template.dart'; import 'package:new_learn/features/report/widgets/computation_finished_popup.dart'; import 'package:new_learn/features/report/widgets/computation_progress_ring.dart'; import 'package:new_learn/features/report/widgets/computation_subject_item.dart'; /// ============================================================ /// 【ReportComputationPage】AI 加载动画页 —— 对标 Vue reportComputation.vue /// /// 入门测 H5 壳子看完所有科目报告 → navigateTo reportComputation → /// 圆环进度 + 每个科目依次"AI计算中"动画 → 500ms 后 finishedPopup 弹窗 → /// 调 ChangeShowReport → refresh userProvider → switchTab 首页 /// /// Vue 端用 tPageImg/tPageColor 从 themes/default.json 抽主题; /// Flutter 端未做主题抽离,本页资源路径/颜色集中在 _ComputationTheme。 /// ============================================================ /// 本页主题资源(对标 default.json → pages/reportPackage/reportComputation) class _ComputationTheme { // ---- images ---- static const String bg = '/StaticResource/2026-06-11/bg-report21781144905849.png'; static const String bgPad = '/StaticResource/2026-06-23/bg-report_pad1782196580176.png'; static const String bubble = '/StaticResource/2026-06-11/Group33(2)1781142719442.png'; // ---- colors ---- static const Color pageBg = Color(0xFF275FD9); // background } String _img(String path) => ImgUrl.build(path); class ReportComputationPage extends ConsumerStatefulWidget { const ReportComputationPage({super.key}); @override ConsumerState createState() => _ReportComputationPageState(); } class _ReportComputationPageState extends ConsumerState { List<_SubjectData> _subjectList = []; double _ringProgress = 0; bool _showFinishedPopup = false; Timer? _animTimer; /// 每个科目卡片的 key,用于动画切换时自动滚动到可见区域 /// (对标 Vue scroll-into-view / scrollToItemIfNeeded) final List _itemKeys = []; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _precacheImages(); _loadAndStart(); }); } @override void dispose() { _animTimer?.cancel(); super.dispose(); } /// 预加载弹窗/背景图,避免首次显示闪烁(对标 Vue onMounted getImageInfo) void _precacheImages() { final isPad = MediaQuery.sizeOf(context).width >= AppAdapt.tabletBreakpoint; for (final path in [ isPad ? _ComputationTheme.bgPad : _ComputationTheme.bg, ComputationFinishedPopup.topLightPadImg, ComputationFinishedPopup.contentBgImg, ]) { precacheImage(CachedNetworkImageProvider(_img(path)), context); } } 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(); final list = 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(); if (!mounted) return; setState(() { _subjectList = list; _itemKeys ..clear() ..addAll(List.generate(list.length, (_) => GlobalKey())); }); _startSubjects(0); } catch (e) { talker.error('reportComputation 加载失败', e); } } void _startSubjects(int index) { if (index >= _subjectList.length) { setState(() => _ringProgress = 100); // Vue:最后一个科目完成后延迟 500ms 弹弹窗 Timer(const Duration(milliseconds: 500), () { if (mounted) setState(() => _showFinishedPopup = true); }); return; } setState(() { _subjectList[index].status = ComputationSubjectStatus.inProgress; _subjectList[index].progress = 0; }); _scrollToItem(index); 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 = ComputationSubjectStatus.finished; }); _startSubjects(index + 1); } }); } /// 当前科目卡片不完全可见时,滚动到可见区域 void _scrollToItem(int index) { WidgetsBinding.instance.addPostFrameCallback((_) { final ctx = _itemKeys[index].currentContext; if (ctx != null) { Scrollable.ensureVisible( ctx, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, alignment: 0.2, ); } }); } /// 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) { final isPad = MediaQuery.sizeOf(context).width >= AppAdapt.tabletBreakpoint; final bottomPadding = MediaQuery.paddingOf(context).bottom; return Scaffold( backgroundColor: _ComputationTheme.pageBg, body: Stack( children: [ // ===== 背景图:宽铺满、居顶、高度自适应(Vue mode=widthFix)===== Positioned( top: 0, left: 0, right: 0, child: CachedNetworkImage( imageUrl: _img( isPad ? _ComputationTheme.bgPad : _ComputationTheme.bg, ), fit: BoxFit.fitWidth, errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), SafeArea( bottom: false, child: Column( children: [ SizedBox(height: 8.h), // AI核心算力气泡(454x98rpx) CachedNetworkImage( imageUrl: _img(_ComputationTheme.bubble), width: 227.w, height: 49.h, fit: BoxFit.fill, errorWidget: (_, _, _) => SizedBox(width: 227.w, height: 49.h), ), SizedBox(height: 7.5.h), // 圆环进度 ComputationProgressRing(progress: _ringProgress), // 科目列表 Expanded( child: ListView.separated( padding: EdgeInsets.fromLTRB( 14.w, 10.h, 14.w, 20.h + bottomPadding, ), itemCount: _subjectList.length, separatorBuilder: (_, _) => SizedBox(height: 7.5.h), itemBuilder: (_, index) { final data = _subjectList[index]; return ComputationSubjectItem( key: _itemKeys[index], subjectName: data.subjectReport.subjectName, iconUrl: data.subjectReport.icon, knowCount: data.knowCount, progress: data.progress, status: data.status, ); }, ), ), ], ), ), // finishedPopup 弹窗 if (_showFinishedPopup) ComputationFinishedPopup(onStart: _onFinish), ], ), ); } } // ==================== 内部数据类 ==================== class _SubjectData { final SubjectReportType subjectReport; int progress = 0; ComputationSubjectStatus status = ComputationSubjectStatus.notStarted; final int time; final int knowCount; _SubjectData({ required this.subjectReport, required this.time, required this.knowCount, }); }