import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../core/constants/app_colors.dart'; import '../../../core/router/routes.dart'; import '../../../core/utils/format_utils.dart'; import '../../../data/models/trade_order_model.dart'; import '../../../domain/entities/price_info.dart'; import '../../../domain/entities/market_overview.dart'; import '../../../domain/entities/trade_order.dart'; import '../../../domain/entities/kline.dart'; import '../../providers/user_providers.dart'; import '../../providers/trading_providers.dart'; import '../../providers/asset_providers.dart'; import '../../widgets/shimmer_loading.dart'; import '../../widgets/kline_chart/kline_chart_widget.dart'; class TradingPage extends ConsumerStatefulWidget { const TradingPage({super.key}); @override ConsumerState createState() => _TradingPageState(); } class _TradingPageState extends ConsumerState { // 品牌色彩(不随主题变化) static const Color _orange = AppColors.orange; static const Color _green = AppColors.up; static const Color _red = AppColors.down; // 状态 int _selectedTab = 1; // 0: 买入, 1: 卖出 int _selectedTimeRange = 4; // 时间周期选择,默认1时 final _quantityController = TextEditingController(); final _priceController = TextEditingController(); bool _isFullScreen = false; // K线图全屏状态 final List _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日']; @override void dispose() { _quantityController.dispose(); _priceController.dispose(); super.dispose(); } @override void initState() { super.initState(); // 初始化时加载K线数据 WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[_selectedTimeRange]); }); } @override Widget build(BuildContext context) { final priceAsync = ref.watch(currentPriceProvider); final marketAsync = ref.watch(marketOverviewProvider); final ordersAsync = ref.watch(ordersProvider); final klinesState = ref.watch(klinesNotifierProvider); final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; // 全屏K线图模式 if (_isFullScreen) { return KlineChartWidget( klines: klinesState.klines, currentPrice: priceAsync.valueOrNull?.price ?? '0', isFullScreen: true, onFullScreenToggle: () => setState(() => _isFullScreen = false), timeRanges: _timeRanges, selectedTimeIndex: _selectedTimeRange, onTimeRangeChanged: (index) { setState(() => _selectedTimeRange = index); ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index]; ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[index]); }, isLoadingMore: klinesState.isLoadingMore, hasMoreHistory: klinesState.hasMoreHistory, onLoadMoreHistory: () => ref.read(klinesNotifierProvider.notifier).loadMoreHistory(), ); } return Scaffold( backgroundColor: AppColors.backgroundOf(context), body: SafeArea( bottom: false, child: RefreshIndicator( onRefresh: () async { ref.invalidate(currentPriceProvider); ref.invalidate(marketOverviewProvider); ref.invalidate(ordersProvider); // 重新加载K线数据 await ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[_selectedTimeRange]); }, child: Column( children: [ _buildAppBar(), Expanded( child: SingleChildScrollView( child: Column( children: [ _buildPriceCard(priceAsync), _buildChartSection(priceAsync, klinesState), _buildMarketDataCard(marketAsync), _buildTradingPanel(priceAsync), _buildMyOrdersCard(ordersAsync), const SizedBox(height: 24), ], ), ), ), ], ), ), ), ); } Widget _buildAppBar() { return Container( color: AppColors.surfaceOf(context), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Center( child: Text( '积分股兑换', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryOf(context), ), ), ), ); } Widget _buildPriceCard(AsyncValue priceAsync) { final isLoading = priceAsync.isLoading; final priceInfo = priceAsync.valueOrNull; final hasError = priceAsync.hasError; if (hasError && priceInfo == null) { return _buildErrorCard('价格加载失败'); } final price = priceInfo?.price ?? '0'; return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '当前积分股价值', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.textSecondaryOf(context), ), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ AmountText( amount: priceInfo != null ? formatPrice(price) : null, isLoading: isLoading, style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, color: _orange, letterSpacing: -0.75, ), ), const SizedBox(width: 4), Text( '积分值', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textSecondaryOf(context), ), ), ], ), const SizedBox(height: 8), Builder( builder: (context) { final changePercent = double.tryParse(priceInfo?.priceChangePercent ?? '0') ?? 0; final isPositive = changePercent >= 0; final color = isPositive ? _green : _red; final icon = isPositive ? Icons.trending_up : Icons.trending_down; final sign = isPositive ? '+' : ''; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: color), const SizedBox(width: 4), Text( '较上线首日', style: TextStyle( fontSize: 12, color: AppColors.textSecondaryOf(context), ), ), const SizedBox(width: 4), DataText( data: isLoading ? null : '$sign${changePercent.toStringAsFixed(2)}%', isLoading: isLoading, placeholder: '+--.--%', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: color, ), ), ], ), ); }, ), ], ), ); } Widget _buildChartSection(AsyncValue priceAsync, KlinesState klinesState) { final priceInfo = priceAsync.valueOrNull; final currentPrice = priceInfo?.price ?? '0.000000'; return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: klinesState.isLoading && klinesState.klines.isEmpty ? const SizedBox( height: 280, child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ) : KlineChartWidget( klines: klinesState.klines, currentPrice: currentPrice, isFullScreen: false, onFullScreenToggle: () => setState(() => _isFullScreen = true), timeRanges: _timeRanges, selectedTimeIndex: _selectedTimeRange, onTimeRangeChanged: (index) { setState(() => _selectedTimeRange = index); ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index]; ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[index]); }, isLoadingMore: klinesState.isLoadingMore, hasMoreHistory: klinesState.hasMoreHistory, onLoadMoreHistory: () => ref.read(klinesNotifierProvider.notifier).loadMoreHistory(), ), ); } Widget _buildMarketDataCard(AsyncValue marketAsync) { final isLoading = marketAsync.isLoading; final market = marketAsync.valueOrNull; final hasError = marketAsync.hasError; if (hasError && market == null) { return _buildErrorCard('市场数据加载失败'); } final bgGray = AppColors.backgroundOf(context); final darkText = AppColors.textPrimaryOf(context); final grayText = AppColors.textSecondaryOf(context); return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 4, height: 16, decoration: BoxDecoration( color: _orange, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( '市场数据', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: darkText, ), ), ], ), const SizedBox(height: 24), Row( children: [ _buildMarketDataItem( '总积分股', market != null ? formatCompact(market.totalShares) : null, _orange, isLoading, ), Container(width: 1, height: 24, color: bgGray), const SizedBox(width: 16), _buildMarketDataItem( '流通池', market != null ? formatCompact(market.circulationPool) : null, _orange, isLoading, ), ], ), const SizedBox(height: 24), Container(height: 1, color: bgGray), const SizedBox(height: 24), Row( children: [ _buildMarketDataItem( '积分股池', market != null ? formatCompact(market.greenPoints) : null, _orange, isLoading, ), Container(width: 1, height: 24, color: bgGray), const SizedBox(width: 16), _buildMarketDataItem( '黑洞销毁量', market != null ? formatCompact(market.blackHoleAmount) : null, _red, isLoading, ), ], ), ], ), ); } Widget _buildMarketDataItem(String label, String? value, Color valueColor, bool isLoading) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context))), const SizedBox(height: 4), DataText( data: value, isLoading: isLoading, placeholder: '--,---,---', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: valueColor, ), ), ], ), ); } Widget _buildTradingPanel(AsyncValue priceAsync) { final priceInfo = priceAsync.valueOrNull; final currentPrice = priceInfo?.price ?? '0'; // 获取用户资产信息 final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; final assetAsync = ref.watch(accountAssetProvider(accountSequence)); final asset = assetAsync.valueOrNull; // 获取买入功能开关状态 final buyEnabledAsync = ref.watch(buyEnabledProvider); final buyEnabled = buyEnabledAsync.valueOrNull ?? false; // 挖矿账户积分股(可划转卖出) final miningShareBalance = asset?.miningShareBalance ?? '0'; // 交易账户积分股(可直接卖出) final tradingShareBalance = asset?.tradingShareBalance ?? '0'; // 可用积分股(总计:挖矿 + 交易) final availableShares = asset?.availableShares ?? '0'; // 可用积分值(现金) final availableCash = asset?.availableCash ?? '0'; // 始终使用实时价格(价格不可修改) if (priceInfo != null) { _priceController.text = currentPrice; } // 如果选中买入但买入功能未开启,强制切换到卖出 if (_selectedTab == 0 && !buyEnabled) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _selectedTab == 0) { setState(() => _selectedTab = 1); } }); } final grayText = AppColors.textSecondaryOf(context); final bgGray = AppColors.backgroundOf(context); return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Container( decoration: BoxDecoration( border: Border(bottom: BorderSide(color: bgGray)), ), child: Row( children: [ Expanded( child: GestureDetector( onTap: buyEnabled ? () => setState(() => _selectedTab = 0) : null, child: Container( padding: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: _selectedTab == 0 && buyEnabled ? _orange : Colors.transparent, width: 2, ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '买入', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: buyEnabled ? (_selectedTab == 0 ? _orange : grayText) : grayText.withValues(alpha: 0.5), ), ), if (!buyEnabled) ...[ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: grayText.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( '待开启', style: TextStyle( fontSize: 10, color: grayText.withValues(alpha: 0.7), ), ), ), ], ], ), ), ), ), Expanded( child: GestureDetector( onTap: () => setState(() => _selectedTab = 1), child: Container( padding: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: _selectedTab == 1 ? _orange : Colors.transparent, width: 2, ), ), ), child: Text( '卖出', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _selectedTab == 1 ? _orange : grayText, ), ), ), ), ), ], ), ), const SizedBox(height: 24), // 买入功能未开启时显示提示 if (_selectedTab == 0 && !buyEnabled) ...[ Container( padding: const EdgeInsets.all(24), child: Column( children: [ Icon( Icons.lock_outline, size: 48, color: grayText.withValues(alpha: 0.5), ), const SizedBox(height: 16), Text( '买入功能待开启', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: grayText, ), ), const SizedBox(height: 8), Text( '买入功能暂未开放,请耐心等待', style: TextStyle( fontSize: 14, color: grayText.withValues(alpha: 0.7), ), ), ], ), ), ] else ...[ // 可用余额提示 if (_selectedTab == 0) ...[ // 买入时显示可用积分值 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: _orange.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '可用积分值', style: TextStyle(fontSize: 12, color: grayText), ), Text( formatAmount(availableCash), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _orange, ), ), ], ), ), ] else ...[ // 卖出时显示可用积分股 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: _orange.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '可用积分股', style: TextStyle(fontSize: 12, color: grayText), ), Text( formatAmount(tradingShareBalance), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _orange, ), ), ], ), ), const SizedBox(height: 4), // 划转入口 Align( alignment: Alignment.centerRight, child: GestureDetector( onTap: () => _showTransferDialog(miningShareBalance, tradingShareBalance), child: const Text( '划转', style: TextStyle( fontSize: 12, color: _orange, fontWeight: FontWeight.w500, ), ), ), ), ], const SizedBox(height: 16), // 价格显示(只读,使用实时价格) _buildInputField('价格', _priceController, '实时价格', '积分值', readOnly: true), const SizedBox(height: 16), // 数量输入 - 带"全部"按钮 // 卖出时使用交易账户积分股余额(只能卖出交易账户的,挖矿账户需要先划转) _buildQuantityInputField( '数量', _quantityController, '请输入数量', '积分股', _selectedTab == 1 ? tradingShareBalance : null, _selectedTab == 0 ? availableCash : null, currentPrice, ), const SizedBox(height: 16), // 预计获得/支出 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: bgGray, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _selectedTab == 0 ? '预计支出' : '预计获得', style: TextStyle(fontSize: 12, color: grayText), ), Text( _calculateEstimate(), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _orange, ), ), ], ), ), const SizedBox(height: 16), // 交易手续费说明 (卖出时显示) if (_selectedTab == 1) Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '交易手续费', style: TextStyle(fontSize: 12, color: grayText), ), const Text( '10% 进入积分股池', style: TextStyle( fontSize: 12, color: _green, fontFamily: 'monospace', ), ), ], ), ), const SizedBox(height: 24), // 提交按钮 SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: _handleTrade, style: ElevatedButton.styleFrom( backgroundColor: _orange, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text( '确认交易', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ], ), ); } Widget _buildQuantityInputField( String label, TextEditingController controller, String hint, String suffix, String? availableSharesForSell, String? availableCashForBuy, String currentPrice, ) { final grayText = AppColors.textSecondaryOf(context); final bgGray = AppColors.backgroundOf(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: grayText, ), ), const SizedBox(height: 8), Container( height: 44, decoration: BoxDecoration( color: bgGray, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Expanded( child: TextField( controller: controller, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle( fontSize: 14, color: grayText, ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 16), ), onChanged: (_) => setState(() {}), ), ), // 全部按钮 GestureDetector( onTap: () { if (availableSharesForSell != null) { // 卖出时填入全部可用积分股 controller.text = availableSharesForSell; } else if (availableCashForBuy != null) { // 买入时根据可用积分值计算可买数量 final price = double.tryParse(currentPrice) ?? 0; final cash = double.tryParse(availableCashForBuy) ?? 0; if (price > 0) { final maxQuantity = cash / price; controller.text = maxQuantity.toStringAsFixed(4); } } setState(() {}); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: const EdgeInsets.only(right: 4), decoration: BoxDecoration( color: _orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: const Text( '全部', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _orange, ), ), ), ), Text(suffix, style: TextStyle(fontSize: 12, color: grayText)), const SizedBox(width: 12), ], ), ), ], ); } Widget _buildInputField( String label, TextEditingController controller, String hint, String suffix, { bool readOnly = false, }) { final grayText = AppColors.textSecondaryOf(context); final darkText = AppColors.textPrimaryOf(context); final bgGray = AppColors.backgroundOf(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: grayText, ), ), const SizedBox(height: 8), Container( height: 44, decoration: BoxDecoration( color: readOnly ? bgGray.withOpacity(0.7) : bgGray, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Expanded( child: TextField( controller: controller, readOnly: readOnly, enabled: !readOnly, keyboardType: const TextInputType.numberWithOptions(decimal: true), style: TextStyle( fontSize: 14, color: readOnly ? grayText : darkText, ), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle( fontSize: 14, color: grayText, ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 16), ), ), ), Text(suffix, style: TextStyle(fontSize: 12, color: grayText)), const SizedBox(width: 12), ], ), ), ], ); } /// 计算预估获得/支出 /// 卖出公式:卖出交易额 = (卖出量 + 卖出销毁量) × 价格 × 0.9 /// = 卖出量 × (1 + burnMultiplier) × 价格 × 0.9 String _calculateEstimate() { final price = double.tryParse(_priceController.text) ?? 0; final quantity = double.tryParse(_quantityController.text) ?? 0; if (price == 0 || quantity == 0) { return '0.00 积分值'; } if (_selectedTab == 1) { // 卖出时:有效积分股 = 卖出量 × (1 + burnMultiplier) // 卖出交易额 = 有效积分股 × 价格 × 0.9(扣除10%手续费) final marketAsync = ref.read(marketOverviewProvider); final burnMultiplier = double.tryParse( marketAsync.valueOrNull?.burnMultiplier ?? '0', ) ?? 0; final effectiveQuantity = quantity * (1 + burnMultiplier); final grossAmount = effectiveQuantity * price; final netAmount = grossAmount * 0.9; // 扣除10%手续费 return '${formatAmount(netAmount.toString())} 积分值'; } // 买入时:支出 = 价格 × 数量 final total = price * quantity; return '${formatAmount(total.toString())} 积分值'; } Widget _buildMyOrdersCard(AsyncValue ordersAsync) { final isLoading = ordersAsync.isLoading; final ordersPage = ordersAsync.valueOrNull; final orders = ordersPage?.data ?? []; final darkText = AppColors.textPrimaryOf(context); final grayText = AppColors.textSecondaryOf(context); return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '我的挂单', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: darkText, ), ), GestureDetector( onTap: () => context.push(Routes.tradingRecords), child: const Text( '全部 >', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _orange, ), ), ), ], ), const SizedBox(height: 16), if (isLoading) const Center( child: Padding( padding: EdgeInsets.all(20), child: CircularProgressIndicator(color: _orange), ), ) else if (orders.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Text( '暂无挂单', style: TextStyle(fontSize: 14, color: grayText), ), ) else Column( children: orders.take(3).map((order) => Padding( padding: const EdgeInsets.only(bottom: 8), child: _buildOrderItemFromEntity(order), )).toList(), ), ], ), ); } Widget _buildOrderItemFromEntity(TradeOrder order) { final isSell = order.isSell; final dateFormat = DateFormat('MM/dd HH:mm'); final formattedDate = dateFormat.format(order.createdAt); final darkText = AppColors.textPrimaryOf(context); final grayText = AppColors.textSecondaryOf(context); final bgGray = AppColors.backgroundOf(context); final cardBg = AppColors.cardOf(context); String statusText; switch (order.status) { case OrderStatus.pending: statusText = '待成交'; break; case OrderStatus.partial: statusText = '部分成交'; break; case OrderStatus.filled: statusText = '已成交'; break; case OrderStatus.cancelled: statusText = '已取消'; break; } return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(8), border: Border.all(color: bgGray), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: (isSell ? _red : _green).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text( isSell ? '卖出' : '买入', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isSell ? _red : _green, ), ), ), const SizedBox(width: 8), Text( formatPrice(order.price), style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: darkText, ), ), ], ), const SizedBox(height: 4), Text( formattedDate, style: TextStyle(fontSize: 12, color: grayText), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${formatCompact(order.quantity)} 股', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: darkText, ), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(9999), ), child: Text( statusText, style: const TextStyle(fontSize: 12, color: _orange), ), ), ], ), ], ), ); } Widget _buildErrorCard(String message) { return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: AppColors.cardOf(context), borderRadius: BorderRadius.circular(16), ), child: Center( child: Column( children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.error), const SizedBox(height: 8), Text(message), ], ), ), ); } void _handleTrade() async { if (_priceController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请输入价格')), ); return; } if (_quantityController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请输入数量')), ); return; } final isBuy = _selectedTab == 0; final price = double.tryParse(_priceController.text) ?? 0; final quantity = double.tryParse(_quantityController.text) ?? 0; // 卖出时显示确认弹窗 if (!isBuy) { // 获取销毁倍数,使用与后端一致的公式计算 final marketAsync = ref.read(marketOverviewProvider); final burnMultiplier = double.tryParse( marketAsync.valueOrNull?.burnMultiplier ?? '0', ) ?? 0; // 有效积分股 = 卖出量 × (1 + burnMultiplier) final effectiveQuantity = quantity * (1 + burnMultiplier); // 交易总额 = 有效积分股 × 价格 final grossAmount = effectiveQuantity * price; // 手续费 = 交易总额 × 10% final tradeFee = grossAmount * 0.1; // 实际获得 = 交易总额 × 90% final received = grossAmount * 0.9; final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确认卖出'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('卖出数量: ${formatAmount(quantity.toString())} 积分股'), const SizedBox(height: 8), Text('卖出价格: ${formatPrice(price.toString())} 积分值'), const SizedBox(height: 8), Text('销毁倍数: ${burnMultiplier.toStringAsFixed(4)}'), const SizedBox(height: 8), Text('有效积分股: ${formatAmount(effectiveQuantity.toString())}'), const SizedBox(height: 8), Text('交易总额: ${formatAmount(grossAmount.toString())} 积分值'), const SizedBox(height: 8), Text( '进入积分股池: ${formatAmount(tradeFee.toString())} 积分值 (10%)', style: const TextStyle(color: _green), ), const SizedBox(height: 8), Text( '实际获得: ${formatAmount(received.toString())} 积分值', style: const TextStyle( fontWeight: FontWeight.bold, color: _green, ), ), const SizedBox(height: 16), Text( '注意: 卖出积分股将扣除10%进入积分股池,此操作不可撤销。', style: TextStyle( fontSize: 12, color: AppColors.textSecondaryOf(context), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text( '确认卖出', style: TextStyle(color: _orange), ), ), ], ), ); if (confirmed != true) return; } bool success; if (isBuy) { success = await ref .read(tradingNotifierProvider.notifier) .buyShares(_priceController.text, _quantityController.text); } else { success = await ref .read(tradingNotifierProvider.notifier) .sellShares(_priceController.text, _quantityController.text); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(success ? (isBuy ? '买入订单已提交' : '卖出订单已提交') : (isBuy ? '买入失败' : '卖出失败')), backgroundColor: success ? _green : AppColors.error, ), ); if (success) { _quantityController.clear(); // 交易成功后立即刷新 _refreshAfterTrade(); } } } /// 交易成功后刷新数据 /// 立即刷新一次,然后在2秒和5秒后再各刷新一次 /// 确保能看到做市商处理后的最新状态 void _refreshAfterTrade() { final user = ref.read(userNotifierProvider); final accountSeq = user.accountSequence ?? ''; // 立即刷新 _doRefresh(accountSeq); // 2秒后再刷新(做市商可能在1-4秒内吃单) Future.delayed(const Duration(seconds: 2), () { if (mounted) _doRefresh(accountSeq); }); // 5秒后最终刷新(确保看到最终状态) Future.delayed(const Duration(seconds: 5), () { if (mounted) _doRefresh(accountSeq); }); } void _doRefresh(String accountSeq) { ref.invalidate(ordersProvider); ref.invalidate(currentPriceProvider); ref.invalidate(marketOverviewProvider); ref.invalidate(accountAssetProvider(accountSeq)); } /// 显示划转弹窗 void _showTransferDialog(String miningBalance, String tradingBalance) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _TransferBottomSheet( miningBalance: miningBalance, tradingBalance: tradingBalance, onTransferComplete: () { final user = ref.read(userNotifierProvider); final accountSeq = user.accountSequence ?? ''; _doRefresh(accountSeq); }, ), ); } } /// 划转底部弹窗 class _TransferBottomSheet extends ConsumerStatefulWidget { final String miningBalance; final String tradingBalance; final VoidCallback onTransferComplete; const _TransferBottomSheet({ required this.miningBalance, required this.tradingBalance, required this.onTransferComplete, }); @override ConsumerState<_TransferBottomSheet> createState() => _TransferBottomSheetState(); } class _TransferBottomSheetState extends ConsumerState<_TransferBottomSheet> { static const Color _orange = AppColors.orange; static const Color _green = AppColors.up; // 0: 从挖矿划入交易, 1: 从交易划出到挖矿 int _direction = 0; final _amountController = TextEditingController(); bool _isLoading = false; @override void dispose() { _amountController.dispose(); super.dispose(); } String get _availableBalance { return _direction == 0 ? widget.miningBalance : widget.tradingBalance; } @override Widget build(BuildContext context) { final cardBg = AppColors.cardOf(context); final darkText = AppColors.textPrimaryOf(context); final grayText = AppColors.textSecondaryOf(context); final bgGray = AppColors.backgroundOf(context); return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), decoration: BoxDecoration( color: cardBg, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: SafeArea( child: Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题栏 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '积分股划转', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: darkText, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Icon(Icons.close, color: grayText), ), ], ), const SizedBox(height: 20), // 划转方向切换 Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: bgGray, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( child: GestureDetector( onTap: () => setState(() { _direction = 0; _amountController.clear(); }), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: _direction == 0 ? AppColors.cardOf(context) : Colors.transparent, borderRadius: BorderRadius.circular(6), boxShadow: _direction == 0 ? [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4)] : null, ), child: Text( '划入交易账户', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, fontWeight: _direction == 0 ? FontWeight.bold : FontWeight.normal, color: _direction == 0 ? _orange : grayText, ), ), ), ), ), Expanded( child: GestureDetector( onTap: () => setState(() { _direction = 1; _amountController.clear(); }), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: _direction == 1 ? AppColors.cardOf(context) : Colors.transparent, borderRadius: BorderRadius.circular(6), boxShadow: _direction == 1 ? [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4)] : null, ), child: Text( '划出到分配账户', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, fontWeight: _direction == 1 ? FontWeight.bold : FontWeight.normal, color: _direction == 1 ? _orange : grayText, ), ), ), ), ), ], ), ), const SizedBox(height: 20), // 划转说明 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: bgGray, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _direction == 0 ? '分配账户' : '交易账户', style: TextStyle(fontSize: 12, color: grayText), ), const SizedBox(height: 4), Text( formatAmount(_direction == 0 ? widget.miningBalance : widget.tradingBalance), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: darkText, ), ), ], ), ), const Icon(Icons.arrow_forward, color: _orange), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( _direction == 0 ? '交易账户' : '分配账户', style: TextStyle(fontSize: 12, color: grayText), ), const SizedBox(height: 4), Text( formatAmount(_direction == 0 ? widget.tradingBalance : widget.miningBalance), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: darkText, ), ), ], ), ), ], ), ), const SizedBox(height: 20), // 数量输入 Text( '划转数量', style: TextStyle(fontSize: 14, color: darkText), ), const SizedBox(height: 8), Container( decoration: BoxDecoration( border: Border.all(color: bgGray), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( child: TextField( controller: _amountController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( hintText: '请输入划转数量', hintStyle: TextStyle(color: grayText, fontSize: 14), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), ), ), ), GestureDetector( onTap: () { _amountController.text = _availableBalance; }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: _orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: const Text( '全部', style: TextStyle( fontSize: 12, color: _orange, fontWeight: FontWeight.w500, ), ), ), ), ], ), ), const SizedBox(height: 8), Text( '可用: ${formatAmount(_availableBalance)} 积分股', style: TextStyle(fontSize: 12, color: grayText), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '提示: 最低划转数量为 0.01 积分股', style: TextStyle(fontSize: 12, color: grayText), ), GestureDetector( onTap: () { Navigator.pop(context); context.push(Routes.transferRecords); }, child: const Text( '划转记录', style: TextStyle( fontSize: 12, color: _orange, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: 24), // 提交按钮 SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: _isLoading ? null : _handleTransfer, style: ElevatedButton.styleFrom( backgroundColor: _orange, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text( _direction == 0 ? '划入交易账户' : '划出到分配账户', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ), ), ); } Future _handleTransfer() async { final amount = _amountController.text.trim(); if (amount.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请输入划转数量')), ); return; } final amountValue = double.tryParse(amount) ?? 0; if (amountValue < 0.01) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('最低划转数量为 0.01 积分股')), ); return; } final available = double.tryParse(_availableBalance) ?? 0; if (amountValue > available) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('划转数量超过可用余额')), ); return; } setState(() => _isLoading = true); try { bool success; if (_direction == 0) { // 从挖矿划入交易 success = await ref.read(tradingNotifierProvider.notifier).transferIn(amount); } else { // 从交易划出到挖矿 success = await ref.read(tradingNotifierProvider.notifier).transferOut(amount); } if (mounted) { if (success) { Navigator.pop(context); widget.onTransferComplete(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(_direction == 0 ? '划入成功' : '划出成功'), backgroundColor: _green, ), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('划转失败,请稍后重试'), backgroundColor: Colors.red, ), ); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('划转失败: $e'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() => _isLoading = false); } } } }