feat: 优化web
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ClassicCollapsedSidebar } from '@/components/layout/ClassicCollapsedSidebar';
|
||||
import { IconSidebarLayout } from '@/components/layout/IconSidebarLayout';
|
||||
import { NavTooltip } from '@/components/layout/nav-tooltip';
|
||||
import { SidebarNav } from '@/components/layout/SidebarNav';
|
||||
import { TenantSwitcher } from '@/components/layout/TenantSwitcher';
|
||||
import { UserMenu } from '@/components/layout/UserMenu';
|
||||
import { useMenuNavigation } from '@/lib/hooks/use-menu-navigation';
|
||||
import { useNavMenu } from '@/lib/hooks/use-nav-menu';
|
||||
import { redirectToAuthorize } from '@/lib/oauth/browser';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useLayoutStore } from '@/stores/layout-store';
|
||||
|
||||
export function AppChrome({ children }: { children: React.ReactNode }) {
|
||||
const authed = useAuthStore((s) => Boolean(s.accessToken));
|
||||
const sidebarMode = useLayoutStore((s) => s.sidebarMode);
|
||||
const classicNavRailCollapsed = useLayoutStore((s) => s.classicNavRailCollapsed);
|
||||
const toggleClassicNavRail = useLayoutStore((s) => s.toggleClassicNavRail);
|
||||
|
||||
const { items: nav, loading: navLoading, error: navError } = useNavMenu();
|
||||
const onMenuNavigate = useMenuNavigation();
|
||||
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
|
||||
const [showClassicNarrowUi, setShowClassicNarrowUi] = useState(classicNavRailCollapsed);
|
||||
const classicAsideRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(min-width: 768px)');
|
||||
const onChange = () => {
|
||||
if (mq.matches) setMobileNavOpen(false);
|
||||
};
|
||||
mq.addEventListener('change', onChange);
|
||||
return () => mq.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!classicNavRailCollapsed) setShowClassicNarrowUi(false);
|
||||
}, [classicNavRailCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!classicNavRailCollapsed || showClassicNarrowUi) return;
|
||||
const el = classicAsideRef.current;
|
||||
const finish = () => {
|
||||
if (useLayoutStore.getState().classicNavRailCollapsed) setShowClassicNarrowUi(true);
|
||||
};
|
||||
const onEnd = (e: TransitionEvent) => {
|
||||
if (e.propertyName === 'width') {
|
||||
finish();
|
||||
el?.removeEventListener('transitionend', onEnd);
|
||||
}
|
||||
};
|
||||
el?.addEventListener('transitionend', onEnd);
|
||||
const t = window.setTimeout(finish, 600);
|
||||
return () => {
|
||||
el?.removeEventListener('transitionend', onEnd);
|
||||
window.clearTimeout(t);
|
||||
};
|
||||
}, [classicNavRailCollapsed, showClassicNarrowUi]);
|
||||
|
||||
const classicAsideMotion =
|
||||
'min-w-0 shrink-0 transition-[width] duration-500 ease-in-out motion-reduce:transition-none';
|
||||
|
||||
const asideClass =
|
||||
sidebarMode === 'icon'
|
||||
? 'hidden min-h-0 shrink-0 overflow-hidden border-r border-neutral-200 bg-white md:flex md:flex-col'
|
||||
: `relative hidden h-full min-h-0 flex-col overflow-visible border-r border-neutral-200 bg-white md:flex ${classicAsideMotion}`;
|
||||
|
||||
const classicAsideStyle =
|
||||
sidebarMode !== 'icon' ? { width: classicNavRailCollapsed ? '72px' : '14rem' } : undefined;
|
||||
|
||||
const showExpandedClassicChrome = !classicNavRailCollapsed || !showClassicNarrowUi;
|
||||
|
||||
const sidebarProps = {
|
||||
items: nav,
|
||||
loading: navLoading,
|
||||
error: navError,
|
||||
authed,
|
||||
onMenuNavigate,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh min-h-0 flex-col overflow-hidden">
|
||||
<header className="flex shrink-0 items-center justify-between border-b border-neutral-200 bg-white px-4 py-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 md:gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md p-2 text-neutral-700 hover:bg-neutral-100 md:hidden"
|
||||
aria-label="打开导航菜单"
|
||||
aria-expanded={mobileNavOpen}
|
||||
onClick={() => setMobileNavOpen(true)}
|
||||
>
|
||||
<span className="text-lg leading-none" aria-hidden>
|
||||
☰
|
||||
</span>
|
||||
</button>
|
||||
<Link href="/dashboard" className="shrink-0 font-semibold text-neutral-800">
|
||||
Smart Admin
|
||||
</Link>
|
||||
{authed ? <TenantSwitcher /> : null}
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||
{authed ? (
|
||||
<UserMenu />
|
||||
) : (
|
||||
<>
|
||||
<Link className="text-blue-600 underline" href="/login">
|
||||
账号登录
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-neutral-900 px-2 py-1 text-white"
|
||||
onClick={() => void redirectToAuthorize()}
|
||||
>
|
||||
OAuth 授权
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 items-stretch">
|
||||
<aside ref={classicAsideRef} className={asideClass} style={classicAsideStyle} aria-label="侧栏">
|
||||
{sidebarMode === 'icon' ? (
|
||||
<IconSidebarLayout {...sidebarProps} />
|
||||
) : showExpandedClassicChrome ? (
|
||||
<div className="relative flex h-full min-h-0 flex-1 flex-col overflow-visible">
|
||||
<div className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto">
|
||||
<SidebarNav {...sidebarProps} />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 z-50 hidden md:block" aria-hidden>
|
||||
<NavTooltip label="收起侧栏(仅一级菜单)" side="right">
|
||||
<button
|
||||
type="button"
|
||||
className="pointer-events-auto absolute left-full top-1/2 z-50 flex h-14 w-3.5 -translate-y-1/2 cursor-pointer items-center justify-center rounded-r-full border border-neutral-200 border-l-0 bg-white text-xs text-neutral-500 shadow-sm transition-transform duration-200 ease-out hover:bg-neutral-50 active:scale-95 motion-reduce:transition-none"
|
||||
aria-expanded={true}
|
||||
aria-label="收起侧栏,仅显示一级菜单"
|
||||
onClick={() => toggleClassicNavRail()}
|
||||
>
|
||||
<span aria-hidden>‹</span>
|
||||
</button>
|
||||
</NavTooltip>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ClassicCollapsedSidebar {...sidebarProps} />
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col bg-neutral-100">{children}</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Root open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 z-100 bg-black/40 md:hidden" />
|
||||
<Dialog.Content className="fixed left-0 top-0 z-100 flex h-dvh w-56 max-w-[85vw] min-h-0 flex-col border-r border-neutral-200 bg-white p-0 shadow-lg outline-none focus:outline-none md:hidden">
|
||||
<Dialog.Title className="sr-only">导航菜单</Dialog.Title>
|
||||
<Dialog.Description className="sr-only">浏览并选择页面,关闭按钮在右上角。</Dialog.Description>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 z-10 rounded-md p-1.5 text-neutral-600 hover:bg-neutral-100"
|
||||
aria-label="关闭菜单"
|
||||
onClick={() => setMobileNavOpen(false)}
|
||||
>
|
||||
<span aria-hidden>✕</span>
|
||||
</button>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pt-10">
|
||||
<SidebarNav {...sidebarProps} onInternalNavigate={() => setMobileNavOpen(false)} />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user