From 396b81d6d9fd407fd674ef68b7eb3d80b39581c3 Mon Sep 17 00:00:00 2001 From: sion Date: Wed, 25 Mar 2026 23:59:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E5=92=8C=E5=B8=81=E7=A7=8D=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 交易账户卡片添加总市值显示和持仓列表 - 持仓列表USDT自动排在最上面 - 交易页面添加币种选择弹窗功能 - 行情页面点击币种跳转到交易页面 - 支持从外部传入选中币种参数 Co-Authored-By: Claude Opus 4.6 --- .../lib/ui/pages/asset/asset_page.dart | 93 ++++++- .../lib/ui/pages/main/main_page.dart | 48 +++- .../lib/ui/pages/market/market_page.dart | 198 ++++++++------- .../lib/ui/pages/trade/trade_page.dart | 239 +++++++++++++++--- 4 files changed, 434 insertions(+), 144 deletions(-) diff --git a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart index 082ca4b..62fa56f 100644 --- a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart +++ b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart @@ -63,7 +63,10 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi SizedBox(height: AppSpacing.md), _activeTab == 0 ? _FundAccountCard(provider: provider) - : _TradeAccountCard(holdings: provider.holdings), + : _TradeAccountCard( + holdings: provider.holdings, + tradeBalance: provider.overview?.tradeBalance, + ), ], ), ), @@ -327,40 +330,116 @@ class _FundAccountCard extends StatelessWidget { /// 交易账户卡片 - Glass Panel 风格 class _TradeAccountCard extends StatelessWidget { final List holdings; + final String? tradeBalance; - const _TradeAccountCard({required this.holdings}); + const _TradeAccountCard({required this.holdings, this.tradeBalance}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + // 计算总市值(所有持仓折算成USDT) + double totalValue = 0; + for (var h in holdings) { + final value = double.tryParse(h.currentValue?.toString() ?? '0') ?? 0; + totalValue += value; + } + + // 对持仓进行排序:USDT 放在最上面 + final sortedHoldings = List.from(holdings); + sortedHoldings.sort((a, b) { + final codeA = (a.coinCode ?? a['coinCode'] ?? '').toString().toUpperCase(); + final codeB = (b.coinCode ?? b['coinCode'] ?? '').toString().toUpperCase(); + if (codeA == 'USDT') return -1; + if (codeB == 'USDT') return 1; + return 0; + }); return GlassPanel( padding: AppSpacing.cardPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 标题行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon( + LucideIcons.trendingUp, + size: 18, + color: colorScheme.primary, + ), + ), + SizedBox(width: AppSpacing.sm), + Text( + '交易账户', + style: GoogleFonts.spaceGrotesk( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ], + ), + Icon( + LucideIcons.chevronRight, + size: 14, + color: colorScheme.primary, + ), + ], + ), + SizedBox(height: AppSpacing.md), + // 总市值 Text( - '持仓列表', + '总市值 (USDT)', + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + ), + SizedBox(height: AppSpacing.xs), + Text( + totalValue.toStringAsFixed(2), style: GoogleFonts.spaceGrotesk( - fontSize: 16, + fontSize: 28, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), + SizedBox(height: AppSpacing.lg), + // 持仓列表标题 + Text( + '持仓列表', + style: GoogleFonts.spaceGrotesk( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorScheme.onSurfaceVariant, + ), + ), SizedBox(height: AppSpacing.md), - if (holdings.isEmpty) + if (sortedHoldings.isEmpty) const _EmptyState(icon: LucideIcons.wallet, message: '暂无持仓') else ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: holdings.length, + itemCount: sortedHoldings.length, separatorBuilder: (_, __) => Container( margin: EdgeInsets.only(left: 56), height: 1, color: AppColorScheme.glassPanelBorder, ), - itemBuilder: (context, index) => _HoldingItem(holding: holdings[index]), + itemBuilder: (context, index) => _HoldingItem(holding: sortedHoldings[index]), ), ], ), diff --git a/flutter_monisuo/lib/ui/pages/main/main_page.dart b/flutter_monisuo/lib/ui/pages/main/main_page.dart index d9dddae..9b86a29 100644 --- a/flutter_monisuo/lib/ui/pages/main/main_page.dart +++ b/flutter_monisuo/lib/ui/pages/main/main_page.dart @@ -14,9 +14,8 @@ 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}); + const _NavItem({required this.label, required this.icon}); } /// 主页面 - "The Kinetic Vault" 设计风格 @@ -24,20 +23,26 @@ class MainPage extends StatefulWidget { const MainPage({super.key}); @override - State createState() => _MainPageState(); + State createState() => MainPageState(); } -class _MainPageState extends State { +class MainPageState extends State { int _currentIndex = 0; final Set _loadedPages = {0}; + String? _tradeCoinCode; // 交易页面选中的币种代码 + late final List _pages; - static final _navItems = [ - _NavItem(label: '首页', icon: LucideIcons.house, page: const HomePage()), - _NavItem(label: '行情', icon: LucideIcons.trendingUp, page: const MarketPage()), - _NavItem(label: '交易', icon: LucideIcons.arrowLeftRight, page: const TradePage()), - _NavItem(label: '资产', icon: LucideIcons.wallet, page: const AssetPage()), - _NavItem(label: '我的', icon: LucideIcons.user, page: const MinePage()), - ]; + @override + void initState() { + super.initState(); + _pages = [ + const HomePage(), + const MarketPage(), + TradePage(initialCoinCode: _tradeCoinCode), + const AssetPage(), + const MinePage(), + ]; + } void _onTabChanged(int index) { setState(() { @@ -46,6 +51,25 @@ class _MainPageState extends State { }); } + /// 切换到交易页面并选中指定币种 + void switchToTrade(String coinCode) { + setState(() { + _tradeCoinCode = coinCode; + _currentIndex = 2; // 交易页面索引 + _loadedPages.add(2); + // 重新构建交易页面 + _pages[2] = TradePage(initialCoinCode: _tradeCoinCode); + }); + } + + static const _navItems = [ + _NavItem(label: '首页', icon: LucideIcons.house), + _NavItem(label: '行情', icon: LucideIcons.trendingUp), + _NavItem(label: '交易', icon: LucideIcons.arrowLeftRight), + _NavItem(label: '资产', icon: LucideIcons.wallet), + _NavItem(label: '我的', icon: LucideIcons.user), + ]; + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -61,7 +85,7 @@ class _MainPageState extends State { child: LazyIndexedStack( index: _currentIndex, loadedIndexes: _loadedPages, - children: _navItems.map((item) => item.page).toList(), + children: _pages, ), ), ], diff --git a/flutter_monisuo/lib/ui/pages/market/market_page.dart b/flutter_monisuo/lib/ui/pages/market/market_page.dart index 7b91197..2a5ca25 100644 --- a/flutter_monisuo/lib/ui/pages/market/market_page.dart +++ b/flutter_monisuo/lib/ui/pages/market/market_page.dart @@ -7,6 +7,7 @@ import '../../../core/theme/app_spacing.dart'; import '../../../data/models/coin.dart'; import '../../../providers/market_provider.dart'; import '../../components/glass_panel.dart'; +import '../main/main_page.dart'; /// 行情页面 - Material Design 3 风格 class MarketPage extends StatefulWidget { @@ -254,107 +255,118 @@ class _MarketPageState extends State with AutomaticKeepAliveClientMi ? AppColorScheme.up.withOpacity(0.1) : colorScheme.error.withOpacity(0.1); - return GlassCard( - margin: EdgeInsets.only(bottom: AppSpacing.sm), - child: Row( - children: [ - // 图标容器 - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.2), - ), - ), - child: Center( - child: Text( - coin.displayIcon, - style: TextStyle( - fontSize: 20, - color: coin.isUp ? colorScheme.primary : colorScheme.secondary, - fontWeight: FontWeight.bold, + return GestureDetector( + onTap: () => _navigateToTrade(coin), + child: GlassCard( + margin: EdgeInsets.only(bottom: AppSpacing.sm), + child: Row( + children: [ + // 图标容器 + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(AppRadius.xl), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.2), ), ), - ), - ), - SizedBox(width: AppSpacing.sm + AppSpacing.xs), - // 币种信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - coin.code, - style: GoogleFonts.spaceGrotesk( - fontSize: 16, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - SizedBox(width: AppSpacing.xs), - Text( - '/USDT', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - SizedBox(height: AppSpacing.xs / 2), - Text( - coin.name, + child: Center( + child: Text( + coin.displayIcon, style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant, + fontSize: 20, + color: coin.isUp ? colorScheme.primary : colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + SizedBox(width: AppSpacing.sm + AppSpacing.xs), + // 币种信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + coin.code, + style: GoogleFonts.spaceGrotesk( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + SizedBox(width: AppSpacing.xs), + Text( + '/USDT', + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + SizedBox(height: AppSpacing.xs / 2), + Text( + coin.name, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + // 价格和涨跌幅 + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '\$${coin.formattedPrice}', + style: GoogleFonts.spaceGrotesk( + fontSize: 14, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + SizedBox(height: AppSpacing.xs), + Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: changeBgColor, + borderRadius: BorderRadius.circular(AppRadius.sm), + border: Border.all( + color: changeColor.withOpacity(0.2), + ), + ), + child: Text( + coin.formattedChange, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: changeColor, + ), ), ), ], ), - ), - // 价格和涨跌幅 - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '\$${coin.formattedPrice}', - style: GoogleFonts.spaceGrotesk( - fontSize: 14, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - SizedBox(height: AppSpacing.xs), - Container( - padding: EdgeInsets.symmetric( - horizontal: AppSpacing.sm, - vertical: AppSpacing.xs, - ), - decoration: BoxDecoration( - color: changeBgColor, - borderRadius: BorderRadius.circular(AppRadius.sm), - border: Border.all( - color: changeColor.withOpacity(0.2), - ), - ), - child: Text( - coin.formattedChange, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: changeColor, - ), - ), - ), - ], - ), - ], + ], + ), ), ); } + + void _navigateToTrade(Coin coin) { + // 切换到交易页面并选中该币种 + MainPageState? mainPageState = context.findAncestorStateOfType(); + if (mainPageState != null) { + mainPageState.switchToTrade(coin.code); + } + } } diff --git a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart index a853862..e02932d 100644 --- a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart +++ b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart @@ -13,7 +13,9 @@ import '../../components/neon_glow.dart'; /// 交易页面 - Material Design 3 风格 class TradePage extends StatefulWidget { - const TradePage({super.key}); + final String? initialCoinCode; + + const TradePage({super.key, this.initialCoinCode}); @override State createState() => _TradePageState(); @@ -36,7 +38,23 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi } void _loadData() { - context.read().loadCoins(); + final marketProvider = context.read(); + marketProvider.loadCoins().then((_) { + // 如果有初始币种代码,自动选中 + if (widget.initialCoinCode != null && _selectedCoin == null) { + final coins = marketProvider.allCoins; + final coin = coins.firstWhere( + (c) => c.code.toUpperCase() == widget.initialCoinCode!.toUpperCase(), + orElse: () => coins.isNotEmpty ? coins.first : throw Exception('No coins available'), + ); + if (mounted) { + setState(() { + _selectedCoin = coin; + _priceController.text = coin.formattedPrice; + }); + } + } + }); } @override @@ -64,9 +82,11 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi _CoinSelector( selectedCoin: _selectedCoin, coins: market.allCoins, - onCoinLoaded: (coin) { - _selectedCoin = coin; - _priceController.text = coin.formattedPrice; + onCoinSelected: (coin) { + setState(() { + _selectedCoin = coin; + _priceController.text = coin.formattedPrice; + }); }, ), SizedBox(height: AppSpacing.md), @@ -161,57 +181,212 @@ class _TradePageState extends State with AutomaticKeepAliveClientMixi class _CoinSelector extends StatelessWidget { final Coin? selectedCoin; final List coins; - final ValueChanged onCoinLoaded; + final ValueChanged onCoinSelected; const _CoinSelector({ required this.selectedCoin, required this.coins, - required this.onCoinLoaded, + required this.onCoinSelected, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - // 自动选择第一个币种 - if (selectedCoin == null && coins.isNotEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) => onCoinLoaded(coins.first)); - } + return GestureDetector( + onTap: () => _showCoinPicker(context), + child: GlassCard( + showNeonGlow: false, + child: Row( + children: [ + _CoinAvatar(icon: selectedCoin?.displayIcon), + SizedBox(width: AppSpacing.sm + AppSpacing.xs), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种', + style: GoogleFonts.spaceGrotesk( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + SizedBox(height: AppSpacing.xs), + Text( + selectedCoin?.name ?? '点击选择交易对', + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + LucideIcons.chevronDown, + color: colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ); + } - return GlassCard( - showNeonGlow: false, - child: Row( - children: [ - _CoinAvatar(icon: selectedCoin?.displayIcon), - SizedBox(width: AppSpacing.sm + AppSpacing.xs), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + void _showCoinPicker(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (ctx) => Container( + height: MediaQuery.of(ctx).size.height * 0.7, + decoration: BoxDecoration( + color: isDark ? colorScheme.surface : colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)), + ), + child: Column( + children: [ + // 拖动条 + Container( + margin: EdgeInsets.only(top: AppSpacing.sm), + width: 40, + height: 4, + decoration: BoxDecoration( + color: colorScheme.onSurfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + // 标题 + Padding( + padding: EdgeInsets.all(AppSpacing.lg), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '选择币种', + style: GoogleFonts.spaceGrotesk( + fontSize: 20, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + GestureDetector( + onTap: () => Navigator.of(ctx).pop(), + child: Icon( + LucideIcons.x, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)), + // 币种列表 + Expanded( + child: ListView.builder( + padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), + itemCount: coins.length, + itemBuilder: (ctx, index) => _buildCoinItem(coins[index], context, ctx), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCoinItem(Coin coin, BuildContext context, BuildContext sheetContext) { + final colorScheme = Theme.of(context).colorScheme; + final isSelected = selectedCoin?.code == coin.code; + final isDark = Theme.of(context).brightness == Brightness.dark; + final changeColor = coin.isUp ? AppColorScheme.up : AppColorScheme.down; + + return GestureDetector( + onTap: () { + Navigator.of(sheetContext).pop(); + onCoinSelected(coin); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + color: isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent, + child: Row( + children: [ + _CoinAvatar(icon: coin.displayIcon), + SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + coin.code, + style: GoogleFonts.spaceGrotesk( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + SizedBox(width: AppSpacing.xs), + Text( + '/USDT', + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + SizedBox(height: AppSpacing.xs / 2), + Text( + coin.name, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种', + '\$${coin.formattedPrice}', style: GoogleFonts.spaceGrotesk( - fontSize: 18, - fontWeight: FontWeight.bold, + fontSize: 14, + fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), - SizedBox(height: AppSpacing.xs), + SizedBox(height: AppSpacing.xs / 2), Text( - selectedCoin?.name ?? '点击选择交易对', + coin.formattedChange, style: TextStyle( fontSize: 12, - color: colorScheme.onSurfaceVariant, + color: changeColor, + fontWeight: FontWeight.w600, ), ), ], ), - ), - Icon( - LucideIcons.chevronRight, - color: colorScheme.onSurfaceVariant, - ), - ], + if (isSelected) ...[ + SizedBox(width: AppSpacing.sm), + Icon( + LucideIcons.check, + size: 18, + color: colorScheme.primary, + ), + ], + ], + ), ), ); }