feat(report): 实现AI学习报告计算页及相关组件
1. 新增报告计算进度圆环、科目卡片、完成弹窗组件 2. 重构report_computation_page,替换旧的自定义组件为新封装组件 3. 新增student_info_page的主题资源和UI重构,适配Flutter屏幕适配 4. 新增临时调试入口用于开发测试
This commit is contained in:
@@ -1,21 +1,48 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:new_learn/core/api/auth/api.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/api.dart';
|
||||||
import 'package:new_learn/core/api/report/model.dart';
|
import 'package:new_learn/core/api/report/model.dart';
|
||||||
import 'package:new_learn/core/logger/app_talker.dart';
|
import 'package:new_learn/core/logger/app_talker.dart';
|
||||||
import 'package:new_learn/core/providers/user_provider.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
|
/// 【ReportComputationPage】AI 加载动画页 —— 对标 Vue reportComputation.vue
|
||||||
///
|
///
|
||||||
/// 入门测 H5 壳子看完所有科目报告 → navigateTo reportComputation →
|
/// 入门测 H5 壳子看完所有科目报告 → navigateTo reportComputation →
|
||||||
/// 圆环进度 + 每个科目依次"AI计算中"动画 → finishedPopup 弹窗 →
|
/// 圆环进度 + 每个科目依次"AI计算中"动画 → 500ms 后 finishedPopup 弹窗 →
|
||||||
/// 调 ChangeShowReport → refresh userProvider → switchTab 首页
|
/// 调 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 {
|
class ReportComputationPage extends ConsumerStatefulWidget {
|
||||||
const ReportComputationPage({super.key});
|
const ReportComputationPage({super.key});
|
||||||
|
|
||||||
@@ -26,15 +53,21 @@ class ReportComputationPage extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
||||||
List<_SubjectData> _subjectList = [];
|
List<_SubjectData> _subjectList = [];
|
||||||
int _ringProgress = 0;
|
double _ringProgress = 0;
|
||||||
bool _showFinishedPopup = false;
|
bool _showFinishedPopup = false;
|
||||||
int _currentIndex = -1;
|
|
||||||
Timer? _animTimer;
|
Timer? _animTimer;
|
||||||
|
|
||||||
|
/// 每个科目卡片的 key,用于动画切换时自动滚动到可见区域
|
||||||
|
/// (对标 Vue scroll-into-view / scrollToItemIfNeeded)
|
||||||
|
final List<GlobalKey> _itemKeys = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadAndStart());
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_precacheImages();
|
||||||
|
_loadAndStart();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -43,6 +76,18 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
super.dispose();
|
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<void> _loadAndStart() async {
|
Future<void> _loadAndStart() async {
|
||||||
try {
|
try {
|
||||||
final results = await Future.wait([
|
final results = await Future.wait([
|
||||||
@@ -55,8 +100,7 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
if (report.subjectReports.isEmpty) return;
|
if (report.subjectReports.isEmpty) return;
|
||||||
|
|
||||||
final itemTime = (15 / report.subjectReports.length).floor();
|
final itemTime = (15 / report.subjectReports.length).floor();
|
||||||
setState(() {
|
final list = report.subjectReports.map((item) {
|
||||||
_subjectList = report.subjectReports.map((item) {
|
|
||||||
final knowItem = knowList.isNotEmpty
|
final knowItem = knowList.isNotEmpty
|
||||||
? knowList.firstWhere(
|
? knowList.firstWhere(
|
||||||
(k) => k.subjectName == item.subjectName,
|
(k) => k.subjectName == item.subjectName,
|
||||||
@@ -69,6 +113,13 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
time: itemTime,
|
time: itemTime,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_subjectList = list;
|
||||||
|
_itemKeys
|
||||||
|
..clear()
|
||||||
|
..addAll(List.generate(list.length, (_) => GlobalKey()));
|
||||||
});
|
});
|
||||||
_startSubjects(0);
|
_startSubjects(0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -78,18 +129,19 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
|
|
||||||
void _startSubjects(int index) {
|
void _startSubjects(int index) {
|
||||||
if (index >= _subjectList.length) {
|
if (index >= _subjectList.length) {
|
||||||
setState(() {
|
setState(() => _ringProgress = 100);
|
||||||
_ringProgress = 100;
|
// Vue:最后一个科目完成后延迟 500ms 弹弹窗
|
||||||
_showFinishedPopup = true;
|
Timer(const Duration(milliseconds: 500), () {
|
||||||
|
if (mounted) setState(() => _showFinishedPopup = true);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIndex = index;
|
_subjectList[index].status = ComputationSubjectStatus.inProgress;
|
||||||
_subjectList[index].status = _SubjectStatus.inProgress;
|
|
||||||
_subjectList[index].progress = 0;
|
_subjectList[index].progress = 0;
|
||||||
});
|
});
|
||||||
|
_scrollToItem(index);
|
||||||
|
|
||||||
final totalTimeMs = _subjectList[index].time * 1000;
|
final totalTimeMs = _subjectList[index].time * 1000;
|
||||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
@@ -100,8 +152,7 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_subjectList[index].progress = progress;
|
_subjectList[index].progress = progress;
|
||||||
_ringProgress =
|
_ringProgress = (index * 100 + progress) / _subjectList.length;
|
||||||
((index * 100) + progress) ~/ _subjectList.length;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (elapsed >= totalTimeMs) {
|
if (elapsed >= totalTimeMs) {
|
||||||
@@ -109,13 +160,28 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
_animTimer = null;
|
_animTimer = null;
|
||||||
setState(() {
|
setState(() {
|
||||||
_subjectList[index].progress = 100;
|
_subjectList[index].progress = 100;
|
||||||
_subjectList[index].status = _SubjectStatus.finished;
|
_subjectList[index].status = ComputationSubjectStatus.finished;
|
||||||
});
|
});
|
||||||
_startSubjects(index + 1);
|
_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 的"立即开启学习蝶变"按钮
|
/// finishedPopup 的"立即开启学习蝶变"按钮
|
||||||
Future<void> _onFinish() async {
|
Future<void> _onFinish() async {
|
||||||
try {
|
try {
|
||||||
@@ -131,37 +197,64 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isPad = MediaQuery.sizeOf(context).width >= AppAdapt.tabletBreakpoint;
|
||||||
|
final bottomPadding = MediaQuery.paddingOf(context).bottom;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0F1A2E),
|
backgroundColor: _ComputationTheme.pageBg,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
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(
|
SafeArea(
|
||||||
|
bottom: false,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: 8.h),
|
||||||
const Text(
|
// AI核心算力气泡(454x98rpx)
|
||||||
'AI智能生成学习计划',
|
CachedNetworkImage(
|
||||||
style: TextStyle(
|
imageUrl: _img(_ComputationTheme.bubble),
|
||||||
color: Colors.white,
|
width: 227.w,
|
||||||
fontSize: 15,
|
height: 49.h,
|
||||||
fontWeight: FontWeight.w600,
|
fit: BoxFit.fill,
|
||||||
|
errorWidget: (_, _, _) =>
|
||||||
|
SizedBox(width: 227.w, height: 49.h),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 7.5.h),
|
||||||
const SizedBox(height: 24),
|
|
||||||
// 圆环进度
|
// 圆环进度
|
||||||
_ProgressRing(progress: _ringProgress),
|
ComputationProgressRing(progress: _ringProgress),
|
||||||
const SizedBox(height: 20),
|
|
||||||
// 科目列表
|
// 科目列表
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.fromLTRB(
|
||||||
horizontal: 14,
|
14.w,
|
||||||
vertical: 10,
|
10.h,
|
||||||
|
14.w,
|
||||||
|
20.h + bottomPadding,
|
||||||
),
|
),
|
||||||
itemCount: _subjectList.length,
|
itemCount: _subjectList.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
separatorBuilder: (_, _) => SizedBox(height: 7.5.h),
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
return _SubjectItem(data: _subjectList[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,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -169,55 +262,7 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// finishedPopup 弹窗
|
// finishedPopup 弹窗
|
||||||
if (_showFinishedPopup)
|
if (_showFinishedPopup) ComputationFinishedPopup(onStart: _onFinish),
|
||||||
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('立即开启学习蝶变'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -226,215 +271,16 @@ class _ReportComputationPageState extends ConsumerState<ReportComputationPage> {
|
|||||||
|
|
||||||
// ==================== 内部数据类 ====================
|
// ==================== 内部数据类 ====================
|
||||||
|
|
||||||
enum _SubjectStatus { notStarted, inProgress, finished }
|
|
||||||
|
|
||||||
class _SubjectData {
|
class _SubjectData {
|
||||||
final SubjectReportType subjectReport;
|
final SubjectReportType subjectReport;
|
||||||
int progress;
|
int progress = 0;
|
||||||
_SubjectStatus status;
|
ComputationSubjectStatus status = ComputationSubjectStatus.notStarted;
|
||||||
int time;
|
final int time;
|
||||||
int knowCount;
|
final int knowCount;
|
||||||
|
|
||||||
_SubjectData({
|
_SubjectData({
|
||||||
required this.subjectReport,
|
required this.subjectReport,
|
||||||
required this.time,
|
required this.time,
|
||||||
required this.knowCount,
|
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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/api.dart';
|
||||||
import 'package:new_learn/core/api/report/model.dart';
|
import 'package:new_learn/core/api/report/model.dart';
|
||||||
import 'package:new_learn/core/config/app_config.dart';
|
|
||||||
import 'package:new_learn/core/logger/app_talker.dart';
|
import 'package:new_learn/core/logger/app_talker.dart';
|
||||||
import 'package:new_learn/core/network/dio_client.dart';
|
|
||||||
import 'package:new_learn/core/providers/user_provider.dart';
|
import 'package:new_learn/core/providers/user_provider.dart';
|
||||||
|
import 'package:new_learn/core/utils/img_url.dart';
|
||||||
|
|
||||||
/// ============================================================
|
/// ============================================================
|
||||||
/// 【StudentInfoPage】诊断报告欢迎介绍页 —— 对标 Vue studentInfo.vue
|
/// 【StudentInfoPage】诊断报告欢迎介绍页 —— 对标 Vue studentInfo.vue
|
||||||
///
|
///
|
||||||
|
/// Vue 端用 tPageImg / tPageCss / tPageColor 从 themes/default.json 抽主题;
|
||||||
|
/// Flutter 端未做主题抽离,所以直接硬编码 default.json 里这一页的资源路径、
|
||||||
|
/// 颜色值、渐变配置。集中在本文件顶部 _StudentTheme 常量,便于后续抽离。
|
||||||
|
///
|
||||||
/// 路由参数(GoRouteState.extra):
|
/// 路由参数(GoRouteState.extra):
|
||||||
/// context.go('/studentInfo', extra: {'type': 'placementTest'});
|
/// context.go('/studentInfo', extra: {'type': 'placementTest'});
|
||||||
/// - type: placementTest(入门测)| exitTest(出门测)
|
/// - type: placementTest(入门测)| exitTest(出门测)
|
||||||
@@ -19,6 +21,87 @@ import 'package:new_learn/core/providers/user_provider.dart';
|
|||||||
/// 完整链路:登录成功/全局拦截 → 此页 → H5壳子 → reportComputation(入门测)
|
/// 完整链路:登录成功/全局拦截 → 此页 → H5壳子 → reportComputation(入门测)
|
||||||
/// 或 switchTab 首页(出门测)
|
/// 或 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 {
|
class StudentInfoPage extends ConsumerStatefulWidget {
|
||||||
const StudentInfoPage({super.key});
|
const StudentInfoPage({super.key});
|
||||||
|
|
||||||
@@ -35,7 +118,8 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final extra = GoRouterState.of(context).extra as Map<String, dynamic>? ?? {};
|
final extra =
|
||||||
|
GoRouterState.of(context).extra as Map<String, dynamic>? ?? {};
|
||||||
_reportType = (extra['type'] as String?) ?? 'placementTest';
|
_reportType = (extra['type'] as String?) ?? 'placementTest';
|
||||||
_loadReport();
|
_loadReport();
|
||||||
});
|
});
|
||||||
@@ -45,18 +129,9 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
try {
|
try {
|
||||||
final isExit = _reportType == 'exitTest';
|
final isExit = _reportType == 'exitTest';
|
||||||
final apiPath = isExit
|
|
||||||
? '/api/v1/UserReport/GetCurrentAfterReport'
|
|
||||||
: '/api/v1/UserReport/GetCurrentReport';
|
|
||||||
talker.debug('studentInfo 调 $apiPath');
|
|
||||||
final report = isExit
|
final report = isExit
|
||||||
? await ReportApi.instance.getCurrentAfterReport()
|
? await ReportApi.instance.getCurrentAfterReport()
|
||||||
: await ReportApi.instance.getCurrentReport();
|
: await ReportApi.instance.getCurrentReport();
|
||||||
talker.debug(
|
|
||||||
'studentInfo 响应: userName=${report.userName} '
|
|
||||||
'subjectReports.length=${report.subjectReports.length} '
|
|
||||||
'raw=${report.subjectReports.toString()}',
|
|
||||||
);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_report = report;
|
_report = report;
|
||||||
@@ -69,11 +144,8 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按钮点击:subjectReports 为空 → 降级置 false 回首页;否则进 H5 壳子
|
|
||||||
Future<void> _onStart() async {
|
Future<void> _onStart() async {
|
||||||
if (_report.subjectReports.isEmpty) {
|
if (_report.subjectReports.isEmpty) {
|
||||||
// 降级:只改本地标记,不调后端
|
|
||||||
talker.debug('studentInfo: subjectReports 为空,降级本地置 false');
|
|
||||||
if (_reportType == 'exitTest') {
|
if (_reportType == 'exitTest') {
|
||||||
await ref.read(currentUserProvider.notifier).markShowAfterReportFalse();
|
await ref.read(currentUserProvider.notifier).markShowAfterReportFalse();
|
||||||
} else {
|
} else {
|
||||||
@@ -82,10 +154,7 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
if (mounted) context.go('/');
|
if (mounted) context.go('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 进 H5 壳子
|
context.push('/reportWebView', extra: {'type': _reportType});
|
||||||
context.push('/reportWebView', extra: {
|
|
||||||
'type': _reportType,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -96,29 +165,76 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
: (user?.userName ?? '');
|
: (user?.userName ?? '');
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F8FF),
|
backgroundColor: _StudentTheme.pageBg,
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back_ios_new, color: Color(0xFF1D2B47)),
|
|
||||||
onPressed: () => context.canPop() ? context.pop() : context.go('/'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: _loading
|
body: _loading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: SafeArea(
|
: SizedBox.expand(
|
||||||
child: Column(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// 顶部问候
|
// ===== 背景层(Vue bg-wrapper)铺满 =====
|
||||||
Padding(
|
Positioned.fill(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 28),
|
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(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Hi . ',
|
'Hi . ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
@@ -126,7 +242,7 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
Text(
|
Text(
|
||||||
userName,
|
userName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
@@ -134,16 +250,148 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 科目卡片列表
|
const SizedBox(height: 10),
|
||||||
Expanded(
|
// welcome 插画(占位,没图时跳过)
|
||||||
child: SingleChildScrollView(
|
Image.network(
|
||||||
padding: const EdgeInsets.fromLTRB(28, 120, 28, 20),
|
_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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
// analysis-title 图
|
||||||
padding: EdgeInsets.only(bottom: 12),
|
Row(
|
||||||
child: Text(
|
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(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -151,15 +399,23 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
color: Color(0xFF1D2B47),
|
color: Color(0xFF1D2B47),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Image.network(
|
||||||
|
_img(_StudentTheme.verify),
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
Container(
|
const SizedBox(width: 3),
|
||||||
width: double.infinity,
|
const Text(
|
||||||
padding: const EdgeInsets.all(16),
|
'数据已校准',
|
||||||
decoration: BoxDecoration(
|
style: TextStyle(fontSize: 11, color: Color(0xFF00CC96)),
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
),
|
||||||
child: _report.subjectReports.isEmpty
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// subject list
|
||||||
|
_report.subjectReports.isEmpty
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'暂无数据',
|
'暂无数据',
|
||||||
@@ -170,49 +426,133 @@ class _StudentInfoPageState extends ConsumerState<StudentInfoPage> {
|
|||||||
spacing: 10,
|
spacing: 10,
|
||||||
runSpacing: 10,
|
runSpacing: 10,
|
||||||
children: _report.subjectReports
|
children: _report.subjectReports
|
||||||
.map((s) => _SubjectCard(
|
.map((s) => _SubjectCard(subject: s))
|
||||||
subject: s,
|
|
||||||
))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 底部按钮
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(29, 20, 29, 20),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 50,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _onStart,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFF3D7DF5),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'开启智能诊断报告',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 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 {
|
class _SubjectCard extends StatelessWidget {
|
||||||
const _SubjectCard({required this.subject});
|
const _SubjectCard({required this.subject});
|
||||||
final SubjectReportType subject;
|
final SubjectReportType subject;
|
||||||
@@ -220,15 +560,18 @@ class _SubjectCard extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 72,
|
width: 72, // 144rpx
|
||||||
height: 88,
|
height: 88, // 176rpx
|
||||||
|
// 渐变边框用双层 Container 实现
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
gradient: const LinearGradient(
|
gradient: _StudentTheme.itemBorder,
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Color(0xFFF5F8FF), Color(0xFFE6EEFF)],
|
|
||||||
),
|
),
|
||||||
|
padding: const EdgeInsets.all(1), // 边框厚度
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(11),
|
||||||
|
gradient: _StudentTheme.itemBg,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -264,6 +607,48 @@ class _SubjectCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 右上角等级图标: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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
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';
|
||||||
|
import 'package:new_learn/core/utils/screen_adapt_template.dart';
|
||||||
|
|
||||||
|
/// ============================================================
|
||||||
|
/// 【ComputationFinishedPopup】报告生成完成弹窗
|
||||||
|
/// —— 对标 Vue finishedPopup.vue
|
||||||
|
///
|
||||||
|
/// 黑色 60% 遮罩 + 顶部 62% 屏高的 top-light 光效图 +
|
||||||
|
/// 640x570rpx content-bg 内容图,底部「立即开启学习蝶变」渐变按钮,
|
||||||
|
/// 入场 0.5s 淡入上移(popup-fade-up)。
|
||||||
|
/// ============================================================
|
||||||
|
class ComputationFinishedPopup extends StatefulWidget {
|
||||||
|
const ComputationFinishedPopup({super.key, required this.onStart});
|
||||||
|
|
||||||
|
final VoidCallback onStart;
|
||||||
|
|
||||||
|
static const String topLightImg =
|
||||||
|
'/StaticResource/2026-06-11/top-light1781173654372.png';
|
||||||
|
static const String topLightPadImg =
|
||||||
|
'/StaticResource/2026-06-24/top-light_pad1782270110001.png';
|
||||||
|
static const String contentBgImg =
|
||||||
|
'/StaticResource/2026-06-11/content-bg1781173794633.png';
|
||||||
|
static const String starImg =
|
||||||
|
'/StaticResource/2026-06-10/star1781089096898.png';
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ComputationFinishedPopup> createState() =>
|
||||||
|
_ComputationFinishedPopupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ComputationFinishedPopupState extends State<ComputationFinishedPopup>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
)..forward();
|
||||||
|
|
||||||
|
late final Animation<double> _opacity = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
late final Animation<Offset> _offset =
|
||||||
|
Tween(begin: const Offset(0, 0.08), end: Offset.zero).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final screenHeight = MediaQuery.sizeOf(context).height;
|
||||||
|
final isPad =
|
||||||
|
MediaQuery.sizeOf(context).width >= AppAdapt.tabletBreakpoint;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// popup-mask
|
||||||
|
const Positioned.fill(child: ColoredBox(color: Color(0x99000000))),
|
||||||
|
// top-light:宽铺满,高 62%,居顶
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: screenHeight * 0.62,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: ImgUrl.build(
|
||||||
|
isPad
|
||||||
|
? ComputationFinishedPopup.topLightPadImg
|
||||||
|
: ComputationFinishedPopup.topLightImg,
|
||||||
|
),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
errorWidget: (_, _, _) => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// popup-content:640x570rpx
|
||||||
|
Center(
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _opacity,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _offset,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 320.w,
|
||||||
|
height: 310.h,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl:
|
||||||
|
ImgUrl.build(ComputationFinishedPopup.contentBgImg),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
errorWidget: (_, _, _) => DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8.r),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// gogo-button:90% 宽,距底 50rpx
|
||||||
|
Positioned(
|
||||||
|
left: 16.w,
|
||||||
|
right: 16.w,
|
||||||
|
bottom: 25.h,
|
||||||
|
child: _buildStartButton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStartButton() {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: widget.onStart,
|
||||||
|
child: Container(
|
||||||
|
height: 50.h, // 100rpx
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
// start-button-bg 多层 radial-gradient 用线性渐变近似
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Color(0xFF6AA2FF), Color(0xFF3D7DF5)],
|
||||||
|
),
|
||||||
|
border: Border.all(color: const Color(0x4DFFFFFF), width: 1.w),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x3D3D7DF5),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CachedNetworkImage(
|
||||||
|
imageUrl: ImgUrl.build(ComputationFinishedPopup.starImg),
|
||||||
|
width: 24.w,
|
||||||
|
height: 24.w,
|
||||||
|
errorWidget: (_, _, _) => SizedBox(width: 24.w, height: 24.w),
|
||||||
|
),
|
||||||
|
SizedBox(width: 6.w),
|
||||||
|
Text(
|
||||||
|
'立即开启学习蝶变',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.spMin,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/// 科目计算状态(对标 Vue SubjectStatus)
|
||||||
|
enum ComputationSubjectStatus { notStarted, inProgress, finished }
|
||||||
|
|
||||||
|
/// ============================================================
|
||||||
|
/// 【ComputationSubjectItem】AI 计算科目卡片
|
||||||
|
/// —— 对标 reportComputation.vue 的 .subject-item
|
||||||
|
///
|
||||||
|
/// 右上角标(subscript):未开始/进行中蓝色闪电底,已完成绿色对勾底;
|
||||||
|
/// 标题行:科目名 + 科目图标 + ai-plan「AI学习计划+」图;
|
||||||
|
/// 知识点行 + 底部渐变进度条(进行中蓝 / 已完成绿)。
|
||||||
|
/// ============================================================
|
||||||
|
class ComputationSubjectItem extends StatelessWidget {
|
||||||
|
const ComputationSubjectItem({
|
||||||
|
super.key,
|
||||||
|
required this.subjectName,
|
||||||
|
required this.iconUrl,
|
||||||
|
required this.knowCount,
|
||||||
|
required this.progress,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String subjectName;
|
||||||
|
final String iconUrl;
|
||||||
|
final int knowCount;
|
||||||
|
|
||||||
|
/// 0-100
|
||||||
|
final int progress;
|
||||||
|
final ComputationSubjectStatus status;
|
||||||
|
|
||||||
|
// ---- 图片资源(default.json ai-plan + Vue 内联 $img 角标图)----
|
||||||
|
static const String _aiPlanImg =
|
||||||
|
'/StaticResource/2026-06-11/aiPlan1781157211012.png';
|
||||||
|
static const String _subscriptBgBlue =
|
||||||
|
'/StaticResource/2026-06-11/subscript-bg11781162055363.png';
|
||||||
|
static const String _subscriptBgGreen =
|
||||||
|
'/StaticResource/2026-06-11/subscript-bg21781162055358.png';
|
||||||
|
static const String _iconFlash =
|
||||||
|
'/StaticResource/2026-06-11/subscript-cion1-1781162401643.png';
|
||||||
|
static const String _iconCheck =
|
||||||
|
'/StaticResource/2026-06-11/subscript-cion2-1781162401644.png';
|
||||||
|
|
||||||
|
bool get _isFinished => status == ComputationSubjectStatus.finished;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Opacity(
|
||||||
|
// Vue: 未开始 opacity 0.8
|
||||||
|
opacity: status == ComputationSubjectStatus.notStarted ? 0.8 : 1,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16.r), // 16rpx
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
stops: const [0, 0.3, 1],
|
||||||
|
colors: _isFinished
|
||||||
|
? const [Color(0xFFE3FCF4), Color(0xFFF7FCFA), Colors.white]
|
||||||
|
: const [Color(0xFFE3F7FC), Color(0xFFF7FBFC), Colors.white],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(14.w, 14.w, 14.w, 20.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildTitle(),
|
||||||
|
SizedBox(height: 16.h), // 知识点行 margin 32rpx 0
|
||||||
|
_buildKnowledge(),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
_buildProgressBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 右上角标
|
||||||
|
Positioned(top: 0, right: 0, child: _buildSubscript()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 标题行:科目名 + 科目图标 + AI学习计划图 =====
|
||||||
|
Widget _buildTitle() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
subjectName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.spMin, // 36rpx
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: const Color(0xFF1D2B47),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 6.w),
|
||||||
|
if (iconUrl.isNotEmpty)
|
||||||
|
CachedNetworkImage(
|
||||||
|
imageUrl: ImgUrl.build(iconUrl),
|
||||||
|
width: 26.w, // 52rpx,widthFix
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
errorWidget: (_, _, _) => Icon(
|
||||||
|
Icons.menu_book,
|
||||||
|
size: 26.w,
|
||||||
|
color: const Color(0xFF3D7DF5),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Icon(Icons.menu_book, size: 26.w, color: const Color(0xFF3D7DF5)),
|
||||||
|
SizedBox(width: 6.w),
|
||||||
|
CachedNetworkImage(
|
||||||
|
imageUrl: ImgUrl.build(_aiPlanImg),
|
||||||
|
width: 77.w, // 154rpx,widthFix
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
errorWidget: (_, _, _) => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 知识点行(Vue 预计时间已注释,只保留知识点)=====
|
||||||
|
Widget _buildKnowledge() {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF3F8FE),
|
||||||
|
borderRadius: BorderRadius.circular(4.r),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'知识点',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.spMin,
|
||||||
|
color: const Color(0xFF495A7A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 6.w),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 3.5.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE1EAFA),
|
||||||
|
borderRadius: BorderRadius.circular(4.r),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$knowCount',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.spMin,
|
||||||
|
color: const Color(0xFF495A7A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 底部进度条:蓝渐变 / 绿渐变 =====
|
||||||
|
Widget _buildProgressBar() {
|
||||||
|
return SizedBox(
|
||||||
|
height: 6.h, // 12rpx
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
final fillWidth =
|
||||||
|
constraints.maxWidth * (progress.clamp(0, 100) / 100);
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE9EDF5),
|
||||||
|
borderRadius: BorderRadius.circular(3.h),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Container(
|
||||||
|
width: fillWidth,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(3.h),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: _isFinished
|
||||||
|
? const [Color(0xFFB4EADA), Color(0xFF19CE92)]
|
||||||
|
: const [
|
||||||
|
Color(0xFF8AD1FD),
|
||||||
|
Color(0xFF3C84FF),
|
||||||
|
Color(0xFF3D7DF5),
|
||||||
|
],
|
||||||
|
stops: _isFinished ? null : const [0, 0.64, 1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 右上角标:162x66rpx 背景图 + 状态图标 + 百分比 =====
|
||||||
|
Widget _buildSubscript() {
|
||||||
|
final bgImg = _isFinished ? _subscriptBgGreen : _subscriptBgBlue;
|
||||||
|
return SizedBox(
|
||||||
|
width: 81.w,
|
||||||
|
height: 33.h,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: ImgUrl.build(bgImg),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
errorWidget: (_, _, _) => DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _isFinished
|
||||||
|
? const Color(0xFF19CE92)
|
||||||
|
: const Color(0xFF3D7DF5),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(8.r),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 5.h),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (status != ComputationSubjectStatus.notStarted)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(right: 4.w),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: ImgUrl.build(
|
||||||
|
_isFinished ? _iconCheck : _iconFlash,
|
||||||
|
),
|
||||||
|
width: 18.w,
|
||||||
|
height: 18.w,
|
||||||
|
errorWidget: (_, _, _) =>
|
||||||
|
SizedBox(width: 18.w, height: 18.w),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$progress%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.spMin, // 32rpx
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
// ignore: unused_import —— 调试结束切回 TabApp 时使用
|
||||||
import 'package:new_learn/core/app.dart';
|
import 'package:new_learn/core/app.dart';
|
||||||
import 'package:new_learn/core/network/dio_client.dart';
|
import 'package:new_learn/core/network/dio_client.dart';
|
||||||
import 'package:new_learn/core/providers/privacy_provider.dart';
|
import 'package:new_learn/core/providers/privacy_provider.dart';
|
||||||
|
import 'package:new_learn/core/router/app_router.dart';
|
||||||
import 'package:new_learn/core/storage/database/app_database.dart';
|
import 'package:new_learn/core/storage/database/app_database.dart';
|
||||||
import 'package:new_learn/core/storage/privacy_storage.dart';
|
import 'package:new_learn/core/storage/privacy_storage.dart';
|
||||||
|
import 'package:new_learn/core/utils/screen_adapt_template.dart';
|
||||||
|
|
||||||
/// App 启动入口 —— 真正的业务代码都在 app.dart / pages/ 里
|
/// App 启动入口 —— 真正的业务代码都在 app.dart / pages/ 里
|
||||||
///
|
///
|
||||||
@@ -127,4 +131,44 @@ Future<void> main() async {
|
|||||||
await AppDatabase.instance.init();
|
await AppDatabase.instance.init();
|
||||||
// ProviderScope:Riverpod 所有 provider 的容器,必须包在根
|
// ProviderScope:Riverpod 所有 provider 的容器,必须包在根
|
||||||
runApp(const ProviderScope(child: TabApp()));
|
runApp(const ProviderScope(child: TabApp()));
|
||||||
|
// 临时调试用,跑完改回。
|
||||||
|
// 注意两点:
|
||||||
|
// 1. 页面组件用了 .w/.h/.spMin,必须包 ScreenUtilInit(与 TabApp 一致),
|
||||||
|
// 否则 ScreenUtil 的 late 字段未初始化会抛 LateInitializationError;
|
||||||
|
// 2. 必须用真实 appRouter(MaterialApp.router),不能用裸 MaterialApp(home:),
|
||||||
|
// 否则页面里 context.go('/') 找不到 GoRouter,点"立即开启学习蝶变"不会跳首页。
|
||||||
|
// runApp(const ProviderScope(child: _DebugApp()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 临时调试壳:ScreenUtilInit + 真实路由表,启动后直接进 /reportComputation
|
||||||
|
class _DebugApp extends StatefulWidget {
|
||||||
|
const _DebugApp();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DebugApp> createState() => _DebugAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DebugAppState extends State<_DebugApp> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 首帧后跳到调试页(默认初始位置是 '/',直接进会先闪一帧首页)
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
appRouter.go('/reportComputation');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ScreenUtilInit(
|
||||||
|
designSize: AppAdapt.designSizeOf(context),
|
||||||
|
minTextAdapt: true,
|
||||||
|
splitScreenMode: true,
|
||||||
|
fontSizeResolver: AppAdapt.fontSizeResolver,
|
||||||
|
builder: (context, _) => MaterialApp.router(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
routerConfig: appRouter,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user