transfer_history_screen.dart 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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 '../../../data/models/asset/transfer_record.dart';
  7. import '../../../providers/transfer_provider.dart';
  8. import '../../widgets/common/app_refresh_indicator.dart';
  9. class TransferHistoryScreen extends ConsumerStatefulWidget {
  10. const TransferHistoryScreen({super.key});
  11. @override
  12. ConsumerState<TransferHistoryScreen> createState() =>
  13. _TransferHistoryScreenState();
  14. }
  15. class _TransferHistoryScreenState extends ConsumerState<TransferHistoryScreen> {
  16. @override
  17. void initState() {
  18. super.initState();
  19. // 进入页面时自动刷新一次
  20. Future.microtask(() {
  21. ref.read(transferHistoryProvider.notifier).refresh();
  22. });
  23. }
  24. @override
  25. Widget build(BuildContext context) {
  26. final state = ref.watch(transferHistoryProvider);
  27. final notifier = ref.read(transferHistoryProvider.notifier);
  28. return Scaffold(
  29. appBar: AppBar(
  30. leading: IconButton(
  31. icon: const Icon(Icons.chevron_left, size: 28),
  32. onPressed: () => context.pop(),
  33. ),
  34. title: Text(AppLocalizations.of(context)!.transferRecord, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
  35. centerTitle: true,
  36. ),
  37. body: _buildBody(context, state, notifier),
  38. );
  39. }
  40. Widget _buildBody(BuildContext context, TransferHistoryState state, TransferHistoryNotifier notifier) {
  41. final cs = Theme.of(context).colorScheme;
  42. if (state.isLoading && state.records.isEmpty) {
  43. return const Center(child: CircularProgressIndicator());
  44. }
  45. if (state.errorMessage != null && state.records.isEmpty) {
  46. return AppRefreshIndicator(
  47. onRefresh: notifier.refresh,
  48. child: SingleChildScrollView(
  49. physics: const AlwaysScrollableScrollPhysics(),
  50. child: Center(
  51. child: Column(
  52. mainAxisSize: MainAxisSize.min,
  53. mainAxisAlignment: MainAxisAlignment.center,
  54. children: [
  55. Text(state.errorMessage!, style: TextStyle(color: cs.onSurface.withAlpha(153))),
  56. const SizedBox(height: 16),
  57. ElevatedButton(onPressed: notifier.refresh, child: Text(AppLocalizations.of(context)!.retry)),
  58. ],
  59. ),
  60. ),
  61. ),
  62. );
  63. }
  64. if (state.records.isEmpty) {
  65. return AppRefreshIndicator(
  66. onRefresh: notifier.refresh,
  67. child: SingleChildScrollView(
  68. physics: const AlwaysScrollableScrollPhysics(),
  69. child: Center(
  70. child: Padding(
  71. padding: const EdgeInsets.symmetric(vertical: 100),
  72. child: Text(AppLocalizations.of(context)!.noRecord, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 15)),
  73. ),
  74. ),
  75. ),
  76. );
  77. }
  78. return AppRefreshIndicator(
  79. onRefresh: notifier.refresh,
  80. child: NotificationListener<ScrollNotification>(
  81. onNotification: (notification) {
  82. if (notification is ScrollEndNotification &&
  83. notification.metrics.pixels >= notification.metrics.maxScrollExtent - 100) {
  84. notifier.loadMore();
  85. }
  86. return false;
  87. },
  88. child: ListView.separated(
  89. padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
  90. itemCount: state.records.length + (state.hasMore ? 1 : 0),
  91. separatorBuilder: (_, __) => Divider(height: 1, color: cs.outline.withAlpha(30)),
  92. itemBuilder: (_, i) {
  93. if (i >= state.records.length) {
  94. return const Padding(
  95. padding: EdgeInsets.symmetric(vertical: 16),
  96. child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
  97. );
  98. }
  99. return _RecordTile(record: state.records[i]);
  100. },
  101. ),
  102. ),
  103. );
  104. }
  105. }
  106. // ── 划转记录行 ──────────────────────────────────────────────────
  107. class _RecordTile extends StatelessWidget {
  108. const _RecordTile({required this.record});
  109. final TransferRecord record;
  110. String _walletName(BuildContext context, String type) {
  111. final l10n = AppLocalizations.of(context)!;
  112. switch (type) {
  113. case 'SPOT': return l10n.fundAccount;
  114. case 'SWAP': return l10n.futuresAccount;
  115. case 'FOLLOW': return l10n.copyAccount;
  116. case 'EXCHANGE': return l10n.exchangeAccount;
  117. default: return type;
  118. }
  119. }
  120. @override
  121. Widget build(BuildContext context) {
  122. final cs = Theme.of(context).colorScheme;
  123. // 去除尾部零
  124. final amount = record.amount.replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r'\.$'), '');
  125. return Padding(
  126. padding: const EdgeInsets.symmetric(vertical: 14),
  127. child: Row(
  128. crossAxisAlignment: CrossAxisAlignment.start,
  129. children: [
  130. Expanded(
  131. child: Column(
  132. crossAxisAlignment: CrossAxisAlignment.start,
  133. children: [
  134. Text(
  135. '${_walletName(context, record.source)} → ${_walletName(context, record.target)}',
  136. style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600),
  137. ),
  138. const SizedBox(height: 4),
  139. Text(record.createTime, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12)),
  140. ],
  141. ),
  142. ),
  143. Text(
  144. '$amount ${record.unit}',
  145. style: const TextStyle(color: AppColors.rise, fontSize: 15, fontWeight: FontWeight.w600),
  146. ),
  147. ],
  148. ),
  149. );
  150. }
  151. }