import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../../core/constants/app_colors.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 '../../providers/user_providers.dart'; import '../../providers/trading_providers.dart'; import '../../widgets/shimmer_loading.dart'; class TradingPage extends ConsumerStatefulWidget { const TradingPage({super.key}); @override ConsumerState createState() => _TradingPageState(); } class _TradingPageState extends ConsumerState { // 设计色彩 static const Color _orange = Color(0xFFFF6B00); static const Color _green = Color(0xFF10B981); static const Color _red = Color(0xFFEF4444); static const Color _grayText = Color(0xFF6B7280); static const Color _darkText = Color(0xFF1F2937); static const Color _bgGray = Color(0xFFF3F4F6); static const Color _lightGray = Color(0xFFF9FAFB); static const Color _borderGray = Color(0xFFE5E7EB); // 状态 int _selectedTab = 1; // 0: 买入, 1: 卖出 int _selectedTimeRange = 1; // 时间周期选择 final _quantityController = TextEditingController(); final _priceController = TextEditingController(); final List _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日']; @override void dispose() { _quantityController.dispose(); _priceController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final priceAsync = ref.watch(currentPriceProvider); final marketAsync = ref.watch(marketOverviewProvider); final ordersAsync = ref.watch(ordersProvider); final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; return Scaffold( backgroundColor: const Color(0xFFF5F5F5), body: SafeArea( bottom: false, child: RefreshIndicator( onRefresh: () async { ref.invalidate(currentPriceProvider); ref.invalidate(marketOverviewProvider); ref.invalidate(ordersProvider); }, child: Column( children: [ _buildAppBar(), Expanded( child: SingleChildScrollView( child: Column( children: [ _buildPriceCard(priceAsync), _buildChartSection(priceAsync), _buildMarketDataCard(marketAsync), _buildTradingPanel(priceAsync), _buildMyOrdersCard(ordersAsync), const SizedBox(height: 100), ], ), ), ), ], ), ), ), ); } Widget _buildAppBar() { return Container( color: _bgGray.withValues(alpha: 0.9), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: _orange, borderRadius: BorderRadius.circular(9999), ), child: const Text( '积分股交易', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Stack( children: [ const Center( child: Icon(Icons.notifications_outlined, color: _grayText), ), Positioned( right: 10, top: 10, child: Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), ), ], ), ), ], ), ); } 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'; final greenPoints = priceInfo?.greenPoints ?? '0'; return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '当前积分股价格', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _grayText, ), ), DataText( data: priceInfo != null ? '= ${formatCompact(greenPoints)} 绿积分' : null, isLoading: isLoading, placeholder: '= -- 绿积分', style: const TextStyle(fontSize: 12, color: _grayText), ), ], ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ AmountText( amount: priceInfo != null ? formatPrice(price) : null, isLoading: isLoading, prefix: '\u00A5 ', style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, color: _orange, letterSpacing: -0.75, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _green.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.trending_up, size: 16, color: _green), DataText( data: isLoading ? null : '+0.00%', isLoading: isLoading, placeholder: '+--.--%', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _green, ), ), ], ), ), ], ), ], ), ); } Widget _buildChartSection(AsyncValue priceAsync) { final priceInfo = priceAsync.valueOrNull; final currentPrice = priceInfo?.price ?? '0.000000'; return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Container( height: 200, decoration: BoxDecoration( color: _lightGray, borderRadius: BorderRadius.circular(8), ), child: Stack( children: [ CustomPaint( size: const Size(double.infinity, 200), painter: _CandlestickPainter(), ), Positioned( right: 0, top: 60, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: _orange, borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Text( formatPrice(currentPrice), style: const TextStyle(fontSize: 10, color: Colors.white), ), ), ), ], ), ), const SizedBox(height: 16), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: List.generate(_timeRanges.length, (index) { final isSelected = _selectedTimeRange == index; return Padding( padding: const EdgeInsets.only(right: 8), child: GestureDetector( onTap: () => setState(() => _selectedTimeRange = index), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: isSelected ? _orange : Colors.white, borderRadius: BorderRadius.circular(9999), border: isSelected ? null : Border.all(color: _borderGray), ), child: Text( _timeRanges[index], style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: isSelected ? Colors.white : _grayText, ), ), ), ), ); }), ), ), ], ), ); } Widget _buildMarketDataCard(AsyncValue marketAsync) { final isLoading = marketAsync.isLoading; final market = marketAsync.valueOrNull; final hasError = marketAsync.hasError; if (hasError && market == null) { return _buildErrorCard('市场数据加载失败'); } return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, 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), const 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: const TextStyle(fontSize: 12, color: _grayText)), 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'; // 设置默认价格 if (_priceController.text.isEmpty && priceInfo != null) { _priceController.text = currentPrice; } return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Container( decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: _bgGray)), ), child: Row( children: [ Expanded( child: GestureDetector( onTap: () => setState(() => _selectedTab = 0), child: Container( padding: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: _selectedTab == 0 ? _orange : Colors.transparent, width: 2, ), ), ), child: Text( '买入', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _selectedTab == 0 ? _orange : _grayText, ), ), ), ), ), 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), // 价格输入 _buildInputField('价格', _priceController, '请输入价格', '绿积分'), const SizedBox(height: 16), // 数量输入 _buildInputField('数量', _quantityController, '请输入数量', '积分股'), 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: const 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: const [ Text( '销毁比例', style: TextStyle(fontSize: 12, color: _grayText), ), Text( '10% 进入黑洞', style: TextStyle( fontSize: 12, color: _red, 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: Text( _selectedTab == 0 ? '买入积分股' : '卖出积分股', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ); } Widget _buildInputField( String label, TextEditingController controller, String hint, String suffix, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const 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: const TextStyle( fontSize: 14, color: Color(0xFF9CA3AF), ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 16), ), ), ), Text(suffix, style: const TextStyle(fontSize: 12, color: _grayText)), const SizedBox(width: 12), ], ), ), ], ); } String _calculateEstimate() { final price = double.tryParse(_priceController.text) ?? 0; final quantity = double.tryParse(_quantityController.text) ?? 0; final total = price * quantity; if (total == 0) { return '0.00 绿积分'; } if (_selectedTab == 1) { // 卖出时扣除10%销毁 final afterBurn = total * 0.9; return '${formatAmount(afterBurn.toString())} 绿积分'; } return '${formatAmount(total.toString())} 绿积分'; } Widget _buildMyOrdersCard(AsyncValue ordersAsync) { final isLoading = ordersAsync.isLoading; final ordersPage = ordersAsync.valueOrNull; final orders = ordersPage?.data ?? []; return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '我的挂单', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _darkText, ), ), GestureDetector( onTap: () { // TODO: 查看全部挂单 }, 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) const Padding( padding: 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); 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: _lightGray, 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: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _darkText, ), ), ], ), const SizedBox(height: 4), Text( formattedDate, style: const TextStyle(fontSize: 12, color: _grayText), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${formatCompact(order.quantity)} 股', style: const 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: Colors.white, 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; 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(); // 交易成功后刷新订单列表 ref.invalidate(ordersProvider); ref.invalidate(currentPriceProvider); ref.invalidate(marketOverviewProvider); } } } } // K线图绘制器(简化版本,显示模拟数据) class _CandlestickPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final greenPaint = Paint()..color = const Color(0xFF10B981); final redPaint = Paint()..color = const Color(0xFFEF4444); final dashPaint = Paint() ..color = const Color(0xFFFF6B00) ..strokeWidth = 1 ..style = PaintingStyle.stroke; // 模拟K线数据 final candleData = [ {'open': 0.6, 'close': 0.5, 'high': 0.7, 'low': 0.45}, {'open': 0.5, 'close': 0.55, 'high': 0.6, 'low': 0.48}, {'open': 0.55, 'close': 0.52, 'high': 0.58, 'low': 0.5}, {'open': 0.52, 'close': 0.6, 'high': 0.65, 'low': 0.5}, {'open': 0.6, 'close': 0.58, 'high': 0.65, 'low': 0.55}, {'open': 0.58, 'close': 0.62, 'high': 0.68, 'low': 0.55}, {'open': 0.62, 'close': 0.55, 'high': 0.65, 'low': 0.52}, {'open': 0.55, 'close': 0.58, 'high': 0.62, 'low': 0.52}, {'open': 0.58, 'close': 0.52, 'high': 0.6, 'low': 0.5}, {'open': 0.52, 'close': 0.65, 'high': 0.7, 'low': 0.5}, {'open': 0.65, 'close': 0.7, 'high': 0.75, 'low': 0.62}, {'open': 0.7, 'close': 0.75, 'high': 0.8, 'low': 0.68}, ]; final candleWidth = (size.width - 40) / candleData.length; const padding = 20.0; for (int i = 0; i < candleData.length; i++) { final data = candleData[i]; final open = data['open']!; final close = data['close']!; final high = data['high']!; final low = data['low']!; final isGreen = close >= open; final paint = isGreen ? greenPaint : redPaint; final x = padding + i * candleWidth + candleWidth / 2; final yOpen = size.height - (open * size.height * 0.8 + size.height * 0.1); final yClose = size.height - (close * size.height * 0.8 + size.height * 0.1); final yHigh = size.height - (high * size.height * 0.8 + size.height * 0.1); final yLow = size.height - (low * size.height * 0.8 + size.height * 0.1); canvas.drawLine( Offset(x, yHigh), Offset(x, yLow), paint..strokeWidth = 1, ); final bodyTop = isGreen ? yClose : yOpen; final bodyBottom = isGreen ? yOpen : yClose; canvas.drawRect( Rect.fromLTRB(x - candleWidth * 0.3, bodyTop, x + candleWidth * 0.3, bodyBottom), paint..style = PaintingStyle.fill, ); } final dashY = size.height * 0.35; const dashWidth = 5.0; const dashSpace = 3.0; double startX = 0; while (startX < size.width - 60) { canvas.drawLine( Offset(startX, dashY), Offset(startX + dashWidth, dashY), dashPaint, ); startX += dashWidth + dashSpace; } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }