refactor(game): 重构游戏组件和数据结构
- 移除中心模态框的背景模糊效果并调整透明度 - 为桌面游戏历史组件添加空状态显示组件 - 重构头部时钟显示逻辑,提取为独立组件并优化时间同步 - 移除用户ID遮罩功能,直接使用昵称显示中奖信息 - 调整入口页面的模态框渲染结构和认证状态检查逻辑 - 更新奖池广播数据结构,替换用户ID为昵称字段 - 优化实时同步中的数据验证和映射逻辑 - 调整移动端头部时钟组件的实现方式
This commit is contained in:
@@ -71,7 +71,7 @@ export function CenterModal({
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed inset-0 z-50 flex items-center justify-center bg-slate-950/72 backdrop-blur-sm',
|
'fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80',
|
||||||
backdropClassName,
|
backdropClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -214,10 +214,10 @@ export interface GamePeriodTickDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface JackpotHitItemDto {
|
export interface JackpotHitItemDto {
|
||||||
|
nickname: string
|
||||||
period_no: string
|
period_no: string
|
||||||
result_number: number
|
result_number: number
|
||||||
total_win: string
|
total_win: string
|
||||||
user_id: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JackpotHitEventDataDto {
|
export interface JackpotHitEventDataDto {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { History } from 'lucide-react'
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import historyBg from '@/assets/system/history-bg.png'
|
import historyBg from '@/assets/system/history-bg.png'
|
||||||
@@ -43,6 +44,25 @@ function HistoryRewardNumber({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HistoryEmptyState({ label }: { label: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-full w-full flex-1 items-center justify-center px-design-10 py-design-10">
|
||||||
|
<div className="flex w-fit max-w-full flex-col items-center rounded-[calc(var(--design-unit)*8)] border border-[rgba(94,212,230,0.14)] bg-[rgba(5,23,33,0.3)] px-design-14 py-design-12 text-center">
|
||||||
|
<div className="mb-design-8 flex h-design-42 w-design-42 items-center justify-center rounded-full border border-[#56EFFF]/18 bg-[#061D29]/55 shadow-[0_0_calc(var(--design-unit)*10)_rgba(86,239,255,0.08)]">
|
||||||
|
<History
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-design-20 w-design-20 text-[#A8EAF1]"
|
||||||
|
strokeWidth={1.8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-nowrap text-design-16 font-semibold text-[#9CCFD4]">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function DesktopGameHistory() {
|
export function DesktopGameHistory() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
@@ -99,13 +119,7 @@ export function DesktopGameHistory() {
|
|||||||
className="min-h-full flex-1"
|
className="min-h-full flex-1"
|
||||||
/>
|
/>
|
||||||
) : isEmpty ? (
|
) : isEmpty ? (
|
||||||
<div
|
<HistoryEmptyState label={emptyText} />
|
||||||
className={
|
|
||||||
'flex w-full flex-1 items-center justify-center text-design-18 text-[#84A2A2]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{emptyText}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
|||||||
@@ -14,7 +14,16 @@ import avatar from '@/assets/system/avatar.webp'
|
|||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import logo from '@/assets/system/logo.webp'
|
import logo from '@/assets/system/logo.webp'
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
import { useHeaderVm } from '@/features/game/hooks/use-header-vm'
|
import {
|
||||||
|
useHeaderClockLabel,
|
||||||
|
useHeaderVm,
|
||||||
|
} from '@/features/game/hooks/use-header-vm'
|
||||||
|
|
||||||
|
function HeaderClock() {
|
||||||
|
const systemTimeLabel = useHeaderClockLabel()
|
||||||
|
|
||||||
|
return <div className={'text-[#D2FCFF] font-bold'}>{systemTimeLabel}</div>
|
||||||
|
}
|
||||||
|
|
||||||
function SignalBars({
|
function SignalBars({
|
||||||
activeBars,
|
activeBars,
|
||||||
@@ -66,12 +75,11 @@ export function DesktopHeader() {
|
|||||||
onOpenRules,
|
onOpenRules,
|
||||||
onOpenUserInfo,
|
onOpenUserInfo,
|
||||||
signalPresentation,
|
signalPresentation,
|
||||||
systemTimeLabel,
|
|
||||||
toggleSoundEnabled,
|
toggleSoundEnabled,
|
||||||
} = useHeaderVm()
|
} = useHeaderVm()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-30 border-b border-white/8 bg-slate-950/70 backdrop-blur-xl">
|
<header className="sticky top-0 z-30 border-b border-white/8 bg-[#020B14]/95 ">
|
||||||
<div className="flex h-design-70 w-full items-center px-design-12">
|
<div className="flex h-design-70 w-full items-center px-design-12">
|
||||||
<div className="flex h-full w-design-375 items-center justify-center border-r border-[rgba(128,223,231,0.65)] px-design-10">
|
<div className="flex h-full w-design-375 items-center justify-center border-r border-[rgba(128,223,231,0.65)] px-design-10">
|
||||||
<SmartImage
|
<SmartImage
|
||||||
@@ -99,7 +107,7 @@ export function DesktopHeader() {
|
|||||||
<div className={'text-[#B4E4E9]'}>
|
<div className={'text-[#B4E4E9]'}>
|
||||||
{t('gameDesktop.header.systemTime')}
|
{t('gameDesktop.header.systemTime')}
|
||||||
</div>
|
</div>
|
||||||
<div className={'text-[#D2FCFF] font-bold'}>{systemTimeLabel}</div>
|
<HeaderClock />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-full flex-1 items-center justify-around gap-design-10 border-r border-[rgba(128,223,231,0.65)] px-design-20">
|
<div className="flex h-full flex-1 items-center justify-around gap-design-10 border-r border-[rgba(128,223,231,0.65)] px-design-20">
|
||||||
|
|||||||
@@ -6,20 +6,6 @@ const winAmountFormatter = new Intl.NumberFormat('en-US', {
|
|||||||
maximumFractionDigits: 6,
|
maximumFractionDigits: 6,
|
||||||
})
|
})
|
||||||
|
|
||||||
function maskParticipantLabel(value: number | string) {
|
|
||||||
const label = String(value).trim()
|
|
||||||
|
|
||||||
if (label.length <= 1) {
|
|
||||||
return '*'
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleLength = Math.ceil(label.length / 2)
|
|
||||||
|
|
||||||
return `${label.slice(0, visibleLength)}${'*'.repeat(
|
|
||||||
label.length - visibleLength,
|
|
||||||
)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatWinAmount(value: string) {
|
function formatWinAmount(value: string) {
|
||||||
const amount = Number(value)
|
const amount = Number(value)
|
||||||
|
|
||||||
@@ -34,9 +20,9 @@ export function DesktopTitle() {
|
|||||||
jackpotBroadcasts.length > 0
|
jackpotBroadcasts.length > 0
|
||||||
? jackpotBroadcasts.map((broadcast) => ({
|
? jackpotBroadcasts.map((broadcast) => ({
|
||||||
id: broadcast.id,
|
id: broadcast.id,
|
||||||
message: `Player ${maskParticipantLabel(
|
message: `Player ${broadcast.nickname} won ${formatWinAmount(
|
||||||
broadcast.userId,
|
broadcast.totalWin,
|
||||||
)} won ${formatWinAmount(broadcast.totalWin)}`,
|
)}`,
|
||||||
}))
|
}))
|
||||||
: [{ id: 'empty', message: '' }]
|
: [{ id: 'empty', message: '' }]
|
||||||
const marqueeTitles =
|
const marqueeTitles =
|
||||||
|
|||||||
@@ -12,7 +12,20 @@ import avatar from '@/assets/system/avatar.webp'
|
|||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import logo from '@/assets/system/logo.webp'
|
import logo from '@/assets/system/logo.webp'
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
import { useHeaderVm } from '@/features/game/hooks/use-header-vm'
|
import {
|
||||||
|
useHeaderClockLabel,
|
||||||
|
useHeaderVm,
|
||||||
|
} from '@/features/game/hooks/use-header-vm'
|
||||||
|
|
||||||
|
function MobileHeaderClock() {
|
||||||
|
const systemTimeLabel = useHeaderClockLabel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-design-3 whitespace-nowrap text-design-7 font-bold text-[#D2FCFF]">
|
||||||
|
{systemTimeLabel}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function MobileHeader() {
|
export function MobileHeader() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -30,7 +43,6 @@ export function MobileHeader() {
|
|||||||
onOpenRules,
|
onOpenRules,
|
||||||
onOpenUserInfo,
|
onOpenUserInfo,
|
||||||
signalPresentation,
|
signalPresentation,
|
||||||
systemTimeLabel,
|
|
||||||
toggleSoundEnabled,
|
toggleSoundEnabled,
|
||||||
} = useHeaderVm()
|
} = useHeaderVm()
|
||||||
|
|
||||||
@@ -155,9 +167,7 @@ export function MobileHeader() {
|
|||||||
<div className="text-[#B4E4E9]">
|
<div className="text-[#B4E4E9]">
|
||||||
{t('gameDesktop.header.systemTime')}
|
{t('gameDesktop.header.systemTime')}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-design-3 whitespace-nowrap text-design-7 font-bold text-[#D2FCFF]">
|
<MobileHeaderClock />
|
||||||
{systemTimeLabel}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export function EntryNoticeGateModal() {
|
|||||||
setHasEntered(false)
|
setHasEntered(false)
|
||||||
setHasAgreed(false)
|
setHasAgreed(false)
|
||||||
|
|
||||||
if (!hasStoredLoginInfo) {
|
if (!hasStoredLoginInfo || shouldSuppressEntryGate) {
|
||||||
setShouldGateEntry(!shouldSuppressEntryGate)
|
setShouldGateEntry(false)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,54 @@ import { startTransition, useEffect, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { getGameLobbyInit } from '@/features/game'
|
import { getGameLobbyInit } from '@/features/game'
|
||||||
|
import { EntryNoticeGateModal } from '@/features/game/components'
|
||||||
import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx'
|
import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx'
|
||||||
import { PcEntry } from '@/features/game/entry/pc-entry.tsx'
|
import { PcEntry } from '@/features/game/entry/pc-entry.tsx'
|
||||||
import { useGameRealtimeSync } from '@/features/game/hooks/use-game-realtime-sync.ts'
|
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 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 { useDocumentMetadata } from '@/lib/head/document-metadata'
|
||||||
import { notify } from '@/lib/notify'
|
import { notify } from '@/lib/notify'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
||||||
|
|
||||||
|
function EntryModalHost() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 桌面端登录弹窗:用于未登录用户进入登录流程 */}
|
||||||
|
<DesktopLoginModal />
|
||||||
|
{/* 桌面端注册弹窗:用于新用户注册账号 */}
|
||||||
|
<DesktopRegisterModal />
|
||||||
|
{/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */}
|
||||||
|
<DesktopLanguageModal />
|
||||||
|
{/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */}
|
||||||
|
<DesktopRulesModal />
|
||||||
|
{/* 桌面端用户信息弹窗:展示个人资料与站内消息 */}
|
||||||
|
<DesktopUserInfoModal />
|
||||||
|
{/* 桌面端公告弹窗:展示活动公告或运营通知内容 */}
|
||||||
|
<DesktopNoticeModal />
|
||||||
|
{/* 桌面端自动托管弹窗:配置自动托管相关条件 */}
|
||||||
|
<DesktopAutoSettingModal />
|
||||||
|
{/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */}
|
||||||
|
<DesktopProceduresModal />
|
||||||
|
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
||||||
|
<DesktopWithdrawTopupModal />
|
||||||
|
{/* 强制弹窗 */}
|
||||||
|
<EntryNoticeGateModal />
|
||||||
|
{/* 历史开奖信息弹窗 */}
|
||||||
|
<DesktopPeriodHistoryDrawer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function EntryPage() {
|
export function EntryPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
useGameRealtimeSync()
|
useGameRealtimeSync()
|
||||||
@@ -19,6 +59,8 @@ export function EntryPage() {
|
|||||||
const syncConnection = useGameSessionStore((state) => state.syncConnection)
|
const syncConnection = useGameSessionStore((state) => state.syncConnection)
|
||||||
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
||||||
const authStatus = useAuthStore((state) => state.status)
|
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 lastUnauthorizedAt = useAuthStore((state) => state.lastUnauthorizedAt)
|
||||||
const isReloginRequired =
|
const isReloginRequired =
|
||||||
authStatus === 'anonymous' && Boolean(lastUnauthorizedAt)
|
authStatus === 'anonymous' && Boolean(lastUnauthorizedAt)
|
||||||
@@ -38,7 +80,13 @@ export function EntryPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReloginRequired) {
|
if (!authIsHydrated) {
|
||||||
|
setIsHydrating(true)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReloginRequired || authStatus !== 'authenticated' || !accessToken) {
|
||||||
setIsHydrating(false)
|
setIsHydrating(false)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -121,6 +169,8 @@ export function EntryPage() {
|
|||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
accessToken,
|
||||||
|
authIsHydrated,
|
||||||
authStatus,
|
authStatus,
|
||||||
hydrateRound,
|
hydrateRound,
|
||||||
hydrateSession,
|
hydrateSession,
|
||||||
@@ -156,6 +206,7 @@ export function EntryPage() {
|
|||||||
className="flex min-h-0 flex-1 flex-col"
|
className="flex min-h-0 flex-1 flex-col"
|
||||||
>
|
>
|
||||||
{isMobile ? <MobileEntry /> : <PcEntry />}
|
{isMobile ? <MobileEntry /> : <PcEntry />}
|
||||||
|
<EntryModalHost />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { DesktopHeader, EntryNoticeGateModal } from '@/features/game/components'
|
import { DesktopHeader } from '@/features/game/components'
|
||||||
import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
|
import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
|
||||||
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
||||||
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
||||||
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
|
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
|
||||||
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts'
|
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.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 DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx'
|
|
||||||
import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx'
|
|
||||||
|
|
||||||
export function PcEntry() {
|
export function PcEntry() {
|
||||||
useAutoHostingRunner()
|
useAutoHostingRunner()
|
||||||
@@ -50,28 +40,6 @@ export function PcEntry() {
|
|||||||
>
|
>
|
||||||
<DesktopControl />
|
<DesktopControl />
|
||||||
</div>
|
</div>
|
||||||
{/* 桌面端登录弹窗:用于未登录用户进入登录流程 */}
|
|
||||||
<DesktopLoginModal />
|
|
||||||
{/* 桌面端注册弹窗:用于新用户注册账号 */}
|
|
||||||
<DesktopRegisterModal />
|
|
||||||
{/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */}
|
|
||||||
<DesktopLanguageModal />
|
|
||||||
{/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */}
|
|
||||||
<DesktopRulesModal />
|
|
||||||
{/* 桌面端用户信息弹窗:展示个人资料与站内消息 */}
|
|
||||||
<DesktopUserInfoModal />
|
|
||||||
{/* 桌面端公告弹窗:展示活动公告或运营通知内容 */}
|
|
||||||
<DesktopNoticeModal />
|
|
||||||
{/* 桌面端自动托管弹窗:配置自动托管相关条件 */}
|
|
||||||
<DesktopAutoSettingModal />
|
|
||||||
{/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */}
|
|
||||||
<DesktopProceduresModal />
|
|
||||||
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
|
||||||
<DesktopWithdrawTopupModal />
|
|
||||||
{/* 强制弹窗 */}
|
|
||||||
<EntryNoticeGateModal />
|
|
||||||
{/* 历史开奖信息弹窗 */}
|
|
||||||
<DesktopPeriodHistoryDrawer />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
|
|||||||
const source = value as Record<string, unknown>
|
const source = value as Record<string, unknown>
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof source.user_id !== 'number' ||
|
typeof source.nickname !== 'string' ||
|
||||||
typeof source.period_no !== 'string' ||
|
typeof source.period_no !== 'string' ||
|
||||||
typeof source.total_win !== 'string'
|
typeof source.total_win !== 'string'
|
||||||
) {
|
) {
|
||||||
@@ -270,10 +270,10 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
nickname: source.nickname,
|
||||||
period_no: source.period_no,
|
period_no: source.period_no,
|
||||||
result_number: resultNumber,
|
result_number: resultNumber,
|
||||||
total_win: source.total_win,
|
total_win: source.total_win,
|
||||||
user_id: source.user_id,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,18 +286,31 @@ function extractJackpotHitData(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverTime = toOptionalNumber(data.server_time)
|
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') {
|
if (typeof serverTime !== 'number') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceHits = Array.isArray(data.hits) ? data.hits : [data]
|
|
||||||
const hits = sourceHits
|
|
||||||
.map((item) => extractJackpotHitItem(item))
|
|
||||||
.filter((item): item is JackpotHitItemDto => item !== null)
|
|
||||||
const resultNumber = toOptionalNumber(data.result_number)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hits,
|
hits,
|
||||||
period_id:
|
period_id:
|
||||||
@@ -589,11 +602,11 @@ function applyJackpotHitMessage(message: GameSocketMessage) {
|
|||||||
if (jackpotHitData?.hits.length) {
|
if (jackpotHitData?.hits.length) {
|
||||||
useGameSessionStore.getState().pushJackpotBroadcasts(
|
useGameSessionStore.getState().pushJackpotBroadcasts(
|
||||||
jackpotHitData.hits.map((hit) => ({
|
jackpotHitData.hits.map((hit) => ({
|
||||||
id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.user_id}:${hit.total_win}`,
|
id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.nickname}:${hit.total_win}`,
|
||||||
message: `恭喜${hit.user_id} 用户中奖,获得${hit.total_win}`,
|
message: `恭喜${hit.nickname} 用户中奖,获得${hit.total_win}`,
|
||||||
|
nickname: hit.nickname,
|
||||||
periodNo: hit.period_no,
|
periodNo: hit.period_no,
|
||||||
totalWin: hit.total_win,
|
totalWin: hit.total_win,
|
||||||
userId: hit.user_id,
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ function resolveSignalPresentation(input: {
|
|||||||
|
|
||||||
export function useHeaderVm() {
|
export function useHeaderVm() {
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
const [clockNow, setClockNow] = useState(() => Date.now())
|
|
||||||
const [isOnline, setIsOnline] = useState(() =>
|
const [isOnline, setIsOnline] = useState(() =>
|
||||||
typeof navigator === 'undefined' ? true : navigator.onLine,
|
typeof navigator === 'undefined' ? true : navigator.onLine,
|
||||||
)
|
)
|
||||||
@@ -128,31 +127,6 @@ export function useHeaderVm() {
|
|||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
const { currentLanguageLabel, currentLanguageOption } = useAppLanguage()
|
const { currentLanguageLabel, currentLanguageOption } = useAppLanguage()
|
||||||
|
|
||||||
const serverClockOffsetMs = useMemo(() => {
|
|
||||||
if (
|
|
||||||
connection.status !== 'connected' ||
|
|
||||||
connection.transport !== 'websocket' ||
|
|
||||||
!connection.lastMessageAt
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverTimestamp = Date.parse(connection.lastMessageAt)
|
|
||||||
|
|
||||||
if (Number.isNaN(serverTimestamp)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return serverTimestamp - Date.now()
|
|
||||||
}, [connection.lastMessageAt, connection.status, connection.transport])
|
|
||||||
|
|
||||||
const systemTimeLabel = useMemo(() => {
|
|
||||||
const activeTimestamp =
|
|
||||||
serverClockOffsetMs === null ? clockNow : clockNow + serverClockOffsetMs
|
|
||||||
|
|
||||||
return formatHeaderTime(new Date(activeTimestamp))
|
|
||||||
}, [clockNow, serverClockOffsetMs])
|
|
||||||
|
|
||||||
const signalLatencyMs = useMemo(() => {
|
const signalLatencyMs = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
typeof connection.latencyMs === 'number' &&
|
typeof connection.latencyMs === 'number' &&
|
||||||
@@ -184,16 +158,6 @@ export function useHeaderVm() {
|
|||||||
return subscribeDesktopFullscreenChange(syncFullscreenState)
|
return subscribeDesktopFullscreenChange(syncFullscreenState)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
setClockNow(Date.now())
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearInterval(timer)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const syncBrowserNetworkState = () => {
|
const syncBrowserNetworkState = () => {
|
||||||
setIsOnline(navigator.onLine)
|
setIsOnline(navigator.onLine)
|
||||||
@@ -238,7 +202,52 @@ export function useHeaderVm() {
|
|||||||
onOpenRules: () => setModalOpen('desktopRules', true),
|
onOpenRules: () => setModalOpen('desktopRules', true),
|
||||||
onOpenUserInfo: () => setModalOpen('desktopUserInfo', true),
|
onOpenUserInfo: () => setModalOpen('desktopUserInfo', true),
|
||||||
signalPresentation,
|
signalPresentation,
|
||||||
systemTimeLabel,
|
|
||||||
toggleSoundEnabled,
|
toggleSoundEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useHeaderClockLabel() {
|
||||||
|
const [clockNow, setClockNow] = useState(() => Date.now())
|
||||||
|
const lastMessageAt = useGameSessionStore(
|
||||||
|
(state) => state.connection.lastMessageAt,
|
||||||
|
)
|
||||||
|
const connectionStatus = useGameSessionStore(
|
||||||
|
(state) => state.connection.status,
|
||||||
|
)
|
||||||
|
const connectionTransport = useGameSessionStore(
|
||||||
|
(state) => state.connection.transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
const serverClockOffsetMs = useMemo(() => {
|
||||||
|
if (
|
||||||
|
connectionStatus !== 'connected' ||
|
||||||
|
connectionTransport !== 'websocket' ||
|
||||||
|
!lastMessageAt
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverTimestamp = Date.parse(lastMessageAt)
|
||||||
|
|
||||||
|
if (Number.isNaN(serverTimestamp)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverTimestamp - Date.now()
|
||||||
|
}, [connectionStatus, connectionTransport, lastMessageAt])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setClockNow(Date.now())
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const activeTimestamp =
|
||||||
|
serverClockOffsetMs === null ? clockNow : clockNow + serverClockOffsetMs
|
||||||
|
|
||||||
|
return formatHeaderTime(new Date(activeTimestamp))
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ const MAX_JACKPOT_BROADCAST_COUNT = 20
|
|||||||
export interface JackpotBroadcastItem {
|
export interface JackpotBroadcastItem {
|
||||||
id: string
|
id: string
|
||||||
message: string
|
message: string
|
||||||
|
nickname: string
|
||||||
periodNo: string
|
periodNo: string
|
||||||
receivedAt: string
|
receivedAt: string
|
||||||
totalWin: string
|
totalWin: string
|
||||||
userId: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type JackpotBroadcastInput = Omit<JackpotBroadcastItem, 'receivedAt'>
|
type JackpotBroadcastInput = Omit<JackpotBroadcastItem, 'receivedAt'>
|
||||||
|
|||||||
Reference in New Issue
Block a user