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 && 收起菜单}
);
}