feat: add mobile nav menu support

This commit is contained in:
moonrailgun 2024-01-08 00:01:49 +08:00
parent ac7f4011cd
commit 1e0d077f2a
5 changed files with 187 additions and 49 deletions

View File

@ -9,7 +9,7 @@ import { Register } from './pages/Register';
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './api/cache'; import { queryClient } from './api/cache';
import { TokenLoginContainer } from './components/TokenLoginContainer'; import { TokenLoginContainer } from './components/TokenLoginContainer';
import React, { Suspense } from 'react'; import React, { Suspense, useRef } from 'react';
import { trpc, trpcClient } from './api/trpc'; import { trpc, trpcClient } from './api/trpc';
import { MonitorPage } from './pages/Monitor'; import { MonitorPage } from './pages/Monitor';
import { WebsitePage } from './pages/Website'; import { WebsitePage } from './pages/Website';
@ -58,12 +58,14 @@ export const AppRoutes: React.FC = React.memo(() => {
AppRoutes.displayName = 'AppRoutes'; AppRoutes.displayName = 'AppRoutes';
export const App: React.FC = React.memo(() => { export const App: React.FC = React.memo(() => {
const rootRef = useRef<HTMLDivElement | null>(null);
const colorScheme = useSettingsStore((state) => state.colorScheme); const colorScheme = useSettingsStore((state) => state.colorScheme);
const algorithm = const algorithm =
colorScheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm; colorScheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm;
return ( return (
<div <div
ref={rootRef}
className={clsx('App', { className={clsx('App', {
dark: colorScheme === 'dark', dark: colorScheme === 'dark',
})} })}
@ -71,7 +73,10 @@ export const App: React.FC = React.memo(() => {
<trpc.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
<ConfigProvider theme={{ algorithm }}> <ConfigProvider
theme={{ algorithm }}
getPopupContainer={() => rootRef.current!}
>
<TokenLoginContainer> <TokenLoginContainer>
<AppRoutes /> <AppRoutes />
</TokenLoginContainer> </TokenLoginContainer>

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import clsx from 'clsx';
export const MobileNavItem: React.FC<{
to: string;
label: string;
onClick?: () => void;
}> = React.memo((props) => {
const location = useLocation();
const isCurrent = location.pathname.startsWith(props.to);
return (
<Link to={props.to}>
<div
className={clsx('rounded-md px-4 py-4', {
'bg-neutral-100 text-neutral-900 dark:text-neutral-700': isCurrent,
'text-neutral-900 dark:text-neutral-100': !isCurrent,
})}
onClick={props.onClick}
>
{props.label}
</div>
</Link>
);
});
MobileNavItem.displayName = 'MobileNavItem';

View File

@ -0,0 +1,7 @@
import { useWindowSize } from './useWindowSize';
export function useIsMobile(): boolean {
const { width } = useWindowSize();
return width <= 768;
}

View File

@ -0,0 +1,31 @@
import { useEffect, useState } from 'react';
interface WindowScreenSize {
width: number;
height: number;
}
function getWindowSize(): WindowScreenSize {
return {
width: window.screen.width,
height: window.screen.height,
};
}
export function useWindowSize(): WindowScreenSize {
const [size, setSize] = useState(getWindowSize());
useEffect(() => {
const handleResize = () => {
setSize(getWindowSize());
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return size;
}

View File

@ -1,12 +1,15 @@
import React from 'react'; import React, { useState } from 'react';
import { Outlet, useSearchParams } from 'react-router-dom'; import { Outlet, useSearchParams } from 'react-router-dom';
import { NavItem } from '../components/NavItem'; import { NavItem } from '../components/NavItem';
import { MobileNavItem } from '../components/MobileNavItem';
import { UserOutlined } from '@ant-design/icons'; import { UserOutlined } from '@ant-design/icons';
import { Button, Dropdown } from 'antd'; import { Button, Divider, Drawer, Dropdown, Menu } from 'antd';
import { useUserStore } from '../store/user'; import { useUserStore } from '../store/user';
import { useLogout } from '../api/model/user'; import { useLogout } from '../api/model/user';
import { ColorSchemeSwitcher } from '../components/ColorSchemeSwitcher'; import { ColorSchemeSwitcher } from '../components/ColorSchemeSwitcher';
import { version } from '../../shared'; import { version } from '../../shared';
import { useIsMobile } from '../hooks/useIsMobile';
import { RiMenuUnfoldLine } from 'react-icons/ri';
export const Layout: React.FC = React.memo(() => { export const Layout: React.FC = React.memo(() => {
const [params] = useSearchParams(); const [params] = useSearchParams();
@ -23,64 +26,128 @@ export const Layout: React.FC = React.memo(() => {
return []; return [];
}); });
const [openDraw, setOpenDraw] = useState(false);
const logout = useLogout(); const logout = useLogout();
const isMobile = useIsMobile();
const showHeader = !params.has('hideHeader'); const showHeader = !params.has('hideHeader');
const accountEl = (
<Dropdown
placement="bottomRight"
menu={{
items: [
{
key: 'workspaces',
label: 'Workspaces',
children: workspaces.map((w) => ({
key: w.id,
label: `${w.name}${w.current ? '(current)' : ''}`,
disabled: w.current,
})),
},
{
key: 'logout',
label: 'Logout',
onClick: () => {
logout();
},
},
{
type: 'divider',
},
{
key: 'version',
label: `v${version}`,
disabled: true,
},
],
}}
>
<Button shape="circle" size="large" icon={<UserOutlined />} />
</Dropdown>
);
return ( return (
<div className="flex flex-col h-full dark:bg-gray-900 dark:text-gray-300"> <div className="flex flex-col h-full dark:bg-gray-900 dark:text-gray-300">
{showHeader && ( {showHeader && (
<div className="flex items-center bg-gray-100 dark:bg-gray-800 px-4 sticky top-0 z-20"> <div className="flex items-center bg-gray-100 dark:bg-gray-800 px-4 sticky top-0 z-20 h-[62px]">
{isMobile && (
<>
<Button
className="mr-2"
icon={<RiMenuUnfoldLine className="anticon" />}
onClick={() => setOpenDraw(true)}
/>
<Drawer
open={openDraw}
onClose={() => setOpenDraw(false)}
placement="left"
closeIcon={false}
>
<div className="flex flex-col h-full pt-12">
<div className="flex-1">
<MobileNavItem
to="/dashboard"
label="Dashboard"
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/monitor"
label="Monitor"
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/website"
label="Website"
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/servers"
label="Servers"
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/settings"
label="Settings"
onClick={() => setOpenDraw(false)}
/>
</div>
<Divider />
<div className="flex justify-between">
<ColorSchemeSwitcher />
{accountEl}
</div>
</div>
</Drawer>
</>
)}
<div className="px-2 mr-10 font-bold flex items-center"> <div className="px-2 mr-10 font-bold flex items-center">
<img src="/icon.svg" className="w-10 h-10 mr-2" /> <img src="/icon.svg" className="w-10 h-10 mr-2" />
<span className="text-xl dark:text-gray-200">Tianji</span> <span className="text-xl dark:text-gray-200">Tianji</span>
</div> </div>
<div className="flex gap-8">
<NavItem to="/dashboard" label="Dashboard" />
<NavItem to="/monitor" label="Monitor" />
<NavItem to="/website" label="Website" />
<NavItem to="/servers" label="Servers" />
<NavItem to="/settings" label="Settings" />
</div>
<div className="flex-1" /> {!isMobile && (
<>
<div className="flex gap-8">
<NavItem to="/dashboard" label="Dashboard" />
<NavItem to="/monitor" label="Monitor" />
<NavItem to="/website" label="Website" />
<NavItem to="/servers" label="Servers" />
<NavItem to="/settings" label="Settings" />
</div>
<div className="flex gap-2"> <div className="flex-1" />
<ColorSchemeSwitcher />
<Dropdown <div className="flex gap-2">
placement="bottomRight" <ColorSchemeSwitcher />
menu={{
items: [ {accountEl}
{ </div>
key: 'workspaces', </>
label: 'Workspaces', )}
children: workspaces.map((w) => ({
key: w.id,
label: `${w.name}${w.current ? '(current)' : ''}`,
disabled: w.current,
})),
},
{
key: 'logout',
label: 'Logout',
onClick: () => {
logout();
},
},
{
type: 'divider',
},
{
key: 'version',
label: `v${version}`,
disabled: true,
},
],
}}
>
<Button shape="circle" size="large" icon={<UserOutlined />} />
</Dropdown>
</div>
</div> </div>
)} )}
<div className="flex-1 w-full overflow-hidden"> <div className="flex-1 w-full overflow-hidden">