文件
hyb-oa/client/src/layouts/AppShellLayout.tsx
T
wangsli c018ea4eb8
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
fix(pc-detail-responsive-approval-polish): 协调详情断点并收敛审批展示
2026-08-03 21:31:44 +08:00

88 行
4.0 KiB
TypeScript

import { useEffect } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import { AppPwaUpdateNotice } from "../components/common/AppPwaUpdateNotice";
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 { permissions } = useAuth();
const location = useLocation();
const isMobileRoute = location.pathname.startsWith("/mobile");
const mobileSearchParams = new URLSearchParams(location.search);
const isMobileDeepRoute =
/^\/mobile\/(?:reimbursements|approvals|leave|purchase-requests)\/[^/]+$/.test(location.pathname) ||
(location.pathname === "/mobile/approvals" && mobileSearchParams.has("taskId"));
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, permissions).title, permissions);
}, [location.pathname, permissions]);
if (mobileEquivalentPath && mobileEquivalentPath !== `${location.pathname}${location.search}`) {
return <Navigate to={mobileEquivalentPath} replace />;
}
return (
<div className="app-shell relative flex min-h-screen overflow-hidden">
{isMobileRoute ? null : <Sidebar />}
<main
className={`app-shell-main flex min-w-0 flex-1 flex-col ${isMobileRoute ? "hyb-mobile-shell overflow-hidden" : "xl: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">
<AppPwaUpdateNotice />
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</div>
</div>
{showMobileBottomNav ? <MobileBottomNav /> : null}
</main>
</div>
);
}