import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/staking_node_level.dart'; import '../../../data/models/finance/staking_team_overview.dart'; import '../../../providers/auth_provider.dart'; import '../../../providers/staking_team_provider.dart'; bool _isDark(BuildContext context) => Theme.of(context).brightness == Brightness.dark; Color _pageBg(BuildContext context) => _isDark(context) ? AppColors.darkBg : Theme.of(context).colorScheme.surface; Color _primaryText(BuildContext context) => _isDark(context) ? Colors.white : Theme.of(context).colorScheme.onSurface; Color _secondaryText(BuildContext context) => _primaryText(context).withAlpha(160); String _fmtNum(num value, {int maxFrac = 2}) { if (!value.isFinite) { return '0'; } return NumberFormat('#,##0.${'#' * maxFrac}', 'en_US').format(value); } /// 我的团队页(对齐 Web `StakingTeamView.vue`) class StakingTeamScreen extends ConsumerStatefulWidget { const StakingTeamScreen({super.key}); @override ConsumerState createState() => _StakingTeamScreenState(); } class _StakingTeamScreenState extends ConsumerState { @override void initState() { super.initState(); Future.microtask(() { if (!ref.read(isLoggedInProvider)) { return; } ref.read(stakingTeamProvider.notifier).init(); }); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final isLoggedIn = ref.watch(isLoggedInProvider); final state = ref.watch(stakingTeamProvider); if (!isLoggedIn) { return Scaffold( backgroundColor: _pageBg(context), appBar: _buildAppBar(context, l10n), body: Center( child: TextButton( onPressed: () => context.push( '/login?redirect=${Uri.encodeComponent('/finance/team')}', ), child: Text(l10n.login), ), ), ); } return Scaffold( backgroundColor: _pageBg(context), appBar: _buildAppBar(context, l10n), body: state.isLoading ? const Center(child: CircularProgressIndicator()) : state.errorMessage != null && state.overview == null ? _ErrorBody( message: state.errorMessage!, onRetry: () => ref.read(stakingTeamProvider.notifier).init(), ) : _TeamBody(state: state), ); } PreferredSizeWidget _buildAppBar( BuildContext context, AppLocalizations l10n, ) { return AppBar( backgroundColor: Colors.transparent, foregroundColor: _primaryText(context), elevation: 0, centerTitle: true, leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new, size: 20), onPressed: () { if (context.canPop()) { context.pop(); } else { context.go('/finance/ido'); } }, ), title: Text( l10n.financeMyTeam, style: TextStyle( color: _primaryText(context), fontSize: 18, fontWeight: FontWeight.w600, ), ), ); } } class _ErrorBody extends StatelessWidget { const _ErrorBody({required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( message, textAlign: TextAlign.center, style: TextStyle(color: _secondaryText(context)), ), const SizedBox(height: 16), OutlinedButton( onPressed: onRetry, style: OutlinedButton.styleFrom( foregroundColor: AppColors.brand, side: const BorderSide(color: AppColors.brand), ), child: Text(l10n.retry), ), ], ), ), ); } } class _TeamBody extends ConsumerWidget { const _TeamBody({required this.state}); final StakingTeamState state; @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context)!; final overview = state.overview ?? const StakingTeamOverview(); final statCards = [ (l10n.financeDirectNumLabel, _fmtNum(overview.directNum, maxFrac: 0)), (l10n.financeTeamNumLabel, _fmtNum(overview.teamNum, maxFrac: 0)), (l10n.financeTeamPerformanceLabel, _fmtNum(overview.teamPerformance)), (l10n.financeTotalRevenueLabel, _fmtNum(overview.totalRevenue)), ]; return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), children: [ _NodeBanner( label: l10n.financeCurrentNodeLevel, value: stakingNodeLevelLabel(l10n, overview.nodeLevel), ), const SizedBox(height: 12), GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.35, children: [ for (final card in statCards) _StatCard(label: card.$1, value: card.$2), ], ), const SizedBox(height: 24), Text( l10n.financeDirectListTitle, style: TextStyle( color: _primaryText(context), fontSize: 18, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), if (state.directList.isEmpty && !state.isLoadingMore) Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: Text( l10n.noData, style: TextStyle( color: _secondaryText(context), fontSize: 14, ), ), ), ) else _DirectTable(members: state.directList), if (state.hasMore) ...[ const SizedBox(height: 16), Center( child: OutlinedButton( onPressed: state.isLoadingMore ? null : () => ref.read(stakingTeamProvider.notifier).loadMore(), style: OutlinedButton.styleFrom( foregroundColor: _secondaryText(context), side: BorderSide(color: _primaryText(context).withAlpha(40)), shape: const StadiumBorder(), ), child: state.isLoadingMore ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(l10n.financeTeamLoadMore), ), ), ], ], ); } } class _NodeBanner extends StatelessWidget { const _NodeBanner({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( colors: [Color(0xFFC9A227), Color(0xFFE8C547), Color(0xFFC9A227)], ), ), child: Row( children: [ Expanded( child: Text( label, style: const TextStyle( color: Color(0xFF1A1408), fontSize: 15, fontWeight: FontWeight.w500, ), ), ), Text( value, style: const TextStyle( color: Color(0xFF1A1408), fontSize: 16, fontWeight: FontWeight.w700, ), ), ], ), ); } } class _StatCard extends StatelessWidget { const _StatCard({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { final bg = _isDark(context) ? AppColors.darkBgSecondary : Theme.of(context).colorScheme.surfaceContainerHighest; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( value, style: const TextStyle( color: AppColors.brand, fontSize: 22, fontWeight: FontWeight.w700, fontFeatures: [FontFeature.tabularFigures()], ), ), const SizedBox(height: 8), Text( label, textAlign: TextAlign.center, style: TextStyle( color: _secondaryText(context), fontSize: 13, height: 1.35, ), ), ], ), ); } } class _DirectTable extends StatelessWidget { const _DirectTable({required this.members}); final List members; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( headingTextStyle: TextStyle( color: _secondaryText(context), fontSize: 12, fontWeight: FontWeight.w400, ), dataTextStyle: TextStyle( color: _primaryText(context), fontSize: 13, ), columns: [ DataColumn(label: Text(l10n.financeDirectAddress)), DataColumn(label: Text(l10n.financeLevelCol)), DataColumn(label: Text(l10n.financeBuyAmountCol)), DataColumn(label: Text(l10n.financeMyRewardCol)), ], rows: [ for (final row in members) DataRow( cells: [ DataCell( SizedBox( width: 120, child: Text( row.directAddress.isEmpty ? '—' : row.directAddress, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), DataCell(Text(stakingNodeLevelLabel(l10n, row.level))), DataCell(Text(_fmtNum(row.buyAmount))), DataCell(Text(_fmtNum(row.reward))), ], ), ], ), ); } }