refactor: use shadcn sidebar
This commit is contained in:
@@ -30,6 +30,28 @@
|
|||||||
--color-bg-canvas: #f9fbfd;
|
--color-bg-canvas: #f9fbfd;
|
||||||
--color-bg-log: #17212b;
|
--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-face {
|
||||||
font-family: "Inter";
|
font-family: "Inter";
|
||||||
|
|||||||
@@ -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<unknown | null>;
|
|
||||||
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 (
|
|
||||||
<aside
|
|
||||||
className={`sidebar-shell relative z-[3] flex min-h-screen flex-col text-[#c9d7e7] shadow-[5px_0_18px_rgb(19_47_77_/_9%)] transition-[width,min-width] duration-200 ease-in-out ${
|
|
||||||
collapsed
|
|
||||||
? "w-[70px] min-w-[70px]"
|
|
||||||
: "w-[210px] min-w-[210px] max-xl:w-[184px] max-xl:min-w-[184px]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex h-[70px] items-center gap-2.5 border-b border-white/8 text-white ${
|
|
||||||
collapsed ? "justify-center px-0" : "px-[17px]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="grid size-9 shrink-0 place-items-center text-[#5ca9ff]">
|
|
||||||
<Icon name="brand" size={31} />
|
|
||||||
</span>
|
|
||||||
{!collapsed && (
|
|
||||||
<span className="whitespace-nowrap text-[15px] font-bold tracking-[0.01em] max-xl:text-[13px]">
|
|
||||||
模型实验开发平台
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!collapsed && (
|
|
||||||
<nav
|
|
||||||
className="flex flex-col gap-[7px] px-[9px] py-[19px]"
|
|
||||||
aria-label="主导航"
|
|
||||||
>
|
|
||||||
{navigation.map((item) => {
|
|
||||||
const isActive = item.page === activePage;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={`flex h-12 w-full items-center gap-[13px] rounded-md border-0 px-[17px] text-left text-sm transition-[color,background] duration-[180ms] ease-in-out ${
|
|
||||||
isActive
|
|
||||||
? "bg-gradient-to-br from-[#1479e8] to-[#1267ca] text-white shadow-[0_7px_18px_rgb(7_96_193_/_26%)]"
|
|
||||||
: "bg-transparent text-[#b9cade] hover:bg-white/6 hover:text-white"
|
|
||||||
}`}
|
|
||||||
key={item.label}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleNavigationClick(item.page)}
|
|
||||||
>
|
|
||||||
<Icon name={item.icon} size={19} />
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`mt-auto mb-[17px] flex h-12 cursor-pointer items-center gap-[13px] rounded-md border-0 bg-transparent text-sm text-[#b9cade] transition-[color,background] duration-[180ms] ease-in-out hover:bg-white/6 hover:text-white ${
|
|
||||||
collapsed
|
|
||||||
? "mx-auto w-[calc(100%-18px)] justify-center px-[17px]"
|
|
||||||
: "mx-[9px] w-[calc(100%-18px)] px-[17px]"
|
|
||||||
}`}
|
|
||||||
type="button"
|
|
||||||
onClick={onToggleCollapse}
|
|
||||||
aria-label={collapsed ? "展开菜单" : "收起菜单"}
|
|
||||||
>
|
|
||||||
<Icon name="menu" size={19} />
|
|
||||||
{!collapsed && <span>收起菜单</span>}
|
|
||||||
</button>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<section className="admin-page">
|
|
||||||
<div className="admin-tabs">
|
|
||||||
<button
|
|
||||||
className={`admin-tab${activeTab === "users" ? " is-active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setActiveTab("users")}
|
|
||||||
>
|
|
||||||
<Icon name="workspace" size={16} />
|
|
||||||
用户管理
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`admin-tab${activeTab === "projects" ? " is-active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setActiveTab("projects")}
|
|
||||||
>
|
|
||||||
<Icon name="folder" size={16} />
|
|
||||||
项目管理
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeTab === "users" && (
|
|
||||||
<UserManagementPage onNotify={onNotify} onConnectionChange={onConnectionChange} />
|
|
||||||
)}
|
|
||||||
{activeTab === "projects" && (
|
|
||||||
<ProjectManagementPage onNotify={onNotify} onConnectionChange={onConnectionChange} />
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<SystemAdminPage
|
|
||||||
onNotify={pushToast}
|
|
||||||
onConnectionChange={setApiOnline}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,10 @@ export default [
|
|||||||
route("workbench", "features/platform/DashboardRoute.tsx"),
|
route("workbench", "features/platform/DashboardRoute.tsx"),
|
||||||
route("scripts", "features/platform/ScriptsPage.tsx"),
|
route("scripts", "features/platform/ScriptsPage.tsx"),
|
||||||
route("schedules", "features/schedules/SchedulesPageRoute.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;
|
] satisfies RouteConfig;
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router";
|
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 type { Route } from "./+types/platform";
|
||||||
import { Sidebar } from "../components/common/Sidebar";
|
|
||||||
import { Toast } from "../components/common/Toast";
|
import { Toast } from "../components/common/Toast";
|
||||||
import { Topbar } from "../components/common/Topbar";
|
import { Topbar } from "../components/common/Topbar";
|
||||||
import { useApi, useAuth } from "../context/AuthContext";
|
import Icon from "../components/common/Icon";
|
||||||
import { useEditSessionLifecycle } from "../features/platform/hooks/useEditSessionLifecycle";
|
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 {
|
import {
|
||||||
bindScriptWorkspaceApi,
|
bindScriptWorkspaceApi,
|
||||||
bindScriptWorkspaceId,
|
bindScriptWorkspaceId,
|
||||||
@@ -14,17 +32,19 @@ import {
|
|||||||
editSessionHandle,
|
editSessionHandle,
|
||||||
useScriptWorkspaceStore,
|
useScriptWorkspaceStore,
|
||||||
} from "../features/platform/state/scriptWorkspaceStore";
|
} from "../features/platform/state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "../features/platform/state/uiStore";
|
import { useUiStore } from "~/features/platform/state/uiStore";
|
||||||
import { bindAdminApi } from "../features/admin/state/adminStore";
|
import { bindAdminApi } from "~/features/admin/state/adminStore";
|
||||||
import { bindSchedulesApi } from "../features/schedules/state/useSchedulesStore";
|
import { bindSchedulesApi } from "~/features/schedules/state/helpers";
|
||||||
|
|
||||||
type ActivePage = "home" | "scripts" | "schedules" | "system";
|
type ActivePage = "home" | "scripts" | "schedules" | "system";
|
||||||
|
|
||||||
function pageFromPath(pathname: string): ActivePage {
|
function pageFromPath(pathname: string): ActivePage {
|
||||||
const page = pathname.replace(/^\/+|\/+$/g, "");
|
const first = pathname.replace(/^\/+/, "").split("/")[0];
|
||||||
return ["scripts", "schedules", "system"].includes(page)
|
if (first === "workbench" || first === "") return "home";
|
||||||
? (page as ActivePage)
|
if (first === "scripts" || first === "schedules" || first === "system") {
|
||||||
: "home";
|
return first as ActivePage;
|
||||||
|
}
|
||||||
|
return "home";
|
||||||
}
|
}
|
||||||
|
|
||||||
function pathForPage(page: ActivePage): string {
|
function pathForPage(page: ActivePage): string {
|
||||||
@@ -78,6 +98,8 @@ function AuthenticatedLayout() {
|
|||||||
|
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const isSystemAdmin = user?.is_system_admin ?? false;
|
||||||
|
|
||||||
// 绑定 api 到 script workspace store。
|
// 绑定 api 到 script workspace store。
|
||||||
// 必须放在 render body(同步),不能用 useEffect([api]) + cleanup —
|
// 必须放在 render body(同步),不能用 useEffect([api]) + cleanup —
|
||||||
// 后者会在 deps 变化时先 cleanup(null),再让 ScriptsPage 的 useEffect 跑,
|
// 后者会在 deps 变化时先 cleanup(null),再让 ScriptsPage 的 useEffect 跑,
|
||||||
@@ -110,20 +132,126 @@ function AuthenticatedLayout() {
|
|||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [toast, dismissToast]);
|
}, [toast, dismissToast]);
|
||||||
|
|
||||||
return (
|
// 原 common/Sidebar 的编辑会话守卫上移到这里:切出 /scripts 时有活动编辑
|
||||||
<div className="flex min-h-screen bg-[#eef2f6]">
|
// 会话先 endEditing,回工作台时清空选中脚本;再按当前激活页导航。
|
||||||
<Sidebar
|
function guardedNavigate(page: ActivePage, sub?: "users" | "projects") {
|
||||||
activePage={activePage}
|
if (page !== "scripts" && editSessionHandle.current) {
|
||||||
collapsed={sidebarCollapsed}
|
void endEditing(true);
|
||||||
onNavigate={(page) => navigate(pathForPage(page))}
|
} else if (page === "home") {
|
||||||
onToggleCollapse={() => setSidebarCollapsed((v) => !v)}
|
selectScript(null);
|
||||||
onEndEditing={() => void endEditing(true)}
|
}
|
||||||
onSelectScript={selectScript}
|
const base = pathForPage(page);
|
||||||
editSessionRef={editSessionHandle}
|
navigate(sub ? `${base}/${sub}` : base);
|
||||||
isSystemAdmin={user?.is_system_admin ?? false}
|
}
|
||||||
/>
|
|
||||||
|
|
||||||
<main className="flex h-screen min-w-0 flex-1 flex-col">
|
return (
|
||||||
|
<SidebarProvider
|
||||||
|
defaultOpen={!sidebarCollapsed}
|
||||||
|
open={!sidebarCollapsed}
|
||||||
|
onOpenChange={(open) => setSidebarCollapsed(!open)}
|
||||||
|
className="bg-[#eef2f6]"
|
||||||
|
>
|
||||||
|
<Sidebar collapsible="icon">
|
||||||
|
<SidebarHeader>
|
||||||
|
<div
|
||||||
|
className={`flex h-[70px] items-center gap-2.5 border-b border-[var(--sidebar-border)] text-white ${sidebarCollapsed ? "justify-center px-0" : "px-[17px]"}`}
|
||||||
|
>
|
||||||
|
<span className="grid size-9 shrink-0 place-items-center text-[#5ca9ff]">
|
||||||
|
<Icon name="brand" size={31} />
|
||||||
|
</span>
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<span className="whitespace-nowrap text-[15px] font-bold tracking-[0.01em] max-xl:text-[13px]">
|
||||||
|
模型实验开发平台
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarMenu className="gap-1.5">
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
className="py-3 text-sm"
|
||||||
|
onClick={() => guardedNavigate("home")}
|
||||||
|
isActive={activePage === "home"}
|
||||||
|
data-active={activePage === "home" ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<Home /> <span>工作台</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
className="py-3 text-sm"
|
||||||
|
onClick={() => guardedNavigate("scripts")}
|
||||||
|
isActive={activePage === "scripts"}
|
||||||
|
data-active={activePage === "scripts" ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<Code /> <span>构建脚本</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
className="py-3 text-sm"
|
||||||
|
onClick={() => guardedNavigate("schedules")}
|
||||||
|
isActive={activePage === "schedules"}
|
||||||
|
data-active={activePage === "schedules" ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<Calendar /> <span>调度配置</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
{isSystemAdmin && (
|
||||||
|
<Collapsible.Root
|
||||||
|
defaultOpen={activePage === "system"}
|
||||||
|
className="group/collapsible"
|
||||||
|
>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<Collapsible.Trigger
|
||||||
|
render={
|
||||||
|
<SidebarMenuButton
|
||||||
|
className="py-3 text-sm"
|
||||||
|
isActive={activePage === "system"}
|
||||||
|
data-active={activePage === "system" ? "true" : undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Settings />
|
||||||
|
<span>系统管理</span>
|
||||||
|
<ChevronRight className="ml-auto size-4 transition-transform group-data-[open]/collapsible:rotate-90" />
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Panel>
|
||||||
|
<SidebarMenuSub className="gap-1.5 mt-2">
|
||||||
|
<SidebarMenuSubItem>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
className="h-9 text-xs"
|
||||||
|
onClick={() => guardedNavigate("system", "users")}
|
||||||
|
isActive={location.pathname === "/system/users"}
|
||||||
|
>
|
||||||
|
<Users /> <span>用户管理</span>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
<SidebarMenuSubItem>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
className="h-9 text-xs"
|
||||||
|
onClick={() => guardedNavigate("system", "projects")}
|
||||||
|
isActive={location.pathname === "/system/projects"}
|
||||||
|
>
|
||||||
|
<Folder /> <span>项目管理</span>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</Collapsible.Panel>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible.Root>
|
||||||
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarContent>
|
||||||
|
<SidebarFooter>
|
||||||
|
<SidebarTrigger>
|
||||||
|
<PanelLeft /> {!sidebarCollapsed && <span>收起菜单</span>}
|
||||||
|
</SidebarTrigger>
|
||||||
|
</SidebarFooter>
|
||||||
|
</Sidebar>
|
||||||
|
|
||||||
|
<SidebarInset className="h-screen min-w-0">
|
||||||
<Topbar
|
<Topbar
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
apiOnline={apiOnline}
|
apiOnline={apiOnline}
|
||||||
@@ -137,9 +265,9 @@ function AuthenticatedLayout() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</SidebarInset>
|
||||||
|
|
||||||
<Toast toast={toast} />
|
<Toast toast={toast} />
|
||||||
</div>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Navigate } from "react-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "/system" 命中时直接跳到 /system/users(默认子页)。
|
||||||
|
* 抽成独立文件是为了不和 route("users", ...) 撞 route id。
|
||||||
|
*/
|
||||||
|
export default function SystemAdminIndexRoute() {
|
||||||
|
return <Navigate to="/system/users" replace />;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<ProjectManagementPage
|
||||||
|
key={`${user?.user_id ?? "anon"}-${currentWorkspace?.workspace_id ?? "none"}`}
|
||||||
|
onNotify={pushToast}
|
||||||
|
onConnectionChange={setApiOnline}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<UserManagementPage
|
||||||
|
key={`${user?.user_id ?? "anon"}-${currentWorkspace?.workspace_id ?? "none"}`}
|
||||||
|
onNotify={pushToast}
|
||||||
|
onConnectionChange={setApiOnline}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统管理父布局:只负责挂载子路由。
|
||||||
|
* 页面级 state 由子路由 (UserManagementRoute / ProjectManagementRoute) 持有,
|
||||||
|
* 并在 user / workspace 切换时通过 key 重挂载重置。
|
||||||
|
*/
|
||||||
|
export default function SystemAdminRoute() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user