fix(auth): 解决认证错误处理和会话管理问题
- 替换 AUTH_INVALID_TOKEN_CODE 为 AUTH_RELOGIN_REQUIRED_CODES 数组支持多种错误码 - 实现 hasClearableSessionState 和 hasRecordedUnauthorizedSession 函数优化会话清理逻辑 - 添加 clearQueryCache 选项控制查询缓存清理行为 - 修复马来西亚手机号正则验证模式导致的用户名验证问题 - 更新 API 错误消息处理优先级,优先使用服务端返回的消息 - 添加服务器消息检查函数 hasServerMessage 避免重复错误提示 - 在登录表单中实现密码可见性切换功能 - 添加密码可见性国际化文案支持 - 实现页面历史记录抽屉组件和相关动效 - 优化模态框背景遮罩样式和键盘事件处理 - 调整多个组件的 z-index 层级避免显示冲突
This commit is contained in:
@@ -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 ||
|
||||
|
||||
113
src/features/game/components/shared/period-history-list.tsx
Normal file
113
src/features/game/components/shared/period-history-list.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user