refactor(game): 重构提现功能中的钻石金额范围计算逻辑
- 移除硬编码的快速提现金额配置,改为动态计算钻石金额边界 - 添加新的辅助函数 getWithdrawDiamondBounds 和 getQuickWithdrawAmounts - 在桌面和移动端提现组件中调整钻石金额字段的位置 - 更新初始金额设置逻辑以使用新的钻石边界计算 - 优化快速金额选择卡片的ID生成方式,包含支付渠道和类型信息 - 添加对支付方法最小/最大金额限制的支持
This commit is contained in:
@@ -390,62 +390,6 @@ function DesktopWithdraw() {
|
||||
>
|
||||
<div className="flex min-h-full min-w-0 flex-[1.7] flex-col px-design-16 py-design-14">
|
||||
<div className="flex flex-col gap-design-12">
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
||||
>
|
||||
<AmountShell
|
||||
amount={vm.amount}
|
||||
availableBalanceText={t(
|
||||
'gameDesktop.withdraw.availableBalance',
|
||||
{ amount: formatNumber(vm.availableBalance) },
|
||||
)}
|
||||
onAmountChange={handleAmountChange}
|
||||
onMinus={() => handleAmountChange(vm.amount - 1)}
|
||||
onPlus={() => handleAmountChange(vm.amount + 1)}
|
||||
/>
|
||||
{hasSubmitted && vm.amountRequiredError ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountRequired')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountExceedsBalance ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
||||
amount: vm.paymentMinimumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
||||
amount: vm.paymentMaximumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</WithdrawField>
|
||||
|
||||
<div className="flex items-start gap-design-14">
|
||||
<div className="w-design-132 shrink-0" />
|
||||
<div className="grid min-w-0 flex-1 grid-cols-3 gap-design-10">
|
||||
{vm.quickAmounts.map((option) => (
|
||||
<QuickAmountCard
|
||||
key={option.id}
|
||||
amount={option.diamonds}
|
||||
preview={option.preview}
|
||||
active={option.id === activeQuickAmountId}
|
||||
onClick={() =>
|
||||
handleQuickAmountSelect(option.id, option.diamonds)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.paymentChannel')}
|
||||
>
|
||||
@@ -516,6 +460,62 @@ function DesktopWithdraw() {
|
||||
</div>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
||||
>
|
||||
<AmountShell
|
||||
amount={vm.amount}
|
||||
availableBalanceText={t(
|
||||
'gameDesktop.withdraw.availableBalance',
|
||||
{ amount: formatNumber(vm.availableBalance) },
|
||||
)}
|
||||
onAmountChange={handleAmountChange}
|
||||
onMinus={() => handleAmountChange(vm.amount - 1)}
|
||||
onPlus={() => handleAmountChange(vm.amount + 1)}
|
||||
/>
|
||||
{hasSubmitted && vm.amountRequiredError ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountRequired')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountExceedsBalance ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
||||
amount: vm.paymentMinimumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
||||
amount: vm.paymentMaximumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</WithdrawField>
|
||||
|
||||
<div className="flex items-start gap-design-14">
|
||||
<div className="w-design-132 shrink-0" />
|
||||
<div className="grid min-w-0 flex-1 grid-cols-3 gap-design-10">
|
||||
{vm.quickAmounts.map((option) => (
|
||||
<QuickAmountCard
|
||||
key={option.id}
|
||||
amount={option.diamonds}
|
||||
preview={option.preview}
|
||||
active={option.id === activeQuickAmountId}
|
||||
onClick={() =>
|
||||
handleQuickAmountSelect(option.id, option.diamonds)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.currencyType')}
|
||||
alignStart={false}
|
||||
|
||||
@@ -380,59 +380,6 @@ function MobileWithdraw() {
|
||||
>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7">
|
||||
<div className="flex flex-col !gap-design-10">
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
||||
>
|
||||
<AmountShell
|
||||
amount={vm.amount}
|
||||
availableBalanceText={t(
|
||||
'gameDesktop.withdraw.availableBalance',
|
||||
{ amount: formatNumber(vm.availableBalance) },
|
||||
)}
|
||||
onAmountChange={handleAmountChange}
|
||||
onMinus={() => handleAmountChange(vm.amount - 1)}
|
||||
onPlus={() => handleAmountChange(vm.amount + 1)}
|
||||
/>
|
||||
{hasSubmitted && vm.amountRequiredError ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountRequired')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountExceedsBalance ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
||||
amount: vm.paymentMinimumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
||||
amount: vm.paymentMaximumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</WithdrawField>
|
||||
|
||||
<div className="grid min-w-0 grid-cols-3 gap-design-5">
|
||||
{vm.quickAmounts.map((option) => (
|
||||
<QuickAmountCard
|
||||
key={option.id}
|
||||
amount={option.diamonds}
|
||||
preview={option.preview}
|
||||
active={option.id === activeQuickAmountId}
|
||||
onClick={() =>
|
||||
handleQuickAmountSelect(option.id, option.diamonds)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.paymentChannel')}
|
||||
>
|
||||
@@ -500,6 +447,59 @@ function MobileWithdraw() {
|
||||
</div>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
||||
>
|
||||
<AmountShell
|
||||
amount={vm.amount}
|
||||
availableBalanceText={t(
|
||||
'gameDesktop.withdraw.availableBalance',
|
||||
{ amount: formatNumber(vm.availableBalance) },
|
||||
)}
|
||||
onAmountChange={handleAmountChange}
|
||||
onMinus={() => handleAmountChange(vm.amount - 1)}
|
||||
onPlus={() => handleAmountChange(vm.amount + 1)}
|
||||
/>
|
||||
{hasSubmitted && vm.amountRequiredError ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountRequired')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountExceedsBalance ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
||||
amount: vm.paymentMinimumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
||||
amount: vm.paymentMaximumAmountLabel,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</WithdrawField>
|
||||
|
||||
<div className="grid min-w-0 grid-cols-3 gap-design-5">
|
||||
{vm.quickAmounts.map((option) => (
|
||||
<QuickAmountCard
|
||||
key={option.id}
|
||||
amount={option.diamonds}
|
||||
preview={option.preview}
|
||||
active={option.id === activeQuickAmountId}
|
||||
onClick={() =>
|
||||
handleQuickAmountSelect(option.id, option.diamonds)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.currencyType')}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
DEFAULT_WITHDRAW_CONFIG,
|
||||
QUICK_FIAT_AMOUNTS,
|
||||
WITHDRAW_EMAIL_PATTERN,
|
||||
WITHDRAW_PHONE_PATTERN,
|
||||
} from '@/constants'
|
||||
@@ -10,22 +9,76 @@ import { useDepositWithdrawConfig } from '@/hooks/use-deposit-withdraw-config'
|
||||
import { useAuthStore } from '@/store'
|
||||
import type { DepositWithdrawConfig } from '@/type'
|
||||
|
||||
const QUICK_WITHDRAW_OPTION_COUNT = 6
|
||||
|
||||
interface WithdrawDiamondBounds {
|
||||
maximumDiamonds: number
|
||||
minimumDiamonds: number
|
||||
}
|
||||
|
||||
function formatNumber(locale: string, value: number) {
|
||||
return new Intl.NumberFormat(locale).format(value)
|
||||
}
|
||||
|
||||
function getInitialWithdrawAmount(
|
||||
function getInitialWithdrawAmount(bounds: WithdrawDiamondBounds | null) {
|
||||
return bounds?.minimumDiamonds ?? 0
|
||||
}
|
||||
|
||||
function getWithdrawDiamondBounds(
|
||||
selectedExchangeRate: number,
|
||||
maxWithdrawAmount: number,
|
||||
) {
|
||||
if (maxWithdrawAmount <= 0) {
|
||||
return 0
|
||||
minimumFiatAmount: number | null,
|
||||
maximumFiatAmount: number | null,
|
||||
): WithdrawDiamondBounds | null {
|
||||
if (selectedExchangeRate <= 0 || maxWithdrawAmount <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Math.min(
|
||||
maxWithdrawAmount,
|
||||
Math.max(1, Math.round(QUICK_FIAT_AMOUNTS[0] / selectedExchangeRate)),
|
||||
const minimumDiamonds = Math.max(
|
||||
1,
|
||||
minimumFiatAmount === null
|
||||
? 1
|
||||
: Math.ceil(minimumFiatAmount / selectedExchangeRate),
|
||||
)
|
||||
const methodMaximumDiamonds =
|
||||
maximumFiatAmount === null
|
||||
? maxWithdrawAmount
|
||||
: Math.floor(maximumFiatAmount / selectedExchangeRate)
|
||||
const maximumDiamonds = Math.min(maxWithdrawAmount, methodMaximumDiamonds)
|
||||
|
||||
if (maximumDiamonds < minimumDiamonds) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
maximumDiamonds,
|
||||
minimumDiamonds,
|
||||
}
|
||||
}
|
||||
|
||||
function getQuickWithdrawAmounts(bounds: WithdrawDiamondBounds) {
|
||||
const { maximumDiamonds, minimumDiamonds } = bounds
|
||||
const availableOptionCount = maximumDiamonds - minimumDiamonds + 1
|
||||
|
||||
if (availableOptionCount <= QUICK_WITHDRAW_OPTION_COUNT) {
|
||||
return Array.from(
|
||||
{ length: availableOptionCount },
|
||||
(_, index) => minimumDiamonds + index,
|
||||
)
|
||||
}
|
||||
|
||||
const amounts = new Set<number>([minimumDiamonds, maximumDiamonds])
|
||||
const span = maximumDiamonds - minimumDiamonds
|
||||
|
||||
for (let index = 1; index < QUICK_WITHDRAW_OPTION_COUNT - 1; index += 1) {
|
||||
amounts.add(
|
||||
Math.round(
|
||||
minimumDiamonds + (span * index) / (QUICK_WITHDRAW_OPTION_COUNT - 1),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return [...amounts].sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
function getCurrencyExchangeRate(
|
||||
@@ -170,6 +223,21 @@ export function useWithdrawVm() {
|
||||
const selectedPaymentMaximumAmount = selectedPaymentMethod
|
||||
? toOptionalNumber(selectedPaymentMethod.maximumAmount)
|
||||
: null
|
||||
const withdrawDiamondBounds = useMemo(
|
||||
() =>
|
||||
getWithdrawDiamondBounds(
|
||||
selectedExchangeRate,
|
||||
maxWithdrawAmount,
|
||||
selectedPaymentMinimumAmount,
|
||||
selectedPaymentMaximumAmount,
|
||||
),
|
||||
[
|
||||
maxWithdrawAmount,
|
||||
selectedExchangeRate,
|
||||
selectedPaymentMaximumAmount,
|
||||
selectedPaymentMinimumAmount,
|
||||
],
|
||||
)
|
||||
const amountBelowPaymentMinimum =
|
||||
amount > 0 &&
|
||||
selectedPaymentMinimumAmount !== null &&
|
||||
@@ -274,13 +342,11 @@ export function useWithdrawVm() {
|
||||
}, [bankCode, sortedBanks])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitializedAmount && selectedExchangeRate > 0) {
|
||||
setAmount(
|
||||
getInitialWithdrawAmount(selectedExchangeRate, maxWithdrawAmount),
|
||||
)
|
||||
if (!hasInitializedAmount && withdrawDiamondBounds) {
|
||||
setAmount(getInitialWithdrawAmount(withdrawDiamondBounds))
|
||||
setHasInitializedAmount(true)
|
||||
}
|
||||
}, [hasInitializedAmount, maxWithdrawAmount, selectedExchangeRate, setAmount])
|
||||
}, [hasInitializedAmount, setAmount, withdrawDiamondBounds])
|
||||
|
||||
useEffect(() => {
|
||||
if (amount > maxWithdrawAmount) {
|
||||
@@ -289,19 +355,26 @@ export function useWithdrawVm() {
|
||||
}, [amount, maxWithdrawAmount, setAmount])
|
||||
|
||||
const quickAmounts = useMemo(() => {
|
||||
if (!selectedCurrency || selectedExchangeRate <= 0) {
|
||||
if (!selectedCurrency || !withdrawDiamondBounds) {
|
||||
return []
|
||||
}
|
||||
|
||||
return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({
|
||||
diamonds: Math.min(
|
||||
maxWithdrawAmount,
|
||||
Math.max(1, Math.round(fiatAmount / selectedExchangeRate)),
|
||||
),
|
||||
id: `quick-${selectedCurrency.code}-${fiatAmount}`,
|
||||
preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`,
|
||||
return getQuickWithdrawAmounts(withdrawDiamondBounds).map((diamonds) => ({
|
||||
diamonds,
|
||||
id: `quick-${paymentChannelCode}-${paymentType}-${selectedCurrency.code}-${diamonds}`,
|
||||
preview: `${selectedCurrency.code} ${formatNumber(
|
||||
locale,
|
||||
diamonds * selectedExchangeRate,
|
||||
)}`,
|
||||
}))
|
||||
}, [locale, maxWithdrawAmount, selectedCurrency, selectedExchangeRate])
|
||||
}, [
|
||||
locale,
|
||||
paymentChannelCode,
|
||||
paymentType,
|
||||
selectedCurrency,
|
||||
selectedExchangeRate,
|
||||
withdrawDiamondBounds,
|
||||
])
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? ''
|
||||
@@ -311,6 +384,7 @@ export function useWithdrawVm() {
|
||||
) ?? null
|
||||
const nextCurrency =
|
||||
getChannelCurrencies(config, nextPaymentChannel)[0] ?? null
|
||||
const nextPaymentMethod = nextPaymentChannel?.methods[0] ?? null
|
||||
const nextExchangeRate = nextCurrency
|
||||
? getCurrencyExchangeRate(
|
||||
config,
|
||||
@@ -318,16 +392,23 @@ export function useWithdrawVm() {
|
||||
nextCurrency.withdrawCoinsPerFiatValue || 1,
|
||||
)
|
||||
: 0
|
||||
|
||||
setAmountState(
|
||||
nextExchangeRate > 0
|
||||
? getInitialWithdrawAmount(nextExchangeRate, maxWithdrawAmount)
|
||||
: 0,
|
||||
const nextWithdrawDiamondBounds = getWithdrawDiamondBounds(
|
||||
nextExchangeRate,
|
||||
maxWithdrawAmount,
|
||||
nextPaymentMethod
|
||||
? toOptionalNumber(nextPaymentMethod.minimumAmount)
|
||||
: null,
|
||||
nextPaymentMethod
|
||||
? toOptionalNumber(nextPaymentMethod.maximumAmount)
|
||||
: null,
|
||||
)
|
||||
setHasInitializedAmount(true)
|
||||
const nextAmount = getInitialWithdrawAmount(nextWithdrawDiamondBounds)
|
||||
|
||||
setAmountState(nextAmount)
|
||||
setHasInitializedAmount(nextAmount > 0)
|
||||
setCurrencyCode(nextCurrency?.code ?? '')
|
||||
setPaymentChannelCode(nextPaymentChannelCode)
|
||||
setPaymentType(nextPaymentChannel?.methods[0]?.code ?? '')
|
||||
setPaymentType(nextPaymentMethod?.code ?? '')
|
||||
setBankCode('')
|
||||
setHolderName('')
|
||||
setBankAccount('')
|
||||
|
||||
Reference in New Issue
Block a user