新增:提现弹窗展示手续费与最低提现金额,读取后台系统参数配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
马丁 2026-08-05 22:44:40 +08:00
parent 4f695953d3
commit 1c64d3a834
7 changed files with 140 additions and 13 deletions

View File

@ -174,3 +174,28 @@ export function amountToUsdcDisplay(raw: number | undefined): string {
if (raw == null || !Number.isFinite(raw)) return '0.00' if (raw == null || !Number.isFinite(raw)) return '0.00'
return (raw / USDC_SCALE).toFixed(2) return (raw / USDC_SCALE).toFixed(2)
} }
/** 提现配置(GET /pmset/getWithdrawConfig) */
export interface WithdrawConfig {
/** 手续费,最小精度整数(1U=1e6;系统参数配置口径为 1=1U) */
fee: number
/** 最低提现金额,最小精度整数(1U=1e6;系统参数配置口径为 1=1U) */
minWithdrawAmount: number
}
export interface WithdrawConfigResponse extends ApiResponse<WithdrawConfig> {
code: number
data?: WithdrawConfig
msg: string
}
/**
* GET /pmset/getWithdrawConfig
* 获取提现手续费与最低提现金额(系统参数 withdrawFee / minWithdrawAmount,配置 1=1U;缺失自动插入)
*/
export async function getWithdrawConfig(
config?: { headers?: Record<string, string> },
): Promise<WithdrawConfigResponse> {
return get<WithdrawConfigResponse>('/pmset/getWithdrawConfig', undefined, config)
}

View File

