This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { Button, Drawer, Layout } from "tdesign-react";
|
||||
import { X } from "lucide-react";
|
||||
import { DEFAULT_ROUTE_ID, type RouteId } from "../router";
|
||||
import AppContent from "./Content";
|
||||
import AppHeader from "./Header";
|
||||
import AppMenu from "./Menu";
|
||||
import { useDrawerFocus } from "../hooks/useDrawerFocus";
|
||||
|
||||
export interface AppLayoutProps {
|
||||
activeId?: RouteId;
|
||||
adminName?: string;
|
||||
adminUsername?: string;
|
||||
children: ReactNode;
|
||||
onNavigate: (id: RouteId) => void;
|
||||
onLogout?: () => void;
|
||||
onOpenPassword?: () => void;
|
||||
}
|
||||
|
||||
export default function AppLayout({ activeId = DEFAULT_ROUTE_ID, adminName, adminUsername, children, onNavigate, onLogout, onOpenPassword }: AppLayoutProps) {
|
||||
const [desktopVisible, setDesktopVisible] = useState(true);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [mobileVisible, setMobileVisible] = useState(false);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
useDrawerFocus(mobileVisible, ".tn-mobile-drawer");
|
||||
|
||||
useEffect(() => {
|
||||
const media = typeof window.matchMedia === "function" ? window.matchMedia("(max-width: 900px)") : null;
|
||||
const sync = () => {
|
||||
const mobile = media?.matches ?? window.innerWidth <= 900;
|
||||
setIsMobile(mobile);
|
||||
if (!mobile) setMobileVisible(false);
|
||||
};
|
||||
sync();
|
||||
if (!media) return;
|
||||
if (typeof media.addEventListener === "function") {
|
||||
media.addEventListener("change", sync);
|
||||
return () => media.removeEventListener("change", sync);
|
||||
}
|
||||
media.addListener?.(sync);
|
||||
return () => media.removeListener?.(sync);
|
||||
}, []);
|
||||
|
||||
const restoreMenuFocus = () => {
|
||||
window.requestAnimationFrame(() => menuButtonRef.current?.focus());
|
||||
};
|
||||
|
||||
const closeMobileMenu = () => {
|
||||
setMobileVisible(false);
|
||||
restoreMenuFocus();
|
||||
};
|
||||
|
||||
const choose = (id: RouteId) => {
|
||||
onNavigate(id);
|
||||
closeMobileMenu();
|
||||
};
|
||||
|
||||
const toggleMenu = () => {
|
||||
if (isMobile) {
|
||||
setMobileVisible(value => !value);
|
||||
return;
|
||||
}
|
||||
setMobileVisible(false);
|
||||
setDesktopVisible(value => !value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout className="tn-shell tn-layout">
|
||||
<nav id="tn-desktop-navigation" className={`tn-desktop-menu ${desktopVisible ? "" : "is-hidden"}`} aria-label="主导航" aria-hidden={!desktopVisible || undefined} inert={!desktopVisible || undefined}>
|
||||
<AppMenu activeId={activeId} onChange={choose} />
|
||||
</nav>
|
||||
<Layout className="tn-layout-main">
|
||||
<AppHeader
|
||||
adminName={adminName}
|
||||
adminUsername={adminUsername}
|
||||
menuButtonRef={menuButtonRef}
|
||||
menuOpen={isMobile ? mobileVisible : desktopVisible}
|
||||
menuControls={isMobile ? "tn-mobile-navigation" : "tn-desktop-navigation"}
|
||||
onToggleMenu={toggleMenu}
|
||||
onLogout={onLogout}
|
||||
onOpenPassword={onOpenPassword}
|
||||
/>
|
||||
<AppContent>{children}</AppContent>
|
||||
</Layout>
|
||||
<Drawer
|
||||
className="tn-mobile-drawer"
|
||||
visible={mobileVisible}
|
||||
placement="left"
|
||||
size="216px"
|
||||
closeBtn={false}
|
||||
header={<div className="tn-mobile-drawer-header"><span>主导航</span><Button variant="text" shape="square" aria-label="关闭导航菜单" onClick={closeMobileMenu} icon={<X size={17} />} /></div>}
|
||||
footer={false}
|
||||
destroyOnClose
|
||||
onClose={closeMobileMenu}
|
||||
>
|
||||
<nav id="tn-mobile-navigation" className="tn-mobile-menu-shell" aria-label="主导航">
|
||||
<AppMenu activeId={activeId} onChange={choose} width={["216px", "64px"]} />
|
||||
</nav>
|
||||
</Drawer>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export { AppContent, AppHeader, AppMenu };
|
||||
Reference in New Issue
Block a user