diff --git a/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart index 6fb55b98..77578e47 100644 --- a/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart +++ b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/constants/app_colors.dart'; import '../../../core/utils/format_utils.dart'; import '../../providers/user_providers.dart'; import '../../providers/mining_providers.dart'; @@ -27,6 +26,10 @@ class AssetPage extends ConsumerWidget { final accountSequence = user.accountSequence ?? ''; final accountAsync = ref.watch(shareAccountProvider(accountSequence)); + // 提取数据和加载状态 + final isLoading = accountAsync.isLoading; + final account = accountAsync.valueOrNull; + return Scaffold( backgroundColor: Colors.white, body: SafeArea( @@ -51,32 +54,20 @@ class AssetPage extends ConsumerWidget { child: Column( children: [ const SizedBox(height: 8), - // 总资产卡片 - accountAsync.when( - data: (account) => _buildTotalAssetCard(account), - loading: () => _buildLoadingCard(), - error: (_, __) => _buildErrorCard('资产加载失败'), - ), + // 总资产卡片 - 始终显示,数字部分闪烁 + _buildTotalAssetCard(account, isLoading), const SizedBox(height: 24), // 快捷操作按钮 _buildQuickActions(), const SizedBox(height: 24), - // 资产列表 - accountAsync.when( - data: (account) => _buildAssetList(account), - loading: () => _buildAssetListSkeleton(), - error: (_, __) => const SizedBox.shrink(), - ), + // 资产列表 - 始终显示,数字部分闪烁 + _buildAssetList(account, isLoading), const SizedBox(height: 24), // 收益统计 - _buildEarningsCard(), + _buildEarningsCard(account, isLoading), const SizedBox(height: 24), // 账户列表 - accountAsync.when( - data: (account) => _buildAccountList(account), - loading: () => _buildAssetListSkeleton(), - error: (_, __) => const SizedBox.shrink(), - ), + _buildAccountList(account, isLoading), const SizedBox(height: 100), ], ), @@ -190,8 +181,7 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildTotalAssetCard(account) { - final totalAsset = account?.tradingBalance ?? '88888.88'; + Widget _buildTotalAssetCard(account, bool isLoading) { return Container( decoration: BoxDecoration( color: Colors.white, @@ -226,11 +216,11 @@ class AssetPage extends ConsumerWidget { right: 0, child: Container( height: 4, - decoration: BoxDecoration( - gradient: const LinearGradient( + decoration: const BoxDecoration( + gradient: LinearGradient( colors: [Color(0xFFFF6B00), Color(0xFFFDBA74)], ), - borderRadius: const BorderRadius.only( + borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), @@ -262,9 +252,11 @@ class AssetPage extends ConsumerWidget { ], ), const SizedBox(height: 8), - // 金额 - Text( - '¥ ${formatAmount(totalAsset)}', + // 金额 - 闪烁占位符 + AmountText( + amount: account != null ? formatAmount(account.tradingBalance ?? '0') : null, + isLoading: isLoading, + prefix: '¥ ', style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, @@ -274,9 +266,11 @@ class AssetPage extends ConsumerWidget { ), const SizedBox(height: 4), // USDT估值 - const Text( - '≈ 12,345.67 USDT', - style: TextStyle( + DataText( + data: account != null ? '≈ 12,345.67 USDT' : null, + isLoading: isLoading, + placeholder: '≈ -- USDT', + style: const TextStyle( fontSize: 14, color: Color(0xFF9CA3AF), ), @@ -292,11 +286,13 @@ class AssetPage extends ConsumerWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.trending_up, size: 14, color: _green), + const Icon(Icons.trending_up, size: 14, color: _green), const SizedBox(width: 6), - Text( - '+¥ 156.78 今日', - style: TextStyle( + DataText( + data: account != null ? '+¥ 156.78 今日' : null, + isLoading: isLoading, + placeholder: '+¥ -- 今日', + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _green, @@ -350,7 +346,7 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildAssetList(account) { + Widget _buildAssetList(account, bool isLoading) { return Column( children: [ // 积分股 @@ -359,7 +355,8 @@ class AssetPage extends ConsumerWidget { iconColor: _orange, iconBgColor: _serenade, title: '积分股', - amount: account?.miningBalance ?? '123456.78', + amount: account?.miningBalance, + isLoading: isLoading, valueInCny: '¥15,234.56', tag: '含倍数资产: 246,913.56', growthText: '每秒 +0.0015', @@ -371,7 +368,8 @@ class AssetPage extends ConsumerWidget { iconColor: _green, iconBgColor: _feta, title: '绿积分', - amount: account?.tradingBalance ?? '88888.88', + amount: account?.tradingBalance, + isLoading: isLoading, valueInCny: '¥10,986.54', badge: '可提现', badgeColor: _jewel, @@ -385,6 +383,7 @@ class AssetPage extends ConsumerWidget { iconBgColor: _serenade, title: '待分配积分股', amount: '1,234.56', + isLoading: isLoading, subtitle: '次日开始参与分配', ), ], @@ -396,7 +395,8 @@ class AssetPage extends ConsumerWidget { required Color iconColor, required Color iconBgColor, required String title, - required String amount, + String? amount, + bool isLoading = false, String? valueInCny, String? tag, String? growthText, @@ -468,9 +468,11 @@ class AssetPage extends ConsumerWidget { ], ), const SizedBox(height: 2), - // 数量 - Text( - formatAmount(amount), + // 数量 - 闪烁占位符 + DataText( + data: amount != null ? formatAmount(amount) : null, + isLoading: isLoading, + placeholder: '--', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -479,8 +481,10 @@ class AssetPage extends ConsumerWidget { ), // 估值 if (valueInCny != null) - Text( - '≈ $valueInCny', + DataText( + data: isLoading ? null : '≈ $valueInCny', + isLoading: isLoading, + placeholder: '≈ ¥--', style: const TextStyle( fontSize: 12, color: Color(0xFF9CA3AF), @@ -511,8 +515,10 @@ class AssetPage extends ConsumerWidget { color: _serenade, borderRadius: BorderRadius.circular(12), ), - child: Text( - tag, + child: DataText( + data: isLoading ? null : tag, + isLoading: isLoading, + placeholder: '含倍数资产: --', style: const TextStyle( fontSize: 10, color: _orange, @@ -523,10 +529,12 @@ class AssetPage extends ConsumerWidget { const SizedBox(width: 8), Row( children: [ - Icon(Icons.bolt, size: 12, color: _green), - Text( - growthText, - style: TextStyle( + const Icon(Icons.bolt, size: 12, color: _green), + DataText( + data: isLoading ? null : growthText, + isLoading: isLoading, + placeholder: '每秒 +--', + style: const TextStyle( fontSize: 10, color: _green, ), @@ -547,7 +555,7 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildEarningsCard() { + Widget _buildEarningsCard(account, bool isLoading) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -589,19 +597,19 @@ class AssetPage extends ConsumerWidget { // 统计数据 Row( children: [ - _buildEarningsItem('累计收益', '12,345.67', _orange), + _buildEarningsItem('累计收益', isLoading ? null : '12,345.67', _orange, isLoading), Container( width: 1, height: 40, color: _serenade, ), - _buildEarningsItem('今日收益', '+156.78', _green), + _buildEarningsItem('今日收益', isLoading ? null : '+156.78', _green, isLoading), Container( width: 1, height: 40, color: _serenade, ), - _buildEarningsItem('昨日收益', '143.21', const Color(0xFF9CA3AF)), + _buildEarningsItem('昨日收益', isLoading ? null : '143.21', const Color(0xFF9CA3AF), isLoading), ], ), ], @@ -609,7 +617,7 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildEarningsItem(String label, String value, Color valueColor) { + Widget _buildEarningsItem(String label, String? value, Color valueColor, bool isLoading) { return Expanded( child: Column( children: [ @@ -622,8 +630,10 @@ class AssetPage extends ConsumerWidget { textAlign: TextAlign.center, ), const SizedBox(height: 4), - Text( - value, + DataText( + data: value, + isLoading: isLoading, + placeholder: '--', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -636,7 +646,7 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildAccountList(account) { + Widget _buildAccountList(account, bool isLoading) { return Column( children: [ // 交易账户 @@ -644,7 +654,8 @@ class AssetPage extends ConsumerWidget { icon: Icons.account_balance_wallet, iconColor: _orange, title: '交易账户', - balance: account?.tradingBalance ?? '5678.90', + balance: account?.tradingBalance, + isLoading: isLoading, unit: '绿积分', status: '正常', statusColor: _green, @@ -657,6 +668,7 @@ class AssetPage extends ConsumerWidget { iconColor: _orange, title: '提现账户', balance: '1,234.56', + isLoading: isLoading, unit: '绿积分', status: '已绑定', statusColor: const Color(0xFF9CA3AF), @@ -671,7 +683,8 @@ class AssetPage extends ConsumerWidget { required IconData icon, required Color iconColor, required String title, - required String balance, + String? balance, + bool isLoading = false, required String unit, required String status, required Color statusColor, @@ -718,26 +731,26 @@ class AssetPage extends ConsumerWidget { ), ), const SizedBox(height: 2), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: '$balance ', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: _orange, - ), + Row( + children: [ + DataText( + data: balance, + isLoading: isLoading, + placeholder: '--', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _orange, ), - TextSpan( - text: unit, - style: const TextStyle( - fontSize: 12, - color: Color(0xFF9CA3AF), - ), + ), + Text( + ' $unit', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF9CA3AF), ), - ], - ), + ), + ], ), ], ), @@ -766,38 +779,4 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildLoadingCard() { - return const AssetCardSkeleton(); - } - - Widget _buildAssetListSkeleton() { - return Column( - children: const [ - AssetItemSkeleton(), - SizedBox(height: 16), - AssetItemSkeleton(), - SizedBox(height: 16), - AssetItemSkeleton(), - ], - ); - } - - Widget _buildErrorCard(String message) { - return Container( - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - ), - child: Center( - child: Column( - children: [ - const Icon(Icons.error_outline, size: 48, color: AppColors.error), - const SizedBox(height: 8), - Text(message), - ], - ), - ), - ); - } } diff --git a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart index 541d0617..a1686f02 100644 --- a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart +++ b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart @@ -32,6 +32,12 @@ class ContributionPage extends ConsumerWidget { ); final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams)); + // Extract loading state and data from AsyncValue + final isLoading = contributionAsync.isLoading; + final contribution = contributionAsync.valueOrNull; + final hasError = contributionAsync.hasError; + final error = contributionAsync.error; + return Scaffold( backgroundColor: const Color(0xFFF5F5F5), body: SafeArea( @@ -41,58 +47,54 @@ class ContributionPage extends ConsumerWidget { ref.invalidate(contributionProvider(accountSequence)); ref.invalidate(contributionRecordsProvider(recordsParams)); }, - child: contributionAsync.when( - data: (contribution) { - return CustomScrollView( - slivers: [ - // 顶部导航栏 - SliverToBoxAdapter(child: _buildAppBar(context)), - // 内容 - SliverPadding( - padding: const EdgeInsets.all(16), - sliver: SliverList( - delegate: SliverChildListDelegate([ - // 总贡献值卡片 - _buildTotalContributionCard(contribution), - const SizedBox(height: 16), - // 三栏统计 - _buildThreeColumnStats(contribution), - const SizedBox(height: 16), - // 今日预估收益 - _buildTodayEstimateCard(contribution), - const SizedBox(height: 16), - // 贡献值明细 - _buildContributionDetailCard(context, ref, recordsAsync), - const SizedBox(height: 16), - // 团队层级统计 - _buildTeamStatsCard(contribution), - const SizedBox(height: 16), - // 贡献值失效倒计时 - _buildExpirationCard(contribution, recordsAsync), - const SizedBox(height: 100), - ]), + child: hasError && contribution == null + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: AppColors.error), + const SizedBox(height: 16), + Text('加载失败: $error'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => ref.invalidate(contributionProvider(accountSequence)), + child: const Text('重试'), + ), + ], + ), + ) + : CustomScrollView( + slivers: [ + // 顶部导航栏 + SliverToBoxAdapter(child: _buildAppBar(context)), + // 内容 + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverList( + delegate: SliverChildListDelegate([ + // 总贡献值卡片 + _buildTotalContributionCard(contribution, isLoading), + const SizedBox(height: 16), + // 三栏统计 + _buildThreeColumnStats(contribution, isLoading), + const SizedBox(height: 16), + // 今日预估收益 + _buildTodayEstimateCard(contribution, isLoading), + const SizedBox(height: 16), + // 贡献值明细 + _buildContributionDetailCard(context, ref, recordsAsync), + const SizedBox(height: 16), + // 团队层级统计 + _buildTeamStatsCard(contribution, isLoading), + const SizedBox(height: 16), + // 贡献值失效倒计时 + _buildExpirationCard(contribution, recordsAsync, isLoading), + const SizedBox(height: 100), + ]), + ), ), - ), - ], - ); - }, - loading: () => const PageSkeleton(), - error: (error, _) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, size: 48, color: AppColors.error), - const SizedBox(height: 16), - Text('加载失败: $error'), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => ref.invalidate(contributionProvider(accountSequence)), - child: const Text('重试'), - ), - ], - ), - ), - ), + ], + ), ), ), ); @@ -156,7 +158,7 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildTotalContributionCard(Contribution? contribution) { + Widget _buildTotalContributionCard(Contribution? contribution, bool isLoading) { final total = contribution?.effectiveContribution ?? '0'; return Container( padding: const EdgeInsets.all(20), @@ -178,8 +180,10 @@ class ContributionPage extends ConsumerWidget { ], ), const SizedBox(height: 8), - Text( - formatAmount(total), + DataText( + data: isLoading ? null : formatAmount(total), + isLoading: isLoading, + placeholder: '----', style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, @@ -212,7 +216,7 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildThreeColumnStats(Contribution? contribution) { + Widget _buildThreeColumnStats(Contribution? contribution, bool isLoading) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -221,15 +225,15 @@ class ContributionPage extends ConsumerWidget { ), child: Row( children: [ - _buildStatColumn('个人贡献值', contribution?.personalContribution ?? '0', false), - _buildStatColumn('团队贡献值', contribution?.teamLevelContribution ?? '0', true), - _buildStatColumn('省市公司', contribution?.systemContribution ?? '0', true), + _buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false), + _buildStatColumn('团队贡献值', contribution?.teamLevelContribution, isLoading, true), + _buildStatColumn('省市公司', contribution?.systemContribution, isLoading, true), ], ), ); } - Widget _buildStatColumn(String label, String value, bool showLeftBorder) { + Widget _buildStatColumn(String label, String? value, bool isLoading, bool showLeftBorder) { return Expanded( child: Container( decoration: showLeftBorder @@ -242,9 +246,12 @@ class ContributionPage extends ConsumerWidget { children: [ Text(label, style: const TextStyle(fontSize: 12, color: _grayText)), const SizedBox(height: 4), - Text( - formatAmount(value), + DataText( + data: value != null ? formatAmount(value) : null, + isLoading: isLoading, + placeholder: '--', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _darkText), + textAlign: TextAlign.center, ), ], ), @@ -252,7 +259,7 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildTodayEstimateCard(Contribution? contribution) { + Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) { // 基于贡献值计算预估收益(暂时显示占位符,后续可接入实际计算API) final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0; // 简单估算:假设每日发放总量为 10000 积分股,按贡献值占比分配 @@ -298,24 +305,29 @@ class ContributionPage extends ConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text.rich( - TextSpan( - children: [ - TextSpan( - text: hasContribution ? '计算中' : '--', - style: TextStyle( - fontSize: hasContribution ? 14 : 18, - fontWeight: FontWeight.bold, - color: _green, + isLoading + ? const ShimmerText( + placeholder: '-- 积分股', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _green), + ) + : Text.rich( + TextSpan( + children: [ + TextSpan( + text: hasContribution ? '计算中' : '--', + style: TextStyle( + fontSize: hasContribution ? 14 : 18, + fontWeight: FontWeight.bold, + color: _green, + ), + ), + const TextSpan( + text: ' 积分股', + style: TextStyle(fontSize: 12, color: _green), + ), + ], ), ), - const TextSpan( - text: ' 积分股', - style: TextStyle(fontSize: 12, color: _green), - ), - ], - ), - ), ], ), ], @@ -328,6 +340,9 @@ class ContributionPage extends ConsumerWidget { WidgetRef ref, AsyncValue recordsAsync, ) { + final isRecordsLoading = recordsAsync.isLoading; + final recordsPage = recordsAsync.valueOrNull; + return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -359,55 +374,83 @@ class ContributionPage extends ConsumerWidget { ), const SizedBox(height: 16), // 明细列表 - recordsAsync.when( - data: (recordsPage) { - if (recordsPage == null || recordsPage.data.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Text( - '暂无贡献值记录', - style: TextStyle(fontSize: 14, color: _grayText), - ), - ); - } - return Column( - children: recordsPage.data.asMap().entries.map((entry) { - final index = entry.key; - final record = entry.value; - return Column( - children: [ - _buildDetailRow(record), - if (index < recordsPage.data.length - 1) const Divider(height: 24), - ], - ); - }).toList(), - ); - }, - loading: () => Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: ShimmerLoading( - child: Column( - children: const [ - ShimmerBox(height: 48), - SizedBox(height: 12), - ShimmerBox(height: 48), - ], - ), - ), - ), - error: (error, _) => Padding( + if (isRecordsLoading) + _buildRecordsShimmer() + else if (recordsAsync.hasError && recordsPage == null) + Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Text( '加载失败', style: TextStyle(fontSize: 14, color: _grayText.withOpacity(0.7)), ), + ) + else if (recordsPage == null || recordsPage.data.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text( + '暂无贡献值记录', + style: TextStyle(fontSize: 14, color: _grayText), + ), + ) + else + Column( + children: recordsPage.data.asMap().entries.map((entry) { + final index = entry.key; + final record = entry.value; + return Column( + children: [ + _buildDetailRow(record), + if (index < recordsPage.data.length - 1) const Divider(height: 24), + ], + ); + }).toList(), ), - ), ], ), ); } + Widget _buildRecordsShimmer() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + children: [ + _buildShimmerDetailRow(), + const Divider(height: 24), + _buildShimmerDetailRow(), + const Divider(height: 24), + _buildShimmerDetailRow(), + ], + ), + ); + } + + Widget _buildShimmerDetailRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerText( + placeholder: '认种贡献', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText), + ), + SizedBox(height: 2), + ShimmerText( + placeholder: '2024-01-01 12:00', + style: TextStyle(fontSize: 12, color: _grayText), + ), + ], + ), + const ShimmerText( + placeholder: '+1,000', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green), + ), + ], + ); + } + Widget _buildDetailRow(ContributionRecord record) { final dateFormat = DateFormat('yyyy-MM-dd HH:mm'); final formattedDate = dateFormat.format(record.createdAt); @@ -435,7 +478,7 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildTeamStatsCard(Contribution? contribution) { + Widget _buildTeamStatsCard(Contribution? contribution, bool isLoading) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -453,18 +496,38 @@ class ContributionPage extends ConsumerWidget { // 第一行 Row( children: [ - _buildTeamStatItem('直推人数', '${contribution?.directReferralAdoptedCount ?? 0}', '人'), + _buildTeamStatItem( + '直推人数', + contribution?.directReferralAdoptedCount.toString(), + '人', + isLoading, + ), const SizedBox(width: 16), - _buildTeamStatItem('已解锁奖励', '${contribution?.unlockedBonusTiers ?? 0}', '档'), + _buildTeamStatItem( + '已解锁奖励', + contribution?.unlockedBonusTiers.toString(), + '档', + isLoading, + ), ], ), const SizedBox(height: 16), // 第二行 Row( children: [ - _buildTeamStatItem('已解锁层级', '${contribution?.unlockedLevelDepth ?? 0}', '级'), + _buildTeamStatItem( + '已解锁层级', + contribution?.unlockedLevelDepth.toString(), + '级', + isLoading, + ), const SizedBox(width: 16), - _buildTeamStatItem('是否认种', contribution?.hasAdopted == true ? '是' : '否', ''), + _buildTeamStatItem( + '是否认种', + contribution != null ? (contribution.hasAdopted == true ? '是' : '否') : null, + '', + isLoading, + ), ], ), ], @@ -472,7 +535,7 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildTeamStatItem(String label, String value, String unit) { + Widget _buildTeamStatItem(String label, String? value, String unit, bool isLoading) { return Expanded( child: Container( padding: const EdgeInsets.all(12), @@ -485,21 +548,26 @@ class ContributionPage extends ConsumerWidget { children: [ Text(label, style: const TextStyle(fontSize: 12, color: _grayText)), const SizedBox(height: 4), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: '$value ', + isLoading + ? ShimmerText( + placeholder: unit.isNotEmpty ? '-- $unit' : '--', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange), - ), - if (unit.isNotEmpty) + ) + : Text.rich( TextSpan( - text: unit, - style: const TextStyle(fontSize: 12, color: _grayText), + children: [ + TextSpan( + text: '${value ?? '0'} ', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange), + ), + if (unit.isNotEmpty) + TextSpan( + text: unit, + style: const TextStyle(fontSize: 12, color: _grayText), + ), + ], ), - ], - ), - ), + ), ], ), ), @@ -509,19 +577,21 @@ class ContributionPage extends ConsumerWidget { Widget _buildExpirationCard( Contribution? contribution, AsyncValue recordsAsync, + bool isLoading, ) { + final isRecordsLoading = recordsAsync.isLoading; + final recordsPage = recordsAsync.valueOrNull; + // 从记录中获取最近的过期日期 DateTime? nearestExpireDate; - recordsAsync.whenData((recordsPage) { - if (recordsPage != null && recordsPage.data.isNotEmpty) { - // 找到未过期记录中最近的过期日期 - final activeRecords = recordsPage.data.where((r) => !r.isExpired).toList(); - if (activeRecords.isNotEmpty) { - activeRecords.sort((a, b) => a.expireDate.compareTo(b.expireDate)); - nearestExpireDate = activeRecords.first.expireDate; - } + if (recordsPage != null && recordsPage.data.isNotEmpty) { + // 找到未过期记录中最近的过期日期 + final activeRecords = recordsPage.data.where((r) => !r.isExpired).toList(); + if (activeRecords.isNotEmpty) { + activeRecords.sort((a, b) => a.expireDate.compareTo(b.expireDate)); + nearestExpireDate = activeRecords.first.expireDate; } - }); + } // 计算剩余天数和进度 final now = DateTime.now(); @@ -530,15 +600,17 @@ class ContributionPage extends ConsumerWidget { String expireDateText = '暂无过期信息'; if (nearestExpireDate != null) { - daysRemaining = nearestExpireDate!.difference(now).inDays; + daysRemaining = nearestExpireDate.difference(now).inDays; if (daysRemaining < 0) daysRemaining = 0; // 假设总有效期为730天 progress = daysRemaining / 730; if (progress > 1) progress = 1; if (progress < 0) progress = 0; - expireDateText = '您的贡献值将于 ${DateFormat('yyyy-MM-dd').format(nearestExpireDate!)} 失效'; + expireDateText = '您的贡献值将于 ${DateFormat('yyyy-MM-dd').format(nearestExpireDate)} 失效'; } + final showShimmer = isLoading || isRecordsLoading; + return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -564,23 +636,35 @@ class ContributionPage extends ConsumerWidget { ClipRRect( borderRadius: BorderRadius.circular(5), child: LinearProgressIndicator( - value: progress, + value: showShimmer ? 1.0 : progress, minHeight: 10, backgroundColor: _bgGray, - valueColor: const AlwaysStoppedAnimation(_orange), + valueColor: AlwaysStoppedAnimation( + showShimmer ? _bgGray : _orange, + ), ), ), const SizedBox(height: 12), // 说明文字 - Text( - expireDateText, - style: const TextStyle(fontSize: 12, color: _grayText), - ), + showShimmer + ? const ShimmerText( + placeholder: '您的贡献值将于 ---- 失效', + style: TextStyle(fontSize: 12, color: _grayText), + ) + : Text( + expireDateText, + style: const TextStyle(fontSize: 12, color: _grayText), + ), const SizedBox(height: 4), - Text( - '剩余 $daysRemaining 天', - style: const TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), - ), + showShimmer + ? const ShimmerText( + placeholder: '剩余 --- 天', + style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), + ) + : Text( + '剩余 $daysRemaining 天', + style: const TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), + ), const SizedBox(height: 8), // 提示 Container( diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index 09272d0d..45342f32 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -44,6 +44,11 @@ class _TradingPageState extends ConsumerState { final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; + // Extract loading state and data using shimmer placeholder approach + final isLoading = globalState.isLoading; + final state = globalState.valueOrNull; + final hasError = globalState.hasError; + return Scaffold( backgroundColor: const Color(0xFFF5F5F5), body: SafeArea( @@ -57,20 +62,16 @@ class _TradingPageState extends ConsumerState { child: SingleChildScrollView( child: Column( children: [ - // 价格卡片 - globalState.when( - data: (state) => _buildPriceCard(state), - loading: () => _buildLoadingCard(), - error: (_, __) => _buildErrorCard('价格加载失败'), - ), + // 价格卡片 - always render, use shimmer for loading + hasError + ? _buildErrorCard('价格加载失败') + : _buildPriceCard(state, isLoading), // K线图占位区域 _buildChartSection(), - // 市场数据 - globalState.when( - data: (state) => _buildMarketDataCard(state), - loading: () => _buildLoadingCard(), - error: (_, __) => _buildErrorCard('市场数据加载失败'), - ), + // 市场数据 - always render, use shimmer for loading + hasError + ? _buildErrorCard('市场数据加载失败') + : _buildMarketDataCard(state, isLoading), // 买入/卖出交易面板 _buildTradingPanel(accountSequence), // 我的挂单 @@ -149,10 +150,10 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildPriceCard(state) { + Widget _buildPriceCard(dynamic state, bool isLoading) { final isPriceUp = state?.isPriceUp ?? true; - final currentPrice = state?.currentPrice ?? '0.000156'; - final priceChange = state?.priceChange24h ?? '8.52'; + final currentPrice = state?.currentPrice; + final priceChange = state?.priceChange24h; return Container( margin: const EdgeInsets.all(16), @@ -176,8 +177,10 @@ class _TradingPageState extends ConsumerState { color: _grayText, ), ), - Text( - '= 156.00 绿积分', + DataText( + data: '= 156.00 绿积分', + isLoading: isLoading, + placeholder: '= -- 绿积分', style: TextStyle( fontSize: 12, color: _grayText, @@ -190,8 +193,10 @@ class _TradingPageState extends ConsumerState { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - '¥ ${formatPrice(currentPrice)}', + AmountText( + amount: currentPrice != null ? formatPrice(currentPrice) : null, + isLoading: isLoading, + prefix: '\u00A5 ', style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, @@ -214,8 +219,10 @@ class _TradingPageState extends ConsumerState { size: 16, color: _green, ), - Text( - '+$priceChange%', + DataText( + data: priceChange != null ? '+$priceChange%' : null, + isLoading: isLoading, + placeholder: '+--.--%', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -322,7 +329,12 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildMarketDataCard(state) { + Widget _buildMarketDataCard(dynamic state, bool isLoading) { + final sharesPool = state?.sharesPool; + final circulatingPool = state?.circulatingPool; + final greenPointsPool = state?.greenPointsPool; + final blackHoleBurned = state?.blackHoleBurned; + return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), @@ -359,10 +371,20 @@ class _TradingPageState extends ConsumerState { // 第一行数据 Row( children: [ - _buildMarketDataItem('积分股池', '8,888,888,888', _orange), + _buildMarketDataItem( + '积分股池', + sharesPool ?? '8,888,888,888', + _orange, + isLoading, + ), Container(width: 1, height: 24, color: _bgGray), const SizedBox(width: 16), - _buildMarketDataItem('流通池', '1,234,567', _orange), + _buildMarketDataItem( + '流通池', + circulatingPool ?? '1,234,567', + _orange, + isLoading, + ), ], ), const SizedBox(height: 24), @@ -371,10 +393,20 @@ class _TradingPageState extends ConsumerState { // 第二行数据 Row( children: [ - _buildMarketDataItem('绿积分池', '99,999,999', _orange), + _buildMarketDataItem( + '绿积分池', + greenPointsPool ?? '99,999,999', + _orange, + isLoading, + ), Container(width: 1, height: 24, color: _bgGray), const SizedBox(width: 16), - _buildMarketDataItem('黑洞销毁量', '50,000,000', _red), + _buildMarketDataItem( + '黑洞销毁量', + blackHoleBurned ?? '50,000,000', + _red, + isLoading, + ), ], ), ], @@ -382,7 +414,7 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildMarketDataItem(String label, String value, Color valueColor) { + Widget _buildMarketDataItem(String label, String value, Color valueColor, bool isLoading) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -395,8 +427,10 @@ class _TradingPageState extends ConsumerState { ), ), const SizedBox(height: 4), - Text( - value, + DataText( + data: value, + isLoading: isLoading, + placeholder: '--,---,---', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -767,29 +801,6 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildLoadingCard() { - return ShimmerLoading( - child: Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - ShimmerBox(width: 100, height: 16), - SizedBox(height: 12), - ShimmerBox(width: 150, height: 28), - SizedBox(height: 8), - ShimmerBox(width: 80, height: 14), - ], - ), - ), - ); - } - Widget _buildErrorCard(String message) { return Container( margin: const EdgeInsets.all(16), diff --git a/frontend/mining-app/lib/presentation/widgets/shimmer_loading.dart b/frontend/mining-app/lib/presentation/widgets/shimmer_loading.dart index 44ba8939..fe44e099 100644 --- a/frontend/mining-app/lib/presentation/widgets/shimmer_loading.dart +++ b/frontend/mining-app/lib/presentation/widgets/shimmer_loading.dart @@ -1,15 +1,147 @@ import 'package:flutter/material.dart'; -/// 骨架屏加载组件 - 提供更好的加载体验 +/// 闪烁文字占位符 - 数据加载时显示闪烁的占位文字 +class ShimmerText extends StatefulWidget { + final String placeholder; + final TextStyle? style; + final TextAlign? textAlign; + + const ShimmerText({ + super.key, + this.placeholder = '--', + this.style, + this.textAlign, + }); + + @override + State createState() => _ShimmerTextState(); +} + +class _ShimmerTextState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + )..repeat(reverse: true); + _animation = Tween(begin: 0.3, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Opacity( + opacity: _animation.value, + child: Text( + widget.placeholder, + style: widget.style, + textAlign: widget.textAlign, + ), + ); + }, + ); + } +} + +/// 数据文字组件 - 加载中显示闪烁占位符,加载完成显示真实数据 +class DataText extends StatelessWidget { + final String? data; + final bool isLoading; + final String placeholder; + final TextStyle? style; + final TextAlign? textAlign; + final String Function(String)? formatter; + + const DataText({ + super.key, + this.data, + this.isLoading = false, + this.placeholder = '--', + this.style, + this.textAlign, + this.formatter, + }); + + @override + Widget build(BuildContext context) { + if (isLoading || data == null) { + return ShimmerText( + placeholder: placeholder, + style: style, + textAlign: textAlign, + ); + } + + final displayText = formatter != null ? formatter!(data!) : data!; + return Text( + displayText, + style: style, + textAlign: textAlign, + ); + } +} + +/// 金额文字组件 - 专门用于显示金额 +class AmountText extends StatelessWidget { + final String? amount; + final bool isLoading; + final String prefix; + final String suffix; + final TextStyle? style; + final TextAlign? textAlign; + + const AmountText({ + super.key, + this.amount, + this.isLoading = false, + this.prefix = '', + this.suffix = '', + this.style, + this.textAlign, + }); + + @override + Widget build(BuildContext context) { + if (isLoading || amount == null) { + return ShimmerText( + placeholder: '$prefix--$suffix', + style: style, + textAlign: textAlign, + ); + } + + return Text( + '$prefix$amount$suffix', + style: style, + textAlign: textAlign, + ); + } +} + +// ============================================================================ +// 向后兼容的组件 - 用于尚未迁移的页面 +// ============================================================================ + +/// 闪烁加载效果容器(向后兼容) class ShimmerLoading extends StatefulWidget { final Widget child; - final bool isLoading; - const ShimmerLoading({ - super.key, - required this.child, - this.isLoading = true, - }); + const ShimmerLoading({super.key, required this.child}); @override State createState() => _ShimmerLoadingState(); @@ -24,11 +156,11 @@ class _ShimmerLoadingState extends State void initState() { super.initState(); _controller = AnimationController( - duration: const Duration(milliseconds: 1500), + duration: const Duration(milliseconds: 1000), vsync: this, - )..repeat(); - _animation = Tween(begin: -2, end: 2).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), + )..repeat(reverse: true); + _animation = Tween(begin: 0.4, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), ); } @@ -40,29 +172,11 @@ class _ShimmerLoadingState extends State @override Widget build(BuildContext context) { - if (!widget.isLoading) return widget.child; - return AnimatedBuilder( animation: _animation, builder: (context, child) { - return ShaderMask( - shaderCallback: (bounds) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: const [ - Color(0xFFEBEBF4), - Color(0xFFF4F4F4), - Color(0xFFEBEBF4), - ], - stops: [ - _animation.value - 1, - _animation.value, - _animation.value + 1, - ].map((e) => e.clamp(0.0, 1.0)).toList(), - ).createShader(bounds); - }, - blendMode: BlendMode.srcATop, + return Opacity( + opacity: _animation.value, child: widget.child, ); }, @@ -70,17 +184,17 @@ class _ShimmerLoadingState extends State } } -/// 骨架屏占位框 +/// 闪烁占位块(向后兼容) class ShimmerBox extends StatelessWidget { final double? width; final double height; - final double borderRadius; + final BorderRadius? borderRadius; const ShimmerBox({ super.key, this.width, - required this.height, - this.borderRadius = 8, + this.height = 16, + this.borderRadius, }); @override @@ -90,199 +204,36 @@ class ShimmerBox extends StatelessWidget { height: height, decoration: BoxDecoration( color: const Color(0xFFE5E7EB), - borderRadius: BorderRadius.circular(borderRadius), + borderRadius: borderRadius ?? BorderRadius.circular(4), ), ); } } -/// 资产卡片骨架屏 -class AssetCardSkeleton extends StatelessWidget { - const AssetCardSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return ShimmerLoading( - child: Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 30, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ShimmerBox(width: 80, height: 16), - const SizedBox(height: 12), - const ShimmerBox(width: 180, height: 36), - const SizedBox(height: 8), - const ShimmerBox(width: 120, height: 14), - const SizedBox(height: 12), - const ShimmerBox(width: 100, height: 24, borderRadius: 12), - ], - ), - ), - ); - } -} - -/// 资产项目骨架屏 -class AssetItemSkeleton extends StatelessWidget { - const AssetItemSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return ShimmerLoading( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Row( - children: [ - const ShimmerBox(width: 40, height: 40, borderRadius: 20), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ShimmerBox(width: 60, height: 16), - const SizedBox(height: 8), - const ShimmerBox(width: 100, height: 20), - const SizedBox(height: 4), - const ShimmerBox(width: 80, height: 12), - ], - ), - ), - const ShimmerBox(width: 14, height: 20), - ], - ), - ), - ); - } -} - -/// 收益统计骨架屏 -class EarningsStatsSkeleton extends StatelessWidget { - const EarningsStatsSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - return ShimmerLoading( - child: Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - children: [ - Row( - children: [ - Container( - width: 4, - height: 20, - decoration: BoxDecoration( - color: const Color(0xFFE5E7EB), - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 8), - const ShimmerBox(width: 60, height: 16), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: Column( - children: const [ - ShimmerBox(width: 50, height: 12), - SizedBox(height: 8), - ShimmerBox(width: 70, height: 16), - ], - ), - ), - Container( - width: 1, - height: 40, - color: const Color(0xFFE5E7EB), - ), - Expanded( - child: Column( - children: const [ - ShimmerBox(width: 50, height: 12), - SizedBox(height: 8), - ShimmerBox(width: 70, height: 16), - ], - ), - ), - Container( - width: 1, - height: 40, - color: const Color(0xFFE5E7EB), - ), - Expanded( - child: Column( - children: const [ - ShimmerBox(width: 50, height: 12), - SizedBox(height: 8), - ShimmerBox(width: 70, height: 16), - ], - ), - ), - ], - ), - ], - ), - ), - ); - } -} - -/// 页面骨架屏 +/// 页面骨架屏(向后兼容) class PageSkeleton extends StatelessWidget { const PageSkeleton({super.key}); @override Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: const [ - AssetCardSkeleton(), - SizedBox(height: 24), - AssetItemSkeleton(), - SizedBox(height: 16), - AssetItemSkeleton(), - SizedBox(height: 16), - AssetItemSkeleton(), - SizedBox(height: 24), - EarningsStatsSkeleton(), - ], + return ShimmerLoading( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 60), + const ShimmerBox(width: 120, height: 20), + const SizedBox(height: 16), + const ShimmerBox(height: 100), + const SizedBox(height: 24), + const ShimmerBox(height: 80), + const SizedBox(height: 24), + const ShimmerBox(height: 120), + const SizedBox(height: 24), + const ShimmerBox(height: 80), + ], + ), ), ); }