Explorar o código

✨ feat(asset): 充提对接 Web 主网/子币 API 并优化选择 UI

对齐 ibit-web 充提流程:先选币种再选主网/子币;链上提现动态手续费与余额;
内部转账支持 transfer-parents 多主币;补充 l10n 与最小提币 Toast 解析。

Co-authored-by: Cursor <cursoragent@cursor.com>
maotouying_cc hai 1 mes
pai
achega
6a45cf679b

+ 1 - 1
lib/core/config/app_config.dart

@@ -102,7 +102,7 @@ abstract class AppConfig {
   );
 
   /// 应用版本号(与 pubspec.yaml version 保持一致)
-  static const String appVersion = '4.1.5';
+  static const String appVersion = '4.1.6';
 
   /// 是否启用 Mock 模式
   static const bool enableMock = bool.fromEnvironment(

+ 53 - 0
lib/core/l10n/app_en.arb

@@ -538,6 +538,7 @@
   "errTimeout": "Request timed out, check network",
   "errNetworkError": "Network error, check connection",
   "errSelectNetwork": "Please select withdrawal network",
+  "errSelectCoin": "Please select a coin",
   "errEnterAddress": "Please enter withdrawal address",
   "errEnterAmount": "Please enter amount",
   "errEnterFundPassword": "Please enter fund password",
@@ -561,6 +562,28 @@
       }
     }
   },
