Files
36-character-flower/src/features/game/entry/entry-page.tsx
JiaJun 68cf8c0be2 feat(auth): 切换注册方式为手机验证码并添加客服功能
- 将注册表单从用户名改为手机号输入
- 集成短信验证码功能,添加验证码输入字段
- 实现发送短信验证码的API调用和倒计时逻辑
- 更新国际化配置以支持验证码相关文案
- 添加桌面端客服聊天弹窗功能
- 调整注册表单UI布局和样式优化
- 修改认证相关常量和类型定义以适配新流程
2026-06-02 11:09:24 +08:00

216 lines
7.3 KiB
TypeScript

import { startTransition, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getGameLobbyInit } from '@/features/game'
import { EntryNoticeGateModal } from '@/features/game/components'
import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx'
import { PcEntry } from '@/features/game/entry/pc-entry.tsx'
import { useGameRealtimeSync } from '@/features/game/hooks/use-game-realtime-sync.ts'
import DesktopAutoSettingModal from '@/features/game/modal/desktop/desktop-auto-setting-modal.tsx'
import DesktopLanguageModal from '@/features/game/modal/desktop/desktop-language-modal.tsx'
import DesktopLoginModal from '@/features/game/modal/desktop/desktop-login-modal.tsx'
import DesktopNoticeModal from '@/features/game/modal/desktop/desktop-notice-modal.tsx'
import { DesktopPeriodHistoryDrawer } from '@/features/game/modal/desktop/desktop-period-history-drawer.tsx'
import DesktopProceduresModal from '@/features/game/modal/desktop/desktop-procedures-modal.tsx'
import DesktopRegisterModal from '@/features/game/modal/desktop/desktop-register-modal.tsx'
import DesktopRulesModal from '@/features/game/modal/desktop/desktop-rules-modal.tsx'
import DesktopSupportModal from '@/features/game/modal/desktop/desktop-support-modal.tsx'
import DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx'
import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx'
import { useDocumentMetadata } from '@/lib/head/document-metadata'
import { notify } from '@/lib/notify'
import { useAuthStore } from '@/store/auth'
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
function EntryModalHost() {
return (
<>
{/* 桌面端登录弹窗:用于未登录用户进入登录流程 */}
<DesktopLoginModal />
{/* 桌面端注册弹窗:用于新用户注册账号 */}
<DesktopRegisterModal />
{/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */}
<DesktopLanguageModal />
{/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */}
<DesktopRulesModal />
{/* 桌面端用户信息弹窗:展示个人资料与站内消息 */}
<DesktopUserInfoModal />
{/* 桌面端公告弹窗:展示活动公告或运营通知内容 */}
<DesktopNoticeModal />
{/* 桌面端自动托管弹窗:配置自动托管相关条件 */}
<DesktopAutoSettingModal />
{/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */}
<DesktopProceduresModal />
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
<DesktopWithdrawTopupModal />
{/* 桌面端客服弹窗:承载在线客服 iframe */}
<DesktopSupportModal />
{/* 强制弹窗 */}
<EntryNoticeGateModal />
{/* 历史开奖信息弹窗 */}
<DesktopPeriodHistoryDrawer />
</>
)
}
export function EntryPage() {
const { t } = useTranslation()
useGameRealtimeSync()
const hydrateRound = useGameRoundStore((state) => state.hydrateRound)
const selectChip = useGameRoundStore((state) => state.selectChip)
const hydrateSession = useGameSessionStore((state) => state.hydrateSession)
const syncConnection = useGameSessionStore((state) => state.syncConnection)
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
const authStatus = useAuthStore((state) => state.status)
const authIsHydrated = useAuthStore((state) => state.isHydrated)
const accessToken = useAuthStore((state) => state.accessToken)
const lastUnauthorizedAt = useAuthStore((state) => state.lastUnauthorizedAt)
const isReloginRequired =
authStatus === 'anonymous' && Boolean(lastUnauthorizedAt)
const [isHydrating, setIsHydrating] = useState(true)
const [isMobile, setIsMobile] = useState(() => {
if (typeof window === 'undefined') {
return false
}
return window.matchMedia('(max-width: 768px)').matches
})
useDocumentMetadata({
title: t('game.metaTitle'),
description: t('game.metaDescription'),
})
useEffect(() => {
if (!authIsHydrated) {
setIsHydrating(true)
return
}
if (isReloginRequired || authStatus !== 'authenticated' || !accessToken) {
setIsHydrating(false)
return
}
let cancelled = false
void getGameLobbyInit()
.then((result) => {
if (cancelled) {
return
}
startTransition(() => {
const snapshot = result.snapshot
hydrateRound({
cells: snapshot.cells,
chips: snapshot.chips,
history: snapshot.history,
maxSelectionCount: snapshot.maxSelectionCount,
round: snapshot.round,
selections: snapshot.selections,
trends: snapshot.trends,
})
const defaultChipId =
snapshot.chips.find((chip) => chip.isDefault)?.id ?? null
if (defaultChipId) {
selectChip(defaultChipId)
}
hydrateSession({
announcements: snapshot.announcements,
connection: snapshot.connection,
dashboard: snapshot.dashboard,
})
const currentUser = useAuthStore.getState().currentUser
if (currentUser) {
setCurrentUser({
...currentUser,
coin: result.userSnapshot.coin,
currentStreak: result.userSnapshot.current_streak,
isJackpot: result.userSnapshot.is_jackpot,
oddsFactor: result.userSnapshot.odds_factor,
streakLevel: result.userSnapshot.streak_level,
})
}
setIsHydrating(false)
})
})
.catch((error) => {
console.error('Failed to load game lobby init', error)
if (!cancelled) {
if (authStatus === 'authenticated') {
notify.error(t('commonUi.toast.lobbyInitFailed'), {
description: error instanceof Error ? error.message : undefined,
})
}
syncConnection({
connectedAt: null,
lastError:
error instanceof Error
? error.message
: 'Failed to load game lobby init',
lastMessageAt: null,
latencyMs: null,
status: 'disconnected',
transport: 'offline',
})
setIsHydrating(false)
}
})
return () => {
cancelled = true
}
}, [
accessToken,
authIsHydrated,
authStatus,
hydrateRound,
hydrateSession,
isReloginRequired,
selectChip,
setCurrentUser,
syncConnection,
t,
])
useEffect(() => {
if (typeof window === 'undefined') {
return
}
const mediaQuery = window.matchMedia('(max-width: 768px)')
const syncLayout = (event?: MediaQueryListEvent) => {
setIsMobile(event?.matches ?? mediaQuery.matches)
}
syncLayout()
mediaQuery.addEventListener('change', syncLayout)
return () => {
mediaQuery.removeEventListener('change', syncLayout)
}
}, [])
return (
<section
aria-busy={isHydrating}
aria-label={t('game.lobbyTitle')}
className="flex min-h-0 flex-1 flex-col"
>
{isMobile ? <MobileEntry /> : <PcEntry />}
<EntryModalHost />
</section>
)
}