top_toast.dart 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import 'package:flutter/material.dart';
  2. import '../l10n/app_localizations.dart';
  3. /// 将 provider 返回的错误码解析为本地化字符串。
  4. /// err 为 null 时返回 null;未知错误码(API 服务端消息)直接透传。
  5. String? resolveProviderError(String? err, AppLocalizations l10n) {
  6. if (err == null) return null;
  7. switch (err) {
  8. case 'errEnterVolume': return l10n.errEnterVolume;
  9. case 'errEnterPrice': return l10n.errEnterPrice;
  10. case 'errEnterTriggerPrice': return l10n.errEnterTriggerPrice;
  11. case 'errContractNotReady': return l10n.errContractNotReady;
  12. case 'errPriceNotReady': return l10n.errPriceNotReady;
  13. case 'errVolumeInsufficient': return l10n.errVolumeInsufficient;
  14. case 'errEnterClosePrice': return l10n.errEnterClosePrice;
  15. case 'errInvalidOrderId': return l10n.errInvalidOrderId;
  16. case 'errNoLongPosition': return l10n.errNoLongPosition;
  17. case 'errNoShortPosition': return l10n.errNoShortPosition;
  18. case 'errNoOrdersToCancel': return l10n.errNoOrdersToCancel;
  19. case 'errServiceUnavailable': return l10n.errServiceUnavailable;
  20. case 'errTimeout': return l10n.errTimeout;
  21. case 'errNetworkError': return l10n.errNetworkError;
  22. case 'errLoginCredentialWrong':
  23. return l10n.errLoginCredentialWrong;
  24. case 'errAccountAlreadyRegistered':
  25. return l10n.errAccountAlreadyRegistered;
  26. // withdraw errors
  27. case 'errSelectNetwork': return l10n.errSelectNetwork;
  28. case 'errSelectCoin': return l10n.errSelectCoin;
  29. case 'errEnterAddress': return l10n.errEnterAddress;
  30. case 'errEnterAmount': return l10n.errEnterAmount;
  31. case 'errEnterFundPassword': return l10n.errEnterFundPassword;
  32. case 'errEnterVerifyCode': return l10n.errEnterVerifyCode;
  33. case 'errBindGoogleFirst': return l10n.errBindGoogleFirst;
  34. case 'errEnterGoogleCode': return l10n.errEnterGoogleCode;
  35. case 'errAmountFormat': return l10n.errAmountFormat;
  36. case 'errExceedBalance': return l10n.errExceedBalance;
  37. case 'errEnterStartTime': return l10n.errEnterStartTime;
  38. case 'errEnterEndTime': return l10n.errEnterEndTime;
  39. // success codes
  40. case 'setSuccess': return l10n.setSuccess;
  41. case 'changeSuccess': return l10n.changeSuccess;
  42. default:
  43. // errMinWithdraw:amount / errMinWithdraw:amount:coin
  44. if (err.startsWith('errMinWithdraw:')) {
  45. final rest = err.substring('errMinWithdraw:'.length);
  46. final parts = rest.split(':');
  47. if (parts.length >= 2) {
  48. return l10n.errMinWithdrawWithCoin(parts[0], parts[1]);
  49. }
  50. return l10n.errMinWithdraw(rest);
  51. }
  52. if (err.startsWith('errMinTransfer:')) {
  53. final rest = err.substring('errMinTransfer:'.length);
  54. final parts = rest.split(':');
  55. if (parts.length >= 2) {
  56. return l10n.errMinTransferWithCoin(parts[0], parts[1]);
  57. }
  58. return l10n.errMinTransfer(rest);
  59. }
  60. return err; // 服务端消息直接透传
  61. }
  62. }
  63. /// 从错误字符串中提取用户可读的消息(兜底清洗)
  64. String _cleanToastMessage(String message) {
  65. // 已经是简洁消息,直接返回
  66. if (!message.contains('Exception') && !message.contains('\n')) return message;
  67. // 从多行中找第一个有意义的非技术行
  68. final lines = message.split('\n');
  69. for (final line in lines) {
  70. final t = line.trim();
  71. if (t.isEmpty) {
  72. continue;
  73. }
  74. if (t.startsWith('DioException') || t.startsWith('Uri:') ||
  75. t.startsWith('#') || t.startsWith('Error:')) {
  76. continue;
  77. }
  78. return t;
  79. }
  80. // 处理 "Error: ApiException(code): 中文消息" 格式
  81. final m = RegExp(r'ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true)
  82. .firstMatch(message);
  83. if (m != null) return m.group(1)!.trim();
  84. // 取最后一个 ": " 之后的内容
  85. final idx = message.lastIndexOf(': ');
  86. if (idx != -1 && idx < message.length - 2) {
  87. final tail = message.substring(idx + 2).trim();
  88. if (!tail.startsWith('//') && !tail.startsWith('http')) return tail;
  89. }
  90. return message;
  91. }
  92. /// 从顶部滑入的错误/提示 toast,自动 2.5 秒后消失
  93. void showTopToast(
  94. BuildContext context, {
  95. required String message,
  96. Color? backgroundColor,
  97. Duration duration = const Duration(milliseconds: 2500),
  98. }) {
  99. final displayMessage = _cleanToastMessage(message);
  100. final overlay = Overlay.of(context);
  101. final cs = Theme.of(context).colorScheme;
  102. final bgColor = backgroundColor ?? cs.error;
  103. late final OverlayEntry entry;
  104. final controller = AnimationController(
  105. vsync: overlay,
  106. duration: const Duration(milliseconds: 300),
  107. );
  108. final animation = CurvedAnimation(parent: controller, curve: Curves.easeOut);
  109. entry = OverlayEntry(
  110. builder: (context) => AnimatedBuilder(
  111. animation: animation,
  112. builder: (context, child) => Positioned(
  113. top: MediaQuery.of(context).padding.top +
  114. (-60 + 60 * animation.value),
  115. left: 0,
  116. right: 0,
  117. child: Opacity(
  118. opacity: animation.value,
  119. child: child,
  120. ),
  121. ),
  122. child: Material(
  123. color: Colors.transparent,
  124. child: Container(
  125. margin: const EdgeInsets.symmetric(horizontal: 12),
  126. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  127. decoration: BoxDecoration(
  128. color: bgColor,
  129. borderRadius: BorderRadius.circular(8),
  130. ),
  131. child: Text(
  132. displayMessage,
  133. style: const TextStyle(
  134. color: Colors.white,
  135. fontSize: 13,
  136. fontWeight: FontWeight.w500,
  137. ),
  138. ),
  139. ),
  140. ),
  141. ),
  142. );
  143. overlay.insert(entry);
  144. controller.forward();
  145. Future.delayed(duration, () {
  146. if (!entry.mounted) {
  147. controller.dispose();
  148. return;
  149. }
  150. controller.reverse().then((_) {
  151. try {
  152. if (entry.mounted) entry.remove();
  153. } catch (_) {}
  154. controller.dispose();
  155. });
  156. });
  157. }