Files
36-character-flower/src/hooks/use-withdraw-vm.ts
JiaJun bfb4b76611 refactor(game): 重构项目结构,优化链路, 移动端适配
- 移除 useGameBoardVm 数据层实施说明文档
- 移除核心玩法与前端规则摘要文档
- 移除游戏模块数据与界面分层第一阶段实施稿文档
- 清理与数据层重构相关的技术方案说明
- 删除关于 PC 和 Mobile 界面分离的设计规划
- 移除 view-model hooks 架构设计相关内容
2026-06-03 17:21:13 +08:00

313 lines
8.9 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { DepositWithdrawConfig } from '@/api'
import {
DEFAULT_CURRENCY_CODE,
DEFAULT_WITHDRAW_CONFIG,
QUICK_FIAT_AMOUNTS,
WITHDRAW_EMAIL_PATTERN,
WITHDRAW_PHONE_PATTERN,
} from '@/constants'
import { useDepositWithdrawConfig } from '@/hooks/use-deposit-withdraw-config'
import { useAuthStore } from '@/store'
function formatNumber(locale: string, value: number) {
return new Intl.NumberFormat(locale).format(value)
}
function getInitialWithdrawAmount(
selectedRate: number,
maxWithdrawAmount: number,
) {
if (maxWithdrawAmount <= 0) {
return 0
}
return Math.min(
maxWithdrawAmount,
Math.max(1, Math.round(selectedRate * QUICK_FIAT_AMOUNTS[0])),
)
}
function getActiveCurrencyCode(
currencies: DepositWithdrawConfig['currencies'],
selectedCurrencyCode: string,
) {
return (
currencies.find((item) => item.code === selectedCurrencyCode) ??
currencies[0] ??
DEFAULT_WITHDRAW_CONFIG.currencies[0]
)
}
function getNormalizedConfig(
config: DepositWithdrawConfig | undefined,
fallback: DepositWithdrawConfig,
) {
return config ?? fallback
}
function isValidEmail(value: string) {
if (value.trim().length === 0) {
return false
}
return WITHDRAW_EMAIL_PATTERN.test(value.trim())
}
function isValidPhone(value: string) {
const normalized = value.replace(/[^\d+]/g, '')
if (normalized.length === 0) {
return false
}
return WITHDRAW_PHONE_PATTERN.test(normalized)
}
export function useWithdrawVm() {
const { i18n, t } = useTranslation()
const currentUser = useAuthStore((state) => state.currentUser)
const withdrawConfigQuery = useDepositWithdrawConfig()
const config = useMemo(() => {
const baseConfig = getNormalizedConfig(
withdrawConfigQuery.data,
DEFAULT_WITHDRAW_CONFIG,
)
return {
...baseConfig,
currencies:
baseConfig.currencies.length > 0
? baseConfig.currencies
: DEFAULT_WITHDRAW_CONFIG.currencies,
payChannels: baseConfig.payChannels,
withdraw: {
...baseConfig.withdraw,
banks: baseConfig.withdraw.banks,
},
}
}, [withdrawConfigQuery.data])
const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US'
const [amount, setAmountState] = useState(0)
const [hasInitializedAmount, setHasInitializedAmount] = useState(false)
const [currencyCode, setCurrencyCode] = useState(
config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE,
)
const [paymentChannelCode, setPaymentChannelCode] = useState('')
const [bankCode, setBankCode] = useState('')
const [holderName, setHolderName] = useState('')
const [bankAccount, setBankAccount] = useState('')
const [receiverEmail, setReceiverEmail] = useState('')
const [receiverPhone, setReceiverPhone] = useState('')
const selectedCurrency = getActiveCurrencyCode(
config.currencies,
currencyCode,
)
const selectedRate = selectedCurrency.withdrawCoinsPerFiatValue || 1
const sortedPayChannels = useMemo(
() =>
[...config.payChannels]
.filter((channel) => channel.status === 1)
.sort((left, right) => left.sort - right.sort),
[config.payChannels],
)
const sortedBanks = useMemo(
() =>
[...config.withdraw.banks]
.filter((bank) => bank.status === 1)
.sort((left, right) => left.sort - right.sort),
[config.withdraw.banks],
)
const availableBalance = Number(currentUser?.coin ?? 0)
const maxWithdrawAmount = Math.max(0, Math.floor(availableBalance))
const selectedPaymentChannel =
sortedPayChannels.find((channel) => channel.code === paymentChannelCode) ??
null
const setAmount = useCallback(
(nextAmount: number) => {
setAmountState(
Math.min(maxWithdrawAmount, Math.max(0, Math.floor(nextAmount))),
)
},
[maxWithdrawAmount],
)
useEffect(() => {
if (
selectedCurrency &&
selectedCurrency.code !== currencyCode &&
config.currencies.some((item) => item.code === currencyCode)
) {
return
}
if (selectedCurrency && selectedCurrency.code !== currencyCode) {
setCurrencyCode(selectedCurrency.code)
}
}, [config.currencies, currencyCode, selectedCurrency])
useEffect(() => {
const firstAvailablePayChannel = sortedPayChannels[0]
if (!firstAvailablePayChannel) {
if (paymentChannelCode) {
setPaymentChannelCode('')
}
return
}
const hasSelectedAvailablePayChannel = sortedPayChannels.some(
(channel) => channel.code === paymentChannelCode,
)
if (!hasSelectedAvailablePayChannel) {
setPaymentChannelCode(firstAvailablePayChannel.code)
}
}, [paymentChannelCode, sortedPayChannels])
useEffect(() => {
if (sortedBanks.length === 0) {
if (bankCode) {
setBankCode('')
}
return
}
const hasSelectedAvailableBank = sortedBanks.some(
(bank) => bank.code === bankCode,
)
if (!hasSelectedAvailableBank) {
setBankCode('')
}
}, [bankCode, sortedBanks])
useEffect(() => {
if (!hasInitializedAmount && selectedRate > 0) {
setAmount(getInitialWithdrawAmount(selectedRate, maxWithdrawAmount))
setHasInitializedAmount(true)
}
}, [hasInitializedAmount, maxWithdrawAmount, selectedRate, setAmount])
useEffect(() => {
if (amount > maxWithdrawAmount) {
setAmount(maxWithdrawAmount)
}
}, [amount, maxWithdrawAmount, setAmount])
const quickAmounts = useMemo(() => {
return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({
diamonds: Math.min(
maxWithdrawAmount,
Math.max(1, Math.round(selectedRate * fiatAmount)),
),
id: `quick-${selectedCurrency.code}-${fiatAmount}`,
preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`,
}))
}, [locale, maxWithdrawAmount, selectedCurrency.code, selectedRate])
const resetForm = useCallback(() => {
const nextCurrencyCode = config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE
const nextCurrency = getActiveCurrencyCode(
config.currencies,
nextCurrencyCode,
)
const nextRate = nextCurrency.withdrawCoinsPerFiatValue || 1
setAmountState(getInitialWithdrawAmount(nextRate, maxWithdrawAmount))
setHasInitializedAmount(true)
setCurrencyCode(nextCurrencyCode)
setPaymentChannelCode(sortedPayChannels[0]?.code ?? '')
setBankCode('')
setHolderName('')
setBankAccount('')
setReceiverEmail('')
setReceiverPhone('')
}, [config.currencies, maxWithdrawAmount, sortedPayChannels])
const selectedCurrencyPreview = useMemo(
() => ({
currencyCode: selectedCurrency.code,
currencyLabel: selectedCurrency.label,
exchangeRateLabel: t('gameDesktop.withdraw.preview.exchangeRate', {
currency: selectedCurrency.code,
}),
exchangeRateValue: t('gameDesktop.withdraw.preview.exchangeRateValue', {
coins: formatNumber(locale, selectedRate),
currency: selectedCurrency.code,
platformCoinLabel: config.platformCoinLabel,
}),
convertibleLabel: t('gameDesktop.withdraw.preview.convertible', {
currency: selectedCurrency.code,
}),
convertibleValue: `${formatNumber(
locale,
selectedRate > 0 ? amount / selectedRate : 0,
)} ${selectedCurrency.code}`,
}),
[
amount,
config.platformCoinLabel,
locale,
selectedCurrency.code,
selectedCurrency.label,
selectedRate,
t,
],
)
return {
amount,
amountExceedsBalance: amount > maxWithdrawAmount,
amountRequiredError: amount <= 0,
availableBalance,
bankAccount,
bankAccountError: bankAccount.trim().length === 0,
bankCode,
bankCodeError: bankCode.trim().length === 0,
config,
currencyCode,
holderName,
holderNameError: holderName.trim().length === 0,
isLoading: withdrawConfigQuery.isLoading,
isRefetching: withdrawConfigQuery.isFetching,
maxWithdrawAmount,
paymentChannelCode,
paymentChannelCodeError: paymentChannelCode.trim().length === 0,
quickAmounts,
receiverEmail,
receiverEmailError: !isValidEmail(receiverEmail),
receiverPhone,
receiverPhoneError: !isValidPhone(receiverPhone),
selectedCurrency,
selectedCurrencyPreview,
selectedPaymentChannel,
selectedRate,
resetForm,
setAmount,
setBankAccount,
setBankCode,
setCurrencyCode,
setHolderName,
setPaymentChannelCode,
setReceiverEmail,
setReceiverPhone,
sortedBanks,
sortedPayChannels,
withdrawCopy: {
bankLabel: t('gameDesktop.withdraw.bank'),
eWalletLabel: t('gameDesktop.withdraw.eWallet'),
feeNote: config.withdraw.feeNote,
noticeLabel: t('gameDesktop.withdraw.notice'),
processingLabel: t('gameDesktop.withdraw.processingTime'),
processingValue: config.withdraw.processingNote,
rateHint: config.withdraw.rateHint,
},
}
}