feat(reward): 新增客户端奖励中心
This commit is contained in:
1 parent
67670d46b1
commit
bea08110c9
68 files changed
+3987
-332
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user