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:
董宏凯andClaude Opus 4.8 committed 2026-06-16 23:34:12 +08:00
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] }),
}),
};
}