import 'package:flutter/material.dart'; import 'package:flutter_chen_kchart/k_chart.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:decimal/decimal.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 线图页面 - 使用 k_chart 库 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') ..loadCandles(), 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, ), body: Consumer( builder: (context, provider, _) { if (provider.loading) { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { return _buildErrorView(context, provider); } final data = provider.klineData; if (data.isEmpty) { return const Center(child: Text('暂无数据')); } return Column( children: [ // 1. 顶部价格信息栏 _buildPriceHeader(context, provider), // 2. 时间周期选择 _buildIntervalTabs(context, provider), // 3. K线图区域 Expanded( child: Container( color: colorScheme.surface, child: Builder( builder: (context) { final chartColors = ChartColors(); chartColors.upColor = context.appColors.up; chartColors.dnColor = context.appColors.down; chartColors.bgColor = [colorScheme.surface, colorScheme.surface]; chartColors.gridColor = colorScheme.outlineVariant.withValues(alpha: 0.2); chartColors.ma5Color = Colors.blue; chartColors.ma10Color = Colors.orange; chartColors.ma30Color = Colors.purple; chartColors.volColor = colorScheme.primary.withValues(alpha: 0.5); return KChartWidget( provider.klineData, chartStyle: ChartStyle(), chartColors: chartColors, enableTheme: true, controller: KChartController(), mainState: _getMainState(provider), secondaryState: _getSecondaryState(provider), volHidden: !provider.indicators.showVOL, fixedLength: 2, timeFormat: TimeFormat.YEAR_MONTH_DAY, onLoadMore: (m) async { return false; }, isTrendLine: false, minScale: 0.1, maxScale: 5.0, scaleSensitivity: 2.5, ); }, ), ), ), // 4. 底部指标切换 _buildIndicatorTabs(context, provider), // 5. 底部操作栏 _buildBottomActions(context, provider), ], ); }, ), ), ); } MainState _getMainState(ChartProvider provider) { if (provider.indicators.showBOLL) { return MainState.BOLL; } else if (provider.indicators.showEMA) { return MainState.MA; } else if (provider.indicators.showMA) { return MainState.MA; } return MainState.MA; } SecondaryState _getSecondaryState(ChartProvider provider) { if (provider.indicators.showMACD) { return SecondaryState.MACD; } else if (provider.indicators.showKDJ) { return SecondaryState.KDJ; } else if (provider.indicators.showRSI) { return SecondaryState.RSI; } else if (provider.indicators.showWR) { return SecondaryState.WR; } else if (provider.indicators.showCCI) { return SecondaryState.CCI; } return SecondaryState.MACD; } Widget _buildPriceHeader(BuildContext context, ChartProvider provider) { final colorScheme = context.colors; final candles = provider.candles; if (candles.isEmpty) return const SizedBox.shrink(); final lastCandle = candles.last; final firstCandle = candles.first; final change = lastCandle.close - firstCandle.open; final changePercent = firstCandle.open == Decimal.zero ? 0.0 : (change.toDouble() / firstCandle.openDouble * 100); final isUp = change >= Decimal.fromInt(0); Decimal high24h = Decimal.fromInt(0); Decimal low24h = Decimal.parse('999999999999'); Decimal volume24h = Decimal.fromInt(0); for (var c in candles) { if (c.high > high24h) high24h = c.high; if (c.low < low24h) low24h = c.low; 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.closeDouble.toStringAsFixed(2)}', 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), Row( children: [ _buildDataItem(context, '24h高', '\$${high24h.toDouble().toStringAsFixed(2)}'), const SizedBox(width: AppSpacing.lg), _buildDataItem(context, '24h低', '\$${low24h.toDouble().toStringAsFixed(2)}'), const SizedBox(width: AppSpacing.lg), _buildDataItem(context, '24h量', '${(volume24h.toDouble() / 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(), ), ); } /// 底部指标切换(官方 demo 风格) Widget _buildIndicatorTabs(BuildContext context, ChartProvider provider) { final colorScheme = context.colors; final selectedColor = colorScheme.primary; final unselectedColor = colorScheme.onSurfaceVariant; return Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), decoration: BoxDecoration( border: Border(top: BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.2))), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ // 主图指标 TextButton( onPressed: () => provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA ? false : true, showEMA: false, showBOLL: false, showVOL: provider.indicators.showVOL, showMACD: provider.indicators.showMACD, showKDJ: provider.indicators.showKDJ, showRSI: provider.indicators.showRSI, )), style: TextButton.styleFrom(minimumSize: const Size(0, 0), padding: const EdgeInsets.symmetric(horizontal: 12)), child: Text('MA', style: TextStyle(color: provider.indicators.showMA ? selectedColor : unselectedColor, fontSize: 13)), ), TextButton( onPressed: () => provider.updateIndicators(IndicatorSettings( showMA: false, showEMA: false, showBOLL: provider.indicators.showBOLL ? false : true, showVOL: provider.indicators.showVOL, showMACD: provider.indicators.showMACD, showKDJ: provider.indicators.showKDJ, showRSI: provider.indicators.showRSI, )), style: TextButton.styleFrom(minimumSize: const Size(0, 0), padding: const EdgeInsets.symmetric(horizontal: 12)), child: Text('BOLL', style: TextStyle(color: provider.indicators.showBOLL ? selectedColor : unselectedColor, fontSize: 13)), ), // 副图指标 TextButton( onPressed: () => provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: provider.indicators.showVOL, showMACD: provider.indicators.showMACD ? false : true, showKDJ: false, showRSI: false, )), style: TextButton.styleFrom(minimumSize: const Size(0, 0), padding: const EdgeInsets.symmetric(horizontal: 12)), child: Text('MACD', style: TextStyle(color: provider.indicators.showMACD ? selectedColor : unselectedColor, fontSize: 13)), ), TextButton( onPressed: () => provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: provider.indicators.showVOL, showMACD: false, showKDJ: provider.indicators.showKDJ ? false : true, showRSI: false, )), style: TextButton.styleFrom(minimumSize: const Size(0, 0), padding: const EdgeInsets.symmetric(horizontal: 12)), child: Text('KDJ', style: TextStyle(color: provider.indicators.showKDJ ? selectedColor : unselectedColor, fontSize: 13)), ), TextButton( onPressed: () => provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: provider.indicators.showVOL, showMACD: false, showKDJ: false, showRSI: provider.indicators.showRSI ? false : true, )), style: TextButton.styleFrom(minimumSize: const Size(0, 0), padding: const EdgeInsets.symmetric(horizontal: 12)), child: Text('RSI', style: TextStyle(color: provider.indicators.showRSI ? selectedColor : unselectedColor, fontSize: 13)), ), TextButton( onPressed: () => provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: !provider.indicators.showVOL, showMACD: provider.indicators.showMACD, showKDJ: provider.indicators.showKDJ, showRSI: provider.indicators.showRSI, )), style: TextButton.styleFrom(minimumSize: const Size(0, 0), padding: const EdgeInsets.symmetric(horizontal: 12)), child: Text('VOL', style: TextStyle(color: provider.indicators.showVOL ? selectedColor : unselectedColor, fontSize: 13)), ), ], ), ), ); } 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) { 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('重试')), ], ), ); } }