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 '../../../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. 底部操作栏 _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 != 0 ? (change / firstCandle.open) * 100 : 0; final isUp = change >= 0; double high24h = 0; double low24h = double.infinity; double volume24h = 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.close.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.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: [ // 时间周期选择 Expanded( 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(), ), ), // 设置按钮 IconButton( icon: const Icon(LucideIcons.settings, size: 18), onPressed: () => _showIndicatorSettings(context, provider), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 36, minHeight: 36), ), ], ), ); } /// 指标设置弹窗 void _showIndicatorSettings(BuildContext context, ChartProvider provider) { 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), // 主图指标 Text( '主图指标', style: AppTextStyles.labelMedium(context).copyWith( color: context.colors.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, children: [ _buildIndicatorRadio(context, 'MA', provider.indicators.showMA, () { provider.updateIndicators(IndicatorSettings( showMA: true, showEMA: false, showBOLL: false, showVOL: provider.indicators.showVOL, showMACD: provider.indicators.showMACD, showKDJ: provider.indicators.showKDJ, showRSI: provider.indicators.showRSI, )); setState(() {}); }), _buildIndicatorRadio(context, 'EMA', provider.indicators.showEMA, () { provider.updateIndicators(IndicatorSettings( showMA: false, showEMA: true, showBOLL: false, showVOL: provider.indicators.showVOL, showMACD: provider.indicators.showMACD, showKDJ: provider.indicators.showKDJ, showRSI: provider.indicators.showRSI, )); setState(() {}); }), _buildIndicatorRadio(context, 'BOLL', provider.indicators.showBOLL, () { provider.updateIndicators(IndicatorSettings( showMA: false, showEMA: false, showBOLL: true, showVOL: provider.indicators.showVOL, showMACD: provider.indicators.showMACD, showKDJ: provider.indicators.showKDJ, showRSI: provider.indicators.showRSI, )); setState(() {}); }), ], ), const SizedBox(height: AppSpacing.lg), // 副图指标 Text( '副图指标', style: AppTextStyles.labelMedium(context).copyWith( color: context.colors.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, children: [ _buildIndicatorRadio(context, 'MACD', provider.indicators.showMACD, () { provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: false, showMACD: true, showKDJ: false, showRSI: false, )); setState(() {}); }), _buildIndicatorRadio(context, 'KDJ', provider.indicators.showKDJ, () { provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: false, showMACD: false, showKDJ: true, showRSI: false, )); setState(() {}); }), _buildIndicatorRadio(context, 'RSI', provider.indicators.showRSI, () { provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: false, showMACD: false, showKDJ: false, showRSI: true, )); setState(() {}); }), _buildIndicatorRadio(context, 'VOL', provider.indicators.showVOL, () { provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: true, showMACD: false, showKDJ: false, showRSI: false, )); setState(() {}); }), ], ), const SizedBox(height: AppSpacing.xl), ], ), ); }, ), ); } Widget _buildIndicatorRadio(BuildContext context, String label, bool isSelected, VoidCallback onTap) { final colorScheme = context.colors; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.sm), child: Container( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm), 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.labelMedium(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) { 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('重试')), ], ), ); } }