Files
App.K12.Student.Miniapp_Flu…/lib/features/class/widgets/point_rank_widgets.dart
T
2026-09-04 15:22:20 +08:00

327 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Color> _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<String> _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<String> _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<String> 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<Path> {
@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<Path> oldClipper) => false;
}