import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/router/routes.dart'; import '../../../core/utils/format_utils.dart'; import '../../../core/network/price_websocket_service.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/constants/app_colors.dart'; import '../../../domain/entities/asset_display.dart'; import '../../../domain/entities/trade_order.dart'; import '../../../data/models/trade_order_model.dart'; import '../../providers/user_providers.dart'; import '../../providers/asset_providers.dart'; import '../../providers/mining_providers.dart'; import '../../providers/trading_providers.dart'; import '../../widgets/shimmer_loading.dart'; class AssetPage extends ConsumerStatefulWidget { const AssetPage({super.key}); @override ConsumerState createState() => _AssetPageState(); } class _AssetPageState extends ConsumerState { // 设计色彩 static const Color _orange = Color(0xFFFF6B00); static const Color _green = Color(0xFF10B981); static const Color _grayText = Color(0xFF6B7280); static const Color _darkText = Color(0xFF1F2937); static const Color _bgGray = Color(0xFFF3F4F6); static const Color _riverBed = Color(0xFF4B5563); static const Color _serenade = Color(0xFFFFF7ED); static const Color _feta = Color(0xFFF0FDF4); static const Color _scandal = Color(0xFFDCFCE7); static const Color _jewel = Color(0xFF15803D); // 实时刷新相关状态 Timer? _refreshTimer; int _elapsedSeconds = 0; double _initialShareBalance = 0; double _growthPerSecond = 0; String? _lastAccountSequence; bool _timerStarted = false; // WebSocket 相关 StreamSubscription? _priceSubscription; String _currentPrice = '0'; String _currentBurnMultiplier = '0'; @override void initState() { super.initState(); _connectWebSocket(); } @override void dispose() { _disconnectWebSocket(); _refreshTimer?.cancel(); super.dispose(); } /// 连接 WebSocket void _connectWebSocket() { final wsService = PriceWebSocketService.instance; wsService.connect(AppConstants.baseUrl); // 监听价格更新 _priceSubscription = wsService.priceUpdates.listen((update) { if (mounted) { setState(() { _currentPrice = update.price; _currentBurnMultiplier = update.burnMultiplier; }); } }); } /// 断开 WebSocket void _disconnectWebSocket() { _priceSubscription?.cancel(); _priceSubscription = null; PriceWebSocketService.instance.disconnect(); } /// 启动定时器(使用外部传入的每秒增长值) void _startTimerWithGrowth(AssetDisplay asset, String perSecondEarning) { // 防止重复启动 if (_timerStarted && _refreshTimer != null) { return; } _refreshTimer?.cancel(); _elapsedSeconds = 0; _initialShareBalance = double.tryParse(asset.shareBalance) ?? 0; // 使用传入的每秒增长值(来自 mining-service) _growthPerSecond = double.tryParse(perSecondEarning) ?? 0; // 初始化价格(如果 WebSocket 还没推送) if (_currentPrice == '0') { _currentPrice = asset.currentPrice; _currentBurnMultiplier = asset.burnMultiplier; } _timerStarted = true; _refreshTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (mounted) { setState(() { _elapsedSeconds++; }); } }); } /// 重置定时器(刷新时调用) void _resetTimer() { _refreshTimer?.cancel(); _refreshTimer = null; _elapsedSeconds = 0; _timerStarted = false; } /// 计算当前实时总资产显示值 /// 总资产 = 积分股价值 + 积分值余额 /// 积分股价值 = 当前积分股余额 × (1 + burnMultiplier) × price /// 积分值余额 = 可用积分值 + 冻结积分值 double _calculateTotalAssetValue(AssetDisplay? asset) { // 优先使用 WebSocket 推送的价格,否则使用 API 返回的价格 final price = double.tryParse(_currentPrice) ?? 0; final burnMultiplier = double.tryParse(_currentBurnMultiplier) ?? 0; final multiplierFactor = 1 + burnMultiplier; // 积分股价值 final shareValue = _currentShareBalance * multiplierFactor * price; // 积分值余额(现金 = 可用 + 冻结) final availableCash = double.tryParse(asset?.availableCash ?? '0') ?? 0; final frozenCash = double.tryParse(asset?.frozenCash ?? '0') ?? 0; final totalCash = availableCash + frozenCash; return shareValue + totalCash; } /// 计算当前实时积分股余额 double get _currentShareBalance { return _initialShareBalance + (_elapsedSeconds * _growthPerSecond); } /// 计算当前有效积分股(含倍数) double get _currentEffectiveShares { final burnMultiplier = double.tryParse(_currentBurnMultiplier) ?? 0; return _currentShareBalance * (1 + burnMultiplier); } AssetDisplay? _lastAsset; @override Widget build(BuildContext context) { final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; // 使用 public API,不依赖 JWT token final assetAsync = ref.watch(accountAssetProvider(accountSequence)); // 从 mining-service 获取每秒收益 final shareAccountAsync = ref.watch(shareAccountProvider(accountSequence)); // 获取订单列表,用于显示冻结状态 final ordersAsync = ref.watch(ordersProvider); // 提取数据和加载状态 final isLoading = assetAsync.isLoading || accountSequence.isEmpty; final asset = assetAsync.valueOrNull; final shareAccount = shareAccountAsync.valueOrNull; final orders = ordersAsync.valueOrNull?.data ?? []; // 获取每秒收益(优先使用 mining-service 的数据) final perSecondEarning = shareAccount?.perSecondEarning ?? '0'; final hasValidGrowth = (double.tryParse(perSecondEarning) ?? 0) > 0; // 当数据加载完成时启动定时器 if (asset != null && hasValidGrowth) { // 账户切换或首次加载时重置并启动定时器 if (_lastAccountSequence != accountSequence) { _lastAccountSequence = accountSequence; _lastAsset = asset; _resetTimer(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _startTimerWithGrowth(asset, perSecondEarning); }); } else if (!_timerStarted) { // 定时器未启动时启动(例如页面刚进入) _lastAsset = asset; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _startTimerWithGrowth(asset, perSecondEarning); }); } else { _lastAsset = asset; } } else if (asset != null) { _lastAsset = asset; } return Scaffold( backgroundColor: AppColors.backgroundOf(context), body: SafeArea( bottom: false, child: LayoutBuilder( builder: (context, constraints) { return RefreshIndicator( onRefresh: () async { _resetTimer(); _lastAsset = null; ref.invalidate(accountAssetProvider(accountSequence)); ref.invalidate(shareAccountProvider(accountSequence)); ref.invalidate(ordersProvider); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: Column( children: [ // 顶部导航栏 _buildAppBar(context, user), // 内容 Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ const SizedBox(height: 8), // 总资产卡片 - 始终显示,数字部分闪烁,实时刷新 _buildTotalAssetCard(context, asset, isLoading, _calculateTotalAssetValue(asset), _currentShareBalance, perSecondEarning), const SizedBox(height: 24), // 快捷操作按钮 _buildQuickActions(context), const SizedBox(height: 24), // 资产列表 - 始终显示,数字部分闪烁,实时刷新 _buildAssetList(context, asset, isLoading, _currentShareBalance, perSecondEarning, orders), const SizedBox(height: 24), // 交易统计 _buildEarningsCard(context, asset, isLoading), const SizedBox(height: 24), // 账户列表 _buildAccountList(context, asset, isLoading), const SizedBox(height: 24), ], ), ), ], ), ), ), ); }, ), ), ); } Widget _buildAppBar(BuildContext context, user) { return Container( color: AppColors.surfaceOf(context).withOpacity(0.9), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Center( child: Text( '我的资产', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context), ), ), ), ); } Widget _buildTotalAssetCard(BuildContext context, AssetDisplay? asset, bool isLoading, double totalAssetValue, double currentShareBalance, String perSecondEarning) { // 使用传入的每秒增长值(来自 mining-service) final growthPerSecond = double.tryParse(perSecondEarning) ?? 0.0; final isDark = AppColors.isDark(context); // 使用实时计算的总资产值(积分股价值 + 积分值余额) final displayValue = asset != null && totalAssetValue > 0 ? totalAssetValue.toString() : asset?.displayAssetValue; // 计算有效积分股(含倍数)= 实时积分股余额 × (1 + burnMultiplier) final burnMultiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0; final effectiveShareBalance = asset != null && currentShareBalance > 0 ? (currentShareBalance * (1 + burnMultiplier)).toString() : asset?.effectiveShares; return Container( decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withOpacity(0.3) : Colors.black.withOpacity(0.04), blurRadius: 30, offset: const Offset(0, 8), ), ], ), child: Stack( children: [ // 背景装饰圆 Positioned( right: -20, top: -40, child: Container( width: 128, height: 128, decoration: BoxDecoration( color: isDark ? _orange.withOpacity(0.1) : _serenade, borderRadius: BorderRadius.circular(64), ), ), ), // 顶部渐变条 Positioned( top: 0, left: 0, right: 0, child: Container( height: 4, decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFFFF6B00), Color(0xFFFDBA74)], ), borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), ), ), // 内容 Padding( padding: const EdgeInsets.fromLTRB(24, 20, 24, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ Text( '总资产估值', style: TextStyle( fontSize: 14, color: AppColors.textSecondaryOf(context), ), ), const SizedBox(width: 8), Icon( Icons.visibility_outlined, size: 14, color: AppColors.textMutedOf(context), ), ], ), const SizedBox(height: 8), // 金额 - 实时刷新显示 AmountText( amount: displayValue != null ? formatAmount(displayValue) : null, isLoading: isLoading, suffix: ' 积分值', style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, color: _orange, letterSpacing: -0.75, ), ), const SizedBox(height: 4), // 有效积分股(含倍数)- 暂时隐藏 // DataText( // data: effectiveShareBalance != null ? '≈ ${formatCompact(effectiveShareBalance)} 积分股 (含倍数)' : null, // isLoading: isLoading, // placeholder: '≈ -- 积分股', // style: const TextStyle( // fontSize: 14, // color: Color(0xFF9CA3AF), // ), // ), // 每秒增长 - 暂时隐藏 // const SizedBox(height: 12), // Container( // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // decoration: BoxDecoration( // color: isDark ? _green.withOpacity(0.15) : _feta, // borderRadius: BorderRadius.circular(8), // ), // child: Row( // mainAxisSize: MainAxisSize.min, // children: [ // const Icon(Icons.bolt, size: 14, color: _green), // const SizedBox(width: 6), // DataText( // data: asset != null // ? '+${formatDecimal(growthPerSecond.toString(), 8)}/秒' // : null, // isLoading: isLoading, // placeholder: '+--/秒', // style: const TextStyle( // fontSize: 12, // fontWeight: FontWeight.w500, // color: _green, // ), // ), // ], // ), // ), ], ), ), ], ), ); } Widget _buildQuickActions(BuildContext context) { final isDark = AppColors.isDark(context); return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildQuickActionItem( context, Icons.add, '接收', _orange, () => context.push(Routes.receiveShares), isDark, ), _buildQuickActionItem( context, Icons.remove, '发送', _orange, () => context.push(Routes.sendShares), isDark, ), _buildQuickActionItem( context, Icons.people_outline, 'C2C', _orange, () => context.push(Routes.c2cMarket), isDark, ), ], ); } Widget _buildQuickActionItem( BuildContext context, IconData icon, String label, Color color, VoidCallback onTap, bool isDark, ) { return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Column( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: isDark ? _orange.withOpacity(0.15) : _serenade, borderRadius: BorderRadius.circular(16), ), child: Icon(icon, color: color, size: 24), ), const SizedBox(height: 8), Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.textSecondaryOf(context), ), ), ], ), ); } Widget _buildAssetList(BuildContext context, AssetDisplay? asset, bool isLoading, double currentShareBalance, String perSecondEarning, List orders) { // 使用实时积分股余额 final shareBalance = asset != null && currentShareBalance > 0 ? currentShareBalance : double.tryParse(asset?.shareBalance ?? '0') ?? 0; final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0; final multipliedAsset = shareBalance * multiplier; final currentPrice = double.tryParse(asset?.currentPrice ?? '0') ?? 0; final isDark = AppColors.isDark(context); // 根据订单状态动态计算冻结原因 final frozenShares = double.tryParse(asset?.frozenShares ?? '0') ?? 0; final frozenSharesSubtitle = _getFrozenSharesSubtitle(frozenShares, orders); return Column( children: [ // 积分股 - 实时刷新 _buildAssetItem( context: context, icon: Icons.trending_up, iconColor: _orange, iconBgColor: isDark ? _orange.withOpacity(0.15) : _serenade, title: '积分股', amount: asset != null ? shareBalance.toString() : null, isLoading: isLoading, valueInCny: asset != null ? '${formatAmount((shareBalance * (1 + multiplier) * currentPrice).toString())} 积分值' : null, // tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null, // 暂时隐藏 growthText: asset != null ? '每秒 +${formatDecimal(perSecondEarning, 8)}' : null, ), const SizedBox(height: 16), // 积分值(现金余额)- 1积分值=1人民币,不需要显示估值 _buildAssetItem( context: context, icon: Icons.eco, iconColor: _green, iconBgColor: isDark ? _green.withOpacity(0.15) : _feta, title: '积分值', amount: asset?.cashBalance, isLoading: isLoading, ), const SizedBox(height: 16), // 冻结积分股 _buildAssetItem( context: context, icon: Icons.lock_outline, iconColor: _orange, iconBgColor: isDark ? _orange.withOpacity(0.15) : _serenade, title: '冻结积分股', amount: asset?.frozenShares, isLoading: isLoading, subtitle: frozenSharesSubtitle, onTap: frozenShares > 0 ? () => context.push(Routes.tradingRecords) : null, ), ], ); } /// 根据订单状态获取冻结积分股的显示文字 String? _getFrozenSharesSubtitle(double frozenShares, List orders) { if (frozenShares <= 0) { return null; } // 检查是否有进行中的卖单(pending 或 partial) final hasPendingSellOrder = orders.any( (order) => order.isSell && (order.isPending || order.isPartial), ); if (hasPendingSellOrder) { return '交易挂单中'; } // 有冻结但没有进行中的挂单,可能是订单已成交但资产还未释放 return '处理中'; } Widget _buildAssetItem({ required BuildContext context, required IconData icon, required Color iconColor, required Color iconBgColor, required String title, String? amount, bool isLoading = false, VoidCallback? onTap, String? valueInCny, String? tag, String? growthText, String? badge, Color? badgeColor, Color? badgeBgColor, String? subtitle, }) { final isDark = AppColors.isDark(context); return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Row( children: [ // 图标 Container( width: 40, height: 40, decoration: BoxDecoration( color: iconBgColor, borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: iconColor, size: 24), ), const SizedBox(width: 12), // 内容 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context), ), ), if (badge != null) ...[ const SizedBox(width: 7), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: badgeBgColor ?? (isDark ? _green.withOpacity(0.2) : _scandal), borderRadius: BorderRadius.circular(9999), ), child: Text( badge, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: badgeColor ?? _jewel, ), ), ), ], ], ), const SizedBox(height: 2), // 数量 - 闪烁占位符 DataText( data: amount != null ? formatAmount(amount) : null, isLoading: isLoading, placeholder: '--', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context), ), ), // 估值 if (valueInCny != null) DataText( data: isLoading ? null : '≈ $valueInCny', isLoading: isLoading, placeholder: '≈ --', style: TextStyle( fontSize: 12, color: AppColors.textMutedOf(context), ), ), // 副标题 if (subtitle != null) Padding( padding: const EdgeInsets.only(top: 3), child: Text( subtitle, style: TextStyle( fontSize: 12, color: AppColors.textMutedOf(context), ), ), ), // 标签行 if (tag != null || growthText != null) Padding( padding: const EdgeInsets.only(top: 8), child: Row( children: [ if (tag != null) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: isDark ? _orange.withOpacity(0.15) : _serenade, borderRadius: BorderRadius.circular(12), ), child: DataText( data: isLoading ? null : tag, isLoading: isLoading, placeholder: '含倍数资产: --', style: const TextStyle( fontSize: 10, color: _orange, ), ), ), if (growthText != null) ...[ const SizedBox(width: 8), Row( children: [ const Icon(Icons.bolt, size: 12, color: _green), DataText( data: isLoading ? null : growthText, isLoading: isLoading, placeholder: '每秒 +--', style: const TextStyle( fontSize: 10, color: _green, ), ), ], ), ], ], ), ), ], ), ), // 箭头 Icon(Icons.chevron_right, size: 14, color: AppColors.textMutedOf(context)), ], ), ), ); } Widget _buildEarningsCard(BuildContext context, AssetDisplay? asset, bool isLoading) { final isDark = AppColors.isDark(context); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Column( children: [ // 标题 Row( children: [ Container( width: 4, height: 20, decoration: BoxDecoration( color: _orange, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( '交易统计', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context), ), ), ], ), const SizedBox(height: 16), // 统计数据 Row( children: [ _buildEarningsItem( context, '累计买入', asset != null ? formatCompact(asset.totalBought) : null, _orange, isLoading, ), Container( width: 1, height: 40, color: isDark ? AppColors.borderOf(context) : _serenade, ), _buildEarningsItem( context, '累计卖出', asset != null ? formatCompact(asset.totalSold) : null, _green, isLoading, ), Container( width: 1, height: 40, color: isDark ? AppColors.borderOf(context) : _serenade, ), _buildEarningsItem( context, '销毁倍数', asset != null ? '${formatDecimal(asset.burnMultiplier, 4)}x' : null, AppColors.textMutedOf(context), isLoading, ), ], ), ], ), ); } Widget _buildEarningsItem(BuildContext context, String label, String? value, Color valueColor, bool isLoading) { return Expanded( child: Column( children: [ Text( label, style: TextStyle( fontSize: 12, color: AppColors.textSecondaryOf(context), ), textAlign: TextAlign.center, ), const SizedBox(height: 4), DataText( data: value, isLoading: isLoading, placeholder: '--', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: valueColor, ), textAlign: TextAlign.center, ), ], ), ); } Widget _buildAccountList(BuildContext context, AssetDisplay? asset, bool isLoading) { final isDark = AppColors.isDark(context); return Column( children: [ // 交易账户(可用现金) _buildAccountItem( context: context, icon: Icons.account_balance_wallet, iconColor: _orange, title: '可用积分值', balance: asset?.availableCash, isLoading: isLoading, unit: '积分值', status: '可交易', statusColor: _green, statusBgColor: isDark ? _green.withOpacity(0.15) : _feta, ), const SizedBox(height: 16), // 冻结现金 _buildAccountItem( context: context, icon: Icons.lock_outline, iconColor: _orange, title: '冻结积分值', balance: asset?.frozenCash, isLoading: isLoading, unit: '积分值', status: (double.tryParse(asset?.frozenCash ?? '0') ?? 0) > 0 ? '挂单中' : '无', statusColor: AppColors.textMutedOf(context), statusBgColor: AppColors.cardOf(context), statusBorder: true, ), ], ); } Widget _buildAccountItem({ required BuildContext context, required IconData icon, required Color iconColor, required String title, String? balance, bool isLoading = false, required String unit, required String status, required Color statusColor, required Color statusBgColor, bool statusBorder = false, }) { final isDark = AppColors.isDark(context); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withOpacity(0.2) : Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Row( children: [ // 图标 Container( width: 36, height: 36, decoration: BoxDecoration( color: isDark ? _orange.withOpacity(0.15) : _serenade, borderRadius: BorderRadius.circular(18), ), child: Icon(icon, color: iconColor, size: 20), ), const SizedBox(width: 12), // 内容 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textPrimaryOf(context), ), ), const SizedBox(height: 2), Row( children: [ DataText( data: balance != null ? formatAmount(balance) : null, isLoading: isLoading, placeholder: '--', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _orange, ), ), Text( ' $unit', style: TextStyle( fontSize: 12, color: AppColors.textMutedOf(context), ), ), ], ), ], ), ), // 状态标签 Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: statusBgColor, borderRadius: BorderRadius.circular(9999), border: statusBorder ? Border.all(color: AppColors.borderOf(context)) : null, ), child: Text( status, style: TextStyle( fontSize: 10, color: statusColor, ), ), ), const SizedBox(width: 8), // 箭头 Icon(Icons.chevron_right, size: 14, color: AppColors.textMutedOf(context)), ], ), ); } }