asset_overview_tab.dart 9.2 KB

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