diff --git a/frontend/src/layouts/AppLayout/CurrentUserMenu.tsx b/frontend/src/layouts/AppLayout/CurrentUserMenu.tsx
new file mode 100644
index 0000000..fd6ec33
--- /dev/null
+++ b/frontend/src/layouts/AppLayout/CurrentUserMenu.tsx
@@ -0,0 +1,35 @@
+// REQ-USR-004: 当前用户下拉(展示 sUserName(sUserType) + 退出登录,BR3/BR9)。
+import { Dropdown } from 'antd';
+import { DownOutlined, LogoutOutlined } from '@ant-design/icons';
+import type { MenuProps } from 'antd';
+import type { AuthUser } from '../../api/types';
+import { formatCurrentUser, LOGOUT_MENU_TEXT } from './shellMessages';
+import styles from './AppLayout.module.css';
+
+interface CurrentUserMenuProps {
+ user: AuthUser | null;
+ onLogout: () => void;
+}
+
+export default function CurrentUserMenu({ user, onLogout }: CurrentUserMenuProps) {
+ const items: MenuProps['items'] = [
+ {
+ key: 'logout',
+ icon: ,
+ label: LOGOUT_MENU_TEXT,
+ },
+ ];
+
+ const onMenuClick: MenuProps['onClick'] = ({ key }) => {
+ if (key === 'logout') onLogout();
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/layouts/AppLayout/TopBar.tsx b/frontend/src/layouts/AppLayout/TopBar.tsx
new file mode 100644
index 0000000..1707f0d
--- /dev/null
+++ b/frontend/src/layouts/AppLayout/TopBar.tsx
@@ -0,0 +1,103 @@
+// REQ-USR-003 / REQ-USR-004: 顶栏(Logo + 全部导航按钮 + 标签条 + 右侧搜索/通知/当前用户/更多)。
+import { MenuOutlined, SearchOutlined, BellOutlined, HomeOutlined } from '@ant-design/icons';
+import type { AuthUser } from '../../api/types';
+import type { TabItem } from './useTabStack';
+import CurrentUserMenu from './CurrentUserMenu';
+import styles from './AppLayout.module.css';
+
+interface TopBarProps {
+ user: AuthUser | null;
+ tabs: TabItem[];
+ activeKey: string;
+ navOverlayOpen: boolean;
+ onToggleNav: () => void;
+ onSelectTab: (key: string) => void;
+ onCloseTab: (key: string) => void;
+ onLogout: () => void;
+ onLogoHome: () => void;
+}
+
+export default function TopBar({
+ user,
+ tabs,
+ activeKey,
+ navOverlayOpen,
+ onToggleNav,
+ onSelectTab,
+ onCloseTab,
+ onLogout,
+ onLogoHome,
+}: TopBarProps) {
+ return (
+
+ {/* 品牌 Logo(鹿角 SVG),点击回主页 */}
+
+
+
+
+
+ {tabs.map((tab) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ ⋯
+
+
+
+ );
+}
diff --git a/frontend/src/layouts/AppLayout/shellMessages.ts b/frontend/src/layouts/AppLayout/shellMessages.ts
new file mode 100644
index 0000000..82b598f
--- /dev/null
+++ b/frontend/src/layouts/AppLayout/shellMessages.ts
@@ -0,0 +1,27 @@
+// REQ-USR-003 / REQ-USR-004: 外壳层文案常量(单一来源,跨组件复用,逐字复刻 spec / 原型)。
+
+/** 退出登录成功提示(message.success,BR9) */
+export const LOGOUT_SUCCESS_TEXT = '已退出登录';
+
+/** 被动 401 提示(message.warning,BR10) */
+export const SESSION_EXPIRED_TEXT = '登录已失效,请重新登录';
+
+/** 导航占位项点击提示(message.info,BR7/D4) */
+export const FEATURE_WIP_TEXT = '功能开发中';
+
+/** 当前用户为空时的占位用户名(BR3/D10:user 缺失时退化展示) */
+export const FALLBACK_USER_NAME = '未登录用户';
+
+/** 退出登录菜单项文案 */
+export const LOGOUT_MENU_TEXT = '退出登录';
+
+/**
+ * 当前用户区文案规则(BR3/D10):`${sUserName}(${sUserType})`。
+ * user 为 null 时退化为占位用户名。sUserType 已是中文,不再映射。
+ */
+export function formatCurrentUser(
+ user: { sUserName: string; sUserType: string } | null,
+): string {
+ if (!user) return FALLBACK_USER_NAME;
+ return `${user.sUserName}(${user.sUserType})`;
+}
diff --git a/frontend/tests/unit/AppLayout.topbar.test.tsx b/frontend/tests/unit/AppLayout.topbar.test.tsx
new file mode 100644
index 0000000..d15713b
--- /dev/null
+++ b/frontend/tests/unit/AppLayout.topbar.test.tsx
@@ -0,0 +1,133 @@
+// REQ-USR-004: 顶栏结构 + 当前用户 + 退出登录(BR3/BR9)
+import { describe, it, expect, vi } from 'vitest';
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useNavigate, useLocation, Routes, Route } from 'react-router-dom';
+import { App as AntdApp } from 'antd';
+import { renderShell } from './renderShell';
+import TopBar from '../../src/layouts/AppLayout/TopBar';
+import { useAppDispatch } from '../../src/store/hooks';
+import { clearCredentials } from '../../src/store/slices/authSlice';
+import { LOGOUT_SUCCESS_TEXT } from '../../src/layouts/AppLayout/shellMessages';
+import { HOME_TAB, BIZ_TABS, type TabItem } from '../../src/layouts/AppLayout/useTabStack';
+import type { AuthUser } from '../../src/api/types';
+
+const ADMIN: AuthUser = { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' };
+
+// 顶栏测试宿主:以真实 onLogout(dispatch + message + navigate)驱动,模拟 AppLayout 提供的回调
+function TopBarHost({
+ user,
+ tabs,
+ activeKey = 'home',
+ navOverlayOpen = false,
+ onToggleNav = vi.fn(),
+ onSelectTab = vi.fn(),
+ onCloseTab = vi.fn(),
+ onLogoHome = vi.fn(),
+}: {
+ user: AuthUser | null;
+ tabs: TabItem[];
+ activeKey?: string;
+ navOverlayOpen?: boolean;
+ onToggleNav?: () => void;
+ onSelectTab?: (k: string) => void;
+ onCloseTab?: (k: string) => void;
+ onLogoHome?: () => void;
+}) {
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const { message } = AntdApp.useApp();
+ const handleLogout = () => {
+ dispatch(clearCredentials());
+ message.success(LOGOUT_SUCCESS_TEXT);
+ navigate('/login', { replace: true });
+ };
+ return (
+
+ );
+}
+
+function LoginProbe() {
+ const loc = useLocation();
+ return {loc.pathname}
;
+}
+
+describe('TopBar', () => {
+ it('renders brand logo / 全部导航 button / 主页 tab', () => {
+ renderShell(, {
+ preloadedAuth: { token: 't', user: ADMIN },
+ });
+ expect(screen.getByRole('button', { name: '全部导航' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '品牌Logo 回到主页' })).toBeInTheDocument();
+ const homeTab = screen.getByTestId('tab-home');
+ expect(homeTab).toHaveTextContent('主页');
+ // 主页 tab 无关闭按钮
+ expect(within(homeTab).queryByText('✕')).not.toBeInTheDocument();
+ });
+
+ it('renders current user as sUserName(sUserType)', () => {
+ renderShell(, {
+ preloadedAuth: { token: 't', user: ADMIN },
+ });
+ expect(screen.getByText('朱子纯(超级管理员)')).toBeInTheDocument();
+ });
+
+ it('user fallback when user is null', () => {
+ renderShell(, {
+ preloadedAuth: { token: 't', user: null },
+ });
+ expect(screen.getByText('未登录用户')).toBeInTheDocument();
+ });
+
+ it('logout menu dispatches clearCredentials, shows success, navigates /login', async () => {
+ localStorage.setItem('xly_erp_token', 't');
+ const { getState } = renderShell(
+
+ } />
+ } />
+ ,
+ { initialEntries: ['/'], preloadedAuth: { token: 't', user: ADMIN } },
+ );
+ // 展开当前用户下拉
+ await userEvent.click(screen.getByText('朱子纯(超级管理员)'));
+ const logout = await screen.findByText('退出登录');
+ await userEvent.click(logout);
+ expect(getState().auth.token).toBeNull();
+ expect(localStorage.getItem('xly_erp_token')).toBeNull();
+ expect(await screen.findByText(LOGOUT_SUCCESS_TEXT)).toBeInTheDocument();
+ expect(screen.getByTestId('login-probe').textContent).toBe('/login');
+ });
+
+ it('nav toggle button highlights when navOverlayOpen', () => {
+ renderShell(, {
+ preloadedAuth: { token: 't', user: ADMIN },
+ });
+ const navBtn = screen.getByRole('button', { name: '全部导航' });
+ expect(navBtn.getAttribute('aria-pressed')).toBe('true');
+ });
+
+ it('clicking business tab close calls onCloseTab', async () => {
+ const onCloseTab = vi.fn();
+ renderShell(
+ ,
+ { preloadedAuth: { token: 't', user: ADMIN } },
+ );
+ await userEvent.click(screen.getByTestId('tab-close-userlist'));
+ expect(onCloseTab).toHaveBeenCalledWith('userlist');
+ });
+});