@ -34,6 +34,21 @@
</template> </template>
</v-text-field> </v-text-field>
<div class="withdraw-meta">
<div class="withdraw-meta-row">
<span>{{ t('withdraw.minAmount') }}</span>
<span>${{ minAmountDisplay }}</span>
</div>
<div class="withdraw-meta-row">
<span>{{ t('withdraw.fee') }}</span>
<span>${{ feeDisplay }}</span>
</div>
<div v-if="amountNum > 0" class="withdraw-meta-row withdraw-meta-row--receive">
<span>{{ t('withdraw.receiveAmount') }}</span>
<span>${{ receiveAmountDisplay }}</span>
</div>
</div>
<v-select <v-select
v-model="selectedNetwork" v-model="selectedNetwork"
:items="networks" :items="networks"
@ -86,7 +101,12 @@ import { BrowserProvider } from 'ethers'
import { SiweMessage } from 'siwe' import { SiweMessage } from 'siwe'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useToastStore } from '@/stores/toast' import { useToastStore } from '@/stores/toast'
import { withdrawByWallet, usdcToAmount } from '@/api/pmset' import {
withdrawByWallet,
usdcToAmount,
getWithdrawConfig,
amountToUsdcDisplay,
} from '@/api/pmset'
import { WALLET_PRIMARY_NETWORK_ID, useWalletNetworkGuard } from '@/composables/useWalletNetworkPolicy' import { WALLET_PRIMARY_NETWORK_ID, useWalletNetworkGuard } from '@/composables/useWalletNetworkPolicy'
const { t } = useI18n() const { t } = useI18n()
@ -107,6 +127,9 @@ const connectedAddress = ref('')
const connecting = ref(false) const connecting = ref(false)
const submitting = ref(false) const submitting = ref(false)
const errorMessage = ref('') const errorMessage = ref('')
const feeRaw = ref(0)
const minWithdrawRaw = ref(0)
const configLoading = ref(false)
const networks = [ const networks = [
{ id: 'ethereum', label: 'Ethereum' }, { id: 'ethereum', label: 'Ethereum' },
@ -128,11 +151,25 @@ const { onWalletNetworkChange } = useWalletNetworkGuard(
const balanceNum = computed(() => parseFloat(props.balance) || 0) const balanceNum = computed(() => parseFloat(props.balance) || 0)
const amountNum = computed(() => parseFloat(amount.value) || 0) const amountNum = computed(() => parseFloat(amount.value) || 0)
const feeDisplayNum = computed(() => feeRaw.value / 1_000_000)
const minAmountDisplayNum = computed(() => minWithdrawRaw.value / 1_000_000)
const feeDisplay = computed(() => amountToUsdcDisplay(feeRaw.value))
const minAmountDisplay = computed(() => amountToUsdcDisplay(minWithdrawRaw.value))
const receiveAmountDisplay = computed(() => {
const receive = Math.max(0, amountNum.value - feeDisplayNum.value)
return receive.toFixed(2)
})
const amountError = computed(() => { const amountError = computed(() => {
if (!amount.value) return '' if (!amount.value) return ''
if (amountNum.value <= 0) return t('withdraw.amountMustBePositive') if (amountNum.value <= 0) return t('withdraw.amountMustBePositive')
if (amountNum.value > balanceNum.value) return t('withdraw.insufficientBalance') if (amountNum.value > balanceNum.value) return t('withdraw.insufficientBalance')
if (minAmountDisplayNum.value > 0 && amountNum.value < minAmountDisplayNum.value) {
return t('withdraw.belowMinAmount', { min: minAmountDisplay.value })
}
if (feeDisplayNum.value > 0 && amountNum.value <= feeDisplayNum.value) {
return t('withdraw.amountMustExceedFee', { fee: feeDisplay.value })
}
return '' return ''
}) })
@ -145,7 +182,8 @@ const canSubmit = computed(
amountNum.value <= balanceNum.value && amountNum.value <= balanceNum.value &&
hasValidDestination.value && hasValidDestination.value &&
!amountError.value && !amountError.value &&
!errorMessage.value, !errorMessage.value &&
!configLoading.value,
) )
function shortAddress(addr: string) { function shortAddress(addr: string) {
@ -175,6 +213,23 @@ function resolveUserLoginWalletAddress(): string | undefined {
return undefined return undefined
} }
async function loadWithdrawConfig() {
configLoading.value = true
try {
const res = await getWithdrawConfig()
if (res.code === 0 || res.code === 200) {
feeRaw.value = Number(res.data?.fee ?? 0) || 0
minWithdrawRaw.value = Number(res.data?.minWithdrawAmount ?? 0) || 0
}
} catch (e) {
console.error('[loadWithdrawConfig]', e)
feeRaw.value = 0
minWithdrawRaw.value = 0
} finally {
configLoading.value = false
}
}
/** 打开弹窗时自动连接浏览器钱包,且必须与登录账户地址一致 */ /** 打开弹窗时自动连接浏览器钱包,且必须与登录账户地址一致 */
async function autoConnectWallet() { async function autoConnectWallet() {
connectedAddress.value = '' connectedAddress.value = ''
@ -299,7 +354,7 @@ watch(
errorMessage.value = '' errorMessage.value = ''
return return
} }
await autoConnectWallet() await Promise.all([loadWithdrawConfig(), autoConnectWallet()])
}, },
) )
</script> </script>
@ -358,13 +413,35 @@ watch(
} }
.amount-field { .amount-field {
margin-bottom: 16px; margin-bottom: 10px;
} }
.max-btn { .max-btn {
text-transform: none; text-transform: none;
font-size: 0.8rem; font-size: 0.8rem;
} }
.withdraw-meta {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
padding: 10px 12px;
background: #f9fafb;
border-radius: 8px;
font-size: 0.8125rem;
color: #6b7280;
}
.withdraw-meta-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.withdraw-meta-row--receive {
color: #111827;
font-weight: 600;
}
.network-select { .network-select {
margin-bottom: 20px; margin-bottom: 20px;
} }

View File

@ -519,7 +519,12 @@
"customAddressNotSupported": "Custom address is not supported for this withdrawal.", "customAddressNotSupported": "Custom address is not supported for this withdrawal.",
"loginWalletLabel": "Your login wallet", "loginWalletLabel": "Your login wallet",
"connectingWallet": "Connecting wallet…", "connectingWallet": "Connecting wallet…",
"walletMismatch": "Switch your browser wallet to the same account you used to sign in." "walletMismatch": "Switch your browser wallet to the same account you used to sign in.",
"fee": "Fee",
"minAmount": "Minimum withdrawal",
"receiveAmount": "You will receive",
"belowMinAmount": "Amount must be at least {min} USDC",
"amountMustExceedFee": "Amount must be greater than the fee of {fee} USDC"
}, },
"earnActivity": { "earnActivity": {
"title": "Participate to Earn", "title": "Participate to Earn",
@ -543,4 +548,4 @@
"ja": "日本語", "ja": "日本語",
"ko": "한국어" "ko": "한국어"
} }
} }

View File

