import { Bot, Heart, LayoutDashboard, PackageSearch, PanelLeft, PanelRight, ServerCog, ShieldCheck, UserRoundPen, WandSparkles, Wrench, X } from "lucide-react"; import { type ComponentType, type ReactNode, type TouchEvent, useEffect, useRef, useState } from "react"; import type { PageId, PageParams, PageRoute } from "../contracts/page"; import type { CurrentUserView, OperationRecord } from "../contracts/workspace"; import { MagicalParticleLayer } from "./MagicalParticleLayer"; import { OperationsTray } from "./OperationsTray"; import { applyBackgroundImage, applyThemeBackgroundPreset, applyThemePalette, loadThemeState, themePaletteChangeEvent, themePalettes, themeTokens, type ThemePaletteChangeDetail, type WorkspaceThemeState } from "../theme/tokens"; import { cx } from "../utils/classes"; interface AppShellProps { routes: PageRoute[]; currentPage: PageId; session: CurrentUserView; operations: OperationRecord[]; onNavigate: (pageId: PageId, params?: PageParams) => void; children: ReactNode; } interface MenuGroup { id: string; label: string; routeIds: PageId[]; icon: ComponentType<{ size?: number; className?: string }>; } const menuGroups: MenuGroup[] = [ { id: "overview", label: "平台概览", routeIds: ["home"], icon: LayoutDashboard }, { id: "servers", label: "服务器管理", routeIds: ["servers"], icon: ServerCog }, { id: "plugins", label: "插件市场", routeIds: ["plugins"], icon: PackageSearch }, { id: "users", label: "用户管理", routeIds: ["users"], icon: ShieldCheck }, { id: "ai", label: "AI 提供商管理", routeIds: ["aiProviders"], icon: Bot }, { id: "tools", label: "系统工具", routeIds: ["maintenance"], icon: Wrench } ]; const roleLabels: Record = { platformAdmin: "平台管理员", serverOwner: "服主", serverAdmin: "服务器管理员" }; export function AppShell({ routes, currentPage, session, operations, onNavigate, children }: AppShellProps) { const [themeState, setThemeState] = useState(() => loadThemeState()); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const touchStart = useRef<{ x: number; y: number } | null>(null); useEffect(() => { function handleThemePaletteChange(event: Event) { const paletteId = (event as CustomEvent).detail?.paletteId; if (!paletteId) { return; } setThemeState((current) => (current.paletteId === paletteId ? current : { ...current, paletteId })); } window.addEventListener(themePaletteChangeEvent, handleThemePaletteChange); const stored = loadThemeState(); applyThemePalette(stored.paletteId); applyThemeBackgroundPreset(stored.backgroundPresetId); applyBackgroundImage(stored.backgroundImage); setThemeState(stored); return () => window.removeEventListener(themePaletteChangeEvent, handleThemePaletteChange); }, []); useEffect(() => { if (!isMobileSidebarOpen) { return; } function handleKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { setIsMobileSidebarOpen(false); } } const isNarrow = window.matchMedia("(max-width: 760px)").matches; const previousOverflow = document.body.style.overflow; if (isNarrow) { document.body.style.overflow = "hidden"; } window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); if (isNarrow) { document.body.style.overflow = previousOverflow; } }; }, [isMobileSidebarOpen]); const activePalette = themePalettes.find((palette) => palette.id === themeState.paletteId) ?? themePalettes[0]; const routesById = new Map(routes.map((route) => [route.id, route])); const visibleGroups = menuGroups .map((group) => ({ ...group, routes: group.routeIds.map((routeId) => routesById.get(routeId)).filter((route): route is PageRoute => Boolean(route)) })) .filter((group) => group.routes.length > 0); function activateGroup(group: (typeof visibleGroups)[number]) { setIsMobileSidebarOpen(false); if (group.routes[0].id !== currentPage) { onNavigate(group.routes[0].id); } } function handleTouchStart(event: TouchEvent) { const touch = event.touches[0]; touchStart.current = touch ? { x: touch.clientX, y: touch.clientY } : null; } function handleTouchEnd(event: TouchEvent) { const start = touchStart.current; const touch = event.changedTouches[0]; touchStart.current = null; if (!start || !touch || !window.matchMedia("(max-width: 760px)").matches) { return; } const deltaX = touch.clientX - start.x; const deltaY = touch.clientY - start.y; if (Math.abs(deltaY) >= Math.abs(deltaX)) { return; } if (!isMobileSidebarOpen && start.x <= 28 && deltaX >= 56) { setIsMobileSidebarOpen(true); } else if (isMobileSidebarOpen && deltaX <= -56) { setIsMobileSidebarOpen(false); } } return (
{themeTokens.appName} {activePalette.label} / OPS
导航
{activePalette.swatches.map((swatch) => ( ))}
{children}
); }