diff --git a/.claude/settings.json b/.claude/settings.json index 4de4301..e513066 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -18,7 +18,8 @@ "Bash(pnpm add:*)", "Bash(flutter run:*)", "Bash(pnpm dev:*)", - "mcp__server-mysql__list_tables" + "mcp__server-mysql__list_tables", + "Bash(flutter analyze:*)" ], "additionalDirectories": [ "/Users/sion/Desktop/projects/monisuo/monisuo-admin/.git" diff --git a/flutter_monisuo/lib/data/services/asset_service.dart b/flutter_monisuo/lib/data/services/asset_service.dart index 53e6dfe..c350595 100644 --- a/flutter_monisuo/lib/data/services/asset_service.dart +++ b/flutter_monisuo/lib/data/services/asset_service.dart @@ -1,4 +1,5 @@ import '../../core/constants/api_endpoints.dart'; +import '../../core/network/api_response.dart'; import '../../core/network/dio_client.dart'; import '../models/account_models.dart'; diff --git a/flutter_monisuo/lib/data/services/fund_service.dart b/flutter_monisuo/lib/data/services/fund_service.dart index 10632ae..155fd00 100644 --- a/flutter_monisuo/lib/data/services/fund_service.dart +++ b/flutter_monisuo/lib/data/services/fund_service.dart @@ -1,4 +1,5 @@ import '../../core/constants/api_endpoints.dart'; +import '../../core/network/api_response.dart'; import '../../core/network/dio_client.dart'; import '../models/order_models.dart'; diff --git a/flutter_monisuo/lib/data/services/market_service.dart b/flutter_monisuo/lib/data/services/market_service.dart index 7e9981f..fa7855e 100644 --- a/flutter_monisuo/lib/data/services/market_service.dart +++ b/flutter_monisuo/lib/data/services/market_service.dart @@ -1,4 +1,5 @@ import '../../core/constants/api_endpoints.dart'; +import '../../core/network/api_response.dart'; import '../../core/network/dio_client.dart'; import '../models/coin.dart'; diff --git a/flutter_monisuo/lib/data/services/trade_service.dart b/flutter_monisuo/lib/data/services/trade_service.dart index 106b5e3..55f8b06 100644 --- a/flutter_monisuo/lib/data/services/trade_service.dart +++ b/flutter_monisuo/lib/data/services/trade_service.dart @@ -1,4 +1,5 @@ import '../../core/constants/api_endpoints.dart'; +import '../../core/network/api_response.dart'; import '../../core/network/dio_client.dart'; import '../models/order_models.dart'; diff --git a/flutter_monisuo/lib/data/services/user_service.dart b/flutter_monisuo/lib/data/services/user_service.dart index 0753110..607328c 100644 --- a/flutter_monisuo/lib/data/services/user_service.dart +++ b/flutter_monisuo/lib/data/services/user_service.dart @@ -1,4 +1,5 @@ import '../../core/constants/api_endpoints.dart'; +import '../../core/network/api_response.dart'; import '../../core/network/dio_client.dart'; import '../models/user.dart'; diff --git a/flutter_monisuo/lib/main.dart b/flutter_monisuo/lib/main.dart index a8b73da..f158677 100644 --- a/flutter_monisuo/lib/main.dart +++ b/flutter_monisuo/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'core/network/dio_client.dart'; @@ -135,6 +136,13 @@ class _AuthNavigatorState extends State { void _navigateToAuthPage(bool isLoggedIn) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; + + // 退出登录时重置其他 Provider 的状态 + if (!isLoggedIn) { + context.read().resetLoadState(); + context.read().resetLoadState(); + } + Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (_) => isLoggedIn ? const MainPage() : const LoginPage(), diff --git a/flutter_monisuo/lib/providers/asset_provider.dart b/flutter_monisuo/lib/providers/asset_provider.dart index 8ee7ee5..8a11d86 100644 --- a/flutter_monisuo/lib/providers/asset_provider.dart +++ b/flutter_monisuo/lib/providers/asset_provider.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +import '../core/network/api_response.dart'; +import '../core/network/dio_client.dart'; import '../data/models/account_models.dart'; import '../data/models/order_models.dart'; import '../data/services/asset_service.dart'; import '../data/services/fund_service.dart'; -import '../core/network/dio_client.dart'; /// 资产状态管理 class AssetProvider extends ChangeNotifier { @@ -20,6 +21,11 @@ class AssetProvider extends ChangeNotifier { bool _isLoadingOrders = false; String? _error; + // 加载状态标记,防止重复加载 + bool _overviewLoaded = false; + bool _fundAccountLoaded = false; + bool _tradeAccountLoaded = false; + AssetProvider(this._assetService, this._fundService); // Getters @@ -35,7 +41,12 @@ class AssetProvider extends ChangeNotifier { String? get error => _error; /// 加载资产总览 - Future loadOverview() async { + Future loadOverview({bool force = false}) async { + // 如果已经加载过且不是强制刷新,则跳过 + if (_overviewLoaded && !force && _overview != null) { + return; + } + _isLoading = true; _error = null; notifyListeners(); @@ -44,6 +55,7 @@ class AssetProvider extends ChangeNotifier { final response = await _assetService.getOverview(); if (response.success) { _overview = response.data; + _overviewLoaded = true; } else { _error = response.message; } @@ -56,11 +68,17 @@ class AssetProvider extends ChangeNotifier { } /// 加载资金账户 - Future loadFundAccount() async { + Future loadFundAccount({bool force = false}) async { + // 如果已经加载过且不是强制刷新,则跳过 + if (_fundAccountLoaded && !force && _fundAccount != null) { + return; + } + try { final response = await _assetService.getFundAccount(); if (response.success) { _fundAccount = response.data; + _fundAccountLoaded = true; notifyListeners(); } } catch (_) { @@ -69,11 +87,17 @@ class AssetProvider extends ChangeNotifier { } /// 加载交易账户 - Future loadTradeAccount() async { + Future loadTradeAccount({bool force = false}) async { + // 如果已经加载过且不是强制刷新,则跳过 + if (_tradeAccountLoaded && !force && _tradeAccounts.isNotEmpty) { + return; + } + try { final response = await _assetService.getTradeAccount(); if (response.success) { _tradeAccounts = response.data ?? []; + _tradeAccountLoaded = true; notifyListeners(); } } catch (_) { @@ -114,10 +138,10 @@ class AssetProvider extends ChangeNotifier { amount: amount, ); if (response.success) { - // 刷新数据 - await loadOverview(); - await loadFundAccount(); - await loadTradeAccount(); + // 强制刷新数据 + await loadOverview(force: true); + await loadFundAccount(force: true); + await loadTradeAccount(force: true); } return response; } catch (e) { @@ -130,8 +154,8 @@ class AssetProvider extends ChangeNotifier { try { final response = await _fundService.deposit(amount: amount, remark: remark); if (response.success) { - await loadOverview(); - await loadFundAccount(); + await loadOverview(force: true); + await loadFundAccount(force: true); } return response; } catch (e) { @@ -167,8 +191,8 @@ class AssetProvider extends ChangeNotifier { remark: remark, ); if (response.success) { - await loadOverview(); - await loadFundAccount(); + await loadOverview(force: true); + await loadFundAccount(force: true); } return response; } catch (e) { @@ -214,11 +238,25 @@ class AssetProvider extends ChangeNotifier { } /// 刷新所有资产数据 - Future refreshAll() async { + Future refreshAll({bool force = false}) async { await Future.wait([ - loadOverview(), - loadFundAccount(), - loadTradeAccount(), + loadOverview(force: force), + loadFundAccount(force: force), + loadTradeAccount(force: force), ]); } + + /// 重置加载状态(用于退出登录时) + void resetLoadState() { + _overviewLoaded = false; + _fundAccountLoaded = false; + _tradeAccountLoaded = false; + _overview = null; + _fundAccount = null; + _tradeAccounts = []; + _flows = []; + _fundOrders = []; + _error = null; + notifyListeners(); + } } diff --git a/flutter_monisuo/lib/providers/auth_provider.dart b/flutter_monisuo/lib/providers/auth_provider.dart index c00e1df..14cac21 100644 --- a/flutter_monisuo/lib/providers/auth_provider.dart +++ b/flutter_monisuo/lib/providers/auth_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../core/network/api_response.dart'; import '../core/network/dio_client.dart'; import '../core/storage/local_storage.dart'; import '../data/models/user.dart'; diff --git a/flutter_monisuo/lib/providers/market_provider.dart b/flutter_monisuo/lib/providers/market_provider.dart index 2a6c530..a1dff85 100644 --- a/flutter_monisuo/lib/providers/market_provider.dart +++ b/flutter_monisuo/lib/providers/market_provider.dart @@ -12,6 +12,7 @@ class MarketProvider extends ChangeNotifier { String _searchKeyword = ''; bool _isLoading = false; String? _error; + bool _coinsLoaded = false; // 标记是否已加载 MarketProvider(this._marketService); @@ -24,7 +25,12 @@ class MarketProvider extends ChangeNotifier { String get searchKeyword => _searchKeyword; /// 加载币种列表 - Future loadCoins() async { + Future loadCoins({bool force = false}) async { + // 如果已经加载过且不是强制刷新,则跳过 + if (_coinsLoaded && !force && _allCoins.isNotEmpty) { + return; + } + _isLoading = true; _error = null; notifyListeners(); @@ -35,6 +41,7 @@ class MarketProvider extends ChangeNotifier { if (response.success) { _allCoins = response.data ?? []; _filterCoins(); + _coinsLoaded = true; } else { _error = response.message; } @@ -100,6 +107,15 @@ class MarketProvider extends ChangeNotifier { /// 刷新 Future refresh() async { - await loadCoins(); + await loadCoins(force: true); + } + + /// 重置加载状态(用于退出登录时) + void resetLoadState() { + _coinsLoaded = false; + _allCoins = []; + _filteredCoins = []; + _error = null; + notifyListeners(); } } diff --git a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart index 3289f80..de5962f 100644 --- a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart +++ b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../providers/asset_provider.dart'; +import '../../shared/ui_constants.dart'; import '../orders/fund_orders_page.dart'; /// 资产页面 - 使用 shadcn_ui 现代化设计 @@ -17,18 +18,12 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi @override bool get wantKeepAlive => true; - int _activeTab = 0; // 0=资金账户, 1=交易账户 - - // 颜色常量 - static const upColor = Color(0xFF00C853); - static const downColor = Color(0xFFFF5252); + int _activeTab = 0; @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadData(); - }); + WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); } void _loadData() { @@ -45,20 +40,24 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi body: Consumer( builder: (context, provider, _) { return RefreshIndicator( - onRefresh: provider.refreshAll, + onRefresh: () => provider.refreshAll(force: true), color: theme.colorScheme.primary, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( children: [ - _buildAssetCard(provider), + _AssetCard(overview: provider.overview), const SizedBox(height: 16), - _buildAccountTabs(), + _TabSelector( + tabs: const ['资金账户', '交易账户'], + selectedIndex: _activeTab, + onChanged: (index) => setState(() => _activeTab = index), + ), const SizedBox(height: 16), _activeTab == 0 - ? _buildFundAccount(provider) - : _buildTradeAccount(provider), + ? _FundAccountCard(provider: provider) + : _TradeAccountCard(holdings: provider.holdings), ], ), ), @@ -67,23 +66,24 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi ), ); } +} - Widget _buildAssetCard(AssetProvider provider) { +/// 资产总览卡片 +class _AssetCard extends StatelessWidget { + final dynamic overview; + + const _AssetCard({required this.overview}); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); - final overview = provider.overview; - - // 自定义渐变色 - const gradientColors = [ - Color(0xFF00D4AA), - Color(0xFF00B894), - ]; return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: const LinearGradient( - colors: gradientColors, + colors: AppColors.gradientColors, begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -107,11 +107,7 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - LucideIcons.trendingUp, - color: Colors.white70, - size: 16, - ), + Icon(LucideIcons.trendingUp, color: Colors.white70, size: 16), const SizedBox(width: 4), Text( '总盈亏: ${overview?.totalProfit ?? '0.00'} USDT', @@ -123,8 +119,22 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi ), ); } +} - Widget _buildAccountTabs() { +/// Tab 选择器 +class _TabSelector extends StatelessWidget { + final List tabs; + final int selectedIndex; + final ValueChanged onChanged; + + const _TabSelector({ + required this.tabs, + required this.selectedIndex, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Container( @@ -134,67 +144,46 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi borderRadius: BorderRadius.circular(12), ), child: Row( - children: [ - Expanded( + children: tabs.asMap().entries.map((entry) { + final index = entry.key; + final label = entry.value; + final isSelected = index == selectedIndex; + + return Expanded( child: GestureDetector( - onTap: () => setState(() => _activeTab = 0), + onTap: () => onChanged(index), child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( - color: _activeTab == 0 - ? theme.colorScheme.primary - : Colors.transparent, + color: isSelected ? theme.colorScheme.primary : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Center( child: Text( - '资金账户', + label, style: TextStyle( - color: _activeTab == 0 - ? Colors.white - : theme.colorScheme.mutedForeground, - fontWeight: _activeTab == 0 - ? FontWeight.w600 - : FontWeight.normal, + color: isSelected ? Colors.white : theme.colorScheme.mutedForeground, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), ), ), - ), - Expanded( - child: GestureDetector( - onTap: () => setState(() => _activeTab = 1), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: _activeTab == 1 - ? theme.colorScheme.primary - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Text( - '交易账户', - style: TextStyle( - color: _activeTab == 1 - ? Colors.white - : theme.colorScheme.mutedForeground, - fontWeight: _activeTab == 1 - ? FontWeight.w600 - : FontWeight.normal, - ), - ), - ), - ), - ), - ), - ], + ); + }).toList(), ), ); } +} - Widget _buildFundAccount(AssetProvider provider) { +/// 资金账户卡片 +class _FundAccountCard extends StatelessWidget { + final AssetProvider provider; + + const _FundAccountCard({required this.provider}); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); final fund = provider.fundAccount; @@ -206,152 +195,144 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'USDT余额', - style: theme.textTheme.muted, - ), + Text('USDT余额', style: theme.textTheme.muted), GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const FundOrdersPage()), - ); - }, + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const FundOrdersPage()), + ), child: Row( children: [ - Text( - '充提记录', - style: TextStyle( - color: theme.colorScheme.primary, - fontSize: 12, - ), - ), - Icon( - LucideIcons.chevronRight, - size: 14, - color: theme.colorScheme.primary, - ), + Text('充提记录', style: TextStyle(color: theme.colorScheme.primary, fontSize: 12)), + Icon(LucideIcons.chevronRight, size: 14, color: theme.colorScheme.primary), ], ), ), ], ), const SizedBox(height: 8), - Text( - fund?.balance ?? '0.00', - style: theme.textTheme.h2.copyWith( - fontWeight: FontWeight.bold, - ), - ), + Text(fund?.balance ?? '0.00', style: theme.textTheme.h2.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 24), Row( children: [ - Expanded( - child: ShadButton( - backgroundColor: const Color(0xFF00C853), - onPressed: () => _showDepositDialog(provider), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(LucideIcons.plus, size: 18, color: Colors.white), - const SizedBox(width: 4), - const Text('充值'), - ], - ), - ), - ), + Expanded(child: _ActionButton(label: '充值', icon: LucideIcons.plus, color: AppColors.deposit, onTap: () => _showDepositDialog(context))), const SizedBox(width: 12), - Expanded( - child: ShadButton( - backgroundColor: const Color(0xFFFF9800), - onPressed: () => _showWithdrawDialog(provider), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(LucideIcons.minus, size: 18, color: Colors.white), - const SizedBox(width: 4), - const Text('提现'), - ], - ), - ), - ), + Expanded(child: _ActionButton(label: '提现', icon: LucideIcons.minus, color: AppColors.withdraw, onTap: () => _showWithdrawDialog(context, fund?.balance))), const SizedBox(width: 12), - Expanded( - child: ShadButton.outline( - onPressed: () => _showTransferDialog(provider), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(LucideIcons.arrowRightLeft, size: 18), - const SizedBox(width: 4), - const Text('划转'), - ], - ), - ), - ), + Expanded(child: _ActionButton.outline(label: '划转', icon: LucideIcons.arrowRightLeft, onTap: () => _showTransferDialog(context))), ], ), ], ), ); } +} - Widget _buildTradeAccount(AssetProvider provider) { +/// 操作按钮 +class _ActionButton extends StatelessWidget { + final String label; + final IconData icon; + final Color? color; + final bool isOutline; + final VoidCallback onTap; + + const _ActionButton({ + required this.label, + required this.icon, + required this.color, + required this.onTap, + }) : isOutline = false; + + const _ActionButton.outline({ + required this.label, + required this.icon, + required this.onTap, + }) : color = null, isOutline = true; + + @override + Widget build(BuildContext context) { + final child = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 18, color: isOutline ? null : Colors.white), + const SizedBox(width: 4), + Text(label), + ], + ); + + return isOutline + ? ShadButton.outline(onPressed: onTap, child: child) + : ShadButton(backgroundColor: color, onPressed: onTap, child: child); + } +} + +/// 交易账户卡片 +class _TradeAccountCard extends StatelessWidget { + final List holdings; + + const _TradeAccountCard({required this.holdings}); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); - final holdings = provider.holdings; return ShadCard( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '持仓列表', - style: theme.textTheme.large.copyWith( - fontWeight: FontWeight.bold, - ), - ), + Text('持仓列表', style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 16), if (holdings.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Icon( - LucideIcons.wallet, - size: 48, - color: theme.colorScheme.mutedForeground, - ), - const SizedBox(height: 12), - Text( - '暂无持仓', - style: theme.textTheme.muted, - ), - ], - ), - ), - ) + const _EmptyState(icon: LucideIcons.wallet, message: '暂无持仓') else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: holdings.length, - separatorBuilder: (_, __) => Divider( - color: theme.colorScheme.border, - height: 1, - ), - itemBuilder: (context, index) { - final holding = holdings[index]; - return _buildHoldingItem(holding); - }, + separatorBuilder: (_, __) => Divider(color: theme.colorScheme.border, height: 1), + itemBuilder: (context, index) => _HoldingItem(holding: holdings[index]), ), ], ), ); } +} - Widget _buildHoldingItem(holding) { +/// 空状态 +class _EmptyState extends StatelessWidget { + final IconData icon; + final String message; + + const _EmptyState({required this.icon, required this.message}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Icon(icon, size: 48, color: theme.colorScheme.mutedForeground), + const SizedBox(height: 12), + Text(message, style: theme.textTheme.muted), + ], + ), + ), + ); + } +} + +/// 持仓项 +class _HoldingItem extends StatelessWidget { + final dynamic holding; + + const _HoldingItem({required this.holding}); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Padding( @@ -363,10 +344,7 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), child: Text( holding.coinCode.substring(0, 1), - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.bold), ), ), const SizedBox(width: 12), @@ -374,30 +352,19 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - holding.coinCode, - style: theme.textTheme.large.copyWith( - fontWeight: FontWeight.w600, - ), - ), - Text( - '数量: ${holding.quantity}', - style: theme.textTheme.muted.copyWith(fontSize: 12), - ), + Text(holding.coinCode, style: theme.textTheme.large.copyWith(fontWeight: FontWeight.w600)), + Text('数量: ${holding.quantity}', style: theme.textTheme.muted.copyWith(fontSize: 12)), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - '${holding.currentValue} USDT', - style: theme.textTheme.small, - ), + Text('${holding.currentValue} USDT', style: theme.textTheme.small), Text( holding.formattedProfitRate, style: TextStyle( - color: holding.isProfit ? upColor : downColor, + color: holding.isProfit ? AppColors.up : AppColors.down, fontSize: 12, ), ), @@ -407,358 +374,309 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi ), ); } +} - void _showDepositDialog(AssetProvider provider) { - final amountController = TextEditingController(); - final formKey = GlobalKey(); +// Dialogs +void _showDepositDialog(BuildContext context) { + final amountController = TextEditingController(); + final formKey = GlobalKey(); - showShadDialog( - context: context, - builder: (context) => ShadDialog( - title: const Text('充值'), - child: ShadForm( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, + showShadDialog( + context: context, + builder: (ctx) => ShadDialog( + title: const Text('充值'), + child: ShadForm( + key: formKey, + child: ShadInputFormField( + id: 'amount', + controller: amountController, + placeholder: const Text('请输入充值金额(USDT)'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + ), + actions: [ + ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), + ShadButton( + child: const Text('下一步'), + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().deposit(amount: amountController.text); + if (context.mounted) { + if (response.success && response.data != null) { + _showDepositResultDialog(context, response.data!); + } else { + _showResultDialog(context, '申请失败', response.message); + } + } + } + }, + ), + ], + ), + ); +} + +void _showDepositResultDialog(BuildContext context, Map data) { + final orderNo = data['orderNo'] as String? ?? ''; + final amount = data['amount']?.toString() ?? '0.00'; + final walletAddress = data['walletAddress'] as String? ?? ''; + final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20'; + + showShadDialog( + context: context, + builder: (ctx) => ShadDialog( + title: const Text('充值申请成功'), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('订单号: $orderNo', style: const TextStyle(fontSize: 12)), + const SizedBox(height: 8), + Text('充值金额: $amount USDT', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + const Text('请向以下地址转账:', style: TextStyle(fontSize: 12)), + const SizedBox(height: 8), + _WalletAddressCard(address: walletAddress, network: walletNetwork), + const SizedBox(height: 12), + const Text('转账完成后请点击"已打款"按钮确认', style: TextStyle(fontSize: 12, color: Colors.orange)), + ], + ), + actions: [ + ShadButton.outline(child: const Text('稍后确认'), onPressed: () => Navigator.of(ctx).pop()), + ShadButton( + child: const Text('已打款'), + onPressed: () async { + Navigator.of(ctx).pop(); + final response = await context.read().confirmPay(orderNo); + if (context.mounted) { + _showResultDialog( + context, + response.success ? '确认成功' : '确认失败', + response.success ? '请等待管理员审核' : response.message, + ); + } + }, + ), + ], + ), + ); +} + +class _WalletAddressCard extends StatelessWidget { + final String address; + final String network; + + const _WalletAddressCard({required this.address, required this.network}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - ShadInputFormField( - id: 'amount', - controller: amountController, - placeholder: const Text('请输入充值金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入金额'; - } - final amount = double.tryParse(value); - if (amount == null || amount <= 0) { - return '请输入有效金额'; - } - return null; + Expanded( + child: Text(address, style: const TextStyle(fontFamily: 'monospace', fontSize: 12)), + ), + IconButton( + icon: const Icon(LucideIcons.copy, size: 18), + onPressed: () { + Clipboard.setData(ClipboardData(text: address)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('地址已复制到剪贴板')), + ); }, + tooltip: '复制地址', ), ], ), - ), - actions: [ - ShadButton.outline( - child: const Text('取消'), - onPressed: () => Navigator.of(context).pop(), - ), - ShadButton( - child: const Text('下一步'), - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(context).pop(); - // 提交充值申请 - final response = await provider.deposit(amount: amountController.text); - if (mounted) { - if (response.success && response.data != null) { - // 显示钱包地址 - _showDepositResultDialog(response.data!); - } else { - _showResult('申请失败', response.message); - } - } - } - }, - ), - ], - ), - ); - } - - /// 显示充值结果 - 包含钱包地址 - void _showDepositResultDialog(Map data) { - final orderNo = data['orderNo'] as String? ?? ''; - final amount = data['amount']?.toString() ?? '0.00'; - final walletAddress = data['walletAddress'] as String? ?? ''; - final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20'; - final assetProvider = context.read(); - - showShadDialog( - context: context, - builder: (context) => ShadDialog( - title: const Text('充值申请成功'), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('订单号: $orderNo', style: const TextStyle(fontSize: 12)), - const SizedBox(height: 8), - Text('充值金额: $amount USDT', style: const TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 16), - const Text('请向以下地址转账:', style: TextStyle(fontSize: 12)), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - walletAddress, - style: const TextStyle(fontFamily: 'monospace', fontSize: 12), - ), - ), - IconButton( - icon: const Icon(LucideIcons.copy, size: 18), - onPressed: () { - Clipboard.setData(ClipboardData(text: walletAddress)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('地址已复制到剪贴板')), - ); - }, - tooltip: '复制地址', - ), - ], - ), - const SizedBox(height: 4), - Text('网络: $walletNetwork', style: const TextStyle(fontSize: 12, color: Colors.grey)), - ], - ), - ), - const SizedBox(height: 12), - const Text( - '转账完成后请点击"已打款"按钮确认', - style: TextStyle(fontSize: 12, color: Colors.orange), - ), - ], - ), - actions: [ - ShadButton.outline( - child: const Text('稍后确认'), - onPressed: () => Navigator.of(context).pop(), - ), - ShadButton( - child: const Text('已打款'), - onPressed: () async { - Navigator.of(context).pop(); - final response = await assetProvider.confirmPay(orderNo); - if (mounted) { - _showResult( - response.success ? '确认成功' : '确认失败', - response.success ? '请等待管理员审核' : response.message, - ); - } - }, - ), - ], - ), - ); - } - - void _showWithdrawDialog(AssetProvider provider) { - final amountController = TextEditingController(); - final addressController = TextEditingController(); - final contactController = TextEditingController(); - final formKey = GlobalKey(); - final fund = provider.fundAccount; - - showShadDialog( - context: context, - builder: (context) => ShadDialog( - title: const Text('提现'), - child: SingleChildScrollView( - child: ShadForm( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (fund != null) - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Text( - '可用余额: ${fund.balance} USDT', - style: const TextStyle(color: Colors.grey), - ), - ), - ShadInputFormField( - id: 'amount', - controller: amountController, - placeholder: const Text('请输入提现金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入金额'; - } - final amount = double.tryParse(value); - if (amount == null || amount <= 0) { - return '请输入有效金额'; - } - return null; - }, - ), - const SizedBox(height: 12), - ShadInputFormField( - id: 'address', - controller: addressController, - placeholder: const Text('请输入提现地址'), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入提现地址'; - } - return null; - }, - ), - const SizedBox(height: 12), - ShadInputFormField( - id: 'contact', - controller: contactController, - placeholder: const Text('联系方式(可选)'), - ), - ], - ), - ), - ), - actions: [ - ShadButton.outline( - child: const Text('取消'), - onPressed: () => Navigator.of(context).pop(), - ), - ShadButton( - child: const Text('提交'), - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(context).pop(); - final response = await provider.withdraw( - amount: amountController.text, - withdrawAddress: addressController.text, - withdrawContact: contactController.text.isNotEmpty ? contactController.text : null, - ); - if (mounted) { - _showResult( - response.success ? '申请成功' : '申请失败', - response.success ? '请等待管理员审批' : response.message, - ); - } - } - }, - ), - ], - ), - ); - } - - void _showTransferDialog(AssetProvider provider) { - final controller = TextEditingController(); - final formKey = GlobalKey(); - int direction = 1; - - showShadDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setState) => ShadDialog( - title: const Text('划转'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ShadButton.outline( - size: ShadButtonSize.sm, - onPressed: () => setState(() => direction = 1), - child: Row( - children: [ - if (direction == 1) - Icon(LucideIcons.check, size: 14) - else - const SizedBox(width: 14), - const SizedBox(width: 4), - const Text('资金→交易'), - ], - ), - ), - const SizedBox(width: 8), - ShadButton.outline( - size: ShadButtonSize.sm, - onPressed: () => setState(() => direction = 2), - child: Row( - children: [ - if (direction == 2) - Icon(LucideIcons.check, size: 14) - else - const SizedBox(width: 14), - const SizedBox(width: 4), - const Text('交易→资金'), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - ShadForm( - key: formKey, - child: ShadInputFormField( - id: 'amount', - controller: controller, - placeholder: const Text('请输入划转金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入金额'; - } - final amount = double.tryParse(value); - if (amount == null || amount <= 0) { - return '请输入有效金额'; - } - return null; - }, - ), - ), - ], - ), - actions: [ - ShadButton.outline( - child: const Text('取消'), - onPressed: () => Navigator.of(context).pop(), - ), - ShadButton( - child: const Text('确认'), - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(context).pop(); - final response = await provider.transfer( - direction: direction, - amount: controller.text, - ); - if (mounted) { - _showResult( - response.success ? '划转成功' : '划转失败', - response.message, - ); - } - } - }, - ), - ], - ), - ), - ); - } - - void _showResult(String title, String? message) { - showShadDialog( - context: context, - builder: (context) => ShadDialog.alert( - title: Text(title), - description: message != null ? Text(message) : null, - actions: [ - ShadButton( - child: const Text('确定'), - onPressed: () => Navigator.of(context).pop(), - ), + const SizedBox(height: 4), + Text('网络: $network', style: const TextStyle(fontSize: 12, color: Colors.grey)), ], ), ); } } + +void _showWithdrawDialog(BuildContext context, String? balance) { + final amountController = TextEditingController(); + final addressController = TextEditingController(); + final contactController = TextEditingController(); + final formKey = GlobalKey(); + + showShadDialog( + context: context, + builder: (ctx) => ShadDialog( + title: const Text('提现'), + child: SingleChildScrollView( + child: ShadForm( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (balance != null) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text('可用余额: $balance USDT', style: const TextStyle(color: Colors.grey)), + ), + ShadInputFormField( + id: 'amount', + controller: amountController, + placeholder: const Text('请输入提现金额(USDT)'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + const SizedBox(height: 12), + ShadInputFormField( + id: 'address', + controller: addressController, + placeholder: const Text('请输入提现地址'), + validator: (v) => Validators.required(v, '提现地址'), + ), + const SizedBox(height: 12), + ShadInputFormField( + id: 'contact', + controller: contactController, + placeholder: const Text('联系方式(可选)'), + ), + ], + ), + ), + ), + actions: [ + ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), + ShadButton( + child: const Text('提交'), + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().withdraw( + amount: amountController.text, + withdrawAddress: addressController.text, + withdrawContact: contactController.text.isNotEmpty ? contactController.text : null, + ); + if (context.mounted) { + _showResultDialog( + context, + response.success ? '申请成功' : '申请失败', + response.success ? '请等待管理员审批' : response.message, + ); + } + } + }, + ), + ], + ), + ); +} + +void _showTransferDialog(BuildContext context) { + final controller = TextEditingController(); + final formKey = GlobalKey(); + int direction = 1; + + showShadDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setState) => ShadDialog( + title: const Text('划转'), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _DirectionButton( + label: '资金→交易', + isSelected: direction == 1, + onTap: () => setState(() => direction = 1), + ), + const SizedBox(width: 8), + _DirectionButton( + label: '交易→资金', + isSelected: direction == 2, + onTap: () => setState(() => direction = 2), + ), + ], + ), + const SizedBox(height: 16), + ShadForm( + key: formKey, + child: ShadInputFormField( + id: 'amount', + controller: controller, + placeholder: const Text('请输入划转金额(USDT)'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + ), + ], + ), + actions: [ + ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), + ShadButton( + child: const Text('确认'), + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().transfer( + direction: direction, + amount: controller.text, + ); + if (context.mounted) { + _showResultDialog(context, response.success ? '划转成功' : '划转失败', response.message); + } + } + }, + ), + ], + ), + ), + ); +} + +class _DirectionButton extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + + const _DirectionButton({required this.label, required this.isSelected, required this.onTap}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + size: ShadButtonSize.sm, + onPressed: onTap, + child: Row( + children: [ + if (isSelected) const Icon(LucideIcons.check, size: 14) else const SizedBox(width: 14), + const SizedBox(width: 4), + Text(label), + ], + ), + ); + } +} + +void _showResultDialog(BuildContext context, String title, String? message) { + showShadDialog( + context: context, + builder: (ctx) => ShadDialog.alert( + title: Text(title), + description: message != null ? Text(message) : null, + actions: [ + ShadButton(child: const Text('确定'), onPressed: () => Navigator.of(ctx).pop()), + ], + ), + ); +} diff --git a/flutter_monisuo/lib/ui/pages/home/home_page.dart b/flutter_monisuo/lib/ui/pages/home/home_page.dart index de09048..175ab2f 100644 --- a/flutter_monisuo/lib/ui/pages/home/home_page.dart +++ b/flutter_monisuo/lib/ui/pages/home/home_page.dart @@ -3,6 +3,7 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../providers/asset_provider.dart'; import '../../../providers/auth_provider.dart'; +import '../../shared/ui_constants.dart'; /// 首页 - 使用 shadcn_ui 现代化设计 class HomePage extends StatefulWidget { @@ -19,15 +20,13 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadData(); - }); + WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); } void _loadData() { - final assetProvider = context.read(); - assetProvider.loadOverview(); - assetProvider.loadTradeAccount(); + final provider = context.read(); + provider.loadOverview(); + provider.loadTradeAccount(); } @override @@ -40,7 +39,7 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin body: Consumer( builder: (context, provider, _) { return RefreshIndicator( - onRefresh: () => provider.refreshAll(), + onRefresh: () => provider.refreshAll(force: true), color: theme.colorScheme.primary, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -49,13 +48,18 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), - _buildHeader(), + _Header(), const SizedBox(height: 20), - _buildAssetCard(provider), + _AssetOverviewCard(overview: provider.overview), const SizedBox(height: 16), - _buildQuickActions(), + _QuickActions( + onDeposit: _showDeposit, + onWithdraw: _showWithdraw, + onTransfer: _showTransfer, + onTrade: _navigateToTrade, + ), const SizedBox(height: 24), - _buildHoldings(provider), + _HoldingsList(holdings: provider.holdings), ], ), ), @@ -65,7 +69,115 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin ); } - Widget _buildHeader() { + void _showDeposit() => _showAmountDialog('充值', (amount) { + context.read().deposit(amount: amount); + }); + + void _showWithdraw() { + final amountController = TextEditingController(); + final addressController = TextEditingController(); + final contactController = TextEditingController(); + final formKey = GlobalKey(); + + showShadDialog( + context: context, + builder: (ctx) => ShadDialog( + title: const Text('提现'), + child: ShadForm( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ShadInputFormField( + id: 'amount', + placeholder: const Text('请输入提现金额(USDT)'), + controller: amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + const SizedBox(height: 12), + ShadInputFormField( + id: 'address', + placeholder: const Text('请输入提现地址'), + controller: addressController, + validator: (v) => Validators.required(v, '提现地址'), + ), + const SizedBox(height: 12), + ShadInputFormField( + id: 'contact', + placeholder: const Text('联系方式(可选)'), + controller: contactController, + ), + ], + ), + ), + actions: [ + ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), + ShadButton( + child: const Text('确认'), + onPressed: () { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + context.read().withdraw( + amount: amountController.text.trim(), + withdrawAddress: addressController.text.trim(), + withdrawContact: contactController.text.trim().isEmpty ? null : contactController.text.trim(), + ); + } + }, + ), + ], + ), + ); + } + + void _showTransfer() => _showAmountDialog('划转', (amount) { + context.read().transfer(direction: 1, amount: amount); + }); + + void _showAmountDialog(String title, Function(String) onSubmit) { + final controller = TextEditingController(); + final formKey = GlobalKey(); + + showShadDialog( + context: context, + builder: (ctx) => ShadDialog( + title: Text(title), + child: ShadForm( + key: formKey, + child: ShadInputFormField( + id: 'amount', + placeholder: Text('请输入${title}金额(USDT)'), + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + ), + actions: [ + ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), + ShadButton( + child: const Text('确认'), + onPressed: () { + if (formKey.currentState!.saveAndValidate()) { + onSubmit(controller.text); + Navigator.of(ctx).pop(); + } + }, + ), + ], + ), + ); + } + + void _navigateToTrade() { + // 切换到交易页 - 通过 MainController + } +} + +/// 头部组件 +class _Header extends StatelessWidget { + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Consumer( @@ -73,17 +185,7 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin final user = auth.user; return Row( children: [ - CircleAvatar( - radius: 20, - backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2), - child: Text( - user?.avatarText ?? 'U', - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - ), + _Avatar(text: user?.avatarText), const SizedBox(width: 12), Expanded( child: Column( @@ -91,15 +193,10 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin children: [ Text( '你好,${user?.username ?? '用户'}', - style: theme.textTheme.large.copyWith( - fontWeight: FontWeight.bold, - ), + style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 2), - Text( - '欢迎来到模拟所', - style: theme.textTheme.muted, - ), + Text('欢迎来到模拟所', style: theme.textTheme.muted), ], ), ), @@ -108,23 +205,45 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin }, ); } +} - Widget _buildAssetCard(AssetProvider provider) { +/// 头像组件 +class _Avatar extends StatelessWidget { + final String? text; + + const _Avatar({this.text}); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); - final overview = provider.overview; - // 自定义渐变色 - const gradientColors = [ - Color(0xFF00D4AA), - Color(0xFF00B894), - ]; + return CircleAvatar( + radius: 20, + backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2), + child: Text( + text ?? 'U', + style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.bold), + ), + ); + } +} + +/// 资产总览卡片 +class _AssetOverviewCard extends StatelessWidget { + final dynamic overview; + + const _AssetOverviewCard({required this.overview}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( - colors: gradientColors, + colors: AppColors.gradientColors, begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -133,95 +252,95 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - '总资产(USDT)', - style: theme.textTheme.small.copyWith(color: Colors.white70), - ), + Text('总资产(USDT)', style: theme.textTheme.small.copyWith(color: Colors.white70)), const SizedBox(height: 8), Text( overview?.totalAsset ?? '0.00', - style: theme.textTheme.h2.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), + style: theme.textTheme.h2.copyWith(color: Colors.white, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildAssetItem('资金账户', overview?.fundBalance ?? '0.00'), - _buildAssetItem('交易账户', overview?.tradeBalance ?? '0.00'), + _AssetItem(label: '资金账户', value: overview?.fundBalance ?? '0.00'), + _AssetItem(label: '交易账户', value: overview?.tradeBalance ?? '0.00'), ], ), ], ), ).animate().fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0); } +} - Widget _buildAssetItem(String label, String value) { +/// 资产项 +class _AssetItem extends StatelessWidget { + final String label; + final String value; + + const _AssetItem({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: const TextStyle(fontSize: 12, color: Colors.white70), - ), + Text(label, style: const TextStyle(fontSize: 12, color: Colors.white70)), const SizedBox(height: 4), - Text( - value, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), + Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white)), ], ); } +} - Widget _buildQuickActions() { - final theme = ShadTheme.of(context); +/// 快捷操作 +class _QuickActions extends StatelessWidget { + final VoidCallback onDeposit; + final VoidCallback onWithdraw; + final VoidCallback onTransfer; + final VoidCallback onTrade; + const _QuickActions({ + required this.onDeposit, + required this.onWithdraw, + required this.onTransfer, + required this.onTrade, + }); + + @override + Widget build(BuildContext context) { return ShadCard( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildActionItem( - icon: LucideIcons.arrowDownToLine, - text: '充值', - color: const Color(0xFF00C853), - onTap: () => _showDeposit(), - ), - _buildActionItem( - icon: LucideIcons.arrowUpFromLine, - text: '提现', - color: const Color(0xFFFF9800), - onTap: () => _showWithdraw(), - ), - _buildActionItem( - icon: LucideIcons.arrowRightLeft, - text: '划转', - color: theme.colorScheme.primary, - onTap: () => _showTransfer(), - ), - _buildActionItem( - icon: LucideIcons.trendingUp, - text: '交易', - color: const Color(0xFF2196F3), - onTap: () => _navigateToTrade(), - ), + _ActionButton(icon: LucideIcons.arrowDownToLine, text: '充值', color: AppColors.deposit, onTap: onDeposit), + _ActionButton(icon: LucideIcons.arrowUpFromLine, text: '提现', color: AppColors.withdraw, onTap: onWithdraw), + _ActionButton(icon: LucideIcons.arrowRightLeft, text: '划转', color: AppColors.trade, onTap: onTransfer), + _ActionButton(icon: LucideIcons.trendingUp, text: '交易', color: AppColors.trade, onTap: onTrade), ], ), ).animate().fadeIn(duration: 500.ms, delay: 100.ms); } +} + +/// 操作按钮 +class _ActionButton extends StatelessWidget { + final IconData icon; + final String text; + final Color color; + final VoidCallback onTap; + + const _ActionButton({ + required this.icon, + required this.text, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); - Widget _buildActionItem({ - required IconData icon, - required String text, - required Color color, - required VoidCallback onTap, - }) { return GestureDetector( onTap: onTap, child: Column( @@ -229,28 +348,26 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin Container( width: 48, height: 48, - decoration: BoxDecoration( - color: color.withOpacity(0.15), - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: color.withOpacity(0.15), shape: BoxShape.circle), child: Icon(icon, color: color, size: 22), ), const SizedBox(height: 8), - Text( - text, - style: TextStyle( - fontSize: 12, - color: ShadTheme.of(context).colorScheme.foreground, - ), - ), + Text(text, style: TextStyle(fontSize: 12, color: theme.colorScheme.foreground)), ], ), ); } +} - Widget _buildHoldings(AssetProvider provider) { +/// 持仓列表 +class _HoldingsList extends StatelessWidget { + final List holdings; + + const _HoldingsList({required this.holdings}); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); - final holdings = provider.holdings; return ShadCard( padding: const EdgeInsets.all(16), @@ -260,70 +377,78 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '我的持仓', - style: theme.textTheme.large.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Icon( - LucideIcons.chevronRight, - color: theme.colorScheme.mutedForeground, - size: 20, - ), + Text('我的持仓', style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold)), + Icon(LucideIcons.chevronRight, color: theme.colorScheme.mutedForeground, size: 20), ], ), const SizedBox(height: 16), if (holdings.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Icon( - LucideIcons.wallet, - size: 48, - color: theme.colorScheme.mutedForeground, - ), - const SizedBox(height: 12), - Text( - '暂无持仓', - style: theme.textTheme.muted, - ), - const SizedBox(height: 4), - Text( - '快去交易吧~', - style: theme.textTheme.muted.copyWith(fontSize: 12), - ), - ], - ), - ), - ) + _EmptyHoldings() else - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: holdings.length > 5 ? 5 : holdings.length, - separatorBuilder: (_, __) => Divider( - color: theme.colorScheme.border, - height: 1, - ), - itemBuilder: (context, index) { - final holding = holdings[index]; - return _buildHoldingItem(holding) - .animate() - .fadeIn(delay: Duration(milliseconds: 50 * index)); - }, - ), + _HoldingsListView(holdings: holdings), ], ), ).animate().fadeIn(duration: 500.ms, delay: 200.ms); } +} - Widget _buildHoldingItem(holding) { +/// 空持仓提示 +class _EmptyHoldings extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Icon(LucideIcons.wallet, size: 48, color: theme.colorScheme.mutedForeground), + const SizedBox(height: 12), + Text('暂无持仓', style: theme.textTheme.muted), + const SizedBox(height: 4), + Text('快去交易吧~', style: theme.textTheme.muted.copyWith(fontSize: 12)), + ], + ), + ), + ); + } +} + +/// 持仓列表视图 +class _HoldingsListView extends StatelessWidget { + final List holdings; + + const _HoldingsListView({required this.holdings}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final displayHoldings = holdings.length > 5 ? holdings.sublist(0, 5) : holdings; + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: displayHoldings.length, + separatorBuilder: (_, __) => Divider(color: theme.colorScheme.border, height: 1), + itemBuilder: (context, index) { + return _HoldingItem(holding: displayHoldings[index]) + .animate() + .fadeIn(delay: Duration(milliseconds: 50 * index)); + }, + ); + } +} + +/// 持仓项 +class _HoldingItem extends StatelessWidget { + final dynamic holding; + + const _HoldingItem({required this.holding}); + + @override + Widget build(BuildContext context) { final theme = ShadTheme.of(context); - final upColor = const Color(0xFF00C853); - final downColor = const Color(0xFFFF5252); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -337,26 +462,15 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), child: Text( holding.coinCode.substring(0, 1), - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.bold), ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - holding.coinCode, - style: theme.textTheme.large.copyWith( - fontWeight: FontWeight.bold, - ), - ), - Text( - holding.quantity, - style: theme.textTheme.muted, - ), + Text(holding.coinCode, style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold)), + Text(holding.quantity, style: theme.textTheme.muted), ], ), ], @@ -364,16 +478,11 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - '${holding.currentValue} USDT', - style: theme.textTheme.small.copyWith( - fontWeight: FontWeight.w500, - ), - ), + Text('${holding.currentValue} USDT', style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w500)), Text( holding.formattedProfitRate, style: TextStyle( - color: holding.isProfit ? upColor : downColor, + color: holding.isProfit ? AppColors.up : AppColors.down, fontSize: 12, ), ), @@ -383,144 +492,4 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin ), ); } - - void _showDeposit() { - _showActionDialog('充值', '请输入充值金额(USDT)', (amount) { - context.read().deposit(amount: amount); - }); - } - - void _showWithdraw() { - final amountController = TextEditingController(); - final addressController = TextEditingController(); - final contactController = TextEditingController(); - final formKey = GlobalKey(); - - showShadDialog( - context: context, - builder: (context) => ShadDialog( - title: const Text('提现'), - child: ShadForm( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ShadInputFormField( - id: 'amount', - placeholder: const Text('请输入提现金额(USDT)'), - controller: amountController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入金额'; - } - final amount = double.tryParse(value); - if (amount == null || amount <= 0) { - return '请输入有效金额'; - } - return null; - }, - ), - const SizedBox(height: 12), - ShadInputFormField( - id: 'address', - placeholder: const Text('请输入提现地址'), - controller: addressController, - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入提现地址'; - } - return null; - }, - ), - const SizedBox(height: 12), - ShadInputFormField( - id: 'contact', - placeholder: const Text('联系方式(可选)'), - controller: contactController, - ), - ], - ), - ), - actions: [ - ShadButton.outline( - child: const Text('取消'), - onPressed: () => Navigator.of(context).pop(), - ), - ShadButton( - child: const Text('确认'), - onPressed: () { - if (formKey.currentState!.saveAndValidate()) { - final amount = amountController.text.trim(); - final address = addressController.text.trim(); - final contact = contactController.text.trim(); - Navigator.of(context).pop(); - context.read().withdraw( - amount: amount, - withdrawAddress: address, - withdrawContact: contact.isEmpty ? null : contact, - ); - } - }, - ), - ], - ), - ); - } - - void _showTransfer() { - _showActionDialog('划转', '请输入划转金额(USDT)', (amount) { - context.read().transfer(direction: 1, amount: amount); - }); - } - - void _showActionDialog(String title, String hint, Function(String) onSubmit) { - final controller = TextEditingController(); - final formKey = GlobalKey(); - - showShadDialog( - context: context, - builder: (context) => ShadDialog( - title: Text(title), - child: ShadForm( - key: formKey, - child: ShadInputFormField( - id: 'amount', - placeholder: Text(hint), - controller: controller, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入金额'; - } - final amount = double.tryParse(value); - if (amount == null || amount <= 0) { - return '请输入有效金额'; - } - return null; - }, - ), - ), - actions: [ - ShadButton.outline( - child: const Text('取消'), - onPressed: () => Navigator.of(context).pop(), - ), - ShadButton( - child: const Text('确认'), - onPressed: () { - if (formKey.currentState!.saveAndValidate()) { - onSubmit(controller.text); - Navigator.of(context).pop(); - } - }, - ), - ], - ), - ); - } - - void _navigateToTrade() { - // 切换到交易页 - 通过 MainController - } } diff --git a/flutter_monisuo/lib/ui/pages/main/main_page.dart b/flutter_monisuo/lib/ui/pages/main/main_page.dart index b78f544..12f94c9 100644 --- a/flutter_monisuo/lib/ui/pages/main/main_page.dart +++ b/flutter_monisuo/lib/ui/pages/main/main_page.dart @@ -6,6 +6,15 @@ import '../trade/trade_page.dart'; import '../asset/asset_page.dart'; import '../mine/mine_page.dart'; +/// 底部导航项 +class _NavItem { + final String label; + final IconData icon; + final Widget page; + + const _NavItem({required this.label, required this.icon, required this.page}); +} + /// 主页面(使用 shadcn_ui 风格) class MainPage extends StatefulWidget { const MainPage({super.key}); @@ -16,81 +25,75 @@ class MainPage extends StatefulWidget { class _MainPageState extends State { int _currentIndex = 0; + final Set _loadedPages = {0}; - final List _pages = [ - const HomePage(), - const MarketPage(), - const TradePage(), - const AssetPage(), - const MinePage(), + static const _navItems = [ + _NavItem(label: '首页', icon: LucideIcons.house, page: HomePage()), + _NavItem(label: '行情', icon: LucideIcons.trendingUp, page: MarketPage()), + _NavItem(label: '交易', icon: LucideIcons.arrowLeftRight, page: TradePage()), + _NavItem(label: '资产', icon: LucideIcons.wallet, page: AssetPage()), + _NavItem(label: '我的', icon: LucideIcons.user, page: MinePage()), ]; - final List<_TabItem> _tabs = [ - _TabItem('首页', LucideIcons.house, LucideIcons.house), - _TabItem('行情', LucideIcons.trendingUp, LucideIcons.trendingUp), - _TabItem('交易', LucideIcons.arrowLeftRight, LucideIcons.arrowLeftRight), - _TabItem('资产', LucideIcons.wallet, LucideIcons.wallet), - _TabItem('我的', LucideIcons.user, LucideIcons.user), - ]; + void _onTabChanged(int index) { + setState(() { + _currentIndex = index; + _loadedPages.add(index); + }); + } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Scaffold( - body: IndexedStack( + body: LazyIndexedStack( index: _currentIndex, - children: _pages, + loadedIndexes: _loadedPages, + children: _navItems.map((item) => item.page).toList(), ), - bottomNavigationBar: Container( - decoration: BoxDecoration( - color: theme.colorScheme.background, - border: Border( - top: BorderSide(color: theme.colorScheme.border), - ), - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: _tabs.asMap().entries.map((entry) { - final index = entry.key; - final tab = entry.value; - final isSelected = index == _currentIndex; + bottomNavigationBar: _BottomNavBar( + items: _navItems, + currentIndex: _currentIndex, + onTap: _onTabChanged, + ), + ); + } +} - return GestureDetector( - onTap: () => setState(() => _currentIndex = index), - behavior: HitTestBehavior.opaque, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - tab.icon, - color: isSelected - ? theme.colorScheme.primary - : theme.colorScheme.mutedForeground, - size: 24, - ), - const SizedBox(height: 4), - Text( - tab.label, - style: TextStyle( - fontSize: 12, - color: isSelected - ? theme.colorScheme.primary - : theme.colorScheme.mutedForeground, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - ), - ), - ], - ), - ), - ); - }).toList(), - ), +/// 底部导航栏 +class _BottomNavBar extends StatelessWidget { + final List<_NavItem> items; + final int currentIndex; + final ValueChanged onTap; + + const _BottomNavBar({ + required this.items, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.background, + border: Border(top: BorderSide(color: theme.colorScheme.border)), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: items.asMap().entries.map((entry) { + return _NavItemWidget( + item: entry.value, + isSelected: entry.key == currentIndex, + onTap: () => onTap(entry.key), + ); + }).toList(), ), ), ), @@ -98,40 +101,79 @@ class _MainPageState extends State { } } -class _TabItem { - final String label; - final IconData icon; - final IconData selectedIcon; +/// 导航项组件 +class _NavItemWidget extends StatelessWidget { + final _NavItem item; + final bool isSelected; + final VoidCallback onTap; - _TabItem(this.label, this.icon, this.selectedIcon); + const _NavItemWidget({ + required this.item, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final color = isSelected ? theme.colorScheme.primary : theme.colorScheme.mutedForeground; + + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(item.icon, color: color, size: 24), + const SizedBox(height: 4), + Text( + item.label, + style: TextStyle( + fontSize: 12, + color: color, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } } -/// IndexedStack 用于保持页面状态 -class IndexedStack extends StatefulWidget { +/// 懒加载 IndexedStack - 只渲染已访问过的页面 +class LazyIndexedStack extends StatefulWidget { final int index; + final Set loadedIndexes; final List children; - const IndexedStack({ + const LazyIndexedStack({ super.key, required this.index, + required this.loadedIndexes, required this.children, }); @override - State createState() => _IndexedStackState(); + State createState() => _LazyIndexedStackState(); } -class _IndexedStackState extends State { +class _LazyIndexedStackState extends State { @override Widget build(BuildContext context) { return Stack( children: widget.children.asMap().entries.map((entry) { + final isVisible = entry.key == widget.index; + final isLoaded = widget.loadedIndexes.contains(entry.key); + return Positioned.fill( child: Offstage( - offstage: entry.key != widget.index, + offstage: !isVisible, child: TickerMode( - enabled: entry.key == widget.index, - child: entry.value, + enabled: isVisible, + child: isLoaded ? entry.value : const SizedBox.shrink(), ), ), ); diff --git a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart index d989662..79486b8 100644 --- a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart +++ b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../../../data/models/coin.dart'; import '../../../providers/market_provider.dart'; import '../../../providers/asset_provider.dart'; +import '../../shared/ui_constants.dart'; /// 交易页面 - 使用 shadcn_ui 现代化设计 class TradePage extends StatefulWidget { @@ -23,21 +24,14 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi final _priceController = TextEditingController(); final _quantityController = TextEditingController(); - // 颜色常量 - static const upColor = Color(0xFF00C853); - static const downColor = Color(0xFFFF5252); - @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadData(); - }); + WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); } void _loadData() { context.read().loadCoins(); - context.read().loadOverview(); } @override @@ -62,13 +56,35 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi key: _formKey, child: Column( children: [ - _buildCoinSelector(market), + _CoinSelector( + selectedCoin: _selectedCoin, + coins: market.allCoins, + onCoinLoaded: (coin) { + _selectedCoin = coin; + _priceController.text = coin.formattedPrice; + }, + ), const SizedBox(height: 16), - _buildPriceCard(), + if (_selectedCoin != null) _PriceCard(coin: _selectedCoin!), const SizedBox(height: 16), - _buildTradeForm(asset), + _TradeForm( + tradeType: _tradeType, + selectedCoin: _selectedCoin, + priceController: _priceController, + quantityController: _quantityController, + tradeBalance: asset.overview?.tradeBalance, + onTradeTypeChanged: (type) => setState(() => _tradeType = type), + ), const SizedBox(height: 16), - _buildTradeButton(), + _TradeButton( + isBuy: _tradeType == 0, + coinCode: _selectedCoin?.code, + onPressed: () { + if (_formKey.currentState!.saveAndValidate()) { + _executeTrade(); + } + }, + ), ], ), ), @@ -78,311 +94,22 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi ); } - Widget _buildCoinSelector(MarketProvider market) { - final theme = ShadTheme.of(context); - final coins = market.allCoins; - - if (_selectedCoin == null && coins.isNotEmpty) { - _selectedCoin = coins.first; - _priceController.text = _selectedCoin!.formattedPrice; - } - - return ShadCard( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - CircleAvatar( - radius: 22, - backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), - child: Text( - _selectedCoin?.displayIcon ?? '?', - style: TextStyle( - fontSize: 20, - color: theme.colorScheme.primary, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _selectedCoin != null ? '${_selectedCoin!.code}/USDT' : '选择币种', - style: theme.textTheme.large.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - _selectedCoin != null ? _selectedCoin!.name : '点击选择交易对', - style: theme.textTheme.muted, - ), - ], - ), - ), - Icon( - LucideIcons.chevronRight, - color: theme.colorScheme.mutedForeground, - ), - ], - ), - ); - } - - Widget _buildPriceCard() { - final theme = ShadTheme.of(context); - - if (_selectedCoin == null) { - return const SizedBox.shrink(); - } - - final coin = _selectedCoin!; - - return ShadCard( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '最新价', - style: theme.textTheme.muted, - ), - const SizedBox(height: 4), - Text( - '\$${coin.formattedPrice}', - style: theme.textTheme.h2.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: coin.isUp ? upColor.withValues(alpha: 0.2) : downColor.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - coin.formattedChange, - style: TextStyle( - fontSize: 16, - color: coin.isUp ? upColor : downColor, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ); - } - - Widget _buildTradeForm(AssetProvider asset) { - final theme = ShadTheme.of(context); - - return ShadCard( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - // 买入/卖出切换 - Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => setState(() => _tradeType = 0), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: _tradeType == 0 ? upColor : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: _tradeType != 0 ? Border.all(color: upColor) : null, - ), - child: Center( - child: Text( - '买入', - style: TextStyle( - color: _tradeType == 0 ? Colors.white : upColor, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: GestureDetector( - onTap: () => setState(() => _tradeType = 1), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: _tradeType == 1 ? downColor : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: _tradeType != 1 ? Border.all(color: downColor) : null, - ), - child: Center( - child: Text( - '卖出', - style: TextStyle( - color: _tradeType == 1 ? Colors.white : downColor, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 20), - // 价格输入 - ShadInputFormField( - id: 'price', - label: const Text('价格(USDT)'), - controller: _priceController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - placeholder: const Text('输入价格'), - trailing: const Padding( - padding: EdgeInsets.only(right: 8), - child: Text('USDT'), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入价格'; - } - final price = double.tryParse(value); - if (price == null || price <= 0) { - return '请输入有效价格'; - } - return null; - }, - ), - const SizedBox(height: 12), - // 数量输入 - ShadInputFormField( - id: 'quantity', - label: const Text('数量'), - controller: _quantityController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - placeholder: const Text('输入数量'), - trailing: Padding( - padding: const EdgeInsets.only(right: 8), - child: Text(_selectedCoin?.code ?? ''), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return '请输入数量'; - } - final quantity = double.tryParse(value); - if (quantity == null || quantity <= 0) { - return '请输入有效数量'; - } - return null; - }, - ), - const SizedBox(height: 16), - // 交易金额 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '交易金额', - style: theme.textTheme.muted, - ), - Text( - '${_calculateAmount()} USDT', - style: theme.textTheme.small.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 8), - // 可用余额 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '可用', - style: theme.textTheme.muted, - ), - Text( - '${asset.overview?.tradeBalance ?? '0.00'} USDT', - style: theme.textTheme.muted, - ), - ], - ), - ], - ), - ); - } - - String _calculateAmount() { - final price = double.tryParse(_priceController.text) ?? 0; - final quantity = double.tryParse(_quantityController.text) ?? 0; - return (price * quantity).toStringAsFixed(2); - } - - Widget _buildTradeButton() { - final isBuy = _tradeType == 0; - final color = isBuy ? upColor : downColor; - - return SizedBox( - width: double.infinity, - height: 48, - child: ShadButton( - backgroundColor: color, - onPressed: () { - if (_formKey.currentState!.saveAndValidate()) { - _executeTrade(); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - isBuy ? LucideIcons.arrowDownToLine : LucideIcons.arrowUpFromLine, - size: 18, - color: Colors.white, - ), - const SizedBox(width: 8), - Text( - '${isBuy ? '买入' : '卖出'} ${_selectedCoin?.code ?? ''}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ); - } - void _executeTrade() { final price = _priceController.text; final quantity = _quantityController.text; + final isBuy = _tradeType == 0; showShadDialog( context: context, - builder: (context) => ShadDialog.alert( - title: Text(_tradeType == 0 ? '确认买入' : '确认卖出'), - description: Text( - '${_tradeType == 0 ? '买入' : '卖出'} $quantity ${_selectedCoin?.code ?? ''} @ $price USDT', - ), + builder: (ctx) => ShadDialog.alert( + title: Text(isBuy ? '确认买入' : '确认卖出'), + description: Text('${isBuy ? '买入' : '卖出'} $quantity ${_selectedCoin?.code ?? ''} @ $price USDT'), actions: [ - ShadButton.outline( - child: const Text('取消'), - onPressed: () => Navigator.of(context).pop(), - ), + ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), ShadButton( child: const Text('确认'), onPressed: () { - Navigator.of(context).pop(); + Navigator.of(ctx).pop(); _showTradeResult(); }, ), @@ -393,29 +120,24 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi void _showTradeResult() { final theme = ShadTheme.of(context); + final isBuy = _tradeType == 0; showShadDialog( context: context, - builder: (context) => ShadDialog.alert( + builder: (ctx) => ShadDialog.alert( title: Row( children: [ - Icon( - LucideIcons.circleCheck, - color: theme.colorScheme.primary, - size: 24, - ), + Icon(LucideIcons.circleCheck, color: theme.colorScheme.primary, size: 24), const SizedBox(width: 8), const Text('交易成功'), ], ), - description: Text( - '已${_tradeType == 0 ? '买入' : '卖出'} ${_quantityController.text} ${_selectedCoin?.code ?? ''}', - ), + description: Text('已${isBuy ? '买入' : '卖出'} ${_quantityController.text} ${_selectedCoin?.code ?? ''}'), actions: [ ShadButton( child: const Text('确定'), onPressed: () { - Navigator.of(context).pop(); + Navigator.of(ctx).pop(); _quantityController.clear(); }, ), @@ -424,3 +146,320 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi ); } } + +/// 币种选择器 +class _CoinSelector extends StatelessWidget { + final Coin? selectedCoin; + final List coins; + final ValueChanged 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 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 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), + ), + ], + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/shared/ui_constants.dart b/flutter_monisuo/lib/ui/shared/ui_constants.dart new file mode 100644 index 0000000..010aae0 --- /dev/null +++ b/flutter_monisuo/lib/ui/shared/ui_constants.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +/// 应用颜色常量 +class AppColors { + AppColors._(); + + static const Color up = Color(0xFF00C853); + static const Color down = Color(0xFFFF5252); + static const Color deposit = Color(0xFF00C853); + static const Color withdraw = Color(0xFFFF9800); + static const Color trade = Color(0xFF2196F3); + + static const List gradientColors = [ + Color(0xFF00D4AA), + Color(0xFF00B894), + ]; +} + +/// 表单验证器 +class Validators { + Validators._(); + + static String? amount(String? value) { + if (value == null || value.isEmpty) { + return '请输入金额'; + } + final amount = double.tryParse(value); + if (amount == null || amount <= 0) { + return '请输入有效金额'; + } + return null; + } + + static String? price(String? value) { + if (value == null || value.isEmpty) { + return '请输入价格'; + } + final price = double.tryParse(value); + if (price == null || price <= 0) { + return '请输入有效价格'; + } + return null; + } + + static String? quantity(String? value) { + if (value == null || value.isEmpty) { + return '请输入数量'; + } + final quantity = double.tryParse(value); + if (quantity == null || quantity <= 0) { + return '请输入有效数量'; + } + return null; + } + + static String? required(String? value, String fieldName) { + if (value == null || value.isEmpty) { + return '请输入$fieldName'; + } + return null; + } +}