Files
TallyNote/web-next/src/layouts/AppLayout.tsx
T
Qiufeng 12495fb6a4
TallyNote release / linux-x64 (push) Successful in 6m24s
release: 1.1.0
2026-08-31 20:11:34 +08:00

105 lines
3.6 KiB
TypeScript

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 };