refactor(game): 重构项目结构,优化链路, 移动端适配
- 移除 useGameBoardVm 数据层实施说明文档 - 移除核心玩法与前端规则摘要文档 - 移除游戏模块数据与界面分层第一阶段实施稿文档 - 清理与数据层重构相关的技术方案说明 - 删除关于 PC 和 Mobile 界面分离的设计规划 - 移除 view-model hooks 架构设计相关内容
This commit is contained in:
186
src/features/game/components/mobile/mobile-topup.tsx
Normal file
186
src/features/game/components/mobile/mobile-topup.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createDeposit, type DepositTierItem } from '@/api'
|
||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||
import { useDepositTierList } from '@/hooks/use-deposit-tier-list'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const PANEL_CLASS =
|
||||
'rounded-md border border-[rgba(110,229,243,0.24)] bg-[linear-gradient(180deg,rgba(7,30,43,0.9),rgba(3,15,26,0.94))] shadow-[inset_0_0_calc(var(--design-unit)*12)_rgba(88,225,238,0.08)]'
|
||||
|
||||
function formatNumber(value: number) {
|
||||
return new Intl.NumberFormat('en-US').format(value)
|
||||
}
|
||||
|
||||
function MobileTopup() {
|
||||
const { t } = useTranslation()
|
||||
const tierListQuery = useDepositTierList()
|
||||
const tiers = tierListQuery.data ?? []
|
||||
const createDepositInFlightRef = useRef(false)
|
||||
const pendingPayWindowRef = useRef<Window | null>(null)
|
||||
const createDepositMutation = useMutation({
|
||||
mutationFn: ({
|
||||
channelCode,
|
||||
tierId,
|
||||
}: {
|
||||
channelCode: string
|
||||
tierId: string
|
||||
}) =>
|
||||
createDeposit({
|
||||
channel_code: channelCode,
|
||||
idempotency_key: String(Date.now()),
|
||||
tier_id: tierId,
|
||||
}),
|
||||
})
|
||||
|
||||
const handleCreateDeposit = async (tier: DepositTierItem) => {
|
||||
if (createDepositInFlightRef.current || createDepositMutation.isPending) {
|
||||
return
|
||||
}
|
||||
|
||||
const channelCode = tier.payChannelCode ?? tier.channels[0]?.code ?? ''
|
||||
|
||||
if (!channelCode) {
|
||||
notify.error(t('commonUi.toast.requestFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
createDepositInFlightRef.current = true
|
||||
const payWindow = window.open('', '_blank')
|
||||
|
||||
if (!payWindow) {
|
||||
createDepositInFlightRef.current = false
|
||||
notify.error(t('gameDesktop.topup.tier.openPayUrlFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
payWindow.opener = null
|
||||
pendingPayWindowRef.current = payWindow
|
||||
|
||||
try {
|
||||
const result = await createDepositMutation.mutateAsync({
|
||||
channelCode,
|
||||
tierId: tier.id,
|
||||
})
|
||||
const payUrl = result.pay_url.trim()
|
||||
|
||||
if (!payUrl) {
|
||||
payWindow.close()
|
||||
notify.error(t('gameDesktop.topup.tier.missingPayUrl'))
|
||||
return
|
||||
}
|
||||
|
||||
payWindow.location.replace(payUrl)
|
||||
notify.success(t('gameDesktop.topup.tier.createSuccess'))
|
||||
} catch (error) {
|
||||
payWindow.close()
|
||||
notify.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('commonUi.toast.requestFailed'),
|
||||
)
|
||||
} finally {
|
||||
createDepositInFlightRef.current = false
|
||||
|
||||
if (pendingPayWindowRef.current === payWindow) {
|
||||
pendingPayWindowRef.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full px-design-8 pb-design-8 text-[#D9FFFF]">
|
||||
<div
|
||||
className={cn(
|
||||
PANEL_CLASS,
|
||||
'flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden px-design-10 py-design-10',
|
||||
)}
|
||||
>
|
||||
<div className="mb-design-8 flex items-center border-b border-[rgba(89,209,223,0.2)] pb-design-8">
|
||||
<div className="text-design-16 font-semibold text-[#9AF5FB]">
|
||||
{t('gameDesktop.topup.tier.title')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tierListQuery.isLoading ? (
|
||||
<DataLoadingIndicator
|
||||
label={t('gameDesktop.topup.tier.loading')}
|
||||
className="h-full min-h-0 rounded-[calc(var(--design-unit)*6)] border border-[rgba(103,227,239,0.18)] bg-[rgba(6,24,35,0.52)]"
|
||||
/>
|
||||
) : tierListQuery.isError ? (
|
||||
<div className="flex h-full min-h-0 items-center justify-center rounded-[calc(var(--design-unit)*6)] border border-[rgba(185,63,68,0.28)] bg-[rgba(34,13,16,0.42)] px-design-12 text-center text-design-14 text-[#F4A9AE]">
|
||||
{t('gameDesktop.topup.tier.failed')}
|
||||
</div>
|
||||
) : tiers.length === 0 ? (
|
||||
<div className="flex h-full min-h-0 items-center justify-center rounded-[calc(var(--design-unit)*6)] border border-[rgba(103,227,239,0.18)] bg-[rgba(6,24,35,0.52)] px-design-12 text-center text-design-14 text-[#8FDDE6]">
|
||||
{t('gameDesktop.topup.tier.empty')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid min-h-0 min-w-0 grid-cols-2 gap-design-8 overflow-y-auto pr-design-1">
|
||||
{tiers.map((tier) => (
|
||||
<button
|
||||
key={tier.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleCreateDeposit(tier)
|
||||
}}
|
||||
className={cn(
|
||||
'relative min-h-design-126 overflow-hidden rounded-[calc(var(--design-unit)*7)] border border-[rgba(103,227,239,0.24)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] px-design-8 py-design-8 text-left shadow-[0_0_calc(var(--design-unit)*8)_rgba(88,225,238,0.08)] transition-[border-color,box-shadow,filter] duration-150',
|
||||
createDepositMutation.isPending
|
||||
? 'cursor-wait opacity-80'
|
||||
: 'cursor-pointer hover:border-[rgba(170,247,255,0.62)] hover:brightness-110 active:brightness-90',
|
||||
)}
|
||||
>
|
||||
<div className="absolute right-design-6 top-design-6 max-w-design-48 truncate rounded-full border border-[rgba(121,219,229,0.28)] bg-[rgba(10,39,52,0.7)] px-design-5 py-[2px] text-design-8 leading-none text-[#7CDDE7]">
|
||||
{tier.currency ?? 'FIAT'}
|
||||
</div>
|
||||
|
||||
<div className="pr-design-46 text-design-10 uppercase leading-tight tracking-[0.04em] text-[#63AEB6]">
|
||||
{tier.title}
|
||||
</div>
|
||||
|
||||
<div className="pt-design-5 text-design-18 font-semibold leading-none text-[#FFE229]">
|
||||
{formatNumber(tier.payAmount)}
|
||||
</div>
|
||||
<div className="pt-design-3 text-design-10 text-[#9FDCE3]">
|
||||
{t('gameDesktop.topup.tier.coins')}:{' '}
|
||||
{formatNumber(tier.totalAmount)}
|
||||
</div>
|
||||
|
||||
<div className="mt-design-7 rounded-[calc(var(--design-unit)*5)] border border-[rgba(89,209,223,0.18)] bg-[rgba(4,19,28,0.58)] px-design-6 py-design-5">
|
||||
<div className="flex items-center justify-between gap-design-6 text-design-10">
|
||||
<span className="text-[#7CE3E8]">
|
||||
{t('gameDesktop.topup.tier.bonus')}
|
||||
</span>
|
||||
<span className="text-[#FFF1C9]">
|
||||
{formatNumber(tier.bonusAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-design-3 flex items-center justify-between gap-design-6 text-design-10">
|
||||
<span className="text-[#7CE3E8]">Channels</span>
|
||||
<span className="line-clamp-1 text-right text-[#6DFF83]">
|
||||
{tier.channels.length > 0
|
||||
? tier.channels
|
||||
.map((channel) => channel.name)
|
||||
.join(', ')
|
||||
: '--'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{tier.desc ? (
|
||||
<div className="mt-design-5 line-clamp-2 text-design-9 leading-[1.25] text-[#6DAAB0]">
|
||||
{tier.desc}
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileTopup
|
||||
Reference in New Issue
Block a user