feat: 优化

This commit is contained in:
2026-03-23 00:43:19 +08:00
parent ae1aa21445
commit 7be22da0f0
15 changed files with 1369 additions and 1270 deletions

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart';
import '../../../providers/asset_provider.dart';
import '../../shared/ui_constants.dart';
/// 交易页面 - 使用 shadcn_ui 现代化设计
class TradePage extends StatefulWidget {
@@ -23,21 +24,14 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
final _priceController = TextEditingController();
final _quantityController = TextEditingController();
// 颜色常量
static const upColor = Color(0xFF00C853);
static const downColor = Color(0xFFFF5252);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadData();
});
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
void _loadData() {
context.read<MarketProvider>().loadCoins();
context.read<AssetProvider>().loadOverview();
}
@override
@@ -62,13 +56,35 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
key: _formKey,
child: Column(
children: [
_buildCoinSelector(market),
_CoinSelector(
selectedCoin: _selectedCoin,
coins: market.allCoins,
onCoinLoaded: (coin) {
_selectedCoin = coin;
_priceController.text = coin.formattedPrice;
},
),
const SizedBox(height: 16),
_buildPriceCard(),
if (_selectedCoin != null) _PriceCard(coin: _selectedCoin!),
const SizedBox(height: 16),
_buildTradeForm(asset),
_TradeForm(
tradeType: _tradeType,
selectedCoin: _selectedCoin,
priceController: _priceController,
quantityController: _quantityController,
tradeBalance: asset.overview?.tradeBalance,
onTradeTypeChanged: (type) => setState(() => _tradeType = type),
),
const SizedBox(height: 16),
_buildTradeButton(),
_TradeButton(
isBuy: _tradeType == 0,
coinCode: _selectedCoin?.code,
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
_executeTrade();
}
},
),
],
),
),
@@ -78,311 +94,22 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
);
}
Widget _buildCoinSelector(MarketProvider market) {
final theme = ShadTheme.of(context);
final coins = market.allCoins;
if (_selectedCoin == null && coins.isNotEmpty) {
_selectedCoin = coins.first;
_priceController.text = _selectedCoin!.formattedPrice;
}
return ShadCard(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 22,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
child: Text(
_selectedCoin?.displayIcon ?? '?',
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.primary,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_selectedCoin != null ? '${_selectedCoin!.code}/USDT' : '选择币种',
style: theme.textTheme.large.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
_selectedCoin != null ? _selectedCoin!.name : '点击选择交易对',
style: theme.textTheme.muted,
),
],
),
),
Icon(
LucideIcons.chevronRight,
color: theme.colorScheme.mutedForeground,
),
],
),
);
}
Widget _buildPriceCard() {
final theme = ShadTheme.of(context);
if (_selectedCoin == null) {
return const SizedBox.shrink();
}
final coin = _selectedCoin!;
return ShadCard(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最新价',
style: theme.textTheme.muted,
),
const SizedBox(height: 4),
Text(
'\$${coin.formattedPrice}',
style: theme.textTheme.h2.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: coin.isUp ? upColor.withValues(alpha: 0.2) : downColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
coin.formattedChange,
style: TextStyle(
fontSize: 16,
color: coin.isUp ? upColor : downColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildTradeForm(AssetProvider asset) {
final theme = ShadTheme.of(context);
return ShadCard(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 买入/卖出切换
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _tradeType = 0),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _tradeType == 0 ? upColor : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: _tradeType != 0 ? Border.all(color: upColor) : null,
),
child: Center(
child: Text(
'买入',
style: TextStyle(
color: _tradeType == 0 ? Colors.white : upColor,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _tradeType = 1),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _tradeType == 1 ? downColor : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: _tradeType != 1 ? Border.all(color: downColor) : null,
),
child: Center(
child: Text(
'卖出',
style: TextStyle(
color: _tradeType == 1 ? Colors.white : downColor,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
],
),
const SizedBox(height: 20),
// 价格输入
ShadInputFormField(
id: 'price',
label: const Text('价格(USDT)'),
controller: _priceController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
placeholder: const Text('输入价格'),
trailing: const Padding(
padding: EdgeInsets.only(right: 8),
child: Text('USDT'),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入价格';
}
final price = double.tryParse(value);
if (price == null || price <= 0) {
return '请输入有效价格';
}
return null;
},
),
const SizedBox(height: 12),
// 数量输入
ShadInputFormField(
id: 'quantity',
label: const Text('数量'),
controller: _quantityController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
placeholder: const Text('输入数量'),
trailing: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(_selectedCoin?.code ?? ''),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入数量';
}
final quantity = double.tryParse(value);
if (quantity == null || quantity <= 0) {
return '请输入有效数量';
}
return null;
},
),
const SizedBox(height: 16),
// 交易金额
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'交易金额',
style: theme.textTheme.muted,
),
Text(
'${_calculateAmount()} USDT',
style: theme.textTheme.small.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
// 可用余额
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'可用',
style: theme.textTheme.muted,
),
Text(
'${asset.overview?.tradeBalance ?? '0.00'} USDT',
style: theme.textTheme.muted,
),
],
),
],
),
);
}
String _calculateAmount() {
final price = double.tryParse(_priceController.text) ?? 0;
final quantity = double.tryParse(_quantityController.text) ?? 0;
return (price * quantity).toStringAsFixed(2);
}
Widget _buildTradeButton() {
final isBuy = _tradeType == 0;
final color = isBuy ? upColor : downColor;
return SizedBox(
width: double.infinity,
height: 48,
child: ShadButton(
backgroundColor: color,
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
_executeTrade();
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isBuy ? LucideIcons.arrowDownToLine : LucideIcons.arrowUpFromLine,
size: 18,
color: Colors.white,
),
const SizedBox(width: 8),
Text(
'${isBuy ? '买入' : '卖出'} ${_selectedCoin?.code ?? ''}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
void _executeTrade() {
final price = _priceController.text;
final quantity = _quantityController.text;
final isBuy = _tradeType == 0;
showShadDialog(
context: context,
builder: (context) => ShadDialog.alert(
title: Text(_tradeType == 0 ? '确认买入' : '确认卖出'),
description: Text(
'${_tradeType == 0 ? '买入' : '卖出'} $quantity ${_selectedCoin?.code ?? ''} @ $price USDT',
),
builder: (ctx) => ShadDialog.alert(
title: Text(isBuy ? '确认买入' : '确认卖出'),
description: Text('${isBuy ? '买入' : '卖出'} $quantity ${_selectedCoin?.code ?? ''} @ $price USDT'),
actions: [
ShadButton.outline(
child: const Text('取消'),
onPressed: () => Navigator.of(context).pop(),
),
ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()),
ShadButton(
child: const Text('确认'),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(ctx).pop();
_showTradeResult();
},
),
@@ -393,29 +120,24 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
void _showTradeResult() {
final theme = ShadTheme.of(context);
final isBuy = _tradeType == 0;
showShadDialog(
context: context,
builder: (context) => ShadDialog.alert(
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
Icon(
LucideIcons.circleCheck,
color: theme.colorScheme.primary,
size: 24,
),
Icon(LucideIcons.circleCheck, color: theme.colorScheme.primary, size: 24),
const SizedBox(width: 8),
const Text('交易成功'),
],
),
description: Text(
'${_tradeType == 0 ? '买入' : '卖出'} ${_quantityController.text} ${_selectedCoin?.code ?? ''}',
),
description: Text('${isBuy ? '买入' : '卖出'} ${_quantityController.text} ${_selectedCoin?.code ?? ''}'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(ctx).pop();
_quantityController.clear();
},
),
@@ -424,3 +146,320 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
);
}
}
/// 币种选择器
class _CoinSelector extends StatelessWidget {
final Coin? selectedCoin;
final List<Coin> coins;
final ValueChanged<Coin> onCoinLoaded;
const _CoinSelector({
required this.selectedCoin,
required this.coins,
required this.onCoinLoaded,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
// 自动选择第一个币种
if (selectedCoin == null && coins.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) => onCoinLoaded(coins.first));
}
return ShadCard(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_CoinAvatar(icon: selectedCoin?.displayIcon),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种',
style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(selectedCoin?.name ?? '点击选择交易对', style: theme.textTheme.muted),
],
),
),
Icon(LucideIcons.chevronRight, color: theme.colorScheme.mutedForeground),
],
),
);
}
}
/// 币种头像
class _CoinAvatar extends StatelessWidget {
final String? icon;
const _CoinAvatar({this.icon});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return CircleAvatar(
radius: 22,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
child: Text(
icon ?? '?',
style: TextStyle(fontSize: 20, color: theme.colorScheme.primary),
),
);
}
}
/// 价格卡片
class _PriceCard extends StatelessWidget {
final Coin coin;
const _PriceCard({required this.coin});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final color = coin.isUp ? AppColors.up : AppColors.down;
return ShadCard(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('最新价', style: theme.textTheme.muted),
const SizedBox(height: 4),
Text('\$${coin.formattedPrice}', style: theme.textTheme.h2.copyWith(fontWeight: FontWeight.bold)),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
coin.formattedChange,
style: TextStyle(fontSize: 16, color: color, fontWeight: FontWeight.w600),
),
),
],
),
);
}
}
/// 交易表单
class _TradeForm extends StatelessWidget {
final int tradeType;
final Coin? selectedCoin;
final TextEditingController priceController;
final TextEditingController quantityController;
final String? tradeBalance;
final ValueChanged<int> onTradeTypeChanged;
const _TradeForm({
required this.tradeType,
required this.selectedCoin,
required this.priceController,
required this.quantityController,
required this.tradeBalance,
required this.onTradeTypeChanged,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ShadCard(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 买入/卖出切换
_TradeTypeSelector(
tradeType: tradeType,
onChanged: onTradeTypeChanged,
),
const SizedBox(height: 20),
// 价格输入
ShadInputFormField(
id: 'price',
label: const Text('价格(USDT)'),
controller: priceController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
placeholder: const Text('输入价格'),
trailing: const Padding(
padding: EdgeInsets.only(right: 8),
child: Text('USDT'),
),
validator: Validators.price,
),
const SizedBox(height: 12),
// 数量输入
ShadInputFormField(
id: 'quantity',
label: const Text('数量'),
controller: quantityController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
placeholder: const Text('输入数量'),
trailing: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(selectedCoin?.code ?? ''),
),
validator: Validators.quantity,
),
const SizedBox(height: 16),
// 交易金额
_InfoRow(label: '交易金额', value: '${_calculateAmount()} USDT'),
const SizedBox(height: 8),
// 可用余额
_InfoRow(label: '可用', value: '${tradeBalance ?? '0.00'} USDT'),
],
),
);
}
String _calculateAmount() {
final price = double.tryParse(priceController.text) ?? 0;
final quantity = double.tryParse(quantityController.text) ?? 0;
return (price * quantity).toStringAsFixed(2);
}
}
/// 交易类型选择器
class _TradeTypeSelector extends StatelessWidget {
final int tradeType;
final ValueChanged<int> onChanged;
const _TradeTypeSelector({required this.tradeType, required this.onChanged});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: _TypeButton(
label: '买入',
isSelected: tradeType == 0,
color: AppColors.up,
onTap: () => onChanged(0),
),
),
const SizedBox(width: 16),
Expanded(
child: _TypeButton(
label: '卖出',
isSelected: tradeType == 1,
color: AppColors.down,
onTap: () => onChanged(1),
),
),
],
);
}
}
/// 类型按钮
class _TypeButton extends StatelessWidget {
final String label;
final bool isSelected;
final Color color;
final VoidCallback onTap;
const _TypeButton({
required this.label,
required this.isSelected,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? color : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: isSelected ? null : Border.all(color: color),
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : color,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
}
/// 信息行
class _InfoRow extends StatelessWidget {
final String label;
final String value;
const _InfoRow({required this.label, required this.value});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: theme.textTheme.muted),
Text(value, style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w600)),
],
);
}
}
/// 交易按钮
class _TradeButton extends StatelessWidget {
final bool isBuy;
final String? coinCode;
final VoidCallback onPressed;
const _TradeButton({
required this.isBuy,
required this.coinCode,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final color = isBuy ? AppColors.up : AppColors.down;
return SizedBox(
width: double.infinity,
height: 48,
child: ShadButton(
backgroundColor: color,
onPressed: onPressed,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isBuy ? LucideIcons.arrowDownToLine : LucideIcons.arrowUpFromLine, size: 18, color: Colors.white),
const SizedBox(width: 8),
Text(
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ''}',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
],
),
),
);
}
}