Files
monisuo/flutter_monisuo/lib/ui/pages/trade/trade_page.dart
sion ed25bb2da4 refactor: 优化字号主题体系,参考成熟交易平台标准
- 重构主题字号体系 (h1-h4, body, amount等)
- 修复16个页面文件中的硬编码字号
- 新字号层级参考币安/OKX标准
- Display: 22/20/18px (总资产、价格)
- Headline: 15/14/13px (标题、副标题)
- Body: 13/12/11px (正文、辅助文字)
- Label: 11/10/9px (标签)
- Number: 22/16/13px (数字)
2026-04-01 12:49:17 +08:00

1198 lines
39 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart';
import '../../../providers/asset_provider.dart';
import '../../../data/services/trade_service.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
/// 交易页面
class TradePage extends StatefulWidget {
final String? initialCoinCode;
const TradePage({super.key, this.initialCoinCode});
@override
State<TradePage> createState() => _TradePageState();
}
class _TradePageState extends State<TradePage>
with AutomaticKeepAliveClientMixin {
int _tradeType = 0; // 0=买入, 1=卖出
Coin? _selectedCoin;
final _amountController = TextEditingController();
bool _isSubmitting = false;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
void _loadData() {
final marketProvider = context.read<MarketProvider>();
marketProvider.loadCoins().then((_) {
if (widget.initialCoinCode != null && _selectedCoin == null) {
final coins = marketProvider.allCoins;
final coin = coins.firstWhere(
(c) =>
c.code.toUpperCase() == widget.initialCoinCode!.toUpperCase(),
orElse: () =>
coins.isNotEmpty ? coins.first : throw Exception('No coins'),
);
if (mounted) setState(() => _selectedCoin = coin);
}
});
context.read<AssetProvider>().refreshAll(force: true);
}
@override
void dispose() {
_amountController.dispose();
super.dispose();
}
/// 获取交易账户中 USDT 可用余额
String get _availableUsdt {
final holdings = context.read<AssetProvider>().holdings;
final usdt = holdings.where((h) => h.coinCode == 'USDT').firstOrNull;
return usdt?.quantity ?? '0';
}
/// 获取交易账户中当前币种的持仓数量
String get _availableCoinQty {
if (_selectedCoin == null) return '0';
final holdings = context.read<AssetProvider>().holdings;
final pos = holdings
.where((h) => h.coinCode == _selectedCoin!.code)
.firstOrNull;
return pos?.quantity ?? '0';
}
/// 计算可买入/卖出的最大 USDT 金额
String get _maxAmount {
if (_selectedCoin == null) return '0';
final price = _selectedCoin!.price;
if (price <= 0) return '0';
if (_tradeType == 0) {
return _availableUsdt;
} else {
final qty = double.tryParse(_availableCoinQty) ?? 0;
return (qty * price).toStringAsFixed(2);
}
}
/// 计算数量
String get _calculatedQuantity {
final amount = double.tryParse(_amountController.text) ?? 0;
final price = _selectedCoin?.price ?? 0;
if (price <= 0 || amount <= 0) return '0';
return (amount / price).toStringAsFixed(6);
}
@override
Widget build(BuildContext context) {
super.build(context);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.background,
body: Consumer2<MarketProvider, AssetProvider>(
builder: (context, market, asset, _) {
return SingleChildScrollView(
padding: AppSpacing.pagePadding,
child: Column(
children: [
// 币种选择器
_CoinSelector(
selectedCoin: _selectedCoin,
coins: market.allCoins
.where((c) =>
c.code != 'USDT' &&
c.code != 'BTC' &&
c.code != 'ETH')
.toList(),
onCoinSelected: (coin) {
setState(() {
_selectedCoin = coin;
_amountController.clear();
});
},
),
SizedBox(height: AppSpacing.md),
// 价格卡片
if (_selectedCoin != null)
_PriceCard(coin: _selectedCoin!)
else
const _PlaceholderCard(message: '请先选择交易币种'),
SizedBox(height: AppSpacing.md),
// 交易表单
_TradeFormCard(
tradeType: _tradeType,
selectedCoin: _selectedCoin,
amountController: _amountController,
availableUsdt: _availableUsdt,
availableCoinQty: _availableCoinQty,
calculatedQuantity: _calculatedQuantity,
maxAmount: _maxAmount,
onTradeTypeChanged: (type) => setState(() {
_tradeType = type;
_amountController.clear();
}),
onAmountChanged: () => setState(() {}),
onFillPercent: (pct) => _fillPercent(pct),
),
SizedBox(height: AppSpacing.lg),
// 买入 + 卖出双按钮
Row(
children: [
Expanded(
child: _TradeButton(
isBuy: true,
coinCode: _selectedCoin?.code,
enabled: _canTrade() && !_isSubmitting,
isLoading: _isSubmitting && _tradeType == 0,
onPressed: () {
_tradeType = 0;
_executeTrade();
},
),
),
SizedBox(width: AppSpacing.md),
Expanded(
child: _TradeButton(
isBuy: false,
coinCode: _selectedCoin?.code,
enabled: _canTrade() && !_isSubmitting,
isLoading: _isSubmitting && _tradeType == 1,
onPressed: () {
_tradeType = 1;
_executeTrade();
},
),
),
],
),
],
),
);
},
),
);
}
bool _canTrade() {
if (_selectedCoin == null) return false;
final amount = double.tryParse(_amountController.text) ?? 0;
if (amount <= 0) return false;
// 买入时校验不超过可用USDT
if (_tradeType == 0) {
final available = double.tryParse(_availableUsdt) ?? 0;
if (amount > available) return false;
}
return true;
}
void _fillPercent(double pct) {
final max = double.tryParse(_maxAmount) ?? 0;
_amountController.text = (max * pct).toStringAsFixed(2);
setState(() {});
}
void _executeTrade() async {
final isBuy = _tradeType == 0;
final amount = _amountController.text;
final quantity = _calculatedQuantity;
final price = _selectedCoin!.price.toStringAsFixed(2);
final coinCode = _selectedCoin!.code;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => _ConfirmDialog(
isBuy: isBuy,
coinCode: coinCode,
price: price,
quantity: quantity,
amount: amount,
),
);
if (confirmed != true) return;
setState(() => _isSubmitting = true);
try {
final tradeService = context.read<TradeService>();
final response = isBuy
? await tradeService.buy(
coinCode: coinCode, price: price, quantity: quantity)
: await tradeService.sell(
coinCode: coinCode, price: price, quantity: quantity);
if (!mounted) return;
if (response.success) {
_amountController.clear();
// 刷新资产数据
context.read<AssetProvider>().refreshAll(force: true);
_showResultDialog(true, '${isBuy ? '买入' : '卖出'}成功',
'$quantity $coinCode @ $price USDT');
} else {
_showResultDialog(false, '交易失败', response.message ?? '请稍后重试');
}
} catch (e) {
if (mounted) {
_showResultDialog(false, '交易失败', e.toString());
}
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
void _showResultDialog(bool success, String title, String message) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
NeonIcon(
icon: success ? Icons.check_circle : Icons.error,
color: success
? AppColorScheme.getUpColor(isDark)
: colorScheme.error,
size: 24,
),
SizedBox(width: AppSpacing.sm),
Text(title),
],
),
description: Text(message),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}
}
/// 确认对话框
class _ConfirmDialog extends StatelessWidget {
final bool isBuy;
final String coinCode;
final String price;
final String quantity;
final String amount;
const _ConfirmDialog({
required this.isBuy,
required this.coinCode,
required this.price,
required this.quantity,
required this.amount,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final actionColor = isBuy
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
return Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
'确认${isBuy ? '买入' : '卖出'}',
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
SizedBox(height: AppSpacing.lg),
_dialogRow('交易对', '$coinCode/USDT', colorScheme),
SizedBox(height: AppSpacing.sm),
_dialogRow('委托价格', '$price USDT', colorScheme),
SizedBox(height: AppSpacing.sm),
_dialogRow('交易金额', '$amount USDT', colorScheme,
valueColor: actionColor),
SizedBox(height: AppSpacing.sm),
_dialogRow('交易数量', '$quantity $coinCode', colorScheme),
SizedBox(height: AppSpacing.lg),
Row(
children: [
Expanded(
child: NeonButton(
text: '取消',
type: NeonButtonType.outline,
onPressed: () => Navigator.of(context).pop(false),
height: 44,
showGlow: false,
),
),
SizedBox(width: AppSpacing.sm),
Expanded(
child: NeonButton(
text: '确认${isBuy ? '买入' : '卖出'}',
type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error,
onPressed: () => Navigator.of(context).pop(true),
height: 44,
showGlow: true,
),
),
],
),
],
),
),
);
}
Widget _dialogRow(String label, String value, ColorScheme colorScheme,
{Color? valueColor}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: TextStyle(
fontSize: 14, color: colorScheme.onSurfaceVariant)),
Text(value,
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w600,
color: valueColor ?? colorScheme.onSurface,
)),
],
);
}
}
/// 币种选择器
class _CoinSelector extends StatelessWidget {
final Coin? selectedCoin;
final List<Coin> coins;
final ValueChanged<Coin> onCoinSelected;
const _CoinSelector({
required this.selectedCoin,
required this.coins,
required this.onCoinSelected,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: () => _showCoinPicker(context),
child: GlassPanel(
padding: EdgeInsets.all(AppSpacing.md),
child: Row(
children: [
_CoinAvatar(icon: selectedCoin?.displayIcon),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedCoin != null
? '${selectedCoin!.code}/USDT'
: '选择币种',
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.xs),
Text(
selectedCoin?.name ?? '点击选择交易对',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(LucideIcons.chevronDown, color: colorScheme.onSurfaceVariant),
],
),
),
);
}
void _showCoinPicker(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (ctx) => Container(
height: MediaQuery.of(ctx).size.height * 0.65,
decoration: BoxDecoration(
color: isDark
? colorScheme.surface
: colorScheme.surfaceContainerLowest,
borderRadius:
BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)),
),
child: Column(
children: [
Container(
margin: EdgeInsets.only(top: AppSpacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: EdgeInsets.all(AppSpacing.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('选择币种',
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
)),
GestureDetector(
onTap: () => Navigator.of(ctx).pop(),
child: Icon(LucideIcons.x,
color: colorScheme.onSurfaceVariant),
),
],
),
),
Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)),
Expanded(
child: ListView.builder(
padding: 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 colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isSelected = selectedCoin?.code == coin.code;
final changeColor = coin.isUp
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
return GestureDetector(
onTap: () {
Navigator.of(sheetContext).pop();
onCoinSelected(coin);
},
child: Container(
padding:
EdgeInsets.symmetric(horizontal: AppSpacing.lg, vertical: AppSpacing.md),
color:
isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent,
child: Row(
children: [
_CoinAvatar(icon: coin.displayIcon),
SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:币种代码 + 价格 + 涨跌幅
Row(
children: [
Text(coin.code,
style: GoogleFonts.spaceGrotesk(
fontSize: 15,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
)),
SizedBox(width: AppSpacing.xs),
Text('/USDT',
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
)),
const Spacer(),
Text('\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
)),
SizedBox(width: AppSpacing.sm),
Container(
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: changeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(coin.formattedChange,
style: TextStyle(
fontSize: 11,
color: changeColor,
fontWeight: FontWeight.w600,
)),
),
if (isSelected) ...[
SizedBox(width: AppSpacing.sm),
Icon(LucideIcons.check,
size: 16, color: colorScheme.primary),
],
],
),
SizedBox(height: 3),
// 第二行:币种名称
Text(coin.name,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
)),
],
),
),
],
),
),
);
}
}
/// 币种头像
class _CoinAvatar extends StatelessWidget {
final String? icon;
const _CoinAvatar({this.icon});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: colorScheme.primary.withOpacity(0.2)),
),
child: Center(
child: Text(icon ?? '?',
style: TextStyle(
fontSize: 20,
color: colorScheme.primary,
fontWeight: FontWeight.bold,
)),
),
);
}
}
/// 价格卡片 - 重新设计
class _PriceCard extends StatelessWidget {
final Coin coin;
const _PriceCard({required this.coin});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isUp = coin.isUp;
final changeColor =
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down;
final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark)
: colorScheme.error.withOpacity(0.1);
return GlassPanel(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.lg, vertical: AppSpacing.md + AppSpacing.sm),
child: Row(
children: [
// 左侧:币种标签 + 价格
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm, vertical: 2),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'${coin.code}/USDT',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
],
),
SizedBox(height: AppSpacing.sm),
Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
],
),
),
// 右侧:涨跌幅
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: changeColor.withOpacity(0.15)),
),
child: Column(
children: [
Text(
'24h',
style: TextStyle(
fontSize: 10,
color: changeColor.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2),
Text(
coin.formattedChange,
style: TextStyle(
fontSize: 16, color: changeColor, fontWeight: FontWeight.w700),
),
],
),
),
],
),
);
}
}
/// 占位卡片
class _PlaceholderCard extends StatelessWidget {
final String message;
const _PlaceholderCard({required this.message});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.xl),
child: Center(
child: Text(message,
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 14,
)),
),
);
}
}
/// 交易表单卡片 - 重新设计
class _TradeFormCard extends StatelessWidget {
final int tradeType;
final Coin? selectedCoin;
final TextEditingController amountController;
final String availableUsdt;
final String availableCoinQty;
final String calculatedQuantity;
final String maxAmount;
final ValueChanged<int> onTradeTypeChanged;
final VoidCallback onAmountChanged;
final ValueChanged<double> onFillPercent;
const _TradeFormCard({
required this.tradeType,
required this.selectedCoin,
required this.amountController,
required this.availableUsdt,
required this.availableCoinQty,
required this.calculatedQuantity,
required this.maxAmount,
required this.onTradeTypeChanged,
required this.onAmountChanged,
required this.onFillPercent,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isBuy = tradeType == 0;
final actionColor = isBuy
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 买入/卖出切换 - 重新设计
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Row(
children: [
Expanded(
child: _buildTypeButton(
context: context,
label: '买入',
isActive: isBuy,
color: AppColorScheme.buyButtonFill,
icon: LucideIcons.trendingUp,
onTap: () => onTradeTypeChanged(0),
),
),
SizedBox(width: 4),
Expanded(
child: _buildTypeButton(
context: context,
label: '卖出',
isActive: !isBuy,
color: AppColorScheme.sellButtonFill,
icon: LucideIcons.trendingDown,
onTap: () => onTradeTypeChanged(1),
),
),
],
),
),
SizedBox(height: AppSpacing.lg),
// 交易金额输入
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('交易金额',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
)),
Text('USDT',
style: GoogleFonts.spaceGrotesk(
fontSize: 11,
fontWeight: FontWeight.w700,
color: actionColor.withOpacity(0.7),
letterSpacing: 0.5,
)),
],
),
SizedBox(height: AppSpacing.sm),
_AmountInput(
amountController: amountController,
maxAmount: maxAmount,
isBuy: isBuy,
actionColor: actionColor,
onChanged: onAmountChanged,
),
SizedBox(height: AppSpacing.sm),
// 快捷比例按钮 - 药丸样式
Row(
children: [
_buildPctButton('25%', 0.25, colorScheme, actionColor),
SizedBox(width: 6),
_buildPctButton('50%', 0.5, colorScheme, actionColor),
SizedBox(width: 6),
_buildPctButton('75%', 0.75, colorScheme, actionColor),
SizedBox(width: 6),
_buildPctButton('全部', 1.0, colorScheme, actionColor),
],
),
SizedBox(height: AppSpacing.lg),
// 预计数量 + 可用余额 - 卡片样式
Container(
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.1)),
),
child: Column(
children: [
// 预计数量
Row(
children: [
Icon(LucideIcons.calculator,
size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text('预计数量',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
)),
const Spacer(),
Text(
'$calculatedQuantity ${selectedCoin?.code ?? ''}',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
Padding(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm),
child: Divider(
height: 1,
color: colorScheme.outlineVariant.withOpacity(0.08)),
),
// 可用余额
Row(
children: [
Icon(LucideIcons.wallet,
size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text(isBuy ? '可用 USDT' : '可用 ${selectedCoin?.code ?? ""}',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
)),
const Spacer(),
Text(
isBuy
? '$availableUsdt USDT'
: '$availableCoinQty ${selectedCoin?.code ?? ""}',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
],
),
),
],
),
);
}
/// 买入/卖出切换按钮 - 实心填充 + 图标
Widget _buildTypeButton({
required BuildContext context,
required String label,
required bool isActive,
required Color color,
required IconData icon,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + 4),
decoration: BoxDecoration(
color: isActive ? color : Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon,
size: 16,
color: isActive ? Colors.white : color.withOpacity(0.5)),
SizedBox(width: AppSpacing.xs),
Text(
label,
style: TextStyle(
color: isActive ? Colors.white : color.withOpacity(0.5),
fontWeight: FontWeight.w700,
fontSize: 15,
letterSpacing: 0.5,
),
),
],
),
),
);
}
/// 百分比按钮 - 药丸样式
Widget _buildPctButton(
String label, double pct, ColorScheme colorScheme, Color actionColor) {
return Expanded(
child: GestureDetector(
onTap: () => onFillPercent(pct),
child: Container(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm - 2),
decoration: BoxDecoration(
color: actionColor.withOpacity(0.06),
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(color: actionColor.withOpacity(0.12)),
),
child: Center(
child: Text(label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: actionColor.withOpacity(0.8),
)),
),
),
),
);
}
}
/// 交易按钮 - 使用 NeonButton 风格
class _TradeButton extends StatelessWidget {
final bool isBuy;
final String? coinCode;
final bool enabled;
final bool isLoading;
final VoidCallback onPressed;
const _TradeButton({
required this.isBuy,
required this.coinCode,
required this.enabled,
required this.isLoading,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final fillColor =
isBuy ? AppColorScheme.buyButtonFill : AppColorScheme.sellButtonFill;
return GestureDetector(
onTap: enabled ? onPressed : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 52,
decoration: BoxDecoration(
color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isBuy ? LucideIcons.trendingUp : LucideIcons.trendingDown,
size: 16,
color: enabled
? Colors.white
: colorScheme.onSurface.withOpacity(0.3),
),
SizedBox(width: AppSpacing.xs),
Text(
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}',
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.w700,
color: enabled
? Colors.white
: colorScheme.onSurface.withOpacity(0.3),
letterSpacing: 0.5,
),
),
],
),
),
),
);
}
}
/// 金额输入框(含超额提示)
class _AmountInput extends StatefulWidget {
final TextEditingController amountController;
final String maxAmount;
final bool isBuy;
final Color actionColor;
final VoidCallback onChanged;
const _AmountInput({
required this.amountController,
required this.maxAmount,
required this.isBuy,
required this.actionColor,
required this.onChanged,
});
@override
State<_AmountInput> createState() => _AmountInputState();
}
class _AmountInputState extends State<_AmountInput> {
bool _isExceeded = false;
void _checkLimit() {
final input = double.tryParse(widget.amountController.text) ?? 0;
final max = double.tryParse(widget.maxAmount) ?? 0;
final exceeded = widget.isBuy && input > max && max > 0 && input > 0;
if (exceeded != _isExceeded) {
setState(() => _isExceeded = exceeded);
}
widget.onChanged();
}
@override
void initState() {
super.initState();
widget.amountController.addListener(_checkLimit);
}
@override
void dispose() {
widget.amountController.removeListener(_checkLimit);
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final warningColor = AppColorScheme.warning;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(
color: _isExceeded
? warningColor.withOpacity(0.5)
: widget.actionColor.withOpacity(0.15),
),
),
child: TextField(
controller: widget.amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (_) => _checkLimit(),
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: TextStyle(
color: colorScheme.outlineVariant.withOpacity(0.4)),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
suffixIcon: Padding(
padding: EdgeInsets.only(right: AppSpacing.sm),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: widget.actionColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text('USDT',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: widget.actionColor.withOpacity(0.7),
)),
),
],
),
),
suffixIconConstraints: const BoxConstraints(minWidth: 60),
),
),
),
if (_isExceeded)
Padding(
padding: EdgeInsets.only(top: AppSpacing.xs),
child: Row(
children: [
Icon(Icons.error_outline, size: 13, color: warningColor),
SizedBox(width: 4),
Text(
'超出可用USDT余额',
style: TextStyle(fontSize: 11, color: warningColor),
),
],
),
),
],
);
}
}