feat(auth): 集成认证授权功能并优化API客户端
- 实现了完整的登录注册认证流程,包括密码验证和用户资料获取 - 集成了JWT令牌管理和自动刷新机制,支持设备ID生成和管理 - 添加了WebSocket连接配置和API基础URL环境变量设置 - 实现了API客户端的请求拦截器,包括令牌验证和错误处理逻辑 - 集成了MD5加密和认证令牌缓存机制,提升安全性 - 添加了多语言国际化支持,包括英语、中文、马来语和印尼语 - 实现了认证状态管理和本地存储持久化功能 - 添加了表单验证schema和错误处理机制,增强用户体验
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { type ReactNode, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import modalBg from '@/assets/system/modal-bg.webp'
|
||||
import modalClose from '@/assets/system/modal-close.webp'
|
||||
import modalNormalBg from '@/assets/system/modal-normal-bg.png'
|
||||
@@ -29,6 +30,7 @@ export function CenterModal({
|
||||
children,
|
||||
className,
|
||||
}: CenterModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const handleClose = () => {
|
||||
onClose?.()
|
||||
}
|
||||
@@ -63,7 +65,11 @@ export function CenterModal({
|
||||
<SmartBackground
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={typeof title === 'string' ? title : 'Modal'}
|
||||
aria-label={
|
||||
typeof title === 'string'
|
||||
? title
|
||||
: t('commonUi.modal.defaultAriaLabel')
|
||||
}
|
||||
className={cn(
|
||||
'relative flex h-design-640 w-design-720 flex-col overflow-hidden rounded-[calc(var(--design-unit)*28)] px-design-20 text-white',
|
||||
className,
|
||||
@@ -93,7 +99,7 @@ export function CenterModal({
|
||||
{isShowClose && onClose ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close modal"
|
||||
aria-label={t('commonUi.modal.close')}
|
||||
onClick={handleClose}
|
||||
className={cn(
|
||||
'absolute top-1/2 inline-flex h-design-60 w-design-60 -translate-y-1/2 items-center justify-center rounded-full transition hover:scale-105 active:scale-95',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { AppLanguage } from '@/i18n'
|
||||
import { type AppLanguage, supportedLanguages } from '@/i18n'
|
||||
|
||||
const languagePrefixPattern = new RegExp(
|
||||
`^/(${supportedLanguages.join('|')})(?=/|$)`,
|
||||
)
|
||||
|
||||
interface LanguageLinkProps {
|
||||
currentPathname: string
|
||||
@@ -14,7 +18,7 @@ export function LanguageLink({
|
||||
language,
|
||||
}: LanguageLinkProps) {
|
||||
const nextPathname = currentPathname.replace(
|
||||
/^\/(zh-CN|en-US)(?=\/|$)/,
|
||||
languagePrefixPattern,
|
||||
`/${language}`,
|
||||
)
|
||||
|
||||
|
||||
71
src/components/ui/toaster.tsx
Normal file
71
src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
CheckCircle2,
|
||||
Info,
|
||||
LoaderCircle,
|
||||
TriangleAlert,
|
||||
X,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { notify, useNotificationStore } from '@/lib/notify'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const TOAST_ICON_BY_TYPE = {
|
||||
error: <XCircle className="h-4 w-4 shrink-0 text-[#FF8A9E]" />,
|
||||
info: <Info className="h-4 w-4 shrink-0 text-[#7CE8FF]" />,
|
||||
loading: (
|
||||
<LoaderCircle className="h-4 w-4 shrink-0 animate-spin text-[#7CE8FF]" />
|
||||
),
|
||||
success: <CheckCircle2 className="h-4 w-4 shrink-0 text-[#7CF0B8]" />,
|
||||
warning: <TriangleAlert className="h-4 w-4 shrink-0 text-[#FFD66E]" />,
|
||||
} as const
|
||||
|
||||
const TOAST_TONE_CLASS_BY_TYPE = {
|
||||
error: 'game-toast-error',
|
||||
info: 'game-toast-info',
|
||||
loading: 'game-toast-loading',
|
||||
success: 'game-toast-success',
|
||||
warning: 'game-toast-warning',
|
||||
} as const
|
||||
|
||||
export function AppToaster() {
|
||||
const toasts = useNotificationStore((state) => state.toasts)
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-atomic="true"
|
||||
aria-live="polite"
|
||||
className="game-toaster pointer-events-none fixed top-[calc(var(--design-unit)*88)] left-1/2 z-[9999] flex w-full -translate-x-1/2 flex-col items-center gap-3 px-4 md:top-[calc(var(--design-unit)*88)]"
|
||||
>
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
role="status"
|
||||
className={cn(
|
||||
'game-toast pointer-events-auto',
|
||||
TOAST_TONE_CLASS_BY_TYPE[toast.type],
|
||||
)}
|
||||
>
|
||||
<span aria-hidden="true" className="game-toast-icon">
|
||||
{TOAST_ICON_BY_TYPE[toast.type]}
|
||||
</span>
|
||||
|
||||
<div className="game-toast-content">
|
||||
<div className="game-toast-title">{toast.message}</div>
|
||||
{toast.description ? (
|
||||
<div className="game-toast-description">{toast.description}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close notification"
|
||||
onClick={() => notify.dismiss(toast.id)}
|
||||
className="game-toast-close"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 text-[#D5FBFF]" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user