import 'package:flutter/material.dart'; import '../l10n/app_localizations.dart'; /// 将 provider 返回的错误码解析为本地化字符串。 /// err 为 null 时返回 null;未知错误码(API 服务端消息)直接透传。 String? resolveProviderError(String? err, AppLocalizations l10n) { if (err == null) return null; switch (err) { case 'errEnterVolume': return l10n.errEnterVolume; case 'errEnterPrice': return l10n.errEnterPrice; case 'errEnterTriggerPrice': return l10n.errEnterTriggerPrice; case 'errContractNotReady': return l10n.errContractNotReady; case 'errPriceNotReady': return l10n.errPriceNotReady; case 'errVolumeInsufficient': return l10n.errVolumeInsufficient; case 'errEnterClosePrice': return l10n.errEnterClosePrice; case 'errInvalidOrderId': return l10n.errInvalidOrderId; case 'errNoLongPosition': return l10n.errNoLongPosition; case 'errNoShortPosition': return l10n.errNoShortPosition; case 'errNoOrdersToCancel': return l10n.errNoOrdersToCancel; case 'errServiceUnavailable': return l10n.errServiceUnavailable; case 'errTimeout': return l10n.errTimeout; case 'errNetworkError': return l10n.errNetworkError; case 'errLoginCredentialWrong': return l10n.errLoginCredentialWrong; case 'errAccountAlreadyRegistered': return l10n.errAccountAlreadyRegistered; // withdraw errors case 'errSelectNetwork': return l10n.errSelectNetwork; case 'errEnterAddress': return l10n.errEnterAddress; case 'errEnterAmount': return l10n.errEnterAmount; case 'errEnterFundPassword': return l10n.errEnterFundPassword; case 'errEnterVerifyCode': return l10n.errEnterVerifyCode; case 'errBindGoogleFirst': return l10n.errBindGoogleFirst; case 'errEnterGoogleCode': return l10n.errEnterGoogleCode; case 'errAmountFormat': return l10n.errAmountFormat; case 'errExceedBalance': return l10n.errExceedBalance; case 'errEnterStartTime': return l10n.errEnterStartTime; case 'errEnterEndTime': return l10n.errEnterEndTime; // success codes case 'setSuccess': return l10n.setSuccess; case 'changeSuccess': return l10n.changeSuccess; default: // errMinWithdraw:xxx / errMinTransfer:xxx if (err.startsWith('errMinWithdraw:')) { return l10n.errMinWithdraw(err.substring('errMinWithdraw:'.length)); } if (err.startsWith('errMinTransfer:')) { return l10n.errMinTransfer(err.substring('errMinTransfer:'.length)); } return err; // 服务端消息直接透传 } } /// 从错误字符串中提取用户可读的消息(兜底清洗) String _cleanToastMessage(String message) { // 已经是简洁消息,直接返回 if (!message.contains('Exception') && !message.contains('\n')) return message; // 从多行中找第一个有意义的非技术行 final lines = message.split('\n'); for (final line in lines) { final t = line.trim(); if (t.isEmpty) { continue; } if (t.startsWith('DioException') || t.startsWith('Uri:') || t.startsWith('#') || t.startsWith('Error:')) { continue; } return t; } // 处理 "Error: ApiException(code): 中文消息" 格式 final m = RegExp(r'ApiException\(\d+\):\s*(.+?)[\r\n]?$', multiLine: true) .firstMatch(message); if (m != null) return m.group(1)!.trim(); // 取最后一个 ": " 之后的内容 final idx = message.lastIndexOf(': '); if (idx != -1 && idx < message.length - 2) { final tail = message.substring(idx + 2).trim(); if (!tail.startsWith('//') && !tail.startsWith('http')) return tail; } return message; } /// 从顶部滑入的错误/提示 toast,自动 2.5 秒后消失 void showTopToast( BuildContext context, { required String message, Color? backgroundColor, Duration duration = const Duration(milliseconds: 2500), }) { final displayMessage = _cleanToastMessage(message); final overlay = Overlay.of(context); final cs = Theme.of(context).colorScheme; final bgColor = backgroundColor ?? cs.error; late final OverlayEntry entry; final controller = AnimationController( vsync: overlay, duration: const Duration(milliseconds: 300), ); final animation = CurvedAnimation(parent: controller, curve: Curves.easeOut); entry = OverlayEntry( builder: (context) => AnimatedBuilder( animation: animation, builder: (context, child) => Positioned( top: MediaQuery.of(context).padding.top + (-60 + 60 * animation.value), left: 0, right: 0, child: Opacity( opacity: animation.value, child: child, ), ), child: Material( color: Colors.transparent, child: Container( margin: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(8), ), child: Text( displayMessage, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500, ), ), ), ), ), ); overlay.insert(entry); controller.forward(); Future.delayed(duration, () { if (!entry.mounted) { controller.dispose(); return; } controller.reverse().then((_) { try { if (entry.mounted) entry.remove(); } catch (_) {} controller.dispose(); }); }); }