实现了入门测/出门测报告的完整链路: 1. 新增UserInfo模型的出门测报告标记字段与相关接口 2. 新增全局报告路由拦截器,自动在登录后/应用回前台时检查报告状态 3. 新增报告相关API、页面与路由配置,包含欢迎页、H5壳子、AI计算页 4. 适配登录成功后的报告跳转逻辑,更新用户状态管理方法
441 lines
14 KiB
Dart
441 lines
14 KiB
Dart
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<ReportComputationPage> createState() =>
|
|
_ReportComputationPageState();
|
|
}
|
|
|
|
class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|
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<void> _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<UserReportKnowType>;
|
|
|
|
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<void> _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>(
|
|
Color(0xFF1D2B47),
|
|
),
|
|
),
|
|
),
|
|
// 进度圈
|
|
SizedBox(
|
|
width: 240,
|
|
height: 240,
|
|
child: CircularProgressIndicator(
|
|
value: progress / 100,
|
|
strokeWidth: 8,
|
|
backgroundColor: Colors.transparent,
|
|
valueColor: const AlwaysStoppedAnimation<Color>(
|
|
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<Color>(
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|