diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx
new file mode 100644
index 0000000..bcd1193
--- /dev/null
+++ b/frontend/src/layouts/AppLayout/AppLayout.tsx
@@ -0,0 +1,129 @@
+// REQ-USR-003 / REQ-USR-004: 应用外壳(TopBar + NavOverlay + + AppFooter)。
+// 持有标签栈 / overlay 开关本地态(D3);据路由反向同步激活标签;登录态复用 Redux authSlice。
+import { useCallback, useEffect, useMemo, useState } from 'react';
+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 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 styles from './AppLayout.module.css';
+
+/** 据当前路径推导应激活/应打开的业务标签 key(路由 → 标签反向同步) */
+function deriveBizTab(pathname: string): BizTabKey | null {
+ if (matchPath('/usr/users/new', pathname) || matchPath('/usr/users/:id', pathname)) {
+ return 'userdetail';
+ }
+ if (matchPath('/usr/users', pathname)) {
+ return 'userlist';
+ }
+ return null;
+}
+
+export default function AppLayout() {
+ const user = useAppSelector((s) => s.auth.user);
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { message } = AntdApp.useApp();
+
+ const { tabs, activeKey, openTab, closeTab, setActive } = useTabStack();
+ const [navOverlayOpen, setNavOverlayOpen] = useState(false);
+
+ // 路由 → 标签反向同步:进入业务路由时确保对应标签打开并激活;回主页激活 home
+ useEffect(() => {
+ const biz = deriveBizTab(location.pathname);
+ if (biz) {
+ openTab(biz);
+ } else if (matchPath('/', location.pathname)) {
+ setActive('home');
+ }
+ // openTab/setActive 为稳定回调;仅在路径变化时同步
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [location.pathname]);
+
+ const handleSelectTab = useCallback(
+ (key: string) => {
+ const tab = tabs.find((t) => t.key === key);
+ if (tab) {
+ setActive(key);
+ navigate(tab.routePath);
+ }
+ },
+ [tabs, setActive, navigate],
+ );
+
+ const handleCloseTab = useCallback(
+ (key: string) => {
+ closeTab(key);
+ // 关闭联动后跳转到对应路由(BR5/BR6)
+ if (key === 'userlist') {
+ navigate('/');
+ } else if (key === 'userdetail') {
+ navigate(BIZ_TABS.userlist.routePath);
+ }
+ },
+ [closeTab, navigate],
+ );
+
+ const handleLogout = useCallback(() => {
+ dispatch(clearCredentials());
+ message.success(LOGOUT_SUCCESS_TEXT);
+ navigate('/login', { replace: true });
+ }, [dispatch, message, navigate]);
+
+ const handleLogoHome = useCallback(() => {
+ setActive('home');
+ navigate('/');
+ }, [setActive, navigate]);
+
+ const handleNavToggle = useCallback(() => {
+ setNavOverlayOpen((v) => !v);
+ }, []);
+
+ const handleOverlayNavigate = useCallback(
+ (routePath: string) => {
+ setNavOverlayOpen(false);
+ if (routePath === BIZ_TABS.userlist.routePath) {
+ openTab('userlist');
+ }
+ navigate(routePath);
+ },
+ [openTab, navigate],
+ );
+
+ const handleOverlayPlaceholder = useCallback(() => {
+ setNavOverlayOpen(false);
+ message.info(FEATURE_WIP_TEXT);
+ }, [message]);
+
+ const stableTabs = useMemo(() => tabs, [tabs]);
+
+ return (
+
+
+
+ setNavOverlayOpen(false)}
+ onNavigate={handleOverlayNavigate}
+ onPlaceholder={handleOverlayPlaceholder}
+ />
+
+
+
+ );
+}
diff --git a/frontend/tests/unit/AppLayout.shell.test.tsx b/frontend/tests/unit/AppLayout.shell.test.tsx
new file mode 100644
index 0000000..2ff61a8
--- /dev/null
+++ b/frontend/tests/unit/AppLayout.shell.test.tsx
@@ -0,0 +1,97 @@
+// REQ-USR-003: AppLayout 外壳装配 + 标签↔路由同步(ready / navOverlayOpen / tabOpen 态)
+import { describe, it, expect } from 'vitest';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Routes, Route, useLocation } from 'react-router-dom';
+import { renderShell } from './renderShell';
+import AppLayout from '../../src/layouts/AppLayout/AppLayout';
+import type { AuthUser } from '../../src/api/types';
+
+const ADMIN: AuthUser = { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' };
+
+function LocProbe() {
+ const loc = useLocation();
+ return {loc.pathname}
;
+}
+
+function renderLayout(initialEntries: string[]) {
+ return renderShell(
+
+ }>
+
+
+ home-outlet
+ >
+ }
+ />
+
+
+ users-outlet
+ >
+ }
+ />
+
+ ,
+ { initialEntries, preloadedAuth: { token: 't', user: ADMIN } },
+ );
+}
+
+describe('AppLayout shell', () => {
+ it('renders TopBar + Outlet when ready', () => {
+ renderLayout(['/']);
+ // 顶栏(全部导航 + 当前用户)
+ expect(screen.getByRole('button', { name: '全部导航' })).toBeInTheDocument();
+ expect(screen.getByText('朱子纯(超级管理员)')).toBeInTheDocument();
+ // Outlet 子内容
+ expect(screen.getByTestId('home-outlet')).toBeInTheDocument();
+ });
+
+ it('toggle 全部导航 opens/closes overlay', async () => {
+ renderLayout(['/']);
+ expect(screen.queryByTestId('nav-overlay')).not.toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button', { name: '全部导航' }));
+ expect(screen.getByTestId('nav-overlay')).toBeInTheDocument();
+ // 点遮罩关闭
+ await userEvent.click(screen.getByTestId('nav-overlay-mask'));
+ expect(screen.queryByTestId('nav-overlay')).not.toBeInTheDocument();
+ });
+
+ it('nav overlay 用户列表 navigates and opens tab', async () => {
+ renderLayout(['/']);
+ await userEvent.click(screen.getByRole('button', { name: '全部导航' }));
+ await userEvent.click(screen.getByRole('button', { name: /用户列表/ }));
+ // overlay 关闭
+ expect(screen.queryByTestId('nav-overlay')).not.toBeInTheDocument();
+ // URL 到 /usr/users
+ expect(screen.getByTestId('loc').textContent).toBe('/usr/users');
+ // 顶栏出现「用户列表」标签并激活
+ const tab = screen.getByTestId('tab-userlist');
+ expect(tab).toHaveTextContent('用户列表');
+ expect(tab.getAttribute('aria-pressed')).toBe('true');
+ });
+
+ it('clicking home tab navigates back to /', async () => {
+ renderLayout(['/']);
+ // 先打开用户列表标签
+ await userEvent.click(screen.getByRole('button', { name: '全部导航' }));
+ await userEvent.click(screen.getByRole('button', { name: /用户列表/ }));
+ expect(screen.getByTestId('loc').textContent).toBe('/usr/users');
+ // 点主页标签回 /
+ await userEvent.click(screen.getByTestId('tab-home'));
+ expect(screen.getByTestId('loc').textContent).toBe('/');
+ expect(screen.getByTestId('tab-home').getAttribute('aria-pressed')).toBe('true');
+ });
+
+ it('active tab syncs with current route', () => {
+ renderLayout(['/usr/users']);
+ // 直接进 /usr/users,用户列表标签应存在且激活
+ const tab = screen.getByTestId('tab-userlist');
+ expect(tab.getAttribute('aria-pressed')).toBe('true');
+ });
+});