import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:bot_toast/bot_toast.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/asset_service.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 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> { late DateTime _currentMonth; Map? _profitData; bool _isLoadingCalendar = false; @override void initState() { super.initState(); _currentMonth = DateTime.now(); WidgetsBinding.instance.addPostFrameCallback((_) => _loadProfit()); } double get _totalProfit { final v = widget.overview?.totalProfit; if (v == null) return 0; return double.tryParse(v) ?? 0; } Future _loadProfit() async { setState(() => _isLoadingCalendar = true); try { final assetService = context.read(); final response = await assetService.getDailyProfit( year: _currentMonth.year, month: _currentMonth.month, ); if (mounted) { setState(() { _profitData = response.data; _isLoadingCalendar = false; }); } } catch (_) { if (mounted) setState(() => _isLoadingCalendar = false); } } void _previousMonth() { setState(() { _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1); }); _loadProfit(); } void _nextMonth() { setState(() { _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1); }); _loadProfit(); } double? _getDayProfit(int day) { if (_profitData == null) return null; final daily = _profitData!['daily'] as Map?; if (daily == null) return null; final dateStr = '${_currentMonth.year}-${_currentMonth.month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; final value = daily[dateStr]; if (value == null) return null; return (value is num) ? value.toDouble() : double.tryParse(value.toString()); } double get _monthProfit { if (_profitData == null) return 0; final value = _profitData!['totalProfit']; if (value == null) return 0; return (value is num) ? value.toDouble() : double.tryParse(value.toString()) ?? 0; } double? get _todayProfit { if (_profitData == null) return null; final daily = _profitData!['daily'] as Map?; if (daily == null) return null; final now = DateTime.now(); final todayKey = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; final value = daily[todayKey]; if (value == null) return null; return (value is num) ? value.toDouble() : double.tryParse(value.toString()); } @override Widget build(BuildContext context) { final upColor = context.appColors.up; final downColor = context.appColors.down; final isProfit = _totalProfit >= 0; final todayProfit = _todayProfit; final isTodayProfit = (todayProfit ?? 0) >= 0; // 总资产 final totalAsset = widget.overview?.totalAsset ?? '0.00'; final displayAsset = _formatAsset(totalAsset); return GlassPanel( padding: EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部行:总资产标签 + 充值按钮 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '预估总资产(USDT)', style: AppTextStyles.bodyMedium(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: [ 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.displaySmall(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()), ), ), ), ], ), // 盈利日历 _buildCalendarSection(context), ], ), ); } Widget _buildCalendarSection(BuildContext context) { final now = DateTime.now(); final isCurrentMonth = _currentMonth.year == now.year && _currentMonth.month == now.month; final firstDayOfMonth = DateTime(_currentMonth.year, _currentMonth.month, 1); final daysInMonth = DateTime(_currentMonth.year, _currentMonth.month + 1, 0).day; final startWeekday = firstDayOfMonth.weekday; final upColor = context.appColors.up; final downColor = context.appColors.down; return Column( mainAxisSize: MainAxisSize.min, children: [ // 分隔线 Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.md), child: Divider(color: context.appColors.ghostBorder, height: 1), ), // 月份导航行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( onTap: _previousMonth, child: Container( padding: EdgeInsets.all(AppSpacing.xs + 1), decoration: BoxDecoration( color: context.colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Icon(LucideIcons.chevronLeft, size: 16, color: context.colors.onSurfaceVariant), ), ), Column( children: [ Text( '${_currentMonth.year}年${_currentMonth.month}月', style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, ), ), SizedBox(height: 2), if (!_isLoadingCalendar && _profitData != null) Text( '月度盈亏: ${_monthProfit >= 0 ? '+' : ''}${_monthProfit.toStringAsFixed(2)}', style: AppTextStyles.bodySmall(context).copyWith( fontWeight: FontWeight.w600, color: _monthProfit >= 0 ? upColor : downColor, ), ), ], ), GestureDetector( onTap: isCurrentMonth ? null : _nextMonth, child: Container( padding: EdgeInsets.all(AppSpacing.xs + 1), decoration: BoxDecoration( color: isCurrentMonth ? context.colors.surfaceContainerHigh.withValues(alpha: 0.5) : context.colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Icon( LucideIcons.chevronRight, size: 16, color: isCurrentMonth ? context.colors.onSurfaceVariant.withValues(alpha: 0.4) : context.colors.onSurfaceVariant, ), ), ), ], ), SizedBox(height: AppSpacing.sm), // 星期标题 Row( children: ['一', '二', '三', '四', '五', '六', '日'].map((d) { return Expanded( child: Center( child: Text( d, style: AppTextStyles.bodySmall(context).copyWith( fontWeight: FontWeight.w500, color: context.colors.onSurfaceVariant.withValues(alpha: 0.6), ), ), ), ); }).toList(), ), SizedBox(height: AppSpacing.xs), // 日历网格 if (_isLoadingCalendar) Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.lg), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: context.colors.primary, ), ), ) else ..._buildCalendarGrid( startWeekday, daysInMonth, now, isCurrentMonth, ), ], ); } List _buildCalendarGrid( int startWeekday, int daysInMonth, DateTime now, bool isCurrentMonth, ) { final upColor = context.appColors.up; final downColor = context.appColors.down; final List rows = []; List cells = []; for (int i = 1; i < startWeekday; i++) { cells.add(const Expanded(child: SizedBox.shrink())); } for (int day = 1; day <= daysInMonth; day++) { final profit = _getDayProfit(day); final isToday = isCurrentMonth && day == now.day; final hasProfit = profit != null && profit != 0; cells.add( Expanded( child: AspectRatio( aspectRatio: 1, child: Container( margin: EdgeInsets.all(1), decoration: BoxDecoration( color: isToday ? context.colors.primary.withValues(alpha: 0.12) : hasProfit ? (profit! > 0 ? upColor.withValues(alpha: 0.08) : downColor.withValues(alpha: 0.08)) : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.sm), border: isToday ? Border.all(color: context.colors.primary.withValues(alpha: 0.4), width: 1) : null, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$day', style: AppTextStyles.bodySmall(context).copyWith( fontSize: 10, fontWeight: isToday ? FontWeight.bold : FontWeight.w400, color: isToday ? context.colors.primary : context.colors.onSurface, ), ), if (hasProfit) ...[ SizedBox(height: 1), Text( '${profit! > 0 ? '+' : ''}${profit.abs() < 10 ? profit.toStringAsFixed(2) : profit.toStringAsFixed(1)}', style: TextStyle( fontSize: 7, fontWeight: FontWeight.w600, color: profit > 0 ? upColor : downColor, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), ), ), ); if (cells.length == 7) { rows.add(Row(children: cells)); cells = []; } } if (cells.isNotEmpty) { while (cells.length < 7) { cells.add(const Expanded(child: SizedBox.shrink())); } rows.add(Row(children: cells)); } return rows; } 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( gradient: LinearGradient( colors: [ context.colors.primary.withValues(alpha: 0.15), context.colors.secondary.withValues(alpha: 0.1), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: context.colors.primary.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( gradient: context.appColors.ctaGradient, 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: context.colors.onPrimary, ), ), ], ), ), ], ), ), ); } } /// 持仓部分 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.w500, color: color.withValues(alpha: 0.8), ), ), ], ), SizedBox(height: AppSpacing.xs), Text( hasValue ? '${isProfit ? '+' : ''}${value!.toStringAsFixed(2)}' : '--', style: AppTextStyles.numberMedium(context).copyWith( fontWeight: FontWeight.bold, color: color, ), ), ], ), ), ); } }