This commit is contained in:
sion
2026-04-06 18:45:04 +08:00
parent 2e34072f45
commit ee8979f471
37 changed files with 5 additions and 2500 deletions

View File

@@ -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(),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,