feat(v2): add mobile layout

This commit is contained in:
moonrailgun 2024-04-03 11:30:22 +08:00
parent 0711c3c003
commit af1d99d2ff
5 changed files with 195 additions and 61 deletions

View File

@ -23,6 +23,7 @@ import { isDev } from './utils/env';
import { RouterProvider, createRouter } from '@tanstack/react-router'; import { RouterProvider, createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen'; import { routeTree } from './routeTree.gen';
import { DefaultNotFound } from './components/DefaultNotFound'; import { DefaultNotFound } from './components/DefaultNotFound';
import { TooltipProvider } from './components/ui/tooltip';
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
@ -95,7 +96,9 @@ export const App: React.FC = React.memo(() => {
{isDev ? ( {isDev ? (
// Compatible with old routes // Compatible with old routes
<BrowserRouter> <BrowserRouter>
<RouterProvider router={router} context={{ userInfo }} /> <TooltipProvider delayDuration={0}>
<RouterProvider router={router} context={{ userInfo }} />
</TooltipProvider>
</BrowserRouter> </BrowserRouter>
) : ( ) : (
<BrowserRouter> <BrowserRouter>

View File

@ -22,6 +22,7 @@ import { Outlet } from '@tanstack/react-router';
import { trpc } from '@/api/trpc'; import { trpc } from '@/api/trpc';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
import { LayoutProps } from './types'; import { LayoutProps } from './types';
import { useTranslation } from '@i18next-toolkit/react';
const defaultLayout: [number, number, number] = [265, 440, 655]; const defaultLayout: [number, number, number] = [265, 440, 655];
@ -45,6 +46,7 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
enabled: !!workspaceId, enabled: !!workspaceId,
} }
); );
const { t } = useTranslation();
const navbar = ( const navbar = (
<> <>
@ -61,31 +63,31 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
links={[ links={[
{ {
title: 'Website', title: t('Website'),
label: String(serviceCount?.website ?? ''), label: String(serviceCount?.website ?? ''),
icon: LuAreaChart, icon: LuAreaChart,
to: '/website', to: '/website',
}, },
{ {
title: 'Monitor', title: t('Monitor'),
label: String(serviceCount?.monitor ?? ''), label: String(serviceCount?.monitor ?? ''),
icon: LuMonitorDot, icon: LuMonitorDot,
to: '/monitor', to: '/monitor',
}, },
{ {
title: 'Servers', title: t('Servers'),
label: '', label: '',
icon: LuServer, icon: LuServer,
to: '/server', to: '/server',
}, },
{ {
title: 'Telemetry', title: t('Telemetry'),
label: String(serviceCount?.telemetry ?? ''), label: String(serviceCount?.telemetry ?? ''),
icon: LuWifi, icon: LuWifi,
to: '/telemetry', to: '/telemetry',
}, },
{ {
title: 'Pages', title: t('Pages'),
label: String(serviceCount?.page ?? ''), label: String(serviceCount?.page ?? ''),
icon: LuFilePieChart, icon: LuFilePieChart,
to: '/page', to: '/page',
@ -103,60 +105,57 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
); );
return ( return (
<TooltipProvider delayDuration={0}> <ResizablePanelGroup
<ResizablePanelGroup direction="horizontal"
direction="horizontal" onLayout={(sizes: number[]) => {
onLayout={(sizes: number[]) => { if (sizes.length === 3) {
if (sizes.length === 3) { setLayout(sizes as typeof defaultLayout);
setLayout(sizes as typeof defaultLayout); } else if (sizes.length === 2) {
} else if (sizes.length === 2) { const listSize = layout[1];
const listSize = layout[1]; const rest = 100 - sizes[0] - listSize;
const rest = 100 - sizes[0] - listSize; setLayout([sizes[0], listSize, rest]);
setLayout([sizes[0], listSize, rest]); }
} }}
className="h-full items-stretch"
>
<ResizablePanel
defaultSize={layout[0]}
collapsedSize={1}
collapsible={true}
minSize={10}
maxSize={20}
onCollapse={() => {
setIsCollapsed(true);
}} }}
className="h-full items-stretch" onExpand={() => {
> setIsCollapsed(false);
<ResizablePanel }}
defaultSize={layout[0]} className={cn(
collapsedSize={1} 'flex flex-col',
collapsible={true} isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out'
minSize={10}
maxSize={20}
onCollapse={() => {
setIsCollapsed(true);
}}
onExpand={() => {
setIsCollapsed(false);
}}
className={cn(
'flex flex-col',
isCollapsed &&
'min-w-[50px] transition-all duration-300 ease-in-out'
)}
>
{navbar}
</ResizablePanel>
{props.list && (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={layout[1]} minSize={25}>
<div className="h-full overflow-hidden">{props.list}</div>
</ResizablePanel>
</>
)} )}
>
{navbar}
</ResizablePanel>
<ResizableHandle withHandle /> {props.list && (
<ResizablePanel <>
defaultSize={props.list ? layout[2] : layout[1] + layout[2]} <ResizableHandle withHandle />
> <ResizablePanel defaultSize={layout[1]} minSize={25}>
<div className="h-full overflow-hidden"> <div className="h-full overflow-hidden">{props.list}</div>
{props.children ?? <Outlet />} </ResizablePanel>
</div> </>
</ResizablePanel> )}
</ResizablePanelGroup>
</TooltipProvider> <ResizableHandle withHandle />
<ResizablePanel
defaultSize={props.list ? layout[2] : layout[1] + layout[2]}
>
<div className="h-full overflow-hidden">
{props.children ?? <Outlet />}
</div>
</ResizablePanel>
</ResizablePanelGroup>
); );
}); });
DesktopLayout.displayName = 'DesktopLayout'; DesktopLayout.displayName = 'DesktopLayout';

View File

@ -1,6 +1,105 @@
import React from 'react'; import React from 'react';
import {
LuAreaChart,
LuFilePieChart,
LuMenu,
LuMonitorDot,
LuServer,
LuWifi,
} from 'react-icons/lu';
import { useTranslation } from '@i18next-toolkit/react';
import { IconType } from 'react-icons';
import { useRouterState, Link, Outlet } from '@tanstack/react-router';
import { cn } from '@/utils/style';
import { Separator } from '@/components/ui/separator';
import { LayoutProps } from './types';
import { UserConfig } from './UserConfig';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
export const MobileLayout: React.FC = React.memo(() => { export const MobileLayout: React.FC<LayoutProps> = React.memo((props) => {
return <div>Mobile</div>; const { t } = useTranslation();
return (
<div className="flex h-svh flex-col">
<div className="flex h-[52px] items-center justify-between px-2">
<Sheet>
<SheetTrigger>
<Button variant="outline" size="icon">
<LuMenu />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-11/12">
<ScrollArea className="h-full">{props.list}</ScrollArea>
</SheetContent>
</Sheet>
<div>
<img className="m-auto h-8 w-8" src="/icon.svg" />
</div>
<div>
<UserConfig isCollapsed={true} />
</div>
</div>
<Separator />
<div className="flex-1 overflow-hidden">
{props.children ?? <Outlet />}
</div>
<Separator />
<div className="p-2">
<div className="flex justify-between">
<MobileNavItem
title={t('Website')}
icon={LuAreaChart}
to="/website"
/>
<MobileNavItem
title={t('Monitor')}
icon={LuMonitorDot}
to="/monitor"
/>
<MobileNavItem title={t('Servers')} icon={LuServer} to="/server" />
<MobileNavItem title={t('Telemetry')} icon={LuWifi} to="/telemetry" />
<MobileNavItem title={t('Pages')} icon={LuFilePieChart} to="/page" />
</div>
</div>
</div>
);
}); });
MobileLayout.displayName = 'MobileLayout'; MobileLayout.displayName = 'MobileLayout';
const MobileNavItem: React.FC<{
title: string;
icon: IconType;
to: string;
}> = React.memo((props) => {
const pathname = useRouterState({
select: (state) => state.location.pathname,
});
const isSelect = pathname.startsWith(props.to);
return (
<Link
className={cn(
'flex-1 rounded-lg p-1 text-center',
isSelect
? 'bg-muted text-black dark:text-white'
: 'text-muted-foreground'
)}
to={props.to}
>
<props.icon size={24} className="m-auto mb-1" />
<div className={cn('text-sm font-semibold', isSelect && 'font-bold')}>
{props.title}
</div>
</Link>
);
});
MobileNavItem.displayName = 'MobileNavItem';

View File

@ -16,7 +16,7 @@ import {
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { useSettingsStore } from '@/store/settings'; import { useSettingsStore } from '@/store/settings';
import { useUserInfo } from '@/store/user'; import { useCurrentWorkspaceId, useUserInfo, useUserStore } from '@/store/user';
import { languages } from '@/utils/constants'; import { languages } from '@/utils/constants';
import { useTranslation, setLanguage } from '@i18next-toolkit/react'; import { useTranslation, setLanguage } from '@i18next-toolkit/react';
import { useNavigate } from '@tanstack/react-router'; import { useNavigate } from '@tanstack/react-router';
@ -32,6 +32,20 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
const { i18n, t } = useTranslation(); const { i18n, t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const colorScheme = useSettingsStore((state) => state.colorScheme); const colorScheme = useSettingsStore((state) => state.colorScheme);
const workspaceId = useCurrentWorkspaceId();
const workspaces = useUserStore((state) => {
const userInfo = state.info;
if (userInfo) {
return userInfo.workspaces.map((w) => ({
id: w.workspace.id,
name: w.workspace.name,
role: w.role,
current: userInfo.currentWorkspace?.id === w.workspace.id,
}));
}
return [];
});
const handleChangeColorSchema = useEvent((colorScheme) => { const handleChangeColorSchema = useEvent((colorScheme) => {
useSettingsStore.setState({ useSettingsStore.setState({
@ -97,6 +111,25 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
{t('Notifications')} {t('Notifications')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>{t('Workspaces')}</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={workspaceId}>
{workspaces.map((workspace) => (
<DropdownMenuRadioItem
key={workspace.id}
value={workspace.id}
disabled={true}
>
{workspace.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub> <DropdownMenuSub>
<DropdownMenuSubTrigger>{t('Language')}</DropdownMenuSubTrigger> <DropdownMenuSubTrigger>{t('Language')}</DropdownMenuSubTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>

View File

@ -8,7 +8,7 @@ export const LayoutV2: React.FC<LayoutProps> = React.memo((props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (isMobile) { if (isMobile) {
return <MobileLayout />; return <MobileLayout {...props} />;
} }
return <DesktopLayout {...props} />; return <DesktopLayout {...props} />;