// 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? onTaskAction; final VoidCallback? onOpenWallet; @override State createState() => _RewardPageState(); } class _RewardPageState extends State { 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(); final auth = context.read(); final locale = Localizations.localeOf(context).languageCode; final loadKey = '$locale$_loadKeySeparator${auth.isSignedIn}'; if (!force && _autoLoadKey == loadKey) return; _autoLoadKey = loadKey; _loadScheduled = true; Future.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(); context.watch(); 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 _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 _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( 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, ), ), ); } }