281 lines
8.5 KiB
Dart
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);
|
|
}
|
|
}
|