+  "errMinWithdrawWithCoin": "Min withdrawal {min} {coin}",
+  "@errMinWithdrawWithCoin": {
+    "placeholders": {
+      "min": {
+        "type": "String"
+      },
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
+  "errMinTransferWithCoin": "Min transfer {min} {coin}",
+  "@errMinTransferWithCoin": {
+    "placeholders": {
+      "min": {
+        "type": "String"
+      },
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
   "errExceedBalance": "Exceeds available balance",
   "errEnterStartTime": "Please select start time",
   "errEnterEndTime": "Please select end time",
@@ -769,6 +792,32 @@
   "liqPrice": "Liq. Price",
   "marginRatioLabel": "Margin Ratio",
   "withdrawMinAmountHint": "Minimum withdrawal: {minAmount}",
+  "withdrawMainCoinLabel": "Main coin",
+  "withdrawTransferMainCoinHint": "Internal transfer supports main coins only",
+  "withdrawAddressForCoin": "Enter {coin} address",
+  "@withdrawAddressForCoin": {
+    "placeholders": {
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
+  "withdrawAmountCoin": "Withdraw amount ({coin})",
+  "@withdrawAmountCoin": {
+    "placeholders": {
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
+  "transferAmountCoin": "Transfer amount ({coin})",
+  "@transferAmountCoin": {
+    "placeholders": {
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
   "fundAccountAvailable": "Funding account available",
   "recipientUidOrAccount": "Recipient UID or Account",
   "enterRecipientUid": "Enter recipient's platform UID",
@@ -1436,6 +1485,10 @@
   "depositContractAddress": "Contract address",
   "depositOrderAmount": "Order amount",
   "depositSelectNetworkFirst": "Select a network first",
+  "depositNoNetwork": "No networks available",
+  "depositNoSubCoin": "No sub-coins available on this network",
+  "depositMainnetProtocol": "Network / Protocol",
+  "depositSubCoin": "Sub-coin",
   "passwordLengthError": "Password must be 6-16 characters",
   "passwordSpaceError": "Password cannot contain spaces",
   "passwordLetterDigitError": "Must contain both letters and numbers",

+ 56 - 2
lib/core/l10n/app_localizations.dart

@@ -2981,6 +2981,12 @@ abstract class AppLocalizations {
   /// **'请选择提币网络'**
   String get errSelectNetwork;
 
+  /// No description provided for @errSelectCoin.
+  ///
+  /// In zh, this message translates to:
+  /// **'请选择币种'**
+  String get errSelectCoin;
+
   /// No description provided for @errEnterAddress.
   ///
   /// In zh, this message translates to:
@@ -3035,6 +3041,18 @@ abstract class AppLocalizations {
   /// **'最小转账额 {amount}'**
   String errMinTransfer(String amount);
 
+  /// No description provided for @errMinWithdrawWithCoin.
+  ///
+  /// In zh, this message translates to:
+  /// **'最小提币额 {min} {coin}'**
+  String errMinWithdrawWithCoin(String min, String coin);
+
+  /// No description provided for @errMinTransferWithCoin.
+  ///
+  /// In zh, this message translates to:
+  /// **'最小转账额 {min} {coin}'**
+  String errMinTransferWithCoin(String min, String coin);
+
   /// No description provided for @errExceedBalance.
   ///
   /// In zh, this message translates to:
@@ -4112,8 +4130,8 @@ abstract class AppLocalizations {
   /// No description provided for @transferAmountCoin.
   ///
   /// In zh, this message translates to:
-  /// **'数量 ({coin})'**
-  String transferAmountCoin(Object coin);
+  /// **'转账数量 ({coin})'**
+  String transferAmountCoin(String coin);
 
   /// No description provided for @confirmTransfer.
   ///
@@ -4217,6 +4235,30 @@ abstract class AppLocalizations {
   /// **'最小提现额 {minAmount}'**
   String withdrawMinAmountHint(Object minAmount);
 
+  /// No description provided for @withdrawMainCoinLabel.
+  ///
+  /// In zh, this message translates to:
+  /// **'主币种'**
+  String get withdrawMainCoinLabel;
+
+  /// No description provided for @withdrawTransferMainCoinHint.
+  ///
+  /// In zh, this message translates to:
+  /// **'内部转账仅支持平台内主币'**
+  String get withdrawTransferMainCoinHint;
+
+  /// No description provided for @withdrawAddressForCoin.
+  ///
+  /// In zh, this message translates to:
+  /// **'请输入 {coin} 地址'**
+  String withdrawAddressForCoin(String coin);
+
+  /// No description provided for @withdrawAmountCoin.
+  ///
+  /// In zh, this message translates to:
+  /// **'提币数量 ({coin})'**
+  String withdrawAmountCoin(String coin);
+
   /// No description provided for @fundAccountAvailable.
   ///
   /// In zh, this message translates to:
@@ -6767,6 +6809,18 @@ abstract class AppLocalizations {
   /// **'请先选择充值网络'**
   String get depositSelectNetworkFirst;
 
+  /// No description provided for @depositNoNetwork.
+  ///
+  /// In zh, this message translates to:
+  /// **'暂无可用网络'**
+  String get depositNoNetwork;
+
+  /// No description provided for @depositNoSubCoin.
+  ///
+  /// In zh, this message translates to:
+  /// **'该主网下暂无可用子币种'**
+  String get depositNoSubCoin;
+
   /// No description provided for @depositMainnetProtocol.
   ///
   /// In zh, this message translates to:

+ 40 - 4
lib/core/l10n/app_localizations_en.dart

@@ -1474,6 +1474,9 @@ class AppLocalizationsEn extends AppLocalizations {
   @override
   String get errSelectNetwork => 'Please select withdrawal network';
 
+  @override
+  String get errSelectCoin => 'Please select a coin';
+
   @override
   String get errEnterAddress => 'Please enter withdrawal address';
 
@@ -1505,6 +1508,16 @@ class AppLocalizationsEn extends AppLocalizations {
     return 'Min transfer: $amount';
   }
 
+  @override
+  String errMinWithdrawWithCoin(String min, String coin) {
+    return 'Min withdrawal $min $coin';
+  }
+
+  @override
+  String errMinTransferWithCoin(String min, String coin) {
+    return 'Min transfer $min $coin';
+  }
+
   @override
   String get errExceedBalance => 'Exceeds available balance';
 
@@ -2065,8 +2078,8 @@ class AppLocalizationsEn extends AppLocalizations {
   }
 
   @override
-  String transferAmountCoin(Object coin) {
-    return 'Amount ($coin)';
+  String transferAmountCoin(String coin) {
+    return 'Transfer amount ($coin)';
   }
 
   @override
@@ -2131,6 +2144,23 @@ class AppLocalizationsEn extends AppLocalizations {
     return 'Minimum withdrawal: $minAmount';
   }
 
+  @override
+  String get withdrawMainCoinLabel => 'Main coin';
+
+  @override
+  String get withdrawTransferMainCoinHint =>
+      'Internal transfer supports main coins only';
+
+  @override
+  String withdrawAddressForCoin(String coin) {
+    return 'Enter $coin address';
+  }
+
+  @override
+  String withdrawAmountCoin(String coin) {
+    return 'Withdraw amount ($coin)';
+  }
+
   @override
   String get fundAccountAvailable => 'Funding account available';
 
@@ -3513,10 +3543,16 @@ class AppLocalizationsEn extends AppLocalizations {
   String get depositSelectNetworkFirst => 'Select a network first';
 
   @override
-  String get depositMainnetProtocol => '主网/协议';
+  String get depositNoNetwork => 'No networks available';
+
+  @override
+  String get depositNoSubCoin => 'No sub-coins available on this network';
+
+  @override
+  String get depositMainnetProtocol => 'Network / Protocol';
 
   @override
-  String get depositSubCoin => '子币种';
+  String get depositSubCoin => 'Sub-coin';
 
   @override
   String get depositRules => '规则';

+ 37 - 2
lib/core/l10n/app_localizations_hi.dart

@@ -1475,6 +1475,9 @@ class AppLocalizationsHi extends AppLocalizations {
   @override
   String get errSelectNetwork => 'कृपया निकासी नेटवर्क चुनें';
 
+  @override
+  String get errSelectCoin => '请选择币种';
+
   @override
   String get errEnterAddress => 'कृपया निकासी पता दर्ज करें';
 
@@ -1506,6 +1509,16 @@ class AppLocalizationsHi extends AppLocalizations {
     return 'न्यूनतम स्थानांतरण: $amount';
   }
 
+  @override
+  String errMinWithdrawWithCoin(String min, String coin) {
+    return '最小提币额 $min $coin';
+  }
+
+  @override
+  String errMinTransferWithCoin(String min, String coin) {
+    return '最小转账额 $min $coin';
+  }
+
   @override
   String get errExceedBalance => 'उपलब्ध शेष से अधिक';
 
@@ -2061,8 +2074,8 @@ class AppLocalizationsHi extends AppLocalizations {
   }
 
   @override
-  String transferAmountCoin(Object coin) {
-    return '数量 ($coin)';
+  String transferAmountCoin(String coin) {
+    return '转账数量 ($coin)';
   }
 
   @override
@@ -2127,6 +2140,22 @@ class AppLocalizationsHi extends AppLocalizations {
     return 'न्यूनतम निकासी: $minAmount';
   }
 
+  @override
+  String get withdrawMainCoinLabel => '主币种';
+
+  @override
+  String get withdrawTransferMainCoinHint => '内部转账仅支持平台内主币';
+
+  @override
+  String withdrawAddressForCoin(String coin) {
+    return '请输入 $coin 地址';
+  }
+
+  @override
+  String withdrawAmountCoin(String coin) {
+    return '提币数量 ($coin)';
+  }
+
   @override
   String get fundAccountAvailable => 'फंडिंग खाता उपलब्ध';
 
@@ -3505,6 +3534,12 @@ class AppLocalizationsHi extends AppLocalizations {
   @override
   String get depositSelectNetworkFirst => 'पहले एक नेटवर्क चुनें';
 
+  @override
+  String get depositNoNetwork => '暂无可用网络';
+
+  @override
+  String get depositNoSubCoin => '该主网下暂无可用子币种';
+
   @override
   String get depositMainnetProtocol => '主网/协议';
 

+ 37 - 2
lib/core/l10n/app_localizations_id.dart

@@ -1475,6 +1475,9 @@ class AppLocalizationsId extends AppLocalizations {
   @override
   String get errSelectNetwork => 'Silakan pilih jaringan penarikan';
 
+  @override
+  String get errSelectCoin => '请选择币种';
+
   @override
   String get errEnterAddress => 'Silakan masukkan alamat penarikan';
 
@@ -1506,6 +1509,16 @@ class AppLocalizationsId extends AppLocalizations {
     return 'Min transfer: $amount';
   }
 
+  @override
+  String errMinWithdrawWithCoin(String min, String coin) {
+    return '最小提币额 $min $coin';
+  }
+
+  @override
+  String errMinTransferWithCoin(String min, String coin) {
+    return '最小转账额 $min $coin';
+  }
+
   @override
   String get errExceedBalance => 'Melebihi saldo tersedia';
 
@@ -2065,8 +2078,8 @@ class AppLocalizationsId extends AppLocalizations {
   }
 
   @override
-  String transferAmountCoin(Object coin) {
-    return '数量 ($coin)';
+  String transferAmountCoin(String coin) {
+    return '转账数量 ($coin)';
   }
 
   @override
@@ -2131,6 +2144,22 @@ class AppLocalizationsId extends AppLocalizations {
     return 'Minimal penarikan: $minAmount';
   }
 
+  @override
+  String get withdrawMainCoinLabel => '主币种';
+
+  @override
+  String get withdrawTransferMainCoinHint => '内部转账仅支持平台内主币';
+
+  @override
+  String withdrawAddressForCoin(String coin) {
+    return '请输入 $coin 地址';
+  }
+
+  @override
+  String withdrawAmountCoin(String coin) {
+    return '提币数量 ($coin)';
+  }
+
   @override
   String get fundAccountAvailable => 'Akun dana tersedia';
 
@@ -3515,6 +3544,12 @@ class AppLocalizationsId extends AppLocalizations {
   @override
   String get depositSelectNetworkFirst => 'Pilih jaringan terlebih dahulu';
 
+  @override
+  String get depositNoNetwork => '暂无可用网络';
+
+  @override
+  String get depositNoSubCoin => '该主网下暂无可用子币种';
+
   @override
   String get depositMainnetProtocol => '主网/协议';
 

+ 37 - 2
lib/core/l10n/app_localizations_ja.dart

@@ -1468,6 +1468,9 @@ class AppLocalizationsJa extends AppLocalizations {
   @override
   String get errSelectNetwork => '出金ネットワークを選択してください';
 
+  @override
+  String get errSelectCoin => '请选择币种';
+
   @override
   String get errEnterAddress => '出金アドレスを入力してください';
 
@@ -1499,6 +1502,16 @@ class AppLocalizationsJa extends AppLocalizations {
     return '最小送金額:$amount';
   }
 
+  @override
+  String errMinWithdrawWithCoin(String min, String coin) {
+    return '最小提币额 $min $coin';
+  }
+
+  @override
+  String errMinTransferWithCoin(String min, String coin) {
+    return '最小转账额 $min $coin';
+  }
+
   @override
   String get errExceedBalance => '利用可能残高を超えています';
 
@@ -2048,8 +2061,8 @@ class AppLocalizationsJa extends AppLocalizations {
   }
 
   @override
-  String transferAmountCoin(Object coin) {
-    return '数量 ($coin)';
+  String transferAmountCoin(String coin) {
+    return '转账数量 ($coin)';
   }
 
   @override
@@ -2111,6 +2124,22 @@ class AppLocalizationsJa extends AppLocalizations {
     return '最小出金額: $minAmount';
   }
 
+  @override
+  String get withdrawMainCoinLabel => '主币种';
+
+  @override
+  String get withdrawTransferMainCoinHint => '内部转账仅支持平台内主币';
+
+  @override
+  String withdrawAddressForCoin(String coin) {
+    return '请输入 $coin 地址';
+  }
+
+  @override
+  String withdrawAmountCoin(String coin) {
+    return '提币数量 ($coin)';
+  }
+
   @override
   String get fundAccountAvailable => '資金口座利用可能';
 
@@ -3462,6 +3491,12 @@ class AppLocalizationsJa extends AppLocalizations {
   @override
   String get depositSelectNetworkFirst => '先にネットワークを選択してください';
 
+  @override
+  String get depositNoNetwork => '暂无可用网络';
+
+  @override
+  String get depositNoSubCoin => '该主网下暂无可用子币种';
+
   @override
   String get depositMainnetProtocol => '主网/协议';
 

+ 37 - 2
lib/core/l10n/app_localizations_ko.dart

@@ -1468,6 +1468,9 @@ class AppLocalizationsKo extends AppLocalizations {
   @override
   String get errSelectNetwork => '출금 네트워크를 선택하세요';
 
+  @override
+  String get errSelectCoin => '请选择币种';
+
   @override
   String get errEnterAddress => '출금 주소를 입력하세요';
 
@@ -1499,6 +1502,16 @@ class AppLocalizationsKo extends AppLocalizations {
     return '최소 송금액: $amount';
   }
 
+  @override
+  String errMinWithdrawWithCoin(String min, String coin) {
+    return '最小提币额 $min $coin';
+  }
+
+  @override
+  String errMinTransferWithCoin(String min, String coin) {
+    return '最小转账额 $min $coin';
+  }
+
   @override
   String get errExceedBalance => '사용 가능 잔액 초과';
 
@@ -2049,8 +2062,8 @@ class AppLocalizationsKo extends AppLocalizations {
   }
 
   @override
-  String transferAmountCoin(Object coin) {
-    return '数量 ($coin)';
+  String transferAmountCoin(String coin) {
+    return '转账数量 ($coin)';
   }
 
   @override
@@ -2113,6 +2126,22 @@ class AppLocalizationsKo extends AppLocalizations {
     return '최소 출금액: $minAmount';
   }
 
+  @override
+  String get withdrawMainCoinLabel => '主币种';
+
+  @override
+  String get withdrawTransferMainCoinHint => '内部转账仅支持平台内主币';
+
+  @override
+  String withdrawAddressForCoin(String coin) {
+    return '请输入 $coin 地址';
+  }
+
+  @override
+  String withdrawAmountCoin(String coin) {
+    return '提币数量 ($coin)';
+  }
+
   @override
   String get fundAccountAvailable => '자금 계정 가용 잔액';
 
@@ -3468,6 +3497,12 @@ class AppLocalizationsKo extends AppLocalizations {
   @override
   String get depositSelectNetworkFirst => '먼저 네트워크를 선택하세요';
 
+  @override
+  String get depositNoNetwork => '暂无可用网络';
+
+  @override
+  String get depositNoSubCoin => '该主网下暂无可用子币种';
+
   @override
   String get depositMainnetProtocol => '主网/协议';
 

+ 37 - 2
lib/core/l10n/app_localizations_zh.dart

@@ -1467,6 +1467,9 @@ class AppLocalizationsZh extends AppLocalizations {
   @override
   String get errSelectNetwork => '请选择提币网络';
 
+  @override
+  String get errSelectCoin => '请选择币种';
+
   @override
   String get errEnterAddress => '请输入提币地址';
 
@@ -1498,6 +1501,16 @@ class AppLocalizationsZh extends AppLocalizations {
     return '最小转账额 $amount';
   }
 
+  @override
+  String errMinWithdrawWithCoin(String min, String coin) {
+    return '最小提币额 $min $coin';
+  }
+
+  @override
+  String errMinTransferWithCoin(String min, String coin) {
+    return '最小转账额 $min $coin';
+  }
+
   @override
   String get errExceedBalance => '超出可用余额';
 
@@ -2043,8 +2056,8 @@ class AppLocalizationsZh extends AppLocalizations {
   }
 
   @override
-  String transferAmountCoin(Object coin) {
-    return '数量 ($coin)';
+  String transferAmountCoin(String coin) {
+    return '转账数量 ($coin)';
   }
 
   @override
@@ -2104,6 +2117,22 @@ class AppLocalizationsZh extends AppLocalizations {
     return '最小提现额 $minAmount';
   }
 
+  @override
+  String get withdrawMainCoinLabel => '主币种';
+
+  @override
+  String get withdrawTransferMainCoinHint => '内部转账仅支持平台内主币';
+
+  @override
+  String withdrawAddressForCoin(String coin) {
+    return '请输入 $coin 地址';
+  }
+
+  @override
+  String withdrawAmountCoin(String coin) {
+    return '提币数量 ($coin)';
+  }
+
   @override
   String get fundAccountAvailable => '资金账户可用';
 
@@ -3452,6 +3481,12 @@ class AppLocalizationsZh extends AppLocalizations {
   @override
   String get depositSelectNetworkFirst => '请先选择充值网络';
 
+  @override
+  String get depositNoNetwork => '暂无可用网络';
+
+  @override
+  String get depositNoSubCoin => '该主网下暂无可用子币种';
+
   @override
   String get depositMainnetProtocol => '主网/协议';
 

+ 51 - 0
lib/core/l10n/app_zh.arb

@@ -538,6 +538,7 @@
   "errTimeout": "请求超时,请检查网络后重试",
   "errNetworkError": "网络连接异常,请检查网络设置",
   "errSelectNetwork": "请选择提币网络",
+  "errSelectCoin": "请选择币种",
   "errEnterAddress": "请输入提币地址",
   "errEnterAmount": "请输入金额",
   "errEnterFundPassword": "请输入资金密码",
@@ -561,6 +562,28 @@
       }
     }
   },
+  "errMinWithdrawWithCoin": "最小提币额 {min} {coin}",
+  "@errMinWithdrawWithCoin": {
+    "placeholders": {
+      "min": {
+        "type": "String"
+      },
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
+  "errMinTransferWithCoin": "最小转账额 {min} {coin}",
+  "@errMinTransferWithCoin": {
+    "placeholders": {
+      "min": {
+        "type": "String"
+      },
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
   "errExceedBalance": "超出可用余额",
   "errEnterStartTime": "请选择开始时间",
   "errEnterEndTime": "请选择结束时间",
@@ -769,6 +792,32 @@
   "liqPrice": "强平价格",
   "marginRatioLabel": "保证金比率",
   "withdrawMinAmountHint": "最小提现额 {minAmount}",
+  "withdrawMainCoinLabel": "主币种",
+  "withdrawTransferMainCoinHint": "内部转账仅支持平台内主币",
+  "withdrawAddressForCoin": "请输入 {coin} 地址",
+  "@withdrawAddressForCoin": {
+    "placeholders": {
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
+  "withdrawAmountCoin": "提币数量 ({coin})",
+  "@withdrawAmountCoin": {
+    "placeholders": {
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
+  "transferAmountCoin": "转账数量 ({coin})",
+  "@transferAmountCoin": {
+    "placeholders": {
+      "coin": {
+        "type": "String"
+      }
+    }
+  },
   "@withdrawMinAmountHint": {
     "placeholders": {
       "minAmount": {
@@ -1464,6 +1513,8 @@
   "depositContractAddress": "合约地址",
   "depositOrderAmount": "充值金额",
   "depositSelectNetworkFirst": "请先选择充值网络",
+  "depositNoNetwork": "暂无可用网络",
+  "depositNoSubCoin": "该主网下暂无可用子币种",
   "depositMainnetProtocol": "主网/协议",
   "depositSubCoin": "子币种",
   "depositRules": "规则",

+ 14 - 3
lib/core/utils/top_toast.dart

@@ -27,6 +27,7 @@ String? resolveProviderError(String? err, AppLocalizations l10n) {
       return l10n.errAccountAlreadyRegistered;
     // withdraw errors
     case 'errSelectNetwork':      return l10n.errSelectNetwork;
+    case 'errSelectCoin':         return l10n.errSelectCoin;
     case 'errEnterAddress':       return l10n.errEnterAddress;
     case 'errEnterAmount':        return l10n.errEnterAmount;
     case 'errEnterFundPassword':  return l10n.errEnterFundPassword;
@@ -41,12 +42,22 @@ String? resolveProviderError(String? err, AppLocalizations l10n) {
     case 'setSuccess':            return l10n.setSuccess;
     case 'changeSuccess':         return l10n.changeSuccess;
     default:
-      // errMinWithdraw:xxx / errMinTransfer:xxx
+      // errMinWithdraw:amount / errMinWithdraw:amount:coin
       if (err.startsWith('errMinWithdraw:')) {
-        return l10n.errMinWithdraw(err.substring('errMinWithdraw:'.length));
+        final rest = err.substring('errMinWithdraw:'.length);
+        final parts = rest.split(':');
+        if (parts.length >= 2) {
+          return l10n.errMinWithdrawWithCoin(parts[0], parts[1]);
+        }
+        return l10n.errMinWithdraw(rest);
       }
       if (err.startsWith('errMinTransfer:')) {
-        return l10n.errMinTransfer(err.substring('errMinTransfer:'.length));
+        final rest = err.substring('errMinTransfer:'.length);
+        final parts = rest.split(':');
+        if (parts.length >= 2) {
+          return l10n.errMinTransferWithCoin(parts[0], parts[1]);
+        }
+        return l10n.errMinTransfer(rest);
       }
       return err; // 服务端消息直接透传
   }

+ 99 - 1
lib/data/models/asset/recharge_order.dart

@@ -28,26 +28,124 @@ class RechargeParentCoin {
   }
 }
 
-/// 扁平化后的充值网络选项(与 Web `RechargeFlatNetworkOption` 一致)
+/// 充值主网 — GET uc/coin-network/recharge-networks
+class RechargeNetworkItem {
+  final int id;
+  final String name;
+  final String protocol;
+  final int? status;
+  final int sort;
+
+  const RechargeNetworkItem({
+    required this.id,
+    required this.name,
+    required this.protocol,
+    this.status,
+    this.sort = 0,
+  });
+
+  factory RechargeNetworkItem.fromJson(Map<String, dynamic> json) {
+    return RechargeNetworkItem(
+      id: int.tryParse(json['id']?.toString() ?? '') ?? 0,
+      name: json['name']?.toString() ?? '',
+      protocol: json['protocol']?.toString() ?? '',
+      status: json['status'] != null
+          ? int.tryParse(json['status']?.toString() ?? '')
+          : null,
+      sort: int.tryParse(json['sort']?.toString() ?? '') ?? 0,
+    );
+  }
+}
+
+/// 某主网下 coin_network_config 一行 — GET uc/coin-network/recharge-configs
+class RechargeConfigOption {
+  final int networkConfigId;
+  final String coinName;
+  final String coinNameCn;
+  final int networkId;
+  final String networkName;
+  final String protocol;
+  final String contractAddress;
+  final String rechargeAddress;
+  final int sort;
+
+  const RechargeConfigOption({
+    required this.networkConfigId,
+    required this.coinName,
+    required this.coinNameCn,
+    required this.networkId,
+    required this.networkName,
+    required this.protocol,
+    required this.contractAddress,
+    required this.rechargeAddress,
+    this.sort = 0,
+  });
+
+  factory RechargeConfigOption.fromJson(Map<String, dynamic> json) {
+    final network = json['network'];
+    return RechargeConfigOption(
+      networkConfigId:
+          int.tryParse(json['networkConfigId']?.toString() ?? json['id']?.toString() ?? '') ??
+              0,
+      coinName: json['coinName']?.toString() ?? '',
+      coinNameCn: json['coinNameCn']?.toString() ?? '',
+      networkId: int.tryParse(json['networkId']?.toString() ??
+              (network is Map ? network['id']?.toString() : null) ??
+              '') ??
+          0,
+      networkName: json['networkName']?.toString() ??
+          (network is Map ? network['name']?.toString() : null) ??
+          '',
+      protocol: json['protocol']?.toString() ??
+          (network is Map ? network['protocol']?.toString() : null) ??
+          '',
+      contractAddress: json['contractAddress']?.toString() ?? '',
+      rechargeAddress: json['rechargeAddress']?.toString() ?? '',
+      sort: int.tryParse(json['sort']?.toString() ?? '') ?? 0,
+    );
+  }
+
+  bool get hasTokenContract => contractAddress.trim().isNotEmpty;
+}
+
+/// 扁平化后的充值网络选项(供钱包/EVM 工具使用,与 Web `RechargeFlatNetworkOption` 一致)
 class RechargeFlatNetworkOption {
   final String coinName;
   final String coinNameCn;
   final int networkConfigId;
+  final int networkId;
   final String networkName;
   final String protocol;
   final String contractAddress;
   final String rechargeAddress;
+  final int sort;
 
   const RechargeFlatNetworkOption({
     required this.coinName,
     required this.coinNameCn,
     required this.networkConfigId,
+    required this.networkId,
     required this.networkName,
     required this.protocol,
     required this.contractAddress,
     required this.rechargeAddress,
+    this.sort = 0,
   });
 
+  factory RechargeFlatNetworkOption.fromConfig(RechargeConfigOption cfg) {
+    return RechargeFlatNetworkOption(
+      coinName: cfg.coinName,
+      coinNameCn: cfg.coinNameCn,
+      networkConfigId: cfg.networkConfigId,
+      networkId: cfg.networkId,
+      networkName: cfg.networkName,
+      protocol: cfg.protocol,
+      contractAddress: cfg.contractAddress,
+      rechargeAddress: cfg.rechargeAddress,
+      sort: cfg.sort,
+    );
+  }
+
   bool get hasTokenContract => contractAddress.trim().isNotEmpty;
 }
 

+ 236 - 0
lib/data/models/asset/withdraw_coin_option.dart

@@ -0,0 +1,236 @@
+/// 提现支持币种 — POST uc/withdraw/support/coin/info
+class SupportCoinInfo {
+  final String unit;
+  final String? aliasName;
+  final String? nameCn;
+  final String? name;
+  final String? balance;
+  final String minWithdrawAmount;
+  final String? minAmount;
+  final String? minTransferAmount;
+  final String? withdrawFeeValue;
+  final String? fee;
+
+  const SupportCoinInfo({
+    required this.unit,
+    this.aliasName,
+    this.nameCn,
+    this.name,
+    this.balance,
+    this.minWithdrawAmount = '0',
+    this.minAmount,
+    this.minTransferAmount,
+    this.withdrawFeeValue,
+    this.fee,
+  });
+
+  factory SupportCoinInfo.fromJson(Map<String, dynamic> json) {
+    return SupportCoinInfo(
+      unit: json['unit']?.toString() ?? '',
+      aliasName: json['aliasName']?.toString(),
+      nameCn: json['nameCn']?.toString(),
+      name: json['name']?.toString(),
+      balance: json['balance']?.toString(),
+      minWithdrawAmount: json['minWithdrawAmount']?.toString() ?? '0',
+      minAmount: json['minAmount']?.toString(),
+      minTransferAmount: json['minTransferAmount']?.toString(),
+      withdrawFeeValue: json['withdrawFeeValue']?.toString(),
+      fee: json['fee']?.toString(),
+    );
+  }
+}
+
+/// 提币主网 — GET uc/coin-network/withdraw-networks
+class WithdrawNetworkItem {
+  final int id;
+  final String name;
+  final String protocol;
+  final int sort;
+
+  const WithdrawNetworkItem({
+    required this.id,
+    required this.name,
+    required this.protocol,
+    this.sort = 0,
+  });
+
+  factory WithdrawNetworkItem.fromJson(Map<String, dynamic> json) {
+    return WithdrawNetworkItem(
+      id: int.tryParse(json['id']?.toString() ?? '') ?? 0,
+      name: json['name']?.toString() ?? '',
+      protocol: json['protocol']?.toString() ?? '',
+      sort: int.tryParse(json['sort']?.toString() ?? '') ?? 0,
+    );
+  }
+}
+
+/// 可提/可转币种选项(链上子币或内部转账主币)
+class WithdrawCoinOption {
+  final String unit;
+  final String displayName;
+  final String networkLabel;
+  final int? networkId;
+  final String balance;
+  final String minWithdraw;
+  final String minTransfer;
+  final String withdrawFee;
+  final bool isUsdtFamily;
+  final bool isMainCoin;
+
+  const WithdrawCoinOption({
+    required this.unit,
+    required this.displayName,
+    this.networkLabel = '',
+    this.networkId,
+    this.balance = '0',
+    this.minWithdraw = '0',
+    this.minTransfer = '0',
+    this.withdrawFee = '0',
+    this.isUsdtFamily = false,
+    this.isMainCoin = false,
+  });
+}
+
+String firstNonEmptyStr(List<Object?> vals) {
+  for (final v in vals) {
+    if (v == null) {
+      continue;
+    }
+    final s = v.toString().trim();
+    if (s.isNotEmpty) {
+      return s;
+    }
+  }
+  return '';
+}
+
+bool isUsdtFamilyUnit(String unit) {
+  final u = unit.toUpperCase();
+  return u == 'USDT' ||
+      u == 'EUSDT' ||
+      u == 'TUSDT' ||
+      u.contains('BEP20');
+}
+
+Map<String, SupportCoinInfo> supportInfoByUnit(List<SupportCoinInfo> list) {
+  final map = <String, SupportCoinInfo>{};
+  for (final c in list) {
+    final u = c.unit.toLowerCase();
+    if (u.isNotEmpty) {
+      map[u] = c;
+    }
+  }
+  return map;
+}
+
+WithdrawCoinOption mapTransferParent(
+  Map<String, dynamic> row,
+  Map<String, SupportCoinInfo> supportMap,
+) {
+  final unit = firstNonEmptyStr([row['unit']]).toLowerCase();
+  final name = row['name']?.toString() ?? '';
+  final support = supportMap[unit] ?? supportMap[name.toLowerCase()];
+  final minFromCoin = firstNonEmptyStr([row['minWithdrawAmount']]);
+  final minFromSupport = firstNonEmptyStr([
+    support?.minAmount,
+    support?.minWithdrawAmount,
+    support?.minTransferAmount,
+  ]);
+  return WithdrawCoinOption(
+    unit: unit,
+    displayName: firstNonEmptyStr([
+          row['nameCn'],
+          support?.nameCn,
+          support?.aliasName,
+          name,
+        ]).isNotEmpty
+        ? firstNonEmptyStr([
+            row['nameCn'],
+            support?.nameCn,
+            support?.aliasName,
+            name,
+          ])
+        : unit.toUpperCase(),
+    balance: firstNonEmptyStr([support?.balance, '0']),
+    minWithdraw: minFromSupport.isNotEmpty ? minFromSupport : minFromCoin,
+    minTransfer: firstNonEmptyStr([
+      support?.minTransferAmount,
+      support?.minAmount,
+      minFromSupport,
+      minFromCoin,
+    ]),
+    withdrawFee: firstNonEmptyStr([
+      support?.withdrawFeeValue,
+      support?.fee,
+      row['withdrawFeeValue'],
+    ]),
+    isUsdtFamily: isUsdtFamilyUnit(unit),
+    isMainCoin: true,
+  );
+}
+
+WithdrawCoinOption mapWithdrawConfigFromDb(
+  Map<String, dynamic> row,
+  Map<String, SupportCoinInfo> supportMap,
+) {
+  final unit = firstNonEmptyStr([row['unit']]).toLowerCase();
+  final coinName = row['coinName']?.toString() ?? '';
+  final support = supportMap[unit] ?? supportMap[coinName.toLowerCase()];
+  final networkName = row['networkName']?.toString() ?? '';
+  final minFromDb = firstNonEmptyStr([row['minWithdrawAmount'], row['minAmount']]);
+  final minFromSupport =
+      firstNonEmptyStr([support?.minAmount, support?.minWithdrawAmount]);
+  return WithdrawCoinOption(
+    unit: unit,
+    displayName: firstNonEmptyStr([
+          row['coinNameCn'],
+          support?.nameCn,
+          support?.aliasName,
+          coinName,
+        ]).isNotEmpty
+        ? firstNonEmptyStr([
+            row['coinNameCn'],
+            support?.nameCn,
+            support?.aliasName,
+            coinName,
+          ])
+        : unit.toUpperCase(),
+    networkLabel: networkName,
+    networkId: int.tryParse(row['networkId']?.toString() ?? '') ?? 0,
+    balance: firstNonEmptyStr([support?.balance, '0']),
+    minWithdraw: minFromSupport.isNotEmpty ? minFromSupport : minFromDb,
+    minTransfer: firstNonEmptyStr([
+      support?.minTransferAmount,
+      support?.minAmount,
+      minFromSupport,
+    ]),
+    withdrawFee: firstNonEmptyStr([
+      support?.withdrawFeeValue,
+      support?.fee,
+      row['withdrawFeeValue'],
+    ]),
+    isUsdtFamily: isUsdtFamilyUnit(unit),
+    isMainCoin: false,
+  );
+}
+
+String symbolForWithdrawCoin(WithdrawCoinOption? coin) {
+  if (coin == null) {
+    return 'USDT';
+  }
+  if (coin.isMainCoin && coin.unit.toUpperCase() == 'USDT') {
+    return 'USDT';
+  }
+  if (coin.isUsdtFamily &&
+      coin.networkLabel.isNotEmpty &&
+      coin.unit.toUpperCase() != 'USDT') {
+    return '${coin.displayName} (${coin.unit.toUpperCase()})';
+  }
+  return coin.displayName.isNotEmpty ? coin.displayName : coin.unit.toUpperCase();
+}
+
+const networkWithdrawDefaults = <String, ({String fee, String min})>{
+  'TRC20': (fee: '1', min: '10'),
+  'ERC20': (fee: '5', min: '20'),
+  'BEP20': (fee: '2', min: '10'),
+};

+ 30 - 49
lib/data/services/recharge_service.dart

@@ -23,58 +23,46 @@ class RechargeService {
     return [];
   }
 
-  Future<List<RechargeFlatNetworkOption>> getChildrenWithNetwork({
+  Future<List<RechargeNetworkItem>> getRechargeNetworks({
     required String coinName,
   }) async {
     final response = await _dio.get<Map<String, dynamic>>(
-      'uc/coin-network/children-with-network',
+      'uc/coin-network/recharge-networks',
       queryParameters: {'coinName': coinName},
     );
     final data = response.data?['data'];
     if (data is! List) {
       return [];
     }
-    final out = <_FlatNetSort>[];
-    for (final raw in data.whereType<Map<String, dynamic>>()) {
-      final coin = raw['coin'];
-      if (coin is! Map<String, dynamic>) {
-        continue;
-      }
-      final coinNameRaw = coin['name']?.toString() ?? '';
-      final coinNameCnRaw = coin['nameCn']?.toString() ?? '';
-      final nets = raw['networkConfigs'];
-      if (nets is! List) {
-        continue;
-      }
-      for (final n in nets.whereType<Map<String, dynamic>>()) {
-        final network = n['network'];
-        final protocol = network is Map<String, dynamic>
-            ? network['protocol']?.toString() ?? ''
-            : '';
-        final netName = network is Map<String, dynamic>
-            ? network['name']?.toString() ?? ''
-            : '';
-        final cfgId =
-            int.tryParse(n['id']?.toString() ?? '') ?? 0;
-        final sort = int.tryParse(n['sort']?.toString() ?? '') ?? 0;
-        out.add(
-          _FlatNetSort(
-            sort: sort,
-            option: RechargeFlatNetworkOption(
-              coinName: coinNameRaw,
-              coinNameCn: coinNameCnRaw,
-              networkConfigId: cfgId,
-              networkName: netName,
-              protocol: protocol,
-              contractAddress: n['contractAddress']?.toString() ?? '',
-              rechargeAddress: n['rechargeAddress']?.toString() ?? '',
-            ),
-          ),
-        );
-      }
+    return data
+        .whereType<Map<String, dynamic>>()
+        .map(RechargeNetworkItem.fromJson)
+        .where((n) => n.id > 0)
+        .toList()
+      ..sort((a, b) => a.sort.compareTo(b.sort));
+  }
+
+  Future<List<RechargeConfigOption>> getRechargeConfigs({
+    required String coinName,
+    required int networkId,
+  }) async {
+    final response = await _dio.get<Map<String, dynamic>>(
+      'uc/coin-network/recharge-configs',
+      queryParameters: {
+        'coinName': coinName,
+        'networkId': networkId,
+      },
+    );
+    final data = response.data?['data'];
+    if (data is! List) {
+      return [];
     }
-    out.sort((a, b) => a.sort.compareTo(b.sort));
-    return out.map((e) => e.option).toList();
+    return data
+        .whereType<Map<String, dynamic>>()
+        .map(RechargeConfigOption.fromJson)
+        .where((c) => c.networkConfigId > 0)
+        .toList()
+      ..sort((a, b) => a.sort.compareTo(b.sort));
   }
 
   Future<RechargeOrderDetail> createRechargeOrder({
@@ -117,10 +105,3 @@ class RechargeService {
     throw StateError('submit-hash: invalid response');
   }
 }
-
-class _FlatNetSort {
-  _FlatNetSort({required this.sort, required this.option});
-
-  final int sort;
-  final RechargeFlatNetworkOption option;
-}

+ 75 - 0
lib/data/services/withdraw_service.dart

@@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
 
 import '../models/asset/account_auth.dart';
 import '../models/asset/withdraw_balance.dart';
+import '../models/asset/withdraw_coin_option.dart';
 import '../models/asset/withdraw_record.dart';
 
 /// 提币 API 服务(无状态)
@@ -78,6 +79,80 @@ class WithdrawService {
     );
   }
 
+  /// 获取支持币种列表
+  Future<List<SupportCoinInfo>> getSupportCoinInfo() async {
+    final response = await _dio.post<Map<String, dynamic>>(
+      'uc/withdraw/support/coin/info',
+      options: Options(extra: {'noCache': true}),
+    );
+    final data = response.data?['data'];
+    if (data is List) {
+      return data
+          .whereType<Map<String, dynamic>>()
+          .map(SupportCoinInfo.fromJson)
+          .toList();
+    }
+    return [];
+  }
+
+  /// 提币主网列表
+  Future<List<WithdrawNetworkItem>> getWithdrawNetworks() async {
+    final response = await _dio.get<Map<String, dynamic>>(
+      'uc/coin-network/withdraw-networks',
+    );
+    final data = response.data?['data'];
+    if (data is List) {
+      return data
+          .whereType<Map<String, dynamic>>()
+          .map(WithdrawNetworkItem.fromJson)
+          .where((n) => n.id > 0)
+          .toList()
+        ..sort((a, b) => a.sort.compareTo(b.sort));
+    }
+    return [];
+  }
+
+  /// 某主网下可提子币种
+  Future<List<WithdrawCoinOption>> getWithdrawConfigs({
+    required int networkId,
+    required List<SupportCoinInfo> coinInfoList,
+  }) async {
+    final response = await _dio.get<Map<String, dynamic>>(
+      'uc/coin-network/withdraw-configs',
+      queryParameters: {'networkId': networkId},
+    );
+    final data = response.data?['data'];
+    if (data is! List) {
+      return [];
+    }
+    final supportMap = supportInfoByUnit(coinInfoList);
+    return data
+        .whereType<Map<String, dynamic>>()
+        .map((r) => mapWithdrawConfigFromDb(r, supportMap))
+        .where((c) => c.unit.isNotEmpty)
+        .toList();
+  }
+
+  /// 内部转账主币列表
+  Future<List<WithdrawCoinOption>> getTransferParents({
+    required List<SupportCoinInfo> coinInfoList,
+  }) async {
+    final response = await _dio.get<Map<String, dynamic>>(
+      'uc/coin-network/transfer-parents',
+    );
+    final data = response.data?['data'];
+    if (data is! List) {
+      return [];
+    }
+    final supportMap = supportInfoByUnit(coinInfoList);
+    return data
+        .whereType<Map<String, dynamic>>()
+        .map((r) => mapTransferParent(r, supportMap))
+        .where((c) => c.unit.isNotEmpty)
+        .toList()
+      ..sort((a, b) => a.displayName.compareTo(b.displayName));
+  }
+
   /// 获取内部转账最小金额(USDT 基础币种配置)
   Future<Decimal> getTransferMinAmount({String unit = 'USDT'}) async {
     try {

+ 163 - 106
lib/presentation/screens/asset/deposit_screen.dart

@@ -324,24 +324,100 @@ class _DepositScreenState extends ConsumerState<DepositScreen>
     return isTronDepositNetwork(net.protocol, net.networkName);
   }
 
-  String _networkProtocolLabel(RechargeFlatNetworkOption net) {
+  String _parentCoinTabLabel(RechargeParentCoin coin) {
+    if (coin.nameCn.trim().isNotEmpty) {
+      return coin.nameCn;
+    }
+    if (coin.unit.trim().isNotEmpty) {
+      return coin.unit;
+    }
+    return coin.name;
+  }
+
+  String _rechargeNetworkTabLabel(RechargeNetworkItem net) {
     final protocol = net.protocol.trim();
-    final name = net.networkName.trim();
+    final name = net.name.trim();
     if (protocol.isEmpty) {
       return name;
     }
     if (name.isEmpty) {
       return protocol;
     }
-    return '$protocol·$name';
+    return '$name · $protocol';
   }
 
-  String _subCoinLabel(RechargeFlatNetworkOption net) {
-    final cn = net.coinNameCn.trim();
-    if (cn.isNotEmpty && cn != net.coinName) {
-      return '${net.coinName} ($cn)';
+  String _configSubCoinTabLabel(RechargeConfigOption cfg) {
+    final cn = cfg.coinNameCn.trim();
+    if (cn.isNotEmpty) {
+      return cn;
     }
-    return net.coinName;
+    return cfg.coinName;
+  }
+
+  Widget _buildSectionLoading(
+    ColorScheme cs,
+    AppLocalizations l10n,
+    bool isDark,
+  ) {
+    return Container(
+      width: double.infinity,
+      constraints: const BoxConstraints(minHeight: 44),
+      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
+      decoration: _panelDecoration(cs, isDark),
+      child: Row(
+        children: [
+          SizedBox(
+            width: 18,
+            height: 18,
+            child: CircularProgressIndicator(
+              strokeWidth: 2,
+              color: AppColors.brand,
+            ),
+          ),
+          const SizedBox(width: 10),
+          Text(
+            l10n.loading,
+            style: TextStyle(
+              color: cs.onSurface.withAlpha(140),
+              fontSize: 13,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildSelectableChip({
+    required BuildContext context,
+    required ColorScheme cs,
+    required String label,
+    required bool selected,
+    VoidCallback? onTap,
+  }) {
+    return Padding(
+      padding: const EdgeInsets.only(right: 10, bottom: 8),
+      child: GestureDetector(
+        onTap: onTap,
+        child: Container(
+          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+          decoration: BoxDecoration(
+            color: selected ? AppColors.brand : Colors.transparent,
+            border: Border.all(
+              color: selected ? AppColors.brand : cs.outline.withAlpha(80),
+            ),
+            borderRadius: BorderRadius.circular(8),
+          ),
+          child: Text(
+            label,
+            style: TextStyle(
+              color: selected ? Colors.black : cs.onSurface,
+              fontSize: 14,
+              fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
+            ),
+          ),
+        ),
+      ),
+    );
   }
 
   String _walletPayButtonLabel(
@@ -508,95 +584,89 @@ class _DepositScreenState extends ConsumerState<DepositScreen>
     bool isDark,
   ) {
     final net = state.selectedNetwork;
-    final flatIndex = state.flatNetworks.isEmpty
-        ? null
-        : state.selectedFlatIndex.clamp(0, state.flatNetworks.length - 1);
+    final canCreate = state.selectedNetworkId > 0 && state.selectedConfig != null;
 
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
         _buildModeTabSwitcher(cs, l10n, isDark),
         const SizedBox(height: 20),
+        if (state.parentCoins.length > 1) ...[
+          Text(
+            l10n.depositCurrency,
+            style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
+          ),
+          const SizedBox(height: 8),
+          Wrap(
+            children: [
+              for (var i = 0; i < state.parentCoins.length; i++)
+                _buildSelectableChip(
+                  context: context,
+                  cs: cs,
+                  label: _parentCoinTabLabel(state.parentCoins[i]),
+                  selected: state.selectedParentIndex == i,
+                  onTap: state.childrenLoading
+                      ? null
+                      : () => notifier.selectParent(i),
+                ),
+            ],
+          ),
+          const SizedBox(height: 16),
+        ],
         Text(
           l10n.depositMainnetProtocol,
           style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
         ),
         const SizedBox(height: 8),
-        Container(
-          padding: const EdgeInsets.symmetric(horizontal: 12),
-          decoration: _panelDecoration(cs, isDark),
-          child: DropdownButtonHideUnderline(
-            child: DropdownButton<int>(
-              isExpanded: true,
-              value: flatIndex,
-              hint: Text(
-                state.childrenLoading
-                    ? l10n.loading
-                    : l10n.depositSelectNetworkFirst,
-                style: TextStyle(color: cs.onSurface.withAlpha(140)),
-              ),
-              icon: Icon(Icons.keyboard_arrow_down,
-                  color: cs.onSurface.withAlpha(153)),
-              items: [
-                for (var i = 0; i < state.flatNetworks.length; i++)
-                  DropdownMenuItem(
-                    value: i,
-                    child: Text(
-                      _networkProtocolLabel(state.flatNetworks[i]),
-                      style: TextStyle(color: cs.onSurface, fontSize: 15),
-                    ),
+        if (state.childrenLoading)
+          _buildSectionLoading(cs, l10n, isDark)
+        else if (state.networkOptions.isEmpty)
+          Text(
+            l10n.depositNoNetwork,
+            style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 13),
+          )
+        else
+          Wrap(
+            children: [
+              for (final item in state.networkOptions)
+                _buildSelectableChip(
+                  context: context,
+                  cs: cs,
+                  label: _rechargeNetworkTabLabel(item),
+                  selected: state.selectedNetworkId == item.id,
+                  onTap: () => notifier.selectNetwork(item.id),
+                ),
+            ],
+          ),
+        if (state.selectedNetworkId > 0 && !state.childrenLoading) ...[
+          const SizedBox(height: 16),
+          Text(
+            l10n.depositSubCoin,
+            style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
+          ),
+          const SizedBox(height: 8),
+          if (state.subCoinsLoading)
+            _buildSectionLoading(cs, l10n, isDark)
+          else if (state.subCoinOptions.isEmpty)
+            Text(
+              l10n.depositNoSubCoin,
+              style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 13),
+            )
+          else
+            Wrap(
+              children: [
+                for (var i = 0; i < state.subCoinOptions.length; i++)
+                  _buildSelectableChip(
+                    context: context,
+                    cs: cs,
+                    label: _configSubCoinTabLabel(state.subCoinOptions[i]),
+                    selected: state.selectedSubCoinIndex == i,
+                    onTap: () => notifier.selectSubCoin(i),
                   ),
               ],
-              onChanged: state.childrenLoading
-                  ? null
-                  : (v) {
-                      if (v != null) {
-                        notifier.selectFlatNetwork(v);
-                      }
-                    },
             ),
-          ),
-        ),
-        if (state.parentCoins.length > 1) ...[
-          const SizedBox(height: 12),
-          Container(
-            padding: const EdgeInsets.symmetric(horizontal: 12),
-            decoration: _panelDecoration(cs, isDark),
-            child: DropdownButtonHideUnderline(
-              child: DropdownButton<int>(
-                isExpanded: true,
-                value: state.parentCoins.isEmpty
-                    ? null
-                    : state.selectedParentIndex.clamp(
-                        0, state.parentCoins.length - 1),
-                items: [
-                  for (var i = 0; i < state.parentCoins.length; i++)
-                    DropdownMenuItem(
-                      value: i,
-                      child: Text(
-                        state.parentCoins[i].nameCn.isNotEmpty
-                            ? state.parentCoins[i].nameCn
-                            : state.parentCoins[i].name,
-                        style: TextStyle(color: cs.onSurface, fontSize: 15),
-                      ),
-                    ),
-                ],
-                onChanged: state.childrenLoading
-                    ? null
-                    : (v) {
-                        if (v != null) {
-                          notifier.selectParent(v);
-                        }
-                      },
-              ),
-            ),
-          ),
-        ],
-        if (state.childrenLoading) ...[
-          const SizedBox(height: 12),
-          const LinearProgressIndicator(),
         ],
-        if (net != null) ...[
+        if (net != null && net.hasTokenContract) ...[
           const SizedBox(height: 12),
           Container(
             width: double.infinity,
@@ -606,32 +676,19 @@ class _DepositScreenState extends ConsumerState<DepositScreen>
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
                 Text(
-                  l10n.depositSubCoin,
-                  style:
-                      TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 12),
+                  l10n.depositContractAddress,
+                  style: TextStyle(
+                      color: cs.onSurface.withAlpha(140), fontSize: 12),
                 ),
                 const SizedBox(height: 4),
-                Text(
-                  _subCoinLabel(net),
-                  style: TextStyle(color: cs.onSurface, fontSize: 14),
-                ),
-                if (net.hasTokenContract) ...[
-                  const SizedBox(height: 12),
-                  Text(
-                    l10n.depositContractAddress,
-                    style: TextStyle(
-                        color: cs.onSurface.withAlpha(140), fontSize: 12),
+                SelectableText(
+                  net.contractAddress,
+                  style: TextStyle(
+                    color: cs.onSurface,
+                    fontSize: 12,
+                    fontFamily: 'monospace',
                   ),
-                  const SizedBox(height: 4),
-                  SelectableText(
-                    net.contractAddress,
-                    style: TextStyle(
-                      color: cs.onSurface,
-                      fontSize: 12,
-                      fontFamily: 'monospace',
-                    ),
-                  ),
-                ],
+                ),
               ],
             ),
           ),
@@ -672,7 +729,7 @@ class _DepositScreenState extends ConsumerState<DepositScreen>
                 borderRadius: BorderRadius.circular(10),
               ),
             ),
-            onPressed: state.orderSubmitting || state.flatNetworks.isEmpty
+            onPressed: state.orderSubmitting || !canCreate
                 ? null
                 : _onCreateOrder,
             child: state.orderSubmitting

+ 210 - 85
lib/presentation/screens/asset/withdraw_screen.dart

@@ -8,6 +8,7 @@ import '../../../core/l10n/app_localizations.dart';
 import '../../../core/theme/app_colors.dart';
 import '../../../core/utils/dialog_utils.dart' show extractErrorMessage;
 import '../../../core/utils/top_toast.dart';
+import '../../../data/models/asset/withdraw_coin_option.dart';
 import '../../../providers/withdraw_provider.dart';
 
 class WithdrawScreen extends ConsumerStatefulWidget {
@@ -105,6 +106,33 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
   }
 
   Widget _buildBody(BuildContext context, WithdrawState state, WithdrawNotifier notifier) {
+    if (state.isLoading) {
+      return const Center(child: CircularProgressIndicator());
+    }
+    if (state.errorMessage != null && state.networkList.isEmpty) {
+      final l10n = AppLocalizations.of(context)!;
+      return Center(
+        child: Padding(
+          padding: const EdgeInsets.all(24),
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Text(
+                resolveProviderError(state.errorMessage, l10n) ??
+                    state.errorMessage!,
+                textAlign: TextAlign.center,
+              ),
+              const SizedBox(height: 16),
+              FilledButton(
+                onPressed: notifier.refresh,
+                child: Text(l10n.retry),
+              ),
+            ],
+          ),
+        ),
+      );
+    }
+
     final isDark = Theme.of(context).brightness == Brightness.dark;
 
     return Column(
@@ -245,12 +273,66 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
     );
   }
 
+  String _withdrawNetworkTabLabel(WithdrawNetworkItem net) {
+    final protocol = net.protocol.trim();
+    final name = net.name.trim();
+    if (protocol.isEmpty) {
+      return name;
+    }
+    if (name.isEmpty) {
+      return protocol;
+    }
+    return '$name · $protocol';
+  }
+
+  String _subCoinTabLabel(WithdrawCoinOption coin) {
+    if (coin.displayName.trim().isNotEmpty) {
+      return coin.displayName;
+    }
+    return coin.unit.toUpperCase();
+  }
+
+  Widget _buildSelectableChip({
+    required BuildContext context,
+    required ColorScheme cs,
+    required String label,
+    required bool selected,
+    required VoidCallback? onTap,
+  }) {
+    return Padding(
+      padding: const EdgeInsets.only(right: 10, bottom: 8),
+      child: GestureDetector(
+        onTap: onTap,
+        child: Container(
+          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+          decoration: BoxDecoration(
+            color: selected ? AppColors.brand : Colors.transparent,
+            border: Border.all(
+              color: selected ? AppColors.brand : cs.outline.withAlpha(80),
+            ),
+            borderRadius: BorderRadius.circular(8),
+          ),
+          child: Text(
+            label,
+            style: TextStyle(
+              color: selected ? Colors.black : cs.onSurface,
+              fontSize: 14,
+              fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
   Widget _buildOnChain(BuildContext context, WithdrawState state, WithdrawNotifier notifier) {
     final cs = Theme.of(context).colorScheme;
     final isDark = Theme.of(context).brightness == Brightness.dark;
+    final l10n = AppLocalizations.of(context)!;
     final fee = state.fee;
     final minAmount = state.minWithdrawAmount;
     final available = state.withdrawableBalance;
+    final coinSymbol = state.onchainCoinSymbol;
 
     // 到账金额计算
     final inputAmount = Decimal.tryParse(_amountController.text) ?? Decimal.zero;
@@ -260,16 +342,9 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
-        // 选择币种
-        _Label(AppLocalizations.of(context)!.selectCoin),
-        const SizedBox(height: 8),
-        _FixedCoinField(coin: 'USDT'),
-        const SizedBox(height: 16),
-
-        // 提币网络
         Row(
           children: [
-            _Label(AppLocalizations.of(context)!.withdrawNetwork),
+            _Label(l10n.withdrawNetwork),
             const SizedBox(width: 4),
             GestureDetector(
               onTap: () => _showNetworkTipDialog(context),
@@ -278,38 +353,65 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
           ],
         ),
         const SizedBox(height: 8),
-        Row(
-          children: List.generate(state.networkNames.length, (i) {
-            final selected = i == state.selectedNetworkIndex;
-            return Padding(
-              padding: const EdgeInsets.only(right: 10),
-              child: GestureDetector(
-                onTap: () => notifier.selectNetwork(i),
-                child: Container(
-                  padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
-                  decoration: BoxDecoration(
-                    color: selected ? AppColors.brand : Colors.transparent,
-                    border: Border.all(color: selected ? AppColors.brand : cs.outline.withAlpha(80)),
-                    borderRadius: BorderRadius.circular(8),
-                  ),
-                  child: Text(state.networkNames[i], style: TextStyle(
-                    color: selected ? Colors.black : cs.onSurface,
-                    fontSize: 14,
-                    fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
-                  )),
+        if (state.networkList.isEmpty)
+          Text(
+            l10n.depositNoNetwork,
+            style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 13),
+          )
+        else
+          Wrap(
+            children: [
+              for (final net in state.networkList)
+                _buildSelectableChip(
+                  context: context,
+                  cs: cs,
+                  label: _withdrawNetworkTabLabel(net),
+                  selected: state.selectedNetworkId == net.id,
+                  onTap: () {
+                    _amountController.clear();
+                    notifier.selectNetwork(net.id);
+                    setState(() {});
+                  },
                 ),
-              ),
-            );
-          }),
-        ),
+            ],
+          ),
+        if (state.selectedNetworkId > 0) ...[
+          const SizedBox(height: 16),
+          _Label(l10n.depositSubCoin),
+          const SizedBox(height: 8),
+          if (state.onchainSubCoinsLoading)
+            const LinearProgressIndicator()
+          else if (state.onchainSubCoins.isEmpty)
+            Text(
+              l10n.depositNoSubCoin,
+              style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 13),
+            )
+          else
+            Wrap(
+              children: [
+                for (var i = 0; i < state.onchainSubCoins.length; i++)
+                  _buildSelectableChip(
+                    context: context,
+                    cs: cs,
+                    label: _subCoinTabLabel(state.onchainSubCoins[i]),
+                    selected: state.selectedSubCoinIndex == i,
+                    onTap: () {
+                      _amountController.clear();
+                      notifier.selectSubCoin(i);
+                      setState(() {});
+                    },
+                  ),
+              ],
+            ),
+        ],
         const SizedBox(height: 16),
 
         // 提币地址
-        _Label(AppLocalizations.of(context)!.withdrawAddress),
+        _Label(l10n.withdrawAddress),
         const SizedBox(height: 8),
         _InputField(
           controller: _addressController,
-          hint: AppLocalizations.of(context)!.enterWithdrawAddress,
+          hint: l10n.withdrawAddressForCoin(coinSymbol),
           suffixIcon: Icon(Icons.qr_code_scanner, size: 20, color: cs.onSurface.withAlpha(153)),
           onSuffixIconTap: _scanQrCode,
         ),
@@ -318,7 +420,7 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
         // 提币金额
         Row(
           children: [
-            _Label(AppLocalizations.of(context)!.withdrawAmount),
+            _Label(l10n.withdrawAmountCoin(coinSymbol)),
             const SizedBox(width: 4),
             Icon(Icons.info_outline, size: 14, color: cs.onSurface.withAlpha(120)),
           ],
@@ -326,21 +428,21 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
         const SizedBox(height: 8),
         _InputField(
           controller: _amountController,
-          hint: AppLocalizations.of(context)!.withdrawMinAmountHint(minAmount),
+          hint: l10n.withdrawMinAmountHint(minAmount),
           keyboardType: const TextInputType.numberWithOptions(decimal: true),
           inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,4}'))],
           onChanged: (_) => setState(() {}),
           suffix: Row(
             mainAxisSize: MainAxisSize.min,
             children: [
-              Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)),
+              Text(coinSymbol, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)),
               const SizedBox(width: 8),
               GestureDetector(
                 onTap: () {
                   _amountController.text = available.toString();
                   setState(() {});
                 },
-                child: Text(AppLocalizations.of(context)!.max, style: const TextStyle(color: AppColors.brand, fontSize: 14, fontWeight: FontWeight.w600)),
+                child: Text(l10n.max, style: const TextStyle(color: AppColors.brand, fontSize: 14, fontWeight: FontWeight.w600)),
               ),
             ],
           ),
@@ -348,26 +450,26 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
         const SizedBox(height: 6),
         Row(
           children: [
-            Text('${AppLocalizations.of(context)!.fundAccountAvailable}:', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12)),
-            Flexible(child: Text('$available USDT ', overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600))),
+            Text('${l10n.fundAccountAvailable}:', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12)),
+            Flexible(child: Text('$available $coinSymbol ', overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600))),
             GestureDetector(
               onTap: () async {
                 await context.push('/asset/transfer');
                 if (mounted) ref.read(withdrawProvider.notifier).refresh();
               },
-              child: Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(color: AppColors.brand, fontSize: 12, fontWeight: FontWeight.w500)),
+              child: Text(l10n.transfer, style: const TextStyle(color: AppColors.brand, fontSize: 12, fontWeight: FontWeight.w500)),
             ),
           ],
         ),
         const SizedBox(height: 20),
 
         // 到账数量
-        _Label(AppLocalizations.of(context)!.receivedAmount),
+        _Label(l10n.receivedAmount),
         const SizedBox(height: 8),
         Container(
           padding: const EdgeInsets.all(12),
           decoration: BoxDecoration(color: isDark ? AppColors.darkBgTertiary : AppColors.lightBgTertiary, borderRadius: BorderRadius.circular(8)),
-          child: Text('$receiveDisplay USDT', style: TextStyle(color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600)),
+          child: Text('$receiveDisplay $coinSymbol', style: TextStyle(color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600)),
         ),
         const SizedBox(height: 16),
 
@@ -375,8 +477,8 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
         Row(
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: [
-            _Label(AppLocalizations.of(context)!.fee),
-            Text('$fee USDT', style: TextStyle(color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w600)),
+            _Label(l10n.fee),
+            Text('$fee $coinSymbol', style: TextStyle(color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w600)),
           ],
         ),
         const SizedBox(height: 20),
@@ -390,7 +492,9 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
           width: double.infinity,
           height: 50,
           child: ElevatedButton(
-            onPressed: state.isSubmitting ? null : () => _submitOnChain(notifier),
+            onPressed: state.isSubmitting || state.onchainSelectedCoin == null
+                ? null
+                : () => _submitOnChain(notifier),
             style: ElevatedButton.styleFrom(
               backgroundColor: AppColors.brand,
               disabledBackgroundColor: AppColors.brand.withAlpha(80),
@@ -399,7 +503,7 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
             ),
             child: state.isSubmitting
                 ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black))
-                : Text(AppLocalizations.of(context)!.withdrawCoin, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
+                : Text(l10n.withdrawCoin, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
           ),
         ),
         const SizedBox(height: 16),
@@ -418,39 +522,82 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
 
   Widget _buildInternal(BuildContext context, WithdrawState state, WithdrawNotifier notifier) {
     final cs = Theme.of(context).colorScheme;
+    final isDark = Theme.of(context).brightness == Brightness.dark;
+    final l10n = AppLocalizations.of(context)!;
     final available = state.transferableBalance;
+    final coinSymbol = state.transferCoinSymbol;
 
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
-        _Label(AppLocalizations.of(context)!.selectCoin),
-        const SizedBox(height: 8),
-        _FixedCoinField(coin: 'USDT'),
-        const SizedBox(height: 16),
+        if (state.transferMainCoins.isNotEmpty) ...[
+          _Label(l10n.withdrawMainCoinLabel),
+          const SizedBox(height: 8),
+          Container(
+            padding: const EdgeInsets.symmetric(horizontal: 12),
+            decoration: BoxDecoration(
+              color: isDark ? AppColors.darkBgTertiary : AppColors.lightBgTertiary,
+              borderRadius: BorderRadius.circular(8),
+              border: Border.all(color: cs.outline.withAlpha(80)),
+            ),
+            child: DropdownButtonHideUnderline(
+              child: DropdownButton<int>(
+                isExpanded: true,
+                value: state.selectedTransferCoinIndex.clamp(
+                  0,
+                  state.transferMainCoins.length - 1,
+                ),
+                items: [
+                  for (var i = 0; i < state.transferMainCoins.length; i++)
+                    DropdownMenuItem(
+                      value: i,
+                      child: Text(
+                        _subCoinTabLabel(state.transferMainCoins[i]),
+                        style: TextStyle(color: cs.onSurface, fontSize: 14),
+                      ),
+                    ),
+                ],
+                onChanged: (v) {
+                  if (v != null) {
+                    _amountController.clear();
+                    notifier.selectTransferCoin(v);
+                    setState(() {});
+                  }
+                },
+              ),
+            ),
+          ),
+          const SizedBox(height: 6),
+          Text(
+            l10n.withdrawTransferMainCoinHint,
+            style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12),
+          ),
+          const SizedBox(height: 16),
+        ],
 
-        _Label(AppLocalizations.of(context)!.recipientUidOrAccount),
+        _Label(l10n.recipientUidOrAccount),
         const SizedBox(height: 8),
         _InputField(
           controller: _addressController,
-          hint: AppLocalizations.of(context)!.enterRecipientUid,
+          hint: l10n.enterRecipientUid,
         ),
         const SizedBox(height: 16),
 
-        _Label(AppLocalizations.of(context)!.withdrawAmount),
+        _Label(l10n.transferAmountCoin(coinSymbol)),
         const SizedBox(height: 8),
         _InputField(
           controller: _amountController,
-          hint: AppLocalizations.of(context)!.transferMinAmountHint(state.transferMinAmount),
+          hint: l10n.transferMinAmountHint(state.transferMinAmount),
           keyboardType: const TextInputType.numberWithOptions(decimal: true),
           inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,4}'))],
           suffix: Row(
             mainAxisSize: MainAxisSize.min,
             children: [
-              Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)),
+              Text(coinSymbol, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)),
               const SizedBox(width: 8),
               GestureDetector(
                 onTap: () => _amountController.text = available.toString(),
-                child: Text(AppLocalizations.of(context)!.max, style: const TextStyle(color: AppColors.brand, fontSize: 14, fontWeight: FontWeight.w600)),
+                child: Text(l10n.max, style: const TextStyle(color: AppColors.brand, fontSize: 14, fontWeight: FontWeight.w600)),
               ),
             ],
           ),
@@ -458,14 +605,14 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
         const SizedBox(height: 6),
         Row(
           children: [
-            Text('${AppLocalizations.of(context)!.fundAccountAvailable}:', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12)),
-            Flexible(child: Text('$available USDT ', overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600))),
+            Text('${l10n.fundAccountAvailable}:', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12)),
+            Flexible(child: Text('$available $coinSymbol ', overflow: TextOverflow.ellipsis, style: TextStyle(color: cs.onSurface, fontSize: 12, fontWeight: FontWeight.w600))),
             GestureDetector(
               onTap: () async {
                 await context.push('/asset/transfer');
                 if (mounted) ref.read(withdrawProvider.notifier).refresh();
               },
-              child: Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(color: AppColors.brand, fontSize: 12, fontWeight: FontWeight.w500)),
+              child: Text(l10n.transfer, style: const TextStyle(color: AppColors.brand, fontSize: 12, fontWeight: FontWeight.w500)),
             ),
           ],
         ),
@@ -478,7 +625,9 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
           width: double.infinity,
           height: 50,
           child: ElevatedButton(
-            onPressed: state.isSubmitting ? null : () => _submitTransfer(notifier),
+            onPressed: state.isSubmitting || state.transferSelectedCoin == null
+                ? null
+                : () => _submitTransfer(notifier),
             style: ElevatedButton.styleFrom(
               backgroundColor: AppColors.brand,
               foregroundColor: Colors.black,
@@ -486,7 +635,7 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
             ),
             child: state.isSubmitting
                 ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black))
-                : Text(AppLocalizations.of(context)!.transfer, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
+                : Text(l10n.transfer, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
           ),
         ),
         const SizedBox(height: 16),
@@ -678,30 +827,6 @@ class _WithdrawScreenState extends ConsumerState<WithdrawScreen>
 
 // ── 共享组件 ─────────────────────────────────────────────────
 
-class _FixedCoinField extends StatelessWidget {
-  const _FixedCoinField({required this.coin});
-  final String coin;
-
-  @override
-  Widget build(BuildContext context) {
-    final cs = Theme.of(context).colorScheme;
-    return Container(
-      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
-      decoration: BoxDecoration(
-        border: Border.all(color: cs.outline.withAlpha(60)),
-        borderRadius: BorderRadius.circular(10),
-      ),
-      child: Row(
-        children: [
-          Text(coin, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w500)),
-          const Spacer(),
-          Icon(Icons.keyboard_arrow_down, color: cs.onSurface.withAlpha(153)),
-        ],
-      ),
-    );
-  }
-}
-
 class _Label extends StatelessWidget {
   const _Label(this.text);
   final String text;

+ 123 - 26
lib/providers/deposit_provider.dart

@@ -10,12 +10,15 @@ import '../data/services/recharge_service.dart';
 
 class DepositState {
   final List<RechargeParentCoin> parentCoins;
-  final List<RechargeFlatNetworkOption> flatNetworks;
+  final List<RechargeNetworkItem> networkOptions;
+  final List<RechargeConfigOption> subCoinOptions;
   final int selectedParentIndex;
-  final int selectedFlatIndex;
+  final int selectedNetworkId;
+  final int selectedSubCoinIndex;
   final RechargeOrderDetail? currentOrder;
   final bool loadingParents;
   final bool childrenLoading;
+  final bool subCoinsLoading;
   final bool orderSubmitting;
   final bool hashSubmitting;
   final bool walletPayBusy;
@@ -23,12 +26,15 @@ class DepositState {
 
   const DepositState({
     this.parentCoins = const [],
-    this.flatNetworks = const [],
+    this.networkOptions = const [],
+    this.subCoinOptions = const [],
     this.selectedParentIndex = 0,
-    this.selectedFlatIndex = 0,
+    this.selectedNetworkId = 0,
+    this.selectedSubCoinIndex = 0,
     this.currentOrder,
     this.loadingParents = false,
     this.childrenLoading = false,
+    this.subCoinsLoading = false,
     this.orderSubmitting = false,
     this.hashSubmitting = false,
     this.walletPayBusy = false,
@@ -40,19 +46,27 @@ class DepositState {
           ? parentCoins[selectedParentIndex]
           : null;
 
-  RechargeFlatNetworkOption? get selectedNetwork =>
-      flatNetworks.isNotEmpty && selectedFlatIndex < flatNetworks.length
-          ? flatNetworks[selectedFlatIndex]
+  RechargeConfigOption? get selectedConfig =>
+      subCoinOptions.isNotEmpty && selectedSubCoinIndex < subCoinOptions.length
+          ? subCoinOptions[selectedSubCoinIndex]
           : null;
 
+  RechargeFlatNetworkOption? get selectedNetwork {
+    final cfg = selectedConfig;
+    return cfg != null ? RechargeFlatNetworkOption.fromConfig(cfg) : null;
+  }
+
   DepositState copyWith({
     List<RechargeParentCoin>? parentCoins,
-    List<RechargeFlatNetworkOption>? flatNetworks,
+    List<RechargeNetworkItem>? networkOptions,
+    List<RechargeConfigOption>? subCoinOptions,
     int? selectedParentIndex,
-    int? selectedFlatIndex,
+    int? selectedNetworkId,
+    int? selectedSubCoinIndex,
     RechargeOrderDetail? currentOrder,
     bool? loadingParents,
     bool? childrenLoading,
+    bool? subCoinsLoading,
     bool? orderSubmitting,
     bool? hashSubmitting,
     bool? walletPayBusy,
@@ -62,12 +76,15 @@ class DepositState {
   }) {
     return DepositState(
       parentCoins: parentCoins ?? this.parentCoins,
-      flatNetworks: flatNetworks ?? this.flatNetworks,
+      networkOptions: networkOptions ?? this.networkOptions,
+      subCoinOptions: subCoinOptions ?? this.subCoinOptions,
       selectedParentIndex: selectedParentIndex ?? this.selectedParentIndex,
-      selectedFlatIndex: selectedFlatIndex ?? this.selectedFlatIndex,
+      selectedNetworkId: selectedNetworkId ?? this.selectedNetworkId,
+      selectedSubCoinIndex: selectedSubCoinIndex ?? this.selectedSubCoinIndex,
       currentOrder: clearOrder ? null : (currentOrder ?? this.currentOrder),
       loadingParents: loadingParents ?? this.loadingParents,
       childrenLoading: childrenLoading ?? this.childrenLoading,
+      subCoinsLoading: subCoinsLoading ?? this.subCoinsLoading,
       orderSubmitting: orderSubmitting ?? this.orderSubmitting,
       hashSubmitting: hashSubmitting ?? this.hashSubmitting,
       walletPayBusy: walletPayBusy ?? this.walletPayBusy,
@@ -101,35 +118,97 @@ class DepositNotifier extends Notifier<DepositState> {
       if (list.isNotEmpty) {
         await loadChildrenForParent(list[idx].name);
       } else {
-        state = state.copyWith(flatNetworks: [], selectedFlatIndex: 0);
+        state = state.copyWith(
+          networkOptions: [],
+          subCoinOptions: [],
+          selectedNetworkId: 0,
+          selectedSubCoinIndex: 0,
+        );
       }
     } catch (e) {
       state = state.copyWith(
         loadingParents: false,
         errorMessage: e.toString(),
         parentCoins: [],
-        flatNetworks: [],
+        networkOptions: [],
+        subCoinOptions: [],
       );
     }
   }
 
   Future<void> loadChildrenForParent(String coinName) async {
-    state = state.copyWith(childrenLoading: true, clearError: true);
+    state = state.copyWith(
+      childrenLoading: true,
+      networkOptions: [],
+      subCoinOptions: [],
+      selectedNetworkId: 0,
+      selectedSubCoinIndex: 0,
+      clearError: true,
+    );
+    try {
+      final dio = ref.read(dioClientProvider);
+      final service = RechargeService(dio);
+      final nets = await service.getRechargeNetworks(coinName: coinName);
+      var networkId = state.selectedNetworkId;
+      if (nets.isEmpty) {
+        networkId = 0;
+      } else if (!nets.any((n) => n.id == networkId)) {
+        networkId = nets.first.id;
+      }
+      state = state.copyWith(
+        networkOptions: nets,
+        selectedNetworkId: networkId,
+        selectedSubCoinIndex: 0,
+      );
+      if (networkId > 0) {
+        await _loadSubCoins(coinName, networkId);
+      } else {
+        state = state.copyWith(
+          subCoinOptions: [],
+          childrenLoading: false,
+          clearError: true,
+        );
+      }
+    } catch (e) {
+      state = state.copyWith(
+        childrenLoading: false,
+        errorMessage: e.toString(),
+        networkOptions: [],
+        subCoinOptions: [],
+      );
+    }
+  }
+
+  Future<void> _loadSubCoins(String coinName, int networkId) async {
+    state = state.copyWith(
+      subCoinsLoading: true,
+      subCoinOptions: [],
+      selectedSubCoinIndex: 0,
+      clearError: true,
+    );
     try {
       final dio = ref.read(dioClientProvider);
-      final nets =
-          await RechargeService(dio).getChildrenWithNetwork(coinName: coinName);
+      final configs = await RechargeService(dio).getRechargeConfigs(
+        coinName: coinName,
+        networkId: networkId,
+      );
+      var subIdx = state.selectedSubCoinIndex;
+      if (subIdx >= configs.length) {
+        subIdx = 0;
+      }
       state = state.copyWith(
-        flatNetworks: nets,
-        selectedFlatIndex: 0,
+        subCoinOptions: configs,
+        selectedSubCoinIndex: subIdx,
+        subCoinsLoading: false,
         childrenLoading: false,
         clearError: true,
       );
     } catch (e) {
       state = state.copyWith(
+        subCoinsLoading: false,
         childrenLoading: false,
         errorMessage: e.toString(),
-        flatNetworks: [],
+        subCoinOptions: [],
       );
     }
   }
@@ -138,21 +217,39 @@ class DepositNotifier extends Notifier<DepositState> {
     if (index < 0 || index >= state.parentCoins.length) {
       return;
     }
-    state = state.copyWith(selectedParentIndex: index);
+    state = state.copyWith(
+      selectedParentIndex: index,
+      selectedSubCoinIndex: 0,
+    );
     final p = state.parentCoins[index];
     await loadChildrenForParent(p.name);
   }
 
-  void selectFlatNetwork(int index) {
-    if (index >= 0 && index < state.flatNetworks.length) {
-      state = state.copyWith(selectedFlatIndex: index);
+  Future<void> selectNetwork(int networkId) async {
+    if (networkId <= 0 ||
+        !state.networkOptions.any((n) => n.id == networkId)) {
+      return;
+    }
+    state = state.copyWith(
+      selectedNetworkId: networkId,
+      selectedSubCoinIndex: 0,
+    );
+    final parent = state.selectedParent;
+    if (parent != null) {
+      await _loadSubCoins(parent.name, networkId);
+    }
+  }
+
+  void selectSubCoin(int index) {
+    if (index >= 0 && index < state.subCoinOptions.length) {
+      state = state.copyWith(selectedSubCoinIndex: index);
     }
   }
 
   Future<void> createRechargeOrder(String amount) async {
-    final opt = state.selectedNetwork;
+    final opt = state.selectedConfig;
     if (opt == null) {
-      state = state.copyWith(errorMessage: '请先选择充值网络');
+      state = state.copyWith(errorMessage: '请先选择充值网络与子币种');
       return;
     }
     state = state.copyWith(orderSubmitting: true, clearError: true);
@@ -305,4 +402,4 @@ class RechargeHistoryNotifier extends Notifier<RechargeHistoryState> {
 final rechargeHistoryProvider =
     NotifierProvider<RechargeHistoryNotifier, RechargeHistoryState>(
   RechargeHistoryNotifier.new,
-);
+);

+ 264 - 108
lib/providers/withdraw_provider.dart

@@ -6,10 +6,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import '../core/network/dio_client.dart';
 import '../core/utils/dialog_utils.dart' show extractErrorMessage;
 import '../data/models/asset/account_auth.dart';
-import '../data/models/asset/deposit_wallet.dart';
 import '../data/models/asset/withdraw_balance.dart';
+import '../data/models/asset/withdraw_coin_option.dart';
 import '../data/models/asset/withdraw_record.dart';
-import '../data/services/asset_service.dart';
 import '../data/services/withdraw_service.dart';
 
 // ══════════════════════════════════════════════════════════════
@@ -20,91 +19,136 @@ class WithdrawState {
   /// 0=链上提币, 1=内部转账
   final int tabIndex;
 
-  /// 钱包列表(含网络信息,复用充币的 DepositWallet)
-  final List<DepositWallet> usdtWallets;
+  final List<WithdrawNetworkItem> networkList;
+  final int selectedNetworkId;
+  final List<WithdrawCoinOption> onchainSubCoins;
+  final bool onchainSubCoinsLoading;
+  final int selectedSubCoinIndex;
 
-  /// 当前选中的网络索引
-  final int selectedNetworkIndex;
+  final List<WithdrawCoinOption> transferMainCoins;
+  final int selectedTransferCoinIndex;
 
-  /// 可用余额
+  final List<SupportCoinInfo> coinInfoList;
   final WithdrawBalance? balance;
-
-  /// 认证信息
   final AccountAuth? auth;
 
-  /// 验证码倒计时秒数(0=可发送)
   final int codeCountdown;
-
-  /// 提交中
   final bool isSubmitting;
-
   final bool isLoading;
   final String? errorMessage;
 
-  /// 内部转账最小金额(USDT 基础配置,null 表示未加载)
-  final Decimal? _transferMinAmount;
-
   const WithdrawState({
     this.tabIndex = 0,
-    this.usdtWallets = const [],
-    this.selectedNetworkIndex = -1,
+    this.networkList = const [],
+    this.selectedNetworkId = 0,
+    this.onchainSubCoins = const [],
+    this.onchainSubCoinsLoading = false,
+    this.selectedSubCoinIndex = 0,
+    this.transferMainCoins = const [],
+    this.selectedTransferCoinIndex = 0,
+    this.coinInfoList = const [],
     this.balance,
     this.auth,
-    Decimal? transferMinAmount,
     this.codeCountdown = 0,
     this.isSubmitting = false,
     this.isLoading = false,
     this.errorMessage,
-  }) : _transferMinAmount = transferMinAmount;
+  });
 
-  /// 当前选中的钱包(链上提币模式下使用)
-  DepositWallet? get selectedWallet =>
-      selectedNetworkIndex >= 0 && usdtWallets.isNotEmpty && selectedNetworkIndex < usdtWallets.length
-          ? usdtWallets[selectedNetworkIndex]
+  WithdrawCoinOption? get onchainSelectedCoin =>
+      onchainSubCoins.isNotEmpty && selectedSubCoinIndex < onchainSubCoins.length
+          ? onchainSubCoins[selectedSubCoinIndex]
           : null;
 
-  /// 当前币种信息
-  WalletCoin? get currentCoin => selectedWallet?.coin;
-
-  /// 网络名称列表
-  List<String> get networkNames =>
-      usdtWallets.map((w) => w.coin?.networkName ?? '').toList();
-
-  /// 链上提币的 unit(币种代号)
-  String get onChainUnit => currentCoin?.code ?? 'TUSDT';
+  WithdrawCoinOption? get transferSelectedCoin =>
+      transferMainCoins.isNotEmpty &&
+              selectedTransferCoinIndex < transferMainCoins.length
+          ? transferMainCoins[selectedTransferCoinIndex]
+          : null;
 
-  /// 手续费(withdrawFeeValue)
-  Decimal get fee => currentCoin?.fee ?? Decimal.zero;
+  Decimal get fee {
+    final coin = onchainSelectedCoin;
+    if (coin == null) {
+      return Decimal.zero;
+    }
+    return Decimal.tryParse(coin.withdrawFee) ?? Decimal.zero;
+  }
 
-  /// 链上提币最小额
-  Decimal get minWithdrawAmount => currentCoin?.minWithdrawAmount ?? Decimal.zero;
+  Decimal get minWithdrawAmount {
+    final coin = onchainSelectedCoin;
+    if (coin == null) {
+      return Decimal.zero;
+    }
+    final min = Decimal.tryParse(coin.minWithdraw) ?? Decimal.zero;
+    if (min > Decimal.zero) {
+      return min;
+    }
+    final defaults = networkWithdrawDefaults[coin.networkLabel];
+    if (defaults != null) {
+      return Decimal.tryParse(defaults.min) ?? Decimal.zero;
+    }
+    return Decimal.zero;
+  }
 
-  /// 内部转账最小额(USDT 基础配置)
-  Decimal get transferMinAmount => _transferMinAmount ?? Decimal.zero;
+  Decimal get transferMinAmount {
+    final coin = transferSelectedCoin;
+    if (coin == null) {
+      return Decimal.zero;
+    }
+    return Decimal.tryParse(coin.minTransfer) ?? Decimal.zero;
+  }
 
-  /// 当前模式下的最小额(链上 or 内部转账)
-  Decimal get currentMinAmount => tabIndex == 0 ? minWithdrawAmount : transferMinAmount;
+  Decimal get currentMinAmount =>
+      tabIndex == 0 ? minWithdrawAmount : transferMinAmount;
 
-  /// 可提现余额(链上提币)
-  Decimal get withdrawableBalance => balance?.withdrawableBalance ?? Decimal.zero;
+  Decimal get withdrawableBalance =>
+      _balanceForCoin(onchainSelectedCoin, isTransfer: false);
 
-  /// 可转账余额(内部转账)
-  Decimal get transferableBalance => balance?.transferableBalance ?? Decimal.zero;
+  Decimal get transferableBalance =>
+      _balanceForCoin(transferSelectedCoin, isTransfer: true);
 
-  /// 当前可用余额(根据 Tab 决定)
   Decimal get availableBalance =>
       tabIndex == 0 ? withdrawableBalance : transferableBalance;
 
-  /// 是否已绑定 Google 验证
+  String get onchainCoinSymbol =>
+      symbolForWithdrawCoin(onchainSelectedCoin);
+
+  String get transferCoinSymbol =>
+      symbolForWithdrawCoin(transferSelectedCoin);
+
+  String get onChainSubmitUnit =>
+      onchainSelectedCoin?.unit.toLowerCase() ?? 'usdt';
+
+  String get transferSubmitUnit =>
+      transferSelectedCoin?.unit.toLowerCase() ?? 'usdt';
+
   bool get isGoogleBound => auth?.isGoogleVerified ?? false;
 
+  Decimal _balanceForCoin(WithdrawCoinOption? coin, {required bool isTransfer}) {
+    if (coin == null) {
+      return Decimal.zero;
+    }
+    if (isTransfer && coin.unit.toUpperCase() == 'USDT') {
+      return balance?.transferableBalance ?? balance?.balance ?? Decimal.zero;
+    }
+    if (!isTransfer && coin.isUsdtFamily) {
+      return balance?.withdrawableBalance ?? balance?.balance ?? Decimal.zero;
+    }
+    return Decimal.tryParse(coin.balance) ?? Decimal.zero;
+  }
+
   WithdrawState copyWith({
     int? tabIndex,
-    List<DepositWallet>? usdtWallets,
-    int? selectedNetworkIndex,
+    List<WithdrawNetworkItem>? networkList,
+    int? selectedNetworkId,
+    List<WithdrawCoinOption>? onchainSubCoins,
+    bool? onchainSubCoinsLoading,
+    int? selectedSubCoinIndex,
+    List<WithdrawCoinOption>? transferMainCoins,
+    int? selectedTransferCoinIndex,
+    List<SupportCoinInfo>? coinInfoList,
     WithdrawBalance? balance,
     AccountAuth? auth,
-    Decimal? transferMinAmount,
     int? codeCountdown,
     bool? isSubmitting,
     bool? isLoading,
@@ -112,11 +156,18 @@ class WithdrawState {
   }) =>
       WithdrawState(
         tabIndex: tabIndex ?? this.tabIndex,
-        usdtWallets: usdtWallets ?? this.usdtWallets,
-        selectedNetworkIndex: selectedNetworkIndex ?? this.selectedNetworkIndex,
+        networkList: networkList ?? this.networkList,
+        selectedNetworkId: selectedNetworkId ?? this.selectedNetworkId,
+        onchainSubCoins: onchainSubCoins ?? this.onchainSubCoins,
+        onchainSubCoinsLoading:
+            onchainSubCoinsLoading ?? this.onchainSubCoinsLoading,
+        selectedSubCoinIndex: selectedSubCoinIndex ?? this.selectedSubCoinIndex,
+        transferMainCoins: transferMainCoins ?? this.transferMainCoins,
+        selectedTransferCoinIndex:
+            selectedTransferCoinIndex ?? this.selectedTransferCoinIndex,
+        coinInfoList: coinInfoList ?? this.coinInfoList,
         balance: balance ?? this.balance,
         auth: auth ?? this.auth,
-        transferMinAmount: transferMinAmount ?? _transferMinAmount,
         codeCountdown: codeCountdown ?? this.codeCountdown,
         isSubmitting: isSubmitting ?? this.isSubmitting,
         isLoading: isLoading ?? this.isLoading,
@@ -139,46 +190,97 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
   }
 
   Future<void> _load() async {
-    state = state.copyWith(isLoading: true, errorMessage: null, selectedNetworkIndex: -1);
+    state = state.copyWith(isLoading: true, errorMessage: null);
     try {
       final dio = ref.read(dioClientProvider);
-      final assetService = AssetService(dio);
       final withdrawService = WithdrawService(dio);
 
-      // 并发请求:钱包地址 + 可用余额 + 认证信息 + 内部转账最小额
       final results = await Future.wait([
-        assetService.getWalletAddresses(),
         withdrawService.getBalance('usdt'),
         withdrawService.getSecuritySetting(),
-        withdrawService.getTransferMinAmount(),
+        withdrawService.getSupportCoinInfo(),
+        withdrawService.getWithdrawNetworks(),
       ]);
 
-      final wallets = results[0] as List<DepositWallet>;
-      final balance = results[1] as WithdrawBalance;
-      final auth = results[2] as AccountAuth;
-      final transferMinAmount = results[3] as Decimal;
-
-      // 过滤 USDT 钱包,TRC20 排最前
-      const networkOrder = {'TRC20': 0, 'ERC20': 1, 'BEP20': 2};
-      final usdtWallets = wallets
-          .where((w) => w.coin != null && w.coin!.code.contains('USDT'))
-          .toList()
-        ..sort((a, b) {
-          final oa = networkOrder[a.coin!.networkName] ?? 99;
-          final ob = networkOrder[b.coin!.networkName] ?? 99;
-          return oa.compareTo(ob);
-        });
+      final balance = results[0] as WithdrawBalance;
+      final auth = results[1] as AccountAuth;
+      final coinInfoList = results[2] as List<SupportCoinInfo>;
+      final networkList = results[3] as List<WithdrawNetworkItem>;
+
+      final transferMainCoins =
+          await withdrawService.getTransferParents(coinInfoList: coinInfoList);
 
       state = state.copyWith(
-        usdtWallets: usdtWallets,
         balance: balance,
         auth: auth,
-        transferMinAmount: transferMinAmount,
-        selectedNetworkIndex: -1,
+        coinInfoList: coinInfoList,
+        networkList: networkList,
+        transferMainCoins: transferMainCoins,
         isLoading: false,
       );
+      await _resetSelection();
     } catch (e) {
-      state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
+      state = state.copyWith(
+        isLoading: false,
+        errorMessage: extractErrorMessage(e),
+      );
+    }
+  }
+
+  Future<void> _resetSelection() async {
+    final nets = state.networkList;
+    var networkId = state.selectedNetworkId;
+    if (nets.isEmpty) {
+      networkId = 0;
+      state = state.copyWith(
+        selectedNetworkId: 0,
+        onchainSubCoins: [],
+        selectedSubCoinIndex: 0,
+      );
+    } else {
+      if (!nets.any((n) => n.id == networkId)) {
+        networkId = nets.first.id;
+      }
+      state = state.copyWith(
+        selectedNetworkId: networkId,
+        selectedSubCoinIndex: 0,
+      );
+      await _loadOnchainSubCoins(networkId);
+    }
+    var transferIdx = state.selectedTransferCoinIndex;
+    if (transferIdx >= state.transferMainCoins.length) {
+      transferIdx = 0;
+      state = state.copyWith(selectedTransferCoinIndex: transferIdx);
+    }
+  }
+
+  Future<void> _loadOnchainSubCoins(int networkId) async {
+    if (networkId <= 0) {
+      state = state.copyWith(onchainSubCoins: []);
+      return;
+    }
+    state = state.copyWith(onchainSubCoinsLoading: true);
+    try {
+      final dio = ref.read(dioClientProvider);
+      final coins = await WithdrawService(dio).getWithdrawConfigs(
+        networkId: networkId,
+        coinInfoList: state.coinInfoList,
+      );
+      var subIdx = state.selectedSubCoinIndex;
+      if (subIdx >= coins.length) {
+        subIdx = 0;
+      }
+      state = state.copyWith(
+        onchainSubCoins: coins,
+        selectedSubCoinIndex: subIdx,
+        onchainSubCoinsLoading: false,
+      );
+    } catch (e) {
+      state = state.copyWith(
+        onchainSubCoins: [],
+        onchainSubCoinsLoading: false,
+        errorMessage: extractErrorMessage(e),
+      );
     }
   }
 
@@ -188,26 +290,56 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
     state = state.copyWith(tabIndex: index);
   }
 
-  void selectNetwork(int index) {
-    state = state.copyWith(selectedNetworkIndex: index);
+  Future<void> selectNetwork(int networkId) async {
+    if (networkId <= 0 ||
+        !state.networkList.any((n) => n.id == networkId)) {
+      return;
+    }
+    state = state.copyWith(
+      selectedNetworkId: networkId,
+      selectedSubCoinIndex: 0,
+    );
+    await _loadOnchainSubCoins(networkId);
+  }
+
+  void selectSubCoin(int index) {
+    if (index >= 0 && index < state.onchainSubCoins.length) {
+      state = state.copyWith(selectedSubCoinIndex: index);
+    }
+  }
+
+  void selectTransferCoin(int index) {
+    if (index >= 0 && index < state.transferMainCoins.length) {
+      state = state.copyWith(selectedTransferCoinIndex: index);
+    }
   }
 
-  /// 发送邮箱验证码,返回错误信息(null 表示成功)
   Future<String?> sendEmailCode({
     required String address,
     required String amount,
   }) async {
-    if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork';
-    if (address.isEmpty) return 'errEnterAddress';
-    if (amount.isEmpty) return 'errEnterAmount';
+    if (state.tabIndex == 0 && state.onchainSelectedCoin == null) {
+      return 'errSelectNetwork';
+    }
+    if (state.tabIndex == 1 && state.transferSelectedCoin == null) {
+      return 'errSelectCoin';
+    }
+    if (address.isEmpty) {
+      return 'errEnterAddress';
+    }
+    if (amount.isEmpty) {
+      return 'errEnterAmount';
+    }
     try {
       final dio = ref.read(dioClientProvider);
+      final unit = state.tabIndex == 1
+          ? state.transferSubmitUnit
+          : state.onChainSubmitUnit;
       await WithdrawService(dio).sendWithdrawEmailCode(
-        unit: state.tabIndex == 1 ? 'USDT' : state.onChainUnit,
+        unit: unit,
         address: address,
         amount: amount,
       );
-      // 启动 60 秒倒计时
       state = state.copyWith(codeCountdown: 60);
       _countdownTimer?.cancel();
       _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
@@ -225,7 +357,6 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
     }
   }
 
-  /// 链上提币提交
   Future<bool> submitOnChainWithdraw({
     required String address,
     required String amount,
@@ -237,7 +368,7 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
     try {
       final dio = ref.read(dioClientProvider);
       await WithdrawService(dio).withdrawApply(
-        unit: state.onChainUnit,
+        unit: state.onChainSubmitUnit,
         amount: amount,
         address: address,
         fee: state.fee.toString(),
@@ -246,16 +377,17 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
         vcode2: vcode2,
       );
       state = state.copyWith(isSubmitting: false);
-      // 刷新余额
-      _load();
+      await _load();
       return true;
     } catch (e) {
-      state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e));
+      state = state.copyWith(
+        isSubmitting: false,
+        errorMessage: extractErrorMessage(e),
+      );
       return false;
     }
   }
 
-  /// 内部转账提交
   Future<bool> submitInternalTransfer({
     required String address,
     required String amount,
@@ -267,7 +399,7 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
     try {
       final dio = ref.read(dioClientProvider);
       await WithdrawService(dio).internalTransfer(
-        unit: 'USDT', // 内部转账用币种名
+        unit: state.transferSubmitUnit,
         amount: amount,
         address: address,
         vcode: vcode,
@@ -275,15 +407,17 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
         vcode2: vcode2,
       );
       state = state.copyWith(isSubmitting: false);
-      _load();
+      await _load();
       return true;
     } catch (e) {
-      state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e));
+      state = state.copyWith(
+        isSubmitting: false,
+        errorMessage: extractErrorMessage(e),
+      );
       return false;
     }
   }
 
-  /// 表单校验
   String? validate({
     required String address,
     required String amount,
@@ -291,20 +425,42 @@ class WithdrawNotifier extends Notifier<WithdrawState> {
     required String vcode,
     required String vcode2,
   }) {
-    if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork';
-    if (address.isEmpty) return 'errEnterAddress';
-    if (amount.isEmpty) return 'errEnterAmount';
-    if (jyPassword.isEmpty) return 'errEnterFundPassword';
-    if (vcode.isEmpty) return 'errEnterVerifyCode';
-    if (!state.isGoogleBound) return 'errBindGoogleFirst';
-    if (vcode2.isEmpty) return 'errEnterGoogleCode';
+    if (state.tabIndex == 0 && state.onchainSelectedCoin == null) {
+      return 'errSelectNetwork';
+    }
+    if (state.tabIndex == 1 && state.transferSelectedCoin == null) {
+      return 'errSelectCoin';
+    }
+    if (address.isEmpty) {
+      return 'errEnterAddress';
+    }
+    if (amount.isEmpty) {
+      return 'errEnterAmount';
+    }
+    if (jyPassword.isEmpty) {
+      return 'errEnterFundPassword';
+    }
+    if (vcode.isEmpty) {
+      return 'errEnterVerifyCode';
+    }
+    if (!state.isGoogleBound) {
+      return 'errBindGoogleFirst';
+    }
+    if (vcode2.isEmpty) {
+      return 'errEnterGoogleCode';
+    }
 
     final amountDecimal = Decimal.tryParse(amount);
-    if (amountDecimal == null) return 'errAmountFormat';
+    if (amountDecimal == null) {
+      return 'errAmountFormat';
+    }
     if (amountDecimal < state.currentMinAmount) {
+      final coinSymbol = state.tabIndex == 0
+          ? state.onchainCoinSymbol
+          : state.transferCoinSymbol;
       return state.tabIndex == 0
-          ? 'errMinWithdraw:${state.currentMinAmount}'
-          : 'errMinTransfer:${state.currentMinAmount}';
+          ? 'errMinWithdraw:${state.currentMinAmount}:$coinSymbol'
+          : 'errMinTransfer:${state.currentMinAmount}:$coinSymbol';
     }
     if (amountDecimal > state.availableBalance) {
       return 'errExceedBalance';
@@ -396,7 +552,9 @@ class WithdrawHistoryNotifier extends Notifier<WithdrawHistoryState> {
   Future<void> refresh() => _loadFirst();
 
   Future<void> loadMore() async {
-    if (state.isLoading || !state.hasMore) return;
+    if (state.isLoading || !state.hasMore) {
+      return;
+    }
     state = state.copyWith(isLoading: true);
     try {
       final service = WithdrawService(ref.read(dioClientProvider));
@@ -424,7 +582,6 @@ class WithdrawHistoryNotifier extends Notifier<WithdrawHistoryState> {
     }
   }
 
-  /// 合并两类记录,按 createTime 倒序
   List<WithdrawRecord> _merge(
     List<WithdrawRecord> withdrawRecords,
     List<WithdrawRecord> transferRecords,
@@ -434,7 +591,6 @@ class WithdrawHistoryNotifier extends Notifier<WithdrawHistoryState> {
     return all;
   }
 
-  /// 取消链上提现
   Future<bool> cancelWithdraw(String id) async {
     try {
       final dio = ref.read(dioClientProvider);

+ 1 - 1
pubspec.yaml

@@ -1,7 +1,7 @@
 name: cex_skeleton
 description: iBit — 加密货币交易 App
 publish_to: 'none'
-version: 4.1.5+40105
+version: 4.1.6+40106
 
 
 environment: