top_toast.dart 5.6 KB

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