refactor(game): 重构提现功能中的钻石金额范围计算逻辑

- 移除硬编码的快速提现金额配置,改为动态计算钻石金额边界
- 添加新的辅助函数 getWithdrawDiamondBounds 和 getQuickWithdrawAmounts
- 在桌面和移动端提现组件中调整钻石金额字段的位置
- 更新初始金额设置逻辑以使用新的钻石边界计算
- 优化快速金额选择卡片的ID生成方式,包含支付渠道和类型信息
- 添加对支付方法最小/最大金额限制的支持
This commit is contained in:
JiaJun
2026-07-10 14:21:49 +08:00
parent 20a6fc5175
commit 6fd9862019
3 changed files with 219 additions and 138 deletions

View File

@@ -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 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"> <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 <WithdrawField
label={t('gameDesktop.withdraw.fields.paymentChannel')} label={t('gameDesktop.withdraw.fields.paymentChannel')}
> >
@@ -516,6 +460,62 @@ function DesktopWithdraw() {
</div> </div>
</WithdrawField> </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 <WithdrawField
label={t('gameDesktop.withdraw.fields.currencyType')} label={t('gameDesktop.withdraw.fields.currencyType')}
alignStart={false} alignStart={false}

View File

@@ -380,59 +380,6 @@ function MobileWithdraw() {
> >
<div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7"> <div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7">
<div className="flex flex-col !gap-design-10"> <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 <WithdrawField
label={t('gameDesktop.withdraw.fields.paymentChannel')} label={t('gameDesktop.withdraw.fields.paymentChannel')}
> >
@@ -500,6 +447,59 @@ function MobileWithdraw() {
</div> </div>
</WithdrawField> </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 <WithdrawField
label={t('gameDesktop.withdraw.fields.currencyType')} label={t('gameDesktop.withdraw.fields.currencyType')}
> >

View File

@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
DEFAULT_WITHDRAW_CONFIG, DEFAULT_WITHDRAW_CONFIG,
QUICK_FIAT_AMOUNTS,
WITHDRAW_EMAIL_PATTERN, WITHDRAW_EMAIL_PATTERN,
WITHDRAW_PHONE_PATTERN, WITHDRAW_PHONE_PATTERN,
} from '@/constants' } from '@/constants'
@@ -10,22 +9,76 @@ import { useDepositWithdrawConfig } from '@/hooks/use-deposit-withdraw-config'
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
import type { DepositWithdrawConfig } from '@/type' import type { DepositWithdrawConfig } from '@/type'
const QUICK_WITHDRAW_OPTION_COUNT = 6
interface WithdrawDiamondBounds {
maximumDiamonds: number
minimumDiamonds: number
}
function formatNumber(locale: string, value: number) { function formatNumber(locale: string, value: number) {
return new Intl.NumberFormat(locale).format(value) return new Intl.NumberFormat(locale).format(value)
} }
function getInitialWithdrawAmount( function getInitialWithdrawAmount(bounds: WithdrawDiamondBounds | null) {
selectedExchangeRate: number, return bounds?.minimumDiamonds ?? 0
maxWithdrawAmount: number,
) {
if (maxWithdrawAmount <= 0) {
return 0
} }
return Math.min( function getWithdrawDiamondBounds(
maxWithdrawAmount, selectedExchangeRate: number,
Math.max(1, Math.round(QUICK_FIAT_AMOUNTS[0] / selectedExchangeRate)), maxWithdrawAmount: number,
minimumFiatAmount: number | null,
maximumFiatAmount: number | null,
): WithdrawDiamondBounds | null {
if (selectedExchangeRate <= 0 || maxWithdrawAmount <= 0) {
return null
}
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( function getCurrencyExchangeRate(
@@ -170,6 +223,21 @@ export function useWithdrawVm() {
const selectedPaymentMaximumAmount = selectedPaymentMethod const selectedPaymentMaximumAmount = selectedPaymentMethod
? toOptionalNumber(selectedPaymentMethod.maximumAmount) ? toOptionalNumber(selectedPaymentMethod.maximumAmount)
: null : null
const withdrawDiamondBounds = useMemo(
() =>
getWithdrawDiamondBounds(
selectedExchangeRate,
maxWithdrawAmount,
selectedPaymentMinimumAmount,
selectedPaymentMaximumAmount,
),
[
maxWithdrawAmount,
selectedExchangeRate,
selectedPaymentMaximumAmount,
selectedPaymentMinimumAmount,
],
)
const amountBelowPaymentMinimum = const amountBelowPaymentMinimum =
amount > 0 && amount > 0 &&
selectedPaymentMinimumAmount !== null && selectedPaymentMinimumAmount !== null &&
@@ -274,13 +342,11 @@ export function useWithdrawVm() {
}, [bankCode, sortedBanks]) }, [bankCode, sortedBanks])
useEffect(() => { useEffect(() => {
if (!hasInitializedAmount && selectedExchangeRate > 0) { if (!hasInitializedAmount && withdrawDiamondBounds) {
setAmount( setAmount(getInitialWithdrawAmount(withdrawDiamondBounds))
getInitialWithdrawAmount(selectedExchangeRate, maxWithdrawAmount),
)
setHasInitializedAmount(true) setHasInitializedAmount(true)
} }
}, [hasInitializedAmount, maxWithdrawAmount, selectedExchangeRate, setAmount]) }, [hasInitializedAmount, setAmount, withdrawDiamondBounds])
useEffect(() => { useEffect(() => {
if (amount > maxWithdrawAmount) { if (amount > maxWithdrawAmount) {
@@ -289,19 +355,26 @@ export function useWithdrawVm() {
}, [amount, maxWithdrawAmount, setAmount]) }, [amount, maxWithdrawAmount, setAmount])
const quickAmounts = useMemo(() => { const quickAmounts = useMemo(() => {
if (!selectedCurrency || selectedExchangeRate <= 0) { if (!selectedCurrency || !withdrawDiamondBounds) {
return [] return []
} }
return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({ return getQuickWithdrawAmounts(withdrawDiamondBounds).map((diamonds) => ({
diamonds: Math.min( diamonds,
maxWithdrawAmount, id: `quick-${paymentChannelCode}-${paymentType}-${selectedCurrency.code}-${diamonds}`,
Math.max(1, Math.round(fiatAmount / selectedExchangeRate)), preview: `${selectedCurrency.code} ${formatNumber(
), locale,
id: `quick-${selectedCurrency.code}-${fiatAmount}`, diamonds * selectedExchangeRate,
preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`, )}`,
})) }))
}, [locale, maxWithdrawAmount, selectedCurrency, selectedExchangeRate]) }, [
locale,
paymentChannelCode,
paymentType,
selectedCurrency,
selectedExchangeRate,
withdrawDiamondBounds,
])
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? '' const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? ''
@@ -311,6 +384,7 @@ export function useWithdrawVm() {
) ?? null ) ?? null
const nextCurrency = const nextCurrency =
getChannelCurrencies(config, nextPaymentChannel)[0] ?? null getChannelCurrencies(config, nextPaymentChannel)[0] ?? null
const nextPaymentMethod = nextPaymentChannel?.methods[0] ?? null
const nextExchangeRate = nextCurrency const nextExchangeRate = nextCurrency
? getCurrencyExchangeRate( ? getCurrencyExchangeRate(
config, config,
@@ -318,16 +392,23 @@ export function useWithdrawVm() {
nextCurrency.withdrawCoinsPerFiatValue || 1, nextCurrency.withdrawCoinsPerFiatValue || 1,
) )
: 0 : 0
const nextWithdrawDiamondBounds = getWithdrawDiamondBounds(
setAmountState( nextExchangeRate,
nextExchangeRate > 0 maxWithdrawAmount,
? getInitialWithdrawAmount(nextExchangeRate, maxWithdrawAmount) nextPaymentMethod
: 0, ? toOptionalNumber(nextPaymentMethod.minimumAmount)
: null,
nextPaymentMethod
? toOptionalNumber(nextPaymentMethod.maximumAmount)
: null,
) )
setHasInitializedAmount(true) const nextAmount = getInitialWithdrawAmount(nextWithdrawDiamondBounds)
setAmountState(nextAmount)
setHasInitializedAmount(nextAmount > 0)
setCurrencyCode(nextCurrency?.code ?? '') setCurrencyCode(nextCurrency?.code ?? '')
setPaymentChannelCode(nextPaymentChannelCode) setPaymentChannelCode(nextPaymentChannelCode)
setPaymentType(nextPaymentChannel?.methods[0]?.code ?? '') setPaymentType(nextPaymentMethod?.code ?? '')
setBankCode('') setBankCode('')
setHolderName('') setHolderName('')
setBankAccount('') setBankAccount('')