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/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'; /// ============================================================ /// 【StudentInfoPage】诊断报告欢迎介绍页 —— 对标 Vue studentInfo.vue /// /// Vue 端用 tPageImg / tPageCss / tPageColor 从 themes/default.json 抽主题; /// Flutter 端未做主题抽离,所以直接硬编码 default.json 里这一页的资源路径、 /// 颜色值、渐变配置。集中在本文件顶部 _StudentTheme 常量,便于后续抽离。 /// /// 路由参数(GoRouteState.extra): /// context.go('/studentInfo', extra: {'type': 'placementTest'}); /// - type: placementTest(入门测)| exitTest(出门测) /// /// 完整链路:登录成功/全局拦截 → 此页 → H5壳子 → reportComputation(入门测) /// 或 switchTab 首页(出门测) /// ============================================================ /// 本页用到的所有主题资源(对标 Vue themes/default.json → pages/reportPackage/studentInfo) class _StudentTheme { // ---- 图片(default.json images)---- // Flutter 端用 network 加载,需拼 CDN 域名;这里存相对路径, // 使用时调 ImgUrl.build(path) static const String bg = '/StaticResource/2026-06-10/bg-report1781093056538.png'; static const String bubble = '/StaticResource/2026-06-12/report_kuang1781230963682.png'; static const String welcome = '/StaticResource/2026-06-10/Group3331781094167604.png'; static const String analytics = '/StaticResource/2026-06-10/analytics1781081543912.png'; static const String logo = '/StaticResource/2026-06-10/logo1781083733383.png'; static const String aiAnalysisTitle = '/StaticResource/2026-06-10/ai-analysis-title1781087439752.png'; static const String analysisTitle = '/StaticResource/2026-06-22/analysis-title1782110217839.png'; static const String levelA = '/StaticResource/2026-06-23/levelA1782178351618.png'; static const String levelB = '/StaticResource/2026-06-23/levelB1782178351618.png'; static const String levelC = '/StaticResource/2026-06-23/levelC1782178351618.png'; static const String verify = '/StaticResource/2026-06-10/verify1781081993368.png'; static const String starIcon = '/StaticResource/2026-06-10/star1781089096898.png'; // ---- 颜色(default.json colors)---- static const Color pageBg = Color(0xFFE5F6FE); // background static const Color highlight = Color(0xFF3D7DF5); // ---- 渐变(default.json css 里的 linear-gradient/rgba 转 Flutter)---- // item-bg: linear-gradient(180deg, #F5FBFF → #E5F4FF) static const LinearGradient itemBg = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFF5FBFF), Color(0xFFE5F4FF)], ); // item-border-color: linear-gradient(180deg, #FFFFFF → #C9E9FF(50%) → #BAE2FF) static const LinearGradient itemBorder = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFFFFFFF), Color(0xFFC9E9FF), Color(0xFFBAE2FF)], ); // analysis-view-bg: linear-gradient(180deg, #DAF3FF → #D4ECFF(52%) → #E5FAFB) static const LinearGradient analysisViewBg = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFDAF3FF), Color(0xFFD4ECFF), Color(0xFFE5FAFB)], ); // text-line: 底部渐变条(大字号等级的下划线) // linear-gradient(90deg, rgba(61,125,245,0) → rgba(29,156,255,0.2) → rgba(138,236,253,0.5)) static const LinearGradient textLine = LinearGradient( colors: [Color(0x003D7DF5), Color(0x331D9CFF), Color(0x808AECFD)], ); // button-text-bg: linear-gradient(0deg, #FFFFFF → #E0F2FF) static const LinearGradient buttonTextBg = LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Color(0xFFFFFFFF), Color(0xFFE0F2FF)], ); // start-button-bg 径向渐变 + #3d7df5(简化:Flutter 径向渐变不太好还原多层 radial, // 这里用线性渐变近似,视觉上接近即可) static const LinearGradient startButtonBg = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF5B9BFF), Color(0xFF3D7DF5)], ); } /// 图片 URL 拼接工具(对标 Vue $img('/StaticResource/xxx'),复用项目 ImgUrl.build) String _img(String path) => ImgUrl.build(path); class StudentInfoPage extends ConsumerStatefulWidget { const StudentInfoPage({super.key}); @override ConsumerState createState() => _StudentInfoPageState(); } class _StudentInfoPageState extends ConsumerState { UserReportType _report = const UserReportType(); bool _loading = true; String _reportType = 'placementTest'; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final extra = GoRouterState.of(context).extra as Map? ?? {}; _reportType = (extra['type'] as String?) ?? 'placementTest'; _loadReport(); }); } Future _loadReport() async { setState(() => _loading = true); try { final isExit = _reportType == 'exitTest'; final report = isExit ? await ReportApi.instance.getCurrentAfterReport() : await ReportApi.instance.getCurrentReport(); if (mounted) { setState(() { _report = report; _loading = false; }); } } catch (e) { talker.error('加载报告数据失败', e); if (mounted) setState(() => _loading = false); } } Future _onStart() async { if (_report.subjectReports.isEmpty) { if (_reportType == 'exitTest') { await ref.read(currentUserProvider.notifier).markShowAfterReportFalse(); } else { await ref.read(currentUserProvider.notifier).markShowReportFalse(); } if (mounted) context.go('/'); return; } context.push('/reportWebView', extra: {'type': _reportType}); } @override Widget build(BuildContext context) { final user = ref.watch(currentUserProvider).value; final userName = _report.userName.isNotEmpty ? _report.userName : (user?.userName ?? ''); return Scaffold( backgroundColor: _StudentTheme.pageBg, body: _loading ? const Center(child: CircularProgressIndicator()) : SizedBox.expand( child: Stack( children: [ // ===== 背景层(Vue bg-wrapper)铺满 ===== Positioned.fill( child: Image.network( _img(_StudentTheme.bg), fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ), // ===== 内容层(滚动,底部留出按钮高度)===== SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.only(bottom: 140), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildHeader(userName), const SizedBox(height: 130), // _buildAnalysisLevel(), // const SizedBox(height: 15), _buildSubjectCard(), const SizedBox(height: 14), _buildLogo(), ], ), ), ), // ===== 底部按钮(fixed,无边距)===== Positioned( left: 0, right: 0, bottom: 0, child: SafeArea(top: false, child: _buildBottomButton()), ), ], ), ), ); } // ===== 顶部问候区 + welcome 插画 ===== // Vue: bubble-wrap(背景图气泡 + userName)+ welcome-wrap(欢迎插画) Widget _buildHeader(String userName) { return Padding( padding: const EdgeInsets.only(left: 14, right: 14, top: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // bubble 气泡(用 DecoratedBox 做背景图) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( // borderRadius: BorderRadius.circular(12), image: DecorationImage( image: NetworkImage(_img(_StudentTheme.bubble)), fit: BoxFit.fill, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Hi . ', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w900, color: Colors.white, ), ), Text( userName, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w900, color: Colors.white, ), ), ], ), ), const SizedBox(height: 10), // welcome 插画(占位,没图时跳过) Image.network( _img(_StudentTheme.welcome), width: 165, fit: BoxFit.fitWidth, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ], ), ); } // ===== 当前档次等级卡片(Vue analysis-view,被注释掉的)===== Widget _buildAnalysisLevel() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 14), child: Container( width: double.infinity, // analysis-view-bg 渐变外层 decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: _StudentTheme.analysisViewBg, boxShadow: const [ BoxShadow( color: Color(0x1A3D7DF5), blurRadius: 15, offset: Offset(0, 5), ), ], ), padding: const EdgeInsets.fromLTRB(16, 18, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // analysis-title 图 Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.network( _img(_StudentTheme.analysisTitle), height: 18, fit: BoxFit.fitHeight, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ], ), const SizedBox(height: 12), // 内层白色卡片:大字号 level + 文字 Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.fromLTRB(16, 12, 16, 18), child: Stack( clipBehavior: Clip.none, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 大字号等级 + text-line 下划线 Stack( alignment: Alignment.bottomLeft, children: [ Text( _report.userLevel.isNotEmpty ? _report.userLevel : '--', style: const TextStyle( fontSize: 30, fontWeight: FontWeight.w600, color: Color(0xFF1D2B47), height: 1.0, ), ), // 底部渐变条 Positioned( left: 0, right: 0, bottom: -2, child: Container( height: 4, decoration: BoxDecoration( borderRadius: BorderRadius.circular(2), gradient: _StudentTheme.textLine, ), ), ), ], ), const SizedBox(height: 12), // ai-analysis-title 图("AI 智能诊断评定") Image.network( _img(_StudentTheme.aiAnalysisTitle), height: 14, fit: BoxFit.fitHeight, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ], ), // 右上角 level${level} 图标 Positioned( right: -18, top: -44, child: _LevelIcon(level: _report.userLevel), ), ], ), ), ], ), ), ); } // ===== 诊断科目分析卡片 ===== Widget _buildSubjectCard() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 14), child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // header: analytics 图标 + title + 数据已校准 Row( children: [ Image.network( _img(_StudentTheme.analytics), width: 18, height: 18, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), const SizedBox(width: 6), const Text( '诊断科目分析', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1D2B47), ), ), const Spacer(), Image.network( _img(_StudentTheme.verify), width: 16, height: 16, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), const SizedBox(width: 3), const Text( '数据已校准', style: TextStyle(fontSize: 11, color: Color(0xFF00CC96)), ), ], ), const SizedBox(height: 16), // subject list _report.subjectReports.isEmpty ? const Center( child: Text( '暂无数据', style: TextStyle(color: Color(0xFF687FAD)), ), ) : Wrap( spacing: 10, runSpacing: 10, children: _report.subjectReports .map((s) => _SubjectCard(subject: s)) .toList(), ), ], ), ), ); } // ===== logo ===== Widget _buildLogo() { return Center( child: Image.network( _img(_StudentTheme.logo), width: 65, // 130rpx ≈ 65dp fit: BoxFit.fitWidth, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ); } // ===== 底部按钮区(fixed 底部,无边距,圆角顶对齐屏幕)===== Widget _buildBottomButton() { return Container( width: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ RichText( text: TextSpan( style: const TextStyle( fontSize: 11, height: 1.6, color: Color(0xFF495A7A), ), children: [ WidgetSpan( alignment: PlaceholderAlignment.top, child: Padding( padding: const EdgeInsets.only(right: 6), child: Image.network( _img(_StudentTheme.aiAnalysisTitle), width: 95, fit: BoxFit.fitWidth, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ), ), const TextSpan(text: '本报告基于'), const TextSpan( text: '15年高考真题精研', style: TextStyle(color: Color(0xFF3D7DF5)), ), const TextSpan(text: '、'), const TextSpan( text: '50万道名校模拟题库', style: TextStyle(color: Color(0xFF3D7DF5)), ), const TextSpan(text: '及'), const TextSpan( text: '3,682个', style: TextStyle(color: Color(0xFF3D7DF5)), ), const TextSpan(text: '底层知识点拓扑结构,通过爱学智算引擎进行深度解析。'), ], ), ), const SizedBox(height: 10), // start-button SizedBox( width: double.infinity, height: 44, child: ElevatedButton( onPressed: _onStart, style: ElevatedButton.styleFrom( padding: EdgeInsets.zero, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(22), ), elevation: 0, backgroundColor: Colors.transparent, ), child: Ink( decoration: BoxDecoration( gradient: _StudentTheme.startButtonBg, borderRadius: BorderRadius.circular(22), border: Border.all(color: const Color(0x4DFFFFFF), width: 2), ), child: Container( alignment: Alignment.center, child: Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( _img(_StudentTheme.starIcon), width: 24, height: 24, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), const SizedBox(width: 6), const Text( '开启智能诊断报告', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ], ), ), ), ), ), ], ), ); } } /// 科目卡片 —— item-bg 渐变 + item-border-color 渐变边框 class _SubjectCard extends StatelessWidget { const _SubjectCard({required this.subject}); final SubjectReportType subject; @override Widget build(BuildContext context) { return Container( width: 72, // 144rpx height: 88, // 176rpx // 渐变边框用双层 Container 实现 decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: _StudentTheme.itemBorder, ), padding: const EdgeInsets.all(1), // 边框厚度 child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(11), gradient: _StudentTheme.itemBg, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: subject.icon.isNotEmpty ? Image.network( subject.icon, width: 40, height: 40, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon( Icons.menu_book, size: 40, color: Color(0xFF3D7DF5), ), ) : const Icon( Icons.menu_book, size: 40, color: Color(0xFF3D7DF5), ), ), const SizedBox(height: 8), Text( subject.subjectName, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF1D2B47), ), ), ], ), ), ); } } /// 右上角等级图标:levelA/B/C 图片,加载失败时用占位 class _LevelIcon extends StatelessWidget { const _LevelIcon({required this.level}); final String level; @override Widget build(BuildContext context) { // 大写化匹配 default.json 的 key final key = level.toUpperCase(); final iconPath = switch (key) { 'A' => _StudentTheme.levelA, 'B' => _StudentTheme.levelB, 'C' => _StudentTheme.levelC, _ => _StudentTheme.levelB, // 兜底 }; return Image.network( _img(iconPath), width: 110, height: 110, fit: BoxFit.contain, errorBuilder: (_, __, ___) => Container( width: 110, height: 110, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(22), gradient: _StudentTheme.startButtonBg, ), child: Text( key, style: const TextStyle( fontSize: 48, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ); } }