diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 7549f61..443927d 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,16 +1,31 @@ import React from 'react'; import { Layout, Button, Space, Tag, Dropdown } from 'antd'; -import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, GlobalOutlined, BgColorsOutlined } from '@ant-design/icons'; +import { + MenuFoldOutlined, + MenuUnfoldOutlined, + MenuOutlined, + UserOutlined, + GlobalOutlined, + BgColorsOutlined, +} from '@ant-design/icons'; import { useAuthStore } from '../stores/authStore'; import { useThemeStore, ThemeMode } from '../stores/themeStore'; import { useI18n } from '../i18n'; +import useIsMobile from '../hooks/useIsMobile'; const { Header } = Layout; -const HeaderBar: React.FC<{ collapsed: boolean; setCollapsed: (v: boolean) => void; }> = ({ collapsed, setCollapsed }) => { +type Props = { + collapsed: boolean; + setCollapsed: (v: boolean) => void; + onOpenMobileNav?: () => void; +}; + +const HeaderBar: React.FC = ({ collapsed, setCollapsed, onOpenMobileNav }) => { const { t, locale, setLocale } = useI18n(); const { mode, setMode } = useThemeStore(); const username = useAuthStore((s) => s.username); + const isMobile = useIsMobile(); const langItems = [ { key: 'zh', label: '中文' }, @@ -23,17 +38,58 @@ const HeaderBar: React.FC<{ collapsed: boolean; setCollapsed: (v: boolean) => vo { key: 'system', label: t('settings.system') }, ]; + const displayName = username || 'Admin'; + const shortName = displayName.length > 8 ? `${displayName.slice(0, 8)}…` : displayName; + return ( -
- +
+ {isMobile ? ( + - setLocale(e.key as 'en' | 'zh') }}> - + setLocale(e.key as 'en' | 'zh'), + }} + > + - }>{username || 'Admin'} + } style={{ marginInlineEnd: 0 }}> + {isMobile ? shortName : displayName} +
); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index f2534ac..d571d4a 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -10,7 +10,7 @@ import { useI18n } from '../i18n'; const { Sider } = Layout; -const Sidebar: React.FC<{ collapsed: boolean }> = ({ collapsed }) => { +export function useSidebarMenuItems(onNavigate?: () => void) { const navigate = useNavigate(); const location = useLocation(); const logout = useAuthStore((s) => s.logout); @@ -26,15 +26,86 @@ const Sidebar: React.FC<{ collapsed: boolean }> = ({ collapsed }) => { { key: '/settings', icon: , label: t('nav.settings') }, ]; + const onMenuClick = ({ key }: { key: string }) => { + navigate(key); + onNavigate?.(); + }; + + const onLogout = () => { + logout(); + navigate('/login'); + onNavigate?.(); + }; + + return { items, selectedKeys: [location.pathname], onMenuClick, onLogout, t }; +} + +/** Shared nav body used by desktop Sider and mobile Drawer. */ +export const SidebarMenu: React.FC<{ onNavigate?: () => void; style?: React.CSSProperties }> = ({ + onNavigate, + style, +}) => { + const { items, selectedKeys, onMenuClick, onLogout, t } = useSidebarMenuItems(onNavigate); + return ( - -
+
+
3M-UI
- navigate(key)} /> - , label: t('nav.logout'), onClick: () => { logout(); navigate('/login'); } }, - ]} style={{ position: 'absolute', bottom: 0, width: '100%' }} /> + + , + label: t('nav.logout'), + onClick: onLogout, + }, + ]} + style={{ borderInlineEnd: 'none', borderTop: '1px solid rgba(5,5,5,0.06)' }} + /> +
+ ); +}; + +const Sidebar: React.FC<{ collapsed: boolean }> = ({ collapsed }) => { + return ( + + ); };