import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:k_chart/flutter_k_chart.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_theme_extension.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../data/models/coin.dart'; import '../../../providers/kline_provider.dart'; import '../main/main_page.dart'; import 'components/interval_selector.dart'; import 'components/kline_stats_bar.dart'; /// K线图表页面 class KlinePage extends StatefulWidget { final Coin coin; const KlinePage({super.key, required this.coin}); @override State createState() => _KlinePageState(); } class _KlinePageState extends State { List? _kLineEntities; final ChartColors _chartColors = ChartColors(); final ChartStyle _chartStyle = ChartStyle(); @override void initState() { super.initState(); } @override Widget build(BuildContext context) { final isDark = context.isDark; _chartColors.bgColor = [isDark ? const Color(0xff1a1a2e) : Colors.white, isDark ? const Color(0xff1a1a2e) : Colors.white]; _chartColors.gridColor = isDark ? const Color(0xff2d2d44) : const Color(0xffe0e0e0); _chartColors.upColor = context.appColors.up; _chartColors.dnColor = context.appColors.down; return Scaffold( backgroundColor: context.colors.background, appBar: AppBar( backgroundColor: context.colors.surface, elevation: 0, scrolledUnderElevation: 0, leading: IconButton( icon: Icon(LucideIcons.arrowLeft, color: context.colors.onSurface), onPressed: () => Navigator.of(context).pop(), ), title: Row( children: [ Text(widget.coin.code, style: AppTextStyles.headlineLarge(context).copyWith( fontWeight: FontWeight.bold, )), const SizedBox(width: 8), Text(widget.coin.formattedPrice, style: AppTextStyles.headlineMedium(context).copyWith( color: context.colors.primary, )), const SizedBox(width: 6), _ChangeBadge(coin: widget.coin), ], ), ), body: Consumer( builder: (context, provider, _) { // 数据转换 _updateEntities(provider); return Column( children: [ // 周期选择器 Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ), child: IntervalSelector( selected: provider.interval, onChanged: (v) => provider.changeInterval(v), ), ), // OHLC 信息栏 KlineStatsBar(candle: provider.currentCandle), const Divider(height: 1), // K线图表 Expanded( child: _buildChart(provider), ), // 底部操作栏 _BottomActionBar(coin: widget.coin), ], ); }, ), ); } void _updateEntities(KlineProvider provider) { final allCandles = [...provider.candles]; if (provider.currentCandle != null) { allCandles.add(provider.currentCandle!); } if (allCandles.isEmpty) { _kLineEntities = null; return; } _kLineEntities = allCandles.map((c) { return KLineEntity.fromJson({ 'open': c.openPrice, 'high': c.highPrice, 'low': c.lowPrice, 'close': c.closePrice, 'vol': c.volume, 'amount': c.closePrice * c.volume, 'time': c.openTime, 'id': c.openTime, }); }).toList(); DataUtil.calculate(_kLineEntities!); } Widget _buildChart(KlineProvider provider) { if (provider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (_kLineEntities == null || _kLineEntities!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.chartNoAxesColumn, size: 48, color: context.appColors.onSurfaceMuted.withValues(alpha: 0.4)), const SizedBox(height: AppSpacing.md), Text('暂无K线数据', style: AppTextStyles.headlineMedium(context).copyWith( color: context.appColors.onSurfaceMuted, )), const SizedBox(height: AppSpacing.sm), ], ), ); } return Stack( children: [ SizedBox( height: double.infinity, width: double.infinity, child: KChartWidget( _kLineEntities, _chartStyle, _chartColors, isLine: false, isTrendLine: false, mainState: MainState.MA, volHidden: false, secondaryState: SecondaryState.MACD, fixedLength: 4, timeFormat: TimeFormat.YEAR_MONTH_DAY, showNowPrice: true, hideGrid: false, isTapShowInfoDialog: false, onSecondaryTap: () {}, ), ), ], ); } } /// 涨跌标签 class _ChangeBadge extends StatelessWidget { final Coin coin; const _ChangeBadge({required this.coin}); @override Widget build(BuildContext context) { final isUp = coin.isUp; final color = isUp ? context.appColors.up : context.appColors.down; return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( coin.formattedChange, style: AppTextStyles.bodySmall(context).copyWith( color: color, fontWeight: FontWeight.w600, ), ), ); } } /// 底部交易操作栏 class _BottomActionBar extends StatelessWidget { final Coin coin; const _BottomActionBar({required this.coin}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB( AppSpacing.lg, AppSpacing.sm, AppSpacing.lg, AppSpacing.lg, ), decoration: BoxDecoration( color: context.colors.surface, border: Border( top: BorderSide( color: context.colors.outlineVariant.withValues(alpha: 0.2), ), ), ), child: Row( children: [ Expanded( child: SizedBox( height: 44, child: ElevatedButton( onPressed: () => _navigateToTrade(context, isBuy: true), style: ElevatedButton.styleFrom( backgroundColor: context.appColors.up, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.lg), ), ), child: Text('买入', style: AppTextStyles.headlineMedium(context).copyWith( color: Colors.white, fontWeight: FontWeight.bold, )), ), ), ), const SizedBox(width: AppSpacing.md), Expanded( child: SizedBox( height: 44, child: ElevatedButton( onPressed: () => _navigateToTrade(context, isBuy: false), style: ElevatedButton.styleFrom( backgroundColor: context.appColors.down, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.lg), ), ), child: Text('卖出', style: AppTextStyles.headlineMedium(context).copyWith( color: Colors.white, fontWeight: FontWeight.bold, )), ), ), ), ], ), ); } void _navigateToTrade(BuildContext context, {required bool isBuy}) { Navigator.of(context).pop(); final mainState = context.findAncestorStateOfType(); mainState?.switchToTrade(coin.code); } }