Procházet zdrojové kódy

锁仓和团队页面

jean před 1 měsícem
rodič
revize
d3912a3e92

+ 11 - 1
code/src/api/staking.ts

@@ -40,6 +40,11 @@ export function fetchMinOpenStakingConfig() {
   return post<StakingConfig>('/uc/staking/config/min-open')
 }
 
+/** Redis 现货价(ExchangeNew2:latestPrice,与质押闪兑同源) */
+export function fetchStakingSpotPrice(coinUnit: string) {
+  return get<{ coinUnit: string; price: string }>('/uc/staking/spot-price', { coinUnit })
+}
+
 /** staking_wallet 表余额(StakingWalletVo) */
 export interface StakingWalletBalance {
   coinUnit: string
@@ -56,11 +61,16 @@ export function fetchStakingWalletBalance(coinUnit: string) {
   return postForm<StakingWalletBalance>('/uc/staking/wallet', { coinUnit })
 }
 
-/** 发起质押 */
+/** 发起质押(直接质押 iBit 等) */
 export function submitStake(configId: number, amount: string) {
   return postForm<unknown>('/uc/staking/stake', { configId, amount })
 }
 
+/** IDO 预售:支付 USDT,后端按现价闪兑为质押币后质押 */
+export function submitStakeByUsdt(configId: number, usdtAmount: string) {
+  return postForm<unknown>('/uc/staking/stake', { configId, usdtAmount })
+}
+
 export interface StakingOrderItem {
   id: number
   coinUnit: string

+ 12 - 2
code/src/locales/en.ts

@@ -636,9 +636,16 @@ export default {
     transferSubmitFailed: 'Transfer failed',
     availWithdrawBalance: 'Withdrawable balance:',
     availTransferBalance: 'Transferable balance:',
+    coinLabel: 'Coin',
+    subCoin: 'Sub-coin',
+    mainCoinLabel: 'Main coin',
+    transferMainCoinHint: 'Internal transfer supports main coins only, not on-chain sub-tokens (e.g. TUSDT, EUSDT).',
     withdrawNetworkLabel: 'Withdrawal network',
     withdrawAmountUsdt: 'Withdraw amount (USDT)',
+    withdrawAmountCoin: 'Withdraw amount ({coin})',
     transferAmountUsdt: 'Transfer amount (USDT)',
+    transferAmountCoin: 'Transfer amount ({coin})',
+    addressForCoin: 'Enter {coin} address',
     expectedArrival: 'Est. received',
     minWithdrawRowLabel: 'Min withdrawal',
     internalTransferFeeLabel: 'Internal transfer fee',
@@ -674,14 +681,16 @@ export default {
     errFillTransferAmtBeforeCode: 'Please enter transfer amount first',
     errEnterWithdrawAddress: 'Enter withdrawal address',
     errEnterWithdrawAmount: 'Enter withdrawal amount',
-    errMinWithdrawWithAmt: 'Min withdrawal {min} USDT',
+    errMinWithdrawWithAmt: 'Min withdrawal {min} {coin}',
+    errMinWithdrawWithAmtUsdt: 'Min withdrawal {min} USDT',
     errExceedWithdrawBalance: 'Exceeds withdrawable balance',
     errEnterFundPassword: 'Enter fund password',
     errEnterEmailCode: 'Enter email code',
     errEnterGoogleCode: 'Enter Google code',
     errEnterRecipientUid: "Enter recipient's UID",
     errEnterTransferAmount: 'Enter transfer amount',
-    errMinTransferWithAmt: 'Min transfer {min} USDT',
+    errMinTransferWithAmt: 'Min transfer {min} {coin}',
+    errMinTransferWithAmtUsdt: 'Min transfer {min} USDT',
     errExceedTransferBalance: 'Exceeds transferable balance',
   },
   deposit: {
@@ -735,6 +744,7 @@ export default {
     broadcastSuccess: 'Transaction broadcast, hash submitted',
     tronBroadcastSuccess: 'Transaction broadcast, TxID submitted',
     noNetwork: 'No networks available',
+    noSubCoin: 'No sub-coins on this network',
     amountRequired: 'Please enter deposit amount',
     amountPositive: 'Please enter an amount greater than 0',
     submittedHash: 'Submitted Hash: ',

+ 4 - 0
code/src/locales/financePage.i18n.ts

@@ -27,7 +27,9 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     joinPresale: '参与预售',
     subscribeQty: '认购数量',
     availableIbit: '可提现可用 iBit',
+    availableUsdt: '可提现可用 USDT',
     availableIbitLine: '可用 iBit:{amount} {unit}',
+    priceUnavailable: '暂无行情价格,请稍后再试',
     ibitUnitPrice: '当前价格',
     ibitUnitPriceLine: '当前价格:{price} USDT',
     correspondingPrice: '对应价格',
@@ -188,7 +190,9 @@ export const financePageByLocale: Record<SupportedLocale, Record<string, string>
     joinPresale: 'Join Presale',
     subscribeQty: 'Subscription Amount',
     availableIbit: 'Withdrawable iBit',
+    availableUsdt: 'Withdrawable USDT',
     availableIbitLine: 'Available iBit: {amount} {unit}',
+    priceUnavailable: 'Price unavailable. Please try again later',
     ibitUnitPrice: 'Current Price',
     ibitUnitPriceLine: 'Current price: {price} USDT',
     correspondingPrice: 'Price in USDT',

+ 1 - 0
code/src/locales/hi.ts

@@ -739,6 +739,7 @@ export default {
     broadcastSuccess: 'ट्रांजेक्शन ब्रॉडकास्ट, हैश सबमिट किया गया',
     tronBroadcastSuccess: 'ट्रांजेक्शन ब्रॉडकास्ट, TxID सबमिट किया गया',
     noNetwork: 'कोई नेटवर्क उपलब्ध नहीं',
+    noSubCoin: 'इस नेटवर्क पर कोई सब-कॉइन उपलब्ध नहीं',
     amountRequired: 'कृपया जमा राशि दर्ज करें',
     amountPositive: '0 से अधिक राशि दर्ज करें',
     submittedHash: 'सबमिट किया गया Hash: ',

+ 1 - 0
code/src/locales/id.ts

@@ -739,6 +739,7 @@ export default {
     broadcastSuccess: 'Transaksi disiarkan, hash terkirim',
     tronBroadcastSuccess: 'Transaksi disiarkan, TxID terkirim',
     noNetwork: 'Tidak ada jaringan tersedia',
+    noSubCoin: 'Tidak ada sub-koin di jaringan ini',
     amountRequired: 'Masukkan jumlah deposit',
     amountPositive: 'Masukkan jumlah lebih dari 0',
     submittedHash: 'Hash yang Dikirim: ',

+ 1 - 0
code/src/locales/ja.ts

@@ -741,6 +741,7 @@ export default {
     broadcastSuccess: 'トランザクションがブロードキャストされ、ハッシュが提出されました',
     tronBroadcastSuccess: 'トランザクションがブロードキャストされ、TxIDが提出されました',
     noNetwork: '利用可能なネットワークなし',
+    noSubCoin: 'このネットワークに利用可能なサブコインがありません',
     amountRequired: '入金金額を入力してください',
     amountPositive: '0より大きい金額を入力してください',
     submittedHash: '提出済みHash: ',

+ 1 - 0
code/src/locales/ko.ts

@@ -738,6 +738,7 @@ export default {
     broadcastSuccess: '트랜잭션 브로드캐스트, 해시 제출됨',
     tronBroadcastSuccess: '트랜잭션 브로드캐스트, TxID 제출됨',
     noNetwork: '사용 가능한 네트워크 없음',
+    noSubCoin: '이 네트워크에 사용 가능한 서브 코인이 없습니다',
     amountRequired: '입금 금액을 입력하세요',
     amountPositive: '0보다 큰 금액을 입력하세요',
     submittedHash: '제출된 해시: ',

+ 12 - 2
code/src/locales/zh-CN.ts

@@ -638,9 +638,16 @@ export default {
     transferSubmitFailed: '转账失败',
     availWithdrawBalance: '可提现余额:',
     availTransferBalance: '可转账余额:',
+    coinLabel: '币种',
+    subCoin: '子币种',
+    mainCoinLabel: '主币种',
+    transferMainCoinHint: '内部转账仅支持主币,不支持链上子币种(如 TUSDT、EUSDT)。',
     withdrawNetworkLabel: '提币网络',
     withdrawAmountUsdt: '提币数量 (USDT)',
+    withdrawAmountCoin: '提币数量 ({coin})',
     transferAmountUsdt: '转账数量 (USDT)',
+    transferAmountCoin: '转账数量 ({coin})',
+    addressForCoin: '输入 {coin} 地址',
     expectedArrival: '预计到账',
     minWithdrawRowLabel: '最小提币额',
     internalTransferFeeLabel: '内部转账手续费',
@@ -676,14 +683,16 @@ export default {
     errFillTransferAmtBeforeCode: '请先填写转账数量',
     errEnterWithdrawAddress: '请输入提币地址',
     errEnterWithdrawAmount: '请输入提币数量',
-    errMinWithdrawWithAmt: '最小提币额 {min} USDT',
+    errMinWithdrawWithAmt: '最小提币额 {min} {coin}',
+    errMinWithdrawWithAmtUsdt: '最小提币额 {min} USDT',
     errExceedWithdrawBalance: '超出可提现余额',
     errEnterFundPassword: '请输入资金密码',
     errEnterEmailCode: '请输入邮箱验证码',
     errEnterGoogleCode: '请输入谷歌验证码',
     errEnterRecipientUid: '请输入对方 UID',
     errEnterTransferAmount: '请输入转账数量',
-    errMinTransferWithAmt: '最小转账额 {min} USDT',
+    errMinTransferWithAmt: '最小转账额 {min} {coin}',
+    errMinTransferWithAmtUsdt: '最小转账额 {min} USDT',
     errExceedTransferBalance: '超出可转账余额',
   },
   deposit: {
@@ -737,6 +746,7 @@ export default {
     broadcastSuccess: '交易已广播,哈希已提交',
     tronBroadcastSuccess: '交易已广播,TxID 已提交',
     noNetwork: '暂无可用网络',
+    noSubCoin: '该主网下暂无可用子币种',
     amountRequired: '请输入充值数量',
     amountPositive: '请输入大于 0 的数量',
     submittedHash: '已提交 Hash:',

+ 1 - 0
code/src/locales/zh-TW.ts

@@ -737,6 +737,7 @@ export default {
     broadcastSuccess: '交易已廣播,哈希已提交',
     tronBroadcastSuccess: '交易已廣播,TxID 已提交',
     noNetwork: '暫無可用網絡',
+    noSubCoin: '該主網下暫無可用子幣種',
     amountRequired: '請輸入充值數量',
     amountPositive: '請輸入大於 0 的數量',
     submittedHash: '已提交 Hash:',

+ 133 - 60
code/src/stores/deposit.ts

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
 import { ref, computed } from 'vue'
 import { get, postForm } from '@/utils/request'
 
-// ── 链上充值(新接口,见 docs/接口文档-2026-05-13.md)────────────────
+// ── 链上充值(coin_network_config 按库查询)────────────────
 
 export interface RechargeParentCoin {
   name: string
@@ -13,34 +13,38 @@ export interface RechargeParentCoin {
   sort: number
 }
 
-export interface RechargeChildGroup {
-  coin: {
-    name: string
-    nameCn: string
-    unit: string
-    parentCoinName: string
-    canRecharge: number
-    sort: number
-  }
-  networkConfigs: {
-    id: number
-    coinName: string
-    network: { id: number; name: string; protocol: string; status: number }
-    contractAddress: string
-    rechargeAddress: string
-    sort: number
-  }[]
+export interface RechargeNetworkItem {
+  id: number
+  name: string
+  protocol: string
+  status?: number
+  sort: number
+}
+
+/** 某主网下 coin_network_config 一行(创建订单用) */
+export interface RechargeConfigOption {
+  networkConfigId: number
+  coinName: string
+  coinNameCn: string
+  networkId: number
+  networkName: string
+  protocol: string
+  contractAddress: string
+  rechargeAddress: string
+  sort: number
 }
 
-/** 扁平化后的可选主网一行(选网 + 创建订单用) */
+/** 与 RechargeConfigOption 对齐,供钱包/EVM 工具使用 */
 export interface RechargeFlatNetworkOption {
   coinName: string
   coinNameCn: string
   networkConfigId: number
+  networkId: number
   networkName: string
   protocol: string
   contractAddress: string
   rechargeAddress: string
+  sort: number
 }
 
 export interface RechargeOrderDetail {
@@ -60,7 +64,6 @@ export interface RechargeOrderDetail {
   updateTime?: string | null
 }
 
-/** 列表单项(与详情字段对齐) */
 export interface RechargeOrderListItem {
   id: string
   orderNo: string
@@ -75,26 +78,32 @@ export interface RechargeOrderListItem {
   updateTime: string
 }
 
-function flattenNetworkOptions(groups: RechargeChildGroup[]): RechargeFlatNetworkOption[] {
-  const out: RechargeFlatNetworkOption[] = []
-  for (const g of groups) {
-    if (!g?.coin || !Array.isArray(g.networkConfigs)) {
-      continue
-    }
-    const nets = [...g.networkConfigs].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
-    for (const n of nets) {
-      out.push({
-        coinName: String(g.coin.name ?? ''),
-        coinNameCn: String(g.coin.nameCn ?? ''),
-        networkConfigId: Number(n.id),
-        networkName: String(n.network?.name ?? ''),
-        protocol: String(n.network?.protocol ?? ''),
-        contractAddress: String(n.contractAddress ?? ''),
-        rechargeAddress: String(n.rechargeAddress ?? ''),
-      })
-    }
+function mapConfigOption(row: Record<string, unknown>): RechargeConfigOption {
+  return {
+    networkConfigId: Number(row.networkConfigId ?? row.id ?? 0),
+    coinName: String(row.coinName ?? ''),
+    coinNameCn: String(row.coinNameCn ?? ''),
+    networkId: Number(row.networkId ?? row.network?.id ?? 0),
+    networkName: String(row.networkName ?? row.network?.name ?? ''),
+    protocol: String(row.protocol ?? row.network?.protocol ?? ''),
+    contractAddress: String(row.contractAddress ?? ''),
+    rechargeAddress: String(row.rechargeAddress ?? ''),
+    sort: Number(row.sort ?? 0),
+  }
+}
+
+function mapNetworkItem(row: Record<string, unknown>): RechargeNetworkItem {
+  return {
+    id: Number(row.id ?? 0),
+    name: String(row.name ?? ''),
+    protocol: String(row.protocol ?? ''),
+    status: row.status != null ? Number(row.status) : undefined,
+    sort: Number(row.sort ?? 0),
   }
-  return out
+}
+
+function toFlatOption(cfg: RechargeConfigOption): RechargeFlatNetworkOption {
+  return { ...cfg }
 }
 
 function mapListItem(row: Record<string, unknown>): RechargeOrderListItem {
@@ -115,12 +124,15 @@ function mapListItem(row: Record<string, unknown>): RechargeOrderListItem {
 
 export const useDepositStore = defineStore('deposit', () => {
   const parentCoins = ref<RechargeParentCoin[]>([])
-  const flatNetworks = ref<RechargeFlatNetworkOption[]>([])
+  const networkOptions = ref<RechargeNetworkItem[]>([])
+  const subCoinOptions = ref<RechargeConfigOption[]>([])
   const selectedParentIndex = ref(0)
-  const selectedFlatIndex = ref(0)
+  const selectedNetworkId = ref(0)
+  const selectedSubCoinIndex = ref(0)
 
   const loadingParents = ref(false)
   const childrenLoading = ref(false)
+  const subCoinsLoading = ref(false)
   const error = ref<string | null>(null)
 
   const currentOrder = ref<RechargeOrderDetail | null>(null)
@@ -128,7 +140,6 @@ export const useDepositStore = defineStore('deposit', () => {
   const hashSubmitting = ref(false)
 
   const records = ref<RechargeOrderListItem[]>([])
-  /** 最近一次已成功请求的页码(从 1 开始);0 表示尚未加载 */
   const lastRecordPageFetched = ref(0)
   const recordPageSize = 15
   const recordTotalPages = ref(0)
@@ -143,7 +154,47 @@ export const useDepositStore = defineStore('deposit', () => {
   })
 
   const selectedParent = computed(() => parentCoins.value[selectedParentIndex.value] ?? null)
-  const selectedNetwork = computed(() => flatNetworks.value[selectedFlatIndex.value] ?? null)
+
+  const selectedConfig = computed(() => subCoinOptions.value[selectedSubCoinIndex.value] ?? null)
+
+  const selectedNetwork = computed<RechargeFlatNetworkOption | null>(() => {
+    const cfg = selectedConfig.value
+    return cfg ? toFlatOption(cfg) : null
+  })
+
+  async function loadRechargeNetworks(parentCoinName: string) {
+    const raw = await get<Record<string, unknown>[]>('/uc/coin-network/recharge-networks', {
+      coinName: parentCoinName,
+    })
+    const list = Array.isArray(raw) ? raw : []
+    networkOptions.value = list.map(r => mapNetworkItem(r)).filter(n => n.id > 0)
+    if (networkOptions.value.length > 0) {
+      const stillValid = networkOptions.value.some(n => n.id === selectedNetworkId.value)
+      if (!stillValid) {
+        selectedNetworkId.value = networkOptions.value[0].id
+      }
+    } else {
+      selectedNetworkId.value = 0
+      subCoinOptions.value = []
+    }
+  }
+
+  async function loadRechargeConfigs(parentCoinName: string, networkId: number) {
+    subCoinsLoading.value = true
+    try {
+      const raw = await get<Record<string, unknown>[]>('/uc/coin-network/recharge-configs', {
+        coinName: parentCoinName,
+        networkId,
+      })
+      const list = Array.isArray(raw) ? raw : []
+      subCoinOptions.value = list.map(r => mapConfigOption(r)).filter(c => c.networkConfigId > 0)
+      if (selectedSubCoinIndex.value >= subCoinOptions.value.length) {
+        selectedSubCoinIndex.value = 0
+      }
+    } finally {
+      subCoinsLoading.value = false
+    }
+  }
 
   async function loadParentCoins() {
     loadingParents.value = true
@@ -158,12 +209,14 @@ export const useDepositStore = defineStore('deposit', () => {
       if (parentCoins.value.length > 0) {
         await loadChildrenForParent(parentCoins.value[selectedParentIndex.value].name)
       } else {
-        flatNetworks.value = []
+        networkOptions.value = []
+        subCoinOptions.value = []
       }
     } catch (e) {
       error.value = e instanceof Error ? e.message : '加载币种失败'
       parentCoins.value = []
-      flatNetworks.value = []
+      networkOptions.value = []
+      subCoinOptions.value = []
       throw e
     } finally {
       loadingParents.value = false
@@ -174,15 +227,16 @@ export const useDepositStore = defineStore('deposit', () => {
     childrenLoading.value = true
     error.value = null
     try {
-      const raw = await get<RechargeChildGroup[]>('/uc/coin-network/children-with-network', {
-        coinName,
-      })
-      const groups = Array.isArray(raw) ? raw : []
-      flatNetworks.value = flattenNetworkOptions(groups)
-      selectedFlatIndex.value = 0
+      await loadRechargeNetworks(coinName)
+      if (selectedNetworkId.value > 0) {
+        await loadRechargeConfigs(coinName, selectedNetworkId.value)
+      } else {
+        subCoinOptions.value = []
+      }
     } catch (e) {
       error.value = e instanceof Error ? e.message : '加载主网失败'
-      flatNetworks.value = []
+      networkOptions.value = []
+      subCoinOptions.value = []
       throw e
     } finally {
       childrenLoading.value = false
@@ -194,22 +248,36 @@ export const useDepositStore = defineStore('deposit', () => {
       return
     }
     selectedParentIndex.value = index
+    selectedSubCoinIndex.value = 0
     const p = parentCoins.value[index]
     if (p) {
       await loadChildrenForParent(p.name)
     }
   }
 
-  function selectFlatNetwork(index: number) {
-    if (index >= 0 && index < flatNetworks.value.length) {
-      selectedFlatIndex.value = index
+  async function selectNetwork(networkId: number) {
+    if (!networkId || !networkOptions.value.some(n => n.id === networkId)) {
+      return
+    }
+    selectedNetworkId.value = networkId
+    selectedSubCoinIndex.value = 0
+    const parent = selectedParent.value
+    if (parent) {
+      await loadRechargeConfigs(parent.name, networkId)
+    }
+  }
+
+  function selectSubCoin(index: number) {
+    if (index < 0 || index >= subCoinOptions.value.length) {
+      return
     }
+    selectedSubCoinIndex.value = index
   }
 
   async function createRechargeOrder(amount: string) {
-    const opt = selectedNetwork.value
+    const opt = selectedConfig.value
     if (!opt) {
-      throw new Error('请先选择充值网络')
+      throw new Error('请先选择充值网络与子币种')
     }
     orderSubmitting.value = true
     error.value = null
@@ -300,11 +368,14 @@ export const useDepositStore = defineStore('deposit', () => {
 
   return {
     parentCoins,
-    flatNetworks,
+    networkOptions,
+    subCoinOptions,
     selectedParentIndex,
-    selectedFlatIndex,
+    selectedNetworkId,
+    selectedSubCoinIndex,
     loadingParents,
     childrenLoading,
+    subCoinsLoading,
     error,
     currentOrder,
     orderSubmitting,
@@ -316,10 +387,12 @@ export const useDepositStore = defineStore('deposit', () => {
     lastRecordPageFetched,
     selectedParent,
     selectedNetwork,
+    selectedConfig,
     loadParentCoins,
     loadChildrenForParent,
     selectParent,
-    selectFlatNetwork,
+    selectNetwork,
+    selectSubCoin,
     createRechargeOrder,
     submitTxHash,
     clearCurrentOrder,

+ 312 - 19
code/src/stores/withdraw.ts

@@ -36,6 +36,9 @@ export interface WalletAddress {
 export interface SupportCoinInfo {
   unit: string
   aliasName?: string
+  nameCn?: string
+  name?: string
+  balance?: string | number
   minWithdrawAmount: string
   maxWithdrawAmount: string
   minTxFee: string
@@ -46,6 +49,39 @@ export interface SupportCoinInfo {
   withdrawFeeValue?: string
 }
 
+/** coin 表顶级主币(parent_coin_name 为空且 can_recharge=1,内部转账用) */
+export interface TransferParentCoin {
+  name: string
+  nameCn: string
+  unit: string
+  sort: number
+  minWithdrawAmount?: string | number
+  withdrawFeeValue?: string | number
+}
+
+/** coin_network 表一行(提币网络列表) */
+export interface WithdrawNetworkItem {
+  id: number
+  name: string
+  protocol: string
+  sort: number
+}
+
+/** 可提/可转币种 */
+export interface WithdrawCoinOption {
+  unit: string
+  displayName: string
+  networkLabel: string
+  networkId?: number
+  balance: string
+  minWithdraw: string
+  minTransfer: string
+  withdrawFee: string
+  isUsdtFamily: boolean
+  /** 主币(内部转账仅主币) */
+  isMainCoin: boolean
+}
+
 export interface WithdrawRecordCoin {
   coinName: string
   aliasName: string
@@ -206,6 +242,88 @@ function walletUnitToNetworkLabel(unit: string | undefined): string {
 /** 网络 Tab 排序:与 App 一致 */
 const NETWORK_TAB_ORDER: Record<string, number> = { TRC20: 0, ERC20: 1, BEP20: 2 }
 
+function isUsdtFamilyUnit(unit: string): boolean {
+  const u = unit.toUpperCase()
+  return u === 'USDT' || u === 'EUSDT' || u === 'TUSDT' || u.includes('BEP20')
+}
+
+function mapTransferParent(
+  row: Record<string, unknown>,
+  supportMap: Map<string, SupportCoinInfo>,
+): WithdrawCoinOption {
+  const unit = String(row.unit ?? '').trim().toLowerCase()
+  const name = String(row.name ?? '')
+  const support = supportMap.get(unit) ?? supportMap.get(name.toLowerCase())
+  const minFromCoin = firstNonEmptyStr(row.minWithdrawAmount)
+  const minFromSupport = firstNonEmptyStr(support?.minAmount, support?.minWithdrawAmount, support?.minTransferAmount)
+  return {
+    unit,
+    displayName: firstNonEmptyStr(row.nameCn, support?.nameCn, support?.aliasName, name) || unit.toUpperCase(),
+    networkLabel: '',
+    balance: firstNonEmptyStr(support?.balance, '0') || '0',
+    minWithdraw: minFromSupport || minFromCoin || '0',
+    minTransfer: firstNonEmptyStr(support?.minTransferAmount, support?.minAmount, minFromSupport, minFromCoin) || '0',
+    withdrawFee: firstNonEmptyStr(support?.withdrawFeeValue, support?.fee, row.withdrawFeeValue) || '0',
+    isUsdtFamily: isUsdtFamilyUnit(unit),
+    isMainCoin: true,
+  }
+}
+
+function mapNetworkItem(row: Record<string, unknown>): WithdrawNetworkItem {
+  return {
+    id: Number(row.id ?? 0),
+    name: String(row.name ?? ''),
+    protocol: String(row.protocol ?? ''),
+    sort: Number(row.sort ?? 0),
+  }
+}
+
+function supportInfoByUnit(list: SupportCoinInfo[]): Map<string, SupportCoinInfo> {
+  const map = new Map<string, SupportCoinInfo>()
+  for (const c of list) {
+    const u = String(c.unit ?? '').toLowerCase()
+    if (u) {
+      map.set(u, c)
+    }
+  }
+  return map
+}
+
+function mapWithdrawConfigFromDb(
+  row: Record<string, unknown>,
+  supportMap: Map<string, SupportCoinInfo>,
+): WithdrawCoinOption {
+  const unit = String(row.unit ?? '').trim().toLowerCase()
+  const coinName = String(row.coinName ?? '')
+  const support = supportMap.get(unit) ?? supportMap.get(coinName.toLowerCase())
+  const networkName = String(row.networkName ?? '')
+  const minFromDb = firstNonEmptyStr(row.minWithdrawAmount, row.minAmount)
+  const minFromSupport = firstNonEmptyStr(support?.minAmount, support?.minWithdrawAmount)
+  return {
+    unit,
+    displayName: firstNonEmptyStr(row.coinNameCn, support?.nameCn, support?.aliasName, coinName) || unit.toUpperCase(),
+    networkLabel: networkName,
+    networkId: Number(row.networkId ?? 0) || undefined,
+    balance: firstNonEmptyStr(support?.balance, '0') || '0',
+    minWithdraw: minFromSupport || minFromDb || '0',
+    minTransfer: firstNonEmptyStr(support?.minTransferAmount, support?.minAmount, minFromSupport) || '0',
+    withdrawFee: firstNonEmptyStr(support?.withdrawFeeValue, support?.fee, row.withdrawFeeValue) || '0',
+    isUsdtFamily: isUsdtFamilyUnit(unit),
+    isMainCoin: false,
+  }
+}
+
+function sortWithdrawCoins(list: WithdrawCoinOption[]): WithdrawCoinOption[] {
+  return [...list].sort((a, b) => {
+    const oa = NETWORK_TAB_ORDER[a.networkLabel] ?? 99
+    const ob = NETWORK_TAB_ORDER[b.networkLabel] ?? 99
+    if (oa !== ob) {
+      return oa - ob
+    }
+    return a.displayName.localeCompare(b.displayName)
+  })
+}
+
 /**
  * 各链默认手续费 / 最小提币(仅当钱包与支持币种接口均未返回有效值时使用,与 App 展示对齐)
  */
@@ -220,6 +338,15 @@ export const useWithdrawStore = defineStore('withdraw', () => {
   const security = ref<AccountAuth | null>(null)
   const coinInfoList = ref<SupportCoinInfo[]>([])
   const walletAddresses = ref<WalletAddress[]>([])
+  /** 链上:coin_network 表 */
+  const networkList = ref<WithdrawNetworkItem[]>([])
+  const selectedNetworkId = ref(0)
+  const onchainSubCoins = ref<WithdrawCoinOption[]>([])
+  const onchainSubCoinsLoading = ref(false)
+  const selectedSubCoinIndex = ref(0)
+  /** 内部转账:仅主币 */
+  const transferMainCoins = ref<WithdrawCoinOption[]>([])
+  const selectedTransferCoinIndex = ref(0)
   const initLoading = ref(false)
   const initError = ref<string | null>(null)
 
@@ -257,16 +384,25 @@ export const useWithdrawStore = defineStore('withdraw', () => {
     initLoading.value = true
     initError.value = null
     try {
-      const [balRes, secRes, coinRes, addrRes] = await Promise.all([
+      const [balRes, secRes, coinRes, netRes, parentRes] = await Promise.all([
         post<WithdrawBalance>('/uc/asset/wallet/usdt', {}),
         postForm<AccountAuth>('/uc/approve/security/setting', {}),
         post<SupportCoinInfo[]>('/uc/withdraw/support/coin/info', {}),
-        post<WalletAddress[]>('/uc/asset/wallet/address', {}),
+        get<Record<string, unknown>[]>('/uc/coin-network/withdraw-networks'),
+        get<Record<string, unknown>[]>('/uc/coin-network/transfer-parents'),
       ])
       walletBalance.value = balRes
       security.value = secRes
       coinInfoList.value = Array.isArray(coinRes) ? coinRes : []
-      walletAddresses.value = Array.isArray(addrRes) ? addrRes : []
+      const nets = Array.isArray(netRes) ? netRes : []
+      networkList.value = nets.map(r => mapNetworkItem(r)).filter(n => n.id > 0)
+      const parents = Array.isArray(parentRes) ? parentRes : []
+      const supportMap = supportInfoByUnit(coinInfoList.value)
+      transferMainCoins.value = parents
+        .map(r => mapTransferParent(r, supportMap))
+        .filter(c => c.unit)
+        .sort((a, b) => a.displayName.localeCompare(b.displayName))
+      await resetSelection()
     } catch (e) {
       initError.value = e instanceof Error ? e.message : 'i18n:withdraw.initFailed'
       throw e
@@ -335,14 +471,167 @@ export const useWithdrawStore = defineStore('withdraw', () => {
     return NETWORK_WITHDRAW_DEFAULTS[network]?.min ?? '0'
   }
 
-  /** Min transfer amount from support/coin/info (look up USDT entry) */
-  const minTransferAmount = computed(() => {
-    const usdt = coinInfoList.value.find(c => {
-      const unit = c.unit?.toUpperCase() ?? ''
-      return unit === 'USDT'
-    })
-    return usdt?.minAmount ?? usdt?.minTransferAmount ?? '0'
-  })
+  const onchainSelectedCoin = computed(
+    () => onchainSubCoins.value[selectedSubCoinIndex.value] ?? null,
+  )
+
+  const transferSelectedCoin = computed(
+    () => transferMainCoins.value[selectedTransferCoinIndex.value] ?? null,
+  )
+
+  const minTransferAmount = computed(() => transferSelectedCoin.value?.minTransfer ?? '0')
+
+  async function loadOnchainSubCoins(networkId: number) {
+    if (!networkId) {
+      onchainSubCoins.value = []
+      return
+    }
+    onchainSubCoinsLoading.value = true
+    try {
+      const raw = await get<Record<string, unknown>[]>('/uc/coin-network/withdraw-configs', {
+        networkId,
+      })
+      const list = Array.isArray(raw) ? raw : []
+      const supportMap = supportInfoByUnit(coinInfoList.value)
+      onchainSubCoins.value = list
+        .map(r => mapWithdrawConfigFromDb(r, supportMap))
+        .filter(c => c.unit)
+      if (selectedSubCoinIndex.value >= onchainSubCoins.value.length) {
+        selectedSubCoinIndex.value = 0
+      }
+    } finally {
+      onchainSubCoinsLoading.value = false
+    }
+  }
+
+  async function resetSelection() {
+    const nets = networkList.value
+    if (nets.length > 0) {
+      const stillValid = nets.some(n => n.id === selectedNetworkId.value)
+      if (!stillValid) {
+        selectedNetworkId.value = nets[0].id
+      }
+      await loadOnchainSubCoins(selectedNetworkId.value)
+    } else {
+      selectedNetworkId.value = 0
+      onchainSubCoins.value = []
+    }
+    selectedSubCoinIndex.value = 0
+    if (selectedTransferCoinIndex.value >= transferMainCoins.value.length) {
+      selectedTransferCoinIndex.value = 0
+    }
+  }
+
+  async function selectNetwork(networkId: number) {
+    if (!networkId || !networkList.value.some(n => n.id === networkId)) {
+      return
+    }
+    selectedNetworkId.value = networkId
+    selectedSubCoinIndex.value = 0
+    await loadOnchainSubCoins(networkId)
+  }
+
+  function selectSubCoin(index: number) {
+    if (index < 0 || index >= onchainSubCoins.value.length) {
+      return
+    }
+    selectedSubCoinIndex.value = index
+  }
+
+  function selectTransferCoin(index: number) {
+    if (index < 0 || index >= transferMainCoins.value.length) {
+      return
+    }
+    selectedTransferCoinIndex.value = index
+  }
+
+  function balanceForCoin(coin: WithdrawCoinOption | null, mode: 'withdraw' | 'transfer'): string {
+    if (!coin) {
+      return '0'
+    }
+    if (mode === 'transfer' && coin.unit.toUpperCase() === 'USDT') {
+      const bal = walletBalance.value
+      return bal?.transferableBalance ?? bal?.balance ?? '0'
+    }
+    if (mode === 'withdraw' && coin.isUsdtFamily) {
+      const bal = walletBalance.value
+      return bal?.withdrawableBalance ?? bal?.balance ?? '0'
+    }
+    return coin.balance
+  }
+
+  /** 链上可提余额 */
+  const onchainAvailableBalance = computed(() =>
+    balanceForCoin(onchainSelectedCoin.value, 'withdraw'),
+  )
+
+  /** 内部转账可转余额 */
+  const transferAvailableBalance = computed(() =>
+    balanceForCoin(transferSelectedCoin.value, 'transfer'),
+  )
+
+  function getOnchainFee(): string {
+    return onchainSelectedCoin.value?.withdrawFee ?? '0'
+  }
+
+  function getOnchainMinWithdraw(): string {
+    const coin = onchainSelectedCoin.value
+    if (!coin) {
+      return '0'
+    }
+    const min = coin.minWithdraw
+    if (min && Number(min) > 0) {
+      return min
+    }
+    return NETWORK_WITHDRAW_DEFAULTS[coin.networkLabel]?.min ?? '0'
+  }
+
+  function getOnchainSubmitUnit(): string {
+    return onchainSelectedCoin.value?.unit ?? 'usdt'
+  }
+
+  function getTransferSubmitUnit(): string {
+    return transferSelectedCoin.value?.unit ?? 'usdt'
+  }
+
+  function symbolForCoin(coin: WithdrawCoinOption | null): string {
+    if (!coin) {
+      return 'USDT'
+    }
+    if (coin.isMainCoin && coin.unit.toUpperCase() === 'USDT') {
+      return 'USDT'
+    }
+    if (coin.isUsdtFamily && coin.networkLabel && coin.unit.toUpperCase() !== 'USDT') {
+      return `${coin.displayName || 'USDT'} (${coin.unit.toUpperCase()})`
+    }
+    return coin.displayName || coin.unit.toUpperCase()
+  }
+
+  function getOnchainCoinSymbol(): string {
+    return symbolForCoin(onchainSelectedCoin.value)
+  }
+
+  function getTransferCoinSymbol(): string {
+    return symbolForCoin(transferSelectedCoin.value)
+  }
+
+  /** @deprecated 链上请用 getOnchainSubmitUnit / 转账请用 getTransferSubmitUnit */
+  function getSubmitUnit(): string {
+    return getOnchainSubmitUnit()
+  }
+
+  /** @deprecated */
+  function getCoinSymbol(): string {
+    return getOnchainCoinSymbol()
+  }
+
+  function getSelectedFee(): string {
+    return getOnchainFee()
+  }
+
+  function getSelectedMinWithdraw(): string {
+    return getOnchainMinWithdraw()
+  }
 
   async function sendEmailCode(unit: string, address: string, amount: string) {
     sendingCode.value = true
@@ -466,12 +755,8 @@ export const useWithdrawStore = defineStore('withdraw', () => {
     return normalizeWithdrawRecord(raw, isTransfer)
   }
 
-  const availableBalance = computed(() =>
-    walletBalance.value?.withdrawableBalance ?? walletBalance.value?.balance ?? '0',
-  )
-  const transferableBalance = computed(() =>
-    walletBalance.value?.transferableBalance ?? walletBalance.value?.balance ?? '0',
-  )
+  const availableBalance = onchainAvailableBalance
+  const transferableBalance = transferAvailableBalance
   const googleBound = computed(() => {
     const s = security.value as (AccountAuth & Record<string, unknown>) | null
     if (!s) {
@@ -483,12 +768,20 @@ export const useWithdrawStore = defineStore('withdraw', () => {
   const fundPasswordSet = computed(() => isSecurityFlagOn(security.value?.fundsVerified))
 
   return {
-    walletBalance, security, coinInfoList, walletAddresses, initLoading, initError,
+    walletBalance, security, coinInfoList, walletAddresses,
+    networkList, selectedNetworkId, onchainSubCoins, onchainSubCoinsLoading,
+    selectedSubCoinIndex, transferMainCoins, selectedTransferCoinIndex,
+    initLoading, initError,
     sendingCode, codeCountdown, submitting, formError,
     records, recordLoading, recordHasMore,
     availableBalance, transferableBalance, googleBound, fundPasswordSet,
-    usdtWallets, availableNetworks, minTransferAmount,
+    onchainSelectedCoin, transferSelectedCoin, minTransferAmount,
+    usdtWallets, availableNetworks,
     getFee, getMinWithdrawAmount, getWalletByNetwork,
+    getOnchainFee, getOnchainMinWithdraw, getOnchainSubmitUnit, getTransferSubmitUnit,
+    getOnchainCoinSymbol, getTransferCoinSymbol,
+    getSelectedFee, getSelectedMinWithdraw, getSubmitUnit, getCoinSymbol,
+    selectNetwork, selectSubCoin, selectTransferCoin, resetSelection, loadOnchainSubCoins,
     init, sendEmailCode, submitOnchain, submitTransfer,
     loadRecords, cancelRecord, fetchDetail, stopCountdown,
   }

+ 51 - 16
code/src/views/assets/DepositView.vue

@@ -14,11 +14,14 @@ const { t } = useI18n()
 const store = useDepositStore()
 const {
   parentCoins,
-  flatNetworks,
   selectedParentIndex,
-  selectedFlatIndex,
+  selectedNetworkId,
+  selectedSubCoinIndex,
+  networkOptions,
+  subCoinOptions,
   loadingParents,
   childrenLoading,
+  subCoinsLoading,
   error,
   currentOrder,
   orderSubmitting,
@@ -90,8 +93,24 @@ async function onSelectParent(i: number) {
   }
 }
 
-function onSelectNetwork(i: number) {
-  store.selectFlatNetwork(i)
+async function onSelectNetwork(networkId: number) {
+  try {
+    await store.selectNetwork(networkId)
+  } catch {
+    toast.error(store.error ?? t('deposit.loadNetworksFailed'))
+  }
+}
+
+function onSelectSubCoin(i: number) {
+  store.selectSubCoin(i)
+}
+
+function subCoinTabLabel(opt: { coinName: string; coinNameCn: string }) {
+  const cn = opt.coinNameCn?.trim()
+  if (cn && cn !== opt.coinName) {
+    return `${cn} (${opt.coinName})`
+  }
+  return opt.coinName
 }
 
 function validateAmount(): boolean {
@@ -433,27 +452,43 @@ function statusLabel(status: number) {
                 <div v-if="childrenLoading" class="sub-loading">
                   <div class="spinner sm" />
                 </div>
-                <div v-else-if="!flatNetworks.length" class="empty-net">{{ t('deposit.noNetwork') }}</div>
+                <div v-else-if="!networkOptions.length" class="empty-net">{{ t('deposit.noNetwork') }}</div>
                 <div v-else class="network-tabs">
                   <button
-                    v-for="(opt, i) in flatNetworks"
+                    v-for="net in networkOptions"
+                    :key="net.id"
+                    type="button"
+                    class="net-tab"
+                    :class="{ active: selectedNetworkId === net.id }"
+                    @click="onSelectNetwork(net.id)"
+                  >
+                    {{ net.name }}{{ net.protocol ? ` · ${net.protocol}` : '' }}
+                  </button>
+                </div>
+              </div>
+
+              <div v-if="selectedNetworkId" class="form-group">
+                <p class="form-label">{{ t('deposit.subCoin') }}</p>
+                <div v-if="subCoinsLoading" class="sub-loading">
+                  <div class="spinner sm" />
+                </div>
+                <div v-else-if="!subCoinOptions.length" class="empty-net">{{ t('deposit.noSubCoin') }}</div>
+                <div v-else class="network-tabs">
+                  <button
+                    v-for="(opt, i) in subCoinOptions"
                     :key="opt.coinName + '-' + opt.networkConfigId"
                     type="button"
                     class="net-tab"
-                    :class="{ active: selectedFlatIndex === i }"
-                    @click="onSelectNetwork(i)"
+                    :class="{ active: selectedSubCoinIndex === i }"
+                    @click="onSelectSubCoin(i)"
                   >
-                    {{ opt.networkName }}{{ opt.protocol ? ` · ${opt.protocol}` : '' }}
+                    {{ subCoinTabLabel(opt) }}
                   </button>
                 </div>
               </div>
 
-              <div v-if="selectedNetwork" class="net-detail">
+              <div v-if="selectedNetwork?.contractAddress" class="net-detail">
                 <div class="info-row">
-                  <span class="info-label">{{ t('deposit.subCoin') }}</span>
-                  <span class="info-val">{{ selectedNetwork.coinNameCn }} ({{ selectedNetwork.coinName }})</span>
-                </div>
-                <div v-if="selectedNetwork.contractAddress" class="info-row">
                   <span class="info-label">{{ t('deposit.contract') }}</span>
                   <span class="info-val mono break">{{ selectedNetwork.contractAddress }}</span>
                 </div>
@@ -475,7 +510,7 @@ function statusLabel(status: number) {
               <button
                 type="button"
                 class="primary-btn full"
-                :disabled="orderSubmitting || !flatNetworks.length"
+                :disabled="orderSubmitting || !selectedNetwork"
                 @click="onCreateOrder"
               >
                 {{ orderSubmitting ? t('deposit.generating') : t('deposit.generateOrder') }}
@@ -488,7 +523,7 @@ function statusLabel(status: number) {
               <p class="hint-p">{{ t('deposit.introText') }}</p>
               <p v-if="selectedParent" class="hint-p">
                 {{ t('deposit.currentPrefix') }}<strong>{{ selectedParent.nameCn || selectedParent.name }}</strong>
-                <template v-if="selectedNetwork"> - {{ selectedNetwork.networkName }}</template>
+                <template v-if="selectedNetwork"> - {{ selectedNetwork.networkName }}{{ selectedConfig ? ` / ${selectedConfig.coinName}` : '' }}</template>
               </p>
             </div>
           </div>

+ 143 - 38
code/src/views/assets/WithdrawView.vue

@@ -7,8 +7,26 @@ import { useToast } from '@/composables/useToast'
 
 const { t } = useI18n()
 const store = useWithdrawStore()
-const { availableBalance, transferableBalance, initLoading, initError,
-  sendingCode, codeCountdown, submitting, formError, googleBound, availableNetworks, minTransferAmount } = storeToRefs(store)
+const {
+  availableBalance,
+  transferableBalance,
+  initLoading,
+  initError,
+  sendingCode,
+  codeCountdown,
+  submitting,
+  formError,
+  googleBound,
+  networkList,
+  selectedNetworkId,
+  onchainSubCoins,
+  onchainSubCoinsLoading,
+  transferMainCoins,
+  selectedTransferCoinIndex,
+  onchainSelectedCoin,
+  transferSelectedCoin,
+  minTransferAmount,
+} = storeToRefs(store)
 const toast = useToast()
 
 /** store 或通用 `i18n:键` 前缀转文案;其余原样(接口报错) */
@@ -24,7 +42,6 @@ function trErr(msg: string | null | undefined) {
 
 const activeTab = ref<'onchain' | 'transfer'>('onchain')
 
-const network = ref('TRC20')
 const toAddress = ref('')
 const amount = ref('')
 const fundPwd = ref('')
@@ -39,20 +56,43 @@ const tfEmailCode = ref('')
 const tfGoogleCode = ref('')
 const transferError = ref('')
 
-const fee = computed(() => store.getFee(network.value))
-const minWithdraw = computed(() => store.getMinWithdrawAmount(network.value))
+const onchainCoinSymbol = computed(() => store.getOnchainCoinSymbol())
+const transferCoinSymbol = computed(() => store.getTransferCoinSymbol())
+const fee = computed(() => store.getOnchainFee())
+const minWithdraw = computed(() => store.getOnchainMinWithdraw())
 const arrive = computed(() => {
   const a = Number(amount.value)
   const f = Number(fee.value)
   return (!a || a <= f) ? '0' : (a - f).toFixed(2)
 })
 
+function subCoinTabLabel(coin: { displayName: string; unit: string }) {
+  return coin.displayName || coin.unit.toUpperCase()
+}
+
+function onSelectNetwork(networkId: number) {
+  store.selectNetwork(networkId)
+  amount.value = ''
+}
+
+function onSelectSubCoin(i: number) {
+  store.selectSubCoin(i)
+  amount.value = ''
+}
+
+function onTransferCoinChange() {
+  transferAmount.value = ''
+}
+
+function onTabChange(tab: 'onchain' | 'transfer') {
+  activeTab.value = tab
+  amount.value = ''
+  transferAmount.value = ''
+}
+
 onMounted(async () => {
   try {
     await store.init()
-    if (availableNetworks.value.length && !availableNetworks.value.includes(network.value)) {
-      network.value = availableNetworks.value[0]
-    }
   } catch {
     toast.error(trErr(initError.value) || t('withdraw.initFailed'))
   }
@@ -70,7 +110,7 @@ async function sendCode() {
         onchainError.value = t('withdraw.errFillAmountBeforeCode')
         return
       }
-      await store.sendEmailCode('usdt', toAddress.value.trim(), amount.value)
+      await store.sendEmailCode(store.getOnchainSubmitUnit(), toAddress.value.trim(), amount.value)
     } else {
       if (!recipient.value.trim()) {
         transferError.value = t('withdraw.errFillUidBeforeCode')
@@ -80,7 +120,7 @@ async function sendCode() {
         transferError.value = t('withdraw.errFillTransferAmtBeforeCode')
         return
       }
-      await store.sendEmailCode('usdt', recipient.value.trim(), transferAmount.value)
+      await store.sendEmailCode(store.getTransferSubmitUnit(), recipient.value.trim(), transferAmount.value)
     }
     toast.success(t('withdraw.codeSentToEmailToast'))
   } catch {
@@ -100,7 +140,7 @@ async function submitOnchain() {
     return
   }
   if (a < Number(minWithdraw.value)) {
-    onchainError.value = t('withdraw.errMinWithdrawWithAmt', { min: minWithdraw.value })
+    onchainError.value = t('withdraw.errMinWithdrawWithAmt', { min: minWithdraw.value, coin: onchainCoinSymbol.value })
     return
   }
   if (a > Number(availableBalance.value)) {
@@ -125,7 +165,7 @@ async function submitOnchain() {
   }
   try {
     await store.submitOnchain({
-      unit: 'usdt',
+      unit: store.getOnchainSubmitUnit(),
       amount: amount.value,
       address: toAddress.value.trim(),
       fee: fee.value,
@@ -157,7 +197,7 @@ async function submitTransfer() {
     return
   }
   if (a < Number(minTransferAmount.value)) {
-    transferError.value = t('withdraw.errMinTransferWithAmt', { min: minTransferAmount.value })
+    transferError.value = t('withdraw.errMinTransferWithAmt', { min: minTransferAmount.value, coin: transferCoinSymbol.value })
     return
   }
   if (a > Number(transferableBalance.value)) {
@@ -182,7 +222,7 @@ async function submitTransfer() {
   }
   try {
     await store.submitTransfer({
-      unit: 'usdt',
+      unit: store.getTransferSubmitUnit(),
       amount: transferAmount.value,
       address: recipient.value.trim(),
       vcode: tfEmailCode.value,
@@ -221,43 +261,73 @@ async function submitTransfer() {
 
       <template v-else>
         <div class="mode-tabs">
-          <button class="mode-tab" :class="{ active: activeTab === 'onchain' }" @click="activeTab = 'onchain'">{{ t('withdraw.typeOnChainWithdraw') }}</button>
-          <button class="mode-tab" :class="{ active: activeTab === 'transfer' }" @click="activeTab = 'transfer'">{{ t('withdraw.typeInternalTransfer') }}</button>
+          <button class="mode-tab" :class="{ active: activeTab === 'onchain' }" @click="onTabChange('onchain')">{{ t('withdraw.typeOnChainWithdraw') }}</button>
+          <button class="mode-tab" :class="{ active: activeTab === 'transfer' }" @click="onTabChange('transfer')">{{ t('withdraw.typeInternalTransfer') }}</button>
         </div>
 
+        <div class="form-card dark-card">
         <template v-if="activeTab === 'onchain'">
-          <div class="form-card dark-card">
-            <p class="avail-tip">{{ t('withdraw.availWithdrawBalance') }}<strong>{{ availableBalance }} USDT</strong></p>
-
             <div class="form-group">
               <label class="form-label">{{ t('withdraw.withdrawNetworkLabel') }}</label>
-              <div class="network-tabs">
+              <div v-if="!networkList.length" class="empty-net">{{ t('deposit.noNetwork') }}</div>
+              <div v-else class="network-tabs">
                 <button
-                  v-for="net in availableNetworks"
-                  :key="net"
+                  v-for="net in networkList"
+                  :key="net.id"
+                  type="button"
                   class="net-tab"
-                  :class="{ active: network === net }"
-                  @click="network = net"
-                >{{ net }}</button>
+                  :class="{ active: selectedNetworkId === net.id }"
+                  @click="onSelectNetwork(net.id)"
+                >
+                  {{ net.name }}{{ net.protocol ? ` · ${net.protocol}` : '' }}
+                </button>
               </div>
             </div>
 
+            <div v-if="selectedNetworkId" class="form-group">
+              <label class="form-label">{{ t('withdraw.subCoin') }}</label>
+              <div v-if="onchainSubCoinsLoading" class="sub-loading">
+                <div class="spinner sm" />
+              </div>
+              <div v-else-if="!onchainSubCoins.length" class="empty-net">{{ t('deposit.noSubCoin') }}</div>
+              <div v-else class="network-tabs">
+                <button
+                  v-for="(coin, i) in onchainSubCoins"
+                  :key="coin.unit"
+                  type="button"
+                  class="net-tab"
+                  :class="{ active: onchainSelectedCoin?.unit === coin.unit }"
+                  @click="onSelectSubCoin(i)"
+                >
+                  {{ subCoinTabLabel(coin) }}
+                </button>
+              </div>
+            </div>
+
+            <p class="avail-tip">
+              {{ t('withdraw.availWithdrawBalance') }}<strong>{{ availableBalance }} {{ onchainCoinSymbol }}</strong>
+            </p>
+
             <div class="form-group">
               <label class="form-label">{{ t('withdraw.address') }}</label>
-              <input v-model="toAddress" class="form-input" :placeholder="t('withdraw.addressForNetwork', { network })" />
+              <input
+                v-model="toAddress"
+                class="form-input"
+                :placeholder="t('withdraw.addressForCoin', { coin: onchainCoinSymbol })"
+              />
             </div>
 
             <div class="form-group">
               <div class="label-row">
-                <label class="form-label">{{ t('withdraw.withdrawAmountUsdt') }}</label>
+                <label class="form-label">{{ t('withdraw.withdrawAmountCoin', { coin: onchainCoinSymbol }) }}</label>
                 <button class="all-btn" @click="amount = availableBalance">{{ t('common.all') }}</button>
               </div>
               <input v-model="amount" type="number" class="form-input" placeholder="0.00" min="0" />
               <div class="fee-preview">
-                <div class="fee-row"><span>{{ t('withdraw.fee') }}</span><span>{{ fee }} USDT</span></div>
-                <div class="fee-row"><span>{{ t('withdraw.expectedArrival') }}</span><span class="arrive-val">{{ arrive || '—' }} USDT</span></div>
+                <div class="fee-row"><span>{{ t('withdraw.fee') }}</span><span>{{ fee }} {{ onchainCoinSymbol }}</span></div>
+                <div class="fee-row"><span>{{ t('withdraw.expectedArrival') }}</span><span class="arrive-val">{{ arrive || '—' }} {{ onchainCoinSymbol }}</span></div>
                 <div class="fee-row">
-                  <span>{{ t('withdraw.minWithdrawRowLabel') }}</span><span>{{ minWithdraw }} USDT</span>
+                  <span>{{ t('withdraw.minWithdrawRowLabel') }}</span><span>{{ minWithdraw }} {{ onchainCoinSymbol }}</span>
                 </div>
               </div>
             </div>
@@ -284,15 +354,33 @@ async function submitTransfer() {
 
             <p v-if="onchainError || formError" class="form-error">{{ trErr(onchainError || formError) }}</p>
 
-            <button class="submit-btn" :disabled="submitting" @click="submitOnchain">
+            <button class="submit-btn" :disabled="submitting || !onchainSelectedCoin" @click="submitOnchain">
               {{ submitting ? t('header.submitting') : t('withdraw.confirmWithdrawSubmit') }}
             </button>
-          </div>
         </template>
 
         <template v-else>
-          <div class="form-card dark-card">
-            <p class="avail-tip">{{ t('withdraw.availTransferBalance') }}<strong>{{ transferableBalance }} USDT</strong></p>
+            <div v-if="transferMainCoins.length" class="form-group">
+              <label class="form-label">{{ t('withdraw.mainCoinLabel') }}</label>
+              <select
+                v-model.number="selectedTransferCoinIndex"
+                class="form-input coin-select"
+                @change="onTransferCoinChange"
+              >
+                <option
+                  v-for="(coin, i) in transferMainCoins"
+                  :key="coin.unit"
+                  :value="i"
+                >
+                  {{ subCoinTabLabel(coin) }}
+                </option>
+              </select>
+              <p class="hint-line">{{ t('withdraw.transferMainCoinHint') }}</p>
+            </div>
+
+            <p class="avail-tip">
+              {{ t('withdraw.availTransferBalance') }}<strong>{{ transferableBalance }} {{ transferCoinSymbol }}</strong>
+            </p>
 
             <div class="form-group">
               <label class="form-label">{{ t('withdraw.recipientUidLabel') }}</label>
@@ -301,13 +389,15 @@ async function submitTransfer() {
 
             <div class="form-group">
               <div class="label-row">
-                <label class="form-label">{{ t('withdraw.transferAmountUsdt') }}</label>
+                <label class="form-label">{{ t('withdraw.transferAmountCoin', { coin: transferCoinSymbol }) }}</label>
                 <button class="all-btn" @click="transferAmount = transferableBalance">{{ t('common.all') }}</button>
               </div>
               <input v-model="transferAmount" type="number" class="form-input" placeholder="0.00" min="0" />
               <div class="fee-preview">
                 <div class="fee-row"><span>{{ t('withdraw.internalTransferFeeLabel') }}</span><span>{{ t('withdraw.free') }}</span></div>
-                <div class="fee-row"><span>{{ t('withdraw.minTransferRowLabel') }}</span><span>{{ minTransferAmount }} USDT</span></div>
+                <div class="fee-row">
+                  <span>{{ t('withdraw.minTransferRowLabel') }}</span><span>{{ minTransferAmount }} {{ transferCoinSymbol }}</span>
+                </div>
               </div>
             </div>
 
@@ -333,11 +423,11 @@ async function submitTransfer() {
 
             <p v-if="transferError || formError" class="form-error">{{ trErr(transferError || formError) }}</p>
 
-            <button class="submit-btn" :disabled="submitting" @click="submitTransfer">
+            <button class="submit-btn" :disabled="submitting || !transferSelectedCoin" @click="submitTransfer">
               {{ submitting ? t('header.submitting') : t('withdraw.confirmTransferSubmit') }}
             </button>
-          </div>
         </template>
+        </div>
       </template>
     </div>
   </main>
@@ -353,6 +443,9 @@ async function submitTransfer() {
 .hist-link { font-size: var(--text-small); color: var(--color-primary); text-decoration: none; }
 .loading-state { display: flex; justify-content: center; padding: var(--space-16); }
 .spinner { width: 32px; height: 32px; border-radius: 50%; border: 2px solid var(--color-border); border-top-color: var(--color-primary); animation: spin .8s linear infinite; }
+.spinner.sm { width: 20px; height: 20px; }
+.sub-loading { display: flex; padding: var(--space-2) 0; }
+.empty-net { font-size: var(--text-small); color: var(--color-text-muted); padding: var(--space-2) 0; }
 @keyframes spin { to { transform: rotate(360deg); } }
 .error-state { padding: var(--space-8); text-align: center; color: var(--color-text-secondary); }
 .retry-btn { margin-top: var(--space-4); background: transparent; border: 1px solid var(--color-primary); border-radius: var(--radius-md); color: var(--color-primary); padding: var(--space-2) var(--space-5); cursor: pointer; }
@@ -367,10 +460,22 @@ async function submitTransfer() {
 .form-input { width: 100%; box-sizing: border-box; background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); color: var(--color-text-primary); outline: none; }
 .form-input:focus { border-color: var(--color-primary); }
 .form-input::placeholder { color: var(--color-text-muted); }
+.coin-select {
+  width: 100%;
+  box-sizing: border-box;
+  appearance: none;
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23999' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: right var(--space-4) center;
+  padding-right: calc(var(--space-4) + 20px);
+  cursor: pointer;
+}
 .network-tabs { display: flex; gap: var(--space-2); flex-wrap: wrap; }
 .net-tab { padding: var(--space-2) var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: transparent; color: var(--color-text-secondary); font-size: var(--text-small); cursor: pointer; transition: all var(--transition); }
 .net-tab:hover { border-color: var(--color-primary); color: var(--color-primary); }
 .net-tab.active { border-color: var(--color-primary); background: rgba(240,185,11,.1); color: var(--color-primary); }
+.network-hint { font-size: var(--text-small); color: var(--color-text-primary); padding: var(--space-2) 0; }
+.hint-line { font-size: var(--text-caption); color: var(--color-text-muted); margin-top: var(--space-2); line-height: 1.5; }
 .label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2); }
 .all-btn { background: none; border: none; color: var(--color-primary); font-size: var(--text-small); cursor: pointer; }
 .fee-preview { background: var(--color-bg-card); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-top: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2); }

+ 110 - 125
code/src/views/finance/StakeView.vue

@@ -6,7 +6,8 @@ import {
   fetchMinOpenStakingConfig,
   fetchStakingConfigList,
   fetchStakingTeamOverview,
-  submitStake,
+  fetchStakingSpotPrice,
+  submitStakeByUsdt,
   type StakingConfig,
 } from '@/api/staking'
 import { loadFundCoinBalance, loadStakingWalletSnapshot, STAKING_COIN_UNIT } from '@/utils/stakingWallet'
@@ -30,9 +31,10 @@ const toast = useToast()
 const config = ref<StakingConfig | null>(null)
 const loading = ref(true)
 const submitting = ref(false)
-const subscribeQty = ref('')
+/** 用户输入的 USDT 金额 */
+const subscribeUsdt = ref('')
 const error = ref<string | null>(null)
-const availableBal = ref('0')
+const availableUsdtBal = ref('0')
 const showTransfer = ref(false)
 const teamCount = ref(0)
 const lockedTotal = ref('0')
@@ -63,9 +65,6 @@ const displayCoinUnit = computed(() => {
   return u.toUpperCase() === 'IBIT' ? 'iBit' : u
 })
 
-/** 可提现账户可用 iBit(/uc/asset/wallet) */
-const availableIbit = computed(() => availableBal.value)
-
 function fmtAmount(n: string | number, maxFrac = 8) {
   const v = Number(n)
   if (!Number.isFinite(v)) return '0'
@@ -79,51 +78,71 @@ const priceCoinUnit = computed(
   () => config.value?.coinUnit?.trim() || STAKING_COIN_UNIT,
 )
 
-/** iBit/USDT 实时价(现货 WS:market_{coin}usdt_trade_ticker) */
+/** iBit/USDT 实时价(现货 WS) */
 const { livePrice: ibitUsdtPrice, disconnect: disconnectLivePrice } =
   useLiveCoinUsdtPrice(priceCoinUnit)
 
-/** 现货页路由交易对,如 IBIT-USDT */
-const spotTradeSymbol = computed(() => {
-  const u = (config.value?.coinUnit?.trim() || STAKING_COIN_UNIT).toUpperCase()
-  return `${u}-USDT`
-})
-
-function goToIbitSpotTrade() {
-  disconnectLivePrice()
-  router.push({ name: 'spot', params: { symbol: spotTradeSymbol.value } })
-}
+/** Redis 现货价(与提交闪兑同源,WS 未连上时回退) */
+const redisSpotPrice = ref(0)
 
 onBeforeRouteLeave(() => {
   disconnectLivePrice()
 })
 
-const unitPriceUsdt = computed(() => ibitUsdtPrice.value)
+/** 优先 WS,否则用 Redis HTTP 价 */
+const unitPriceUsdt = computed(() => {
+  if (ibitUsdtPrice.value > 0) {
+    return ibitUsdtPrice.value
+  }
+  return redisSpotPrice.value
+})
 
-/** 认购数量 × 实时 USDT 价;左侧输入或 WS 推送时自动重算 */
-const priceUsdt = computed(() => {
-  const qty = Number(subscribeQty.value)
-  if (!Number.isFinite(qty) || qty <= 0) {
-    return '0.00'
+async function refreshRedisSpotPrice() {
+  const unit = priceCoinUnit.value
+  if (!unit || unit.toUpperCase() === 'USDT') {
+    redisSpotPrice.value = 1
+    return
+  }
+  try {
+    const res = await fetchStakingSpotPrice(unit)
+    const p = Number(res?.price)
+    redisSpotPrice.value = Number.isFinite(p) && p > 0 ? p : 0
+  } catch {
+    redisSpotPrice.value = 0
+  }
+}
+
+/** USDT → iBit 实时折算(左侧展示) */
+const subscribeIbit = computed(() => {
+  const usdt = Number(subscribeUsdt.value)
+  if (!Number.isFinite(usdt) || usdt <= 0) {
+    return '0'
   }
   const price = ibitUsdtPrice.value
-  if (!price) {
-    return '0.00'
+  if (!price || price <= 0) {
+    return '0'
   }
-  return (qty * price).toLocaleString('en-US', {
-    minimumFractionDigits: 2,
-    maximumFractionDigits: 2,
+  const ibit = usdt / price
+  return ibit.toLocaleString('en-US', {
+    minimumFractionDigits: 0,
+    maximumFractionDigits: 8,
   })
 })
 
+const subscribeIbitNum = computed(() => {
+  const s = subscribeIbit.value.replace(/,/g, '')
+  const n = Number(s)
+  return Number.isFinite(n) ? n : 0
+})
+
 function fillAllAvailable() {
-  const avail = Number(availableBal.value)
+  const avail = Number(availableUsdtBal.value)
   if (avail > 0) {
-    subscribeQty.value = String(avail)
+    subscribeUsdt.value = String(avail)
   }
 }
 
-function openSpotFundTransfer() {
+function openUsdtTransfer() {
   if (!AUTH_ENABLED) {
     router.push({ name: 'login', query: { redirect: route.fullPath } })
     return
@@ -135,11 +154,11 @@ function stakingCoinUnit() {
   return config.value?.coinUnit?.trim() || STAKING_COIN_UNIT
 }
 
-async function refreshWithdrawableIbit() {
+async function refreshAvailableUsdt() {
   try {
-    availableBal.value = await loadFundCoinBalance(stakingCoinUnit())
+    availableUsdtBal.value = await loadFundCoinBalance('USDT')
   } catch {
-    availableBal.value = '0'
+    availableUsdtBal.value = '0'
   }
 }
 
@@ -204,7 +223,7 @@ async function load(opts?: { silent?: boolean }) {
       error.value = t('finance.configNotFound')
       return
     }
-    await Promise.all([refreshWithdrawableIbit(), refreshOverviewStats()])
+    await Promise.all([refreshAvailableUsdt(), refreshOverviewStats(), refreshRedisSpotPrice()])
   } catch (e) {
     error.value = e instanceof Error ? e.message : t('common.loadFailed')
     config.value = null
@@ -221,29 +240,41 @@ async function onSubmit() {
     return
   }
   if (!config.value) return
-  const amt = Number(subscribeQty.value)
-  if (!amt || amt <= 0) {
+
+  const usdt = Number(subscribeUsdt.value)
+  if (!usdt || usdt <= 0) {
+    toast.error(t('finance.amountRequired'))
+    return
+  }
+  if (!unitPriceUsdt.value || unitPriceUsdt.value <= 0) {
+    toast.error(t('finance.priceUnavailable'))
+    return
+  }
+
+  const ibit = subscribeIbitNum.value
+  if (!ibit || ibit <= 0) {
     toast.error(t('finance.amountRequired'))
     return
   }
-  if (amt < Number(config.value.minAmount)) {
+  if (ibit < Number(config.value.minAmount)) {
     toast.error(t('finance.belowMin', { min: config.value.minAmount }))
     return
   }
   const max = Number(config.value.maxAmount)
-  if (max > 0 && amt > max) {
+  if (max > 0 && ibit > max) {
     toast.error(t('finance.aboveMax', { max: config.value.maxAmount }))
     return
   }
-  if (amt > Number(availableBal.value)) {
+  if (usdt > Number(availableUsdtBal.value)) {
     toast.error(t('assets.insufficientBalance'))
     return
   }
+
   submitting.value = true
   try {
-    await submitStake(config.value.id, subscribeQty.value)
+    await submitStakeByUsdt(config.value.id, subscribeUsdt.value)
     toast.success(t('finance.stakeSuccess'))
-    subscribeQty.value = ''
+    subscribeUsdt.value = ''
     await load({ silent: true })
     await refreshOverviewStats()
   } catch (e) {
@@ -256,6 +287,10 @@ async function onSubmit() {
 onMounted(() => load())
 
 watch(() => route.params.configId, () => load())
+
+watch(priceCoinUnit, () => {
+  void refreshRedisSpotPrice()
+})
 </script>
 
 <template>
@@ -265,7 +300,6 @@ watch(() => route.params.configId, () => load())
     </div>
 
     <template v-else-if="config">
-      <!-- Hero -->
       <section class="ido-hero">
         <div class="page-container hero-inner">
           <div class="hero-copy">
@@ -286,7 +320,6 @@ watch(() => route.params.configId, () => load())
         </div>
       </section>
 
-      <!-- 团队 / 锁仓总览 -->
       <section class="ido-overview-section page-container">
         <div class="ido-overview-grid">
           <button type="button" class="ido-overview-card" @click="goToMyTeam">
@@ -306,35 +339,23 @@ watch(() => route.params.configId, () => load())
         </div>
       </section>
 
-      <!-- 参与预售 -->
       <section class="ido-form-section page-container">
         <h2 class="section-title">{{ t('finance.joinPresale') }}</h2>
 
         <div class="subscribe-grid">
           <div class="subscribe-col subscribe-col--label">
-            <div class="field-label-row">
-              <label class="field-label">{{ t('finance.subscribeQty') }}</label>
-              <div class="label-actions">
-                <button
-                  type="button"
-                  class="link-action-btn"
-                  @click="goToIbitSpotTrade"
-                >
-                  {{ t('assets.goSpotTrade') }}
-                </button>
-              </div>
-            </div>
+            <label class="field-label">{{ t('finance.correspondingPrice') }}</label>
           </div>
           <div class="subscribe-col subscribe-col--label">
-            <label class="field-label">{{ t('finance.correspondingPrice') }}</label>
+            <label class="field-label">{{ t('finance.subscribeQty') }}</label>
           </div>
 
           <div class="subscribe-col subscribe-col--meta">
             <div class="meta-line-row">
               <p class="meta-line">
-                {{ t('finance.availableIbit') }}:
-                <span class="meta-amount num">{{ fmtAmount(availableIbit) }}</span>
-                {{ displayCoinUnit }}
+                {{ t('finance.availableUsdt') }}:
+                <span class="meta-amount num">{{ fmtAmount(availableUsdtBal, 2) }}</span>
+                USDT
               </p>
               <button
                 type="button"
@@ -346,50 +367,49 @@ watch(() => route.params.configId, () => load())
             </div>
           </div>
           <div class="subscribe-col subscribe-col--meta">
-            <p v-if="unitPriceUsdt > 0" class="meta-line">
-              {{ t('finance.ibitUnitPriceLine', { price: fmtAmount(unitPriceUsdt, 4) }) }}
+            <p class="meta-line">
+              {{ t('finance.ibitUnitPriceLine', { price: unitPriceUsdt > 0 ? fmtAmount(unitPriceUsdt, 4) : '—' }) }}
             </p>
-            <p v-else class="meta-line meta-line--placeholder" aria-hidden="true">&nbsp;</p>
           </div>
 
-          <div class="subscribe-col subscribe-col--input">
-            <div class="field-input-wrap">
-              <input
-                v-model="subscribeQty"
-                type="number"
-                class="field-input"
-                min="0"
-                :max="availableIbit"
-                placeholder="0"
-                step="any"
-              />
-              <span class="field-unit">{{ displayCoinUnit }}</span>
-            </div>
-          </div>
           <div class="subscribe-col subscribe-col--input subscribe-col--price">
             <div class="price-transfer-row">
-              <div class="field-input-wrap field-input-wrap--readonly">
+              <div class="field-input-wrap">
                 <input
-                  :value="priceUsdt"
-                  type="text"
-                  class="field-input field-input--readonly"
-                  readonly
-                  tabindex="-1"
-                  aria-readonly="true"
+                  v-model="subscribeUsdt"
+                  type="number"
+                  class="field-input"
+                  min="0"
+                  placeholder="0.00"
+                  step="any"
                 />
                 <span class="field-unit">USDT</span>
               </div>
-              <button type="button" class="transfer-btn" @click="openSpotFundTransfer">
+              <button type="button" class="transfer-btn" @click="openUsdtTransfer">
                 {{ t('finance.ibitTransfer') }}
               </button>
             </div>
           </div>
+          <div class="subscribe-col subscribe-col--input">
+            <div class="field-input-wrap field-input-wrap--readonly">
+              <input
+                :value="subscribeIbit"
+                type="text"
+                class="field-input field-input--readonly"
+                readonly
+                tabindex="-1"
+                aria-readonly="true"
+                placeholder="0"
+              />
+              <span class="field-unit">{{ displayCoinUnit }}</span>
+            </div>
+          </div>
         </div>
 
         <button
           type="button"
           class="confirm-btn"
-          :disabled="submitting"
+          :disabled="submitting || !unitPriceUsdt"
           @click="onSubmit"
         >
           {{ submitting ? t('common.loading') : t('finance.confirmPresale') }}
@@ -413,11 +433,10 @@ watch(() => route.params.configId, () => load())
 
     <TransferModal
       v-if="showTransfer"
-      spot-trading-bridge-only
       prefer-default-symbol
-      default-from="SPOT"
-      default-to="SPOT_TRADING"
-      :default-symbol="stakingCoinUnit()"
+      default-from="SWAP"
+      default-to="SPOT"
+      default-symbol="USDT"
       @close="showTransfer = false"
       @success="onTransferSuccess"
     />
@@ -456,7 +475,6 @@ watch(() => route.params.configId, () => load())
   display: inline-block;
 }
 
-/* Hero */
 .ido-hero {
   padding-top: calc(var(--header-height) + var(--space-10));
   padding-bottom: var(--space-8);
@@ -514,7 +532,6 @@ watch(() => route.params.configId, () => load())
   display: block;
 }
 
-/* Overview */
 .ido-overview-section {
   max-width: 960px;
   padding-bottom: var(--space-6);
@@ -566,7 +583,6 @@ watch(() => route.params.configId, () => load())
   font-variant-numeric: tabular-nums;
 }
 
-/* Form */
 .ido-form-section {
   max-width: 960px;
   padding-bottom: var(--space-8);
@@ -618,10 +634,6 @@ watch(() => route.params.configId, () => load())
   margin: 0 4px;
 }
 
-.meta-line--placeholder {
-  visibility: hidden;
-}
-
 .subscribe-col--input .field-input-wrap {
   width: 100%;
 }
@@ -657,25 +669,11 @@ watch(() => route.params.configId, () => load())
   background: rgba(240, 185, 11, 0.1);
 }
 
-.field-label-row {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: var(--space-2);
-}
-
 .field-label {
   font-size: 14px;
   color: rgba(255, 255, 255, 0.55);
 }
 
-.label-actions {
-  display: flex;
-  align-items: center;
-  gap: var(--space-4);
-  flex-shrink: 0;
-}
-
 .link-action-btn {
   padding: 0;
   border: none;
@@ -724,15 +722,6 @@ watch(() => route.params.configId, () => load())
   pointer-events: none;
 }
 
-.field-value {
-  flex: 1;
-  font-size: 16px;
-  color: #fff;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
 .field-unit {
   flex-shrink: 0;
   margin-left: var(--space-3);
@@ -812,10 +801,6 @@ watch(() => route.params.configId, () => load())
     grid-template-columns: 1fr;
   }
 
-  .subscribe-col--meta:empty {
-    display: none;
-  }
-
   .ido-hero {
     padding-top: calc(var(--header-height) + var(--space-6));
   }