VetNet-LCPS / vetcopilot-app /src /components /ProtectedRoute.tsx
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
2.1 kB
import React from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { useTranslation } from '../i18n'
interface ProtectedRouteProps {
children: React.ReactNode
/** 可指定的最小角色。admin = 超级管理员 */
requiredRole?: string
}
/**
* 路由守卫组件
*
* - 正在验证 token 时,显示加载中状态
* - 未认证时,重定向到 /login 并携带来源路径
* - 已认证时,检查 requiredRole 角色匹配,不匹配则重定向到首页
* - 已认证时,正常渲染子组件
*/
export default function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
const { t } = useTranslation()
const { isAuthenticated, isLoading, user } = useAuth()
const location = useLocation()
if (isLoading) {
return (
<div className="h-screen flex items-center justify-center bg-surface-50">
<div className="flex flex-col items-center gap-3">
<svg
className="animate-spin h-8 w-8 text-medical-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span className="text-sm text-gray-500">{t('components.protectedRoute.verifying')}</span>
</div>
</div>
)
}
if (!isAuthenticated) {
// 将当前路径作为 state 传递,登录后可跳回
return <Navigate to="/login" state={{ from: location.pathname }} replace />
}
// 角色检查:如果指定了 requiredRole,且用户角色不匹配,则重定向到首页
if (requiredRole && user?.role !== requiredRole) {
return <Navigate to="/" replace />
}
return <>{children}</>
}