import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:provider/provider.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_theme_extension.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../data/models/account_models.dart'; import '../../../data/services/bonus_service.dart'; import '../../../providers/asset_provider.dart'; /// 賬單頁面 — 代幣盈虧賬單 + 新人福利賬單 + 推廣福利賬單 class BillsPage extends StatefulWidget { const BillsPage({super.key}); @override State createState() => _BillsPageState(); } class _BillsPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; List _holdings = []; List> _welfareRecords = []; bool _isLoading = true; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); } @override void dispose() { _tabController.dispose(); super.dispose(); } Future _loadData() async { setState(() => _isLoading = true); try { final provider = context.read(); final bonusService = context.read(); // 並行加載持倉和福利記錄 await provider.loadTradeAccount(force: true); final welfareResponse = await bonusService.getWelfareStatus(); if (mounted) { setState(() { _holdings = provider.holdings; if (welfareResponse.success && welfareResponse.data != null) { _welfareRecords = _parseWelfareRecords(welfareResponse.data!); } _isLoading = false; }); } } catch (_) { if (mounted) { setState(() { _holdings = context.read().holdings; _isLoading = false; }); } } } List> _parseWelfareRecords(Map data) { final records = >[]; // 新人福利 final newUser = data['newUserBonus'] as Map?; if (newUser != null) { final claimed = newUser['claimed'] as bool? ?? false; final eligible = newUser['eligible'] as bool? ?? false; // 狀態: 1=已領取, 0=可領取(待領取), 2=不可用(未解鎖) final int status; if (claimed) { status = 1; } else if (eligible) { status = 0; } else { status = 2; } records.add({ 'type': 'new_user', 'title': '新人福利', 'amount': newUser['amount']?.toString() ?? '100.00', 'status': status, 'time': newUser['claimTime'] ?? newUser['createTime'], }); } // 推廣福利列表 final referralRewards = data['referralRewards'] as List? ?? []; for (var r in referralRewards) { final map = r as Map; final username = map['username'] as String? ?? '用戶'; final milestones = map['milestones'] as List? ?? []; final claimableCount = map['claimableCount'] as int? ?? 0; // 每個 milestone 生成一條記錄 for (var m in milestones) { final ms = m as Map; final earned = ms['earned'] as bool? ?? false; final claimable = ms['claimable'] as bool? ?? false; final milestoneVal = ms['milestone'] as int? ?? 1; final int status; if (earned) { status = 1; // 已領取 } else if (claimable) { status = 0; // 可領取 } else { status = 2; // 未達標 } records.add({ 'type': 'referral', 'title': '推廣福利 - $username (${milestoneVal}000)', 'amount': '100.00', 'status': status, 'time': ms['claimTime'] ?? ms['createTime'], }); } // 如果沒有 milestone 但有 claimableCount,也生成記錄 if (milestones.isEmpty && claimableCount > 0) { records.add({ 'type': 'referral', 'title': '推廣福利 - $username', 'amount': '${claimableCount * 100}', 'status': 0, 'time': null, }); } } return records; } bool get _isDark => Theme.of(context).brightness == Brightness.dark; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: _isDark ? AppColorScheme.darkBackground : AppColorScheme.lightBackground, appBar: AppBar( leading: IconButton( icon: const Icon(LucideIcons.arrowLeft, size: 20), onPressed: () => Navigator.of(context).pop(), ), title: Text('我的賬單', style: AppTextStyles.headlineLarge(context)), backgroundColor: _isDark ? AppColorScheme.darkBackground : AppColorScheme.lightBackground, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, bottom: TabBar( controller: _tabController, labelColor: colorScheme.primary, unselectedLabelColor: colorScheme.onSurfaceVariant, indicatorColor: colorScheme.primary, labelStyle: AppTextStyles.headlineMedium(context).copyWith(fontWeight: FontWeight.w600), unselectedLabelStyle: AppTextStyles.headlineMedium(context), tabs: const [ Tab(text: '代幣盈虧'), Tab(text: '新人福利'), Tab(text: '推廣福利'), ], ), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : TabBarView( controller: _tabController, children: [ _buildCoinProfitTab(), _buildWelfareTab('new_user'), _buildWelfareTab('referral'), ], ), ); } // ============================================ // 代幣盈虧賬單 // ============================================ Widget _buildCoinProfitTab() { final colorScheme = Theme.of(context).colorScheme; if (_holdings.isEmpty) { return _buildEmptyState(LucideIcons.wallet, '暫無持倉記錄'); } // 彙總統計 double totalCost = 0; double totalValue = 0; double totalProfit = 0; for (var h in _holdings) { totalCost += double.tryParse(h.totalCost) ?? 0; totalValue += double.tryParse(h.currentValue) ?? 0; totalProfit += double.tryParse(h.profit) ?? 0; } final profitRate = totalCost > 0 ? (totalProfit / totalCost * 100) : 0.0; final isProfit = totalProfit >= 0; final profitColor = isProfit ? context.appColors.up : context.appColors.down; return RefreshIndicator( onRefresh: _loadData, child: ListView( padding: const EdgeInsets.all(AppSpacing.md), children: [ // 彙總卡片 Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: _isDark ? AppColorScheme.darkSurfaceContainer : AppColorScheme.lightSurfaceLowest, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all( color: _isDark ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), ), ), child: Column( children: [ Text('總盈虧 (USDT)', style: AppTextStyles.bodyMedium(context).copyWith( color: colorScheme.onSurfaceVariant, )), const SizedBox(height: AppSpacing.xs), Text( '${isProfit ? '+' : ''}${totalProfit.toStringAsFixed(2)}', style: AppTextStyles.displaySmall(context).copyWith( fontWeight: FontWeight.bold, color: profitColor, ), ), const SizedBox(height: AppSpacing.sm), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildSummaryItem('總成本', totalCost.toStringAsFixed(2)), Container(width: 1, height: 16, color: colorScheme.outlineVariant.withValues(alpha: 0.3)), _buildSummaryItem('總市值', totalValue.toStringAsFixed(2)), Container(width: 1, height: 16, color: colorScheme.outlineVariant.withValues(alpha: 0.3)), _buildSummaryItem('收益率', '${profitRate >= 0 ? '+' : ''}${profitRate.toStringAsFixed(2)}%'), ], ), ], ), ), const SizedBox(height: AppSpacing.md), // 各幣種盈虧明細 ..._holdings.map((h) => _buildCoinProfitCard(h)), ], ), ); } Widget _buildSummaryItem(String label, String value) { final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), child: Column( children: [ Text(label, style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant, )), const SizedBox(height: 2), Text(value, style: AppTextStyles.labelMedium(context).copyWith( fontWeight: FontWeight.w600, )), ], ), ); } Widget _buildCoinProfitCard(AccountTrade h) { final colorScheme = Theme.of(context).colorScheme; final profit = double.tryParse(h.profit) ?? 0; final isProfit = profit >= 0; final profitColor = isProfit ? context.appColors.up : context.appColors.down; return Container( margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: _isDark ? AppColorScheme.darkSurfaceContainer : AppColorScheme.lightSurfaceLowest, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: _isDark ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), ), ), child: Column( children: [ // 幣名 + 盈虧金額 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ CircleAvatar( radius: 16, backgroundColor: colorScheme.primary.withValues(alpha: 0.1), child: Text( h.coinCode.substring(0, 1), style: AppTextStyles.labelLarge(context).copyWith( color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: AppSpacing.sm), Text(h.coinCode, style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, )), const SizedBox(width: AppSpacing.xs), Text('x ${double.tryParse(h.quantity)?.toStringAsFixed(4) ?? h.quantity}', style: AppTextStyles.bodySmall(context).copyWith(color: colorScheme.onSurfaceVariant), ), ], ), Text( '${isProfit ? '+' : ''}${profit.toStringAsFixed(2)} USDT', style: AppTextStyles.headlineMedium(context).copyWith( color: profitColor, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: AppSpacing.sm), // 明細行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('均價: ${h.avgPrice}', style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant, )), Text('市值: ${h.currentValue} USDT', style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant, )), Text(h.formattedProfitRate, style: AppTextStyles.bodySmall(context).copyWith( color: profitColor, fontWeight: FontWeight.w600, )), ], ), ], ), ); } // ============================================ // 福利賬單 // ============================================ Widget _buildWelfareTab(String type) { final records = _welfareRecords .where((r) => r['type'] == type && r['status'] == 1) .toList(); if (records.isEmpty) { return _buildEmptyState( LucideIcons.gift, type == 'new_user' ? '暫無新人福利記錄' : '暫無推廣福利記錄', ); } return RefreshIndicator( onRefresh: _loadData, child: ListView.builder( padding: const EdgeInsets.all(AppSpacing.md), itemCount: records.length, itemBuilder: (context, index) => _buildWelfareCard(records[index]), ), ); } Widget _buildWelfareCard(Map record) { final colorScheme = Theme.of(context).colorScheme; final amount = double.tryParse(record['amount']?.toString() ?? '0') ?? 0; final status = record['status'] as int? ?? 0; // status: 0=待領取, 1=已領取, 2=未達標 String statusText; Color statusColor; switch (status) { case 1: statusText = '已領取'; statusColor = context.appColors.up; break; case 2: statusText = '未達標'; statusColor = colorScheme.onSurfaceVariant; break; default: statusText = '待領取'; statusColor = AppColorScheme.warning; } return Container( margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: _isDark ? AppColorScheme.darkSurfaceContainer : AppColorScheme.lightSurfaceLowest, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: _isDark ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(record['title'] ?? '', style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, )), const SizedBox(height: AppSpacing.xs), if (record['time'] != null) Text( _formatTime(record['time']), style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '+${amount.toStringAsFixed(2)} USDT', style: AppTextStyles.headlineMedium(context).copyWith( color: context.appColors.up, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: 2), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text(statusText, style: AppTextStyles.bodySmall(context).copyWith( color: statusColor, fontWeight: FontWeight.w600, fontSize: 11, )), ), ], ), ], ), ); } // ============================================ // 通用組件 // ============================================ Widget _buildEmptyState(IconData icon, String text) { final colorScheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 64, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4)), const SizedBox(height: AppSpacing.md), Text(text, style: AppTextStyles.headlineMedium(context).copyWith( color: colorScheme.onSurfaceVariant, )), ], ), ); } String _formatTime(dynamic time) { if (time == null) return '-'; if (time is DateTime) { return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} ' '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } return time.toString(); } }