111
This commit is contained in:
280
flutter_monisuo/lib/ui/pages/kline/kline_page.dart
Normal file
280
flutter_monisuo/lib/ui/pages/kline/kline_page.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user