feat(reward): 新增客户端奖励中心

This commit is contained in:
Zhou Weidong committed 2026-11-13 13:43:38 +08:00
1 parent 67670d46b1
commit bea08110c9
68 files changed
+3987 -332

No files matched your search

+40 -28
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
import 'app_press_scale.dart';
const double _maxNavigationTextScale = 1.3;
@@ -32,7 +33,7 @@ class AppBottomNavItem {
final bool selected;
}
/// 四槽底部导航:纯黑背景,白色选中态,灰色未选中态。
/// 均分底部导航:纯黑背景,品牌红选中态,白色未选中态。
class AppBottomNav extends StatelessWidget {
const AppBottomNav({super.key, required this.items, this.barKey});
@@ -91,35 +92,46 @@ class _NavDestination extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedScale(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
scale: item.selected ? 1 : 0.96,
child:
item.customIcon ??
(assetPath == null
? Icon(
fallbackIcon,
size: AppDimens.navIcon,
color: color,
)
: ColorFiltered(
colorFilter: ColorFilter.mode(
color,
BlendMode.srcIn,
AppPressScale(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppDimens.navIcon),
boxShadow: item.selected
? const [
BoxShadow(
color: AppColors.navSelectedGlow,
blurRadius: 12,
spreadRadius: 1,
),
child: Image.asset(
assetPath,
width: AppDimens.navIcon,
height: AppDimens.navIcon,
fit: BoxFit.contain,
errorBuilder: (_, _, _) => Icon(
fallbackIcon,
size: AppDimens.navIcon,
color: color,
]
: const [],
),
child:
item.customIcon ??
(assetPath == null
? Icon(
fallbackIcon,
size: AppDimens.navIcon,
color: color,
)
: ColorFiltered(
colorFilter: ColorFilter.mode(
color,
BlendMode.srcIn,
),
),
)),
child: Image.asset(
assetPath,
width: AppDimens.navIcon,
height: AppDimens.navIcon,
fit: BoxFit.contain,
errorBuilder: (_, _, _) => Icon(
fallbackIcon,
size: AppDimens.navIcon,
color: color,
),
),
)),
),
),
const SizedBox(height: AppDimens.navLabelGap),
Text(
+53
View File
@@ -0,0 +1,53 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
const double _pressedScale = 0.97;
/// 为可点击内容提供轻量、短促的按压缩放反馈。
class AppPressScale extends StatefulWidget {
const AppPressScale({
super.key,
required this.child,
this.onTap,
this.scale = _pressedScale,
});
final Widget child;
final VoidCallback? onTap;
final double scale;
@override
State<AppPressScale> createState() => _AppPressScaleState();
}
class _AppPressScaleState extends State<AppPressScale> {
bool _pressed = false;
void _setPressed(bool pressed) {
if (_pressed == pressed) {
return;
}
setState(() => _pressed = pressed);
}
@override
Widget build(BuildContext context) {
final scale = _pressed ? widget.scale : 1.0;
final feedback = Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _setPressed(true),
onPointerUp: (_) => _setPressed(false),
onPointerCancel: (_) => _setPressed(false),
child: Transform.scale(scale: scale, child: widget.child),
);
if (widget.onTap == null) {
return feedback;
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.onTap,
child: feedback,
);
}
}
+6 -1
View File
@@ -11,6 +11,7 @@ const double _maxPullDistance = 96;
const double _refreshThreshold = 56;
const double _indicatorHeight = 56;
const double _indicatorDisplacement = 40;
const double _indicatorTopPadding = 8;
const Duration _settleDuration = Duration(milliseconds: 200);
enum _PullState { idle, pull, armed, refreshing }
@@ -27,6 +28,10 @@ class AppPullToRefresh extends StatefulWidget {
static const AlwaysScrollableScrollPhysics physics =
AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics());
/// 达到刷新阈值时,指示器顶部相对容器顶部的下移距离。
static const double indicatorRevealOffset =
_indicatorDisplacement + _indicatorTopPadding;
final Widget child;
final Future<void> Function() onRefresh;
final double edgeOffset;
@@ -191,7 +196,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.only(top: _indicatorTopPadding),
child: _indicator(),
),
),
+7 -1
View File
@@ -11,6 +11,7 @@ import 'root_nav_glyph.dart';
enum AppRootTab {
home(),
recommend(),
reward(),
following(),
mine();
@@ -21,6 +22,7 @@ extension AppRootTabUi on AppRootTab {
RootNavGlyphKind get glyphKind => switch (this) {
AppRootTab.home => RootNavGlyphKind.home,
AppRootTab.recommend => RootNavGlyphKind.recommend,
AppRootTab.reward => RootNavGlyphKind.reward,
AppRootTab.following => RootNavGlyphKind.following,
AppRootTab.mine => RootNavGlyphKind.mine,
};
@@ -28,6 +30,7 @@ extension AppRootTabUi on AppRootTab {
Key get tabKey => switch (this) {
AppRootTab.home => const Key('tab-home'),
AppRootTab.recommend => const Key('tab-recommend'),
AppRootTab.reward => const Key('tab-reward'),
AppRootTab.following => const Key('tab-follow'),
AppRootTab.mine => const Key('tab-mine'),
};
@@ -35,6 +38,7 @@ extension AppRootTabUi on AppRootTab {
IconData get icon => switch (this) {
AppRootTab.home => Icons.home_outlined,
AppRootTab.recommend => Icons.smart_display_outlined,
AppRootTab.reward => Icons.card_giftcard_outlined,
AppRootTab.following => Icons.bookmark_border_rounded,
AppRootTab.mine => Icons.person_outline_rounded,
};
@@ -42,6 +46,7 @@ extension AppRootTabUi on AppRootTab {
IconData get selectedIcon => switch (this) {
AppRootTab.home => Icons.home_rounded,
AppRootTab.recommend => Icons.smart_display_rounded,
AppRootTab.reward => Icons.card_giftcard_rounded,
AppRootTab.following => Icons.bookmark_rounded,
AppRootTab.mine => Icons.person_rounded,
};
@@ -49,12 +54,13 @@ extension AppRootTabUi on AppRootTab {
String label(AppLocalizations l10n) => switch (this) {
AppRootTab.home => l10n.homeTab,
AppRootTab.recommend => l10n.recommendedTab,
AppRootTab.reward => l10n.rewardTab,
AppRootTab.following => l10n.followingTab,
AppRootTab.mine => l10n.myTab,
};
}
/// 根导航唯一入口,确保四个主页面使用完全一致的底栏配置。
/// 根导航唯一入口,确保五个主页面使用完全一致的底栏配置。
class AppRootBottomNav extends StatelessWidget {
const AppRootBottomNav({
super.key,
+1 -1
View File
@@ -168,7 +168,7 @@ class _SkeletonBlock extends StatelessWidget {
return DecoratedBox(
decoration: BoxDecoration(
color: AppColors.skeletonSurface,
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
borderRadius: BorderRadius.circular(AppDimens.coverRadius),
),
);
}
+30 -2
View File
@@ -7,8 +7,8 @@ import '../theme/app_colors.dart';
const double _designSize = 30;
/// 根导航四个入口对应的图标种类。
enum RootNavGlyphKind { home, recommend, following, mine }
/// 根导航五个入口对应的图标种类。
enum RootNavGlyphKind { home, recommend, reward, following, mine }
/// 根导航插画图标,根据选中态使用品牌红和中性灰白配色。
class RootNavGlyph extends StatelessWidget {
@@ -52,6 +52,8 @@ class _RootNavGlyphPainter extends CustomPainter {
_paintHome(canvas);
case RootNavGlyphKind.recommend:
_paintRecommend(canvas);
case RootNavGlyphKind.reward:
_paintReward(canvas);
case RootNavGlyphKind.following:
_paintFollowing(canvas);
case RootNavGlyphKind.mine:
@@ -151,6 +153,32 @@ class _RootNavGlyphPainter extends CustomPainter {
canvas.drawPath(bookmark, Paint()..color = _light);
}
void _paintReward(Canvas canvas) {
final box = RRect.fromRectAndRadius(
const Rect.fromLTRB(4, 10, 26, 26),
const Radius.circular(4),
);
canvas.drawShadow(Path()..addRRect(box), AppColors.shadow, 2, false);
canvas.drawRRect(box, _gradient(const Rect.fromLTWH(4, 10, 22, 16)));
canvas.drawRect(const Rect.fromLTRB(3, 8, 27, 13), Paint()..color = _main);
canvas.drawRect(
const Rect.fromLTRB(13, 8, 17, 26),
Paint()..color = _light,
);
final left = Path()
..moveTo(15, 9)
..cubicTo(8, 9, 7, 3, 11, 3)
..cubicTo(14, 3, 15, 7, 15, 9)
..close();
final right = Path()
..moveTo(15, 9)
..cubicTo(22, 9, 23, 3, 19, 3)
..cubicTo(16, 3, 15, 7, 15, 9)
..close();
canvas.drawPath(left, Paint()..color = _light);
canvas.drawPath(right, Paint()..color = _light);
}
void _paintMine(Canvas canvas) {
final bounds = Rect.fromCircle(center: const Offset(15, 15), radius: 13);
canvas.drawShadow(Path()..addOval(bounds), AppColors.shadow, 2, false);