文件
hyb-oa/client/src/layouts/AppShellLayout.tsx
T

81 行
3.6 KiB
TypeScript

import { useEffect } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { ErrorBoundary } from "../components/common/ErrorBoundary";
import { MobileBottomNav } from "../components/mobile/MobileBottomNav";
import { Sidebar } from "../components/navigation/Sidebar";
import { Topbar } from "../components/navigation/Topbar";
import { resolvePageMeta } from "../features/navigation/schema";
import { writeRecentVisit } from "../lib/recentVisits";
import { resolveMobileEquivalentPath, shouldPreferMobileWorkplace } from "../lib/runtimeContext";
export function AppShellLayout() {
const location = useLocation();
const isMobileRoute = location.pathname.startsWith("/mobile");
const isMobileDeepRoute = /^\/mobile\/reimbursements\/[^/]+$/.test(location.pathname);
const showMobileBottomNav = isMobileRoute && !isMobileDeepRoute;
const mobileEquivalentPath = !isMobileRoute && shouldPreferMobileWorkplace()
? resolveMobileEquivalentPath(location.pathname, location.search)
: null;
useEffect(() => {
if (!isMobileRoute || typeof window === "undefined") {
return;
}
const updateViewportHeight = () => {
const height = Math.max(320, Math.floor(window.visualViewport?.height ?? window.innerHeight));
document.documentElement.style.setProperty("--mobile-viewport-height", `${height}px`);
};
updateViewportHeight();
const frameId = window.requestAnimationFrame(updateViewportHeight);
window.addEventListener("resize", updateViewportHeight);
window.addEventListener("orientationchange", updateViewportHeight);
window.visualViewport?.addEventListener("resize", updateViewportHeight);
window.visualViewport?.addEventListener("scroll", updateViewportHeight);
return () => {
window.cancelAnimationFrame(frameId);
window.removeEventListener("resize", updateViewportHeight);
window.removeEventListener("orientationchange", updateViewportHeight);
window.visualViewport?.removeEventListener("resize", updateViewportHeight);
window.visualViewport?.removeEventListener("scroll", updateViewportHeight);
};
}, [isMobileRoute]);
useEffect(() => {
const pathname = location.pathname;
if (!pathname || pathname === "/login" || pathname.startsWith("/mobile/")) {
return;
}
writeRecentVisit(pathname, resolvePageMeta(pathname).title);
}, [location.pathname]);
if (mobileEquivalentPath && mobileEquivalentPath !== `${location.pathname}${location.search}`) {
return <Navigate to={mobileEquivalentPath} replace />;
}
return (
<div className="relative flex min-h-screen overflow-hidden bg-[var(--color-bg-canvas)]">
{isMobileRoute ? null : <Sidebar />}
<main
className={`flex min-w-0 flex-1 flex-col ${isMobileRoute ? "hyb-mobile-shell overflow-hidden" : "lg:ml-72"}`}
style={isMobileRoute ? { height: "var(--mobile-viewport-height, 100vh)", maxHeight: "var(--mobile-viewport-height, 100vh)", minHeight: "var(--mobile-viewport-height, 100vh)" } : undefined}
>
{isMobileRoute ? null : <Topbar />}
<div className={`flex-1 px-4 py-5 ${isMobileRoute ? `min-h-0 overscroll-contain overflow-y-auto px-[var(--mobile-page-x)] py-[var(--mobile-page-y)] ${showMobileBottomNav ? "pb-[calc(var(--mobile-bottom-nav-height,68px)+env(safe-area-inset-bottom,0px)+var(--mobile-page-y)+0.5rem)]" : "pb-5 sm:pb-6"}` : "sm:px-7 sm:py-7 lg:px-8"}`}>
<div className="layout-container max-w-none">
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</div>
</div>
{showMobileBottomNav ? <MobileBottomNav /> : null}
</main>
</div>
);
}