refactor(game): 重构项目结构,优化链路, 移动端适配

- 移除 useGameBoardVm 数据层实施说明文档
- 移除核心玩法与前端规则摘要文档
- 移除游戏模块数据与界面分层第一阶段实施稿文档
- 清理与数据层重构相关的技术方案说明
- 删除关于 PC 和 Mobile 界面分离的设计规划
- 移除 view-model hooks 架构设计相关内容
This commit is contained in:
JiaJun
2026-06-03 17:21:13 +08:00
parent 3efcb3bba6
commit bfb4b76611
129 changed files with 4534 additions and 4227 deletions

View 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