refactor: permission

This commit is contained in:
tao.chen
2026-09-02 10:10:41 +08:00
committed by tao.chen
parent 1c220490cf
commit dea6b4cfcf
11 changed files with 171 additions and 1037 deletions
+6
View File
@@ -20,6 +20,7 @@ export type AuthUser = {
status: string;
role_code: string | null;
is_system_admin: boolean;
permissions: string[];
};
export type AuthWorkspace = {
@@ -312,3 +313,8 @@ export function useApi(): WorkspaceBoundApi {
deleteWorkspaceMember: (workspaceId, userId) => rawApi.deleteWorkspaceMember(workspaceId, userId),
}), [workspaceId]);
}
export function usePermission(code: string): boolean {
const { user } = useAuth();
return user?.permissions.includes(code) ?? false;
}
@@ -1,5 +1,5 @@
import Icon from "../../components/common/Icon";
import { useAuth } from "../../context/AuthContext";
import { useAuth, usePermission } from "../../context/AuthContext";
const TREND_VALUES = [38, 55, 44, 73, 61, 86, 78] as const;
const TREND_LABELS = ["周一", "周二", "周三", "周四", "周五", "周六", "今天"] as const;
@@ -25,7 +25,7 @@ export function DashboardPage({
onNavigate: (page: "scripts" | "schedules" | "system") => void;
}) {
const { user, currentWorkspace } = useAuth();
const isSystemAdmin = user?.is_system_admin ?? false;
const hasSystemView = usePermission("system:view");
const notebookCount = Math.max(1, Math.round(scriptCount * 0.67));
const pythonCount = Math.max(0, scriptCount - Math.round(scriptCount * 0.67));
@@ -92,7 +92,7 @@ export function DashboardPage({
<Icon name="schedule" />
</button>
{isSystemAdmin && (
{hasSystemView && (
<button
type="button"
className="flex cursor-pointer items-center gap-2 rounded-[7px] border border-[#d8e4ef] bg-white px-[18px] py-[13px] text-[#346587]"
@@ -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" },
],
},
];
+91 -79
View File
@@ -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>