Files
monisuo/flutter_monisuo/lib/ui/pages/kline/kline_page.dart
2026-04-06 16:34:02 +08:00

281 lines
8.5 KiB
Dart

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<KlinePage> createState() => _KlinePageState();
}
class _KlinePageState extends State<KlinePage> {
List<KLineEntity>? _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<KlineProvider>(
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<MainPageState>();
mainState?.switchToTrade(coin.code);
}
}