111
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
class ResponseCode {
|
||||
static const String success = '0000';
|
||||
static const String unauthorized = '0002';
|
||||
static const String kycRequired = 'KYC_REQUIRED';
|
||||
}
|
||||
|
||||
/// API 响应模型
|
||||
@@ -71,4 +72,5 @@ class ApiResponse<T> {
|
||||
|
||||
bool get isSuccess => success;
|
||||
bool get isUnauthorized => code == ResponseCode.unauthorized;
|
||||
bool get isKycRequired => code == ResponseCode.kycRequired;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,24 @@ class DioClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart 文件上传
|
||||
Future<ApiResponse<T>> upload<T>(
|
||||
String path, {
|
||||
required FormData formData,
|
||||
T Function(dynamic)? fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
path,
|
||||
data: formData,
|
||||
options: Options(contentType: 'multipart/form-data'),
|
||||
);
|
||||
return _handleResponse<T>(response, fromJson);
|
||||
} on DioException catch (e) {
|
||||
return _handleError<T>(e);
|
||||
}
|
||||
}
|
||||
|
||||
ApiResponse<T> _handleResponse<T>(
|
||||
Response response,
|
||||
T Function(dynamic)? fromJson,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../../core/network/api_response.dart';
|
||||
import '../../core/network/dio_client.dart';
|
||||
@@ -39,14 +41,19 @@ class UserService {
|
||||
);
|
||||
}
|
||||
|
||||
/// 上传 KYC 资料
|
||||
/// 上传 KYC 资料(身份证正反面图片字节)
|
||||
/// 使用 fromBytes 以兼容 Web 和移动端
|
||||
Future<ApiResponse<void>> uploadKyc(
|
||||
String idCardFront,
|
||||
String idCardBack,
|
||||
Uint8List frontBytes,
|
||||
Uint8List backBytes,
|
||||
) async {
|
||||
return _client.post<void>(
|
||||
final formData = FormData.fromMap({
|
||||
'front': MultipartFile.fromBytes(frontBytes, filename: 'front.jpg'),
|
||||
'back': MultipartFile.fromBytes(backBytes, filename: 'back.jpg'),
|
||||
});
|
||||
return _client.upload<void>(
|
||||
ApiEndpoints.kyc,
|
||||
data: {'idCardFront': idCardFront, 'idCardBack': idCardBack},
|
||||
formData: formData,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/network/api_response.dart';
|
||||
import '../core/network/dio_client.dart';
|
||||
@@ -134,6 +135,21 @@ class AuthProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交KYC实名认证(真实图片上传)
|
||||
Future<ApiResponse<void>> submitKyc(
|
||||
Uint8List frontBytes, Uint8List backBytes) async {
|
||||
try {
|
||||
final response =
|
||||
await _userService.uploadKyc(frontBytes, backBytes);
|
||||
if (response.success) {
|
||||
await refreshUserInfo();
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
return ApiResponse.fail('KYC提交失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
|
||||
@@ -6,11 +6,13 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/theme/app_color_scheme.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../providers/asset_provider.dart';
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import '../../shared/ui_constants.dart';
|
||||
import '../../components/glass_panel.dart';
|
||||
import '../../components/neon_glow.dart';
|
||||
import '../orders/fund_orders_page.dart';
|
||||
import 'transfer_page.dart';
|
||||
import '../mine/kyc_page.dart';
|
||||
|
||||
/// 资产页面 - Material Design 3 风格
|
||||
class AssetPage extends StatefulWidget {
|
||||
@@ -897,6 +899,13 @@ class _WalletAddressCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _showWithdrawDialog(BuildContext context, String? balance) {
|
||||
// KYC校验:未完成实名认证则引导去认证
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (auth.user?.kycStatus != 2) {
|
||||
_showKycRequiredDialog(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final amountController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final contactController = TextEditingController();
|
||||
@@ -1109,23 +1118,17 @@ void _showResultDialog(BuildContext context, String title, String? message) {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(title,
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
)),
|
||||
if (message != null) ...[
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(message,
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center),
|
||||
],
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
SizedBox(
|
||||
@@ -1144,3 +1147,85 @@ void _showResultDialog(BuildContext context, String title, String? message) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showKycRequiredDialog(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: GlassPanel(
|
||||
borderRadius: BorderRadius.circular(AppRadius.xxl),
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorScheme.warning.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.shieldAlert,
|
||||
color: AppColorScheme.warning,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'需要实名认证',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'提现前请先完成实名认证,保障账户安全',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '取消',
|
||||
type: NeonButtonType.outline,
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
height: 44,
|
||||
showGlow: false,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '去认证',
|
||||
type: NeonButtonType.primary,
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const KycPage(returnToWithdraw: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
height: 44,
|
||||
showGlow: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ class _HomePageState extends State<HomePage>
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
// 新人福利卡片
|
||||
if (!_bonusClaimed)
|
||||
_BonusCard(
|
||||
onClaim: _claimBonus,
|
||||
isLoading: _bonusLoading,
|
||||
),
|
||||
_BonusCard(
|
||||
isClaimed: _bonusClaimed,
|
||||
onClaim: _claimBonus,
|
||||
isLoading: _bonusLoading,
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
// 持仓
|
||||
_HoldingsSection(holdings: provider.holdings),
|
||||
@@ -963,105 +963,118 @@ class _AssetCardState extends State<_AssetCard> {
|
||||
|
||||
/// 新人福利卡片
|
||||
class _BonusCard extends StatelessWidget {
|
||||
final bool isClaimed;
|
||||
final VoidCallback onClaim;
|
||||
final bool isLoading;
|
||||
|
||||
const _BonusCard({required this.onClaim, required this.isLoading});
|
||||
const _BonusCard({required this.isClaimed, required this.onClaim, required this.isLoading});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isLoading ? null : onClaim,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
colorScheme.primary.withOpacity(0.15),
|
||||
colorScheme.secondary.withOpacity(0.1),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
return Opacity(
|
||||
opacity: isClaimed ? 0.6 : 1.0,
|
||||
child: GestureDetector(
|
||||
onTap: isClaimed || isLoading ? null : onClaim,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
colorScheme.primary.withOpacity(0.15),
|
||||
colorScheme.secondary.withOpacity(0.1),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: colorScheme.primary.withOpacity(0.2)),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: colorScheme.primary.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 左侧图标
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
child: Row(
|
||||
children: [
|
||||
// 左侧图标
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: isClaimed
|
||||
? colorScheme.onSurfaceVariant.withOpacity(0.1)
|
||||
: colorScheme.primary.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
),
|
||||
child: Icon(
|
||||
isClaimed ? LucideIcons.check : LucideIcons.gift,
|
||||
color: isClaimed ? AppColorScheme.up : colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.gift,
|
||||
color: colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.md),
|
||||
// 中间文字
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'新人福利',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'领取 50 USDT 体验金,开始您的交易之旅',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 右侧按钮
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: isDark
|
||||
? AppColorScheme.darkCtaGradient
|
||||
: AppColorScheme.lightCtaGradient,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: isDark ? colorScheme.background : Colors.white,
|
||||
SizedBox(width: AppSpacing.md),
|
||||
// 中间文字
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'新人福利',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'立即领取',
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
isClaimed ? '50 USDT 体验金已到账' : '领取 50 USDT 体验金,开始您的交易之旅',
|
||||
style: TextStyle(
|
||||
color: isDark ? colorScheme.background : Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
// 右侧按钮
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isClaimed
|
||||
? colorScheme.onSurfaceVariant.withOpacity(0.15)
|
||||
: null,
|
||||
gradient: isClaimed
|
||||
? null
|
||||
: (isDark
|
||||
? AppColorScheme.darkCtaGradient
|
||||
: AppColorScheme.lightCtaGradient),
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: isDark ? colorScheme.background : Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
isClaimed ? '已领取' : '立即领取',
|
||||
style: TextStyle(
|
||||
color: isClaimed
|
||||
? colorScheme.onSurfaceVariant
|
||||
: (isDark ? colorScheme.background : Colors.white),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -170,9 +170,7 @@ class _FeaturedCard extends StatelessWidget {
|
||||
? AppColorScheme.getUpBackgroundColor(isDark)
|
||||
: colorScheme.error.withOpacity(0.1);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _navigateToTrade(context),
|
||||
child: GlassPanel(
|
||||
return GlassPanel(
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -244,14 +242,8 @@ class _FeaturedCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToTrade(BuildContext context) {
|
||||
final mainState = context.findAncestorStateOfType<MainPageState>();
|
||||
mainState?.switchToTrade(coin.code);
|
||||
}
|
||||
}
|
||||
|
||||
/// 下半区列表项
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/theme/app_color_scheme.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import 'kyc_page.dart';
|
||||
import '../../../providers/theme_provider.dart';
|
||||
import '../auth/login_page.dart';
|
||||
import '../../components/glass_panel.dart';
|
||||
@@ -54,7 +55,11 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
|
||||
children: [
|
||||
_UserCard(user: auth.user),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
_MenuList(onShowComingSoon: _showComingSoon, onShowAbout: _showAboutDialog),
|
||||
_MenuList(
|
||||
onShowComingSoon: _showComingSoon,
|
||||
onShowAbout: _showAboutDialog,
|
||||
kycStatus: auth.user?.kycStatus ?? 0,
|
||||
),
|
||||
SizedBox(height: AppSpacing.xl),
|
||||
_LogoutButton(onLogout: () => _handleLogout(auth)),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
@@ -324,8 +329,13 @@ class _InfoRow extends StatelessWidget {
|
||||
class _MenuList extends StatelessWidget {
|
||||
final void Function(String) onShowComingSoon;
|
||||
final VoidCallback onShowAbout;
|
||||
final int kycStatus;
|
||||
|
||||
const _MenuList({required this.onShowComingSoon, required this.onShowAbout});
|
||||
const _MenuList({
|
||||
required this.onShowComingSoon,
|
||||
required this.onShowAbout,
|
||||
required this.kycStatus,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -341,7 +351,7 @@ class _MenuList extends StatelessWidget {
|
||||
_ThemeToggleTile(isDarkMode: themeProvider.isDarkMode),
|
||||
_buildDivider(),
|
||||
// 菜单项
|
||||
..._buildMenuItems(colorScheme),
|
||||
..._buildMenuItems(context, colorScheme),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -355,14 +365,27 @@ class _MenuList extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildMenuItems(ColorScheme colorScheme) {
|
||||
List<Widget> _buildMenuItems(BuildContext context, ColorScheme colorScheme) {
|
||||
final items = [
|
||||
_MenuItem(
|
||||
icon: LucideIcons.userCheck,
|
||||
title: '实名认证',
|
||||
subtitle: '完成实名认证,解锁更多功能',
|
||||
iconColor: colorScheme.primary,
|
||||
onTap: () => onShowComingSoon('实名认证'),
|
||||
subtitle: kycStatus == 2
|
||||
? '已认证'
|
||||
: kycStatus == 1
|
||||
? '审核中'
|
||||
: '完成实名认证,解锁更多功能',
|
||||
iconColor: kycStatus == 2 ? AppColorScheme.up : colorScheme.primary,
|
||||
onTap: () {
|
||||
if (kycStatus == 2) {
|
||||
_showKycStatusDialog(context);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const KycPage()),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_MenuItem(
|
||||
icon: LucideIcons.shield,
|
||||
@@ -403,6 +426,28 @@ class _MenuList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _showKycStatusDialog(BuildContext context) {
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ShadDialog.alert(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: AppColorScheme.up, size: 20),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
const Text('实名认证'),
|
||||
],
|
||||
),
|
||||
description: const Text('您的实名认证已通过'),
|
||||
actions: [
|
||||
ShadButton(
|
||||
child: const Text('确定'),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 主题切换组件
|
||||
class _ThemeToggleTile extends StatelessWidget {
|
||||
final bool isDarkMode;
|
||||
|
||||
@@ -84,10 +84,8 @@ class _TradePageState extends State<TradePage>
|
||||
if (price <= 0) return '0';
|
||||
|
||||
if (_tradeType == 0) {
|
||||
// 买入:最大 = USDT 余额
|
||||
return _availableUsdt;
|
||||
} else {
|
||||
// 卖出:最大 = 持有数量 × 当前价格
|
||||
final qty = double.tryParse(_availableCoinQty) ?? 0;
|
||||
return (qty * price).toStringAsFixed(2);
|
||||
}
|
||||
@@ -118,7 +116,10 @@ class _TradePageState extends State<TradePage>
|
||||
_CoinSelector(
|
||||
selectedCoin: _selectedCoin,
|
||||
coins: market.allCoins
|
||||
.where((c) => c.code != 'USDT')
|
||||
.where((c) =>
|
||||
c.code != 'USDT' &&
|
||||
c.code != 'BTC' &&
|
||||
c.code != 'ETH')
|
||||
.toList(),
|
||||
onCoinSelected: (coin) {
|
||||
setState(() {
|
||||
@@ -133,7 +134,7 @@ class _TradePageState extends State<TradePage>
|
||||
if (_selectedCoin != null)
|
||||
_PriceCard(coin: _selectedCoin!)
|
||||
else
|
||||
_PlaceholderCard(message: '请先选择交易币种'),
|
||||
const _PlaceholderCard(message: '请先选择交易币种'),
|
||||
|
||||
SizedBox(height: AppSpacing.md),
|
||||
|
||||
@@ -197,7 +198,13 @@ class _TradePageState extends State<TradePage>
|
||||
bool _canTrade() {
|
||||
if (_selectedCoin == null) return false;
|
||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||
return amount > 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) {
|
||||
@@ -524,22 +531,52 @@ class _CoinSelector extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 第一行:币种代码 + 价格 + 涨跌幅
|
||||
Row(
|
||||
children: [
|
||||
Text(coin.code,
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
)),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
Text('/USDT',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
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,
|
||||
@@ -548,27 +585,6 @@ class _CoinSelector extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('\$${coin.formattedPrice}',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
)),
|
||||
Text(coin.formattedChange,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: changeColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (isSelected) ...[
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Icon(LucideIcons.check, size: 18, color: colorScheme.primary),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -604,7 +620,7 @@ class _CoinAvatar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 价格卡片
|
||||
/// 价格卡片 - 重新设计
|
||||
class _PriceCard extends StatelessWidget {
|
||||
final Coin coin;
|
||||
const _PriceCard({required this.coin});
|
||||
@@ -613,48 +629,81 @@ class _PriceCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final color =
|
||||
coin.isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down;
|
||||
final bgColor = coin.isUp
|
||||
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.all(AppSpacing.lg),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg, vertical: AppSpacing.md + AppSpacing.sm),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('最新价',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
)),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'\$${coin.formattedPrice}',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
// 左侧:币种标签 + 价格
|
||||
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: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 右侧:涨跌幅
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md, vertical: AppSpacing.sm),
|
||||
horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(color: color.withOpacity(0.2)),
|
||||
color: changeBgColor,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: changeColor.withOpacity(0.15)),
|
||||
),
|
||||
child: Text(
|
||||
coin.formattedChange,
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: color, fontWeight: FontWeight.w700),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -684,7 +733,7 @@ class _PlaceholderCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易表单卡片
|
||||
/// 交易表单卡片 - 重新设计
|
||||
class _TradeFormCard extends StatelessWidget {
|
||||
final int tradeType;
|
||||
final Coin? selectedCoin;
|
||||
@@ -721,9 +770,9 @@ class _TradeFormCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 买入/卖出切换
|
||||
// 买入/卖出切换 - 重新设计
|
||||
Container(
|
||||
padding: EdgeInsets.all(AppSpacing.xs),
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
@@ -731,11 +780,25 @@ class _TradeFormCard extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _typeButton('买入', isBuy, AppColorScheme.up, () => onTradeTypeChanged(0)),
|
||||
child: _buildTypeButton(
|
||||
context: context,
|
||||
label: '买入',
|
||||
isActive: isBuy,
|
||||
color: AppColorScheme.up,
|
||||
icon: LucideIcons.trendingUp,
|
||||
onTap: () => onTradeTypeChanged(0),
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: _typeButton('卖出', !isBuy, AppColorScheme.down, () => onTradeTypeChanged(1)),
|
||||
child: _buildTypeButton(
|
||||
context: context,
|
||||
label: '卖出',
|
||||
isActive: !isBuy,
|
||||
color: AppColorScheme.down,
|
||||
icon: LucideIcons.trendingDown,
|
||||
onTap: () => onTradeTypeChanged(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -743,157 +806,188 @@ class _TradeFormCard extends StatelessWidget {
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// 交易金额输入
|
||||
Text('交易金额 (USDT)',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
)),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.3)),
|
||||
),
|
||||
child: TextField(
|
||||
controller: amountController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (_) => onAmountChanged(),
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入金额',
|
||||
hintStyle: TextStyle(
|
||||
color: colorScheme.outlineVariant.withOpacity(0.5)),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: EdgeInsets.only(right: AppSpacing.sm),
|
||||
child: Text('USDT',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
)),
|
||||
),
|
||||
suffixIconConstraints: const BoxConstraints(minWidth: 50),
|
||||
),
|
||||
),
|
||||
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: [
|
||||
_pctButton('25%', 0.25, colorScheme),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
_pctButton('50%', 0.5, colorScheme),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
_pctButton('75%', 0.75, colorScheme),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
_pctButton('全部', 1.0, colorScheme),
|
||||
_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),
|
||||
|
||||
// 预计数量
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('预计数量',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
)),
|
||||
Text(
|
||||
'$calculatedQuantity ${selectedCoin?.code ?? ''}',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
// 预计数量 + 可用余额 - 卡片样式
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
|
||||
// 可用余额
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(isBuy ? '可用 USDT' : '可用 ${selectedCoin?.code ?? ""}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
)),
|
||||
Text(
|
||||
isBuy
|
||||
? '$availableUsdt USDT'
|
||||
: '$availableCoinQty ${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 _typeButton(
|
||||
String label, bool isActive, Color color, VoidCallback onTap) {
|
||||
/// 买入/卖出切换按钮 - 实心填充 + 图标 + 光效
|
||||
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: 200),
|
||||
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? color.withOpacity(0.15) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: isActive ? null : Border.all(color: color.withOpacity(0.3)),
|
||||
color: isActive ? color : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
boxShadow: isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.35),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isActive ? color : color.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
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 _pctButton(String label, double pct, ColorScheme colorScheme) {
|
||||
/// 百分比按钮 - 药丸样式
|
||||
Widget _buildPctButton(
|
||||
String label, double pct, ColorScheme colorScheme, Color actionColor) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onFillPercent(pct),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: AppSpacing.xs + 2),
|
||||
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm - 2),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
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: colorScheme.onSurfaceVariant,
|
||||
color: actionColor.withOpacity(0.8),
|
||||
)),
|
||||
),
|
||||
),
|
||||
@@ -902,7 +996,7 @@ class _TradeFormCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易按钮
|
||||
/// 交易按钮 - 使用 NeonButton 风格
|
||||
class _TradeButton extends StatelessWidget {
|
||||
final bool isBuy;
|
||||
final String? coinCode;
|
||||
@@ -921,42 +1015,199 @@ class _TradeButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final actionColor = isBuy ? AppColorScheme.up : AppColorScheme.down;
|
||||
final gradient = isBuy
|
||||
? AppColorScheme.getBuyGradient(isDark)
|
||||
: AppColorScheme.sellGradient;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: enabled ? onPressed : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
isBuy ? AppColorScheme.up : AppColorScheme.down,
|
||||
disabledBackgroundColor: colorScheme.onSurface.withOpacity(0.12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
elevation: isBuy && enabled ? 4 : 0,
|
||||
shadowColor:
|
||||
isBuy ? AppColorScheme.up.withOpacity(0.3) : Colors.transparent,
|
||||
return GestureDetector(
|
||||
onTap: enabled ? onPressed : null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
gradient: enabled ? gradient : null,
|
||||
color: enabled ? null : colorScheme.onSurface.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xxl),
|
||||
boxShadow: enabled
|
||||
? [
|
||||
BoxShadow(
|
||||
color: actionColor.withOpacity(isDark ? 0.25 : 0.15),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: isBuy ? Colors.black87 : Colors.white,
|
||||
child: Center(
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: isBuy
|
||||
? AppColorScheme.darkOnTertiary
|
||||
: Colors.white,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isBuy ? LucideIcons.trendingUp : LucideIcons.trendingDown,
|
||||
size: 16,
|
||||
color: enabled
|
||||
? (isBuy
|
||||
? AppColorScheme.darkOnTertiary
|
||||
: Colors.white)
|
||||
: colorScheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: enabled
|
||||
? (isBuy
|
||||
? AppColorScheme.darkOnTertiary
|
||||
: Colors.white)
|
||||
: colorScheme.onSurface.withOpacity(0.3),
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isBuy ? Colors.black87 : Colors.white,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 金额输入框(含超额提示)
|
||||
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: 22,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user