import 'package:flutter/material.dart'; import 'package:interactive_chart/interactive_chart.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_theme_extension.dart'; import '../../../providers/chart_provider.dart'; /// K 线图页面 - 标准金融App行情布局 class ChartPage extends StatelessWidget { final String? symbol; const ChartPage({super.key, this.symbol}); @override Widget build(BuildContext context) { final colorScheme = context.colors; return ChangeNotifierProvider( create: (_) => ChartProvider()..setSymbol(symbol ?? 'BTC'), child: Scaffold( backgroundColor: colorScheme.surface, appBar: AppBar( leading: IconButton( icon: const Icon(LucideIcons.arrowLeft, size: 20), onPressed: () => Navigator.of(context).pop(), ), title: _buildTitle(context), backgroundColor: colorScheme.surface, elevation: 0, scrolledUnderElevation: 0, actions: [ IconButton( icon: const Icon(LucideIcons.settings, size: 20), onPressed: () => _showSettingsSheet(context), ), ], ), body: Consumer( builder: (context, provider, _) { if (provider.loading) { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { return _buildErrorView(context, provider); } final candles = provider.candleData; if (candles.length < 3) { return const Center(child: Text('数据不足')); } return Column( children: [ // 1. 顶部价格信息栏 _buildPriceHeader(context, provider), // 2. 时间周期选择 _buildIntervalTabs(context, provider), // 3. 技术指标切换 _buildIndicatorTabs(context, provider), // 4. K线图区域 Expanded( child: InteractiveChart( candles: candles, style: ChartStyle( priceGainColor: context.appColors.up, priceLossColor: context.appColors.down, volumeColor: colorScheme.primary.withValues(alpha: 0.3), priceLabelStyle: TextStyle( fontSize: 11, color: colorScheme.onSurfaceVariant, ), timeLabelStyle: TextStyle( fontSize: 10, color: colorScheme.onSurfaceVariant, ), overlayTextStyle: TextStyle( fontSize: 12, color: colorScheme.onSurface, ), volumeHeightFactor: provider.indicators.showVOL ? 0.2 : 0, // MA 均线样式 trendLineStyles: provider.indicators.showMA ? [ Paint() ..color = Colors.blue ..strokeWidth = 1.5, Paint() ..color = Colors.orange ..strokeWidth = 1.5, Paint() ..color = Colors.purple ..strokeWidth = 1.5, ] : [], ), ), ), // 5. 底部操作栏 _buildBottomActions(context, provider), ], ); }, ), ), ); } /// 顶部价格信息栏 Widget _buildPriceHeader(BuildContext context, ChartProvider provider) { final colorScheme = context.colors; final candles = provider.candleData; if (candles.isEmpty) return const SizedBox.shrink(); final lastCandle = candles.last; final firstCandle = candles.first; final closePrice = lastCandle.close ?? 0; final openPrice = firstCandle.open ?? 0; final change = closePrice - openPrice; final changePercent = openPrice != 0 ? (change / openPrice) * 100 : 0; final isUp = change >= 0; // 计算24h最高最低 double high24h = 0; double low24h = double.infinity; double volume24h = 0; for (var c in candles) { if (c.high != null && c.high! > high24h) high24h = c.high!; if (c.low != null && c.low! < low24h) low24h = c.low!; if (c.volume != null) volume24h += c.volume!; } return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: colorScheme.surfaceContainer, border: Border( bottom: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.2), ), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 当前价格 Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '\$${lastCandle.close?.toStringAsFixed(2) ?? "0.00"}', style: AppTextStyles.displaySmall(context).copyWith( fontWeight: FontWeight.bold, color: isUp ? context.appColors.up : context.appColors.down, ), ), const SizedBox(width: AppSpacing.md), Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: 2, ), decoration: BoxDecoration( color: (isUp ? context.appColors.up : context.appColors.down) .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Text( '${isUp ? "+" : ""}${changePercent.toStringAsFixed(2)}%', style: AppTextStyles.labelMedium(context).copyWith( color: isUp ? context.appColors.up : context.appColors.down, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: AppSpacing.sm), // 24h 数据 Row( children: [ _buildDataItem(context, '24h高', '\$${high24h.toStringAsFixed(2)}'), const SizedBox(width: AppSpacing.lg), _buildDataItem(context, '24h低', '\$${low24h.toStringAsFixed(2)}'), const SizedBox(width: AppSpacing.lg), _buildDataItem(context, '24h量', '${(volume24h / 1000).toStringAsFixed(1)}K'), ], ), ], ), ); } Widget _buildDataItem(BuildContext context, String label, String value) { final colorScheme = context.colors; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: AppTextStyles.bodySmall(context).copyWith( color: colorScheme.onSurfaceVariant, ), ), Text( value, style: AppTextStyles.labelMedium(context).copyWith( fontWeight: FontWeight.w600, ), ), ], ); } /// 时间周期选择 Widget _buildIntervalTabs(BuildContext context, ChartProvider provider) { final colorScheme = context.colors; return Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.2), ), ), ), child: Row( children: ChartInterval.values.map((interval) { final isSelected = provider.interval == interval; return Expanded( child: InkWell( onTap: () => provider.setInterval(interval), child: Container( alignment: Alignment.center, decoration: BoxDecoration( border: Border( bottom: BorderSide( color: isSelected ? colorScheme.primary : Colors.transparent, width: 2, ), ), ), child: Text( interval.label, style: AppTextStyles.labelMedium(context).copyWith( color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), ), ), ), ); }).toList(), ), ); } /// 技术指标切换 Widget _buildIndicatorTabs(BuildContext context, ChartProvider provider) { final colorScheme = context.colors; return Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), decoration: BoxDecoration( color: colorScheme.surfaceContainer, border: Border( bottom: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.2), ), ), ), child: Row( children: [ _buildIndicatorChip( context, label: 'MA', isSelected: provider.indicators.showMA, onTap: () => provider.updateIndicators( provider.indicators.copyWith(showMA: !provider.indicators.showMA), ), ), _buildIndicatorChip( context, label: 'EMA', isSelected: provider.indicators.showEMA, onTap: () => provider.updateIndicators( provider.indicators.copyWith(showEMA: !provider.indicators.showEMA), ), ), _buildIndicatorChip( context, label: 'BOLL', isSelected: provider.indicators.showBOLL, onTap: () => provider.updateIndicators( provider.indicators.copyWith(showBOLL: !provider.indicators.showBOLL), ), ), _buildIndicatorChip( context, label: 'VOL', isSelected: provider.indicators.showVOL, onTap: () => provider.updateIndicators( provider.indicators.copyWith(showVOL: !provider.indicators.showVOL), ), ), _buildIndicatorChip( context, label: 'MACD', isSelected: provider.indicators.showMACD, onTap: () => provider.updateIndicators( provider.indicators.copyWith(showMACD: !provider.indicators.showMACD), ), ), ], ), ); } Widget _buildIndicatorChip( BuildContext context, { required String label, required bool isSelected, required VoidCallback onTap, }) { final colorScheme = context.colors; return Padding( padding: const EdgeInsets.only(right: AppSpacing.xs), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.sm), child: Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: isSelected ? colorScheme.primary.withValues(alpha: 0.1) : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.sm), border: Border.all( color: isSelected ? colorScheme.primary : colorScheme.outlineVariant.withValues(alpha: 0.3), width: 1, ), ), child: Text( label, style: AppTextStyles.labelSmall(context).copyWith( color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), ), ), ), ); } /// 底部操作栏 Widget _buildBottomActions(BuildContext context, ChartProvider provider) { final colorScheme = context.colors; return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: colorScheme.surface, border: Border( top: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.3), ), ), ), child: Row( children: [ // 买入按钮 Expanded( child: SizedBox( height: 48, child: ElevatedButton( onPressed: () => _navigateToTrade(context, provider.symbol, 'buy'), style: ElevatedButton.styleFrom( backgroundColor: context.appColors.up, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), ), child: Text( '买入', style: AppTextStyles.headlineSmall(context).copyWith( fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ), const SizedBox(width: AppSpacing.md), // 卖出按钮 Expanded( child: SizedBox( height: 48, child: ElevatedButton( onPressed: () => _navigateToTrade(context, provider.symbol, 'sell'), style: ElevatedButton.styleFrom( backgroundColor: context.appColors.down, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), ), child: Text( '卖出', style: AppTextStyles.headlineSmall(context).copyWith( fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ), ], ), ); } void _navigateToTrade(BuildContext context, String symbol, String side) { // TODO: 跳转到交易页面 // Navigator.push( // context, // MaterialPageRoute( // builder: (_) => TradePage(symbol: symbol, side: side), // ), // ); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$symbol $side - 跳转交易页面(待实现)')), ); } Widget _buildTitle(BuildContext context) { return Consumer( builder: (context, provider, _) { final colorScheme = context.colors; return Row( mainAxisSize: MainAxisSize.min, children: [ Text( provider.symbol, style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(width: AppSpacing.xs), Text( '/USDT', style: AppTextStyles.bodyMedium(context).copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ); }, ); } Widget _buildErrorView(BuildContext context, ChartProvider provider) { final colorScheme = context.colors; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: colorScheme.error), const SizedBox(height: AppSpacing.md), Text('加载失败', style: AppTextStyles.bodyLarge(context)), const SizedBox(height: AppSpacing.sm), TextButton( onPressed: provider.loadCandles, child: const Text('重试'), ), ], ), ); } void _showSettingsSheet(BuildContext context) { final provider = context.read(); showModalBottomSheet( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setState) { return Container( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '技术指标', style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: AppSpacing.lg), _buildIndicatorSwitch( context: context, label: 'MA 移动平均线 (7/14/30)', value: provider.indicators.showMA, onChanged: (v) { provider.updateIndicators( provider.indicators.copyWith(showMA: v), ); setState(() {}); }, ), _buildIndicatorSwitch( context: context, label: 'EMA 指数移动平均', value: provider.indicators.showEMA, onChanged: (v) { provider.updateIndicators( provider.indicators.copyWith(showEMA: v), ); setState(() {}); }, ), _buildIndicatorSwitch( context: context, label: 'BOLL 布林带', value: provider.indicators.showBOLL, onChanged: (v) { provider.updateIndicators( provider.indicators.copyWith(showBOLL: v), ); setState(() {}); }, ), _buildIndicatorSwitch( context: context, label: 'VOL 成交量', value: provider.indicators.showVOL, onChanged: (v) { provider.updateIndicators( provider.indicators.copyWith(showVOL: v), ); setState(() {}); }, ), _buildIndicatorSwitch( context: context, label: 'MACD', value: provider.indicators.showMACD, onChanged: (v) { provider.updateIndicators( provider.indicators.copyWith(showMACD: v), ); setState(() {}); }, ), const SizedBox(height: AppSpacing.xl), ], ), ); }, ), ); } Widget _buildIndicatorSwitch({ required BuildContext context, required String label, required bool value, required ValueChanged onChanged, }) { return Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: AppTextStyles.bodyMedium(context)), Switch(value: value, onChanged: onChanged), ], ), ); } }