feat(mining-app): shimmer placeholder for all pages

Replace skeleton blocks with shimmer text placeholders across all pages:
- Asset page: show full UI with flickering numbers during load
- Trading page: show full UI with flickering market data during load
- Contribution page: show full UI with flickering stats during load
- shimmer_loading.dart: add ShimmerText, DataText, AmountText components

This approach shows the complete UI immediately, with only the dynamic
number values flickering while data loads - much better UX than showing
grey skeleton blocks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-12 21:56:22 -08:00
parent b1525bdfa6
commit eba4b3b6e5
4 changed files with 561 additions and 536 deletions

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/utils/format_utils.dart'; import '../../../core/utils/format_utils.dart';
import '../../providers/user_providers.dart'; import '../../providers/user_providers.dart';
import '../../providers/mining_providers.dart'; import '../../providers/mining_providers.dart';
@ -27,6 +26,10 @@ class AssetPage extends ConsumerWidget {
final accountSequence = user.accountSequence ?? ''; final accountSequence = user.accountSequence ?? '';
final accountAsync = ref.watch(shareAccountProvider(accountSequence)); final accountAsync = ref.watch(shareAccountProvider(accountSequence));
//
final isLoading = accountAsync.isLoading;
final account = accountAsync.valueOrNull;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
body: SafeArea( body: SafeArea(
@ -51,32 +54,20 @@ class AssetPage extends ConsumerWidget {
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 8), const SizedBox(height: 8),
// // -
accountAsync.when( _buildTotalAssetCard(account, isLoading),
data: (account) => _buildTotalAssetCard(account),
loading: () => _buildLoadingCard(),
error: (_, __) => _buildErrorCard('资产加载失败'),
),
const SizedBox(height: 24), const SizedBox(height: 24),
// //
_buildQuickActions(), _buildQuickActions(),
const SizedBox(height: 24), const SizedBox(height: 24),
// // -
accountAsync.when( _buildAssetList(account, isLoading),
data: (account) => _buildAssetList(account),
loading: () => _buildAssetListSkeleton(),
error: (_, __) => const SizedBox.shrink(),
),
const SizedBox(height: 24), const SizedBox(height: 24),
// //
_buildEarningsCard(), _buildEarningsCard(account, isLoading),
const SizedBox(height: 24), const SizedBox(height: 24),
// //
accountAsync.when( _buildAccountList(account, isLoading),
data: (account) => _buildAccountList(account),
loading: () => _buildAssetListSkeleton(),
error: (_, __) => const SizedBox.shrink(),
),
const SizedBox(height: 100), const SizedBox(height: 100),
], ],
), ),
@ -190,8 +181,7 @@ class AssetPage extends ConsumerWidget {
); );
} }
Widget _buildTotalAssetCard(account) { Widget _buildTotalAssetCard(account, bool isLoading) {
final totalAsset = account?.tradingBalance ?? '88888.88';
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@ -226,11 +216,11 @@ class AssetPage extends ConsumerWidget {
right: 0, right: 0,
child: Container( child: Container(
height: 4, height: 4,
decoration: BoxDecoration( decoration: const BoxDecoration(
gradient: const LinearGradient( gradient: LinearGradient(
colors: [Color(0xFFFF6B00), Color(0xFFFDBA74)], colors: [Color(0xFFFF6B00), Color(0xFFFDBA74)],
), ),
borderRadius: const BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(20), topLeft: Radius.circular(20),
topRight: Radius.circular(20), topRight: Radius.circular(20),
), ),
@ -262,9 +252,11 @@ class AssetPage extends ConsumerWidget {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// // -
Text( AmountText(
'¥ ${formatAmount(totalAsset)}', amount: account != null ? formatAmount(account.tradingBalance ?? '0') : null,
isLoading: isLoading,
prefix: '¥ ',
style: const TextStyle( style: const TextStyle(
fontSize: 30, fontSize: 30,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -274,9 +266,11 @@ class AssetPage extends ConsumerWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// USDT估值 // USDT估值
const Text( DataText(
'≈ 12,345.67 USDT', data: account != null ? '≈ 12,345.67 USDT' : null,
style: TextStyle( isLoading: isLoading,
placeholder: '≈ -- USDT',
style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF9CA3AF), color: Color(0xFF9CA3AF),
), ),
@ -292,11 +286,13 @@ class AssetPage extends ConsumerWidget {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.trending_up, size: 14, color: _green), const Icon(Icons.trending_up, size: 14, color: _green),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( DataText(
'+¥ 156.78 今日', data: account != null ? '+¥ 156.78 今日' : null,
style: TextStyle( isLoading: isLoading,
placeholder: '+¥ -- 今日',
style: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: _green, color: _green,
@ -350,7 +346,7 @@ class AssetPage extends ConsumerWidget {
); );
} }
Widget _buildAssetList(account) { Widget _buildAssetList(account, bool isLoading) {
return Column( return Column(
children: [ children: [
// //
@ -359,7 +355,8 @@ class AssetPage extends ConsumerWidget {
iconColor: _orange, iconColor: _orange,
iconBgColor: _serenade, iconBgColor: _serenade,
title: '积分股', title: '积分股',
amount: account?.miningBalance ?? '123456.78', amount: account?.miningBalance,
isLoading: isLoading,
valueInCny: '¥15,234.56', valueInCny: '¥15,234.56',
tag: '含倍数资产: 246,913.56', tag: '含倍数资产: 246,913.56',
growthText: '每秒 +0.0015', growthText: '每秒 +0.0015',
@ -371,7 +368,8 @@ class AssetPage extends ConsumerWidget {
iconColor: _green, iconColor: _green,
iconBgColor: _feta, iconBgColor: _feta,
title: '绿积分', title: '绿积分',
amount: account?.tradingBalance ?? '88888.88', amount: account?.tradingBalance,
isLoading: isLoading,
valueInCny: '¥10,986.54', valueInCny: '¥10,986.54',
badge: '可提现', badge: '可提现',
badgeColor: _jewel, badgeColor: _jewel,
@ -385,6 +383,7 @@ class AssetPage extends ConsumerWidget {
iconBgColor: _serenade, iconBgColor: _serenade,
title: '待分配积分股', title: '待分配积分股',
amount: '1,234.56', amount: '1,234.56',
isLoading: isLoading,
subtitle: '次日开始参与分配', subtitle: '次日开始参与分配',
), ),
], ],
@ -396,7 +395,8 @@ class AssetPage extends ConsumerWidget {
required Color iconColor, required Color iconColor,
required Color iconBgColor, required Color iconBgColor,
required String title, required String title,
required String amount, String? amount,
bool isLoading = false,
String? valueInCny, String? valueInCny,
String? tag, String? tag,
String? growthText, String? growthText,
@ -468,9 +468,11 @@ class AssetPage extends ConsumerWidget {
], ],
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
// // -
Text( DataText(
formatAmount(amount), data: amount != null ? formatAmount(amount) : null,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -479,8 +481,10 @@ class AssetPage extends ConsumerWidget {
), ),
// //
if (valueInCny != null) if (valueInCny != null)
Text( DataText(
'$valueInCny', data: isLoading ? null : '$valueInCny',
isLoading: isLoading,
placeholder: '≈ ¥--',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF9CA3AF), color: Color(0xFF9CA3AF),
@ -511,8 +515,10 @@ class AssetPage extends ConsumerWidget {
color: _serenade, color: _serenade,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: DataText(
tag, data: isLoading ? null : tag,
isLoading: isLoading,
placeholder: '含倍数资产: --',
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 10,
color: _orange, color: _orange,
@ -523,10 +529,12 @@ class AssetPage extends ConsumerWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Row( Row(
children: [ children: [
Icon(Icons.bolt, size: 12, color: _green), const Icon(Icons.bolt, size: 12, color: _green),
Text( DataText(
growthText, data: isLoading ? null : growthText,
style: TextStyle( isLoading: isLoading,
placeholder: '每秒 +--',
style: const TextStyle(
fontSize: 10, fontSize: 10,
color: _green, color: _green,
), ),
@ -547,7 +555,7 @@ class AssetPage extends ConsumerWidget {
); );
} }
Widget _buildEarningsCard() { Widget _buildEarningsCard(account, bool isLoading) {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -589,19 +597,19 @@ class AssetPage extends ConsumerWidget {
// //
Row( Row(
children: [ children: [
_buildEarningsItem('累计收益', '12,345.67', _orange), _buildEarningsItem('累计收益', isLoading ? null : '12,345.67', _orange, isLoading),
Container( Container(
width: 1, width: 1,
height: 40, height: 40,
color: _serenade, color: _serenade,
), ),
_buildEarningsItem('今日收益', '+156.78', _green), _buildEarningsItem('今日收益', isLoading ? null : '+156.78', _green, isLoading),
Container( Container(
width: 1, width: 1,
height: 40, height: 40,
color: _serenade, 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( return Expanded(
child: Column( child: Column(
children: [ children: [
@ -622,8 +630,10 @@ class AssetPage extends ConsumerWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( DataText(
value, data: value,
isLoading: isLoading,
placeholder: '--',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -636,7 +646,7 @@ class AssetPage extends ConsumerWidget {
); );
} }
Widget _buildAccountList(account) { Widget _buildAccountList(account, bool isLoading) {
return Column( return Column(
children: [ children: [
// //
@ -644,7 +654,8 @@ class AssetPage extends ConsumerWidget {
icon: Icons.account_balance_wallet, icon: Icons.account_balance_wallet,
iconColor: _orange, iconColor: _orange,
title: '交易账户', title: '交易账户',
balance: account?.tradingBalance ?? '5678.90', balance: account?.tradingBalance,
isLoading: isLoading,
unit: '绿积分', unit: '绿积分',
status: '正常', status: '正常',
statusColor: _green, statusColor: _green,
@ -657,6 +668,7 @@ class AssetPage extends ConsumerWidget {
iconColor: _orange, iconColor: _orange,
title: '提现账户', title: '提现账户',
balance: '1,234.56', balance: '1,234.56',
isLoading: isLoading,
unit: '绿积分', unit: '绿积分',
status: '已绑定', status: '已绑定',
statusColor: const Color(0xFF9CA3AF), statusColor: const Color(0xFF9CA3AF),
@ -671,7 +683,8 @@ class AssetPage extends ConsumerWidget {
required IconData icon, required IconData icon,
required Color iconColor, required Color iconColor,
required String title, required String title,
required String balance, String? balance,
bool isLoading = false,
required String unit, required String unit,
required String status, required String status,
required Color statusColor, required Color statusColor,
@ -718,26 +731,26 @@ class AssetPage extends ConsumerWidget {
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
RichText( Row(
text: TextSpan( children: [
children: [ DataText(
TextSpan( data: balance,
text: '$balance ', isLoading: isLoading,
style: const TextStyle( placeholder: '--',
fontSize: 14, style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14,
color: _orange, fontWeight: FontWeight.bold,
), color: _orange,
), ),
TextSpan( ),
text: unit, Text(
style: const TextStyle( ' $unit',
fontSize: 12, style: const TextStyle(
color: Color(0xFF9CA3AF), 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),
],
),
),
);
}
} }

View File

@ -32,6 +32,12 @@ class ContributionPage extends ConsumerWidget {
); );
final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams)); 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( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
body: SafeArea( body: SafeArea(
@ -41,58 +47,54 @@ class ContributionPage extends ConsumerWidget {
ref.invalidate(contributionProvider(accountSequence)); ref.invalidate(contributionProvider(accountSequence));
ref.invalidate(contributionRecordsProvider(recordsParams)); ref.invalidate(contributionRecordsProvider(recordsParams));
}, },
child: contributionAsync.when( child: hasError && contribution == null
data: (contribution) { ? Center(
return CustomScrollView( child: Column(
slivers: [ mainAxisAlignment: MainAxisAlignment.center,
// children: [
SliverToBoxAdapter(child: _buildAppBar(context)), const Icon(Icons.error_outline, size: 48, color: AppColors.error),
// const SizedBox(height: 16),
SliverPadding( Text('加载失败: $error'),
padding: const EdgeInsets.all(16), const SizedBox(height: 16),
sliver: SliverList( ElevatedButton(
delegate: SliverChildListDelegate([ onPressed: () => ref.invalidate(contributionProvider(accountSequence)),
// child: const Text('重试'),
_buildTotalContributionCard(contribution), ),
const SizedBox(height: 16), ],
// ),
_buildThreeColumnStats(contribution), )
const SizedBox(height: 16), : CustomScrollView(
// slivers: [
_buildTodayEstimateCard(contribution), //
const SizedBox(height: 16), SliverToBoxAdapter(child: _buildAppBar(context)),
// //
_buildContributionDetailCard(context, ref, recordsAsync), SliverPadding(
const SizedBox(height: 16), padding: const EdgeInsets.all(16),
// sliver: SliverList(
_buildTeamStatsCard(contribution), delegate: SliverChildListDelegate([
const SizedBox(height: 16), //
// _buildTotalContributionCard(contribution, isLoading),
_buildExpirationCard(contribution, recordsAsync), const SizedBox(height: 16),
const SizedBox(height: 100), //
]), _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'; final total = contribution?.effectiveContribution ?? '0';
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@ -178,8 +180,10 @@ class ContributionPage extends ConsumerWidget {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( DataText(
formatAmount(total), data: isLoading ? null : formatAmount(total),
isLoading: isLoading,
placeholder: '----',
style: const TextStyle( style: const TextStyle(
fontSize: 30, fontSize: 30,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -212,7 +216,7 @@ class ContributionPage extends ConsumerWidget {
); );
} }
Widget _buildThreeColumnStats(Contribution? contribution) { Widget _buildThreeColumnStats(Contribution? contribution, bool isLoading) {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -221,15 +225,15 @@ class ContributionPage extends ConsumerWidget {
), ),
child: Row( child: Row(
children: [ children: [
_buildStatColumn('个人贡献值', contribution?.personalContribution ?? '0', false), _buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false),
_buildStatColumn('团队贡献值', contribution?.teamLevelContribution ?? '0', true), _buildStatColumn('团队贡献值', contribution?.teamLevelContribution, isLoading, true),
_buildStatColumn('省市公司', contribution?.systemContribution ?? '0', 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( return Expanded(
child: Container( child: Container(
decoration: showLeftBorder decoration: showLeftBorder
@ -242,9 +246,12 @@ class ContributionPage extends ConsumerWidget {
children: [ children: [
Text(label, style: const TextStyle(fontSize: 12, color: _grayText)), Text(label, style: const TextStyle(fontSize: 12, color: _grayText)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( DataText(
formatAmount(value), data: value != null ? formatAmount(value) : null,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _darkText), 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 // API
final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0; final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0;
// 10000 // 10000
@ -298,24 +305,29 @@ class ContributionPage extends ConsumerWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text.rich( isLoading
TextSpan( ? const ShimmerText(
children: [ placeholder: '-- 积分股',
TextSpan( style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _green),
text: hasContribution ? '计算中' : '--', )
style: TextStyle( : Text.rich(
fontSize: hasContribution ? 14 : 18, TextSpan(
fontWeight: FontWeight.bold, children: [
color: _green, 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, WidgetRef ref,
AsyncValue<ContributionRecordsPage?> recordsAsync, AsyncValue<ContributionRecordsPage?> recordsAsync,
) { ) {
final isRecordsLoading = recordsAsync.isLoading;
final recordsPage = recordsAsync.valueOrNull;
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -359,55 +374,83 @@ class ContributionPage extends ConsumerWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
recordsAsync.when( if (isRecordsLoading)
data: (recordsPage) { _buildRecordsShimmer()
if (recordsPage == null || recordsPage.data.isEmpty) { else if (recordsAsync.hasError && recordsPage == null)
return const Padding( 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(
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
child: Text( child: Text(
'加载失败', '加载失败',
style: TextStyle(fontSize: 14, color: _grayText.withOpacity(0.7)), 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) { Widget _buildDetailRow(ContributionRecord record) {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm'); final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
final formattedDate = dateFormat.format(record.createdAt); 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( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -453,18 +496,38 @@ class ContributionPage extends ConsumerWidget {
// //
Row( Row(
children: [ children: [
_buildTeamStatItem('直推人数', '${contribution?.directReferralAdoptedCount ?? 0}', ''), _buildTeamStatItem(
'直推人数',
contribution?.directReferralAdoptedCount.toString(),
'',
isLoading,
),
const SizedBox(width: 16), const SizedBox(width: 16),
_buildTeamStatItem('已解锁奖励', '${contribution?.unlockedBonusTiers ?? 0}', ''), _buildTeamStatItem(
'已解锁奖励',
contribution?.unlockedBonusTiers.toString(),
'',
isLoading,
),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
Row( Row(
children: [ children: [
_buildTeamStatItem('已解锁层级', '${contribution?.unlockedLevelDepth ?? 0}', ''), _buildTeamStatItem(
'已解锁层级',
contribution?.unlockedLevelDepth.toString(),
'',
isLoading,
),
const SizedBox(width: 16), 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( return Expanded(
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@ -485,21 +548,26 @@ class ContributionPage extends ConsumerWidget {
children: [ children: [
Text(label, style: const TextStyle(fontSize: 12, color: _grayText)), Text(label, style: const TextStyle(fontSize: 12, color: _grayText)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text.rich( isLoading
TextSpan( ? ShimmerText(
children: [ placeholder: unit.isNotEmpty ? '-- $unit' : '--',
TextSpan(
text: '$value ',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange),
), )
if (unit.isNotEmpty) : Text.rich(
TextSpan( TextSpan(
text: unit, children: [
style: const TextStyle(fontSize: 12, color: _grayText), 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( Widget _buildExpirationCard(
Contribution? contribution, Contribution? contribution,
AsyncValue<ContributionRecordsPage?> recordsAsync, AsyncValue<ContributionRecordsPage?> recordsAsync,
bool isLoading,
) { ) {
final isRecordsLoading = recordsAsync.isLoading;
final recordsPage = recordsAsync.valueOrNull;
// //
DateTime? nearestExpireDate; DateTime? nearestExpireDate;
recordsAsync.whenData((recordsPage) { if (recordsPage != null && recordsPage.data.isNotEmpty) {
if (recordsPage != null && recordsPage.data.isNotEmpty) { //
// final activeRecords = recordsPage.data.where((r) => !r.isExpired).toList();
final activeRecords = recordsPage.data.where((r) => !r.isExpired).toList(); if (activeRecords.isNotEmpty) {
if (activeRecords.isNotEmpty) { activeRecords.sort((a, b) => a.expireDate.compareTo(b.expireDate));
activeRecords.sort((a, b) => a.expireDate.compareTo(b.expireDate)); nearestExpireDate = activeRecords.first.expireDate;
nearestExpireDate = activeRecords.first.expireDate;
}
} }
}); }
// //
final now = DateTime.now(); final now = DateTime.now();
@ -530,15 +600,17 @@ class ContributionPage extends ConsumerWidget {
String expireDateText = '暂无过期信息'; String expireDateText = '暂无过期信息';
if (nearestExpireDate != null) { if (nearestExpireDate != null) {
daysRemaining = nearestExpireDate!.difference(now).inDays; daysRemaining = nearestExpireDate.difference(now).inDays;
if (daysRemaining < 0) daysRemaining = 0; if (daysRemaining < 0) daysRemaining = 0;
// 730 // 730
progress = daysRemaining / 730; progress = daysRemaining / 730;
if (progress > 1) progress = 1; if (progress > 1) progress = 1;
if (progress < 0) progress = 0; 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( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -564,23 +636,35 @@ class ContributionPage extends ConsumerWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: progress, value: showShimmer ? 1.0 : progress,
minHeight: 10, minHeight: 10,
backgroundColor: _bgGray, backgroundColor: _bgGray,
valueColor: const AlwaysStoppedAnimation<Color>(_orange), valueColor: AlwaysStoppedAnimation<Color>(
showShimmer ? _bgGray : _orange,
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// //
Text( showShimmer
expireDateText, ? const ShimmerText(
style: const TextStyle(fontSize: 12, color: _grayText), placeholder: '您的贡献值将于 ---- 失效',
), style: TextStyle(fontSize: 12, color: _grayText),
)
: Text(
expireDateText,
style: const TextStyle(fontSize: 12, color: _grayText),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( showShimmer
'剩余 $daysRemaining', ? const ShimmerText(
style: const TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500), 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), const SizedBox(height: 8),
// //
Container( Container(

View File

@ -44,6 +44,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
final user = ref.watch(userNotifierProvider); final user = ref.watch(userNotifierProvider);
final accountSequence = user.accountSequence ?? ''; 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( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
body: SafeArea( body: SafeArea(
@ -57,20 +62,16 @@ class _TradingPageState extends ConsumerState<TradingPage> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
// // - always render, use shimmer for loading
globalState.when( hasError
data: (state) => _buildPriceCard(state), ? _buildErrorCard('价格加载失败')
loading: () => _buildLoadingCard(), : _buildPriceCard(state, isLoading),
error: (_, __) => _buildErrorCard('价格加载失败'),
),
// K线图占位区域 // K线图占位区域
_buildChartSection(), _buildChartSection(),
// // - always render, use shimmer for loading
globalState.when( hasError
data: (state) => _buildMarketDataCard(state), ? _buildErrorCard('市场数据加载失败')
loading: () => _buildLoadingCard(), : _buildMarketDataCard(state, isLoading),
error: (_, __) => _buildErrorCard('市场数据加载失败'),
),
// / // /
_buildTradingPanel(accountSequence), _buildTradingPanel(accountSequence),
// //
@ -149,10 +150,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
); );
} }
Widget _buildPriceCard(state) { Widget _buildPriceCard(dynamic state, bool isLoading) {
final isPriceUp = state?.isPriceUp ?? true; final isPriceUp = state?.isPriceUp ?? true;
final currentPrice = state?.currentPrice ?? '0.000156'; final currentPrice = state?.currentPrice;
final priceChange = state?.priceChange24h ?? '8.52'; final priceChange = state?.priceChange24h;
return Container( return Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
@ -176,8 +177,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
color: _grayText, color: _grayText,
), ),
), ),
Text( DataText(
'= 156.00 绿积分', data: '= 156.00 绿积分',
isLoading: isLoading,
placeholder: '= -- 绿积分',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _grayText, color: _grayText,
@ -190,8 +193,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( AmountText(
'¥ ${formatPrice(currentPrice)}', amount: currentPrice != null ? formatPrice(currentPrice) : null,
isLoading: isLoading,
prefix: '\u00A5 ',
style: const TextStyle( style: const TextStyle(
fontSize: 30, fontSize: 30,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -214,8 +219,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
size: 16, size: 16,
color: _green, color: _green,
), ),
Text( DataText(
'+$priceChange%', data: priceChange != null ? '+$priceChange%' : null,
isLoading: isLoading,
placeholder: '+--.--%',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -322,7 +329,12 @@ class _TradingPageState extends ConsumerState<TradingPage> {
); );
} }
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( return Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@ -359,10 +371,20 @@ class _TradingPageState extends ConsumerState<TradingPage> {
// //
Row( Row(
children: [ children: [
_buildMarketDataItem('积分股池', '8,888,888,888', _orange), _buildMarketDataItem(
'积分股池',
sharesPool ?? '8,888,888,888',
_orange,
isLoading,
),
Container(width: 1, height: 24, color: _bgGray), Container(width: 1, height: 24, color: _bgGray),
const SizedBox(width: 16), const SizedBox(width: 16),
_buildMarketDataItem('流通池', '1,234,567', _orange), _buildMarketDataItem(
'流通池',
circulatingPool ?? '1,234,567',
_orange,
isLoading,
),
], ],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -371,10 +393,20 @@ class _TradingPageState extends ConsumerState<TradingPage> {
// //
Row( Row(
children: [ children: [
_buildMarketDataItem('绿积分池', '99,999,999', _orange), _buildMarketDataItem(
'绿积分池',
greenPointsPool ?? '99,999,999',
_orange,
isLoading,
),
Container(width: 1, height: 24, color: _bgGray), Container(width: 1, height: 24, color: _bgGray),
const SizedBox(width: 16), const SizedBox(width: 16),
_buildMarketDataItem('黑洞销毁量', '50,000,000', _red), _buildMarketDataItem(
'黑洞销毁量',
blackHoleBurned ?? '50,000,000',
_red,
isLoading,
),
], ],
), ),
], ],
@ -382,7 +414,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
); );
} }
Widget _buildMarketDataItem(String label, String value, Color valueColor) { Widget _buildMarketDataItem(String label, String value, Color valueColor, bool isLoading) {
return Expanded( return Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -395,8 +427,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( DataText(
value, data: value,
isLoading: isLoading,
placeholder: '--,---,---',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -767,29 +801,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
); );
} }
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) { Widget _buildErrorCard(String message) {
return Container( return Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),

View File

@ -1,15 +1,147 @@
import 'package:flutter/material.dart'; 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<ShimmerText> createState() => _ShimmerTextState();
}
class _ShimmerTextState extends State<ShimmerText>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
)..repeat(reverse: true);
_animation = Tween<double>(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 { class ShimmerLoading extends StatefulWidget {
final Widget child; final Widget child;
final bool isLoading;
const ShimmerLoading({ const ShimmerLoading({super.key, required this.child});
super.key,
required this.child,
this.isLoading = true,
});
@override @override
State<ShimmerLoading> createState() => _ShimmerLoadingState(); State<ShimmerLoading> createState() => _ShimmerLoadingState();
@ -24,11 +156,11 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
void initState() { void initState() {
super.initState(); super.initState();
_controller = AnimationController( _controller = AnimationController(
duration: const Duration(milliseconds: 1500), duration: const Duration(milliseconds: 1000),
vsync: this, vsync: this,
)..repeat(); )..repeat(reverse: true);
_animation = Tween<double>(begin: -2, end: 2).animate( _animation = Tween<double>(begin: 0.4, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
); );
} }
@ -40,29 +172,11 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!widget.isLoading) return widget.child;
return AnimatedBuilder( return AnimatedBuilder(
animation: _animation, animation: _animation,
builder: (context, child) { builder: (context, child) {
return ShaderMask( return Opacity(
shaderCallback: (bounds) { opacity: _animation.value,
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,
child: widget.child, child: widget.child,
); );
}, },
@ -70,17 +184,17 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
} }
} }
/// ///
class ShimmerBox extends StatelessWidget { class ShimmerBox extends StatelessWidget {
final double? width; final double? width;
final double height; final double height;
final double borderRadius; final BorderRadius? borderRadius;
const ShimmerBox({ const ShimmerBox({
super.key, super.key,
this.width, this.width,
required this.height, this.height = 16,
this.borderRadius = 8, this.borderRadius,
}); });
@override @override
@ -90,199 +204,36 @@ class ShimmerBox extends StatelessWidget {
height: height, height: height,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE5E7EB), 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 { class PageSkeleton extends StatelessWidget {
const PageSkeleton({super.key}); const PageSkeleton({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return ShimmerLoading(
padding: const EdgeInsets.all(16), child: SingleChildScrollView(
child: Column( padding: const EdgeInsets.all(16),
children: const [ child: Column(
AssetCardSkeleton(), crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(height: 24), children: [
AssetItemSkeleton(), const SizedBox(height: 60),
SizedBox(height: 16), const ShimmerBox(width: 120, height: 20),
AssetItemSkeleton(), const SizedBox(height: 16),
SizedBox(height: 16), const ShimmerBox(height: 100),
AssetItemSkeleton(), const SizedBox(height: 24),
SizedBox(height: 24), const ShimmerBox(height: 80),
EarningsStatsSkeleton(), const SizedBox(height: 24),
], const ShimmerBox(height: 120),
const SizedBox(height: 24),
const ShimmerBox(height: 80),
],
),
), ),
); );
} }