import { useEffect, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router"; import { PanelLeft, ChevronRight } from "lucide-react"; import { navigation } from "./platform.navigation"; import type { ActivePage, NavigationItem, SubPage } from "./platform.navigation"; import { Collapsible } from "@base-ui/react/collapsible"; import type { Route } from "./+types/platform"; import { Toast } from "../components/common/Toast"; import { Topbar } from "../components/common/Topbar"; import Icon from "../components/common/Icon"; import { Sidebar, SidebarProvider, SidebarHeader, SidebarContent, SidebarFooter, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarTrigger, SidebarInset, } from "~/components/ui/sidebar"; import { useApi, useAuth, usePermission } from "~/context/AuthContext"; import { useEditSessionLifecycle } from "~/features/platform/hooks/useEditSessionLifecycle"; import { bindScriptWorkspaceApi, bindScriptWorkspaceId, bindScriptWorkspaceUser, editSessionHandle, useScriptWorkspaceStore, } from "../features/platform/state/scriptWorkspaceStore"; import { useUiStore } from "~/features/platform/state/uiStore"; import { bindAdminApi } from "~/features/admin/state/adminStore"; import { bindSchedulesApi } from "~/features/schedules/state/helpers"; function pageFromPath(pathname: string): ActivePage { const first = pathname.replace(/^\/+/, "").split("/")[0]; if (first === "workbench" || first === "") return "home"; if (first === "scripts" || first === "schedules" || first === "system") { return first as ActivePage; } return "home"; } function pathForPage(page: ActivePage): string { if (page === "home") return "/workbench"; return `/${page}`; } export function meta({}: Route.MetaArgs) { return [ { title: "模型实验开发平台" }, { name: "description", content: "模型实验、脚本版本与 DAG 调度平台" }, ]; } function NavRow({ item, can, activePage, pathname, onNavigate, }: { item: NavigationItem; can: (code: string) => boolean; activePage: ActivePage; pathname: string; onNavigate: (sub?: SubPage) => void; }) { const itemIcon = ; const childActivePath = (child: NavigationItem) => child.activePath ?? pathForPage(child.page); if (item.children && item.children.length > 0) { return ( } > {itemIcon} {item.label} {item.children .filter((child) => !child.permission || can(child.permission)) .map((child) => ( onNavigate(child.sub)} isActive={pathname === childActivePath(child)} > {child.label} ))} ); } return ( onNavigate()} isActive={activePage === item.page} data-active={activePage === item.page ? "true" : undefined} > {itemIcon} {item.label} ); } export default function PlatformLayout() { const { currentWorkspace } = useAuth(); if (!currentWorkspace) { return (
加载中…
); } return ; } function AuthenticatedLayout() { const api = useApi(); const location = useLocation(); const navigate = useNavigate(); const activePage = pageFromPath(location.pathname); const auth = useAuth(); const { user, workspaces, currentWorkspace, setCurrentWorkspace, logout, } = auth; const apiOnline = useScriptWorkspaceStore((s) => s.apiOnline); const endEditing = useScriptWorkspaceStore((s) => s.endEditing); const selectScript = useScriptWorkspaceStore((s) => s.selectScript); const toast = useUiStore((s) => s.toast); const dismissToast = useUiStore((s) => s.dismissToast); const workspaceMenuOpen = useUiStore((s) => s.workspaceMenuOpen); const setWorkspaceMenuOpen = useUiStore((s) => s.setWorkspaceMenuOpen); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const can = usePermission; // 绑定 api 到 script workspace store。 // 必须放在 render body(同步),不能用 useEffect([api]) + cleanup — // 后者会在 deps 变化时先 cleanup(null),再让 ScriptsPage 的 useEffect 跑, // 此时 _api 为 null,load() 抛 "script workspace API 未绑定"。 bindScriptWorkspaceApi(api); bindSchedulesApi(api); bindAdminApi(api); // 当前用户 id + 工作区 id 同步绑定到 script workspace store(render body, // 与 bindScriptWorkspaceApi 同理)。store 的 lazy 跨 owner 逻辑据此区分 // "我"与他人、并调用需要显式 workspaceId 的 listWorkspaceMembers。 bindScriptWorkspaceUser(user?.user_id ?? null); bindScriptWorkspaceId(currentWorkspace?.workspace_id ?? null); useEffect(() => { return () => { bindScriptWorkspaceApi(null); bindSchedulesApi(null); bindAdminApi(null); bindScriptWorkspaceUser(null); bindScriptWorkspaceId(null); }; }, []); // 心跳 / cleanup / 切页结束编辑 / 卸载前释放 useEditSessionLifecycle({ activePage }); // toast 自动消失 useEffect(() => { if (!toast) return; const timer = window.setTimeout(dismissToast, 3200); return () => window.clearTimeout(timer); }, [toast, dismissToast]); // 原 common/Sidebar 的编辑会话守卫上移到这里:切出 /scripts 时有活动编辑 // 会话先 endEditing,回工作台时清空选中脚本;再按当前激活页导航。 function guardedNavigate(page: ActivePage, sub?: SubPage) { if (page !== "scripts" && editSessionHandle.current) { void endEditing(true); } else if (page === "home") { selectScript(null); } const base = pathForPage(page); navigate(sub ? `${base}/${sub}` : base); } return ( setSidebarCollapsed(!open)} className="bg-[#eef2f6]" >
{!sidebarCollapsed && ( 模型实验开发平台 )}
{navigation .filter((item) => !item.permission || can(item.permission)) .map((item) => ( guardedNavigate(item.page, sub)} /> ))} {!sidebarCollapsed && 收起菜单}
); }