This commit is contained in:
sion
2026-04-06 16:34:02 +08:00
parent 71c8689989
commit 2e34072f45
20 changed files with 2278 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
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

@@ -0,0 +1,77 @@
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);
}
}