feat(auth): 添加登出功能并优化认证处理
- 添加了登出相关的API端点和常量定义 - 实现了登出功能及密码验证登出逻辑 - 添加了登出会话清理和浏览器存储清除 - 在用户信息模态框中集成了登出按钮 - 添加了登出相关的国际化翻译 - 优化了API客户端中的认证错误处理 - 实现了无效令牌的自动处理机制 - 更新了GitNexus索引统计数据 - 修改了构建输出目录配置 - 清理了不必要的注释和代码 - 调整了移动端头部组件结构 - 优化了游戏历史记录查询逻辑 - 添加了控制台日志用于调试 - 设置默认注册邀请码为D97DBC16 - 在.gitignore中添加构建产物忽略规则
This commit is contained in:
@@ -9,6 +9,8 @@ import type {
|
||||
AuthUserProfileDto,
|
||||
LoginPayload,
|
||||
LoginRequestDto,
|
||||
LogoutPayload,
|
||||
LogoutRequestDto,
|
||||
RefreshTokenDto,
|
||||
RefreshTokenRequestDto,
|
||||
RegisterPayload,
|
||||
@@ -107,6 +109,25 @@ export async function loginWithPassword(
|
||||
return session
|
||||
}
|
||||
|
||||
export async function logoutWithPassword(
|
||||
payload: LogoutPayload,
|
||||
): Promise<void> {
|
||||
const response = await api.post<null, LogoutRequestDto>(
|
||||
AUTH_ENDPOINTS.logout,
|
||||
{
|
||||
json: {
|
||||
password: payload.password,
|
||||
username: payload.username,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
unwrapEnvelope(
|
||||
response as ApiResponse<null>,
|
||||
'auth.logout.errors.submitFailed',
|
||||
)
|
||||
}
|
||||
|
||||
export async function registerWithPassword(
|
||||
payload: RegisterPayload,
|
||||
): Promise<AuthSessionInput> {
|
||||
|
||||
@@ -56,6 +56,11 @@ export interface LoginRequestDto {
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface LogoutRequestDto {
|
||||
password: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface RegisterRequestDto extends LoginRequestDto {
|
||||
invite_code: string
|
||||
}
|
||||
@@ -69,6 +74,8 @@ export interface LoginPayload {
|
||||
username: string
|
||||
}
|
||||
|
||||
export type LogoutPayload = LoginPayload
|
||||
|
||||
export interface RegisterPayload extends LoginPayload {
|
||||
inviteCode: string
|
||||
}
|
||||
|
||||
@@ -16,16 +16,17 @@ interface UseRegisterFormOptions {
|
||||
}
|
||||
|
||||
const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode'
|
||||
const DEFAULT_REGISTER_INVITE_CODE = 'D97DBC16'
|
||||
|
||||
function getInitialRegisterInviteCode() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ''
|
||||
return DEFAULT_REGISTER_INVITE_CODE
|
||||
}
|
||||
|
||||
return (
|
||||
new URLSearchParams(window.location.search)
|
||||
.get(REGISTER_INVITE_CODE_QUERY_PARAM)
|
||||
?.trim() ?? ''
|
||||
?.trim() || DEFAULT_REGISTER_INVITE_CODE
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
VolumeX,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import add from '@/assets/game/add.webp'
|
||||
import avatar from '@/assets/system/avatar.webp'
|
||||
import diamond from '@/assets/system/diamond.webp'
|
||||
import logo from '@/assets/system/logo.webp'
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
|
||||
import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
|
||||
import { useGameStatusVm } from '@/features/game/hooks/use-game-status-vm.ts'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
|
||||
export function DesktopStatusLine() {
|
||||
const { t } = useTranslation()
|
||||
@@ -20,7 +21,6 @@ export function DesktopStatusLine() {
|
||||
countdownMs,
|
||||
limitLabel,
|
||||
oddsLabel,
|
||||
phaseDescription,
|
||||
phaseLabel,
|
||||
phaseToneClassName,
|
||||
roundId,
|
||||
@@ -40,6 +40,17 @@ export function DesktopStatusLine() {
|
||||
|
||||
return (
|
||||
<div className={'relative w-full flex flex-col text-design-22'}>
|
||||
{/*<div*/}
|
||||
{/* className={*/}
|
||||
{/* 'absolute top-design-75 left-1/2 -translate-x-1/2 -z-10 w-full px-design-16'*/}
|
||||
{/* }*/}
|
||||
{/*>*/}
|
||||
{/* <DesktopTitle />*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<div className={'w-full px-design-16 mb-design-10'}>
|
||||
<DesktopTitle />
|
||||
</div>
|
||||
<SmartBackground
|
||||
src={statusLine}
|
||||
size="100% 100%"
|
||||
@@ -154,28 +165,25 @@ export function DesktopStatusLine() {
|
||||
className={countdownClassName}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex-1 flex items-center justify-center gap-10'}>
|
||||
<div>
|
||||
{t('gameDesktop.status.roundId')}:{roundId}
|
||||
</div>
|
||||
<div
|
||||
className={'flex-1 flex items-center justify-center gap-design-100'}
|
||||
>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div
|
||||
className={'w-design-20 h-design-20 bg-[#78FF7F] rounded-[50%]'}
|
||||
></div>
|
||||
<div className={phaseToneClassName}>{phaseLabel}</div>
|
||||
<div className={cn(phaseToneClassName, 'font-bold')}>
|
||||
{phaseLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div>{phaseDescription}</div>
|
||||
</div>
|
||||
|
||||
<div className={'text-[#CBD3D5] font-bold'}>
|
||||
{t('gameDesktop.status.roundId')}:{roundId}
|
||||
</div>
|
||||
</div>
|
||||
</SmartBackground>
|
||||
<div
|
||||
className={
|
||||
'absolute top-design-75 left-1/2 -translate-x-1/2 -z-10 w-full px-design-16'
|
||||
}
|
||||
>
|
||||
<DesktopTitle />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import broadcast from '@/assets/system/broadcast.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { useGameSessionStore } from '@/store/game'
|
||||
|
||||
export function DesktopTitle() {
|
||||
const { t } = useTranslation()
|
||||
const jackpotBroadcasts = useGameSessionStore(
|
||||
(state) => state.jackpotBroadcasts,
|
||||
)
|
||||
@@ -30,9 +28,6 @@ export function DesktopTitle() {
|
||||
alt={'broadcast'}
|
||||
src={broadcast}
|
||||
/>
|
||||
<div className="shrink-0 !text-[#FF970F]">
|
||||
{t('gameDesktop.title.announcement')}:
|
||||
</div>
|
||||
<div className="relative h-design-28 min-w-0 flex-1 overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
|
||||
223
src/features/game/components/mobile/mobile-header.tsx
Normal file
223
src/features/game/components/mobile/mobile-header.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import {
|
||||
Info,
|
||||
Mail,
|
||||
UserKey,
|
||||
UserRoundPlus,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Wifi,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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'
|
||||
|
||||
export function MobileHeader() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
authStatus,
|
||||
currentLanguageLabel,
|
||||
currentLanguageOption,
|
||||
currentUser,
|
||||
isSoundEnabled,
|
||||
onOpenLanguage,
|
||||
onOpenLogin,
|
||||
onOpenNotice,
|
||||
onOpenProcedures,
|
||||
onOpenRegister,
|
||||
onOpenRules,
|
||||
onOpenUserInfo,
|
||||
signalPresentation,
|
||||
systemTimeLabel,
|
||||
toggleSoundEnabled,
|
||||
} = useHeaderVm()
|
||||
|
||||
const actionButtonClassName =
|
||||
'common-neon-inset flex h-design-19 shrink-0 cursor-pointer items-center justify-center gap-design-5 !rounded-[3px] !px-design-5 !py-0 text-design-7 leading-none transition-opacity hover:opacity-85'
|
||||
const accountButtonClassName =
|
||||
'common-neon-inset flex h-design-19 items-center justify-end !rounded-[3px] !py-0 text-design-7 leading-none transition-[opacity,transform] duration-150 group-hover:opacity-90 group-active:scale-[0.98]'
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 h-design-62">
|
||||
<div className="border-b-2 border-[#787553] bg-[#020B14]] bg-[#020B14] flex h-design-33 w-full items-center">
|
||||
<div className="flex h-design-23 w-design-130 shrink-0 items-center justify-center border-r border-[rgba(128,223,231,0.45)] px-design-10">
|
||||
<SmartImage
|
||||
src={logo}
|
||||
alt="logo"
|
||||
priority
|
||||
className="h-full w-full"
|
||||
imgClassName="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{authStatus === 'authenticated' ? (
|
||||
<div className="flex h-full min-w-0 flex-1 items-center justify-end gap-design-7 px-design-9">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenUserInfo}
|
||||
className="group relative flex min-w-0 items-center justify-center transition-transform duration-150 hover:-translate-y-[1px] active:translate-y-[1px]"
|
||||
>
|
||||
<SmartImage
|
||||
src={avatar}
|
||||
alt="avatar"
|
||||
priority
|
||||
className="absolute left-0 z-20 h-design-25 w-design-25 rounded-full"
|
||||
/>
|
||||
<div
|
||||
className={`${accountButtonClassName} w-design-92 pl-design-28 pr-design-6`}
|
||||
>
|
||||
<span className="truncate">
|
||||
{currentUser?.username || '--'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenProcedures}
|
||||
className="group relative flex min-w-0 items-center justify-center transition-transform duration-150 hover:-translate-y-[1px] active:translate-y-[1px]"
|
||||
>
|
||||
<SmartImage
|
||||
src={diamond}
|
||||
alt="diamond"
|
||||
priority
|
||||
className="absolute left-0 z-20 h-design-25 w-design-25"
|
||||
/>
|
||||
<div
|
||||
className={`${accountButtonClassName} w-design-90 gap-design-4 pl-design-26 pr-design-4`}
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
{currentUser?.coin || '--'}
|
||||
</span>
|
||||
{/*<div className="common-neon-inset flex h-design-15 w-design-15 shrink-0 items-center justify-center !rounded-[3px] !p-0">*/}
|
||||
{/* <Plus*/}
|
||||
{/* aria-hidden="true"*/}
|
||||
{/* className="h-design-10 w-design-10 shrink-0"*/}
|
||||
{/* color="#57B8BF"*/}
|
||||
{/* strokeWidth={2.5}*/}
|
||||
{/* />*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full min-w-0 flex-1 items-center justify-end gap-design-8 px-design-9">
|
||||
<button
|
||||
type="button"
|
||||
className={`${actionButtonClassName} w-design-72`}
|
||||
onClick={onOpenLogin}
|
||||
>
|
||||
<UserKey
|
||||
className="h-design-8 w-design-8 shrink-0"
|
||||
color="#57B8BF"
|
||||
/>
|
||||
<div className="min-w-0 truncate">
|
||||
{t('gameDesktop.header.login')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${actionButtonClassName} w-design-78`}
|
||||
onClick={onOpenRegister}
|
||||
>
|
||||
<UserRoundPlus
|
||||
className="h-design-8 w-design-8 shrink-0"
|
||||
color="#57B8BF"
|
||||
/>
|
||||
<div className="min-w-0 truncate">
|
||||
{t('gameDesktop.header.register')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={'w-full px-design-10 '}>
|
||||
<div className="flex h-design-29 w-full items-center gap-design-5 my-design-5 rounded-sm border-[#0A353E] border-1">
|
||||
<div className="flex h-design-19 w-design-43 shrink-0 items-center justify-center gap-design-5 !rounded-[3px] !px-design-6 !py-0">
|
||||
<Wifi
|
||||
aria-hidden="true"
|
||||
color="currentColor"
|
||||
strokeWidth={2.4}
|
||||
className={`${signalPresentation.toneClassName} h-design-10 w-design-10 shrink-0`}
|
||||
/>
|
||||
<div
|
||||
className={`${signalPresentation.toneClassName} whitespace-nowrap text-design-7 font-bold leading-none`}
|
||||
>
|
||||
{signalPresentation.latencyLabel}
|
||||
<span className="pl-[1px] text-design-6">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-design-66 shrink-0 flex-col items-start justify-center border-r border-[rgba(128,223,231,0.32)] pr-design-7 text-design-7 leading-none">
|
||||
<div className="text-[#B4E4E9]">
|
||||
{t('gameDesktop.header.systemTime')}
|
||||
</div>
|
||||
<div className="mt-design-3 whitespace-nowrap text-design-7 font-bold text-[#D2FCFF]">
|
||||
{systemTimeLabel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenRules}
|
||||
className={`${actionButtonClassName} !px-design-10`}
|
||||
>
|
||||
<Info className="h-design-8 w-design-8 shrink-0" color="#57B8BF" />
|
||||
<div className="min-w-0 truncate">
|
||||
{t('gameDesktop.header.rules')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenNotice}
|
||||
className={`${actionButtonClassName} !px-design-10`}
|
||||
>
|
||||
<Mail className="h-design-8 w-design-8 shrink-0" color="#57B8BF" />
|
||||
<div className="min-w-0 truncate">
|
||||
{t('gameDesktop.header.message')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSoundEnabled}
|
||||
className={`${actionButtonClassName} !px-design-10`}
|
||||
>
|
||||
{isSoundEnabled ? (
|
||||
<Volume2
|
||||
className="h-design-8 w-design-8 shrink-0"
|
||||
color="#57B8BF"
|
||||
/>
|
||||
) : (
|
||||
<VolumeX
|
||||
className="h-design-8 w-design-8 shrink-0"
|
||||
color="#57B8BF"
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0 truncate">
|
||||
{t('gameDesktop.header.bgm')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenLanguage}
|
||||
className={`${actionButtonClassName} !px-design-10 justify-between`}
|
||||
>
|
||||
<SmartImage
|
||||
src={currentLanguageOption.icon}
|
||||
alt={currentLanguageLabel}
|
||||
className="h-design-14 w-design-14 shrink-0 rounded-full"
|
||||
imgClassName="object-cover"
|
||||
/>
|
||||
<div className="min-w-0 truncate">{currentLanguageLabel}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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'
|
||||
@@ -147,8 +146,6 @@ export function EntryPage() {
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
>
|
||||
{isMobile ? <MobileEntry /> : <PcEntry />}
|
||||
|
||||
<EntryNoticeGateModal />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MobileHeader } from '@/features/game/components/mobile/mobile-header.tsx'
|
||||
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts'
|
||||
|
||||
export function MobileEntry() {
|
||||
const { t } = useTranslation()
|
||||
useAutoHostingRunner()
|
||||
|
||||
return <div>{t('gameDesktop.mobile.placeholder')}</div>
|
||||
return (
|
||||
<>
|
||||
<MobileHeader />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DesktopHeader } from '@/features/game/components'
|
||||
import { DesktopHeader, EntryNoticeGateModal } from '@/features/game/components'
|
||||
import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
|
||||
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
||||
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
||||
@@ -22,9 +22,7 @@ export function PcEntry() {
|
||||
<>
|
||||
<DesktopHeader />
|
||||
<div
|
||||
className={
|
||||
'mx-auto mt-design-20 mb-design-75 w-[calc(100%-40*var(--design-unit))]'
|
||||
}
|
||||
className={'mx-auto my-design-10 w-[calc(100%-40*var(--design-unit))]'}
|
||||
>
|
||||
<DesktopStatusLine />
|
||||
</div>
|
||||
@@ -71,6 +69,8 @@ export function PcEntry() {
|
||||
<DesktopWithdrawTopupModal />
|
||||
{/* 大奖/小奖动画展示 */}
|
||||
<DesktopRewardOverlay />
|
||||
{/* 强制弹窗 */}
|
||||
<EntryNoticeGateModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,14 +38,18 @@ export function useGameHistoryVm() {
|
||||
const { i18n, t } = useTranslation()
|
||||
const accessToken = useAuthStore((state) => state.accessToken)
|
||||
const authStatus = useAuthStore((state) => state.status)
|
||||
const roundId = useGameRoundStore((state) => state.round.id)
|
||||
const winningCellId = useGameRoundStore((state) => state.round.winningCellId)
|
||||
const lastOpenedRoundRef = useRef<string | null>(null)
|
||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||
const revealRoundId = useGameRoundStore(
|
||||
(state) => state.revealAnimation.roundId,
|
||||
)
|
||||
const lastRevealedRoundRef = useRef<string | null>(null)
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ['game', 'bet-my-orders', accessToken],
|
||||
enabled: authStatus === 'authenticated' && Boolean(accessToken),
|
||||
initialPageParam: 1,
|
||||
refetchOnMount: 'always',
|
||||
staleTime: 0,
|
||||
queryFn: ({ pageParam }) =>
|
||||
getGameBetMyOrders({
|
||||
page: pageParam,
|
||||
@@ -63,72 +67,62 @@ export function useGameHistoryVm() {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
(query.data?.pages ?? []).flatMap((page) =>
|
||||
page.list.map((entry) => ({
|
||||
amountLabel: entry.total_amount,
|
||||
createdAtLabel: formatCreatedTime(
|
||||
entry.create_time,
|
||||
i18n.resolvedLanguage ?? 'en-US',
|
||||
),
|
||||
id: entry.order_no,
|
||||
resultState:
|
||||
entry.result_number === null
|
||||
? ('pending' satisfies HistoryResultState)
|
||||
: entry.numbers.includes(entry.result_number)
|
||||
? ('win' satisfies HistoryResultState)
|
||||
: ('lost' satisfies HistoryResultState),
|
||||
numbersLabel: formatNumbers(entry.numbers),
|
||||
numbers: entry.numbers,
|
||||
orderNo: entry.order_no,
|
||||
periodNo: entry.period_no,
|
||||
resultNumberLabel:
|
||||
entry.result_number === null
|
||||
? '--'
|
||||
: String(entry.result_number).padStart(2, '0'),
|
||||
winAmountLabel: entry.win_amount,
|
||||
})),
|
||||
page.list.map((entry) => {
|
||||
const shouldHideResult =
|
||||
entry.period_no === revealRoundId && revealPhase !== 'result'
|
||||
const resultNumber = shouldHideResult ? null : entry.result_number
|
||||
|
||||
return {
|
||||
amountLabel: entry.total_amount,
|
||||
createdAtLabel: formatCreatedTime(
|
||||
entry.create_time,
|
||||
i18n.resolvedLanguage ?? 'en-US',
|
||||
),
|
||||
id: entry.order_no,
|
||||
resultState:
|
||||
resultNumber === null
|
||||
? ('pending' satisfies HistoryResultState)
|
||||
: entry.numbers.includes(resultNumber)
|
||||
? ('win' satisfies HistoryResultState)
|
||||
: ('lost' satisfies HistoryResultState),
|
||||
numbersLabel: formatNumbers(entry.numbers),
|
||||
numbers: entry.numbers,
|
||||
orderNo: entry.order_no,
|
||||
periodNo: entry.period_no,
|
||||
resultNumberLabel:
|
||||
resultNumber === null
|
||||
? '--'
|
||||
: String(resultNumber).padStart(2, '0'),
|
||||
winAmountLabel: entry.win_amount,
|
||||
}
|
||||
}),
|
||||
),
|
||||
[i18n.resolvedLanguage, query.data?.pages],
|
||||
[i18n.resolvedLanguage, query.data?.pages, revealPhase, revealRoundId],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const openedRoundKey =
|
||||
winningCellId === null || roundId.length === 0
|
||||
? null
|
||||
: `${roundId}:${winningCellId}`
|
||||
|
||||
if (openedRoundKey === null) {
|
||||
if (revealPhase !== 'result' || !revealRoundId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (lastOpenedRoundRef.current === null) {
|
||||
lastOpenedRoundRef.current = openedRoundKey
|
||||
if (lastRevealedRoundRef.current === revealRoundId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (lastOpenedRoundRef.current === openedRoundKey) {
|
||||
if (authStatus !== 'authenticated' || query.isFetching || query.isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
lastOpenedRoundRef.current = openedRoundKey
|
||||
|
||||
if (
|
||||
authStatus !== 'authenticated' ||
|
||||
items.length >= GAME_HISTORY_PAGE_SIZE ||
|
||||
query.isFetching ||
|
||||
query.isLoading
|
||||
) {
|
||||
return
|
||||
}
|
||||
lastRevealedRoundRef.current = revealRoundId
|
||||
|
||||
void query.refetch()
|
||||
}, [
|
||||
authStatus,
|
||||
items.length,
|
||||
query.isFetching,
|
||||
query.isLoading,
|
||||
query.refetch,
|
||||
roundId,
|
||||
winningCellId,
|
||||
revealPhase,
|
||||
revealRoundId,
|
||||
])
|
||||
|
||||
return {
|
||||
|
||||
@@ -492,6 +492,8 @@ function applyPeriodOpenedMessage(
|
||||
message: GameSocketMessage,
|
||||
serverTime: number | null,
|
||||
) {
|
||||
console.log('%c[period.opened 开奖数据]', 'color: red;', message)
|
||||
|
||||
applyPeriodMessage(message, serverTime)
|
||||
|
||||
const period = extractPeriodEventData(message)
|
||||
@@ -580,6 +582,8 @@ function applyWalletChangedMessage(message: GameSocketMessage) {
|
||||
}
|
||||
|
||||
function applyJackpotHitMessage(message: GameSocketMessage) {
|
||||
console.log('%c[jackpot.hit 数据]', 'color: red;', message)
|
||||
|
||||
const jackpotHitData = extractJackpotHitData(message)
|
||||
|
||||
if (jackpotHitData?.hits.length) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
CircleUserRound,
|
||||
ClipboardList,
|
||||
LogOut,
|
||||
ReceiptText,
|
||||
WalletCards,
|
||||
} from 'lucide-react'
|
||||
@@ -13,8 +15,10 @@ import userInfoBg from '@/assets/system/userInfo-bg.webp'
|
||||
import { CenterModal } from '@/components/center-modal.tsx'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { logoutWithPassword } from '@/features/auth/api/auth-api'
|
||||
import DesktopFinanceRecordsTab from '@/features/game/modal/desktop/desktop-finance-records-tab'
|
||||
import DesktopWalletRecordsTab from '@/features/game/modal/desktop/desktop-wallet-records-tab'
|
||||
import { clearAuthenticatedSession } from '@/lib/auth/auth-session'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuthStore, useModalStore } from '@/store'
|
||||
@@ -88,6 +92,11 @@ function DesktopUserInfoModal() {
|
||||
const [activeTab, setActiveTab] = useState<UserInfoTabKey>('profile')
|
||||
const currentUser = useAuthStore((state) => state.currentUser)
|
||||
const inviteCode = currentUser?.registerInviteCode?.trim() ?? ''
|
||||
const logoutUsername =
|
||||
currentUser?.username ?? currentUser?.phone ?? currentUser?.name ?? ''
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: logoutWithPassword,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -112,6 +121,25 @@ function DesktopUserInfoModal() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
if (logoutMutation.isPending) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await logoutMutation.mutateAsync({
|
||||
password: '',
|
||||
username: logoutUsername,
|
||||
})
|
||||
notify.success(t('commonUi.toast.logoutSuccess'))
|
||||
} catch {
|
||||
notify.warning(t('commonUi.toast.logoutLocalOnly'))
|
||||
} finally {
|
||||
clearAuthenticatedSession({ clearBrowserStorage: true })
|
||||
setModalOpen('desktopUserInfo', false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CenterModal
|
||||
open={open}
|
||||
@@ -273,6 +301,24 @@ function DesktopUserInfoModal() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'w-full flex justify-end'}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleLogout()
|
||||
}}
|
||||
disabled={logoutMutation.isPending}
|
||||
className="mt-auto inline-flex h-design-44 min-w-design-170 cursor-pointer items-center justify-center gap-design-10 rounded-md border border-[#8F4747] bg-[#3A1111]/80 px-design-18 text-design-18 text-[#FFD7D7] transition-colors duration-200 hover:border-[#FF8A8A] hover:bg-[#5A1818]/85 hover:text-white focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#FF8A8A] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<LogOut className="h-design-20 w-design-20" />
|
||||
<span>
|
||||
{logoutMutation.isPending
|
||||
? t('game.modals.userInfo.profile.loggingOut')
|
||||
: t('game.modals.userInfo.profile.logout')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</SmartBackground>
|
||||
) : activeTab === 'financeRecords' ? (
|
||||
<DesktopFinanceRecordsTab
|
||||
|
||||
Reference in New Issue
Block a user