asset_spot_tab.dart 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import 'dart:ui';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import '../../../core/l10n/app_localizations.dart';
  6. import '../../../core/theme/app_colors.dart';
  7. import '../../../core/utils/number_format.dart';
  8. import '../../../providers/asset_provider.dart';
  9. import '../../../providers/spot_coin_cache_provider.dart';
  10. import '../../../providers/spot_provider.dart' show SpotWalletAsset;
  11. import '../../widgets/common/app_refresh_indicator.dart';
  12. import '../../widgets/common/coin_icon.dart';
  13. /// 资金 Tab — 显示 SPOT 资金账户余额
  14. class AssetSpotTab extends StatelessWidget {
  15. const AssetSpotTab({super.key, required this.state, required this.notifier});
  16. final AssetState state;
  17. final AssetNotifier notifier;
  18. @override
  19. Widget build(BuildContext context) {
  20. final cs = Theme.of(context).colorScheme;
  21. final obscure = state.obscureBalance;
  22. final hideZero = state.hideZeroBalanceInFundTab;
  23. final spotBalance = state.walletBalance('SPOT').toDouble();
  24. final display = obscure ? '******' : formatPrice(spotBalance, decimalPlaces: 2);
  25. final assets = state.fundWallets;
  26. return AppRefreshIndicator(
  27. onRefresh: notifier.silentRefresh,
  28. child: ListView(
  29. children: [
  30. // 资金账户余额
  31. Padding(
  32. padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
  33. child: Column(
  34. crossAxisAlignment: CrossAxisAlignment.start,
  35. children: [
  36. Row(
  37. children: [
  38. Text(AppLocalizations.of(context)!.fundAccount, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)),
  39. const SizedBox(width: 6),
  40. GestureDetector(
  41. onTap: notifier.toggleObscure,
  42. child: Icon(
  43. obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined,
  44. size: 16, color: cs.onSurface.withAlpha(153),
  45. ),
  46. ),
  47. ],
  48. ),
  49. const SizedBox(height: 8),
  50. Row(
  51. crossAxisAlignment: CrossAxisAlignment.end,
  52. children: [
  53. Text(display, style: TextStyle(color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w700, letterSpacing: -0.5)),
  54. const SizedBox(width: 6),
  55. Padding(
  56. padding: const EdgeInsets.only(bottom: 5),
  57. child: Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)),
  58. ),
  59. ],
  60. ),
  61. ],
  62. ),
  63. ),
  64. // 充币 + 提币 + 划转
  65. Padding(
  66. padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
  67. child: Row(
  68. children: [
  69. Expanded(
  70. child: GestureDetector(
  71. onTap: () => context.push('/asset/deposit'),
  72. child: Container(
  73. height: 44,
  74. decoration: BoxDecoration(color: AppColors.brand, borderRadius: BorderRadius.circular(22)),
  75. child: Center(child: Text(AppLocalizations.of(context)!.depositCoin, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600))),
  76. ),
  77. ),
  78. ),
  79. const SizedBox(width: 10),
  80. Expanded(
  81. child: GestureDetector(
  82. onTap: () => context.push('/asset/withdraw'),
  83. child: Container(
  84. height: 44,
  85. decoration: BoxDecoration(color: AppColors.brand, borderRadius: BorderRadius.circular(22)),
  86. child: Center(child: Text(AppLocalizations.of(context)!.withdrawCoin, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600))),
  87. ),
  88. ),
  89. ),
  90. const SizedBox(width: 10),
  91. Expanded(
  92. child: GestureDetector(
  93. onTap: () => context.push('/asset/transfer?from=SPOT&to=SPOT_TRADING'),
  94. child: Container(
  95. height: 44,
  96. decoration: BoxDecoration(color: AppColors.brand, borderRadius: BorderRadius.circular(22)),
  97. child: Center(child: Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600))),
  98. ),
  99. ),
  100. ),
  101. ],
  102. ),
  103. ),
  104. Padding(
  105. padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
  106. child: Row(
  107. mainAxisAlignment: MainAxisAlignment.end,
  108. children: [
  109. GestureDetector(
  110. onTap: notifier.toggleHideZeroBalanceInFundTab,
  111. child: Row(
  112. children: [
  113. Icon(
  114. hideZero
  115. ? Icons.check_box_outlined
  116. : Icons.check_box_outline_blank,
  117. size: 18,
  118. color: hideZero
  119. ? AppColors.brand
  120. : cs.onSurface.withAlpha(140),
  121. ),
  122. const SizedBox(width: 6),
  123. Text(
  124. AppLocalizations.of(context)!.hideZeroBalanceAssets,
  125. style: TextStyle(
  126. color: cs.onSurface.withAlpha(170),
  127. fontSize: 13,
  128. ),
  129. ),
  130. ],
  131. ),
  132. ),
  133. ],
  134. ),
  135. ),
  136. const SizedBox(height: 8),
  137. if (assets.isEmpty)
  138. Padding(
  139. padding: const EdgeInsets.symmetric(vertical: 36),
  140. child: Center(
  141. child: Text(
  142. AppLocalizations.of(context)!.noAssets,
  143. style: TextStyle(
  144. color: cs.onSurface.withAlpha(140),
  145. fontSize: 13,
  146. ),
  147. ),
  148. ),
  149. )
  150. else
  151. for (final asset in assets)
  152. _FundAssetRow(asset: asset, obscure: obscure),
  153. const SizedBox(height: 24),
  154. ],
  155. ),
  156. );
  157. }
  158. }
  159. class _FundAssetRow extends ConsumerWidget {
  160. const _FundAssetRow({required this.asset, required this.obscure});
  161. final SpotWalletAsset asset;
  162. final bool obscure;
  163. @override
  164. Widget build(BuildContext context, WidgetRef ref) {
  165. final cs = Theme.of(context).colorScheme;
  166. final l10n = AppLocalizations.of(context)!;
  167. final value = obscure ? '******' : null;
  168. final mapState = ref.watch(spotCoinCacheProvider);
  169. final iconUrl = spotCoinIconUrl(mapState, asset.coin);
  170. return Container(
  171. padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
  172. decoration: BoxDecoration(
  173. border: Border(
  174. bottom: BorderSide(color: cs.outline.withAlpha(40), width: 0.6),
  175. ),
  176. ),
  177. child: Column(
  178. crossAxisAlignment: CrossAxisAlignment.start,
  179. children: [
  180. Row(
  181. children: [
  182. CoinIcon(
  183. symbol: asset.coin,
  184. iconUrl: iconUrl,
  185. size: 40,
  186. shape: BoxShape.circle,
  187. ),
  188. const SizedBox(width: 10),
  189. Text(
  190. asset.coin,
  191. style: TextStyle(
  192. color: cs.onSurface,
  193. fontSize: 16,
  194. fontWeight: FontWeight.w700,
  195. ),
  196. ),
  197. ],
  198. ),
  199. const SizedBox(height: 14),
  200. Row(
  201. children: [
  202. Expanded(
  203. child: _FundAssetCell(
  204. label: l10n.assetBalance,
  205. value: value ?? formatAmount(asset.total, decimals: 6),
  206. align: CrossAxisAlignment.start,
  207. ),
  208. ),
  209. Expanded(
  210. child: _FundAssetCell(
  211. label: l10n.availableLabel,
  212. value: value ?? formatAmount(asset.balance, decimals: 6),
  213. align: CrossAxisAlignment.center,
  214. ),
  215. ),
  216. Expanded(
  217. child: _FundAssetCell(
  218. label: l10n.unavailableLabel,
  219. value: value ?? formatAmount(asset.frozenBalance, decimals: 6),
  220. align: CrossAxisAlignment.end,
  221. ),
  222. ),
  223. ],
  224. ),
  225. ],
  226. ),
  227. );
  228. }
  229. }
  230. class _FundAssetCell extends StatelessWidget {
  231. const _FundAssetCell({
  232. required this.label,
  233. required this.value,
  234. required this.align,
  235. });
  236. final String label;
  237. final String value;
  238. final CrossAxisAlignment align;
  239. @override
  240. Widget build(BuildContext context) {
  241. final cs = Theme.of(context).colorScheme;
  242. final textAlign = align == CrossAxisAlignment.start
  243. ? TextAlign.left
  244. : align == CrossAxisAlignment.end
  245. ? TextAlign.right
  246. : TextAlign.center;
  247. return Column(
  248. crossAxisAlignment: align,
  249. children: [
  250. Text(
  251. label,
  252. style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 11),
  253. textAlign: textAlign,
  254. ),
  255. const SizedBox(height: 4),
  256. Text(
  257. value,
  258. style: TextStyle(
  259. color: cs.onSurface,
  260. fontSize: 13,
  261. fontWeight: FontWeight.w600,
  262. fontFeatures: const [FontFeature.tabularFigures()],
  263. ),
  264. textAlign: textAlign,
  265. ),
  266. ],
  267. );
  268. }
  269. }