Files
monisuo/flutter_monisuo/lib/ui/pages/trade/trade_page.dart
2026-03-23 02:43:35 +08:00

467 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/app_colors.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 {
const TradePage({super.key});
@override
State<TradePage> createState() => _TradePageState();
}
class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
int _tradeType = 0; // 0=买入, 1=卖出
Coin? _selectedCoin;
final _formKey = GlobalKey<ShadFormState>();
final _priceController = TextEditingController();
final _quantityController = TextEditingController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
void _loadData() {
context.read<MarketProvider>().loadCoins();
}
@override
void dispose() {
_priceController.dispose();
_quantityController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
final theme = ShadTheme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.background,
body: Consumer2<MarketProvider, AssetProvider>(
builder: (context, market, asset, _) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: ShadForm(
key: _formKey,
child: Column(
children: [
_CoinSelector(
selectedCoin: _selectedCoin,
coins: market.allCoins,
onCoinLoaded: (coin) {
_selectedCoin = coin;
_priceController.text = coin.formattedPrice;
},
),
const SizedBox(height: 16),
if (_selectedCoin != null) _PriceCard(coin: _selectedCoin!),
const SizedBox(height: 16),
_TradeForm(
tradeType: _tradeType,
selectedCoin: _selectedCoin,
priceController: _priceController,
quantityController: _quantityController,
tradeBalance: asset.overview?.tradeBalance,
onTradeTypeChanged: (type) => setState(() => _tradeType = type),
),
const SizedBox(height: 16),
_TradeButton(
isBuy: _tradeType == 0,
coinCode: _selectedCoin?.code,
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
_executeTrade();
}
},
),
],
),
),
);
},
),
);
}
void _executeTrade() {
final price = _priceController.text;
final quantity = _quantityController.text;
final isBuy = _tradeType == 0;
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Text(isBuy ? '确认买入' : '确认卖出'),
description: Text('${isBuy ? '买入' : '卖出'} $quantity ${_selectedCoin?.code ?? ''} @ $price USDT'),
actions: [
ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()),
ShadButton(
child: const Text('确认'),
onPressed: () {
Navigator.of(ctx).pop();
_showTradeResult();
},
),
],
),
);
}
void _showTradeResult() {
final theme = ShadTheme.of(context);
final isBuy = _tradeType == 0;
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
Icon(LucideIcons.circleCheck, color: theme.colorScheme.primary, size: 24),
const SizedBox(width: 8),
const Text('交易成功'),
],
),
description: Text('${isBuy ? '买入' : '卖出'} ${_quantityController.text} ${_selectedCoin?.code ?? ''}'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () {
Navigator.of(ctx).pop();
_quantityController.clear();
},
),
],
),
);
}
}
/// 币种选择器
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),
),
],
),
),
);
}
}