fix(auth): 解决认证错误处理和会话管理问题

- 替换 AUTH_INVALID_TOKEN_CODE 为 AUTH_RELOGIN_REQUIRED_CODES 数组支持多种错误码
- 实现 hasClearableSessionState 和 hasRecordedUnauthorizedSession 函数优化会话清理逻辑
- 添加 clearQueryCache 选项控制查询缓存清理行为
- 修复马来西亚手机号正则验证模式导致的用户名验证问题
- 更新 API 错误消息处理优先级,优先使用服务端返回的消息
- 添加服务器消息检查函数 hasServerMessage 避免重复错误提示
- 在登录表单中实现密码可见性切换功能
- 添加密码可见性国际化文案支持
- 实现页面历史记录抽屉组件和相关动效
- 优化模态框背景遮罩样式和键盘事件处理
- 调整多个组件的 z-index 层级避免显示冲突
This commit is contained in:
JiaJun
2026-06-01 17:46:01 +08:00
parent 87e8aca97d
commit 72b6de499e
51 changed files with 3812 additions and 149 deletions

View File

@@ -346,7 +346,7 @@ export function DesktopAnimalOverlay({
return (
<div
className={cn(
'absolute inset-0 z-50 flex items-center justify-center overflow-hidden bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px] transition-opacity duration-300',
'absolute inset-0 z-40 flex items-center justify-center overflow-hidden bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px] transition-opacity duration-300',
isRewardFadingOut && 'pointer-events-none opacity-0',
)}
>
@@ -399,14 +399,14 @@ export function DesktopAnimalOverlay({
return (
<div
aria-hidden="true"
className="absolute inset-0 z-50 bg-[rgba(2,8,14,0.72)] backdrop-blur-[2px]"
className="absolute inset-0 z-40 bg-[rgba(2,8,14,0.72)] backdrop-blur-[2px]"
/>
)
}
if (hostingFlag) {
return (
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center gap-design-22 bg-[rgba(2,8,14,0.38)] px-design-24 backdrop-blur-[1px]">
<div className="absolute inset-0 z-40 flex flex-col items-center justify-center gap-design-22 bg-[rgba(2,8,14,0.38)] px-design-24 backdrop-blur-[1px]">
{showStopOverlay ? (
<div className="relative h-design-170 w-design-520 max-w-[72%] overflow-visible">
<SmartImage
@@ -477,7 +477,7 @@ export function DesktopAnimalOverlay({
if (showStopOverlay) {
return (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px]">
<div className="absolute inset-0 z-40 flex items-center justify-center bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px]">
<div className="flex max-w-[88%] flex-col items-center justify-center gap-design-18">
<StopBetSummary
items={stopBetItems}

View File

@@ -18,8 +18,11 @@ import { SmartImage } from '@/components/smart-image.tsx'
import { ACTION_OPTIONS } from '@/constants'
import { useGameControlVm } from '@/features/game/hooks/use-game-control-vm.ts'
import { cn } from '@/lib/utils'
import { useModalStore } from '@/store'
export function DesktopControl() {
const { t } = useTranslation()
const setModalOpen = useModalStore((state) => state.setModalOpen)
const {
acceptingBets,
actionsEnabled,
@@ -112,10 +115,14 @@ export function DesktopControl() {
}
>
<SmartBackground
as="button"
type="button"
src={leftBottomBg}
size="100% 100%"
onClick={() => setModalOpen('desktopPeriodHistory', true)}
aria-label={t('gameDesktop.periodHistory.title')}
className={
'flex flex-col items-center gap-1 justify-center h-full w-design-110 shrink-0 bg-center bg-no-repeat'
'flex flex-col items-center gap-1 justify-center h-full w-design-110 shrink-0 cursor-pointer bg-center bg-no-repeat transition-[filter] duration-200 hover:brightness-125 focus-visible:ring-2 focus-visible:ring-[#4FEAFF]'
}
>
<div className={'flex flex-col items-center justify-center'}>
@@ -254,7 +261,7 @@ export function DesktopControl() {
}
}
transition={{ type: 'spring', stiffness: 380, damping: 24 }}
className={'relative z-[1]'}
className={'relative z-10'}
>
<motion.img
src={chip.src}
@@ -267,12 +274,12 @@ export function DesktopControl() {
src={chipLock}
alt="chip-locked"
draggable={false}
className="pointer-events-none absolute left-1/2 top-1/2 z-[12] h-design-54 w-design-54 -translate-x-1/2 -translate-y-1/2 object-contain"
className="pointer-events-none absolute left-1/2 top-1/2 z-40 h-design-54 w-design-54 -translate-x-1/2 -translate-y-1/2 object-contain"
/>
)}
<span
className={
'pointer-events-none absolute inset-x-0 top-1/2 z-[8] -translate-y-[calc(50%-1*var(--design-unit))] text-center text-design-16 font-black leading-none tracking-[0.06em] text-[rgba(96,54,0,0.85)] blur-[1px]'
'pointer-events-none absolute inset-x-0 top-1/2 z-20 -translate-y-[calc(50%-1*var(--design-unit))] text-center text-design-16 font-black leading-none tracking-[0.06em] text-[rgba(96,54,0,0.85)] blur-[1px]'
}
>
{chip.valueLabel}
@@ -286,7 +293,7 @@ export function DesktopControl() {
</span>
<span
className={
'pointer-events-none absolute inset-x-0 top-1/2 z-[11] -translate-y-1/2 text-center text-design-16 font-black leading-none tracking-[0.06em] text-white [text-shadow:0_1px_0_rgba(255,255,255,0.6),0_2px_4px_rgba(0,0,0,0.72),0_0_10px_rgba(255,255,255,0.22)]'
'pointer-events-none absolute inset-x-0 top-1/2 z-30 -translate-y-1/2 text-center text-design-16 font-black leading-none tracking-[0.06em] text-white [text-shadow:0_1px_0_rgba(255,255,255,0.6),0_2px_4px_rgba(0,0,0,0.72),0_0_10px_rgba(255,255,255,0.22)]'
}
>
{chip.valueLabel}

View File

@@ -44,7 +44,6 @@ export function DesktopStatusLine() {
<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 />*/}
@@ -66,9 +65,7 @@ export function DesktopStatusLine() {
>
{showStreakLimitOnly && (
<div
className={
'pointer-events-none absolute z-0 bg-center bg-no-repeat'
}
className={'pointer-events-none absolute bg-center bg-no-repeat'}
style={{
top: 'calc(var(--design-unit)*-14)',
right: 'calc(var(--design-unit)*-190)',
@@ -138,7 +135,7 @@ export function DesktopStatusLine() {
<div className="relative z-20 flex h-[105px] w-design-360 items-center justify-center">
<SmartBackground
src={statusCenter}
className="pointer-events-none absolute inset-0 z-0 bg-no-repeat bg-center bg-contain transition-opacity duration-500 ease-out"
className="pointer-events-none absolute inset-0 bg-no-repeat bg-center bg-contain transition-opacity duration-500 ease-out"
size="contain"
style={{
opacity: showWarningCountdown ? 0.18 : 1,
@@ -146,7 +143,7 @@ export function DesktopStatusLine() {
}}
/>
<div
className="pointer-events-none absolute inset-0 z-0 overflow-visible transition-all duration-500 ease-out"
className="pointer-events-none absolute inset-0 overflow-visible transition-all duration-500 ease-out"
style={{
opacity: showWarningCountdown ? 1 : 0,
transform: showWarningCountdown

View File

@@ -2,6 +2,30 @@ import broadcast from '@/assets/system/broadcast.webp'
import { SmartImage } from '@/components/smart-image.tsx'
import { useGameSessionStore } from '@/store/game'
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)
return Number.isFinite(amount) ? winAmountFormatter.format(amount) : value
}
export function DesktopTitle() {
const jackpotBroadcasts = useGameSessionStore(
(state) => state.jackpotBroadcasts,
@@ -10,7 +34,9 @@ export function DesktopTitle() {
jackpotBroadcasts.length > 0
? jackpotBroadcasts.map((broadcast) => ({
id: broadcast.id,
message: broadcast.message,
message: `Player ${maskParticipantLabel(
broadcast.userId,
)} won ${formatWinAmount(broadcast.totalWin)}`,
}))
: [{ id: 'empty', message: '' }]
const marqueeTitles =
@@ -31,12 +57,14 @@ export function DesktopTitle() {
<div className="relative h-design-28 min-w-0 flex-1 overflow-hidden">
<div
className={
jackpotBroadcasts.length > 0 ? 'desktop-title-vertical-marquee' : ''
jackpotBroadcasts.length > 0
? 'desktop-title-horizontal-marquee'
: ''
}
>
{marqueeTitles.map((title) => (
<div
className="flex h-design-28 items-center whitespace-nowrap !text-[#FF970F]"
className="flex h-design-28 shrink-0 items-center whitespace-nowrap !text-[#FF970F]"
key={title.id}
>
{title.message}

View File

@@ -16,6 +16,7 @@ import {
import { getNoticeList } from '@/features/game/api'
import { cn } from '@/lib/utils'
import { useAuthStore } from '@/store/auth'
import { useModalStore } from '@/store/modal'
function getLastConfirmedAt(storageKey: string) {
if (typeof localStorage === 'undefined') {
@@ -41,12 +42,21 @@ export function EntryNoticeGateModal() {
const authIsHydrated = useAuthStore((state) => state.isHydrated)
const accessToken = useAuthStore((state) => state.accessToken)
const currentUserId = useAuthStore((state) => state.currentUser?.id)
const lastUnauthorizedAt = useAuthStore((state) => state.lastUnauthorizedAt)
const isDesktopLoginOpen = useModalStore((state) => state.modals.desktopLogin)
const isDesktopRegisterOpen = useModalStore(
(state) => state.modals.desktopRegister,
)
const [hasEntered, setHasEntered] = useState(false)
const [hasAgreed, setHasAgreed] = useState(false)
const [shouldGateEntry, setShouldGateEntry] = useState(false)
const hasStoredLoginInfo =
authStatus === 'authenticated' && Boolean(accessToken)
const isReloginRequired =
authStatus === 'anonymous' && Boolean(lastUnauthorizedAt)
const isAuthModalOpen = isDesktopLoginOpen || isDesktopRegisterOpen
const shouldSuppressEntryGate = isReloginRequired || isAuthModalOpen
const confirmedAtStorageKey = `${ENTRY_NOTICE_LAST_CONFIRMED_AT_KEY}:${
currentUserId ?? 'authenticated'
}`
@@ -60,7 +70,7 @@ export function EntryNoticeGateModal() {
setHasAgreed(false)
if (!hasStoredLoginInfo) {
setShouldGateEntry(true)
setShouldGateEntry(!shouldSuppressEntryGate)
return
}
@@ -71,12 +81,17 @@ export function EntryNoticeGateModal() {
!lastConfirmedAt ||
Date.now() - lastConfirmedAt >= ENTRY_NOTICE_CONFIRM_INTERVAL_MS,
)
}, [authIsHydrated, confirmedAtStorageKey, hasStoredLoginInfo])
}, [
authIsHydrated,
confirmedAtStorageKey,
hasStoredLoginInfo,
shouldSuppressEntryGate,
])
const noticeListQuery = useQuery({
queryKey: ['game', 'entry-notice-list'],
queryFn: () => getNoticeList({ page: 1, pageSize: 20 }),
enabled: authIsHydrated && shouldGateEntry,
enabled: authIsHydrated && shouldGateEntry && !shouldSuppressEntryGate,
staleTime: 0,
})
@@ -91,6 +106,7 @@ export function EntryNoticeGateModal() {
const shouldShowModal =
authIsHydrated &&
shouldGateEntry &&
!shouldSuppressEntryGate &&
!hasEntered &&
(noticeListQuery.isPending ||
noticeListQuery.isError ||

View File

@@ -0,0 +1,113 @@
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
import type { PeriodHistoryDisplayItem } from '@/features/game/hooks/use-period-history-vm'
import { cn } from '@/lib/utils'
interface PeriodHistoryListLabels {
empty: string
failed: string
loading: string
retry: string
}
interface PeriodHistoryListProps {
className?: string
isError: boolean
isLoading: boolean
items: PeriodHistoryDisplayItem[]
labels: PeriodHistoryListLabels
onRetry: () => void
}
export function PeriodHistoryList({
className,
isError,
isLoading,
items,
labels,
onRetry,
}: PeriodHistoryListProps) {
if (isLoading) {
return (
<DataLoadingIndicator label={labels.loading} className="min-h-full" />
)
}
if (isError) {
return (
<div className="flex min-h-full flex-col items-center justify-center gap-design-16 text-design-18 text-[#8DBCC2]">
<span>{labels.failed}</span>
<button
type="button"
className="cursor-pointer rounded-[calc(var(--design-unit)*5)] border border-[rgba(85,236,255,0.48)] px-design-20 py-design-8 text-design-16 text-[#D5FBFF] transition-colors duration-200 hover:bg-[rgba(85,236,255,0.12)]"
onClick={onRetry}
>
{labels.retry}
</button>
</div>
)
}
if (items.length === 0) {
return (
<div className="flex min-h-full items-center justify-center text-design-18 text-[#8DBCC2]">
{labels.empty}
</div>
)
}
return (
<div className={cn('grid grid-cols-4 gap-design-16', className)}>
{items.map((item) => (
<div
key={`${item.periodNo}-${item.openTime}`}
className="relative grid h-design-70 min-w-0 grid-cols-[minmax(calc(var(--design-unit)*70),1fr)_auto_auto] items-center gap-design-12 overflow-hidden rounded-[calc(var(--design-unit)*5)] border border-[rgba(92,221,242,0.38)] bg-[linear-gradient(180deg,rgba(9,28,43,0.9),rgba(4,15,26,0.92))] px-design-10 shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(70,221,255,0.12),0_0_calc(var(--design-unit)*8)_rgba(53,212,255,0.08)]"
title={item.periodNo}
>
<span className="min-w-0 truncate text-design-16 font-semibold text-white">
{item.displayPeriodNo}
</span>
<span
className={cn(
'relative flex h-design-50 w-design-58 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*6)] text-design-30 font-black leading-none before:pointer-events-none before:absolute before:left-0 before:top-0 before:h-design-13 before:w-design-13 before:rounded-tl-[calc(var(--design-unit)*6)] before:border-l before:border-t after:pointer-events-none after:absolute after:right-0 after:top-0 after:h-design-13 after:w-design-13 after:rounded-tr-[calc(var(--design-unit)*6)] after:border-r after:border-t',
item.isOdd
? 'bg-[rgba(54,5,15,0.5)] text-[#FF3959] shadow-[0_0_calc(var(--design-unit)*12)_rgba(255,55,81,0.2),inset_0_0_calc(var(--design-unit)*10)_rgba(255,55,81,0.12)] before:border-[rgba(255,55,81,0.86)] after:border-[rgba(255,55,81,0.86)]'
: 'bg-[rgba(9,28,75,0.56)] text-[#4B91FF] shadow-[0_0_calc(var(--design-unit)*12)_rgba(69,137,255,0.22),inset_0_0_calc(var(--design-unit)*10)_rgba(69,137,255,0.16)] before:border-[rgba(69,137,255,0.9)] after:border-[rgba(69,137,255,0.9)]',
)}
>
<span
aria-hidden="true"
className={cn(
'pointer-events-none absolute bottom-0 left-0 h-design-13 w-design-13 rounded-bl-[calc(var(--design-unit)*6)] border-b border-l',
item.isOdd
? 'border-[rgba(255,55,81,0.86)]'
: 'border-[rgba(69,137,255,0.9)]',
)}
/>
<span
aria-hidden="true"
className={cn(
'pointer-events-none absolute bottom-0 right-0 h-design-13 w-design-13 rounded-br-[calc(var(--design-unit)*6)] border-b border-r',
item.isOdd
? 'border-[rgba(255,55,81,0.86)]'
: 'border-[rgba(69,137,255,0.9)]',
)}
/>
{item.displayResultNumber}
</span>
<span className="flex h-design-50 w-design-72 shrink-0 items-center justify-end overflow-hidden">
{item.image ? (
<img
src={item.image}
alt={item.displayResultNumber}
draggable={false}
className="h-full w-auto max-w-none shrink-0 object-contain"
/>
) : (
<span className="text-design-13 text-[#668C92]">--</span>
)}
</span>
</div>
))}
</div>
)
}

View File

@@ -52,7 +52,7 @@ export function RoundBettingStartAlert({
<motion.div
aria-live="polite"
className={cn(
'pointer-events-none left-1/2 top-1/2 z-50 w-[min(calc(var(--design-unit)*560),88vw)] -translate-x-1/2 -translate-y-1/2',
'pointer-events-none left-1/2 top-1/2 z-30 w-[min(calc(var(--design-unit)*560),88vw)] -translate-x-1/2 -translate-y-1/2',
placement === 'fixed' ? 'fixed' : 'absolute',
className,
)}