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 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}

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="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')}
>

View File

@@ -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('')