import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/top_toast.dart'; import '../../../data/models/finance/airdrop_eligibility.dart'; import '../../../data/models/finance/airdrop_record_item.dart'; import '../../../providers/auth_provider.dart'; import '../../../providers/staking_provider.dart'; bool _airdropIsDark(BuildContext context) => Theme.of(context).brightness == Brightness.dark; Color _airdropPageBg(BuildContext context) => _airdropIsDark(context) ? const Color(0xFF010A14) : Theme.of(context).colorScheme.surface; Color _airdropCardBg(BuildContext context) => _airdropIsDark(context) ? const Color(0xFF11181D) : Theme.of(context).colorScheme.surfaceContainerHighest; Color _airdropPrimaryText(BuildContext context) => _airdropIsDark(context) ? Colors.white : Theme.of(context).colorScheme.onSurface; Color _airdropHintText(BuildContext context) => _airdropPrimaryText(context).withAlpha(150); class AirdropScreen extends ConsumerStatefulWidget { const AirdropScreen({ super.key, this.showAppBar = true, }); final bool showAppBar; @override ConsumerState createState() => _AirdropScreenState(); } class _AirdropScreenState extends ConsumerState { void _log(String message) { if (kDebugMode) { debugPrint('[Airdrop][UI] $message'); } } @override void initState() { super.initState(); // 避免在 build 阶段触发 provider 更新,导致布局重入断言。 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { ref.read(airdropProvider.notifier).init(); } }); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final state = ref.watch(airdropProvider); final isLoggedIn = ref.watch(isLoggedInProvider); final eligibility = state.eligibility; final canClaim = isLoggedIn && eligibility.eligible && !state.isClaiming; final itemCount = _itemCount(state, isLoggedIn); _log( 'build isLoggedIn=$isLoggedIn eligLoading=${state.isLoadingEligibility} recordsLoading=${state.isLoadingRecords} records=${state.records.length} pageNo=${state.pageNo}/${state.totalPages} itemCount=$itemCount error=${state.errorMessage}', ); final body = _buildBody( l10n: l10n, state: state, isLoggedIn: isLoggedIn, eligibility: eligibility, canClaim: canClaim, ); if (!widget.showAppBar) { return ColoredBox( color: _airdropPageBg(context), child: body, ); } return Scaffold( backgroundColor: _airdropPageBg(context), appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: _airdropPrimaryText(context), elevation: 0, centerTitle: true, title: Text( l10n?.airdropTitle ?? '', style: TextStyle(color: _airdropPrimaryText(context)), ), ), body: body, ); } Future _onRefresh() async { await ref.read(airdropProvider.notifier).init(); } Widget _buildBody({ required AppLocalizations? l10n, required AirdropState state, required bool isLoggedIn, required AirdropEligibility eligibility, required bool canClaim, }) { if (l10n == null) { return const Center(child: CircularProgressIndicator()); } final itemCount = _itemCount(state, isLoggedIn); return RefreshIndicator( onRefresh: _onRefresh, color: AppColors.brand, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(15, 10, 15, 24), itemCount: itemCount, itemBuilder: (context, index) { return _itemBuilder( context: context, index: index, l10n: l10n, state: state, isLoggedIn: isLoggedIn, eligibility: eligibility, canClaim: canClaim, ); }, ), ); } // ── 计算列表项总数 ────────────────────────────────────────── int _itemCount(AirdropState state, bool isLoggedIn) { int n = 0; n++; // hero if (state.errorMessage != null && state.errorMessage!.isNotEmpty) { n++; // error } n += 2; // section spacer + section title n += 2; // invite spacer + invite task n += 2; // staking spacer + staking task n += 2; // claimable spacer + claimable card n += 2; // claim button spacer + claim button if (!isLoggedIn) { n++; // login hint } else { n += 2; // records spacer + records title if (state.isLoadingRecords && state.records.isEmpty) { n++; // loading } else if (state.records.isEmpty) { n++; // empty } else { n += state.records.length; // record items } if (state.hasMore) n++; // load more } return n; } // ── 按 index 构建组件 ────────────────────────────────────── Widget _itemBuilder({ required BuildContext context, required int index, required AppLocalizations l10n, required AirdropState state, required bool isLoggedIn, required AirdropEligibility eligibility, required bool canClaim, }) { int i = 0; final total = _itemCount(state, isLoggedIn); if (index < 0 || index >= total) { _log('itemBuilder out of range index=$index total=$total'); _log( 'itemBuilder fallback index=$index total=$total records=${state.records.length} hasMore=${state.hasMore}', ); return const SizedBox.shrink(); } try { // 0: hero if (index == i++) { return _HeroPanel( title: l10n.airdropTitle, subtitle: eligibility.message.isNotEmpty ? eligibility.message : l10n.airdropHasPendingReward, loading: state.isLoadingEligibility, ); } // 1: error (conditional) if (state.errorMessage != null && state.errorMessage!.isNotEmpty) { if (index == i++) { return _ErrorCard(message: state.errorMessage!); } } // section title if (index == i++) return const SizedBox(height: 20); if (index == i++) { return _SectionTitle(title: l10n.airdropTitle); } // invite task if (index == i++) return const SizedBox(height: 12); if (index == i++) { return _TaskCard( title: l10n.airdropInviteRequirement( '${eligibility.inviteCount}', '${eligibility.requiredInviteCount}', ), subtitle: eligibility.inviteTaskCompleted ? l10n.completed : '${eligibility.inviteCount}/${eligibility.requiredInviteCount}', done: eligibility.inviteTaskCompleted, btnText: l10n.inviteFriends, onTap: eligibility.inviteTaskCompleted ? null : () => context.push('/user/referral'), ); } // staking task if (index == i++) return const SizedBox(height: 10); if (index == i++) { return _TaskCard( title: l10n.airdropHasActiveStaking, subtitle: eligibility.hasActiveStaking ? l10n.completed : l10n.noData, done: eligibility.hasActiveStaking, btnText: l10n.stakingTitle, onTap: () => context.push('/finance/ido'), ); } // claimable card if (index == i++) return const SizedBox(height: 12); if (index == i++) { return _ClaimablePanel( label: l10n.airdropClaimable, amount: _fmtAmount(eligibility), coinUnit: eligibility.claimableCoinUnit ?? 'IBIT', ); } // claim button if (index == i++) return const SizedBox(height: 16); if (index == i++) { return SizedBox( height: 50, child: ElevatedButton( onPressed: canClaim ? () => _onClaim(l10n, eligibility) : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, foregroundColor: Colors.black, disabledBackgroundColor: _airdropCardBg(context), disabledForegroundColor: const Color(0xFF757575), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), child: state.isClaiming ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : Text(l10n.airdropClaimNow), ), ); } // not logged in hint if (!isLoggedIn) { if (index == i++) { return Padding( padding: const EdgeInsets.only(top: 12), child: Center( child: Text(l10n.airdropNotEligible, style: TextStyle( color: _airdropHintText(context), fontSize: 12)), ), ); } return const SizedBox.shrink(); } // records section if (index == i++) return const SizedBox(height: 20); if (index == i++) { return Text( l10n.airdropRecords, style: TextStyle( color: _airdropPrimaryText(context), fontSize: 16, fontWeight: FontWeight.w600), ); } if (state.isLoadingRecords && state.records.isEmpty) { if (index == i++) { return const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))), ); } } else if (state.records.isEmpty) { if (index == i++) { return Container( padding: const EdgeInsets.symmetric(vertical: 20), alignment: Alignment.center, decoration: BoxDecoration( color: _airdropCardBg(context), borderRadius: BorderRadius.circular(10)), child: Text(l10n.noData, style: TextStyle(color: _airdropHintText(context))), ); } } else { final ri = index - i; if (ri >= 0 && ri < state.records.length) { return _RecordCard(record: state.records[ri]); } i += state.records.length; if (state.hasMore) { if (index == i++) { return TextButton( onPressed: state.isLoadingMore ? null : () => ref.read(airdropProvider.notifier).loadMore(), child: Text(state.isLoadingMore ? l10n.loading : l10n.viewMore, style: const TextStyle(color: AppColors.brand)), ); } } } return const SizedBox.shrink(); } catch (e, st) { _log( 'itemBuilder exception index=$index total=$total records=${state.records.length} error=$e', ); _log('$st'); return Container( margin: const EdgeInsets.only(top: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0x33FF5252), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0x66FF5252)), ), child: Text( 'UI render error @index=$index: $e', style: const TextStyle(color: Color(0xFFFF8A80), fontSize: 12), ), ); } } // ── 格式化金额 ────────────────────────────────────────────── String _fmtAmount(AirdropEligibility e) { final d = double.tryParse(e.claimableAmount) ?? 0; if (d == 0) return '0'; final s = d.toStringAsFixed(8); // 去掉尾部多余的 0 final trimmed = s.replaceAll(RegExp(r'0+$'), ''); return trimmed.endsWith('.') ? '${trimmed}0' : trimmed; } // ── 领取逻辑 ──────────────────────────────────────────────── Future _onClaim( AppLocalizations l10n, AirdropEligibility eligibility) async { if (!eligibility.eligible) { showTopToast(context, message: l10n.airdropNotEligible); return; } final err = await ref.read(airdropProvider.notifier).claim(); if (!mounted) return; if (err == null) { await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(l10n.tips), content: Text(l10n.airdropClaimSuccess), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: Text(l10n.confirm)), ], ), ); } else { showTopToast(context, message: err); } } } // ═══════════════════════════════════════════════════════════════ // 子组件 // ═══════════════════════════════════════════════════════════════ // ── 英雄区(文字左 + 图片右)────────────────────────────────── class _HeroPanel extends StatelessWidget { const _HeroPanel( {required this.title, required this.subtitle, this.loading = false}); final String title; final String subtitle; final bool loading; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(title, style: TextStyle( color: _airdropPrimaryText(context), fontSize: 26, fontWeight: FontWeight.w700)), const SizedBox(height: 8), if (loading) const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) else Text(subtitle, maxLines: 5, overflow: TextOverflow.ellipsis, style: TextStyle( color: _airdropHintText(context), fontSize: 10, height: 1.45)), ], ), ), const SizedBox(width: 16), Image.asset( 'assets/images/finance/airdrop_hero.png', width: 126, height: 126, fit: BoxFit.contain, errorBuilder: (_, __, ___) => Container( width: 126, height: 126, alignment: Alignment.center, decoration: BoxDecoration( color: _airdropCardBg(context), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.card_giftcard_outlined, color: AppColors.brand, size: 48), ), ), ], ), ); } } // ── 错误卡片 ────────────────────────────────────────────────── class _ErrorCard extends StatelessWidget { const _ErrorCard({required this.message}); final String message; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: const Color(0x33FF5252), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0x66FF5252)), ), child: Text(message, style: const TextStyle(color: Color(0xFFFF8A80), fontSize: 12)), ); } } // ── 区块标题 ────────────────────────────────────────────────── class _SectionTitle extends StatelessWidget { const _SectionTitle({required this.title}); final String title; @override Widget build(BuildContext context) { return Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 14, height: 3, decoration: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(2))), const SizedBox(width: 10), Text(title, style: TextStyle( color: _airdropPrimaryText(context), fontSize: 14, fontWeight: FontWeight.w600)), const SizedBox(width: 10), Container( width: 14, height: 3, decoration: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(2))), ], ), ); } } // ── 任务卡片 ────────────────────────────────────────────────── class _TaskCard extends StatelessWidget { const _TaskCard({ required this.title, required this.subtitle, required this.done, required this.btnText, this.onTap, }); final String title; final String subtitle; final bool done; final String btnText; final VoidCallback? onTap; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14), decoration: BoxDecoration( color: _airdropCardBg(context), borderRadius: BorderRadius.circular(8)), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(title, style: TextStyle( color: _airdropPrimaryText(context), fontSize: 13)), const SizedBox(height: 6), Text( done ? '$subtitle ✅' : subtitle, style: TextStyle(color: _airdropHintText(context), fontSize: 11), ), ], ), ), const SizedBox(width: 12), SizedBox( height: 30, child: Material( color: onTap == null ? AppColors.brand.withAlpha(128) : AppColors.brand, borderRadius: BorderRadius.circular(6), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(6), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Center( child: Text( btnText, style: TextStyle( fontSize: 12, color: onTap == null ? Colors.black.withAlpha(128) : Colors.black, fontWeight: FontWeight.w500, ), ), ), ), ), ), ), ], ), ); } } // ── 可领取金额面板 ──────────────────────────────────────────── class _ClaimablePanel extends StatelessWidget { const _ClaimablePanel( {required this.label, required this.amount, required this.coinUnit}); final String label; final String amount; final String coinUnit; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14), decoration: BoxDecoration( color: _airdropCardBg(context), borderRadius: BorderRadius.circular(8)), child: Row( children: [ Expanded( child: Text(label, style: TextStyle(color: _airdropHintText(context), fontSize: 12)), ), Text('$amount $coinUnit', style: TextStyle( color: _airdropPrimaryText(context), fontSize: 18, fontWeight: FontWeight.w700)), ], ), ); } } // ── 领取记录条目 ────────────────────────────────────────────── class _RecordCard extends StatelessWidget { const _RecordCard({required this.record}); final AirdropRecordItem record; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final statusText = switch (record.status) { 0 => l10n?.airdropStatusPending ?? 'Pending', 1 => l10n?.airdropStatusGranted ?? 'Granted', 2 => l10n?.airdropStatusReviewing ?? 'Reviewing', 3 => l10n?.airdropStatusRejected ?? 'Rejected', _ => '${record.status}', }; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _airdropCardBg(context), borderRadius: BorderRadius.circular(8)), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '${record.amount} ${record.coinUnit ?? ''}', style: TextStyle( fontWeight: FontWeight.w600, color: _airdropPrimaryText(context)), ), const SizedBox(height: 4), Text( record.createTime ?? '--', style: TextStyle(color: _airdropHintText(context), fontSize: 12), ), ], ), ), Text(statusText, style: TextStyle(color: _airdropHintText(context))), ], ), ); } }