flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user