1603 lines
55 KiB
Dart
1603 lines
55 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:intl/intl.dart';
|
||
import '../../../core/constants/app_colors.dart';
|
||
import '../../../core/router/routes.dart';
|
||
import '../../../core/utils/format_utils.dart';
|
||
import '../../../data/models/trade_order_model.dart';
|
||
import '../../../domain/entities/price_info.dart';
|
||
import '../../../domain/entities/market_overview.dart';
|
||
import '../../../domain/entities/trade_order.dart';
|
||
import '../../../domain/entities/kline.dart';
|
||
import '../../providers/user_providers.dart';
|
||
import '../../providers/trading_providers.dart';
|
||
import '../../providers/asset_providers.dart';
|
||
import '../../widgets/shimmer_loading.dart';
|
||
import '../../widgets/kline_chart/kline_chart_widget.dart';
|
||
|
||
class TradingPage extends ConsumerStatefulWidget {
|
||
const TradingPage({super.key});
|
||
|
||
@override
|
||
ConsumerState<TradingPage> createState() => _TradingPageState();
|
||
}
|
||
|
||
class _TradingPageState extends ConsumerState<TradingPage> {
|
||
// 品牌色彩(不随主题变化)
|
||
static const Color _orange = AppColors.orange;
|
||
static const Color _green = AppColors.up;
|
||
static const Color _red = AppColors.down;
|
||
|
||
// 状态
|
||
int _selectedTab = 1; // 0: 买入, 1: 卖出
|
||
int _selectedTimeRange = 4; // 时间周期选择,默认1时
|
||
final _quantityController = TextEditingController();
|
||
final _priceController = TextEditingController();
|
||
bool _isFullScreen = false; // K线图全屏状态
|
||
|
||
final List<String> _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日'];
|
||
|
||
@override
|
||
void dispose() {
|
||
_quantityController.dispose();
|
||
_priceController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final priceAsync = ref.watch(currentPriceProvider);
|
||
final marketAsync = ref.watch(marketOverviewProvider);
|
||
final ordersAsync = ref.watch(ordersProvider);
|
||
final klinesAsync = ref.watch(klinesProvider);
|
||
final user = ref.watch(userNotifierProvider);
|
||
final accountSequence = user.accountSequence ?? '';
|
||
|
||
// 全屏K线图模式
|
||
if (_isFullScreen) {
|
||
return KlineChartWidget(
|
||
klines: klinesAsync.valueOrNull ?? [],
|
||
currentPrice: priceAsync.valueOrNull?.price ?? '0',
|
||
isFullScreen: true,
|
||
onFullScreenToggle: () => setState(() => _isFullScreen = false),
|
||
timeRanges: _timeRanges,
|
||
selectedTimeIndex: _selectedTimeRange,
|
||
onTimeRangeChanged: (index) {
|
||
setState(() => _selectedTimeRange = index);
|
||
ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index];
|
||
},
|
||
);
|
||
}
|
||
|
||
return Scaffold(
|
||
backgroundColor: AppColors.backgroundOf(context),
|
||
body: SafeArea(
|
||
bottom: false,
|
||
child: RefreshIndicator(
|
||
onRefresh: () async {
|
||
ref.invalidate(currentPriceProvider);
|
||
ref.invalidate(marketOverviewProvider);
|
||
ref.invalidate(ordersProvider);
|
||
ref.invalidate(klinesProvider);
|
||
},
|
||
child: Column(
|
||
children: [
|
||
_buildAppBar(),
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
children: [
|
||
_buildPriceCard(priceAsync),
|
||
_buildChartSection(priceAsync, klinesAsync),
|
||
_buildMarketDataCard(marketAsync),
|
||
_buildTradingPanel(priceAsync),
|
||
_buildMyOrdersCard(ordersAsync),
|
||
const SizedBox(height: 24),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAppBar() {
|
||
return Container(
|
||
color: AppColors.surfaceOf(context),
|
||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||
child: Center(
|
||
child: Text(
|
||
'积分股交易',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
color: AppColors.textPrimaryOf(context),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPriceCard(AsyncValue<PriceInfo?> priceAsync) {
|
||
final isLoading = priceAsync.isLoading;
|
||
final priceInfo = priceAsync.valueOrNull;
|
||
final hasError = priceAsync.hasError;
|
||
|
||
if (hasError && priceInfo == null) {
|
||
return _buildErrorCard('价格加载失败');
|
||
}
|
||
|
||
final price = priceInfo?.price ?? '0';
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.all(16),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.cardOf(context),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'当前积分股价格',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: AppColors.textSecondaryOf(context),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
AmountText(
|
||
amount: priceInfo != null ? formatPrice(price) : null,
|
||
isLoading: isLoading,
|
||
prefix: '\u00A5 ',
|
||
style: const TextStyle(
|
||
fontSize: 30,
|
||
fontWeight: FontWeight.bold,
|
||
color: _orange,
|
||
letterSpacing: -0.75,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: _green.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Icon(Icons.trending_up, size: 16, color: _green),
|
||
DataText(
|
||
data: isLoading ? null : '+0.00%',
|
||
isLoading: isLoading,
|
||
placeholder: '+--.--%',
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _green,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildChartSection(AsyncValue<PriceInfo?> priceAsync, AsyncValue<List<Kline>> klinesAsync) {
|
||
final priceInfo = priceAsync.valueOrNull;
|
||
final currentPrice = priceInfo?.price ?? '0.000000';
|
||
final klines = klinesAsync.valueOrNull ?? [];
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.cardOf(context),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: klinesAsync.isLoading
|
||
? const SizedBox(
|
||
height: 280,
|
||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||
)
|
||
: KlineChartWidget(
|
||
klines: klines,
|
||
currentPrice: currentPrice,
|
||
isFullScreen: false,
|
||
onFullScreenToggle: () => setState(() => _isFullScreen = true),
|
||
timeRanges: _timeRanges,
|
||
selectedTimeIndex: _selectedTimeRange,
|
||
onTimeRangeChanged: (index) {
|
||
setState(() => _selectedTimeRange = index);
|
||
ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index];
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMarketDataCard(AsyncValue<MarketOverview?> marketAsync) {
|
||
final isLoading = marketAsync.isLoading;
|
||
final market = marketAsync.valueOrNull;
|
||
final hasError = marketAsync.hasError;
|
||
|
||
if (hasError && market == null) {
|
||
return _buildErrorCard('市场数据加载失败');
|
||
}
|
||
|
||
final bgGray = AppColors.backgroundOf(context);
|
||
final darkText = AppColors.textPrimaryOf(context);
|
||
final grayText = AppColors.textSecondaryOf(context);
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.all(16),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.cardOf(context),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 4,
|
||
height: 16,
|
||
decoration: BoxDecoration(
|
||
color: _orange,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'市场数据',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: darkText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
Row(
|
||
children: [
|
||
_buildMarketDataItem(
|
||
'总积分股',
|
||
market != null ? formatCompact(market.totalShares) : null,
|
||
_orange,
|
||
isLoading,
|
||
),
|
||
Container(width: 1, height: 24, color: bgGray),
|
||
const SizedBox(width: 16),
|
||
_buildMarketDataItem(
|
||
'流通池',
|
||
market != null ? formatCompact(market.circulationPool) : null,
|
||
_orange,
|
||
isLoading,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
Container(height: 1, color: bgGray),
|
||
const SizedBox(height: 24),
|
||
Row(
|
||
children: [
|
||
_buildMarketDataItem(
|
||
'积分值池',
|
||
market != null ? formatCompact(market.greenPoints) : null,
|
||
_orange,
|
||
isLoading,
|
||
),
|
||
Container(width: 1, height: 24, color: bgGray),
|
||
const SizedBox(width: 16),
|
||
_buildMarketDataItem(
|
||
'黑洞销毁量',
|
||
market != null ? formatCompact(market.blackHoleAmount) : null,
|
||
_red,
|
||
isLoading,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMarketDataItem(String label, String? value, Color valueColor, bool isLoading) {
|
||
return Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label, style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context))),
|
||
const SizedBox(height: 4),
|
||
DataText(
|
||
data: value,
|
||
isLoading: isLoading,
|
||
placeholder: '--,---,---',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: valueColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTradingPanel(AsyncValue<PriceInfo?> priceAsync) {
|
||
final priceInfo = priceAsync.valueOrNull;
|
||
final currentPrice = priceInfo?.price ?? '0';
|
||
|
||
// 获取用户资产信息
|
||
final user = ref.watch(userNotifierProvider);
|
||
final accountSequence = user.accountSequence ?? '';
|
||
final assetAsync = ref.watch(accountAssetProvider(accountSequence));
|
||
final asset = assetAsync.valueOrNull;
|
||
|
||
// 获取买入功能开关状态
|
||
final buyEnabledAsync = ref.watch(buyEnabledProvider);
|
||
final buyEnabled = buyEnabledAsync.valueOrNull ?? false;
|
||
|
||
// 挖矿账户积分股(可划转卖出)
|
||
final miningShareBalance = asset?.miningShareBalance ?? '0';
|
||
// 交易账户积分股(可直接卖出)
|
||
final tradingShareBalance = asset?.tradingShareBalance ?? '0';
|
||
// 可用积分股(总计:挖矿 + 交易)
|
||
final availableShares = asset?.availableShares ?? '0';
|
||
// 可用积分值(现金)
|
||
final availableCash = asset?.availableCash ?? '0';
|
||
|
||
// 始终使用实时价格(价格不可修改)
|
||
if (priceInfo != null) {
|
||
_priceController.text = currentPrice;
|
||
}
|
||
|
||
// 如果选中买入但买入功能未开启,强制切换到卖出
|
||
if (_selectedTab == 0 && !buyEnabled) {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted && _selectedTab == 0) {
|
||
setState(() => _selectedTab = 1);
|
||
}
|
||
});
|
||
}
|
||
|
||
final grayText = AppColors.textSecondaryOf(context);
|
||
final bgGray = AppColors.backgroundOf(context);
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.cardOf(context),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: bgGray)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: buyEnabled ? () => setState(() => _selectedTab = 0) : null,
|
||
child: Container(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: _selectedTab == 0 && buyEnabled ? _orange : Colors.transparent,
|
||
width: 2,
|
||
),
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
'买入',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: buyEnabled
|
||
? (_selectedTab == 0 ? _orange : grayText)
|
||
: grayText.withValues(alpha: 0.5),
|
||
),
|
||
),
|
||
if (!buyEnabled) ...[
|
||
const SizedBox(width: 4),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: grayText.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
'待开启',
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
color: grayText.withValues(alpha: 0.7),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => setState(() => _selectedTab = 1),
|
||
child: Container(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: _selectedTab == 1 ? _orange : Colors.transparent,
|
||
width: 2,
|
||
),
|
||
),
|
||
),
|
||
child: Text(
|
||
'卖出',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _selectedTab == 1 ? _orange : grayText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
// 买入功能未开启时显示提示
|
||
if (_selectedTab == 0 && !buyEnabled) ...[
|
||
Container(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Column(
|
||
children: [
|
||
Icon(
|
||
Icons.lock_outline,
|
||
size: 48,
|
||
color: grayText.withValues(alpha: 0.5),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'买入功能待开启',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: grayText,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'买入功能暂未开放,请耐心等待',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: grayText.withValues(alpha: 0.7),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
] else ...[
|
||
// 可用余额提示
|
||
if (_selectedTab == 0) ...[
|
||
// 买入时显示可用积分值
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: _orange.withValues(alpha: 0.05),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'可用积分值',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
Text(
|
||
formatAmount(availableCash),
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _orange,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
] else ...[
|
||
// 卖出时显示可用积分股
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: _orange.withValues(alpha: 0.05),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'可用积分股',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
Text(
|
||
formatAmount(tradingShareBalance),
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _orange,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
// 划转入口
|
||
Align(
|
||
alignment: Alignment.centerRight,
|
||
child: GestureDetector(
|
||
onTap: () => _showTransferDialog(miningShareBalance, tradingShareBalance),
|
||
child: const Text(
|
||
'划转',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _orange,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
const SizedBox(height: 16),
|
||
// 价格显示(只读,使用实时价格)
|
||
_buildInputField('价格', _priceController, '实时价格', '积分值', readOnly: true),
|
||
const SizedBox(height: 16),
|
||
// 数量输入 - 带"全部"按钮
|
||
// 卖出时使用交易账户积分股余额(只能卖出交易账户的,挖矿账户需要先划转)
|
||
_buildQuantityInputField(
|
||
'数量',
|
||
_quantityController,
|
||
'请输入数量',
|
||
'积分股',
|
||
_selectedTab == 1 ? tradingShareBalance : null,
|
||
_selectedTab == 0 ? availableCash : null,
|
||
currentPrice,
|
||
),
|
||
const SizedBox(height: 16),
|
||
// 预计获得/支出
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: bgGray,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
_selectedTab == 0 ? '预计支出' : '预计获得',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
Text(
|
||
_calculateEstimate(),
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _orange,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
// 交易手续费说明 (卖出时显示)
|
||
if (_selectedTab == 1)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'交易手续费',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
const Text(
|
||
'10% 进入积分股池',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _green,
|
||
fontFamily: 'monospace',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
// 提交按钮
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 48,
|
||
child: ElevatedButton(
|
||
onPressed: _handleTrade,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: _orange,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: const Text(
|
||
'确认交易',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildQuantityInputField(
|
||
String label,
|
||
TextEditingController controller,
|
||
String hint,
|
||
String suffix,
|
||
String? availableSharesForSell,
|
||
String? availableCashForBuy,
|
||
String currentPrice,
|
||
) {
|
||
final grayText = AppColors.textSecondaryOf(context);
|
||
final bgGray = AppColors.backgroundOf(context);
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: grayText,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: bgGray,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: TextField(
|
||
controller: controller,
|
||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||
decoration: InputDecoration(
|
||
hintText: hint,
|
||
hintStyle: TextStyle(
|
||
fontSize: 14,
|
||
color: grayText,
|
||
),
|
||
border: InputBorder.none,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||
),
|
||
onChanged: (_) => setState(() {}),
|
||
),
|
||
),
|
||
// 全部按钮
|
||
GestureDetector(
|
||
onTap: () {
|
||
if (availableSharesForSell != null) {
|
||
// 卖出时填入全部可用积分股
|
||
controller.text = availableSharesForSell;
|
||
} else if (availableCashForBuy != null) {
|
||
// 买入时根据可用积分值计算可买数量
|
||
final price = double.tryParse(currentPrice) ?? 0;
|
||
final cash = double.tryParse(availableCashForBuy) ?? 0;
|
||
if (price > 0) {
|
||
final maxQuantity = cash / price;
|
||
controller.text = maxQuantity.toStringAsFixed(4);
|
||
}
|
||
}
|
||
setState(() {});
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
margin: const EdgeInsets.only(right: 4),
|
||
decoration: BoxDecoration(
|
||
color: _orange.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: const Text(
|
||
'全部',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: _orange,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Text(suffix, style: TextStyle(fontSize: 12, color: grayText)),
|
||
const SizedBox(width: 12),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildInputField(
|
||
String label,
|
||
TextEditingController controller,
|
||
String hint,
|
||
String suffix, {
|
||
bool readOnly = false,
|
||
}) {
|
||
final grayText = AppColors.textSecondaryOf(context);
|
||
final darkText = AppColors.textPrimaryOf(context);
|
||
final bgGray = AppColors.backgroundOf(context);
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: grayText,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: readOnly ? bgGray.withOpacity(0.7) : bgGray,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: TextField(
|
||
controller: controller,
|
||
readOnly: readOnly,
|
||
enabled: !readOnly,
|
||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: readOnly ? grayText : darkText,
|
||
),
|
||
decoration: InputDecoration(
|
||
hintText: hint,
|
||
hintStyle: TextStyle(
|
||
fontSize: 14,
|
||
color: grayText,
|
||
),
|
||
border: InputBorder.none,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||
),
|
||
),
|
||
),
|
||
Text(suffix, style: TextStyle(fontSize: 12, color: grayText)),
|
||
const SizedBox(width: 12),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 计算预估获得/支出
|
||
/// 卖出公式:卖出交易额 = (卖出量 + 卖出销毁量) × 价格 × 0.9
|
||
/// = 卖出量 × (1 + burnMultiplier) × 价格 × 0.9
|
||
String _calculateEstimate() {
|
||
final price = double.tryParse(_priceController.text) ?? 0;
|
||
final quantity = double.tryParse(_quantityController.text) ?? 0;
|
||
|
||
if (price == 0 || quantity == 0) {
|
||
return '0.00 积分值';
|
||
}
|
||
|
||
if (_selectedTab == 1) {
|
||
// 卖出时:有效积分股 = 卖出量 × (1 + burnMultiplier)
|
||
// 卖出交易额 = 有效积分股 × 价格 × 0.9(扣除10%手续费)
|
||
final marketAsync = ref.read(marketOverviewProvider);
|
||
final burnMultiplier = double.tryParse(
|
||
marketAsync.valueOrNull?.burnMultiplier ?? '0',
|
||
) ?? 0;
|
||
final effectiveQuantity = quantity * (1 + burnMultiplier);
|
||
final grossAmount = effectiveQuantity * price;
|
||
final netAmount = grossAmount * 0.9; // 扣除10%手续费
|
||
return '${formatAmount(netAmount.toString())} 积分值';
|
||
}
|
||
|
||
// 买入时:支出 = 价格 × 数量
|
||
final total = price * quantity;
|
||
return '${formatAmount(total.toString())} 积分值';
|
||
}
|
||
|
||
Widget _buildMyOrdersCard(AsyncValue<OrdersPageModel?> ordersAsync) {
|
||
final isLoading = ordersAsync.isLoading;
|
||
final ordersPage = ordersAsync.valueOrNull;
|
||
final orders = ordersPage?.data ?? [];
|
||
final darkText = AppColors.textPrimaryOf(context);
|
||
final grayText = AppColors.textSecondaryOf(context);
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.all(16),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.cardOf(context),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'我的挂单',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: darkText,
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () => context.push(Routes.tradingRecords),
|
||
child: const Text(
|
||
'全部 >',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: _orange,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
if (isLoading)
|
||
const Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(20),
|
||
child: CircularProgressIndicator(color: _orange),
|
||
),
|
||
)
|
||
else if (orders.isEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||
child: Text(
|
||
'暂无挂单',
|
||
style: TextStyle(fontSize: 14, color: grayText),
|
||
),
|
||
)
|
||
else
|
||
Column(
|
||
children: orders.take(3).map((order) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 8),
|
||
child: _buildOrderItemFromEntity(order),
|
||
)).toList(),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildOrderItemFromEntity(TradeOrder order) {
|
||
final isSell = order.isSell;
|
||
final dateFormat = DateFormat('MM/dd HH:mm');
|
||
final formattedDate = dateFormat.format(order.createdAt);
|
||
final darkText = AppColors.textPrimaryOf(context);
|
||
final grayText = AppColors.textSecondaryOf(context);
|
||
final bgGray = AppColors.backgroundOf(context);
|
||
final cardBg = AppColors.cardOf(context);
|
||
|
||
String statusText;
|
||
switch (order.status) {
|
||
case OrderStatus.pending:
|
||
statusText = '待成交';
|
||
break;
|
||
case OrderStatus.partial:
|
||
statusText = '部分成交';
|
||
break;
|
||
case OrderStatus.filled:
|
||
statusText = '已成交';
|
||
break;
|
||
case OrderStatus.cancelled:
|
||
statusText = '已取消';
|
||
break;
|
||
}
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: cardBg,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: bgGray),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: (isSell ? _red : _green).withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
isSell ? '卖出' : '买入',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
color: isSell ? _red : _green,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
formatPrice(order.price),
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: darkText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
formattedDate,
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
],
|
||
),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Text(
|
||
'${formatCompact(order.quantity)} 股',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
color: darkText,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: _orange.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
child: Text(
|
||
statusText,
|
||
style: const TextStyle(fontSize: 12, color: _orange),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildErrorCard(String message) {
|
||
return Container(
|
||
margin: const EdgeInsets.all(16),
|
||
padding: const EdgeInsets.all(32),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.cardOf(context),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Center(
|
||
child: Column(
|
||
children: [
|
||
const Icon(Icons.error_outline, size: 48, color: AppColors.error),
|
||
const SizedBox(height: 8),
|
||
Text(message),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _handleTrade() async {
|
||
if (_priceController.text.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('请输入价格')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (_quantityController.text.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('请输入数量')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
final isBuy = _selectedTab == 0;
|
||
final price = double.tryParse(_priceController.text) ?? 0;
|
||
final quantity = double.tryParse(_quantityController.text) ?? 0;
|
||
|
||
// 卖出时显示确认弹窗
|
||
if (!isBuy) {
|
||
// 获取销毁倍数,使用与后端一致的公式计算
|
||
final marketAsync = ref.read(marketOverviewProvider);
|
||
final burnMultiplier = double.tryParse(
|
||
marketAsync.valueOrNull?.burnMultiplier ?? '0',
|
||
) ?? 0;
|
||
|
||
// 有效积分股 = 卖出量 × (1 + burnMultiplier)
|
||
final effectiveQuantity = quantity * (1 + burnMultiplier);
|
||
// 交易总额 = 有效积分股 × 价格
|
||
final grossAmount = effectiveQuantity * price;
|
||
// 手续费 = 交易总额 × 10%
|
||
final tradeFee = grossAmount * 0.1;
|
||
// 实际获得 = 交易总额 × 90%
|
||
final received = grossAmount * 0.9;
|
||
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('确认卖出'),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('卖出数量: ${formatAmount(quantity.toString())} 积分股'),
|
||
const SizedBox(height: 8),
|
||
Text('卖出价格: ${formatPrice(price.toString())} 积分值'),
|
||
const SizedBox(height: 8),
|
||
Text('销毁倍数: ${burnMultiplier.toStringAsFixed(4)}'),
|
||
const SizedBox(height: 8),
|
||
Text('有效积分股: ${formatAmount(effectiveQuantity.toString())}'),
|
||
const SizedBox(height: 8),
|
||
Text('交易总额: ${formatAmount(grossAmount.toString())} 积分值'),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'进入积分股池: ${formatAmount(tradeFee.toString())} 积分值 (10%)',
|
||
style: const TextStyle(color: _green),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'实际获得: ${formatAmount(received.toString())} 积分值',
|
||
style: const TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
color: _green,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'注意: 卖出积分股将扣除10%进入积分股池,此操作不可撤销。',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: AppColors.textSecondaryOf(context),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context, false),
|
||
child: const Text('取消'),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context, true),
|
||
child: const Text(
|
||
'确认卖出',
|
||
style: TextStyle(color: _orange),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (confirmed != true) return;
|
||
}
|
||
|
||
bool success;
|
||
|
||
if (isBuy) {
|
||
success = await ref
|
||
.read(tradingNotifierProvider.notifier)
|
||
.buyShares(_priceController.text, _quantityController.text);
|
||
} else {
|
||
success = await ref
|
||
.read(tradingNotifierProvider.notifier)
|
||
.sellShares(_priceController.text, _quantityController.text);
|
||
}
|
||
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(success
|
||
? (isBuy ? '买入订单已提交' : '卖出订单已提交')
|
||
: (isBuy ? '买入失败' : '卖出失败')),
|
||
backgroundColor: success ? _green : AppColors.error,
|
||
),
|
||
);
|
||
if (success) {
|
||
_quantityController.clear();
|
||
// 交易成功后立即刷新
|
||
_refreshAfterTrade();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 交易成功后刷新数据
|
||
/// 立即刷新一次,然后在2秒和5秒后再各刷新一次
|
||
/// 确保能看到做市商处理后的最新状态
|
||
void _refreshAfterTrade() {
|
||
final user = ref.read(userNotifierProvider);
|
||
final accountSeq = user.accountSequence ?? '';
|
||
|
||
// 立即刷新
|
||
_doRefresh(accountSeq);
|
||
|
||
// 2秒后再刷新(做市商可能在1-4秒内吃单)
|
||
Future.delayed(const Duration(seconds: 2), () {
|
||
if (mounted) _doRefresh(accountSeq);
|
||
});
|
||
|
||
// 5秒后最终刷新(确保看到最终状态)
|
||
Future.delayed(const Duration(seconds: 5), () {
|
||
if (mounted) _doRefresh(accountSeq);
|
||
});
|
||
}
|
||
|
||
void _doRefresh(String accountSeq) {
|
||
ref.invalidate(ordersProvider);
|
||
ref.invalidate(currentPriceProvider);
|
||
ref.invalidate(marketOverviewProvider);
|
||
ref.invalidate(accountAssetProvider(accountSeq));
|
||
}
|
||
|
||
/// 显示划转弹窗
|
||
void _showTransferDialog(String miningBalance, String tradingBalance) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: Colors.transparent,
|
||
builder: (context) => _TransferBottomSheet(
|
||
miningBalance: miningBalance,
|
||
tradingBalance: tradingBalance,
|
||
onTransferComplete: () {
|
||
final user = ref.read(userNotifierProvider);
|
||
final accountSeq = user.accountSequence ?? '';
|
||
_doRefresh(accountSeq);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 划转底部弹窗
|
||
class _TransferBottomSheet extends ConsumerStatefulWidget {
|
||
final String miningBalance;
|
||
final String tradingBalance;
|
||
final VoidCallback onTransferComplete;
|
||
|
||
const _TransferBottomSheet({
|
||
required this.miningBalance,
|
||
required this.tradingBalance,
|
||
required this.onTransferComplete,
|
||
});
|
||
|
||
@override
|
||
ConsumerState<_TransferBottomSheet> createState() => _TransferBottomSheetState();
|
||
}
|
||
|
||
class _TransferBottomSheetState extends ConsumerState<_TransferBottomSheet> {
|
||
static const Color _orange = AppColors.orange;
|
||
static const Color _green = AppColors.up;
|
||
|
||
// 0: 从挖矿划入交易, 1: 从交易划出到挖矿
|
||
int _direction = 0;
|
||
final _amountController = TextEditingController();
|
||
bool _isLoading = false;
|
||
|
||
@override
|
||
void dispose() {
|
||
_amountController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
String get _availableBalance {
|
||
return _direction == 0 ? widget.miningBalance : widget.tradingBalance;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final cardBg = AppColors.cardOf(context);
|
||
final darkText = AppColors.textPrimaryOf(context);
|
||
final grayText = AppColors.textSecondaryOf(context);
|
||
final bgGray = AppColors.backgroundOf(context);
|
||
|
||
return Container(
|
||
padding: EdgeInsets.only(
|
||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: cardBg,
|
||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||
),
|
||
child: SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 标题栏
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'积分股划转',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
color: darkText,
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () => Navigator.pop(context),
|
||
child: Icon(Icons.close, color: grayText),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
// 划转方向切换
|
||
Container(
|
||
padding: const EdgeInsets.all(4),
|
||
decoration: BoxDecoration(
|
||
color: bgGray,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => setState(() {
|
||
_direction = 0;
|
||
_amountController.clear();
|
||
}),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: _direction == 0 ? AppColors.cardOf(context) : Colors.transparent,
|
||
borderRadius: BorderRadius.circular(6),
|
||
boxShadow: _direction == 0
|
||
? [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4)]
|
||
: null,
|
||
),
|
||
child: Text(
|
||
'划入交易账户',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: _direction == 0 ? FontWeight.bold : FontWeight.normal,
|
||
color: _direction == 0 ? _orange : grayText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => setState(() {
|
||
_direction = 1;
|
||
_amountController.clear();
|
||
}),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: _direction == 1 ? AppColors.cardOf(context) : Colors.transparent,
|
||
borderRadius: BorderRadius.circular(6),
|
||
boxShadow: _direction == 1
|
||
? [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4)]
|
||
: null,
|
||
),
|
||
child: Text(
|
||
'划出到挖矿账户',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: _direction == 1 ? FontWeight.bold : FontWeight.normal,
|
||
color: _direction == 1 ? _orange : grayText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
// 划转说明
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: bgGray,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
_direction == 0 ? '挖矿账户' : '交易账户',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
formatAmount(_direction == 0 ? widget.miningBalance : widget.tradingBalance),
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: darkText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Icon(Icons.arrow_forward, color: _orange),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Text(
|
||
_direction == 0 ? '交易账户' : '挖矿账户',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
formatAmount(_direction == 0 ? widget.tradingBalance : widget.miningBalance),
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: darkText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
// 数量输入
|
||
Text(
|
||
'划转数量',
|
||
style: TextStyle(fontSize: 14, color: darkText),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: bgGray),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: TextField(
|
||
controller: _amountController,
|
||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||
decoration: InputDecoration(
|
||
hintText: '请输入划转数量',
|
||
hintStyle: TextStyle(color: grayText, fontSize: 14),
|
||
border: InputBorder.none,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||
),
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
_amountController.text = _availableBalance;
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
margin: const EdgeInsets.only(right: 8),
|
||
decoration: BoxDecoration(
|
||
color: _orange.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: const Text(
|
||
'全部',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _orange,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'可用: ${formatAmount(_availableBalance)} 积分股',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'提示: 最低划转数量为 0.01 积分股',
|
||
style: TextStyle(fontSize: 12, color: grayText),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
Navigator.pop(context);
|
||
context.push(Routes.transferRecords);
|
||
},
|
||
child: const Text(
|
||
'划转记录',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _orange,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
// 提交按钮
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 48,
|
||
child: ElevatedButton(
|
||
onPressed: _isLoading ? null : _handleTransfer,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: _orange,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: _isLoading
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||
),
|
||
)
|
||
: Text(
|
||
_direction == 0 ? '划入交易账户' : '划出到挖矿账户',
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _handleTransfer() async {
|
||
final amount = _amountController.text.trim();
|
||
if (amount.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('请输入划转数量')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
final amountValue = double.tryParse(amount) ?? 0;
|
||
if (amountValue < 0.01) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('最低划转数量为 0.01 积分股')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
final available = double.tryParse(_availableBalance) ?? 0;
|
||
if (amountValue > available) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('划转数量超过可用余额')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
setState(() => _isLoading = true);
|
||
|
||
try {
|
||
bool success;
|
||
if (_direction == 0) {
|
||
// 从挖矿划入交易
|
||
success = await ref.read(tradingNotifierProvider.notifier).transferIn(amount);
|
||
} else {
|
||
// 从交易划出到挖矿
|
||
success = await ref.read(tradingNotifierProvider.notifier).transferOut(amount);
|
||
}
|
||
|
||
if (mounted) {
|
||
if (success) {
|
||
Navigator.pop(context);
|
||
widget.onTransferComplete();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(_direction == 0 ? '划入成功' : '划出成功'),
|
||
backgroundColor: _green,
|
||
),
|
||
);
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('划转失败,请稍后重试'),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('划转失败: $e'),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
}
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() => _isLoading = false);
|
||
}
|
||
}
|
||
}
|
||
}
|