This commit is contained in:
sion
2026-04-21 08:12:17 +08:00
parent 5264043c21
commit 685202dd55
247 changed files with 18661 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_theme_extension.dart';
import '../../../../data/models/order_book.dart';
/// 订单簿组件
///
/// 显示3档卖单(红) + 价差 + 3档买单(绿),带深度条。
class OrderBook extends StatelessWidget {
final OrderBookDepth depth;
const OrderBook({super.key, required this.depth});
@override
Widget build(BuildContext context) {
final asks = depth.asks.length > 3 ? depth.asks.sublist(0, 3) : depth.asks;
final bids = depth.bids.length > 3 ? depth.bids.sublist(0, 3) : depth.bids;
// 计算最大数量(用于深度条百分比)
double maxQty = 0;
for (var a in asks) {
if (a.quantity > maxQty) maxQty = a.quantity;
}
for (var b in bids) {
if (b.quantity > maxQty) maxQty = b.quantity;
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
decoration: BoxDecoration(
color: context.appColors.surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: context.appColors.ghostBorder),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 表头
_buildHeader(context),
const SizedBox(height: AppSpacing.sm),
// 卖单(从低到高,最接近当前价在底部)
...asks.reversed.map((a) => _OrderBookRow(
level: a,
isAsk: true,
maxQty: maxQty,
)),
// 价差指示器
_buildSpreadIndicator(context),
// 买单(从高到低,最接近当前价在顶部)
...bids.map((b) => _OrderBookRow(
level: b,
isAsk: false,
maxQty: maxQty,
)),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
children: [
Expanded(
child: Text('价格',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w600,
)),
),
Expanded(
child: Text('数量',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.right),
),
],
);
}
Widget _buildSpreadIndicator(BuildContext context) {
final spreadStr = depth.spread >= 1
? depth.spread.toStringAsFixed(2)
: depth.spread.toStringAsFixed(4);
return Container(
margin: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: context.appColors.ghostBorder, width: 0.5),
bottom: BorderSide(
color: context.appColors.ghostBorder, width: 0.5),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('价差 ',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.appColors.onSurfaceMuted,
)),
Text('$spreadStr (${depth.spreadPercent.toStringAsFixed(2)}%)',
style: AppTextStyles.numberSmall(context).copyWith(
color: context.colors.onSurfaceVariant,
)),
],
),
);
}
}
class _OrderBookRow extends StatelessWidget {
final OrderBookLevel level;
final bool isAsk;
final double maxQty;
const _OrderBookRow({
required this.level,
required this.isAsk,
required this.maxQty,
});
@override
Widget build(BuildContext context) {
final color =
isAsk ? context.appColors.down : context.appColors.up;
final bgColor =
isAsk ? context.appColors.downBackground : context.appColors.upBackground;
final percent = maxQty > 0 ? (level.quantity / maxQty).clamp(0.0, 1.0) : 0.0;
return Stack(
children: [
// 深度条背景
Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: FractionallySizedBox(
widthFactor: percent,
child: Container(
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),
),
// 内容
Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Expanded(
child: Text(
level.formattedPrice,
style: AppTextStyles.numberSmall(context).copyWith(
color: color,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Text(
level.formattedQuantity,
style: AppTextStyles.numberSmall(context),
textAlign: TextAlign.right,
),
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_theme_extension.dart';
/// 单条价格跳动记录
class PriceTick {
final double price;
final DateTime timestamp;
final bool isUp;
const PriceTick({
required this.price,
required this.timestamp,
required this.isUp,
});
String get formattedPrice {
if (price >= 1000) return price.toStringAsFixed(2);
if (price >= 1) return price.toStringAsFixed(4);
return price.toStringAsFixed(6);
}
}
/// 价格跳动流 Widget
///
/// 显示模拟实时价格跳动,涨绿跌红,最新一条有背景高亮。
class PriceTickStream extends StatelessWidget {
final List<PriceTick> ticks;
const PriceTickStream({super.key, required this.ticks});
@override
Widget build(BuildContext context) {
final displayTicks = ticks.length > 10
? ticks.sublist(ticks.length - 10)
: ticks;
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
decoration: BoxDecoration(
color: context.appColors.surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: context.appColors.ghostBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'实时报价',
style: AppTextStyles.labelMedium(context).copyWith(
color: context.colors.onSurfaceVariant,
),
),
Text(
'模拟数据',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.appColors.onSurfaceMuted,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
SizedBox(
height: 28,
child: displayTicks.isEmpty
? Center(
child: Text(
'选择币种后开始报价',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.appColors.onSurfaceMuted,
),
),
)
: ListView.separated(
scrollDirection: Axis.horizontal,
reverse: true,
itemCount: displayTicks.length,
separatorBuilder: (_, __) =>
const SizedBox(width: AppSpacing.sm),
itemBuilder: (context, index) {
final tick = displayTicks[displayTicks.length - 1 - index];
final isLatest = index == 0;
return _TickItem(tick: tick, isLatest: isLatest);
},
),
),
],
),
);
}
}
class _TickItem extends StatelessWidget {
final PriceTick tick;
final bool isLatest;
const _TickItem({required this.tick, required this.isLatest});
@override
Widget build(BuildContext context) {
final color = tick.isUp ? context.appColors.up : context.appColors.down;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: isLatest
? (tick.isUp
? context.appColors.upBackground
: context.appColors.downBackground)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
tick.formattedPrice,
style: AppTextStyles.numberSmall(context).copyWith(
color: color,
fontWeight: isLatest ? FontWeight.w600 : FontWeight.w500,
),
),
);
}
}

View File

@@ -0,0 +1,455 @@
import 'package:flutter/material.dart';
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 '../../../../data/models/order_book.dart';
/// 交易面板 — 左侧交易表单 + 右侧订单簿
///
/// 布局左50%表单(买入/卖出tab + 市价/限价下拉) | 右50%订单簿(5档买卖盘)
class SplitTradeForm extends StatefulWidget {
final Coin? coin;
final OrderBookDepth depth;
final double realtimePrice;
final TextEditingController buyPriceController;
final TextEditingController buyQuantityController;
final TextEditingController sellPriceController;
final TextEditingController sellQuantityController;
final String availableUsdt;
final String availableCoinQty;
final VoidCallback onBuyQuantityChanged;
final VoidCallback onSellQuantityChanged;
final ValueChanged<double> onBuyFillPercent;
final ValueChanged<double> onSellFillPercent;
final VoidCallback? onBuySubmit;
final VoidCallback? onSellSubmit;
final int orderType;
final ValueChanged<int> onOrderTypeChanged;
const SplitTradeForm({
super.key,
required this.coin,
required this.depth,
required this.realtimePrice,
required this.buyPriceController,
required this.buyQuantityController,
required this.sellPriceController,
required this.sellQuantityController,
required this.availableUsdt,
required this.availableCoinQty,
required this.onBuyQuantityChanged,
required this.onSellQuantityChanged,
required this.onBuyFillPercent,
required this.onSellFillPercent,
this.onBuySubmit,
this.onSellSubmit,
this.orderType = 1,
required this.onOrderTypeChanged,
});
@override
State<SplitTradeForm> createState() => _SplitTradeFormState();
}
class _SplitTradeFormState extends State<SplitTradeForm>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
bool get _isBuy => _tabController.index == 0;
bool get _isLimitOrder => widget.orderType == 2;
@override
Widget build(BuildContext context) {
final accentColor =
_isBuy ? context.appColors.up : context.appColors.down;
return Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: context.appColors.surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: context.appColors.ghostBorder),
),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(flex: 6, child: _buildFormSide(context, accentColor)),
Container(
width: 1,
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
color: context.appColors.ghostBorder,
),
Expanded(flex: 4, child: _buildOrderBookSide(context)),
],
),
),
);
}
Widget _buildFormSide(BuildContext context, Color accentColor) {
final priceController =
_isBuy ? widget.buyPriceController : widget.sellPriceController;
final quantityController =
_isBuy ? widget.buyQuantityController : widget.sellQuantityController;
final available =
_isBuy ? widget.availableUsdt : widget.availableCoinQty;
final onQuantityChanged =
_isBuy ? widget.onBuyQuantityChanged : widget.onSellQuantityChanged;
final onFillPercent =
_isBuy ? widget.onBuyFillPercent : widget.onSellFillPercent;
final onSubmit = _isBuy ? widget.onBuySubmit : widget.onSellSubmit;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 买入/卖出 Tab
Container(
height: 32,
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: TabBar(
controller: _tabController,
indicatorSize: TabBarIndicatorSize.tab,
indicatorPadding: const EdgeInsets.all(3),
indicator: BoxDecoration(
color: accentColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
labelColor: Colors.white,
unselectedLabelColor: context.colors.onSurfaceVariant,
labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 13),
dividerColor: Colors.transparent,
tabs: const [Tab(text: '买入'), Tab(text: '卖出')],
onTap: (_) => setState(() {}),
),
),
const SizedBox(height: AppSpacing.xs),
// 价格行:左侧标签 + 右侧市价/限价切换
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_isLimitOrder ? '委托价格' : '市价',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
_orderTypeLink(context, '市价', 1, accentColor),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text('|', style: TextStyle(fontSize: 10, color: context.colors.onSurfaceVariant.withValues(alpha: 0.4))),
),
_orderTypeLink(context, '限价', 2, accentColor),
],
),
],
),
const SizedBox(height: 4),
// 价格输入框或市价提示
if (_isLimitOrder) ...[
SizedBox(
height: 36,
child: TextField(
controller: priceController,
enabled: true,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (_) => setState(() {}),
style: AppTextStyles.numberSmall(context),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
filled: true,
fillColor: context.colors.surfaceContainerHighest
.withValues(alpha: 0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
borderSide: BorderSide.none,
),
suffixText: 'USDT',
suffixStyle: AppTextStyles.bodySmall(context)
.copyWith(color: context.colors.onSurfaceVariant),
),
),
),
const SizedBox(height: AppSpacing.sm),
] else ...[
Container(
height: 36,
alignment: Alignment.centerLeft,
child: Text(
'以当前最优价格成交',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
),
),
),
const SizedBox(height: AppSpacing.sm),
],
// 数量
Text('数量', style: AppTextStyles.bodySmall(context)
.copyWith(color: context.colors.onSurfaceVariant)),
const SizedBox(height: 4),
SizedBox(
height: 36,
child: TextField(
controller: quantityController,
enabled: true,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (_) => onQuantityChanged(),
style: AppTextStyles.numberSmall(context),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
filled: true,
fillColor: context.colors.surfaceContainerHighest
.withValues(alpha: 0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
borderSide: BorderSide.none,
),
suffixText: widget.coin?.code ?? '',
suffixStyle: AppTextStyles.bodySmall(context)
.copyWith(color: context.colors.onSurfaceVariant),
),
),
),
const SizedBox(height: AppSpacing.xs),
// 金额
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('金额', style: AppTextStyles.bodySmall(context)
.copyWith(color: context.colors.onSurfaceVariant)),
Builder(builder: (context) {
final price = double.tryParse(priceController.text) ?? 0;
final qty = double.tryParse(quantityController.text) ?? 0;
final amount = price * qty;
return Text(
amount > 0 ? '${amount.toStringAsFixed(2)} USDT' : '0.00 USDT',
style: AppTextStyles.numberSmall(context),
);
}),
],
),
const SizedBox(height: AppSpacing.xs),
// 可用余额
Text(
'可用: $available ${_isBuy ? 'USDT' : (widget.coin?.code ?? '')}',
style: AppTextStyles.bodySmall(context)
.copyWith(color: context.colors.onSurfaceVariant),
),
const SizedBox(height: AppSpacing.sm),
// 快捷百分比按钮
Row(
children: [
_pctBtn(context, '25%', 0.25, onFillPercent),
const SizedBox(width: 4),
_pctBtn(context, '50%', 0.50, onFillPercent),
const SizedBox(width: 4),
_pctBtn(context, '75%', 0.75, onFillPercent),
const SizedBox(width: 4),
_pctBtn(context, '100%', 1.0, onFillPercent),
],
),
const SizedBox(height: AppSpacing.sm),
// 提交按钮
SizedBox(
width: double.infinity,
height: 40,
child: ElevatedButton(
onPressed: onSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
),
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'${_isBuy ? '买入' : '卖出'} ${widget.coin?.code ?? ''}',
style: AppTextStyles.labelLarge(context)
.copyWith(color: Colors.white, fontWeight: FontWeight.w700),
),
),
),
),
],
);
}
Widget _pctBtn(BuildContext context, String label, double pct, ValueChanged<double> onTap) {
return Expanded(
child: GestureDetector(
onTap: () => onTap(pct),
child: Container(
height: 28,
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(label,
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w500)),
),
),
),
);
}
Widget _orderTypeLink(BuildContext context, String label, int type, Color accentColor) {
final selected = widget.orderType == type;
return GestureDetector(
onTap: () => widget.onOrderTypeChanged(type),
child: Text(
label,
style: AppTextStyles.bodySmall(context).copyWith(
color: selected ? accentColor : context.colors.onSurfaceVariant,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
),
),
);
}
// ==========================================
// 右侧订单簿
// ==========================================
Widget _buildOrderBookSide(BuildContext context) {
final asks = widget.depth.asks.length > 5
? widget.depth.asks.sublist(0, 5)
: widget.depth.asks;
final bids = widget.depth.bids.length > 5
? widget.depth.bids.sublist(0, 5)
: widget.depth.bids;
final coinCode = widget.coin?.code ?? '';
double maxQty = 0;
for (var a in asks) { if (a.quantity > maxQty) maxQty = a.quantity; }
for (var b in bids) { if (b.quantity > maxQty) maxQty = b.quantity; }
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: 32,
child: Row(children: [
Expanded(child: Text('委托价格', style: AppTextStyles.bodySmall(context)
.copyWith(color: context.colors.onSurfaceVariant, fontWeight: FontWeight.w600))),
Expanded(child: Text('数量', style: AppTextStyles.bodySmall(context)
.copyWith(color: context.colors.onSurfaceVariant, fontWeight: FontWeight.w600),
textAlign: TextAlign.right)),
]),
),
SizedBox(
height: 28,
child: Row(children: [
Expanded(child: Text('(USDT)', style: AppTextStyles.bodySmall(context)
.copyWith(color: context.appColors.onSurfaceMuted, fontSize: 10))),
Expanded(child: Text('($coinCode)', style: AppTextStyles.bodySmall(context)
.copyWith(color: context.appColors.onSurfaceMuted, fontSize: 10),
textAlign: TextAlign.right)),
]),
),
const SizedBox(height: AppSpacing.xs),
...asks.reversed.map((a) => _OrderBookRow(level: a, isAsk: true, maxQty: maxQty)),
_buildSpreadIndicator(context),
...bids.map((b) => _OrderBookRow(level: b, isAsk: false, maxQty: maxQty)),
],
);
}
Widget _buildSpreadIndicator(BuildContext context) {
final spreadStr = widget.depth.spread >= 1
? widget.depth.spread.toStringAsFixed(2)
: widget.depth.spread.toStringAsFixed(4);
return Container(
margin: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: context.appColors.ghostBorder, width: 0.5),
bottom: BorderSide(color: context.appColors.ghostBorder, width: 0.5),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('价差 ', style: AppTextStyles.bodySmall(context)
.copyWith(color: context.appColors.onSurfaceMuted)),
Text('$spreadStr (${widget.depth.spreadPercent.toStringAsFixed(2)}%)',
style: AppTextStyles.numberSmall(context)
.copyWith(color: context.colors.onSurfaceVariant)),
],
),
);
}
}
class _OrderBookRow extends StatelessWidget {
final OrderBookLevel level;
final bool isAsk;
final double maxQty;
const _OrderBookRow({required this.level, required this.isAsk, required this.maxQty});
@override
Widget build(BuildContext context) {
final color = isAsk ? context.appColors.down : context.appColors.up;
final bgColor = isAsk ? context.appColors.downBackground : context.appColors.upBackground;
final percent = maxQty > 0 ? (level.quantity / maxQty).clamp(0.0, 1.0) : 0.0;
return Stack(children: [
Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: FractionallySizedBox(
widthFactor: percent,
child: Container(decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(2))),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(children: [
Expanded(child: Text(level.formattedPrice,
style: AppTextStyles.numberSmall(context)
.copyWith(color: color, fontWeight: FontWeight.w500))),
Expanded(child: Text(level.formattedQuantity,
style: AppTextStyles.numberSmall(context), textAlign: TextAlign.right)),
]),
),
]);
}
}

