rwadurian/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart

864 lines
28 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/router/routes.dart';
import '../../../core/utils/format_utils.dart';
import '../../../domain/entities/asset_display.dart';
import '../../providers/user_providers.dart';
import '../../providers/asset_providers.dart';
import '../../providers/mining_providers.dart';
import '../../widgets/shimmer_loading.dart';
class AssetPage extends ConsumerStatefulWidget {
const AssetPage({super.key});
@override
ConsumerState<AssetPage> createState() => _AssetPageState();
}
class _AssetPageState extends ConsumerState<AssetPage> {
// 设计色彩
static const Color _orange = Color(0xFFFF6B00);
static const Color _green = Color(0xFF10B981);
static const Color _grayText = Color(0xFF6B7280);
static const Color _darkText = Color(0xFF1F2937);
static const Color _bgGray = Color(0xFFF3F4F6);
static const Color _riverBed = Color(0xFF4B5563);
static const Color _serenade = Color(0xFFFFF7ED);
static const Color _feta = Color(0xFFF0FDF4);
static const Color _scandal = Color(0xFFDCFCE7);
static const Color _jewel = Color(0xFF15803D);
// 实时刷新相关状态
Timer? _refreshTimer;
int _elapsedSeconds = 0;
double _initialShareBalance = 0;
double _growthPerSecond = 0;
String? _lastAccountSequence;
bool _timerStarted = false;
@override
void dispose() {
_refreshTimer?.cancel();
super.dispose();
}
/// 启动定时器(使用外部传入的每秒增长值)
void _startTimerWithGrowth(AssetDisplay asset, String perSecondEarning) {
// 防止重复启动
if (_timerStarted && _refreshTimer != null) {
return;
}
_refreshTimer?.cancel();
_elapsedSeconds = 0;
_initialShareBalance = double.tryParse(asset.shareBalance) ?? 0;
// 使用传入的每秒增长值(来自 mining-service
_growthPerSecond = double.tryParse(perSecondEarning) ?? 0;
_timerStarted = true;
_refreshTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_elapsedSeconds++;
});
}
});
}
/// 重置定时器(刷新时调用)
void _resetTimer() {
_refreshTimer?.cancel();
_refreshTimer = null;
_elapsedSeconds = 0;
_timerStarted = false;
}
/// 计算当前实时资产显示值
/// 资产显示值 = 积分股余额 × price不含倍数使用实际价格
double get _currentDisplayValue {
final price = double.tryParse(_lastAsset?.currentPrice ?? '0') ?? 0;
return _currentShareBalance * price;
}
/// 计算当前实时积分股余额
double get _currentShareBalance {
return _initialShareBalance + (_elapsedSeconds * _growthPerSecond);
}
AssetDisplay? _lastAsset;
@override
Widget build(BuildContext context) {
final user = ref.watch(userNotifierProvider);
final accountSequence = user.accountSequence ?? '';
// 使用 public API不依赖 JWT token
final assetAsync = ref.watch(accountAssetProvider(accountSequence));
// 从 mining-service 获取每秒收益
final shareAccountAsync = ref.watch(shareAccountProvider(accountSequence));
// 提取数据和加载状态
final isLoading = assetAsync.isLoading || accountSequence.isEmpty;
final asset = assetAsync.valueOrNull;
final shareAccount = shareAccountAsync.valueOrNull;
// 获取每秒收益(优先使用 mining-service 的数据)
final perSecondEarning = shareAccount?.perSecondEarning ?? '0';
final hasValidGrowth = (double.tryParse(perSecondEarning) ?? 0) > 0;
// 当数据加载完成时启动定时器
if (asset != null && hasValidGrowth) {
// 账户切换或首次加载时重置并启动定时器
if (_lastAccountSequence != accountSequence) {
_lastAccountSequence = accountSequence;
_lastAsset = asset;
_resetTimer();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _startTimerWithGrowth(asset, perSecondEarning);
});
} else if (!_timerStarted) {
// 定时器未启动时启动(例如页面刚进入)
_lastAsset = asset;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _startTimerWithGrowth(asset, perSecondEarning);
});
} else {
_lastAsset = asset;
}
} else if (asset != null) {
_lastAsset = asset;
}
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
body: SafeArea(
bottom: false,
child: LayoutBuilder(
builder: (context, constraints) {
return RefreshIndicator(
onRefresh: () async {
_resetTimer();
_lastAsset = null;
ref.invalidate(accountAssetProvider(accountSequence));
ref.invalidate(shareAccountProvider(accountSequence));
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Column(
children: [
// 顶部导航栏
_buildAppBar(context, user),
// 内容
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
const SizedBox(height: 8),
// 总资产卡片 - 始终显示,数字部分闪烁,实时刷新
_buildTotalAssetCard(asset, isLoading, _currentDisplayValue, _currentShareBalance, perSecondEarning),
const SizedBox(height: 24),
// 快捷操作按钮
_buildQuickActions(context),
const SizedBox(height: 24),
// 资产列表 - 始终显示,数字部分闪烁,实时刷新
_buildAssetList(asset, isLoading, _currentShareBalance, perSecondEarning),
const SizedBox(height: 24),
// 交易统计
_buildEarningsCard(asset, isLoading),
const SizedBox(height: 24),
// 账户列表
_buildAccountList(asset, isLoading),
const SizedBox(height: 24),
],
),
),
],
),
),
),
);
},
),
),
);
}
Widget _buildAppBar(BuildContext context, user) {
return Container(
color: _bgGray.withOpacity(0.9),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: const Center(
child: Text(
'我的资产',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF111827),
),
),
),
);
}
Widget _buildTotalAssetCard(AssetDisplay? asset, bool isLoading, double currentDisplayValue, double currentShareBalance, String perSecondEarning) {
// 使用传入的每秒增长值(来自 mining-service
final growthPerSecond = double.tryParse(perSecondEarning) ?? 0.0;
// 使用实时计算的资产值(如果有)- 不含倍数
final displayValue = asset != null && currentDisplayValue > 0
? currentDisplayValue.toString()
: null;
// 使用实时计算的积分股余额(如果有)- 实际数量,不含倍数
final shareBalance = asset != null && currentShareBalance > 0
? currentShareBalance.toString()
: asset?.shareBalance;
return Container(
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: Stack(
children: [
// 背景装饰圆
Positioned(
right: -20,
top: -40,
child: Container(
width: 128,
height: 128,
decoration: BoxDecoration(
color: _serenade,
borderRadius: BorderRadius.circular(64),
),
),
),
// 顶部渐变条
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: 4,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFFF6B00), Color(0xFFFDBA74)],
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
),
),
// 内容
Padding(
padding: const EdgeInsets.fromLTRB(24, 20, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
children: [
const Text(
'总资产估值',
style: TextStyle(
fontSize: 14,
color: _grayText,
),
),
const SizedBox(width: 8),
Icon(
Icons.visibility_outlined,
size: 14,
color: _grayText.withOpacity(0.5),
),
],
),
const SizedBox(height: 8),
// 金额 - 实时刷新显示
AmountText(
amount: displayValue != null ? formatAmount(displayValue) : null,
isLoading: isLoading,
prefix: '¥ ',
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: _orange,
letterSpacing: -0.75,
),
),
const SizedBox(height: 4),
// 积分股余额 - 实际数量
DataText(
data: shareBalance != null ? '${formatCompact(shareBalance)} 积分股' : null,
isLoading: isLoading,
placeholder: '≈ -- 积分股',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF9CA3AF),
),
),
const SizedBox(height: 12),
// 每秒增长
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _feta,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.bolt, size: 14, color: _green),
const SizedBox(width: 6),
DataText(
data: asset != null
? '+${formatDecimal(growthPerSecond.toString(), 8)}/秒'
: null,
isLoading: isLoading,
placeholder: '+--/秒',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _green,
),
),
],
),
),
],
),
),
],
),
);
}
Widget _buildQuickActions(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildQuickActionItem(
Icons.add,
'接收',
_orange,
() => context.push(Routes.receiveShares),
),
_buildQuickActionItem(
Icons.remove,
'发送',
_orange,
() => context.push(Routes.sendShares),
),
_buildQuickActionItem(
Icons.people_outline,
'C2C',
_orange,
() => context.push(Routes.c2cMarket),
),
],
);
}
Widget _buildQuickActionItem(
IconData icon,
String label,
Color color,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _serenade,
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _riverBed,
),
),
],
),
);
}
Widget _buildAssetList(AssetDisplay? asset, bool isLoading, double currentShareBalance, String perSecondEarning) {
// 使用实时积分股余额
final shareBalance = asset != null && currentShareBalance > 0
? currentShareBalance
: double.tryParse(asset?.shareBalance ?? '0') ?? 0;
final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0;
final multipliedAsset = shareBalance * multiplier;
final currentPrice = double.tryParse(asset?.currentPrice ?? '0') ?? 0;
return Column(
children: [
// 积分股 - 实时刷新
_buildAssetItem(
icon: Icons.trending_up,
iconColor: _orange,
iconBgColor: _serenade,
title: '积分股',
amount: asset != null ? shareBalance.toString() : null,
isLoading: isLoading,
valueInCny: asset != null
? '¥${formatAmount((shareBalance * currentPrice).toString())}'
: null,
tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null,
growthText: asset != null ? '每秒 +${formatDecimal(perSecondEarning, 8)}' : null,
),
const SizedBox(height: 16),
// 积分值(现金余额)
_buildAssetItem(
icon: Icons.eco,
iconColor: _green,
iconBgColor: _feta,
title: '积分值',
amount: asset?.cashBalance,
isLoading: isLoading,
valueInCny: asset != null ? '¥${formatAmount(asset.cashBalance)}' : null,
),
const SizedBox(height: 16),
// 冻结积分股
_buildAssetItem(
icon: Icons.lock_outline,
iconColor: _orange,
iconBgColor: _serenade,
title: '冻结积分股',
amount: asset?.frozenShares,
isLoading: isLoading,
subtitle: '交易挂单中',
),
],
);
}
Widget _buildAssetItem({
required IconData icon,
required Color iconColor,
required Color iconBgColor,
required String title,
String? amount,
bool isLoading = false,
String? valueInCny,
String? tag,
String? growthText,
String? badge,
Color? badgeColor,
Color? badgeBgColor,
String? subtitle,
}) {
return 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: [
// 图标
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: iconColor, size: 24),
),
const SizedBox(width: 12),
// 内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF111827),
),
),
if (badge != null) ...[
const SizedBox(width: 7),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: badgeBgColor ?? _scandal,
borderRadius: BorderRadius.circular(9999),
),
child: Text(
badge,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: badgeColor ?? _jewel,
),
),
),
],
],
),
const SizedBox(height: 2),
// 数量 - 闪烁占位符
DataText(
data: amount != null ? formatAmount(amount) : null,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF111827),
),
),
// 估值
if (valueInCny != null)
DataText(
data: isLoading ? null : '$valueInCny',
isLoading: isLoading,
placeholder: '≈ ¥--',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
// 副标题
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(top: 3),
child: Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
),
// 标签行
if (tag != null || growthText != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
if (tag != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _serenade,
borderRadius: BorderRadius.circular(12),
),
child: DataText(
data: isLoading ? null : tag,
isLoading: isLoading,
placeholder: '含倍数资产: --',
style: const TextStyle(
fontSize: 10,
color: _orange,
),
),
),
if (growthText != null) ...[
const SizedBox(width: 8),
Row(
children: [
const Icon(Icons.bolt, size: 12, color: _green),
DataText(
data: isLoading ? null : growthText,
isLoading: isLoading,
placeholder: '每秒 +--',
style: const TextStyle(
fontSize: 10,
color: _green,
),
),
],
),
],
],
),
),
],
),
),
// 箭头
Icon(Icons.chevron_right, size: 14, color: _grayText.withOpacity(0.5)),
],
),
);
}
Widget _buildEarningsCard(AssetDisplay? asset, bool isLoading) {
return 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: _orange,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
const Text(
'交易统计',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: _darkText,
),
),
],
),
const SizedBox(height: 16),
// 统计数据
Row(
children: [
_buildEarningsItem(
'累计买入',
asset != null ? formatCompact(asset.totalBought) : null,
_orange,
isLoading,
),
Container(
width: 1,
height: 40,
color: _serenade,
),
_buildEarningsItem(
'累计卖出',
asset != null ? formatCompact(asset.totalSold) : null,
_green,
isLoading,
),
Container(
width: 1,
height: 40,
color: _serenade,
),
_buildEarningsItem(
'销毁倍数',
asset != null ? '${formatDecimal(asset.burnMultiplier, 4)}x' : null,
const Color(0xFF9CA3AF),
isLoading,
),
],
),
],
),
);
}
Widget _buildEarningsItem(String label, String? value, Color valueColor, bool isLoading) {
return Expanded(
child: Column(
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: _grayText,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
DataText(
data: value,
isLoading: isLoading,
placeholder: '--',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: valueColor,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildAccountList(AssetDisplay? asset, bool isLoading) {
return Column(
children: [
// 交易账户(可用现金)
_buildAccountItem(
icon: Icons.account_balance_wallet,
iconColor: _orange,
title: '可用积分值',
balance: asset?.availableCash,
isLoading: isLoading,
unit: '积分值',
status: '可交易',
statusColor: _green,
statusBgColor: _feta,
),
const SizedBox(height: 16),
// 冻结现金
_buildAccountItem(
icon: Icons.lock_outline,
iconColor: _orange,
title: '冻结积分值',
balance: asset?.frozenCash,
isLoading: isLoading,
unit: '积分值',
status: '挂单中',
statusColor: const Color(0xFF9CA3AF),
statusBgColor: Colors.white,
statusBorder: true,
),
],
);
}
Widget _buildAccountItem({
required IconData icon,
required Color iconColor,
required String title,
String? balance,
bool isLoading = false,
required String unit,
required String status,
required Color statusColor,
required Color statusBgColor,
bool statusBorder = false,
}) {
return 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: [
// 图标
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: _serenade,
borderRadius: BorderRadius.circular(18),
),
child: Icon(icon, color: iconColor, size: 20),
),
const SizedBox(width: 12),
// 内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF111827),
),
),
const SizedBox(height: 2),
Row(
children: [
DataText(
data: balance != null ? formatAmount(balance) : null,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: _orange,
),
),
Text(
' $unit',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
],
),
],
),
),
// 状态标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusBgColor,
borderRadius: BorderRadius.circular(9999),
border: statusBorder ? Border.all(color: const Color(0xFFE5E7EB)) : null,
),
child: Text(
status,
style: TextStyle(
fontSize: 10,
color: statusColor,
),
),
),
const SizedBox(width: 8),
// 箭头
Icon(Icons.chevron_right, size: 14, color: _grayText.withOpacity(0.5)),
],
),
);
}
}