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/widgets/recite_circle_progress.dart'; import 'package:new_learn/features/recite/widgets/recite_progress.dart'; /// ============================================================ /// 【ChaptersPage】词汇章节 —— 对标 Vue pages/recitePackage/lexicon/lexicon.vue /// /// 入参 bookId(路由 path 参数,对标 Vue onLoad options.bookId,默认 1)。 /// 数据:getChapters(bookId),失败 toast「页面加载失败,请重试」。 /// 点章节 → /recite/levels(extra 传整个 Chapter,替代 eventChannel); /// 从关卡链路返回后重新拉取(对标 Vue onShow 刷新)。 /// ============================================================ class ChaptersPage extends StatefulWidget { const ChaptersPage({super.key, required this.bookId}); final int bookId; @override State createState() => _ChaptersPageState(); } class _ChaptersPageState extends State { final List _list = []; double _rate = 0; bool _loading = false; bool get _isPad => MediaQuery.sizeOf(context).width >= AppAdapt.tabletBreakpoint; @override void initState() { super.initState(); _getList(); } Future _getList() async { if (mounted) setState(() => _loading = true); try { final res = await LexiconApi.instance.getChapters(widget.bookId); if (!mounted) return; setState(() { _rate = res.totalFinishProgress * 100; _list ..clear() ..addAll(res.chapters); }); } catch (_) { if (mounted) { CenterToast.show( context, '页面加载失败,请重试', duration: const Duration(milliseconds: 2000), ); } } finally { if (mounted) setState(() => _loading = false); } } Future _onClickCell(Chapter item) async { await context.push('/recite/levels', extra: item); if (mounted) _getList(); } @override Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, elevation: 0, iconTheme: const IconThemeData(color: Color(0xFF1D2B47)), ), body: Stack( children: [ // 顶部背景大图 Positioned( top: 0, left: 0, right: 0, child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-04-01/img_index_bgtop.png', ), fit: BoxFit.fitWidth, placeholder: (_, _) => const SizedBox.shrink(), errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), // 白色内容卡(mp margin-top 19%,pad 30%) Positioned.fill( top: 0, child: SafeArea( child: Column( children: [ Expanded( child: LayoutBuilder( builder: (context, constraints) { final topGap = constraints.maxHeight * (_isPad ? 0.30 : 0.19); return Padding( // Vue: -8rpx 让卡片向两侧溢出 padding: EdgeInsets.only(top: topGap), child: Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical( top: Radius.circular(_isPad ? 30.r : 24.r), ), ), child: Stack( clipBehavior: Clip.none, children: [ _buildContent(), // 鳄鱼装饰(top:-35rpx left:30rpx;pad 加大) Positioned( top: -17.5.h, left: _isPad ? 30.w : 15.w, child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-04-01/img_index_crocodile.png', ), width: _isPad ? 160.w : 109.w, fit: BoxFit.fitWidth, placeholder: (_, _) => const SizedBox.shrink(), errorWidget: (_, _, _) => const SizedBox.shrink(), ), ), // 总进度圆环(top:-35rpx right:50rpx) Positioned( top: -17.5.h, right: _isPad ? 50.w : 25.w, child: _CircleTotalProgress(rate: _rate), ), ], ), ), ); }, ), ), ], ), ), ), ], ), ); } Widget _buildContent() { return Padding( // Vue chapter-list: 40rpx 横向,50rpx 顶 padding: EdgeInsets.fromLTRB(20.w, 25.h, 20.w, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTitle(), Expanded( child: _loading ? Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 16.w, height: 16.w, child: const CircularProgressIndicator( strokeWidth: 2, color: Color(0xFF1BC26E), ), ), SizedBox(width: 8.w), Text( '努力加载中...', style: TextStyle( fontSize: 12.sp, color: const Color(0xFF999999), ), ), ], ), ) : ListView.builder( padding: EdgeInsets.only(top: 20.h), itemCount: _list.length, itemBuilder: (_, i) => _ChapterItem(item: _list[i], onTap: _onClickCell), ), ), ], ), ); } /// 词汇章节标题 + 短渐变线 Widget _buildTitle() { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '词汇章节', style: TextStyle( fontSize: 16.sp, color: const Color(0xFF333333), fontWeight: FontWeight.w500, ), ), SizedBox(height: 2.h), Container( width: 62.w, height: 4.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(2.r), gradient: const LinearGradient( colors: [Color(0x661BC26E), Color(0x667ED8AA)], ), ), ), ], ); } } /// 总进度圆环(白色衬底圆 + 绿色环 + 总进度文案) class _CircleTotalProgress extends StatelessWidget { const _CircleTotalProgress({required this.rate}); final double rate; @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 67.5.w, height: 67.5.w, child: Stack( alignment: Alignment.center, children: [ // 白圆衬底(Vue 135rpx,top/left -8rpx) Container( width: 67.5.w, height: 67.5.w, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 8, offset: const Offset(0, 2), ), ], ), ), ReciteCircleProgress( percent: rate, size: 60, strokeWidth: 4, child: Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${rate.toInt()}', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w600, 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: 2.h), Text( '总进度', style: TextStyle(fontSize: 10.sp, color: const Color(0xFF999999)), ), ], ); } } /// 章节列表项(左封面叠字 + 右侧渐变信息卡) class _ChapterItem extends StatelessWidget { const _ChapterItem({required this.item, required this.onTap}); final Chapter item; final ValueChanged onTap; @override Widget build(BuildContext context) { final percent = (item.finishProgress * 100).toInt(); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => onTap(item), child: Container( height: 100.h, margin: EdgeInsets.symmetric(vertical: 10.h), child: Stack( children: [ // 渐变信息卡(左留 96 给封面;高度 90 贴底) Positioned( left: 0, right: 0, bottom: 0, height: 90.h, child: Container( padding: EdgeInsets.fromLTRB(96.w, 7.5.h, 14.w, 10.h), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.r), gradient: const LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [Color(0xFFE1FAED), Colors.white], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 15.sp, color: const Color(0xFF333333), ), ), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Text( '已完成', style: TextStyle( fontSize: 10.sp, color: const Color(0xFF999999), ), ), SizedBox(width: 3.w), Text( '$percent%', style: TextStyle( fontSize: 12.sp, color: const Color(0xFF1BC26E), ), ), ], ), Row( children: [ Text( '${item.finishLexiconCount}', style: TextStyle( fontSize: 10.sp, color: const Color(0xFF1BC26E), ), ), Text( '/${item.lexiconCount}', style: TextStyle( fontSize: 10.sp, color: const Color(0xFF999999), ), ), ], ), ], ), SizedBox(height: 6.h), SegmentedProgressBar(progress: item.finishProgress * 100), ], ), ), ), // 左侧封面(height 100,叠白色 “index title”) Positioned( left: 0, top: 0, bottom: 0, child: CachedNetworkImage( imageUrl: ImgUrl.build(item.cover), fit: BoxFit.fitHeight, placeholder: (_, _) => SizedBox(width: 96.w), errorWidget: (_, _, _) => SizedBox(width: 96.w), imageBuilder: (_, provider) => Container( foregroundDecoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.35), Colors.transparent, ], stops: const [0, 0.45], ), ), child: Image(image: provider, fit: BoxFit.fitHeight), ), ), ), Positioned( left: 12.5.w, top: 9.h, right: 4.w, child: Text( item.index.isEmpty ? item.title : '${item.index} ${item.title}', maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11.sp, color: Colors.white), ), ), ], ), ), ); } }