| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- 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();
- });
- });
- }
|