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:
parent
b1525bdfa6
commit
eba4b3b6e5
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ContributionRecordsPage?> 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<ContributionRecordsPage?> 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<Color>(_orange),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
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<TradingPage> {
|
|||
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<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
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<TradingPage> {
|
|||
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<TradingPage> {
|
|||
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<TradingPage> {
|
|||
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<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(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
|
|
@ -359,10 +371,20 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
// 第一行数据
|
||||
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<TradingPage> {
|
|||
// 第二行数据
|
||||
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<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
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<TradingPage> {
|
|||
),
|
||||
),
|
||||
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<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) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
||||
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<ShimmerLoading> createState() => _ShimmerLoadingState();
|
||||
|
|
@ -24,11 +156,11 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
|
|||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
_animation = Tween<double>(begin: -2, end: 2).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine),
|
||||
)..repeat(reverse: true);
|
||||
_animation = Tween<double>(begin: 0.4, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -40,29 +172,11 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
|
|||
|
||||
@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<ShimmerLoading>
|
|||
}
|
||||
}
|
||||
|
||||
/// 骨架屏占位框
|
||||
/// 闪烁占位块(向后兼容)
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue