refactor: permission
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { Home, Code, Calendar, Settings, Users, Folder } from "lucide-react";
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
export type ActivePage = "home" | "scripts" | "schedules" | "system";
|
||||
export type SubPage = "users" | "projects";
|
||||
|
||||
export type NavigationItem = {
|
||||
/** 菜单显示名 */
|
||||
label: string;
|
||||
/** 渲染在菜单前的图标组件(lucide-react 或等价) */
|
||||
icon: ComponentType;
|
||||
/** 入口页 — 点击后由 guardedNavigate 处理 */
|
||||
page: ActivePage;
|
||||
/** 子页段,仅 children 项使用 */
|
||||
sub?: SubPage;
|
||||
/** 命中高亮的 pathname;不填则按 pathForPage(page) 派生 */
|
||||
activePath?: string;
|
||||
/** 当前端可访问的权限码;未配置时视为所有人可访问 */
|
||||
permission?: string;
|
||||
/** 子菜单条目 */
|
||||
children?: NavigationItem[];
|
||||
};
|
||||
|
||||
export const navigation: NavigationItem[] = [
|
||||
{ label: "工作台", icon: Home, page: "home", permission: "dashboard:view" },
|
||||
{ label: "构建脚本", icon: Code, page: "scripts", permission: "script:view" },
|
||||
{ label: "调度配置", icon: Calendar, page: "schedules", permission: "schedule:view" },
|
||||
{
|
||||
label: "系统管理",
|
||||
icon: Settings,
|
||||
page: "system",
|
||||
permission: "system:view",
|
||||
children: [
|
||||
{ label: "用户管理", icon: Users, page: "system", sub: "users", permission: "system:user:view" },
|
||||
{ label: "项目管理", icon: Folder, page: "system", sub: "projects", permission: "system:project:view" },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,7 +1,9 @@
|
||||
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 { PanelLeft, ChevronRight } from "lucide-react";
|
||||
import { navigation } from "./platform.navigation";
|
||||
import type { ActivePage, NavigationItem } from "./platform.navigation";
|
||||
import { Collapsible } from "@base-ui/react/collapsible";
|
||||
|
||||
import type { Route } from "./+types/platform";
|
||||
@@ -23,7 +25,7 @@ import {
|
||||
SidebarTrigger,
|
||||
SidebarInset,
|
||||
} from "~/components/ui/sidebar";
|
||||
import { useApi, useAuth } from "~/context/AuthContext";
|
||||
import { useApi, useAuth, usePermission } from "~/context/AuthContext";
|
||||
import { useEditSessionLifecycle } from "~/features/platform/hooks/useEditSessionLifecycle";
|
||||
import {
|
||||
bindScriptWorkspaceApi,
|
||||
@@ -36,8 +38,6 @@ 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 first = pathname.replace(/^\/+/, "").split("/")[0];
|
||||
if (first === "workbench" || first === "") return "home";
|
||||
@@ -59,6 +59,80 @@ export function meta({}: Route.MetaArgs) {
|
||||
];
|
||||
}
|
||||
|
||||
function NavRow({
|
||||
item,
|
||||
can,
|
||||
activePage,
|
||||
pathname,
|
||||
onNavigate,
|
||||
}: {
|
||||
item: NavigationItem;
|
||||
can: (code: string) => boolean;
|
||||
activePage: ActivePage;
|
||||
pathname: string;
|
||||
onNavigate: (sub?: "users" | "projects") => void;
|
||||
}) {
|
||||
const itemIcon = <item.icon />;
|
||||
const childActivePath = (child: NavigationItem) =>
|
||||
child.activePath ?? pathForPage(child.page);
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
return (
|
||||
<Collapsible.Root
|
||||
defaultOpen={activePage === item.page}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<Collapsible.Trigger
|
||||
render={
|
||||
<SidebarMenuButton
|
||||
className="py-3 text-sm"
|
||||
isActive={activePage === item.page}
|
||||
data-active={activePage === item.page ? "true" : undefined}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{itemIcon}
|
||||
<span>{item.label}</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">
|
||||
{item.children
|
||||
.filter((child) => !child.permission || can(child.permission))
|
||||
.map((child) => (
|
||||
<SidebarMenuSubItem key={child.label}>
|
||||
<SidebarMenuSubButton
|
||||
className="h-9 text-xs"
|
||||
onClick={() => onNavigate(child.sub)}
|
||||
isActive={pathname === childActivePath(child)}
|
||||
>
|
||||
<child.icon /> <span>{child.label}</span>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</Collapsible.Panel>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className="py-3 text-sm"
|
||||
onClick={() => onNavigate()}
|
||||
isActive={activePage === item.page}
|
||||
data-active={activePage === item.page ? "true" : undefined}
|
||||
>
|
||||
{itemIcon}
|
||||
<span>{item.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlatformLayout() {
|
||||
const { currentWorkspace } = useAuth();
|
||||
if (!currentWorkspace) {
|
||||
@@ -98,7 +172,7 @@ function AuthenticatedLayout() {
|
||||
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
const isSystemAdmin = user?.is_system_admin ?? false;
|
||||
const can = usePermission;
|
||||
|
||||
// 绑定 api 到 script workspace store。
|
||||
// 必须放在 render body(同步),不能用 useEffect([api]) + cleanup —
|
||||
@@ -168,80 +242,18 @@ function AuthenticatedLayout() {
|
||||
</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>
|
||||
)}
|
||||
{navigation
|
||||
.filter((item) => !item.permission || can(item.permission))
|
||||
.map((item) => (
|
||||
<NavRow
|
||||
key={item.page}
|
||||
item={item}
|
||||
can={can}
|
||||
activePage={activePage}
|
||||
pathname={location.pathname}
|
||||
onNavigate={(sub) => guardedNavigate(item.page, sub)}
|
||||
/>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
|
||||
Reference in New Issue
Block a user