- 替换 AUTH_INVALID_TOKEN_CODE 为 AUTH_RELOGIN_REQUIRED_CODES 数组支持多种错误码 - 实现 hasClearableSessionState 和 hasRecordedUnauthorizedSession 函数优化会话清理逻辑 - 添加 clearQueryCache 选项控制查询缓存清理行为 - 修复马来西亚手机号正则验证模式导致的用户名验证问题 - 更新 API 错误消息处理优先级,优先使用服务端返回的消息 - 添加服务器消息检查函数 hasServerMessage 避免重复错误提示 - 在登录表单中实现密码可见性切换功能 - 添加密码可见性国际化文案支持 - 实现页面历史记录抽屉组件和相关动效 - 优化模态框背景遮罩样式和键盘事件处理 - 调整多个组件的 z-index 层级避免显示冲突
182 lines
6.4 KiB
TypeScript
182 lines
6.4 KiB
TypeScript
import { X } from 'lucide-react'
|
|
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { PeriodHistoryList } from '@/features/game/components/shared/period-history-list'
|
|
import {
|
|
DEFAULT_PERIOD_HISTORY_LIMIT,
|
|
type PeriodHistoryDisplayItem,
|
|
usePeriodHistoryVm,
|
|
} from '@/features/game/hooks/use-period-history-vm'
|
|
import { useModalStore } from '@/store'
|
|
|
|
const OVERLAY_EASE = [0.16, 1, 0.3, 1] as const
|
|
const DRAWER_TRANSITION = {
|
|
type: 'tween',
|
|
duration: 0.34,
|
|
ease: OVERLAY_EASE,
|
|
} as const
|
|
|
|
interface PeriodHistoryDrawerLabels {
|
|
close: string
|
|
empty: string
|
|
failed: string
|
|
loading: string
|
|
retry: string
|
|
title: string
|
|
}
|
|
|
|
interface DesktopPeriodHistoryDrawerViewProps {
|
|
isError: boolean
|
|
isLoading: boolean
|
|
items: PeriodHistoryDisplayItem[]
|
|
labels: PeriodHistoryDrawerLabels
|
|
onClose: () => void
|
|
onRetry: () => void
|
|
open: boolean
|
|
}
|
|
|
|
export function DesktopPeriodHistoryDrawer() {
|
|
const { t } = useTranslation()
|
|
const open = useModalStore((state) => state.modals.desktopPeriodHistory)
|
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
|
const vm = usePeriodHistoryVm({
|
|
enabled: open,
|
|
limit: DEFAULT_PERIOD_HISTORY_LIMIT,
|
|
})
|
|
const handleClose = () => {
|
|
setModalOpen('desktopPeriodHistory', false)
|
|
}
|
|
|
|
return (
|
|
<DesktopPeriodHistoryDrawerView
|
|
open={open}
|
|
items={vm.items}
|
|
isLoading={vm.isLoading}
|
|
isError={vm.isError}
|
|
labels={{
|
|
close: t('gameDesktop.periodHistory.close'),
|
|
empty: t('gameDesktop.periodHistory.empty'),
|
|
failed: t('gameDesktop.periodHistory.failed'),
|
|
loading: t('gameDesktop.periodHistory.loading'),
|
|
retry: t('gameDesktop.periodHistory.retry'),
|
|
title: t('gameDesktop.periodHistory.title'),
|
|
}}
|
|
onClose={handleClose}
|
|
onRetry={() => void vm.refetch()}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function DesktopPeriodHistoryDrawerView({
|
|
isError,
|
|
isLoading,
|
|
items,
|
|
labels,
|
|
onClose,
|
|
onRetry,
|
|
open,
|
|
}: DesktopPeriodHistoryDrawerViewProps) {
|
|
const prefersReducedMotion = useReducedMotion()
|
|
const [isDrawerAnimating, setIsDrawerAnimating] = useState(false)
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{open && (
|
|
<>
|
|
<motion.button
|
|
type="button"
|
|
aria-label={labels.close}
|
|
className="fixed left-0 right-0 top-0 bottom-[calc(var(--design-unit)*150)] z-30 cursor-default bg-black/48"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{
|
|
duration: prefersReducedMotion ? 0.12 : 0.26,
|
|
ease: OVERLAY_EASE,
|
|
}}
|
|
onClick={onClose}
|
|
/>
|
|
<motion.aside
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={labels.title}
|
|
className="fixed left-0 top-design-16 bottom-[calc(var(--design-unit)*150)] z-40 flex w-design-1120 max-w-[calc(100vw-var(--design-unit)*24)] origin-left flex-col overflow-hidden rounded-r-[calc(var(--design-unit)*10)] border border-[rgba(81,230,255,0.62)] bg-[linear-gradient(180deg,rgba(6,19,32,0.98),rgba(3,12,22,0.96))] text-[#D5FBFF] shadow-[0_0_calc(var(--design-unit)*18)_rgba(39,216,255,0.28),0_0_calc(var(--design-unit)*54)_rgba(39,216,255,0.16),inset_0_0_calc(var(--design-unit)*18)_rgba(74,224,255,0.16)]"
|
|
initial={
|
|
prefersReducedMotion
|
|
? { opacity: 0 }
|
|
: { x: '-100%', opacity: 0.98 }
|
|
}
|
|
animate={
|
|
prefersReducedMotion ? { opacity: 1 } : { x: 0, opacity: 1 }
|
|
}
|
|
exit={
|
|
prefersReducedMotion
|
|
? { opacity: 0 }
|
|
: { x: '-100%', opacity: 0.98 }
|
|
}
|
|
transition={
|
|
prefersReducedMotion ? { duration: 0.12 } : DRAWER_TRANSITION
|
|
}
|
|
onAnimationStart={() => setIsDrawerAnimating(true)}
|
|
onAnimationComplete={() => setIsDrawerAnimating(false)}
|
|
style={
|
|
isDrawerAnimating
|
|
? { willChange: 'transform, opacity' }
|
|
: undefined
|
|
}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-x-design-8 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(80,241,255,0.96),transparent)]"
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute bottom-0 left-0 h-design-28 w-design-28 border-b-2 border-l-2 border-[#28E6FF]"
|
|
/>
|
|
<span
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute bottom-0 right-0 h-design-28 w-design-28 border-b-2 border-r-2 border-[#28E6FF]"
|
|
/>
|
|
<div className="relative flex h-design-78 shrink-0 items-center justify-between border-b border-[rgba(80,224,255,0.38)] px-design-42">
|
|
<h2 className="text-design-28 font-bold leading-none text-white [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(156,244,255,0.42)]">
|
|
{labels.title}
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
aria-label={labels.close}
|
|
className="flex h-design-42 w-design-42 cursor-pointer items-center justify-center text-[#C8F7FF] transition-colors duration-200 hover:text-white focus-visible:ring-2 focus-visible:ring-[#4FEAFF]"
|
|
onClick={onClose}
|
|
>
|
|
<X size={32} strokeWidth={2.1} />
|
|
</button>
|
|
</div>
|
|
<motion.div
|
|
className="history-scroll-hidden min-h-0 flex-1 overflow-y-auto px-design-34 py-design-26"
|
|
initial={
|
|
prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 8 }
|
|
}
|
|
animate={
|
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
|
}
|
|
transition={
|
|
prefersReducedMotion
|
|
? { duration: 0.12 }
|
|
: { duration: 0.22, delay: 0.08, ease: OVERLAY_EASE }
|
|
}
|
|
>
|
|
<PeriodHistoryList
|
|
items={items}
|
|
isLoading={isLoading}
|
|
isError={isError}
|
|
labels={labels}
|
|
onRetry={onRetry}
|
|
/>
|
|
</motion.div>
|
|
</motion.aside>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|