diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index bcd1193..cc99ea9 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -5,11 +5,12 @@ import { Outlet, useLocation, useNavigate, matchPath } from 'react-router-dom'; import { App as AntdApp } from 'antd'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { clearCredentials } from '../../store/slices/authSlice'; +import { registerUnauthorizedHandler } from '../../api/request'; import TopBar from './TopBar'; import NavOverlay from './NavOverlay'; import { useTabStack, BIZ_TABS } from './useTabStack'; import type { BizTabKey } from './useTabStack'; -import { LOGOUT_SUCCESS_TEXT, FEATURE_WIP_TEXT } from './shellMessages'; +import { LOGOUT_SUCCESS_TEXT, FEATURE_WIP_TEXT, SESSION_EXPIRED_TEXT } from './shellMessages'; import styles from './AppLayout.module.css'; /** 据当前路径推导应激活/应打开的业务标签 key(路由 → 标签反向同步) */ @@ -80,6 +81,17 @@ export default function AppLayout() { navigate('/'); }, [setActive, navigate]); + // 被动 401 统一登出(BR10 / D11):壳层挂载时注册回调,拦截器捕获 HTTP 401 时调用之。 + // 拦截器内无法用 React hooks,故由此处注入 clearCredentials + message.warning + 跳 /login。 + useEffect(() => { + registerUnauthorizedHandler(() => { + dispatch(clearCredentials()); + message.warning(SESSION_EXPIRED_TEXT); + navigate('/login', { replace: true }); + }); + return () => registerUnauthorizedHandler(null); + }, [dispatch, message, navigate]); + const handleNavToggle = useCallback(() => { setNavOverlayOpen((v) => !v); }, []); diff --git a/frontend/tests/unit/AppLayout.unauthorized.test.tsx b/frontend/tests/unit/AppLayout.unauthorized.test.tsx new file mode 100644 index 0000000..9b7d457 --- /dev/null +++ b/frontend/tests/unit/AppLayout.unauthorized.test.tsx @@ -0,0 +1,67 @@ +// REQ-USR-004: AppLayout 注册 401 登出处理(壳层接线,BR10) +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { screen, act } from '@testing-library/react'; +import { Routes, Route, useLocation } from 'react-router-dom'; +import { renderShell } from './renderShell'; +import AppLayout from '../../src/layouts/AppLayout/AppLayout'; +import { + registerUnauthorizedHandler, + TOKEN_STORAGE_KEY, +} from '../../src/api/request'; +import { SESSION_EXPIRED_TEXT } from '../../src/layouts/AppLayout/shellMessages'; +import type { AuthUser } from '../../src/api/types'; + +const ADMIN: AuthUser = { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' }; + +function LocProbe() { + const loc = useLocation(); + return