asset_overview_tab.dart 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../../core/l10n/app_localizations.dart';
  5. import '../../../core/theme/app_colors.dart';
  6. import '../../../core/utils/number_format.dart';
  7. import '../../../providers/asset_provider.dart';
  8. import '../../../providers/currency_provider.dart';
  9. import '../../widgets/common/app_refresh_indicator.dart';
  10. import 'asset_screen.dart' show AssetAccountRow;
  11. /// 总览 Tab
  12. class AssetOverviewTab extends ConsumerWidget {
  13. const AssetOverviewTab({
  14. super.key,
  15. required this.state,
  16. required this.notifier,
  17. this.onNavigateToStaking,
  18. });
  19. final AssetState state;
  20. final AssetNotifier notifier;
  21. final VoidCallback? onNavigateToStaking;
  22. @override
  23. Widget build(BuildContext context, WidgetRef ref) {
  24. ref.watch(currencyProvider); // 法币切换时触发重建
  25. final cs = Theme.of(context).colorScheme;
  26. final total = state.totalUsdtValue;
  27. final display =
  28. state.obscureBalance ? '******' : formatPrice(total, decimalPlaces: 2);
  29. final pnl = state.todayPnl;
  30. final pnlRevenue = pnl?.revenue.toDouble() ?? 0;
  31. final double? pnlRate = pnl?.revenueRate?.toDouble();
  32. final pnlSign = pnlRevenue >= 0 ? '+' : '';
  33. final pnlColor = pnl?.isUpTrend == true
  34. ? AppColors.rise
  35. : pnl?.isUpTrend == false
  36. ? AppColors.fall
  37. : cs.onSurface.withAlpha(153);
  38. return AppRefreshIndicator(
  39. onRefresh: notifier.silentRefresh,
  40. child: ListView(
  41. children: [
  42. // 资产估值
  43. Padding(
  44. padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
  45. child: Column(
  46. crossAxisAlignment: CrossAxisAlignment.start,
  47. children: [
  48. Row(
  49. children: [
  50. Text(AppLocalizations.of(context)!.assetValuation,
  51. style: TextStyle(
  52. color: cs.onSurface.withAlpha(153), fontSize: 13)),
  53. const SizedBox(width: 6),
  54. GestureDetector(
  55. onTap: notifier.toggleObscure,
  56. child: Icon(
  57. state.obscureBalance
  58. ? Icons.visibility_off_outlined
  59. : Icons.visibility_outlined,
  60. size: 16,
  61. color: cs.onSurface.withAlpha(153),
  62. ),
  63. ),
  64. ],
  65. ),
  66. const SizedBox(height: 8),
  67. Row(
  68. crossAxisAlignment: CrossAxisAlignment.end,
  69. children: [
  70. Text(display,
  71. style: TextStyle(
  72. color: cs.onSurface,
  73. fontSize: 32,
  74. fontWeight: FontWeight.w700,
  75. letterSpacing: -0.5)),
  76. const SizedBox(width: 6),
  77. Padding(
  78. padding: const EdgeInsets.only(bottom: 5),
  79. child: Text('USDT',
  80. style: TextStyle(
  81. color: cs.onSurface.withAlpha(153),
  82. fontSize: 14)),
  83. ),
  84. ],
  85. ),
  86. const SizedBox(height: 6),
  87. // 今日盈亏
  88. Text(
  89. state.obscureBalance
  90. ? '${AppLocalizations.of(context)!.todayPnl} ******'
  91. : pnlRate != null
  92. ? '${AppLocalizations.of(context)!.todayPnl} $pnlSign$fiatSymbol${(pnlRevenue * fiatRate).toStringAsFixed(2)} ($pnlSign${(pnlRate * 100).toStringAsFixed(2)}%)'
  93. : '${AppLocalizations.of(context)!.todayPnl} $pnlSign$fiatSymbol${(pnlRevenue * fiatRate).toStringAsFixed(2)} (--)',
  94. style: TextStyle(
  95. color: pnlColor,
  96. fontSize: 13,
  97. fontWeight: FontWeight.w500),
  98. ),
  99. ],
  100. ),
  101. ),
  102. // 快捷操作
  103. Padding(
  104. padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
  105. child: Row(
  106. children: [
  107. Expanded(
  108. child: _ActionChip(
  109. label: AppLocalizations.of(context)!.recharge,
  110. onTap: () => context.push('/asset/deposit'),
  111. ),
  112. ),
  113. const SizedBox(width: 8),
  114. Expanded(
  115. child: _ActionChip(
  116. label: AppLocalizations.of(context)!.withdraw,
  117. onTap: () => context.push('/asset/withdraw'),
  118. ),
  119. ),
  120. const SizedBox(width: 8),
  121. Expanded(
  122. child: _ActionChip(
  123. label: AppLocalizations.of(context)!.transfer,
  124. onTap: () => context.push('/asset/transfer'),
  125. ),
  126. ),
  127. const SizedBox(width: 8),
  128. Expanded(
  129. child: _ActionChip(
  130. label: AppLocalizations.of(context)!.fundHistory,
  131. onTap: () => context.push('/asset/history'),
  132. ),
  133. ),
  134. ],
  135. ),
  136. ),
  137. const SizedBox(height: 24),
  138. _AccountSection(
  139. state: state,
  140. onStakingTap: onNavigateToStaking,
  141. ),
  142. const SizedBox(height: 32),
  143. ],
  144. ),
  145. );
  146. }
  147. }
  148. class _ActionChip extends StatelessWidget {
  149. const _ActionChip({required this.label, required this.onTap});
  150. final String label;
  151. final VoidCallback onTap;
  152. @override
  153. Widget build(BuildContext context) {
  154. final cs = Theme.of(context).colorScheme;
  155. final isDark = Theme.of(context).brightness == Brightness.dark;
  156. return GestureDetector(
  157. onTap: onTap,
  158. child: Container(
  159. width: double.infinity,
  160. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
  161. decoration: BoxDecoration(
  162. color: isDark ? AppColors.darkBgTertiary : AppColors.lightBgTertiary,
  163. borderRadius: BorderRadius.circular(20),
  164. ),
  165. alignment: Alignment.center,
  166. child: FittedBox(
  167. fit: BoxFit.scaleDown,
  168. child: Text(
  169. label,
  170. maxLines: 1,
  171. overflow: TextOverflow.visible,
  172. textAlign: TextAlign.center,
  173. style: TextStyle(
  174. color: cs.onSurface,
  175. fontSize: 13,
  176. fontWeight: FontWeight.w600,
  177. ),
  178. ),
  179. ),
  180. ),
  181. );
  182. }
  183. }
  184. class _AccountSection extends StatelessWidget {
  185. const _AccountSection({required this.state, this.onStakingTap});
  186. final AssetState state;
  187. final VoidCallback? onStakingTap;
  188. String _fmtUsdt(double bal, bool obscure) =>
  189. obscure ? '******' : '${formatPrice(bal, decimalPlaces: 2)} USDT';
  190. String _fmtUsd(double bal, bool obscure) =>
  191. obscure ? '******' : formatFiatPrice(bal, pricePrecision: 2);
  192. String _fmtIbit(String amount, bool obscure) {
  193. if (obscure) {
  194. return '******';
  195. }
  196. final parsed = double.tryParse(amount) ?? 0;
  197. return '${formatPrice(parsed, decimalPlaces: 2)} iBit';
  198. }
  199. @override
  200. Widget build(BuildContext context) {
  201. final cs = Theme.of(context).colorScheme;
  202. final obscure = state.obscureBalance;
  203. final l10n = AppLocalizations.of(context)!;
  204. final swap = state.walletBalance('SWAP').toDouble();
  205. final follow = state.walletBalance('FOLLOW').toDouble();
  206. final fund = state.walletBalance('SPOT').toDouble();
  207. final spotTrading = state.spotTradingTotal;
  208. final stakingLocked = state.stakingOverviewLocked;
  209. final rows = <({
  210. IconData icon,
  211. String label,
  212. String amount,
  213. String usdAmount,
  214. VoidCallback? onTap,
  215. })>[
  216. (
  217. icon: Icons.account_balance_wallet_outlined,
  218. label: l10n.fundAccount,
  219. amount: _fmtUsdt(fund, obscure),
  220. usdAmount: _fmtUsd(fund, obscure),
  221. onTap: null,
  222. ),
  223. (
  224. icon: Icons.currency_exchange_outlined,
  225. label: l10n.spotTradingAccount,
  226. amount: _fmtUsdt(spotTrading, obscure),
  227. usdAmount: _fmtUsd(spotTrading, obscure),
  228. onTap: null,
  229. ),
  230. (
  231. icon: Icons.show_chart,
  232. label: l10n.futuresAccount,
  233. amount: _fmtUsdt(swap, obscure),
  234. usdAmount: _fmtUsd(swap, obscure),
  235. onTap: null,
  236. ),
  237. (
  238. icon: Icons.people_alt_outlined,
  239. label: l10n.copyAccount,
  240. amount: _fmtUsdt(follow, obscure),
  241. usdAmount: _fmtUsd(follow, obscure),
  242. onTap: null,
  243. ),
  244. (
  245. icon: Icons.lock_outline,
  246. label: l10n.assetsLockedStakingAccount,
  247. amount: _fmtIbit(stakingLocked, obscure),
  248. usdAmount: '',
  249. onTap: onStakingTap,
  250. ),
  251. ];
  252. return Padding(
  253. padding: const EdgeInsets.symmetric(horizontal: 16),
  254. child: Column(
  255. crossAxisAlignment: CrossAxisAlignment.start,
  256. children: [
  257. Text(l10n.account,
  258. style: TextStyle(
  259. color: cs.onSurface,
  260. fontSize: 15,
  261. fontWeight: FontWeight.w600)),
  262. const SizedBox(height: 12),
  263. for (final row in rows) ...[
  264. AssetAccountRow(
  265. icon: row.icon,
  266. label: row.label,
  267. amount: row.amount,
  268. usdAmount: row.usdAmount,
  269. onTap: row.onTap,
  270. ),
  271. const SizedBox(height: 8),
  272. ],
  273. ],
  274. ),
  275. );
  276. }
  277. }