@ -519,7 +519,12 @@
"customAddressNotSupported": "この出金ではカスタムアドレスはサポートされていません。", "customAddressNotSupported": "この出金ではカスタムアドレスはサポートされていません。",
"loginWalletLabel": "ログイン中のウォレット", "loginWalletLabel": "ログイン中のウォレット",
"connectingWallet": "ウォレットに接続しています…", "connectingWallet": "ウォレットに接続しています…",
"walletMismatch": "ログイン時に使用したアカウントにブラウザのウォレットを切り替えてください。" "walletMismatch": "ログイン時に使用したアカウントにブラウザのウォレットを切り替えてください。",
"fee": "手数料",
"minAmount": "最低出金額",
"receiveAmount": "受取金額",
"belowMinAmount": "出金額は {min} USDC 以上である必要があります",
"amountMustExceedFee": "出金額は手数料 {fee} USDC より大きい必要があります"
}, },
"earnActivity": { "earnActivity": {
"title": "キャンペーンに参加して稼ぐ", "title": "キャンペーンに参加して稼ぐ",
@ -543,4 +548,4 @@
"ja": "日本語", "ja": "日本語",
"ko": "한국어" "ko": "한국어"
} }
} }

View File

@ -519,7 +519,12 @@
"customAddressNotSupported": "이 출금에서는 사용자 지정 주소가 지원되지 않습니다.", "customAddressNotSupported": "이 출금에서는 사용자 지정 주소가 지원되지 않습니다.",
"loginWalletLabel": "로그인 지갑", "loginWalletLabel": "로그인 지갑",
"connectingWallet": "지갑 연결 중…", "connectingWallet": "지갑 연결 중…",
"walletMismatch": "로그인에 사용한 계정과 동일한 계정으로 브라우저 지갑을 전환해 주세요." "walletMismatch": "로그인에 사용한 계정과 동일한 계정으로 브라우저 지갑을 전환해 주세요.",
"fee": "수수료",
"minAmount": "최소 출금 금액",
"receiveAmount": "실수령액",
"belowMinAmount": "출금 금액은 최소 {min} USDC 이상이어야 합니다",
"amountMustExceedFee": "출금 금액은 수수료 {fee} USDC 보다 커야 합니다"
}, },
"earnActivity": { "earnActivity": {
"title": "이벤트 참여하여 돈 벌기", "title": "이벤트 참여하여 돈 벌기",
@ -543,4 +548,4 @@
"ja": "日本語", "ja": "日本語",
"ko": "한국어" "ko": "한국어"
} }
} }

View File

@ -532,7 +532,12 @@
"customAddressNotSupported": "暂不支持自定义地址提现。", "customAddressNotSupported": "暂不支持自定义地址提现。",
"loginWalletLabel": "登录钱包(提现地址)", "loginWalletLabel": "登录钱包(提现地址)",
"connectingWallet": "正在连接钱包…", "connectingWallet": "正在连接钱包…",
"walletMismatch": "请在浏览器钱包中切换到您登录时使用的账户。" "walletMismatch": "请在浏览器钱包中切换到您登录时使用的账户。",
"fee": "手续费",
"minAmount": "最低提现金额",
"receiveAmount": "实际到账",
"belowMinAmount": "提现金额不能低于 {min} USDC",
"amountMustExceedFee": "提现金额必须大于手续费 {fee} USDC"
}, },
"earnActivity": { "earnActivity": {
"title": "参与活动赚钱", "title": "参与活动赚钱",
@ -556,4 +561,4 @@
"ja": "日本語", "ja": "日本語",
"ko": "한국어" "ko": "한국어"
} }
} }

View File

@ -519,7 +519,12 @@
"customAddressNotSupported": "暫不支援自訂地址提現。", "customAddressNotSupported": "暫不支援自訂地址提現。",
"loginWalletLabel": "登入錢包(提現地址)", "loginWalletLabel": "登入錢包(提現地址)",
"connectingWallet": "正在連接錢包…", "connectingWallet": "正在連接錢包…",
"walletMismatch": "請在瀏覽器錢包中切換至您登入時使用的帳戶。" "walletMismatch": "請在瀏覽器錢包中切換至您登入時使用的帳戶。",
"fee": "手續費",
"minAmount": "最低提現金額",
"receiveAmount": "實際到帳",
"belowMinAmount": "提現金額不能低於 {min} USDC",
"amountMustExceedFee": "提現金額必須大於手續費 {fee} USDC"
}, },
"earnActivity": { "earnActivity": {
"title": "參與活動賺錢", "title": "參與活動賺錢",