import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme_extension.dart'; import '../../../core/event/app_event_bus.dart'; import '../../../data/models/account_models.dart'; import '../../../data/services/bonus_service.dart'; import '../../../data/services/asset_service.dart'; import '../../../providers/asset_provider.dart'; import '../../components/glass_panel.dart'; import '../mine/welfare_center_page.dart'; import '../asset/transfer_page.dart'; import '../asset/components/asset_dialogs.dart'; import 'header_bar.dart'; import 'quick_actions_row.dart'; import 'hot_coins_section.dart'; import 'profit_analysis_page.dart'; import 'bills_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: _showWithdraw, onTransfer: _navigateToTransfer, onProfit: _navigateToProfitAnalysis, onBills: _navigateToBills, ), 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() { showDepositDialog(context); } void _showWithdraw() { final balance = context.read().fundAccount?.balance ?? '0.00'; showWithdrawDialog(context, balance); } void _navigateToTransfer() { Navigator.push( context, MaterialPageRoute(builder: (_) => const TransferPage()), ); } void _navigateToProfitAnalysis() { Navigator.push( context, MaterialPageRoute(builder: (_) => const ProfitAnalysisPage()), ); } void _navigateToBills() { Navigator.push( context, MaterialPageRoute(builder: (_) => const BillsPage()), ); } } /// 资产卡片(含预估盈亏) class _AssetCard extends StatefulWidget { final AssetOverview? overview; final VoidCallback onDeposit; const _AssetCard({required this.overview, required this.onDeposit}); @override State<_AssetCard> createState() => _AssetCardState(); } class _AssetCardState extends State<_AssetCard> { Map? _profitData; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _loadProfit()); } @override void didUpdateWidget(covariant _AssetCard oldWidget) { super.didUpdateWidget(oldWidget); // overview 更新时重新加载盈亏数据 if (widget.overview != oldWidget.overview) { _loadProfit(); } } Future _loadProfit() async { try { final now = DateTime.now(); final response = await context.read().getDailyProfit( year: now.year, month: now.month, ); if (mounted && response.success && response.data != null) { setState(() { _profitData = response.data; }); } } catch (_) {} } double get _todayProfit { if (_profitData == null) return 0; final daily = _profitData!['daily'] as Map?; if (daily == null) return 0; final todayKey = DateTime.now().toIso8601String().substring(0, 10); final todayValue = daily[todayKey]; if (todayValue == null) return 0; return (todayValue as num).toDouble(); } double get _totalProfit { final v = widget.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 totalAsset = widget.overview?.totalAsset ?? '0.00'; final displayAsset = _formatAsset(totalAsset); return GlassPanel( padding: EdgeInsets.all(20), borderRadius: BorderRadius.circular(AppRadius.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部行:总资产标签 + 充值按钮 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '预估总资产(USDT)', style: AppTextStyles.bodyLarge(context), ), GestureDetector( onTap: widget.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: [ 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: _todayProfit, 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 _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 isProfit = value >= 0; final color = isProfit ? upColor : downColor; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( padding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2), decoration: BoxDecoration( color: color.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: color.withValues(alpha: 0.12), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( isProfit ? LucideIcons.trendingUp : LucideIcons.trendingDown, size: 11, color: color.withValues(alpha: 0.7), ), SizedBox(width: 3), Text( label, style: AppTextStyles.bodySmall(context).copyWith( fontWeight: FontWeight.w400, color: color.withValues(alpha: 0.8), ), ), ], ), SizedBox(height: AppSpacing.xs), Text( '${isProfit ? '+' : ''}${value.toStringAsFixed(2)}', style: AppTextStyles.numberMedium(context).copyWith( fontWeight: FontWeight.w600, color: color, ), ), ], ), ), ); } }