feat: add mobile nav menu support
This commit is contained in:
parent
ac7f4011cd
commit
1e0d077f2a
@ -9,7 +9,7 @@ import { Register } from './pages/Register';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { queryClient } from './api/cache';
|
||||
import { TokenLoginContainer } from './components/TokenLoginContainer';
|
||||
import React, { Suspense } from 'react';
|
||||
import React, { Suspense, useRef } from 'react';
|
||||
import { trpc, trpcClient } from './api/trpc';
|
||||
import { MonitorPage } from './pages/Monitor';
|
||||
import { WebsitePage } from './pages/Website';
|
||||
@ -58,12 +58,14 @@ export const AppRoutes: React.FC = React.memo(() => {
|
||||
AppRoutes.displayName = 'AppRoutes';
|
||||
|
||||
export const App: React.FC = React.memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const colorScheme = useSettingsStore((state) => state.colorScheme);
|
||||
const algorithm =
|
||||
colorScheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={clsx('App', {
|
||||
dark: colorScheme === 'dark',
|
||||
})}
|
||||
@ -71,7 +73,10 @@ export const App: React.FC = React.memo(() => {
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<ConfigProvider theme={{ algorithm }}>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm }}
|
||||
getPopupContainer={() => rootRef.current!}
|
||||
>
|
||||
<TokenLoginContainer>
|
||||
<AppRoutes />
|
||||
</TokenLoginContainer>
|
||||
|
28
src/client/components/MobileNavItem.tsx
Normal file
28
src/client/components/MobileNavItem.tsx
Normal 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';
|
7
src/client/hooks/useIsMobile.ts
Normal file
7
src/client/hooks/useIsMobile.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useWindowSize } from './useWindowSize';
|
||||
|
||||
export function useIsMobile(): boolean {
|
||||
const { width } = useWindowSize();
|
||||
|
||||
return width <= 768;
|
||||
}
|
31
src/client/hooks/useWindowSize.ts
Normal file
31
src/client/hooks/useWindowSize.ts
Normal 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;
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Outlet, useSearchParams } from 'react-router-dom';
|
||||
import { NavItem } from '../components/NavItem';
|
||||
import { MobileNavItem } from '../components/MobileNavItem';
|
||||
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 { useLogout } from '../api/model/user';
|
||||
import { ColorSchemeSwitcher } from '../components/ColorSchemeSwitcher';
|
||||
import { version } from '../../shared';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { RiMenuUnfoldLine } from 'react-icons/ri';
|
||||
|
||||
export const Layout: React.FC = React.memo(() => {
|
||||
const [params] = useSearchParams();
|
||||
@ -23,64 +26,128 @@ export const Layout: React.FC = React.memo(() => {
|
||||
|
||||
return [];
|
||||
});
|
||||
const [openDraw, setOpenDraw] = useState(false);
|
||||
const logout = useLogout();
|
||||
const isMobile = useIsMobile();
|
||||
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 (
|
||||
<div className="flex flex-col h-full dark:bg-gray-900 dark:text-gray-300">
|
||||
{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">
|
||||
<img src="/icon.svg" className="w-10 h-10 mr-2" />
|
||||
<span className="text-xl dark:text-gray-200">Tianji</span>
|
||||
</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">
|
||||
<ColorSchemeSwitcher />
|
||||
<div className="flex-1" />
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ColorSchemeSwitcher />
|
||||
|
||||
{accountEl}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 w-full overflow-hidden">
|
||||
|
Loading…
Reference in New Issue
Block a user