View File

@@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_theme_extension.dart';
import '../../../../data/models/coin.dart';
/// 单侧交易表单面板(买入或卖出)
class TradeFormPanel extends StatelessWidget {
final bool isBuy;
final Coin? coin;
final TextEditingController priceController;
final TextEditingController quantityController;
final String availableBalance;
final VoidCallback onQuantityChanged;
final ValueChanged<double> onFillPercent;
final VoidCallback? onSubmit;
const TradeFormPanel({
super.key,
required this.isBuy,
required this.coin,
required this.priceController,
required this.quantityController,
required this.availableBalance,
required this.onQuantityChanged,
required this.onFillPercent,
this.onSubmit,
});
@override
Widget build(BuildContext context) {
final accentColor = isBuy ? context.appColors.up : context.appColors.down;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 标题
Text(
isBuy ? '买入' : '卖出',
style: AppTextStyles.headlineMedium(context).copyWith(
color: accentColor,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: AppSpacing.sm),
// 价格输入
_buildInputField(
context: context,
label: '价格',
controller: priceController,
suffix: 'USDT',
enabled: false,
),
const SizedBox(height: AppSpacing.sm),
// 数量输入
_buildInputField(
context: context,
label: '数量',
controller: quantityController,
suffix: coin?.code ?? '',
enabled: true,
onChanged: (_) => onQuantityChanged(),
),
const SizedBox(height: AppSpacing.xs),
// 金额显示
_buildAmountDisplay(context),
const SizedBox(height: AppSpacing.xs),
// 可用余额
Text(
'可用: $availableBalance ${isBuy ? 'USDT' : (coin?.code ?? '')}',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.sm),
// 快捷百分比按钮
Row(
children: [
_pctBtn(context, '25%', 0.25),
const SizedBox(width: 4),
_pctBtn(context, '50%', 0.50),
const SizedBox(width: 4),
_pctBtn(context, '75%', 0.75),
const SizedBox(width: 4),
_pctBtn(context, '100%', 1.0),
],
),
const SizedBox(height: AppSpacing.sm),
// 提交按钮
SizedBox(
height: 40,
child: ElevatedButton(
onPressed: onSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
),
),
child: Text(
'${isBuy ? '买入' : '卖出'} ${coin?.code ?? ''}',
style: AppTextStyles.labelLarge(context).copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
),
],
);
}
Widget _buildInputField({
required BuildContext context,
required String label,
required TextEditingController controller,
required String suffix,
required bool enabled,
ValueChanged<String>? onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
)),
const SizedBox(height: 4),
SizedBox(
height: 36,
child: TextField(
controller: controller,
enabled: enabled,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: enabled ? (v) => onChanged?.call(v) : null,
style: AppTextStyles.numberSmall(context),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
filled: true,
fillColor: context.colors.surfaceContainerHighest.withValues(alpha: 0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
borderSide: BorderSide.none,
),
suffixText: suffix,
suffixStyle: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
),
),
),
),
],
);
}
Widget _buildAmountDisplay(BuildContext context) {
final price = double.tryParse(priceController.text) ?? 0;
final qty = double.tryParse(quantityController.text) ?? 0;
final amount = price * qty;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('金额', style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
)),
Text(
amount > 0 ? '${amount.toStringAsFixed(2)} USDT' : '0.00 USDT',
style: AppTextStyles.numberSmall(context),
),
],
);
}
Widget _pctBtn(BuildContext context, String label, double pct) {
return Expanded(
child: GestureDetector(
onTap: () => onFillPercent(pct),
child: Container(
height: 28,
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(
label,
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
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 '../../../components/coin_icon.dart';
import 'coin_avatar.dart';
/// 紧凑型币对选择头部栏
///
/// 参考币安顶部交易对栏,显示当前币对 + 24h涨跌幅点击弹出底部选择器。
class TradeHeaderBar extends StatelessWidget {
final Coin? selectedCoin;
final List<Coin> coins;
final ValueChanged<Coin> onCoinSelected;
const TradeHeaderBar({
super.key,
required this.selectedCoin,
required this.coins,
required this.onCoinSelected,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showCoinPicker(context),
behavior: HitTestBehavior.opaque,
child: Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: context.appColors.ghostBorder,
width: 1,
),
),
),
child: Row(
children: [
if (selectedCoin != null)
CoinIcon(symbol: selectedCoin!.code, size: 28)
else
const SizedBox(width: 28),
const SizedBox(width: AppSpacing.sm),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
selectedCoin != null
? '${selectedCoin!.code}/USDT'
: '选择币种',
style: AppTextStyles.headlineLarge(context),
),
if (selectedCoin != null)
Text(
selectedCoin!.name,
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
),
),
],
),
const Spacer(),
const SizedBox(width: AppSpacing.xs),
Icon(
LucideIcons.chevronDown,
size: 16,
color: context.colors.onSurfaceVariant,
),
],
),
),
);
}
void _showCoinPicker(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (ctx) => Container(
height: MediaQuery.of(ctx).size.height * 0.65,
decoration: BoxDecoration(
color: ctx.isDark
? ctx.colors.surface
: ctx.colors.surfaceContainerLowest,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: AppSpacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: ctx.colors.onSurfaceVariant.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
),
Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('选择币种',
style: AppTextStyles.headlineLarge(context)),
GestureDetector(
onTap: () => Navigator.of(ctx).pop(),
child: Icon(LucideIcons.x,
color: ctx.colors.onSurfaceVariant),
),
],
),
),
Divider(
height: 1,
color: ctx.colors.outlineVariant.withValues(alpha: 0.2)),
Expanded(
child: ListView.builder(
padding:
const EdgeInsets.symmetric(vertical: AppSpacing.sm),
itemCount: coins.length,
itemBuilder: (listCtx, index) =>
_buildCoinItem(coins[index], context, listCtx),
),
),
],
),
),
);
}
Widget _buildCoinItem(
Coin coin, BuildContext context, BuildContext sheetContext) {
final isSelected = selectedCoin?.code == coin.code;
return GestureDetector(
onTap: () {
Navigator.of(sheetContext).pop();
onCoinSelected(coin);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg, vertical: AppSpacing.md),
color: isSelected
? context.colors.primary.withValues(alpha: 0.1)
: Colors.transparent,
child: Row(
children: [
CoinAvatar(icon: coin.displayIcon),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(coin.code,
style: AppTextStyles.headlineLarge(context)),
const SizedBox(width: AppSpacing.xs),
Text('/USDT',
style: AppTextStyles.bodySmall(context).copyWith(
color: context.colors.onSurfaceVariant,
)),
const Spacer(),
Text('\$${coin.formattedPrice}',
style: AppTextStyles.numberMedium(context)),
if (isSelected) ...[
const SizedBox(width: AppSpacing.sm),
Icon(LucideIcons.check,
size: 16, color: context.colors.primary),
],
],
),
const SizedBox(height: 3),
Text(coin.name,
style: AppTextStyles.bodyMedium(context).copyWith(
color: context.colors.onSurfaceVariant,
)),
],
),
),
],
),
),
);
}
}