feat: 优化web

This commit is contained in:
2026-04-23 18:58:13 +08:00
commit 544a2f3428
160 changed files with 27327 additions and 0 deletions
+179
View File
@@ -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>
);
}