feat: 菜单管理(侧边栏覆盖层)
管理员可对一/二/三级菜单(含注入的 iframe 轻应用节点)改名、停用/启用、 排序、跨层级移动。静态菜单仍是图标/路由来源,后端只存每节点覆盖项。 后端: /api/menu-overrides(全局共享;开放读、admin 原子替换;≤3 级校验), 新增 persistence/menu_overrides 持久化层(create_all 自动建表)。 前端: 合并逻辑 menu-overrides.ts、MenuManagementPage(@dnd-kit 拖拽+移动到 父级选择器+上下箭头+改名/停用)、左下角设置入口、Header/侧边栏自动生效。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8d0759123a
commit
1c8b5b23a1
19 files changed
+1480
-8
No files matched your search
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 菜单覆盖层(菜单管理)API 客户端 + React Query hooks。
|
||||
*
|
||||
* 后端:`/api/menu-overrides`(见 app/gateway/routers/menu_overrides.py)。
|
||||
* 全局共享、无用户隔离;读对所有登录用户开放(侧边栏要用),写仅 admin(整体替换)。
|
||||
* snake_case ⇄ camelCase 在此边界收敛。
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import type { MenuOverride } from "@/core/page-layout/menu-overrides";
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/menu-overrides`;
|
||||
|
||||
interface ServerOverride {
|
||||
node_id: string;
|
||||
parent_id: string | null;
|
||||
sort_order: number;
|
||||
label: string | null;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
function toOverride(s: ServerOverride): MenuOverride {
|
||||
return {
|
||||
nodeId: s.node_id,
|
||||
parentId: s.parent_id ?? null,
|
||||
sortOrder: s.sort_order ?? 0,
|
||||
label: s.label ?? null,
|
||||
disabled: s.disabled ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function toBody(o: MenuOverride): ServerOverride {
|
||||
return {
|
||||
node_id: o.nodeId,
|
||||
parent_id: o.parentId ?? null,
|
||||
sort_order: o.sortOrder,
|
||||
label: o.label ?? null,
|
||||
disabled: o.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
async function readJson<T>(res: Response, ctx: string): Promise<T> {
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null as unknown);
|
||||
const detail =
|
||||
data && typeof data === "object" && "detail" in data
|
||||
? String((data as { detail: unknown }).detail)
|
||||
: `${ctx} failed (${res.status})`;
|
||||
throw new Error(detail);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export async function listMenuOverrides(): Promise<MenuOverride[]> {
|
||||
const res = await apiFetch(BASE(), { method: "GET" });
|
||||
const data = await readJson<{ overrides: ServerOverride[] }>(res, "list menu overrides");
|
||||
return (data.overrides ?? []).map(toOverride);
|
||||
}
|
||||
|
||||
export async function replaceMenuOverrides(overrides: MenuOverride[]): Promise<MenuOverride[]> {
|
||||
const res = await apiFetch(BASE(), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ overrides: overrides.map(toBody) }),
|
||||
});
|
||||
const data = await readJson<{ overrides: ServerOverride[] }>(res, "save menu overrides");
|
||||
return (data.overrides ?? []).map(toOverride);
|
||||
}
|
||||
|
||||
// ── React Query hooks(侧边栏 + 管理页共用缓存) ────────────────────────────
|
||||
|
||||
const MENU_OVERRIDES_KEY = "menu-overrides";
|
||||
|
||||
export function useMenuOverrides() {
|
||||
return useQuery({
|
||||
queryKey: [MENU_OVERRIDES_KEY],
|
||||
queryFn: listMenuOverrides,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMenuOverrideActions() {
|
||||
const queryClient = useQueryClient();
|
||||
return {
|
||||
save: useMutation({
|
||||
mutationFn: replaceMenuOverrides,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: [MENU_OVERRIDES_KEY] }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user