refactor(game): 优化游戏组件并实现国际化支持
- 在AppBootResourceGate组件中集成react-i18next实现资源加载文本的国际化 - 修改DesktopAnimal组件中的loading dots key以提高渲染性能 - 在DesktopControl组件中添加useRef和useEffect钩子管理定时器清理逻辑 - 将DesktopTitle组件重构为MessageBroadcast组件并增强其响应式设计 - 更新DesktopSupportModal组件中的客户服务文本为国际化格式 - 在AuthSession模块中实现本地存储数据清理时保留关键偏好设置 - 调整多个游戏组件的样式类以改进移动端适配效果 - 移除未使用的桌面提取功能相关代码文件 - 更新GitNexus索引统计数据反映最新的代码变更
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { LOGIN_PROMPT_DEDUP_MS } from '@/constants'
|
||||
import {
|
||||
APP_PREFERENCES_STORAGE_KEY,
|
||||
AUDIO_PREFERENCES_STORAGE_KEY,
|
||||
LOGIN_PROMPT_DEDUP_MS,
|
||||
} from '@/constants'
|
||||
import i18n from '@/i18n'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { queryClient } from '@/lib/query/query-client'
|
||||
@@ -19,9 +23,24 @@ let authInitializationPromise: Promise<void> | null = null
|
||||
let refreshSessionPromise: Promise<boolean> | null = null
|
||||
let lastLoginPromptAt = 0
|
||||
|
||||
const PRESERVED_LOCAL_STORAGE_KEYS = [
|
||||
APP_PREFERENCES_STORAGE_KEY,
|
||||
AUDIO_PREFERENCES_STORAGE_KEY,
|
||||
]
|
||||
|
||||
function clearBrowserStorageData() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const preserved = PRESERVED_LOCAL_STORAGE_KEYS.map(
|
||||
(key) => [key, localStorage.getItem(key)] as const,
|
||||
)
|
||||
|
||||
localStorage.clear()
|
||||
|
||||
for (const [key, value] of preserved) {
|
||||
if (value !== null) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
|
||||
359
src/lib/ws/message-parsers.ts
Normal file
359
src/lib/ws/message-parsers.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import { GAME_SOCKET_TOPIC_VALUES } from '@/constants'
|
||||
import type { GameSocketMessage } from '@/lib/ws/game-socket-client'
|
||||
import type {
|
||||
BetWinEventDataDto,
|
||||
GamePeriodTickDto,
|
||||
JackpotHitEventDataDto,
|
||||
JackpotHitItemDto,
|
||||
PeriodEventData,
|
||||
UserStreakMessageData,
|
||||
WalletChangedData,
|
||||
} from '@/type'
|
||||
|
||||
export function toIsoFromUnixSeconds(seconds: number) {
|
||||
return new Date(seconds * 1000).toISOString()
|
||||
}
|
||||
|
||||
export function toOptionalNumber(value: unknown) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : undefined
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value)
|
||||
|
||||
return Number.isFinite(parsed) ? parsed : undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function toOptionalString(value: unknown) {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function toOptionalBoolean(value: unknown) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (value === 1 || value === '1' || value === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value === 0 || value === '0' || value === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getNestedRecord(
|
||||
value: unknown,
|
||||
key: string,
|
||||
): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const nested = (value as Record<string, unknown>)[key]
|
||||
|
||||
return nested && typeof nested === 'object'
|
||||
? (nested as Record<string, unknown>)
|
||||
: null
|
||||
}
|
||||
|
||||
export function getMessageTopic(message: GameSocketMessage) {
|
||||
const root = message as Record<string, unknown>
|
||||
const event = typeof root.event === 'string' ? root.event : null
|
||||
const topic = typeof root.topic === 'string' ? root.topic : null
|
||||
|
||||
if (event && GAME_SOCKET_TOPIC_VALUES.has(event)) {
|
||||
return event
|
||||
}
|
||||
|
||||
if (topic && GAME_SOCKET_TOPIC_VALUES.has(topic)) {
|
||||
return topic
|
||||
}
|
||||
|
||||
return event ?? topic
|
||||
}
|
||||
|
||||
export function extractServerTime(message: GameSocketMessage) {
|
||||
const root = message as Record<string, unknown>
|
||||
|
||||
if (typeof root.server_time === 'number') {
|
||||
return root.server_time
|
||||
}
|
||||
|
||||
const data = getNestedRecord(message, 'data')
|
||||
|
||||
return typeof data?.server_time === 'number' ? data.server_time : null
|
||||
}
|
||||
|
||||
export function extractUserStreakMessageData(
|
||||
message: GameSocketMessage,
|
||||
): UserStreakMessageData | null {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const direct = getNestedRecord(message, 'user_snapshot')
|
||||
const nested = getNestedRecord(data, 'user_snapshot')
|
||||
const source =
|
||||
data && 'current_streak' in data ? data : (nested ?? direct ?? data)
|
||||
|
||||
if (!source || typeof source.current_streak !== 'number') {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
currentStreak: source.current_streak,
|
||||
oddsFactor: toOptionalNumber(source.odds_factor),
|
||||
streakLevel: toOptionalNumber(source.streak_level),
|
||||
}
|
||||
}
|
||||
|
||||
export function extractPeriodTick(
|
||||
message: GameSocketMessage,
|
||||
): GamePeriodTickDto | null {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const nested = getNestedRecord(data, 'period')
|
||||
const source = nested ?? data
|
||||
|
||||
if (
|
||||
!source ||
|
||||
typeof source.period_no !== 'string' ||
|
||||
typeof source.status !== 'string' ||
|
||||
typeof source.countdown !== 'number' ||
|
||||
typeof source.bet_close_in !== 'number'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
bet_close_in: source.bet_close_in,
|
||||
countdown: source.countdown,
|
||||
period_id: typeof source.period_id === 'number' ? source.period_id : null,
|
||||
period_no: source.period_no,
|
||||
result_number:
|
||||
typeof source.result_number === 'number' ? source.result_number : null,
|
||||
runtime_enabled:
|
||||
typeof source.runtime_enabled === 'boolean'
|
||||
? source.runtime_enabled
|
||||
: true,
|
||||
server_time:
|
||||
typeof source.server_time === 'number'
|
||||
? source.server_time
|
||||
: Math.floor(Date.now() / 1000),
|
||||
status: source.status as GamePeriodTickDto['status'],
|
||||
}
|
||||
}
|
||||
|
||||
export 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,
|
||||
}
|
||||
}
|
||||
|
||||
export function extractWalletChangedData(
|
||||
message: GameSocketMessage,
|
||||
): WalletChangedData | null {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
const source = data ?? (message as Record<string, unknown>)
|
||||
const coin = source.coin ?? source.balance ?? source.balance_after
|
||||
const normalizedCoin =
|
||||
typeof coin === 'string'
|
||||
? coin
|
||||
: typeof coin === 'number' && Number.isFinite(coin)
|
||||
? String(coin)
|
||||
: null
|
||||
|
||||
if (normalizedCoin === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
coin: normalizedCoin,
|
||||
}
|
||||
}
|
||||
|
||||
function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const source = value as Record<string, unknown>
|
||||
|
||||
if (
|
||||
typeof source.nickname !== 'string' ||
|
||||
typeof source.period_no !== 'string' ||
|
||||
typeof source.total_win !== 'string'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const resultNumber = toOptionalNumber(source.result_number)
|
||||
|
||||
if (typeof resultNumber !== 'number' || !Number.isInteger(resultNumber)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
nickname: source.nickname,
|
||||
period_no: source.period_no,
|
||||
result_number: resultNumber,
|
||||
total_win: source.total_win,
|
||||
}
|
||||
}
|
||||
|
||||
export function extractJackpotHitData(
|
||||
message: GameSocketMessage,
|
||||
): JackpotHitEventDataDto | null {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
|
||||
if (!data || typeof data.period_no !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestedHits = data.hits
|
||||
const sourceHits = Array.isArray(nestedHits)
|
||||
? nestedHits
|
||||
: nestedHits && typeof nestedHits === 'object'
|
||||
? [nestedHits]
|
||||
: [data]
|
||||
const firstHitSource = sourceHits.find(
|
||||
(item): item is Record<string, unknown> =>
|
||||
Boolean(item) && typeof item === 'object',
|
||||
)
|
||||
const hits = sourceHits
|
||||
.map((item) => extractJackpotHitItem(item))
|
||||
.filter((item): item is JackpotHitItemDto => item !== null)
|
||||
const root = message as Record<string, unknown>
|
||||
const serverTime = toOptionalNumber(
|
||||
data.server_time ?? firstHitSource?.server_time ?? root.server_time,
|
||||
)
|
||||
const resultNumber = toOptionalNumber(
|
||||
data.result_number ?? data['result number'],
|
||||
)
|
||||
|
||||
if (typeof serverTime !== 'number') {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
hits,
|
||||
period_id:
|
||||
typeof data.period_id === 'number' && Number.isInteger(data.period_id)
|
||||
? data.period_id
|
||||
: null,
|
||||
period_no: data.period_no,
|
||||
result_number:
|
||||
typeof resultNumber === 'number' && Number.isInteger(resultNumber)
|
||||
? resultNumber
|
||||
: null,
|
||||
server_time: serverTime,
|
||||
}
|
||||
}
|
||||
|
||||
export function extractBetWinData(
|
||||
message: GameSocketMessage,
|
||||
): BetWinEventDataDto | null {
|
||||
const data = getNestedRecord(message, 'data')
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const root = message as Record<string, unknown>
|
||||
const userId = toOptionalNumber(data.user_id)
|
||||
const periodId = toOptionalNumber(data.period_id)
|
||||
const resultNumber = toOptionalNumber(data.result_number)
|
||||
const totalWin = toOptionalString(data.total_win)
|
||||
const balanceAfter = toOptionalString(data.balance_after)
|
||||
const serverTime = toOptionalNumber(data.server_time ?? root.server_time)
|
||||
const currentStreak = toOptionalNumber(data.current_streak)
|
||||
const streakLevel = toOptionalNumber(data.streak_level)
|
||||
const oddsFactor = toOptionalNumber(data.odds_factor)
|
||||
const isJackpot = toOptionalBoolean(data.is_jackpot)
|
||||
|
||||
if (
|
||||
typeof totalWin !== 'string' ||
|
||||
typeof data.period_no !== 'string' ||
|
||||
typeof isJackpot !== 'boolean'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
balance_after: balanceAfter,
|
||||
bets: Array.isArray(data.bets)
|
||||
? data.bets
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const bet = item as Record<string, unknown>
|
||||
const betId = toOptionalNumber(bet.bet_id)
|
||||
const winAmount = toOptionalString(bet.win_amount)
|
||||
|
||||
return typeof betId === 'number' && typeof winAmount === 'string'
|
||||
? {
|
||||
bet_id: betId,
|
||||
win_amount: winAmount,
|
||||
}
|
||||
: null
|
||||
})
|
||||
.filter(
|
||||
(item): item is BetWinEventDataDto['bets'][number] => item !== null,
|
||||
)
|
||||
: [],
|
||||
current_streak: currentStreak,
|
||||
is_jackpot: isJackpot,
|
||||
is_win: toOptionalBoolean(data.is_win) ?? true,
|
||||
odds_factor: typeof oddsFactor === 'number' ? oddsFactor : undefined,
|
||||
payout_pending_review:
|
||||
toOptionalBoolean(data.payout_pending_review) ?? false,
|
||||
period_id: periodId,
|
||||
period_no: data.period_no,
|
||||
result_number:
|
||||
typeof resultNumber === 'number' && Number.isInteger(resultNumber)
|
||||
? resultNumber
|
||||
: null,
|
||||
server_time: serverTime,
|
||||
streak_level: typeof streakLevel === 'number' ? streakLevel : undefined,
|
||||
total_win: totalWin,
|
||||
user_id: userId,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user