diff --git a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill index 2f08543..bbd52a9 100644 Binary files a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill and b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill differ diff --git a/flutter_monisuo/lib/providers/market_provider.dart b/flutter_monisuo/lib/providers/market_provider.dart index a1dff85..0094b93 100644 --- a/flutter_monisuo/lib/providers/market_provider.dart +++ b/flutter_monisuo/lib/providers/market_provider.dart @@ -7,26 +7,28 @@ class MarketProvider extends ChangeNotifier { final MarketService _marketService; List _allCoins = []; - List _filteredCoins = []; - String _activeTab = 'all'; - String _searchKeyword = ''; bool _isLoading = false; String? _error; - bool _coinsLoaded = false; // 标记是否已加载 + bool _coinsLoaded = false; MarketProvider(this._marketService); // Getters - List get coins => _filteredCoins; List get allCoins => _allCoins; bool get isLoading => _isLoading; String? get error => _error; - String get activeTab => _activeTab; - String get searchKeyword => _searchKeyword; + + /// BTC 和 ETH(上半区展示) + List get featuredCoins => + _allCoins.where((c) => c.code == 'BTC' || c.code == 'ETH').toList(); + + /// 排除 BTC、ETH、USDT 的代币列表(下半区展示) + List get otherCoins => _allCoins + .where((c) => !{'BTC', 'ETH', 'USDT'}.contains(c.code)) + .toList(); /// 加载币种列表 Future loadCoins({bool force = false}) async { - // 如果已经加载过且不是强制刷新,则跳过 if (_coinsLoaded && !force && _allCoins.isNotEmpty) { return; } @@ -40,7 +42,6 @@ class MarketProvider extends ChangeNotifier { if (response.success) { _allCoins = response.data ?? []; - _filterCoins(); _coinsLoaded = true; } else { _error = response.message; @@ -53,49 +54,6 @@ class MarketProvider extends ChangeNotifier { notifyListeners(); } - /// 设置分类标签 - void setTab(String tab) { - _activeTab = tab; - _filterCoins(); - notifyListeners(); - } - - /// 搜索 - void search(String keyword) { - _searchKeyword = keyword; - _filterCoins(); - notifyListeners(); - } - - /// 清除搜索 - void clearSearch() { - _searchKeyword = ''; - _filterCoins(); - notifyListeners(); - } - - /// 筛选币种 - void _filterCoins() { - List result = List.from(_allCoins); - - // 按分类筛选 - if (_activeTab == 'realtime') { - result = result.where((c) => c.isRealtime).toList(); - } else if (_activeTab == 'hot') { - result = result.take(6).toList(); - } - - // 按关键词筛选 - if (_searchKeyword.isNotEmpty) { - final kw = _searchKeyword.toLowerCase(); - result = result.where((c) => - c.code.toLowerCase().contains(kw) || - c.name.toLowerCase().contains(kw)).toList(); - } - - _filteredCoins = result; - } - /// 根据代码获取币种 Coin? getCoinByCode(String code) { try { @@ -114,7 +72,6 @@ class MarketProvider extends ChangeNotifier { void resetLoadState() { _coinsLoaded = false; _allCoins = []; - _filteredCoins = []; _error = null; notifyListeners(); } diff --git a/flutter_monisuo/lib/ui/pages/market/market_page.dart b/flutter_monisuo/lib/ui/pages/market/market_page.dart index 2a5ca25..1869d8d 100644 --- a/flutter_monisuo/lib/ui/pages/market/market_page.dart +++ b/flutter_monisuo/lib/ui/pages/market/market_page.dart @@ -9,7 +9,7 @@ import '../../../providers/market_provider.dart'; import '../../components/glass_panel.dart'; import '../main/main_page.dart'; -/// 行情页面 - Material Design 3 风格 +/// 行情页面 class MarketPage extends StatefulWidget { const MarketPage({super.key}); @@ -17,9 +17,8 @@ class MarketPage extends StatefulWidget { State createState() => _MarketPageState(); } -class _MarketPageState extends State with AutomaticKeepAliveClientMixin { - final _searchController = TextEditingController(); - +class _MarketPageState extends State + with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @@ -31,196 +30,120 @@ class _MarketPageState extends State with AutomaticKeepAliveClientMi }); } - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { super.build(context); final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: colorScheme.background, body: Consumer( builder: (context, provider, _) { - return Column( - children: [ - _buildSearchBar(provider, colorScheme), - _buildTabs(provider, colorScheme, isDark), - Expanded( - child: _buildCoinList(provider, colorScheme, isDark), + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.error != null) { + return _buildErrorState(provider); + } + + return RefreshIndicator( + onRefresh: () => provider.refresh(), + color: colorScheme.primary, + backgroundColor: colorScheme.surfaceContainerHighest, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: AppSpacing.pagePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 上半区:BTC + ETH 突出展示 + _buildFeaturedSection(provider), + SizedBox(height: AppSpacing.lg), + // 下半区标题 + Text( + '代币列表', + style: GoogleFonts.spaceGrotesk( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + SizedBox(height: AppSpacing.md), + // 下半区:代币列表 + _buildCoinList(provider), + ], ), - ], + ), ); }, ), ); } - Widget _buildSearchBar(MarketProvider provider, ColorScheme colorScheme) { - return Padding( - padding: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0), - child: Container( - decoration: BoxDecoration( - color: colorScheme.surfaceContainerLowest, - borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15), - ), - ), - child: TextField( - controller: _searchController, - onChanged: provider.search, - style: TextStyle(color: colorScheme.onSurface), - decoration: InputDecoration( - hintText: '搜索市场...', - hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), - prefixIcon: Icon( - LucideIcons.search, - size: 18, - color: colorScheme.onSurfaceVariant, - ), - suffixIcon: _searchController.text.isNotEmpty - ? GestureDetector( - onTap: () { - _searchController.clear(); - provider.clearSearch(); - }, - child: Icon( - LucideIcons.x, - size: 18, - color: colorScheme.onSurfaceVariant, - ), - ) - : null, - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.md + AppSpacing.xs, - ), - ), - ), - ), + /// 上半区:BTC + ETH 大卡片 + Widget _buildFeaturedSection(MarketProvider provider) { + final featured = provider.featuredCoins; + if (featured.isEmpty) return const SizedBox.shrink(); + + final btc = featured.where((c) => c.code == 'BTC').firstOrNull; + final eth = featured.where((c) => c.code == 'ETH').firstOrNull; + + return Row( + children: [ + if (btc != null) + Expanded(child: _FeaturedCard(coin: btc)) + else + const Expanded(child: SizedBox.shrink()), + SizedBox(width: AppSpacing.md), + if (eth != null) + Expanded(child: _FeaturedCard(coin: eth)) + else + const Expanded(child: SizedBox.shrink()), + ], ); } - Widget _buildTabs(MarketProvider provider, ColorScheme colorScheme, bool isDark) { - final tabs = [ - {'key': 'all', 'label': '全部'}, - {'key': 'realtime', 'label': '实时'}, - {'key': 'hot', 'label': '热门'}, - ]; + /// 下半区:代币列表 + Widget _buildCoinList(MarketProvider provider) { + final coins = provider.otherCoins; - return Container( - height: 48, - margin: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: tabs.map((tab) { - final isActive = provider.activeTab == tab['key']; - - return GestureDetector( - onTap: () => provider.setTab(tab['key']!), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: EdgeInsets.only(right: AppSpacing.sm), - padding: EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.sm + AppSpacing.xs, - ), - decoration: BoxDecoration( - color: isActive - ? colorScheme.primary.withOpacity(0.1) - : colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.full), - border: isActive - ? Border.all( - color: colorScheme.primary.withOpacity(0.2), - ) - : null, - boxShadow: isActive - ? [ - BoxShadow( - color: colorScheme.primary.withOpacity(isDark ? 0.15 : 0.08), - blurRadius: 15, - ), - ] - : null, - ), - child: Text( - tab['label']!, - style: TextStyle( - color: isActive - ? colorScheme.primary - : colorScheme.onSurfaceVariant, - fontWeight: isActive ? FontWeight.w700 : FontWeight.normal, - fontSize: 12, - ), - ), - ), - ); - }).toList(), - ), - ), - ); - } - - Widget _buildCoinList(MarketProvider provider, ColorScheme colorScheme, bool isDark) { - if (provider.isLoading) { - return Center( - child: CircularProgressIndicator( - color: colorScheme.primary, - ), + if (coins.isEmpty) { + return _EmptyState( + icon: LucideIcons.coins, + message: '暂无数据', + onRetry: () => provider.refresh(), ); } - if (provider.error != null) { - return _buildErrorState(provider, colorScheme); - } - - final coins = provider.coins; - if (coins.isEmpty) { - return _buildEmptyState(colorScheme); - } - - return RefreshIndicator( - onRefresh: provider.refresh, - color: colorScheme.primary, - backgroundColor: colorScheme.surfaceContainer, - child: ListView.builder( - padding: EdgeInsets.all(AppSpacing.md), - itemCount: coins.length, - itemBuilder: (context, index) => _buildCoinItem(coins[index], colorScheme), - ), + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: coins.length, + separatorBuilder: (_, __) => SizedBox(height: AppSpacing.sm), + itemBuilder: (context, index) => _CoinListItem(coin: coins[index]), ); } - Widget _buildErrorState(MarketProvider provider, ColorScheme colorScheme) { + /// 错误状态 + Widget _buildErrorState(MarketProvider provider) { + final colorScheme = Theme.of(context).colorScheme; return Center( child: Padding( padding: AppSpacing.pagePadding, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - LucideIcons.circleAlert, - size: 48, - color: colorScheme.error, - ), + Icon(LucideIcons.circleAlert, size: 48, color: colorScheme.error), SizedBox(height: AppSpacing.md), Text( - provider.error!, + provider.error ?? '加载失败', style: TextStyle(color: colorScheme.error), textAlign: TextAlign.center, ), - SizedBox(height: AppSpacing.lg), + SizedBox(height: AppSpacing.md), ShadButton( - onPressed: provider.loadCoins, + onPressed: () => provider.refresh(), child: const Text('重试'), ), ], @@ -228,57 +151,150 @@ class _MarketPageState extends State with AutomaticKeepAliveClientMi ), ); } +} - Widget _buildEmptyState(ColorScheme colorScheme) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - LucideIcons.coins, - size: 48, - color: colorScheme.onSurfaceVariant, - ), - SizedBox(height: AppSpacing.md), - Text( - '暂无数据', - style: TextStyle(color: colorScheme.onSurfaceVariant), - ), - ], +/// 上半区大卡片:BTC / ETH +class _FeaturedCard extends StatelessWidget { + final Coin coin; + + const _FeaturedCard({required this.coin}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final isUp = coin.isUp; + final changeColor = + isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down; + final changeBgColor = isUp + ? AppColorScheme.getUpBackgroundColor(isDark) + : colorScheme.error.withOpacity(0.1); + + return GestureDetector( + onTap: () => _navigateToTrade(context), + child: GlassPanel( + padding: EdgeInsets.all(AppSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 图标 + 币种代码 + Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.xl), + ), + child: Center( + child: Text( + coin.displayIcon, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + ), + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + coin.code, + style: GoogleFonts.spaceGrotesk( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ), + ], + ), + SizedBox(height: AppSpacing.md), + // 当前价格 + Text( + '\$${coin.formattedPrice}', + style: GoogleFonts.spaceGrotesk( + fontSize: 24, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + SizedBox(height: AppSpacing.xs), + // 24h 涨跌幅 + 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: 13, + fontWeight: FontWeight.w700, + color: changeColor, + ), + ), + ), + ], + ), ), ); } - Widget _buildCoinItem(Coin coin, ColorScheme colorScheme) { - final changeColor = coin.isUp ? AppColorScheme.up : AppColorScheme.down; - final changeBgColor = coin.isUp - ? AppColorScheme.up.withOpacity(0.1) + void _navigateToTrade(BuildContext context) { + final mainState = context.findAncestorStateOfType(); + mainState?.switchToTrade(coin.code); + } +} + +/// 下半区列表项 +class _CoinListItem extends StatelessWidget { + final Coin coin; + + const _CoinListItem({required this.coin}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final isUp = coin.isUp; + final changeColor = + isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down; + final changeBgColor = isUp + ? AppColorScheme.getUpBackgroundColor(isDark) : colorScheme.error.withOpacity(0.1); return GestureDetector( - onTap: () => _navigateToTrade(coin), - child: GlassCard( - margin: EdgeInsets.only(bottom: AppSpacing.sm), + onTap: () => _navigateToTrade(context), + child: GlassPanel( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm + AppSpacing.xs, + ), child: Row( children: [ - // 图标容器 + // 币种图标 Container( - width: 48, - height: 48, + width: 40, + height: 40, decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, + color: colorScheme.primary.withOpacity(0.1), 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, + fontSize: 18, fontWeight: FontWeight.bold, + color: colorScheme.primary, ), ), ), @@ -294,8 +310,8 @@ class _MarketPageState extends State with AutomaticKeepAliveClientMi Text( coin.code, style: GoogleFonts.spaceGrotesk( - fontSize: 16, - fontWeight: FontWeight.bold, + fontSize: 14, + fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), @@ -303,13 +319,12 @@ class _MarketPageState extends State with AutomaticKeepAliveClientMi Text( '/USDT', style: TextStyle( - fontSize: 12, + fontSize: 11, color: colorScheme.onSurfaceVariant, ), ), ], ), - SizedBox(height: AppSpacing.xs / 2), Text( coin.name, style: TextStyle( @@ -328,22 +343,19 @@ class _MarketPageState extends State with AutomaticKeepAliveClientMi '\$${coin.formattedPrice}', style: GoogleFonts.spaceGrotesk( fontSize: 14, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), - SizedBox(height: AppSpacing.xs), Container( padding: EdgeInsets.symmetric( horizontal: AppSpacing.sm, - vertical: AppSpacing.xs, + vertical: 2, ), decoration: BoxDecoration( color: changeBgColor, borderRadius: BorderRadius.circular(AppRadius.sm), - border: Border.all( - color: changeColor.withOpacity(0.2), - ), + border: Border.all(color: changeColor.withOpacity(0.2)), ), child: Text( coin.formattedChange, @@ -362,11 +374,45 @@ class _MarketPageState extends State with AutomaticKeepAliveClientMi ); } - void _navigateToTrade(Coin coin) { - // 切换到交易页面并选中该币种 - MainPageState? mainPageState = context.findAncestorStateOfType(); - if (mainPageState != null) { - mainPageState.switchToTrade(coin.code); - } + void _navigateToTrade(BuildContext context) { + final mainState = context.findAncestorStateOfType(); + mainState?.switchToTrade(coin.code); + } +} + +/// 空状态 +class _EmptyState extends StatelessWidget { + final IconData icon; + final String message; + final VoidCallback? onRetry; + + const _EmptyState({required this.icon, required this.message, this.onRetry}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Center( + child: Padding( + padding: EdgeInsets.all(AppSpacing.xl), + child: Column( + children: [ + Icon(icon, size: 48, color: colorScheme.onSurfaceVariant), + SizedBox(height: AppSpacing.sm + AppSpacing.xs), + Text( + message, + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + if (onRetry != null) ...[ + SizedBox(height: AppSpacing.md), + ShadButton( + onPressed: onRetry, + child: const Text('重试'), + ), + ], + ], + ), + ), + ); } }