111
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_theme_extension.dart';
|
||||
|
||||
/// K线周期选择器:15m / 1h / 4h / 1d / 1M
|
||||
class IntervalSelector extends StatelessWidget {
|
||||
final String selected;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
static const List<MapEntry<String, String>> intervals = [
|
||||
MapEntry('15m', '15分'),
|
||||
MapEntry('1h', '1时'),
|
||||
MapEntry('4h', '4时'),
|
||||
MapEntry('1d', '日线'),
|
||||
MapEntry('1M', '月线'),
|
||||
];
|
||||
|
||||
const IntervalSelector({
|
||||
super.key,
|
||||
required this.selected,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: intervals.map((e) {
|
||||
final isSelected = e.key == selected;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onChanged(e.key),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.colors.primary.withValues(alpha: 0.15)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
e.value,
|
||||
style: AppTextStyles.bodyMedium(context).copyWith(
|
||||
color: isSelected
|
||||
? context.colors.primary
|
||||
: context.appColors.onSurfaceMuted,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_theme_extension.dart';
|
||||
import '../../../../data/models/kline_candle.dart';
|
||||
|
||||
/// K线 OHLC 信息栏
|
||||
class KlineStatsBar extends StatelessWidget {
|
||||
final KlineCandle? candle;
|
||||
|
||||
const KlineStatsBar({super.key, this.candle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (candle == null) return const SizedBox.shrink();
|
||||
|
||||
final c = candle!;
|
||||
final change = c.closePrice - c.openPrice;
|
||||
final changePct = c.openPrice > 0 ? (change / c.openPrice * 100) : 0.0;
|
||||
final isUp = change >= 0;
|
||||
final color = isUp ? context.appColors.up : context.appColors.down;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
_statItem(context, '开', _fmt(c.openPrice), color),
|
||||
const SizedBox(width: 12),
|
||||
_statItem(context, '高', _fmt(c.highPrice), color),
|
||||
const SizedBox(width: 12),
|
||||
_statItem(context, '低', _fmt(c.lowPrice), color),
|
||||
const SizedBox(width: 12),
|
||||
_statItem(context, '收', _fmt(c.closePrice), color),
|
||||
const SizedBox(width: 12),
|
||||
_statItem(context, '量', _fmtVol(c.volume), color),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${isUp ? '+' : ''}${changePct.toStringAsFixed(2)}%',
|
||||
style: AppTextStyles.labelLarge(context).copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statItem(BuildContext context, String label, String value, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(label,
|
||||
style: AppTextStyles.bodySmall(context).copyWith(
|
||||
color: context.appColors.onSurfaceMuted,
|
||||
)),
|
||||
const SizedBox(width: 2),
|
||||
Text(value,
|
||||
style: AppTextStyles.bodySmall(context).copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _fmt(double v) {
|
||||
if (v >= 1000) return v.toStringAsFixed(2);
|
||||
if (v >= 1) return v.toStringAsFixed(4);
|
||||
return v.toStringAsFixed(6);
|
||||
}
|
||||
|
||||
String _fmtVol(double v) {
|
||||
if (v >= 1000000) return '${(v / 1000000).toStringAsFixed(1)}M';
|
||||
if (v >= 1000) return '${(v / 1000).toStringAsFixed(1)}K';
|
||||
return v.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,11 @@ import '../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_theme_extension.dart';
|
||||
import '../../../../data/models/coin.dart';
|
||||
import '../../kline/kline_page.dart';
|
||||
import 'coin_avatar.dart';
|
||||
|
||||
/// 币种选择器组件
|
||||
///
|
||||
/// 显示当前选中的币种交易对,点击弹出底部弹窗选择币种。
|
||||
/// 卡片背景 + 圆角lg + border + padding:16
|
||||
/// 横向布局:coinInfo(竖向 pair+name) + chevronDown
|
||||
class CoinSelector extends StatelessWidget {
|
||||
final Coin? selectedCoin;
|
||||
final List<Coin> coins;
|
||||
@@ -61,22 +58,6 @@ class CoinSelector extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
// K线图标(仅选中币种后显示)
|
||||
if (selectedCoin != null)
|
||||
GestureDetector(
|
||||
onTap: () => _navigateToKline(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: context.appColors.surfaceCard,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(color: context.appColors.ghostBorder),
|
||||
),
|
||||
child: Icon(LucideIcons.chartNoAxesColumn,
|
||||
size: 20, color: context.colors.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
// 下拉箭头
|
||||
Icon(LucideIcons.chevronDown,
|
||||
size: 16, color: context.colors.onSurfaceVariant),
|
||||
@@ -86,14 +67,6 @@ class CoinSelector extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToKline(BuildContext context) {
|
||||
if (selectedCoin == null) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => KlinePage(coin: selectedCoin!)),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCoinPicker(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
||||
Reference in New Issue
Block a user