import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme_extension.dart'; import '../../../core/utils/toast_utils.dart'; import '../../../core/event/app_event_bus.dart'; import '../../../data/models/account_models.dart'; import '../../../data/services/bonus_service.dart'; import '../../../providers/asset_provider.dart'; import '../../components/glass_panel.dart'; import '../../components/neon_glow.dart'; import '../main/main_page.dart'; import '../mine/welfare_center_page.dart'; import 'header_bar.dart'; import 'quick_actions_row.dart'; import 'hot_coins_section.dart'; import 'profit_analysis_page.dart'; /// 首页 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State with AutomaticKeepAliveClientMixin { int _totalClaimable = 0; StreamSubscription? _eventSub; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _loadData(); _listenEvents(); }); } @override void dispose() { _eventSub?.cancel(); super.dispose(); } void _listenEvents() { final eventBus = context.read(); _eventSub = eventBus.on(AppEventType.assetChanged, (_) { if (mounted) { context.read().loadOverview(force: true); _checkBonusStatus(); } }); } void _loadData() { final provider = context.read(); provider.loadOverview(); provider.loadTradeAccount(); _checkBonusStatus(); } Future _checkBonusStatus() async { try { final bonusService = context.read(); final response = await bonusService.getWelfareStatus(); if (response.success && response.data != null) { setState(() { _totalClaimable = response.data!['totalClaimable'] as int? ?? 0; }); } } catch (_) {} } @override Widget build(BuildContext context) { super.build(context); return Scaffold( backgroundColor: context.colors.background, body: Consumer( builder: (context, provider, _) { return RefreshIndicator( onRefresh: () => provider.refreshAll(force: true), color: context.colors.primary, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only( left: AppSpacing.lg, right: AppSpacing.lg, top: AppSpacing.sm, bottom: 100, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header HeaderBar(), SizedBox(height: AppSpacing.md), // 资产卡片(含总盈利 + 可折叠盈亏日历) _AssetCard( overview: provider.overview, onDeposit: _showDeposit, ), SizedBox(height: AppSpacing.md), // 快捷操作栏 QuickActionsRow( onDeposit: _showDeposit, onWithdraw: () => _navigateToAssetPage(), onTransfer: () => _navigateToAssetPage(), onBills: () => _navigateToAssetPage(), ), SizedBox(height: AppSpacing.md), // 福利中心入口卡片 _WelfareCard( totalClaimable: _totalClaimable, onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const WelfareCenterPage()), ), ), SizedBox(height: AppSpacing.lg), // 热门币种 HotCoinsSection(), SizedBox(height: AppSpacing.lg), // 持仓 _HoldingsSection(holdings: provider.holdings), ], ), ), ); }, ), ); } void _showDeposit() { final amountController = TextEditingController(); final formKey = GlobalKey(); showShadDialog( context: context, builder: (ctx) => Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.xxl), padding: EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '充值', style: AppTextStyles.headlineLarge(context).copyWith( fontWeight: FontWeight.bold, ), ), SizedBox(height: AppSpacing.xs), Text( '资产: USDT', style: AppTextStyles.bodyMedium(context), ), ], ), Container( padding: EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: context.colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Icon(LucideIcons.wallet, color: context.colors.secondary), ), ], ), SizedBox(height: AppSpacing.lg), ShadForm( key: formKey, child: ShadInputFormField( id: 'amount', controller: amountController, label: const Text('充值金额'), placeholder: const Text('0.00'), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (v) { if (v == null || v.isEmpty) return '请输入金额'; final n = double.tryParse(v); if (n == null || n <= 0) return '请输入有效金额'; return null; }, ), ), SizedBox(height: AppSpacing.lg), Row( children: [ Expanded( child: NeonButton( text: '取消', type: NeonButtonType.outline, onPressed: () => Navigator.of(ctx).pop(), height: 48, showGlow: false, ), ), SizedBox(width: AppSpacing.sm), Expanded( child: NeonButton( text: '下一步', type: NeonButtonType.primary, onPressed: () async { if (formKey.currentState!.saveAndValidate()) { Navigator.of(ctx).pop(); final response = await context .read() .deposit(amount: amountController.text); if (mounted) { if (response.success && response.data != null) { _showDepositResultDialog(context, response.data!); } else { _showResultDialog( '申请失败', response.message ?? '请稍后重试', ); } } } }, height: 48, showGlow: true, ), ), ], ), ], ), ), ), ); } void _showDepositResultDialog(BuildContext context, Map data) { final orderNo = data['orderNo'] as String? ?? ''; final amount = data['amount']?.toString() ?? '0.00'; final walletAddress = data['walletAddress'] as String? ?? ''; final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20'; showShadDialog( context: context, builder: (ctx) => Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.xxl), padding: EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ NeonIcon( icon: Icons.check_circle, color: AppColorScheme.up, size: 24, ), SizedBox(width: AppSpacing.sm), Text( '充值申请成功', style: AppTextStyles.headlineLarge(context).copyWith( fontWeight: FontWeight.bold, ), ), ], ), SizedBox(height: AppSpacing.lg), _InfoRow(label: '订单号', value: orderNo), SizedBox(height: AppSpacing.sm), _InfoRow(label: '充值金额', value: '$amount USDT', isBold: true), SizedBox(height: AppSpacing.lg), Text( '请向以下地址转账:', style: AppTextStyles.bodyMedium(context), ), SizedBox(height: AppSpacing.sm), _WalletAddressCard(address: walletAddress, network: walletNetwork), SizedBox(height: AppSpacing.md), Container( padding: EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( color: AppColorScheme.warning.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: AppColorScheme.warning.withValues(alpha: 0.2)), ), child: Row( children: [ Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning), SizedBox(width: AppSpacing.sm), Expanded( child: Text( '转账完成后请点击"已打款"按钮确认', style: AppTextStyles.bodyMedium(context).copyWith( color: AppColorScheme.warning, ), ), ), ], ), ), SizedBox(height: AppSpacing.lg), Row( children: [ Expanded( child: NeonButton( text: '稍后确认', type: NeonButtonType.outline, onPressed: () { Navigator.of(ctx).pop(); _navigateToAssetPage(); }, height: 44, showGlow: false, ), ), SizedBox(width: AppSpacing.sm), Expanded( child: NeonButton( text: '已打款', type: NeonButtonType.primary, onPressed: () async { Navigator.of(ctx).pop(); final response = await context .read() .confirmPay(orderNo); if (context.mounted) { _showResultDialog( response.success ? '确认成功' : '确认失败', response.success ? '请等待管理员审核' : response.message, ); _navigateToAssetPage(); } }, height: 44, showGlow: true, ), ), ], ), ], ), ), ), ); } void _showResultDialog(String title, String? message) { showShadDialog( context: context, builder: (ctx) => Dialog( backgroundColor: Colors.transparent, child: GlassPanel( borderRadius: BorderRadius.circular(AppRadius.xxl), padding: EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(title, style: AppTextStyles.headlineLarge(context).copyWith( fontWeight: FontWeight.bold, )), if (message != null) ...[ SizedBox(height: AppSpacing.sm), Text(message, style: AppTextStyles.bodyMedium(context), textAlign: TextAlign.center), ], SizedBox(height: AppSpacing.lg), SizedBox( width: double.infinity, child: NeonButton( text: '确定', type: NeonButtonType.primary, onPressed: () => Navigator.of(ctx).pop(), height: 44, showGlow: false, ), ), ], ), ), ), ); } /// 跳转到资产页面 void _navigateToAssetPage() { final mainState = context.findAncestorStateOfType(); mainState?.switchToTab(3); } void _navigateToWelfareCenter() { Navigator.push( context, MaterialPageRoute(builder: (_) => const WelfareCenterPage()), ); } } /// Header 栏:品牌名 + 搜索/通知/头像 /// 资产卡片(含总盈利) class _AssetCard extends StatelessWidget { final AssetOverview? overview; final VoidCallback onDeposit; const _AssetCard({required this.overview, required this.onDeposit}); double get _totalProfit { final v = overview?.totalProfit; if (v == null) return 0; return double.tryParse(v) ?? 0; } @override Widget build(BuildContext context) { final upColor = context.appColors.up; final downColor = context.appColors.down; final isProfit = _totalProfit >= 0; // 总资产 final totalAsset = overview?.totalAsset ?? '0.00'; final displayAsset = _formatAsset(totalAsset); return GlassPanel( padding: EdgeInsets.all(20), borderRadius: BorderRadius.circular(AppRadius.lg), // 14px child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部行:总资产标签 + 充值按钮 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '预估总资产(USDT)', style: AppTextStyles.bodyLarge(context), // 13px ), GestureDetector( onTap: onDeposit, child: Container( padding: EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: context.colors.primary, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.add, size: 13, color: context.colors.onPrimary), SizedBox(width: 4), Text( '充值', style: AppTextStyles.labelLarge(context).copyWith( color: context.colors.onPrimary, fontSize: 12, ), ), ], ), ), ), ], ), SizedBox(height: AppSpacing.sm), // 总资产金额 Text( displayAsset, style: AppTextStyles.displayLarge(context).copyWith( fontWeight: FontWeight.bold, ), ), SizedBox(height: AppSpacing.md), // 盈亏统计区:今日盈亏 | 总盈亏 Row( children: [ // 今日盈亏卡片 Expanded( child: _ProfitStatCard( label: '今日盈亏', value: null, // 移除日历后不显示今日盈亏 upColor: upColor, downColor: downColor, onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const ProfitAnalysisPage()), ), ), ), SizedBox(width: AppSpacing.sm), // 总盈亏卡片 Expanded( child: _ProfitStatCard( label: '总盈亏', value: _totalProfit, upColor: upColor, downColor: downColor, onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const ProfitAnalysisPage()), ), ), ), ], ), ], ), ); } String _formatAsset(String value) { final d = double.tryParse(value) ?? 0.0; return d.toStringAsFixed(2); } } /// 福利中心入口卡片 class _WelfareCard extends StatelessWidget { final int totalClaimable; final VoidCallback onTap; const _WelfareCard({required this.totalClaimable, required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: double.infinity, padding: EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: context.colors.surface, // 白色背景(跟随主题) borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: context.colors.outlineVariant.withValues(alpha: 0.2)), ), child: Row( children: [ // 左侧图标 Container( width: 48, height: 48, decoration: BoxDecoration( color: context.colors.primary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Icon( LucideIcons.gift, color: context.colors.primary, size: 24, ), ), SizedBox(width: AppSpacing.md), // 中间文字 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '福利中心', style: AppTextStyles.headlineLarge(context).copyWith( fontWeight: FontWeight.bold, ), ), SizedBox(height: AppSpacing.xs), Text( totalClaimable > 0 ? '您有 $totalClaimable 个奖励待领取' : '首充奖励 + 推广奖励', style: AppTextStyles.bodyMedium(context), ), ], ), ), // 右侧按钮 - 黄色背景 Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ), decoration: BoxDecoration( color: const Color(0xFFF59E0B), // 黄色背景 borderRadius: BorderRadius.circular(AppRadius.full), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (totalClaimable > 0) Container( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: context.appColors.down, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Text( '$totalClaimable', style: AppTextStyles.bodySmall(context).copyWith( fontSize: 10, fontWeight: FontWeight.bold, color: context.colors.onPrimary, ), ), ), SizedBox(width: totalClaimable > 0 ? 6 : 0), Text( '查看', style: AppTextStyles.headlineSmall(context).copyWith( fontWeight: FontWeight.w700, color: Colors.white, // 白色文字 ), ), ], ), ), ], ), ), ); } } /// 持仓部分 class _HoldingsSection extends StatelessWidget { final List holdings; const _HoldingsSection({required this.holdings}); @override Widget build(BuildContext context) { return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '我的持仓', style: AppTextStyles.headlineLarge(context).copyWith( fontWeight: FontWeight.bold, ), ), TextButton( onPressed: () {}, style: TextButton.styleFrom( foregroundColor: context.appColors.onSurfaceMuted, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), ), child: Row( children: [ Text('资产详情', style: AppTextStyles.headlineSmall(context).copyWith( fontWeight: FontWeight.bold, )), const SizedBox(width: AppSpacing.xs), Icon(LucideIcons.chevronRight, size: 16, color: context.appColors.onSurfaceMuted), ], ), ), ], ), SizedBox(height: AppSpacing.md), holdings.isEmpty ? const _EmptyHoldings() : _HoldingsList(holdings: holdings), ], ); } } /// 空持仓 class _EmptyHoldings extends StatelessWidget { const _EmptyHoldings(); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: EdgeInsets.symmetric(vertical: AppSpacing.xxl, horizontal: AppSpacing.lg), decoration: BoxDecoration( color: context.colors.surfaceContainerLow.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(AppRadius.xxl), border: Border.all(color: context.colors.outlineVariant.withValues(alpha: 0.1)), ), child: Column( children: [ Icon(LucideIcons.wallet, size: 48, color: context.colors.onSurfaceVariant), SizedBox(height: AppSpacing.md), Text( '暂无持仓', style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.w600, ), ), SizedBox(height: AppSpacing.sm), Text( '快去交易吧~', style: AppTextStyles.bodyLarge(context), ), ], ), ); } } /// 持仓列表 class _HoldingsList extends StatelessWidget { final List holdings; const _HoldingsList({required this.holdings}); @override Widget build(BuildContext context) { final displayHoldings = holdings.length > 5 ? holdings.sublist(0, 5) : holdings; return Container( decoration: BoxDecoration( color: context.colors.surface.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(AppRadius.xxl), border: Border.all(color: context.colors.outlineVariant.withValues(alpha: 0.1)), ), child: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.all(AppSpacing.md), itemCount: displayHoldings.length, separatorBuilder: (_, __) => Divider( color: context.appColors.ghostBorder, height: 1, ), itemBuilder: (context, index) => _HoldingItem(holding: displayHoldings[index]), ), ); } } /// 持仓项 class _HoldingItem extends StatelessWidget { final AccountTrade holding; const _HoldingItem({required this.holding}); @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ CircleAvatar( radius: 18, backgroundColor: context.colors.primary.withValues(alpha: 0.1), child: Text( holding.coinCode.substring(0, 1), style: AppTextStyles.headlineMedium(context).copyWith( color: context.colors.primary, fontWeight: FontWeight.bold, ), ), ), SizedBox(width: AppSpacing.sm + AppSpacing.xs), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(holding.coinCode, style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, )), Text(holding.quantity, style: AppTextStyles.bodyMedium(context)), ], ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${holding.currentValue} USDT', style: AppTextStyles.headlineSmall(context).copyWith( fontWeight: FontWeight.w500, )), Text(holding.formattedProfitRate, style: AppTextStyles.numberSmall(context).copyWith( color: holding.isProfit ? context.appColors.up : context.appColors.down, )), ], ), ], ), ); } } /// 信息行组件 class _InfoRow extends StatelessWidget { final String label; final String value; final bool isBold; const _InfoRow({required this.label, required this.value, this.isBold = false}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: AppTextStyles.bodyMedium(context)), Text(value, style: AppTextStyles.bodyMedium(context).copyWith( fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFeatures: isBold ? const [FontFeature.tabularFigures()] : null, )), ], ); } } /// 钱包地址卡片 class _WalletAddressCard extends StatelessWidget { final String address; final String network; const _WalletAddressCard({required this.address, required this.network}); @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: context.colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: context.colors.outlineVariant.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( address, style: AppTextStyles.bodyMedium(context).copyWith( fontFamily: 'monospace', ), ), ), GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: address)); ToastUtils.show('地址已复制到剪贴板'); }, child: Container( padding: EdgeInsets.all(AppSpacing.xs), decoration: BoxDecoration( color: context.colors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Icon(LucideIcons.copy, size: 16, color: context.colors.primary), ), ), ], ), SizedBox(height: AppSpacing.sm), Text( '网络: $network', style: AppTextStyles.bodySmall(context), ), ], ), ); } } /// 盈亏统计小卡片 class _ProfitStatCard extends StatelessWidget { final String label; final double? value; final Color upColor; final Color downColor; final VoidCallback? onTap; const _ProfitStatCard({ required this.label, required this.value, required this.upColor, required this.downColor, this.onTap, }); @override Widget build(BuildContext context) { final hasValue = value != null; final isProfit = (value ?? 0) >= 0; final color = hasValue ? (isProfit ? upColor : downColor) : context.colors.onSurfaceVariant; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( padding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2), decoration: BoxDecoration( color: hasValue ? (isProfit ? upColor : downColor).withValues(alpha: 0.06) : context.colors.surfaceContainerHigh.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: hasValue ? (isProfit ? upColor : downColor).withValues(alpha: 0.12) : context.colors.outlineVariant.withValues(alpha: 0.1), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( hasValue ? (isProfit ? LucideIcons.trendingUp : LucideIcons.trendingDown) : LucideIcons.minus, size: 11, color: color.withValues(alpha: 0.7), ), SizedBox(width: 3), Text( label, style: AppTextStyles.bodySmall(context).copyWith( fontWeight: FontWeight.w400, // w500 → w400 color: color.withValues(alpha: 0.8), ), ), ], ), SizedBox(height: AppSpacing.xs), Text( hasValue ? '${isProfit ? '+' : ''}${value!.toStringAsFixed(2)}' : '--', style: AppTextStyles.numberMedium(context).copyWith( fontWeight: FontWeight.w600, // bold → w600 color: color, ), ), ], ), ), ); } }