feat: mobile-aware Header and Sidebar menu shared with Drawer
This commit is contained in:
1 parent
f2bb7b574a
commit
e9fbb33ee4
2 files changed
+144
-17
No files matched your search
@@ -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<Props> = ({ 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 (
|
||||
<Header style={{ padding: '0 24px', background: 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Button type="text" icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} onClick={() => setCollapsed(!collapsed)} />
|
||||
<Space>
|
||||
<Dropdown menu={{ items: themeItems, selectedKeys: [mode], onClick: (e) => setMode(e.key as ThemeMode) }}>
|
||||
<Button type="text" icon={<BgColorsOutlined />}>{t('settings.theme')}</Button>
|
||||
<Header
|
||||
style={{
|
||||
padding: isMobile ? '0 12px' : '0 24px',
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
height: 64,
|
||||
lineHeight: '64px',
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
<Button type="text" icon={<MenuOutlined />} onClick={onOpenMobileNav} aria-label="menu" />
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Space size={isMobile ? 4 : 'middle'} wrap>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: themeItems,
|
||||
selectedKeys: [mode],
|
||||
onClick: (e) => setMode(e.key as ThemeMode),
|
||||
}}
|
||||
>
|
||||
<Button type="text" icon={<BgColorsOutlined />}>
|
||||
{!isMobile && t('settings.theme')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown menu={{ items: langItems, selectedKeys: [locale], onClick: (e) => setLocale(e.key as 'en' | 'zh') }}>
|
||||
<Button type="text" icon={<GlobalOutlined />}>{locale === 'zh' ? '中文' : 'English'}</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: langItems,
|
||||
selectedKeys: [locale],
|
||||
onClick: (e) => setLocale(e.key as 'en' | 'zh'),
|
||||
}}
|
||||
>
|
||||
<Button type="text" icon={<GlobalOutlined />}>
|
||||
{!isMobile && (locale === 'zh' ? '中文' : 'English')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Tag icon={<UserOutlined />}>{username || 'Admin'}</Tag>
|
||||
<Tag icon={<UserOutlined />} style={{ marginInlineEnd: 0 }}>
|
||||
{isMobile ? shortName : displayName}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Header>
|
||||
);
|
||||
|
||||
@@ -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: <SettingOutlined />, 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 (
|
||||
<Sider trigger={null} collapsible collapsed={collapsed} theme="light">
|
||||
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700, fontSize: 18 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', ...style }}>
|
||||
<div
|
||||
style={{
|
||||
height: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
3M-UI
|
||||
</div>
|
||||
<Menu mode="inline" selectedKeys={[location.pathname]} items={items} onClick={({ key }) => navigate(key)} />
|
||||
<Menu mode="inline" selectable={false} items={[
|
||||
{ key: 'logout', icon: <LogoutOutlined />, label: t('nav.logout'), onClick: () => { logout(); navigate('/login'); } },
|
||||
]} style={{ position: 'absolute', bottom: 0, width: '100%' }} />
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={selectedKeys}
|
||||
items={items}
|
||||
onClick={onMenuClick}
|
||||
style={{ flex: 1, borderInlineEnd: 'none' }}
|
||||
/>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectable={false}
|
||||
items={[
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: t('nav.logout'),
|
||||
onClick: onLogout,
|
||||
},
|
||||
]}
|
||||
style={{ borderInlineEnd: 'none', borderTop: '1px solid rgba(5,5,5,0.06)' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Sidebar: React.FC<{ collapsed: boolean }> = ({ collapsed }) => {
|
||||
return (
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
theme="light"
|
||||
breakpoint="md"
|
||||
collapsedWidth={80}
|
||||
width={220}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'sticky',
|
||||
insetInlineStart: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<SidebarMenu />
|
||||
</Sider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user