diff --git a/frontend/app/app.css b/frontend/app/app.css index 4632c29..71d6c96 100644 --- a/frontend/app/app.css +++ b/frontend/app/app.css @@ -30,6 +30,28 @@ --color-bg-canvas: #f9fbfd; --color-bg-log: #17212b; } +@theme inline { + --color-sidebar: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --sidebar-background: #09233f; + --sidebar-foreground: #c9d7e7; + --sidebar-primary: #1479e8; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: rgba(255, 255, 255, 0.06); + --sidebar-accent-foreground: #ffffff; + --sidebar-border: rgba(255, 255, 255, 0.08); + --sidebar-ring: #5ca9ff; +} + @font-face { font-family: "Inter"; diff --git a/frontend/app/components/common/Sidebar.tsx b/frontend/app/components/common/Sidebar.tsx deleted file mode 100644 index 65c8801..0000000 --- a/frontend/app/components/common/Sidebar.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import Icon from "./Icon"; - -type ActivePage = "home" | "scripts" | "schedules" | "system"; - -type SidebarProps = { - activePage: ActivePage; - collapsed: boolean; - onNavigate: (page: ActivePage) => void; - onToggleCollapse: () => void; - onEndEditing?: () => void; - onSelectScript?: (scriptId: null) => void; - editSessionRef?: React.RefObject; - isSystemAdmin?: boolean; -}; - -type NavigationItem = { - label: string; - icon: "home" | "script" | "schedule" | "settings"; - page: ActivePage; -}; - -export function Sidebar({ - activePage, - collapsed, - onNavigate, - onToggleCollapse, - onEndEditing, - onSelectScript, - editSessionRef, - isSystemAdmin = false, -}: SidebarProps) { - const handleNavigationClick = (page: ActivePage) => { - if (page !== "scripts") { - if (editSessionRef?.current) { - onEndEditing?.(); - } else { - onSelectScript?.(null); - } - } - onNavigate(page); - }; - - const navigation: NavigationItem[] = [ - { label: "工作台", icon: "home", page: "home" }, - { label: "构建脚本", icon: "script", page: "scripts" }, - { label: "调度配置", icon: "schedule", page: "schedules" }, - ]; - - if (isSystemAdmin) { - navigation.push({ label: "系统管理", icon: "settings", page: "system" }); - } - - return ( - - ); -} diff --git a/frontend/app/features/admin/SystemAdminPage.tsx b/frontend/app/features/admin/SystemAdminPage.tsx deleted file mode 100644 index da0febf..0000000 --- a/frontend/app/features/admin/SystemAdminPage.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useState } from "react"; - -import Icon from "../../components/common/Icon"; -import { UserManagementPage } from "./UserManagementPage"; -import { ProjectManagementPage } from "./ProjectManagementPage"; - -import "../../styles/admin.css"; - -export function SystemAdminPage({ - onNotify, - onConnectionChange, -}: { - onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; - onConnectionChange: (online: boolean) => void; -}) { - const [activeTab, setActiveTab] = useState<"users" | "projects">("users"); - - return ( -
-
- - -
- - {activeTab === "users" && ( - - )} - {activeTab === "projects" && ( - - )} -
- ); -} diff --git a/frontend/app/features/admin/SystemAdminRoute.tsx b/frontend/app/features/admin/SystemAdminRoute.tsx deleted file mode 100644 index e70ea5b..0000000 --- a/frontend/app/features/admin/SystemAdminRoute.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useScriptWorkspaceStore } from "../platform/state/scriptWorkspaceStore"; -import { useUiStore } from "../platform/state/uiStore"; -import { SystemAdminPage } from "./SystemAdminPage"; - -export default function SystemAdminRoute() { - const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline); - const pushToast = useUiStore((s) => s.pushToast); - - return ( - - ); -} \ No newline at end of file diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts index 56f43e6..52380b8 100644 --- a/frontend/app/routes.ts +++ b/frontend/app/routes.ts @@ -7,6 +7,10 @@ export default [ route("workbench", "features/platform/DashboardRoute.tsx"), route("scripts", "features/platform/ScriptsPage.tsx"), route("schedules", "features/schedules/SchedulesPageRoute.tsx"), - route("system", "features/admin/SystemAdminRoute.tsx"), + route("system", "routes/system.tsx", [ + index("routes/system-index.tsx"), + route("users", "routes/system-users.tsx"), + route("projects", "routes/system-projects.tsx"), + ]), ]), ] satisfies RouteConfig; diff --git a/frontend/app/routes/platform.tsx b/frontend/app/routes/platform.tsx index 25dc30d..4220198 100644 --- a/frontend/app/routes/platform.tsx +++ b/frontend/app/routes/platform.tsx @@ -1,12 +1,30 @@ import { useEffect, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router"; +import { Home, Code, Calendar, Settings, Users, Folder, PanelLeft, ChevronRight } from "lucide-react"; +import { Collapsible } from "@base-ui/react/collapsible"; + 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 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 } from "~/context/AuthContext"; +import { useEditSessionLifecycle } from "~/features/platform/hooks/useEditSessionLifecycle"; import { bindScriptWorkspaceApi, bindScriptWorkspaceId, @@ -14,17 +32,19 @@ import { 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"; +import { useUiStore } from "~/features/platform/state/uiStore"; +import { bindAdminApi } from "~/features/admin/state/adminStore"; +import { bindSchedulesApi } from "~/features/schedules/state/helpers"; 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"; + 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 { @@ -78,6 +98,8 @@ function AuthenticatedLayout() { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const isSystemAdmin = user?.is_system_admin ?? false; + // 绑定 api 到 script workspace store。 // 必须放在 render body(同步),不能用 useEffect([api]) + cleanup — // 后者会在 deps 变化时先 cleanup(null),再让 ScriptsPage 的 useEffect 跑, @@ -110,20 +132,126 @@ function AuthenticatedLayout() { return () => window.clearTimeout(timer); }, [toast, dismissToast]); - return ( -
- navigate(pathForPage(page))} - onToggleCollapse={() => setSidebarCollapsed((v) => !v)} - onEndEditing={() => void endEditing(true)} - onSelectScript={selectScript} - editSessionRef={editSessionHandle} - isSystemAdmin={user?.is_system_admin ?? false} - /> + // 原 common/Sidebar 的编辑会话守卫上移到这里:切出 /scripts 时有活动编辑 + // 会话先 endEditing,回工作台时清空选中脚本;再按当前激活页导航。 + function guardedNavigate(page: ActivePage, sub?: "users" | "projects") { + 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 && ( + + 模型实验开发平台 + + )} +
+
+ + + + guardedNavigate("home")} + isActive={activePage === "home"} + data-active={activePage === "home" ? "true" : undefined} + > + 工作台 + + + + guardedNavigate("scripts")} + isActive={activePage === "scripts"} + data-active={activePage === "scripts" ? "true" : undefined} + > + 构建脚本 + + + + guardedNavigate("schedules")} + isActive={activePage === "schedules"} + data-active={activePage === "schedules" ? "true" : undefined} + > + 调度配置 + + + {isSystemAdmin && ( + + + + } + > + + 系统管理 + + + + + + guardedNavigate("system", "users")} + isActive={location.pathname === "/system/users"} + > + 用户管理 + + + + guardedNavigate("system", "projects")} + isActive={location.pathname === "/system/projects"} + > + 项目管理 + + + + + + + )} + + + + + {!sidebarCollapsed && 收起菜单} + + +
+ + -
+ -
+ ); } diff --git a/frontend/app/routes/system-index.tsx b/frontend/app/routes/system-index.tsx new file mode 100644 index 0000000..fd5eff9 --- /dev/null +++ b/frontend/app/routes/system-index.tsx @@ -0,0 +1,9 @@ +import { Navigate } from "react-router"; + +/** + * "/system" 命中时直接跳到 /system/users(默认子页)。 + * 抽成独立文件是为了不和 route("users", ...) 撞 route id。 + */ +export default function SystemAdminIndexRoute() { + return ; +} diff --git a/frontend/app/routes/system-projects.tsx b/frontend/app/routes/system-projects.tsx new file mode 100644 index 0000000..17df487 --- /dev/null +++ b/frontend/app/routes/system-projects.tsx @@ -0,0 +1,23 @@ +import { useAuth } from "~/context/AuthContext"; +import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore"; +import { useUiStore } from "~/features/platform/state/uiStore"; +import { ProjectManagementPage } from "~/features/admin/ProjectManagementPage"; + +/** + * /system/projects 路由页。ProjectManagementPage 内部有 useState, + * 在 user / workspace 切换时必须重挂载以重置本地状态 + * (见 CLAUDE.md「Route components with their own internal state」)。 + */ +export default function ProjectManagementRoute() { + const { user, currentWorkspace } = useAuth(); + const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline); + const pushToast = useUiStore((s) => s.pushToast); + + return ( + + ); +} diff --git a/frontend/app/routes/system-users.tsx b/frontend/app/routes/system-users.tsx new file mode 100644 index 0000000..7225944 --- /dev/null +++ b/frontend/app/routes/system-users.tsx @@ -0,0 +1,23 @@ +import { useAuth } from "~/context/AuthContext"; +import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore"; +import { useUiStore } from "~/features/platform/state/uiStore"; +import { UserManagementPage } from "~/features/admin/UserManagementPage"; + +/** + * /system/users 路由页。UserManagementPage 内部有 useState, + * 在 user / workspace 切换时必须重挂载以重置本地状态 + * (见 CLAUDE.md「Route components with their own internal state」)。 + */ +export default function UserManagementRoute() { + const { user, currentWorkspace } = useAuth(); + const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline); + const pushToast = useUiStore((s) => s.pushToast); + + return ( + + ); +} diff --git a/frontend/app/routes/system.tsx b/frontend/app/routes/system.tsx new file mode 100644 index 0000000..fa09482 --- /dev/null +++ b/frontend/app/routes/system.tsx @@ -0,0 +1,10 @@ +import { Outlet } from "react-router"; + +/** + * 系统管理父布局:只负责挂载子路由。 + * 页面级 state 由子路由 (UserManagementRoute / ProjectManagementRoute) 持有, + * 并在 user / workspace 切换时通过 key 重挂载重置。 + */ +export default function SystemAdminRoute() { + return ; +}