feat: 优化整体项目ui
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import {
|
||||
FALLBACK_POLL_INTERVAL_MS,
|
||||
GAME_SOCKET_TOPIC_VALUES,
|
||||
GAME_SOCKET_TOPICS,
|
||||
PLAYER_SOCKET_TOPICS,
|
||||
SOCKET_DISCONNECT_DELAY_MS,
|
||||
} from '@/constants'
|
||||
import i18n from '@/i18n'
|
||||
import { prefetchAuthToken } from '@/lib/api/api-client'
|
||||
import {
|
||||
@@ -16,51 +23,11 @@ type UserStreakMessageData = {
|
||||
streakLevel?: number
|
||||
}
|
||||
|
||||
const FALLBACK_POLL_INTERVAL_MS = 10_000
|
||||
const GAME_SOCKET_TOPICS = {
|
||||
// 对局状态心跳。每秒推送当前期号、状态、倒计时、runtime_enabled 等。
|
||||
periodTick: 'period.tick',
|
||||
// 本期封盘通知。用于前端立即停止下注。
|
||||
periodLocked: 'period.locked',
|
||||
// 本期开奖通知。用于同步开奖号码、所属期号等阶段结果。
|
||||
periodOpened: 'period.opened',
|
||||
// 本期派彩完成通知。用于结算阶段同步。
|
||||
periodPayout: 'period.payout',
|
||||
// 当前玩家连胜与赔率信息。通常在结算后或演示帧刷新。
|
||||
userStreak: 'user.streak',
|
||||
// 下注成功通知。仅当前用户可见,通常伴随扣款结果。
|
||||
betAccepted: 'bet.accepted',
|
||||
// 余额变化通知。充值、下注、派彩都会走这条流。
|
||||
walletChanged: 'wallet.changed',
|
||||
// 自动托管进度通知。包含托管开关、执行状态等。
|
||||
autoSpinProgress: 'auto.spin.progress',
|
||||
// 大奖命中通知。仅当本期存在中大奖用户时推送。
|
||||
jackpotHit: 'jackpot.hit',
|
||||
// 后台实时页全量快照。仅 admin live 页面使用,当前 H5 前台不订阅。
|
||||
adminLiveSnapshot: 'admin.live.snapshot',
|
||||
// 后台开奖结果通知。仅 admin live 页面使用,当前 H5 前台不订阅。
|
||||
adminLiveOpened: 'admin.live.opened',
|
||||
} as const
|
||||
|
||||
const GAME_SOCKET_TOPIC_VALUES = new Set<string>(
|
||||
Object.values(GAME_SOCKET_TOPICS),
|
||||
)
|
||||
|
||||
// 当前 H5 游戏页实际需要的用户侧事件。
|
||||
// 后台专用事件保持在 GAME_SOCKET_TOPICS 中做口径对齐,但不在这里订阅。
|
||||
const PLAYER_SOCKET_TOPICS = [
|
||||
GAME_SOCKET_TOPICS.periodTick,
|
||||
GAME_SOCKET_TOPICS.userStreak,
|
||||
GAME_SOCKET_TOPICS.periodOpened,
|
||||
GAME_SOCKET_TOPICS.periodLocked,
|
||||
GAME_SOCKET_TOPICS.periodPayout,
|
||||
GAME_SOCKET_TOPICS.betAccepted,
|
||||
GAME_SOCKET_TOPICS.walletChanged,
|
||||
GAME_SOCKET_TOPICS.autoSpinProgress,
|
||||
GAME_SOCKET_TOPICS.jackpotHit,
|
||||
] as const
|
||||
|
||||
const SOCKET_DISCONNECT_DELAY_MS = 150
|
||||
type PeriodEventData = {
|
||||
openTime: number | null
|
||||
periodNo: string
|
||||
resultNumber: number | null
|
||||
}
|
||||
|
||||
let sharedSocketClient: GameSocketClient | null = null
|
||||
let sharedSocketKey: string | null = null
|
||||
@@ -186,6 +153,37 @@ function extractPeriodTick(
|
||||
}
|
||||
}
|
||||
|
||||
function extractPeriodEventData(
|
||||
message: GameSocketMessage,
|
||||
): PeriodEventData | null {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const source = data ?? (message as Record<string, unknown>)
|
||||
const periodNo =
|
||||
typeof source.period_no === 'string'
|
||||
? source.period_no
|
||||
: typeof source.periodNo === 'string'
|
||||
? source.periodNo
|
||||
: null
|
||||
|
||||
if (!periodNo) {
|
||||
return null
|
||||
}
|
||||
|
||||
const resultNumber = toOptionalNumber(
|
||||
source.result_number ?? source.resultNumber,
|
||||
)
|
||||
const openTime = toOptionalNumber(source.open_time ?? source.openTime)
|
||||
|
||||
return {
|
||||
openTime: openTime ?? null,
|
||||
periodNo,
|
||||
resultNumber:
|
||||
typeof resultNumber === 'number' && Number.isInteger(resultNumber)
|
||||
? resultNumber
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
function extractWalletCoin(message: GameSocketMessage) {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const source = data ?? (message as Record<string, unknown>)
|
||||
@@ -288,6 +286,90 @@ function applyPeriodPhase(phase: 'locked' | 'revealing' | 'settled') {
|
||||
useGameRoundStore.getState().setPhase(phase)
|
||||
}
|
||||
|
||||
function applyPeriodLockedMessage(
|
||||
message: GameSocketMessage,
|
||||
serverTime: number | null,
|
||||
) {
|
||||
applyPeriodMessage(message, serverTime)
|
||||
|
||||
const period = extractPeriodEventData(message)
|
||||
const roundState = useGameRoundStore.getState()
|
||||
const roundId = period?.periodNo ?? roundState.round.id
|
||||
|
||||
if (roundId) {
|
||||
roundState.syncRound({
|
||||
id: roundId,
|
||||
phase: 'locked',
|
||||
})
|
||||
} else {
|
||||
roundState.setPhase('locked')
|
||||
}
|
||||
}
|
||||
|
||||
function applyPeriodOpenedMessage(
|
||||
message: GameSocketMessage,
|
||||
serverTime: number | null,
|
||||
) {
|
||||
applyPeriodMessage(message, serverTime)
|
||||
|
||||
const period = extractPeriodEventData(message)
|
||||
|
||||
if (!period || period.resultNumber === null) {
|
||||
applyPeriodPhase('revealing')
|
||||
return
|
||||
}
|
||||
|
||||
const roundState = useGameRoundStore.getState()
|
||||
const openedAt = toIsoFromUnixSeconds(
|
||||
period.openTime ?? serverTime ?? Math.floor(Date.now() / 1000),
|
||||
)
|
||||
const hasSmallReward = roundState.selections.some(
|
||||
(selection) => selection.cellId === period.resultNumber,
|
||||
)
|
||||
const revealKey = `${period.periodNo}:${period.resultNumber}`
|
||||
|
||||
roundState.syncRound({
|
||||
id: period.periodNo,
|
||||
phase: 'revealing',
|
||||
revealingAt: openedAt,
|
||||
winningCellId: period.resultNumber,
|
||||
})
|
||||
useGameRoundStore.getState().prepareRevealAnimation({
|
||||
hasSmallReward,
|
||||
revealKey,
|
||||
roundId: period.periodNo,
|
||||
winningCellId: period.resultNumber,
|
||||
})
|
||||
}
|
||||
|
||||
function applyPeriodPayoutMessage(
|
||||
message: GameSocketMessage,
|
||||
serverTime: number | null,
|
||||
) {
|
||||
applyPeriodMessage(message, serverTime)
|
||||
|
||||
const period = extractPeriodEventData(message)
|
||||
|
||||
if (period?.resultNumber !== null && period?.resultNumber !== undefined) {
|
||||
const roundState = useGameRoundStore.getState()
|
||||
const revealKey = `${period.periodNo}:${period.resultNumber}`
|
||||
|
||||
roundState.prepareRevealAnimation({
|
||||
hasSmallReward: roundState.selections.some(
|
||||
(selection) => selection.cellId === period.resultNumber,
|
||||
),
|
||||
revealKey,
|
||||
roundId: period.periodNo,
|
||||
winningCellId: period.resultNumber,
|
||||
})
|
||||
}
|
||||
|
||||
const roundId = period?.periodNo ?? useGameRoundStore.getState().round.id
|
||||
|
||||
applyPeriodPhase('settled')
|
||||
useGameRoundStore.getState().playPreparedRevealAnimation(roundId || null)
|
||||
}
|
||||
|
||||
function applyUserStreakMessage(message: GameSocketMessage) {
|
||||
const streakData = extractUserStreakMessageData(message)
|
||||
const currentUser = useAuthStore.getState().currentUser
|
||||
@@ -320,6 +402,8 @@ function applyWalletChangedMessage(message: GameSocketMessage) {
|
||||
|
||||
function applyJackpotHitMessage(message: GameSocketMessage) {
|
||||
const currentUser = useAuthStore.getState().currentUser
|
||||
const period = extractPeriodEventData(message)
|
||||
const isJackpot = extractJackpotStatus(message)
|
||||
|
||||
if (!currentUser) {
|
||||
return
|
||||
@@ -327,8 +411,12 @@ function applyJackpotHitMessage(message: GameSocketMessage) {
|
||||
|
||||
useAuthStore.getState().setCurrentUser({
|
||||
...currentUser,
|
||||
isJackpot: extractJackpotStatus(message),
|
||||
isJackpot,
|
||||
})
|
||||
|
||||
if (isJackpot) {
|
||||
useGameRoundStore.getState().showJackpotReward(period?.periodNo ?? null)
|
||||
}
|
||||
}
|
||||
|
||||
function applyRealtimeMessage(message: GameSocketMessage) {
|
||||
@@ -336,20 +424,41 @@ function applyRealtimeMessage(message: GameSocketMessage) {
|
||||
const topic = getMessageTopic(message)
|
||||
|
||||
switch (topic) {
|
||||
case GAME_SOCKET_TOPICS.periodTick:
|
||||
case GAME_SOCKET_TOPICS.periodTick: {
|
||||
const period = extractPeriodTick(message)
|
||||
const resultNumber =
|
||||
typeof period?.result_number === 'number' ? period.result_number : null
|
||||
const shouldStartSettledReveal =
|
||||
period?.status === 'settled' && resultNumber !== null
|
||||
const hasSmallReward = shouldStartSettledReveal
|
||||
? useGameRoundStore
|
||||
.getState()
|
||||
.selections.some((selection) => selection.cellId === resultNumber)
|
||||
: false
|
||||
|
||||
applyPeriodMessage(message, serverTime)
|
||||
|
||||
if (shouldStartSettledReveal) {
|
||||
useGameRoundStore.getState().prepareRevealAnimation({
|
||||
hasSmallReward,
|
||||
revealKey: `${period.period_no}:${resultNumber}`,
|
||||
roundId: period.period_no,
|
||||
winningCellId: resultNumber,
|
||||
})
|
||||
useGameRoundStore
|
||||
.getState()
|
||||
.playPreparedRevealAnimation(period.period_no)
|
||||
}
|
||||
break
|
||||
}
|
||||
case GAME_SOCKET_TOPICS.periodLocked:
|
||||
applyPeriodMessage(message, serverTime)
|
||||
applyPeriodPhase('locked')
|
||||
applyPeriodLockedMessage(message, serverTime)
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.periodOpened:
|
||||
applyPeriodMessage(message, serverTime)
|
||||
applyPeriodPhase('revealing')
|
||||
applyPeriodOpenedMessage(message, serverTime)
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.periodPayout:
|
||||
applyPeriodMessage(message, serverTime)
|
||||
applyPeriodPhase('settled')
|
||||
applyPeriodPayoutMessage(message, serverTime)
|
||||
break
|
||||
case GAME_SOCKET_TOPICS.userStreak:
|
||||
applyUserStreakMessage(message)
|
||||
|
||||
Reference in New Issue
Block a user