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/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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue