diff --git a/src/components/center-modal.tsx b/src/components/center-modal.tsx index 4f528cf..7fb4910 100644 --- a/src/components/center-modal.tsx +++ b/src/components/center-modal.tsx @@ -71,7 +71,7 @@ export function CenterModal({ return createPortal(
diff --git a/src/features/game/api/types.ts b/src/features/game/api/types.ts index 6ae806f..12497da 100644 --- a/src/features/game/api/types.ts +++ b/src/features/game/api/types.ts @@ -214,10 +214,10 @@ export interface GamePeriodTickDto { } export interface JackpotHitItemDto { + nickname: string period_no: string result_number: number total_win: string - user_id: number } export interface JackpotHitEventDataDto { diff --git a/src/features/game/components/desktop/desktop-game-history.tsx b/src/features/game/components/desktop/desktop-game-history.tsx index 03022c2..b9c238e 100644 --- a/src/features/game/components/desktop/desktop-game-history.tsx +++ b/src/features/game/components/desktop/desktop-game-history.tsx @@ -1,3 +1,4 @@ +import { History } from 'lucide-react' import { useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import historyBg from '@/assets/system/history-bg.png' @@ -43,6 +44,25 @@ function HistoryRewardNumber({ ) } +function HistoryEmptyState({ label }: { label: string }) { + return ( +
+
+
+
+
+ {label} +
+
+
+ ) +} + export function DesktopGameHistory() { const { t } = useTranslation() const { @@ -99,13 +119,7 @@ export function DesktopGameHistory() { className="min-h-full flex-1" /> ) : isEmpty ? ( -
- {emptyText} -
+ ) : ( <> {items.map((item) => { diff --git a/src/features/game/components/desktop/desktop-header.tsx b/src/features/game/components/desktop/desktop-header.tsx index e9d3b14..0ec8f8c 100644 --- a/src/features/game/components/desktop/desktop-header.tsx +++ b/src/features/game/components/desktop/desktop-header.tsx @@ -14,7 +14,16 @@ import avatar from '@/assets/system/avatar.webp' import diamond from '@/assets/system/diamond.webp' import logo from '@/assets/system/logo.webp' 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
{systemTimeLabel}
+} function SignalBars({ activeBars, @@ -66,12 +75,11 @@ export function DesktopHeader() { onOpenRules, onOpenUserInfo, signalPresentation, - systemTimeLabel, toggleSoundEnabled, } = useHeaderVm() return ( -
+
{t('gameDesktop.header.systemTime')}
-
{systemTimeLabel}
+
diff --git a/src/features/game/components/desktop/desktop-title.tsx b/src/features/game/components/desktop/desktop-title.tsx index eb0554a..25b8a90 100644 --- a/src/features/game/components/desktop/desktop-title.tsx +++ b/src/features/game/components/desktop/desktop-title.tsx @@ -6,20 +6,6 @@ const winAmountFormatter = new Intl.NumberFormat('en-US', { 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) { const amount = Number(value) @@ -34,9 +20,9 @@ export function DesktopTitle() { jackpotBroadcasts.length > 0 ? jackpotBroadcasts.map((broadcast) => ({ id: broadcast.id, - message: `Player ${maskParticipantLabel( - broadcast.userId, - )} won ${formatWinAmount(broadcast.totalWin)}`, + message: `Player ${broadcast.nickname} won ${formatWinAmount( + broadcast.totalWin, + )}`, })) : [{ id: 'empty', message: '' }] const marqueeTitles = diff --git a/src/features/game/components/mobile/mobile-header.tsx b/src/features/game/components/mobile/mobile-header.tsx index f02b5bc..e13415b 100644 --- a/src/features/game/components/mobile/mobile-header.tsx +++ b/src/features/game/components/mobile/mobile-header.tsx @@ -12,7 +12,20 @@ import avatar from '@/assets/system/avatar.webp' import diamond from '@/assets/system/diamond.webp' import logo from '@/assets/system/logo.webp' 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 ( +
+ {systemTimeLabel} +
+ ) +} export function MobileHeader() { const { t } = useTranslation() @@ -30,7 +43,6 @@ export function MobileHeader() { onOpenRules, onOpenUserInfo, signalPresentation, - systemTimeLabel, toggleSoundEnabled, } = useHeaderVm() @@ -155,9 +167,7 @@ export function MobileHeader() {
{t('gameDesktop.header.systemTime')}
-
- {systemTimeLabel} -
+
- {/* 桌面端登录弹窗:用于未登录用户进入登录流程 */} - - {/* 桌面端注册弹窗:用于新用户注册账号 */} - - {/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */} - - {/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */} - - {/* 桌面端用户信息弹窗:展示个人资料与站内消息 */} - - {/* 桌面端公告弹窗:展示活动公告或运营通知内容 */} - - {/* 桌面端自动托管弹窗:配置自动托管相关条件 */} - - {/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */} - - {/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */} - - {/* 强制弹窗 */} - - {/* 历史开奖信息弹窗 */} - ) } diff --git a/src/features/game/hooks/use-game-realtime-sync.ts b/src/features/game/hooks/use-game-realtime-sync.ts index cf3183f..aea5b3a 100644 --- a/src/features/game/hooks/use-game-realtime-sync.ts +++ b/src/features/game/hooks/use-game-realtime-sync.ts @@ -256,7 +256,7 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null { const source = value as Record if ( - typeof source.user_id !== 'number' || + typeof source.nickname !== 'string' || typeof source.period_no !== 'string' || typeof source.total_win !== 'string' ) { @@ -270,10 +270,10 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null { } return { + nickname: source.nickname, period_no: source.period_no, result_number: resultNumber, total_win: source.total_win, - user_id: source.user_id, } } @@ -286,18 +286,31 @@ function extractJackpotHitData( 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 => + Boolean(item) && typeof item === 'object', + ) + const hits = sourceHits + .map((item) => extractJackpotHitItem(item)) + .filter((item): item is JackpotHitItemDto => item !== null) + const root = message as Record + 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 } - 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 { hits, period_id: @@ -589,11 +602,11 @@ function applyJackpotHitMessage(message: GameSocketMessage) { if (jackpotHitData?.hits.length) { useGameSessionStore.getState().pushJackpotBroadcasts( jackpotHitData.hits.map((hit) => ({ - id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.user_id}:${hit.total_win}`, - message: `恭喜${hit.user_id} 用户中奖,获得${hit.total_win}`, + id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.nickname}:${hit.total_win}`, + message: `恭喜${hit.nickname} 用户中奖,获得${hit.total_win}`, + nickname: hit.nickname, periodNo: hit.period_no, totalWin: hit.total_win, - userId: hit.user_id, })), ) } diff --git a/src/features/game/hooks/use-header-vm.ts b/src/features/game/hooks/use-header-vm.ts index f393232..c49a51d 100644 --- a/src/features/game/hooks/use-header-vm.ts +++ b/src/features/game/hooks/use-header-vm.ts @@ -107,7 +107,6 @@ function resolveSignalPresentation(input: { export function useHeaderVm() { const [isFullscreen, setIsFullscreen] = useState(false) - const [clockNow, setClockNow] = useState(() => Date.now()) const [isOnline, setIsOnline] = useState(() => typeof navigator === 'undefined' ? true : navigator.onLine, ) @@ -128,31 +127,6 @@ export function useHeaderVm() { const setModalOpen = useModalStore((state) => state.setModalOpen) 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(() => { if ( typeof connection.latencyMs === 'number' && @@ -184,16 +158,6 @@ export function useHeaderVm() { return subscribeDesktopFullscreenChange(syncFullscreenState) }, []) - useEffect(() => { - const timer = window.setInterval(() => { - setClockNow(Date.now()) - }, 1000) - - return () => { - window.clearInterval(timer) - } - }, []) - useEffect(() => { const syncBrowserNetworkState = () => { setIsOnline(navigator.onLine) @@ -238,7 +202,52 @@ export function useHeaderVm() { onOpenRules: () => setModalOpen('desktopRules', true), onOpenUserInfo: () => setModalOpen('desktopUserInfo', true), signalPresentation, - systemTimeLabel, 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)) +} diff --git a/src/store/game/game-session-store.ts b/src/store/game/game-session-store.ts index 7310a60..f3c3a0f 100644 --- a/src/store/game/game-session-store.ts +++ b/src/store/game/game-session-store.ts @@ -23,10 +23,10 @@ const MAX_JACKPOT_BROADCAST_COUNT = 20 export interface JackpotBroadcastItem { id: string message: string + nickname: string periodNo: string receivedAt: string totalWin: string - userId: number } type JackpotBroadcastInput = Omit