import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:new_learn/core/api/lexicon/api.dart'; import 'package:new_learn/core/api/lexicon/model.dart'; import 'package:new_learn/core/components/center_toast.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/recite/recite_args.dart'; import 'package:new_learn/features/recite/widgets/recite_audio.dart'; import 'package:new_learn/features/recite/widgets/recite_circle_progress.dart'; import 'package:new_learn/features/recite/widgets/voice_anim_button.dart'; /// ============================================================ /// 【LevelOverPage】学习小结 —— 对标 Vue levelOver.vue /// /// 入参:extra = LevelOverArgs { info, chapterId }(替代 Vue URL JSON + storage) /// 章节标题:getLevels(chapterId)(对标 onLoad 里的 getLevels) /// 下一关:nextLevelId/nextChapterId 都有 → pushReplacement 答题页; /// 否则 toast「已到最后一关」。 /// ============================================================ class LevelOverPage extends StatefulWidget { const LevelOverPage({super.key, required this.args}); final LevelOverArgs args; @override State createState() => _LevelOverPageState(); } class _LevelOverPageState extends State { String _title = ''; LevelOverInfo get _info => widget.args.info; bool get _isPad => MediaQuery.sizeOf(context).width >= AppAdapt.tabletBreakpoint; @override void initState() { super.initState(); _loadTitle(); } @override void dispose() { ReciteAudioManager.instance.stopAll(); super.dispose(); } Future _loadTitle() async { try { final res = await LexiconApi.instance.getLevels(widget.args.chapterId); if (mounted && res.title.isNotEmpty) { setState(() => _title = res.title); } } catch (_) { // 标题仅展示用,失败静默 } } void _onNext() { if (_info.nextLevelId != 0 && _info.nextChapterId != 0) { // 对标 Vue uni.redirectTo context.pushReplacement( '/recite/level-start', extra: LevelStartArgs( chapterId: _info.nextChapterId, levelId: _info.nextLevelId, ), ); } else { CenterToast.show(context, '已到最后一关'); } } @override Widget build(BuildContext context) { final hPadding = _isPad ? 40.w : 20.w; return Scaffold( backgroundColor: const Color(0xFF1BC26E), extendBodyBehindAppBar: true, appBar: AppBar( backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, elevation: 0, centerTitle: true, title: Text( '学习小结', style: TextStyle(fontSize: 17.sp, color: Colors.white), ), iconTheme: const IconThemeData(color: Colors.white), ), body: Stack( children: [ // 背景线纹图(铺满宽度,顶部对齐) Positioned( top: 0, left: 0, right: 0, child: SafeArea( bottom: false, child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-04-03/img_summary_bgline.png', ), fit: BoxFit.fitWidth, errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), ), SafeArea( child: Padding( padding: EdgeInsets.only(top: 56.h), child: Stack( clipBehavior: Clip.none, children: [ // 彩旗(564rpx) Positioned( top: 0, left: 0, right: 0, child: Center( child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-04-03/img_summary_bunting.png', ), width: 282.w, fit: BoxFit.fitWidth, errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), ), // 猴子(192rpx) Positioned( top: 0, left: 0, right: 0, child: Center( child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-04-03/img_summary_monkey.png', ), width: 96.w, fit: BoxFit.fitWidth, errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), ), // 白色信息卡 Positioned.fill( top: 54.h, child: Padding( padding: EdgeInsets.fromLTRB( hPadding, 0, hPadding, _isPad ? 40.h : 20.h, ), child: _OverInfoPanel( info: _info, title: _title, onNext: _onNext, ), ), ), ], ), ), ), ], ), ); } } /// 白色信息卡:标题图 + 汇总灰板(正确率/总数/正确数)+ 单词回顾 + 下一关 class _OverInfoPanel extends StatelessWidget { const _OverInfoPanel({ required this.info, required this.title, required this.onNext, }); final LevelOverInfo info; final String title; final VoidCallback onNext; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: EdgeInsets.all(20.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16.r), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 顶部标题图(336rpx x 83rpx,悬浮在卡片上沿) Transform.translate( offset: Offset(0, -37.h), child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-04-03/img_summary_title.png', ), width: 168.w, fit: BoxFit.fitWidth, errorWidget: (_, _, _) => SizedBox(height: 20.h), ), ), Transform.translate( offset: Offset(0, -20.h), child: _SummaryBoard(info: info, title: title), ), // 单词回顾标题行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _ReviewTitle(), Row( children: [ Text( '错误:${info.wrongCount}', style: TextStyle( fontSize: 12.sp, color: const Color(0xFFF56245), ), ), SizedBox(width: 8.w), Text( '可在"错题"进行复习', style: TextStyle( fontSize: 12.sp, color: const Color(0xFF999999), ), ), ], ), ], ), SizedBox(height: 7.h), // 单词回顾列表(弹性高度) Flexible( child: ListView.separated( padding: EdgeInsets.symmetric(vertical: 7.h), itemCount: info.lexicons.length, separatorBuilder: (_, _) => SizedBox(height: 10.h), itemBuilder: (_, i) => _ReviewItem(item: info.lexicons[i]), ), ), SizedBox(height: 10.h), // 下一关 GestureDetector( behavior: HitTestBehavior.opaque, onTap: onNext, child: Container( width: double.infinity, height: 44.h, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF1BC26E), borderRadius: BorderRadius.circular(999.r), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 0, offset: const Offset(0, 2), ), ], ), child: Text( '下一关', style: TextStyle(fontSize: 15.sp, color: Colors.white), ), ), ), ], ), ); } } /// 汇总灰板:章节标题角标 + 正确率圆环 + 单词总数 + 正确单词 class _SummaryBoard extends StatelessWidget { const _SummaryBoard({required this.info, required this.title}); final LevelOverInfo info; final String title; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: EdgeInsets.fromLTRB(20.w, 25.5.h, 20.w, 14.h), decoration: BoxDecoration( color: const Color(0xFFF8F8F8), borderRadius: BorderRadius.circular(10.r), ), child: Stack( clipBehavior: Clip.none, children: [ // 悬浮章节标题角标 if (title.isNotEmpty) Positioned( top: -34.h / 2 - 8.h, left: 0, right: 0, child: Center( child: Container( constraints: BoxConstraints(minWidth: 103.w), padding: EdgeInsets.symmetric(horizontal: 16.w), height: 34.h, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFFF8F8F8), borderRadius: BorderRadius.circular(999.r), border: Border.all(color: Colors.white, width: 0.5), ), child: Text( title, maxLines: 1, style: TextStyle( fontSize: 13.sp, color: const Color(0xFF666666), ), ), ), ), ), Row( children: [ // 正确率圆环(Vue :clockwise="false") Expanded( child: Center( child: ReciteCircleProgress( percent: info.rate.toDouble(), size: 77, strokeWidth: 5, clockwise: false, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${info.rate}', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, color: const Color(0xFF2E3238), height: 1, ), ), Padding( padding: EdgeInsets.only(bottom: 1.h), child: Text( '%', style: TextStyle( fontSize: 8.sp, color: const Color(0xFF2E3238), height: 1, ), ), ), ], ), SizedBox(height: 4.h), Text( '正确率', style: TextStyle( fontSize: 10.sp, color: const Color(0xFF999999), ), ), ], ), ), ), ), Expanded( child: _Stat(value: '${info.lexiconCount}', label: '单词总数'), ), Expanded( child: _Stat(value: '${info.correctCount}', label: '正确单词'), ), ], ), ], ), ); } } /// 绿字数字 + 灰字标签 class _Stat extends StatelessWidget { const _Stat({required this.value, required this.label}); final String value; final String label; @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text( value, style: TextStyle(fontSize: 21.sp, color: const Color(0xFF1BC26E)), ), SizedBox(height: 6.h), Text( label, style: TextStyle(fontSize: 12.sp, color: const Color(0xFF999999)), ), ], ); } } /// 「单词回顾」左侧绿色竖条标题 class _ReviewTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container(width: 3.w, height: 16.h, color: const Color(0xFF1BC26E)), SizedBox(width: 8.w), Text( '单词回顾', style: TextStyle(fontSize: 14.sp, color: const Color(0xFF333333)), ), ], ); } } /// 回顾行:状态圆点 + 英文/中文 + 发音按钮 class _ReviewItem extends StatelessWidget { const _ReviewItem({required this.item}); final Lexicon item; @override Widget build(BuildContext context) { final dotColor = item.isCorrect ? const Color(0xFF1BC26E) : const Color(0xFFF56245); return Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 9.5.h), decoration: BoxDecoration( color: const Color(0xFFF8F8F8), borderRadius: BorderRadius.circular(10.r), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 8.w, height: 8.w, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: dotColor, width: 1.5), ), ), SizedBox(width: 6.w), Flexible( child: Text( item.english, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 15.sp, color: const Color(0xFF333333), ), ), ), ], ), SizedBox(height: 4.h), Text( item.chineseDesc, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12.sp, color: const Color(0xFF999999), ), ), ], ), ), SizedBox(width: 12.w), VoiceAnimButton(url: item.voiceUrl, size: 18), ], ), ); } }