import { useState, useEffect } from "react"; import { Outlet, Link, useLocation } from "react-router"; import type { LucideIcon } from "lucide-react"; import { Menu, Home, Share2, Compass, Library, Globe, BookOpen, Wrench, } from "lucide-react"; import { TibetanPattern } from "./TibetanPattern"; import { fetchAndApplyKioskBundle } from "../api/kioskApi"; import { DEFAULT_HOME_BG_URLS, KIOSK_EVENT, loadHomeBackgrounds } from "../kioskStorage"; import { useI18n } from "../i18n"; const BG_ROTATE_MS = 7000; function preloadImages(urls: readonly string[]) { urls.forEach((src) => { const img = new Image(); img.src = src; }); } export function Layout() { const { t } = useI18n(); const [sidebarOpen, setSidebarOpen] = useState(false); const [homeBgIndex, setHomeBgIndex] = useState(0); const [homeBackgrounds, setHomeBackgrounds] = useState(loadHomeBackgrounds); const location = useLocation(); const isHome = location.pathname === "/"; const isTourPage = location.pathname === "/tour"; const isSearchPage = location.pathname === "/search"; const isGuidePage = location.pathname === "/guide"; const isWebPage = location.pathname === "/web"; const isAdminPage = location.pathname === "/admin"; const isViewportLockPage = isTourPage || isSearchPage || isGuidePage || isWebPage; /** 顶栏品牌标题全站统一(非首页顶栏显示) */ const headerBrandTitle = t("brand.title"); useEffect(() => { const sync = () => { setHomeBackgrounds(loadHomeBackgrounds()); }; window.addEventListener(KIOSK_EVENT, sync); return () => { window.removeEventListener(KIOSK_EVENT, sync); }; }, []); /** 启动时从后端拉取 bundle 写入内存(不写 localStorage;失败时首页背景等仍用内置默认图) */ useEffect(() => { let cancelled = false; void (async () => { try { await fetchAndApplyKioskBundle(); } catch (e) { if (!cancelled) { console.warn("[kiosk] 无法从后端拉取 bundle", e); } } })(); return () => { cancelled = true; }; }, []); useEffect(() => { preloadImages(homeBackgrounds); }, [homeBackgrounds]); useEffect(() => { setHomeBgIndex((i) => homeBackgrounds.length === 0 ? 0 : Math.min(i, homeBackgrounds.length - 1) ); }, [homeBackgrounds.length]); useEffect(() => { if (!isHome) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); if (reduceMotion.matches) return; const n = homeBackgrounds.length; if (n <= 1) return; const interval = window.setInterval(() => { setHomeBgIndex((prev) => (prev + 1) % n); }, BG_ROTATE_MS); return () => window.clearInterval(interval); }, [isHome, homeBackgrounds.length]); useEffect(() => { const root = document.documentElement; if (isViewportLockPage) { root.classList.add("app-viewport-lock"); } else { root.classList.remove("app-viewport-lock"); } return () => root.classList.remove("app-viewport-lock"); }, [isViewportLockPage]); /** 与首页六块入口顺序、文案一致;首页不含「首页」项,顶栏保留首页便于返回 */ const navItems: ( | { id: string; path: string; label: string; icon: LucideIcon } | { id: string; href: string; label: string; icon: LucideIcon; external: true } )[] = [ { id: "home", path: "/", label: t("nav.home"), icon: Home }, { id: "tour", path: "/tour", label: t("nav.tour"), icon: Share2 }, { id: "planetarium", path: "/planetarium", label: t("nav.planetarium"), icon: Compass }, { id: "wiki", path: "/search", label: t("nav.wiki"), icon: Library }, { id: "web", path: "/web", label: t("nav.web"), icon: Globe }, { id: "guide", path: "/guide", label: t("nav.guide"), icon: BookOpen }, { id: "maintain", path: "/admin", label: t("nav.maintain"), icon: Wrench }, ]; return (