Files
model-platform/frontend/app/routes/platform.tsx
T
2026-09-02 10:10:41 +08:00

146 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router";
import type { Route } from "./+types/platform";
import { Sidebar } from "../components/common/Sidebar";
import { Toast } from "../components/common/Toast";
import { Topbar } from "../components/common/Topbar";
import { useApi, useAuth } 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/useSchedulesStore";
type ActivePage = "home" | "scripts" | "schedules" | "system";
function pageFromPath(pathname: string): ActivePage {
const page = pathname.replace(/^\/+|\/+$/g, "");
return ["scripts", "schedules", "system"].includes(page)
? (page as ActivePage)
: "home";
}
function pathForPage(page: ActivePage): string {
if (page === "home") return "/workbench";
return `/${page}`;
}
export function meta({}: Route.MetaArgs) {
return [
{ title: "模型实验开发平台" },
{ name: "description", content: "模型实验、脚本版本与 DAG 调度平台" },
];
}
export default function PlatformLayout() {
const { currentWorkspace } = useAuth();
if (!currentWorkspace) {
return (
<div className="flex min-h-screen items-center justify-center bg-[#eef2f6]">
<div className="text-center">
<span className="text-lg">加载中…</span>
</div>
</div>
);
}
return <AuthenticatedLayout />;
}
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);
// 绑定 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 storerender 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]);
return (
<div className="flex min-h-screen bg-[#eef2f6]">
<Sidebar
activePage={activePage}
collapsed={sidebarCollapsed}
onNavigate={(page) => navigate(pathForPage(page))}
onToggleCollapse={() => setSidebarCollapsed((v) => !v)}
onEndEditing={() => void endEditing(true)}
onSelectScript={selectScript}
editSessionRef={editSessionHandle}
isSystemAdmin={user?.is_system_admin ?? false}
/>
<main className="flex h-screen min-w-0 flex-1 flex-col">
<Topbar
activePage={activePage}
apiOnline={apiOnline}
user={user}
currentWorkspace={currentWorkspace!}
workspaces={workspaces}
workspaceMenuOpen={workspaceMenuOpen}
onSetWorkspaceMenuOpen={setWorkspaceMenuOpen}
onSetCurrentWorkspace={setCurrentWorkspace}
onLogout={logout}
/>
<Outlet />
</main>
<Toast toast={toast} />
</div>
);
}