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 { 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>
|
||||||
|
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 { 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">
|
||||||
|
Loading…
Reference in New Issue
Block a user