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/api/class/model.dart'; import 'package:new_learn/core/components/user_circle_avatar.dart'; import 'package:new_learn/core/utils/img_url.dart'; /// ============================================================ /// 【积分排行组件】由 point_rank_view.dart 抽离的展示组件 /// /// RankRow(排行榜单行)/ EnergyRuleModal(积分规则弹窗)/ /// RuleSection(规则段落)/ TriangleClipper(白三角裁剪) /// 头像复用 core/components/user_circle_avatar.dart。 /// ============================================================ /// 排行榜单行(对标 rankItem.vue) class RankRow extends StatelessWidget { const RankRow({ super.key, required this.item, required this.category, required this.isSelf, }); final UserRank item; final int category; final bool isSelf; bool get _isTop => item.rank >= 1 && item.rank <= 3; String _formatPoints(double p) { if (p == p.roundToDouble()) return p.toInt().toString(); return p.toString(); } String get _avatar { final url = ImgUrl.build(item.userAvatar); return url.isEmpty ? ImgUrl.build('/StaticResource/2026-04-11/default_avatar.png') : url; } @override Widget build(BuildContext context) { final subInfo = (category == 1 || category == 2) ? item.userClassName : ''; return Container( height: 60.h, margin: EdgeInsets.only(bottom: _isTop ? 10.h : 0), padding: EdgeInsets.symmetric(horizontal: 10.w), decoration: _isTop ? BoxDecoration( gradient: LinearGradient(colors: _topColors(item.rank)), borderRadius: BorderRadius.circular(10.r), ) : null, child: Row( children: [ if (_isTop) CachedNetworkImage( imageUrl: _medalUrl(item.rank), width: 39.w, height: 60.h, fit: BoxFit.fill, errorWidget: (_, _, _) => const SizedBox(width: 39, height: 60), ) else SizedBox( width: 36.w, child: Center( child: Text( '${item.rank}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFFB4C2E0), ), ), ), ), SizedBox(width: 8.w), UserCircleAvatar(url: _avatar, radius: 22.w), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Flexible( child: Text( item.userName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFF1D2B47), ), ), ), if (isSelf) ...[ SizedBox(width: 8.w), Container( width: 16.w, height: 16.h, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFFB4C2E0), borderRadius: BorderRadius.circular(6.r), ), child: const Text( '我', style: TextStyle(fontSize: 10, color: Colors.white), ), ), ], ], ), if (subInfo.isNotEmpty) ...[ SizedBox(height: 4.h), Text( subInfo, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, color: Color(0xFF687FAD), ), ), ], ], ), ), Text( _formatPoints(item.points), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF3D7DF5), ), ), SizedBox(width: 20.w), ], ), ); } List _topColors(int rank) { switch (rank) { case 1: return [const Color(0xFFFCF4DE), Colors.white]; case 2: return [const Color(0xFFE8F0F7), Colors.white]; default: return [const Color(0xFFFAEAE6), Colors.white]; } } String _medalUrl(int rank) { final name = switch (rank) { 1 => 'ranking1Position1785292048368.png', 2 => 'ranking2Position1785292048369.png', _ => 'ranking3Position1785292048369.png', }; return ImgUrl.build('/StaticResource/2026-07-29/$name'); } } /// 积分规则弹窗(对标 EnergyRuleModal) class EnergyRuleModal extends StatelessWidget { const EnergyRuleModal({super.key, required this.onClose}); final VoidCallback onClose; static const List _rule1 = [ '1.每日登录+1分每日提交作业+3分', '2.连续打卡奖励连续打卡3天+5分;连续打卡7天+12分;连续打卡14天+25分;连续打卡30天+50分', '3.答题正确率奖励 正确率≥80%额外+2分每攻克一个知识点+5分', '4.每日上传学习图片 图片>1张+2分', '5.笔记被设为精选+10分(每日可叠加)', '6.每日点赞(非同一个人同一个笔记/攻克知识点)单条+0.5分(单日上限5分)每日评论(同上)单条+1分(单日上限5分)', ]; static const List _rule2 = [ '1.初始等级:0 级,0 积分', '2.0 升 1 级:累计积攒 100 积分', '3.1 升 2 级:再积攒 100 积分(累计 200)', '4.2 升 3 级:再积攒 100 积分(累计 300)', '5.3 升 4 级:再积攒 100 积分(累计 400)', '6.4 升 5 级:再积攒 100 积分(累计 500)', '7.5 升 6 级:再积攒 100 积分(累计 600)', '...', ]; @override Widget build(BuildContext context) { return Stack( children: [ Positioned.fill(child: ColoredBox(color: const Color(0x99000000))), Center( child: SizedBox( width: 301.5.w, height: 530.5.h, child: Stack( children: [ Positioned.fill( child: CachedNetworkImage( imageUrl: ImgUrl.build( '/StaticResource/2026-06-26/pointsRule_bg1782467735027.png', ), fit: BoxFit.fill, errorWidget: (_, _, _) => const ColoredBox(color: Colors.white), ), ), Positioned( left: 25.w, right: 25.w, top: 198.h, bottom: 35.h, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ RuleSection(title: '一、积分获取规则', items: _rule1), SizedBox(height: 10.h), RuleSection(title: '二、等级晋升规则', items: _rule2), ], ), ), ), ], ), ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: EdgeInsets.only(bottom: 90.h), child: GestureDetector( onTap: onClose, child: Container( width: 150.w, height: 49.h, alignment: Alignment.center, decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0xFF8AD1FD), Color(0xFF3C84FF), Color(0xFF3D7DF5), ], ), borderRadius: BorderRadius.circular(49.h), ), child: const Text( '我知道了', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ), ), ], ); } } class RuleSection extends StatelessWidget { const RuleSection({super.key, required this.title, required this.items}); final String title; final List items; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1D2B47), ), ), SizedBox(height: 8.h), for (final item in items) Padding( padding: EdgeInsets.only(bottom: 10.h), child: Text( item, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF1D2B47), height: 36 / 24, ), ), ), ], ); } } /// 白色向下小三角 class TriangleClipper extends CustomClipper { @override Path getClip(Size size) { return Path() ..moveTo(0, 0) ..lineTo(size.width, 0) ..lineTo(size.width / 2, size.height) ..close(); } @override bool shouldReclip(covariant CustomClipper oldClipper) => false; }