feat(reward): 新增客户端奖励中心
This commit is contained in:
1 parent
67670d46b1
commit
bea08110c9
68 files changed
+3987
-332
No files matched your search
@@ -0,0 +1,273 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/data/models/reward.dart';
|
||||
import '../../core/data/services/reward_controller.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_dimens.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'reward_visuals.dart';
|
||||
|
||||
const double _cardRadius = 14;
|
||||
const double _cardBorderWidth = 1;
|
||||
const double _titleFont = 20;
|
||||
const double _boostFont = 12;
|
||||
const double _subtitleFont = 13;
|
||||
const double _dayHeight = 80;
|
||||
const double _dayRadius = 9;
|
||||
const double _dayGap = 5;
|
||||
const double _dayRewardFont = 13;
|
||||
const double _dayLabelFont = 11;
|
||||
const double _dayCoinSize = 24;
|
||||
const double _buttonHeight = 48;
|
||||
const double _buttonFont = 16;
|
||||
const double _loadingSize = 20;
|
||||
const double _loadingStroke = 2;
|
||||
const Color _cardStart = Color(0xFF1A1A1C);
|
||||
const Color _cardEnd = Color(0xFF101010);
|
||||
const Color _cardBorder = Color(0xFF3A3A3C);
|
||||
const Color _daySurface = Color(0xFF202022);
|
||||
const Color _dayBorder = Color(0xFF363638);
|
||||
const Color _currentSurfaceStart = Color(0xFF3E0813);
|
||||
const Color _currentSurfaceEnd = Color(0xFF24070D);
|
||||
const Color _currentBorder = Color(0xFFFF4860);
|
||||
const Color _buttonEnd = Color(0xFFFF565B);
|
||||
|
||||
/// 奖励中心签到卡,展示七日规则与签到操作。
|
||||
class RewardCheckinCard extends StatelessWidget {
|
||||
const RewardCheckinCard({
|
||||
super.key,
|
||||
required this.l10n,
|
||||
required this.rewards,
|
||||
required this.checkin,
|
||||
required this.onCheckIn,
|
||||
required this.onBoost,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final RewardController rewards;
|
||||
final RewardCheckinState checkin;
|
||||
final Future<void> Function() onCheckIn;
|
||||
final VoidCallback onBoost;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
key: const Key('reward-checkin-card'),
|
||||
padding: const EdgeInsets.all(AppDimens.cardPadding),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(colors: [_cardStart, _cardEnd]),
|
||||
borderRadius: BorderRadius.circular(_cardRadius),
|
||||
border: Border.all(color: _cardBorder, width: _cardBorderWidth),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_header(),
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
Text(
|
||||
l10n.rewardCheckInSubtitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: _subtitleFont,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceLg),
|
||||
_days(),
|
||||
const SizedBox(height: AppDimens.spaceLg),
|
||||
_action(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.rewardCheckInTitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _titleFont,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: onBoost,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceSm),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l10n.rewardBoostCheckIn,
|
||||
style: const TextStyle(
|
||||
color: AppColors.brandRed,
|
||||
fontSize: _boostFont,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: AppColors.brandRed,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _days() {
|
||||
if (checkin.days.isEmpty) return const SizedBox.shrink();
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (var index = 0; index < checkin.days.length; index++) ...[
|
||||
Expanded(
|
||||
child: _RewardDay(l10n: l10n, day: checkin.days[index]),
|
||||
),
|
||||
if (index < checkin.days.length - 1) const SizedBox(width: _dayGap),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _action() {
|
||||
final disabled = checkin.checkedInToday || rewards.checkingIn;
|
||||
return Container(
|
||||
key: const Key('reward-checkin-action'),
|
||||
height: _buttonHeight,
|
||||
decoration: BoxDecoration(
|
||||
gradient: disabled
|
||||
? const LinearGradient(
|
||||
colors: [AppColors.contentChip, AppColors.contentChip],
|
||||
)
|
||||
: const LinearGradient(colors: [AppColors.brandRed, _buttonEnd]),
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
),
|
||||
child: Material(
|
||||
color: AppColors.transparent,
|
||||
child: InkWell(
|
||||
onTap: disabled ? null : () => unawaited(onCheckIn()),
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
child: Center(child: _actionContent()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionContent() {
|
||||
if (rewards.checkingIn) {
|
||||
return const SizedBox.square(
|
||||
dimension: _loadingSize,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: _loadingStroke,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
);
|
||||
}
|
||||
final label = checkin.checkedInToday
|
||||
? l10n.rewardCheckedIn
|
||||
: l10n.rewardCheckInActionWithCoins(_currentReward());
|
||||
return Row(
|
||||
key: const Key('reward-checkin-action-content'),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!checkin.checkedInToday) ...[
|
||||
const RewardAdIcon(),
|
||||
const SizedBox(width: AppDimens.spaceMd),
|
||||
],
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 220),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _buttonFont,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
int _currentReward() {
|
||||
for (final day in checkin.days) {
|
||||
if (day.current) return day.rewardCoin;
|
||||
}
|
||||
return checkin.days.isEmpty ? 0 : checkin.days.first.rewardCoin;
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardDay extends StatelessWidget {
|
||||
const _RewardDay({required this.l10n, required this.day});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final RewardCheckinDay day;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: _dayHeight,
|
||||
decoration: BoxDecoration(
|
||||
gradient: day.current
|
||||
? const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [_currentSurfaceStart, _currentSurfaceEnd],
|
||||
)
|
||||
: null,
|
||||
color: day.current ? null : _daySurface,
|
||||
borderRadius: BorderRadius.circular(_dayRadius),
|
||||
border: Border.all(color: day.current ? _currentBorder : _dayBorder),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'+${day.rewardCoin}',
|
||||
style: TextStyle(
|
||||
color: day.current
|
||||
? const Color(0xFFFFE18C)
|
||||
: AppColors.textPrimary,
|
||||
fontSize: _dayRewardFont,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
const RewardCoinIcon(size: _dayCoinSize),
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
day.current
|
||||
? l10n.rewardToday
|
||||
: l10n.rewardDayLabel(day.dayIndex),
|
||||
style: TextStyle(
|
||||
color: day.checked
|
||||
? AppColors.success
|
||||
: AppColors.textSecondary,
|
||||
fontSize: _dayLabelFont,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/data/models/reward.dart';
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../../core/data/services/reward_controller.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_dimens.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../core/widgets/app_glow_panel.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'reward_tasks_view.dart';
|
||||
|
||||
const double _activityHeight = 128;
|
||||
const double _headerHeight = 52;
|
||||
const double _headerIconSize = 16;
|
||||
const double _headerTouchTargetSize = 44;
|
||||
const double _headerTitleFont = 21;
|
||||
const double _tabHeight = 56;
|
||||
const double _tabFont = 18;
|
||||
const double _tabIndicatorWidth = 24;
|
||||
const double _tabIndicatorHeight = 3;
|
||||
const double _helpButtonSize = 24;
|
||||
const double _helpBorderWidth = 1;
|
||||
const double _helpFont = 14;
|
||||
const double _helpDialogMaxWidth = 320;
|
||||
const double _helpDialogRadius = 20;
|
||||
const double _helpDialogIconSize = 48;
|
||||
const double _helpDialogIconFont = 24;
|
||||
const double _helpDialogTitleFont = 18;
|
||||
const double _helpDialogBodyFont = 14;
|
||||
const double _helpDialogButtonHeight = 44;
|
||||
const int _loadDelayMilliseconds = 1;
|
||||
const String _loadKeySeparator = '|';
|
||||
const Color _helpBorder = Color(0xFFE9E9EA);
|
||||
const Color _tabInactive = Color(0xFF9B9B9F);
|
||||
|
||||
/// 奖励中心根页面,展示余额、签到、任务与活动。
|
||||
class RewardPage extends StatefulWidget {
|
||||
const RewardPage({
|
||||
super.key,
|
||||
required this.active,
|
||||
this.onBack,
|
||||
this.onHelp,
|
||||
this.onTaskAction,
|
||||
this.onOpenWallet,
|
||||
});
|
||||
|
||||
final bool active;
|
||||
final VoidCallback? onBack;
|
||||
final VoidCallback? onHelp;
|
||||
final ValueChanged<RewardTask>? onTaskAction;
|
||||
final VoidCallback? onOpenWallet;
|
||||
|
||||
@override
|
||||
State<RewardPage> createState() => _RewardPageState();
|
||||
}
|
||||
|
||||
class _RewardPageState extends State<RewardPage> {
|
||||
bool _showActivities = false;
|
||||
bool _loadScheduled = false;
|
||||
String? _autoLoadKey;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_scheduleLoad();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RewardPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.active && !oldWidget.active) _scheduleLoad(force: true);
|
||||
}
|
||||
|
||||
void _scheduleLoad({bool force = false}) {
|
||||
if (!widget.active || _loadScheduled) return;
|
||||
final rewards = context.read<RewardController>();
|
||||
final auth = context.read<AuthController>();
|
||||
final locale = Localizations.localeOf(context).languageCode;
|
||||
final loadKey = '$locale$_loadKeySeparator${auth.isSignedIn}';
|
||||
if (!force && _autoLoadKey == loadKey) return;
|
||||
_autoLoadKey = loadKey;
|
||||
_loadScheduled = true;
|
||||
Future<void>.delayed(
|
||||
const Duration(milliseconds: _loadDelayMilliseconds),
|
||||
() async {
|
||||
if (!mounted) return;
|
||||
_loadScheduled = false;
|
||||
if (!widget.active) return;
|
||||
await rewards.load(locale);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rewards = context.watch<RewardController>();
|
||||
context.watch<AuthController>();
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [AppColors.followingGlow, AppColors.ink, AppColors.ink],
|
||||
stops: [0, 0.3, 1],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_header(l10n),
|
||||
Expanded(child: _body(l10n, rewards)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header(AppLocalizations l10n) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: _headerHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(left: 4, child: _backButton()),
|
||||
Text(
|
||||
l10n.rewardCenterTitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _headerTitleFont,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Positioned(right: 8, child: _helpButton()),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _tab(
|
||||
key: const Key('reward-tab-tasks'),
|
||||
indicatorKey: const Key('reward-tab-indicator-tasks'),
|
||||
label: l10n.rewardTasksTab,
|
||||
selected: !_showActivities,
|
||||
showActivities: false,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _tab(
|
||||
key: const Key('reward-tab-activities'),
|
||||
indicatorKey: const Key('reward-tab-indicator-activities'),
|
||||
label: l10n.rewardActivitiesTab,
|
||||
selected: _showActivities,
|
||||
showActivities: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _backButton() {
|
||||
return IconButton(
|
||||
key: const Key('reward-back-button'),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: widget.onBack ?? () {},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: AppColors.textPrimary,
|
||||
size: _headerIconSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _helpButton() {
|
||||
return IconButton(
|
||||
key: const Key('reward-help-button'),
|
||||
tooltip: AppLocalizations.of(context)!.rewardHelpTitle,
|
||||
onPressed: widget.onHelp ?? _showHelp,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: _headerTouchTargetSize,
|
||||
height: _headerTouchTargetSize,
|
||||
),
|
||||
icon: Container(
|
||||
key: const Key('reward-help-icon'),
|
||||
width: _helpButtonSize,
|
||||
height: _helpButtonSize,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: _helpBorder, width: _helpBorderWidth),
|
||||
),
|
||||
child: const Text(
|
||||
'?',
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _helpFont,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tab({
|
||||
required Key key,
|
||||
required Key indicatorKey,
|
||||
required String label,
|
||||
required bool selected,
|
||||
required bool showActivities,
|
||||
}) {
|
||||
return InkWell(
|
||||
key: key,
|
||||
onTap: () => setState(() => _showActivities = showActivities),
|
||||
child: SizedBox(
|
||||
height: _tabHeight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: selected ? AppColors.textPrimary : _tabInactive,
|
||||
fontSize: _tabFont,
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceSm),
|
||||
AnimatedContainer(
|
||||
key: indicatorKey,
|
||||
duration: AppDimens.homePrimaryTabAnimationDuration,
|
||||
width: selected ? _tabIndicatorWidth : 0,
|
||||
height: _tabIndicatorHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.brandRed,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _body(AppLocalizations l10n, RewardController rewards) {
|
||||
if (rewards.loading && rewards.overview == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.brandRed),
|
||||
);
|
||||
}
|
||||
if (rewards.error != null && rewards.overview == null) {
|
||||
return _errorState(l10n, rewards);
|
||||
}
|
||||
final overview = rewards.overview;
|
||||
if (overview == null) return const SizedBox.shrink();
|
||||
return _showActivities
|
||||
? _activities(l10n, overview)
|
||||
: RewardTasksView(
|
||||
l10n: l10n,
|
||||
rewards: rewards,
|
||||
overview: overview,
|
||||
onRefresh: () =>
|
||||
rewards.load(Localizations.localeOf(context).languageCode),
|
||||
onCheckIn: () => _checkIn(l10n, rewards),
|
||||
onTaskAction: (task) => _handleTask(l10n, rewards, task),
|
||||
onOpenWallet: widget.onOpenWallet,
|
||||
onBoostCheckIn: () => _showResult(l10n.rewardComingSoon),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _activities(AppLocalizations l10n, RewardOverview overview) {
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(AppDimens.pageHorizontal),
|
||||
children: overview.activities.isEmpty
|
||||
? [_empty(l10n.rewardEmptyActivities)]
|
||||
: [for (final activity in overview.activities) _activity(activity)],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _activity(RewardActivity activity) {
|
||||
return Container(
|
||||
height: _activityHeight,
|
||||
margin: const EdgeInsets.only(bottom: AppDimens.spaceLg),
|
||||
padding: const EdgeInsets.all(AppDimens.cardPadding),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.vipCardStart, AppColors.contentCard],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
|
||||
border: Border.all(color: AppColors.vipCardBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.pageTitle,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceSm),
|
||||
Text(
|
||||
activity.summary,
|
||||
style: const TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorState(AppLocalizations l10n, RewardController rewards) {
|
||||
return Center(
|
||||
child: FilledButton(
|
||||
onPressed: () =>
|
||||
rewards.load(Localizations.localeOf(context).languageCode),
|
||||
child: Text(l10n.rewardRetry),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _empty(String label) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceXl),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(color: AppColors.textTertiary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _checkIn(AppLocalizations l10n, RewardController rewards) async {
|
||||
if (!await ensureSignedIn(context) || !mounted) return;
|
||||
final result = await rewards.checkIn();
|
||||
if (!mounted) return;
|
||||
_showResult(
|
||||
result == null
|
||||
? _message(l10n, rewards.actionError)
|
||||
: l10n.rewardCheckInSuccess(result.rewardCoin),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleTask(
|
||||
AppLocalizations l10n,
|
||||
RewardController rewards,
|
||||
RewardTask task,
|
||||
) async {
|
||||
if (!task.claimable) {
|
||||
widget.onTaskAction?.call(task);
|
||||
return;
|
||||
}
|
||||
if (!await ensureSignedIn(context) || !mounted) return;
|
||||
final result = await rewards.claim(task.id);
|
||||
if (!mounted) return;
|
||||
_showResult(
|
||||
result == null
|
||||
? _message(l10n, rewards.actionError)
|
||||
: l10n.rewardClaimSuccess(result.rewardCoin),
|
||||
);
|
||||
}
|
||||
|
||||
String? _message(AppLocalizations l10n, String? message) {
|
||||
return switch (message) {
|
||||
'rewardActionFailed' => l10n.rewardActionFailed,
|
||||
_ => message,
|
||||
};
|
||||
}
|
||||
|
||||
void _showResult(String? message) {
|
||||
if (message == null || message.isEmpty) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
void _showHelp() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => RepaintBoundary(
|
||||
key: const Key('reward-help-dialog'),
|
||||
child: _RewardHelpDialog(
|
||||
title: l10n.rewardHelpTitle,
|
||||
body: l10n.rewardHelpBody,
|
||||
confirmLabel: MaterialLocalizations.of(dialogContext).okButtonLabel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardHelpDialog extends StatelessWidget {
|
||||
const _RewardHelpDialog({
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.confirmLabel,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String body;
|
||||
final String confirmLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: AppColors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceXl),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: _helpDialogMaxWidth),
|
||||
child: AppGlowPanel(
|
||||
borderRadius: BorderRadius.circular(_helpDialogRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.spaceXl,
|
||||
AppDimens.spaceXl,
|
||||
AppDimens.spaceXl,
|
||||
AppDimens.spaceLg,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const _HelpDialogIcon(),
|
||||
const SizedBox(height: AppDimens.spaceMd),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _helpDialogTitleFont,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceSm),
|
||||
Text(
|
||||
body,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: _helpDialogBodyFont,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXl),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: _helpDialogButtonHeight,
|
||||
child: FilledButton(
|
||||
key: const Key('reward-help-close'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.brandRed,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppDimens.buttonRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
confirmLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: AppDimens.body,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HelpDialogIcon extends StatelessWidget {
|
||||
const _HelpDialogIcon();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: _helpDialogIconSize,
|
||||
height: _helpDialogIconSize,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.filterChipSelected,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.brandRed, width: 1),
|
||||
),
|
||||
child: const Text(
|
||||
'?',
|
||||
style: TextStyle(
|
||||
color: AppColors.brandRed,
|
||||
fontSize: _helpDialogIconFont,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/data/models/reward.dart';
|
||||
import '../../core/data/services/reward_controller.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_dimens.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'reward_checkin_card.dart';
|
||||
import 'reward_visuals.dart';
|
||||
|
||||
const double _balanceLabelFont = 18;
|
||||
const double _balanceFont = 36;
|
||||
const double _sectionTitleFont = 20;
|
||||
const double _sectionSubtitleFont = 13;
|
||||
const double _taskRowMinHeight = 68;
|
||||
const double _taskTitleFont = 15;
|
||||
const double _taskSubtitleFont = 12;
|
||||
const double _taskRewardFont = 14;
|
||||
const double _taskButtonHeight = 36;
|
||||
const double _taskButtonMinWidth = 80;
|
||||
const double _taskButtonFont = 13;
|
||||
const double _taskCardRadius = 14;
|
||||
const double _taskCardBorderWidth = 1;
|
||||
const Color _taskCardStart = Color(0xFF1A1A1C);
|
||||
const Color _taskCardEnd = Color(0xFF101010);
|
||||
const Color _taskCardBorder = Color(0xFF29292B);
|
||||
const Color _mutedActionStart = Color(0xFF3C171C);
|
||||
const Color _divider = Color(0xFF333336);
|
||||
const String _checkInSectionCode = 'CHECK_IN';
|
||||
|
||||
/// 奖励任务内容,负责余额、签到卡与可配置任务列表。
|
||||
class RewardTasksView extends StatelessWidget {
|
||||
const RewardTasksView({
|
||||
super.key,
|
||||
required this.l10n,
|
||||
required this.rewards,
|
||||
required this.overview,
|
||||
required this.onRefresh,
|
||||
required this.onCheckIn,
|
||||
required this.onTaskAction,
|
||||
this.onOpenWallet,
|
||||
this.onBoostCheckIn,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final RewardController rewards;
|
||||
final RewardOverview overview;
|
||||
final Future<void> Function() onRefresh;
|
||||
final Future<void> Function() onCheckIn;
|
||||
final Future<void> Function(RewardTask task) onTaskAction;
|
||||
final VoidCallback? onOpenWallet;
|
||||
final VoidCallback? onBoostCheckIn;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
color: AppColors.brandRed,
|
||||
backgroundColor: AppColors.contentCard,
|
||||
onRefresh: onRefresh,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.spaceLg,
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.spaceXl,
|
||||
),
|
||||
children: [
|
||||
_Balance(
|
||||
key: const Key('reward-balance'),
|
||||
balance: overview.coinBalance,
|
||||
onOpenWallet: onOpenWallet,
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceLg),
|
||||
RewardCheckinCard(
|
||||
l10n: l10n,
|
||||
rewards: rewards,
|
||||
checkin: overview.checkin,
|
||||
onCheckIn: onCheckIn,
|
||||
onBoost: onBoostCheckIn ?? () {},
|
||||
),
|
||||
for (final section in overview.sections)
|
||||
if (_isVisibleSection(section)) ...[
|
||||
const SizedBox(height: AppDimens.spaceXl),
|
||||
_SectionHeader(
|
||||
key: Key('reward-section-${section.code}'),
|
||||
l10n: l10n,
|
||||
section: section,
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceMd),
|
||||
_TaskGroupCard(
|
||||
key: Key('reward-task-card-${section.code}'),
|
||||
l10n: l10n,
|
||||
rewards: rewards,
|
||||
section: section,
|
||||
onTaskAction: onTaskAction,
|
||||
),
|
||||
],
|
||||
if (!overview.sections.any(_isVisibleSection))
|
||||
_Empty(label: l10n.rewardEmptyTasks),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isVisibleSection(RewardSection section) {
|
||||
return section.code.toUpperCase() != _checkInSectionCode &&
|
||||
section.tasks.isNotEmpty;
|
||||
}
|
||||
|
||||
class _Balance extends StatelessWidget {
|
||||
const _Balance({super.key, required this.balance, this.onOpenWallet});
|
||||
|
||||
final int balance;
|
||||
final VoidCallback? onOpenWallet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.rewardMyCoins,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: _balanceLabelFont,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceMd),
|
||||
Text(
|
||||
'$balance',
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _balanceFont,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
const RewardCoinIcon(size: 32),
|
||||
],
|
||||
);
|
||||
if (onOpenWallet == null) return content;
|
||||
return InkWell(
|
||||
onTap: onOpenWallet,
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceXs),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({super.key, required this.l10n, required this.section});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final RewardSection section;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitle = _sectionSubtitle(l10n, section.code);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
section.title,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _sectionTitleFont,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
if (subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: _sectionSubtitleFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _sectionSubtitle(AppLocalizations l10n, String code) {
|
||||
return switch (code.toUpperCase()) {
|
||||
'DAILY' => l10n.rewardDailySectionSubtitle,
|
||||
'NEWBIE' => l10n.rewardNewbieSectionSubtitle,
|
||||
'INVITE' || 'INVITATION' => l10n.rewardInviteSectionSubtitle,
|
||||
_ => '',
|
||||
};
|
||||
}
|
||||
|
||||
class _TaskGroupCard extends StatelessWidget {
|
||||
const _TaskGroupCard({
|
||||
super.key,
|
||||
required this.l10n,
|
||||
required this.rewards,
|
||||
required this.section,
|
||||
required this.onTaskAction,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final RewardController rewards;
|
||||
final RewardSection section;
|
||||
final Future<void> Function(RewardTask task) onTaskAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
key: Key('reward-task-surface-${section.code}'),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(colors: [_taskCardStart, _taskCardEnd]),
|
||||
borderRadius: BorderRadius.circular(_taskCardRadius),
|
||||
border: Border.all(color: _taskCardBorder, width: _taskCardBorderWidth),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (var index = 0; index < section.tasks.length; index++) ...[
|
||||
_TaskRow(
|
||||
l10n: l10n,
|
||||
rewards: rewards,
|
||||
sectionCode: section.code,
|
||||
task: section.tasks[index],
|
||||
onTaskAction: onTaskAction,
|
||||
),
|
||||
if (index < section.tasks.length - 1)
|
||||
const Divider(
|
||||
color: _divider,
|
||||
height: 1,
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskRow extends StatelessWidget {
|
||||
const _TaskRow({
|
||||
required this.l10n,
|
||||
required this.rewards,
|
||||
required this.sectionCode,
|
||||
required this.task,
|
||||
required this.onTaskAction,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final RewardController rewards;
|
||||
final String sectionCode;
|
||||
final RewardTask task;
|
||||
final Future<void> Function(RewardTask task) onTaskAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: _taskRowMinHeight),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppDimens.cardPadding),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
RewardTaskLeading(task: task),
|
||||
const SizedBox(width: AppDimens.spaceMd),
|
||||
Expanded(child: _copy()),
|
||||
const SizedBox(width: AppDimens.spaceMd),
|
||||
_TaskButton(
|
||||
l10n: l10n,
|
||||
rewards: rewards,
|
||||
sectionCode: sectionCode,
|
||||
task: task,
|
||||
onTaskAction: onTaskAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _copy() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
task.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _taskTitleFont,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (task.subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
Text(
|
||||
task.subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: _taskSubtitleFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (task.rewardCoin > 0) ...[
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'+${task.rewardCoin}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.brandRed,
|
||||
fontSize: _taskRewardFont,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceXs),
|
||||
const RewardCoinIcon(size: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskButton extends StatelessWidget {
|
||||
const _TaskButton({
|
||||
required this.l10n,
|
||||
required this.rewards,
|
||||
required this.sectionCode,
|
||||
required this.task,
|
||||
required this.onTaskAction,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final RewardController rewards;
|
||||
final String sectionCode;
|
||||
final RewardTask task;
|
||||
final Future<void> Function(RewardTask task) onTaskAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loading = rewards.claimingTaskId == task.id;
|
||||
final dark = sectionCode.toUpperCase() == 'NEWBIE' && !task.claimable;
|
||||
final label = task.claimed
|
||||
? l10n.rewardClaimed
|
||||
: task.claimable
|
||||
? l10n.rewardClaimAction
|
||||
: (task.ctaText.isEmpty ? l10n.rewardGoAction : task.ctaText);
|
||||
return ConstrainedBox(
|
||||
key: Key('reward-task-action-${task.id}'),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: _taskButtonMinWidth,
|
||||
minHeight: _taskButtonHeight,
|
||||
),
|
||||
child: FilledButton(
|
||||
onPressed: task.claimed || loading
|
||||
? null
|
||||
: () => unawaited(onTaskAction(task)),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: dark ? _mutedActionStart : AppColors.brandRed,
|
||||
disabledBackgroundColor: AppColors.contentChip,
|
||||
minimumSize: const Size(_taskButtonMinWidth, _taskButtonHeight),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceSm),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
),
|
||||
),
|
||||
child: loading
|
||||
? const SizedBox.square(
|
||||
dimension: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(label, style: const TextStyle(fontSize: _taskButtonFont)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Empty extends StatelessWidget {
|
||||
const _Empty({required this.label});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppDimens.spaceXl),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(color: AppColors.textTertiary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/data/models/reward.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
|
||||
const double _coinInnerInsetRatio = 0.1;
|
||||
const double _coinLetterRatio = 0.46;
|
||||
const double _taskIconSize = 46;
|
||||
const double _taskIconRadius = 12;
|
||||
const double _taskGlyphSize = 24;
|
||||
const double _adIconWidth = 28;
|
||||
const double _adIconHeight = 22;
|
||||
const double _adIconRadius = 4;
|
||||
const double _adUnderlineWidth = 12;
|
||||
const double _adUnderlineHeight = 2;
|
||||
const double _adUnderlineBottom = -5;
|
||||
const Color _coinOuter = Color(0xFFF6B84E);
|
||||
const Color _coinInnerStart = Color(0xFFFFD98B);
|
||||
const Color _coinInnerEnd = Color(0xFFE39B31);
|
||||
const Color _coinBorder = Color(0xFFFFE0A1);
|
||||
const Color _taskIconStart = Color(0xFF4C171E);
|
||||
const Color _taskIconEnd = Color(0xFF281014);
|
||||
const Color _taskIconSurface = Color(0xFF202022);
|
||||
|
||||
/// 奖励中心统一金币图标。
|
||||
class RewardCoinIcon extends StatelessWidget {
|
||||
const RewardCoinIcon({super.key, required this.size});
|
||||
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final inset = size * _coinInnerInsetRatio;
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
padding: EdgeInsets.all(inset),
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _coinOuter,
|
||||
boxShadow: [BoxShadow(color: Color(0x4DF6B84E), blurRadius: 6)],
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [_coinInnerStart, _coinInnerEnd],
|
||||
),
|
||||
border: Border.all(color: _coinBorder),
|
||||
),
|
||||
child: Text(
|
||||
'N',
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: size * _coinLetterRatio,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 奖励中心签到按钮的广告标识。
|
||||
class RewardAdIcon extends StatelessWidget {
|
||||
const RewardAdIcon({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: _adIconWidth,
|
||||
height: _adIconHeight,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: _adIconWidth,
|
||||
height: _adIconHeight,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.textPrimary, width: 2),
|
||||
borderRadius: BorderRadius.circular(_adIconRadius),
|
||||
),
|
||||
child: const Text(
|
||||
'AD',
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: _adUnderlineBottom,
|
||||
child: Container(
|
||||
width: _adUnderlineWidth,
|
||||
height: _adUnderlineHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.textPrimary,
|
||||
borderRadius: BorderRadius.circular(_adUnderlineHeight),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据后台任务类型绘制任务图标,不绑定具体任务文案。
|
||||
class RewardTaskLeading extends StatelessWidget {
|
||||
const RewardTaskLeading({super.key, required this.task});
|
||||
|
||||
final RewardTask task;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final invite = _isInviteTask(task);
|
||||
return Container(
|
||||
width: _taskIconSize,
|
||||
height: _taskIconSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: invite ? BoxShape.circle : BoxShape.rectangle,
|
||||
borderRadius: invite ? null : BorderRadius.circular(_taskIconRadius),
|
||||
gradient: invite
|
||||
? const LinearGradient(colors: [_taskIconStart, _taskIconEnd])
|
||||
: null,
|
||||
color: invite ? null : _taskIconSurface,
|
||||
),
|
||||
child: Icon(
|
||||
_taskIcon(task),
|
||||
color: AppColors.textPrimary,
|
||||
size: _taskGlyphSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isInviteTask(RewardTask task) {
|
||||
final code = task.code.toUpperCase();
|
||||
return task.type == 'INVITE_FRIEND' || code.contains('INVITE');
|
||||
}
|
||||
|
||||
IconData _taskIcon(RewardTask task) {
|
||||
final code = task.code.toUpperCase();
|
||||
if (code.contains('FACEBOOK')) return Icons.facebook_rounded;
|
||||
if (code.contains('YOUTUBE')) return Icons.smart_display_rounded;
|
||||
return switch (task.type) {
|
||||
'WATCH_DURATION' => Icons.play_circle_outline_rounded,
|
||||
'WATCH_EPISODES' => Icons.video_collection_outlined,
|
||||
'COMPLETE_PROFILE' => Icons.person_outline_rounded,
|
||||
'FIRST_PURCHASE' => Icons.shopping_bag_outlined,
|
||||
'INVITE_FRIEND' => Icons.card_giftcard_rounded,
|
||||
_ => Icons.stars_rounded,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user