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. 技术指标切换 _buildIndicatorTabs(context, provider), // 4. 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, ); }, ), ), ), // 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 != 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: 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; final mainIndicators = ['MA', 'EMA', 'BOLL']; final secondaryIndicators = ['MACD', 'KDJ', 'RSI', 'WR', 'CCI', 'VOL']; 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: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ ...mainIndicators.map((label) => _buildIndicatorChip( context, label: label, isSelected: _isMainIndicatorActive(provider, label), onTap: () => _toggleMainIndicator(provider, label), )), Container(width: 1, height: 20, margin: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), color: colorScheme.outlineVariant), ...secondaryIndicators.map((label) => _buildIndicatorChip( context, label: label, isSelected: _isSecondaryIndicatorActive(provider, label), onTap: () => _toggleSecondaryIndicator(provider, label), )), ], ), ), ); } bool _isMainIndicatorActive(ChartProvider provider, String label) { switch (label) { case 'MA': return provider.indicators.showMA; case 'EMA': return provider.indicators.showEMA; case 'BOLL': return provider.indicators.showBOLL; default: return false; } } bool _isSecondaryIndicatorActive(ChartProvider provider, String label) { switch (label) { case 'MACD': return provider.indicators.showMACD; case 'KDJ': return provider.indicators.showKDJ; case 'RSI': return provider.indicators.showRSI; case 'WR': return provider.indicators.showWR; case 'CCI': return provider.indicators.showCCI; case 'VOL': return provider.indicators.showVOL; default: return false; } } void _toggleMainIndicator(ChartProvider provider, String label) { provider.updateIndicators(IndicatorSettings( showMA: label == 'MA', showEMA: label == 'EMA', showBOLL: label == 'BOLL', showVOL: provider.indicators.showVOL, showMACD: provider.indicators.showMACD, showKDJ: provider.indicators.showKDJ, showRSI: provider.indicators.showRSI, )); } void _toggleSecondaryIndicator(ChartProvider provider, String label) { provider.updateIndicators(IndicatorSettings( showMA: provider.indicators.showMA, showEMA: provider.indicators.showEMA, showBOLL: provider.indicators.showBOLL, showVOL: label == 'VOL', showMACD: label == 'MACD', showKDJ: label == 'KDJ', showRSI: label == 'RSI', showWR: label == 'WR', showCCI: label == 'CCI', )); } 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) { 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('重试')), ], ), ); } }