refactor(game): 重构项目结构,优化链路, 移动端适配
- 移除 useGameBoardVm 数据层实施说明文档 - 移除核心玩法与前端规则摘要文档 - 移除游戏模块数据与界面分层第一阶段实施稿文档 - 清理与数据层重构相关的技术方案说明 - 删除关于 PC 和 Mobile 界面分离的设计规划 - 移除 view-model hooks 架构设计相关内容
This commit is contained in:
312
src/hooks/use-withdraw-vm.ts
Normal file
312
src/hooks/use-withdraw-vm.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user