From 4bfd9573dba79e4529c7f2ef9e863181ccf708a6 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 11 Apr 2026 14:34:48 +0800 Subject: [PATCH] fix(config): add individual menu CRUD endpoints and fix frontend menu data handling - Add POST /config/menus (create single menu) - Add PUT /config/menus/{id} (update single menu) - Add DELETE /config/menus/{id} (soft delete single menu) - Frontend MenuConfig was calling individual CRUD routes that didn't exist, causing 404 errors on all create/update/delete operations - Fix frontend to correctly handle nested tree response from backend --- apps/web/src/pages/settings/MenuConfig.tsx | 21 ++++- crates/erp-config/src/handler/menu_handler.rs | 76 +++++++++++++++++-- crates/erp-config/src/module.rs | 9 ++- 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/apps/web/src/pages/settings/MenuConfig.tsx b/apps/web/src/pages/settings/MenuConfig.tsx index 15fcafa..99ca46b 100644 --- a/apps/web/src/pages/settings/MenuConfig.tsx +++ b/apps/web/src/pages/settings/MenuConfig.tsx @@ -35,6 +35,20 @@ interface MenuItem { // --- Helpers --- +/** Convert nested menu tree back to flat list */ +function flattenMenuTree(tree: MenuItem[]): MenuItem[] { + const result: MenuItem[] = []; + const walk = (items: MenuItem[]) => { + for (const item of items) { + const { children, ...rest } = item; + result.push(rest as MenuItem); + if (children?.length) walk(children); + } + }; + walk(tree); + return result; +} + /** Convert flat menu list to tree structure for Table children prop */ function buildMenuTree(items: MenuItem[]): MenuItem[] { const map = new Map(); @@ -92,9 +106,10 @@ export default function MenuConfig() { setLoading(true); try { const { data: resp } = await client.get('/config/menus'); - const list: MenuItem[] = resp.data ?? resp; - setMenus(list); - setMenuTree(buildMenuTree(list)); + // 后端返回嵌套树结构,直接使用 + const tree: MenuItem[] = resp.data ?? resp; + setMenus(flattenMenuTree(tree)); + setMenuTree(tree); } catch { message.error('加载菜单失败'); } diff --git a/crates/erp-config/src/handler/menu_handler.rs b/crates/erp-config/src/handler/menu_handler.rs index 62eb984..9048799 100644 --- a/crates/erp-config/src/handler/menu_handler.rs +++ b/crates/erp-config/src/handler/menu_handler.rs @@ -1,5 +1,5 @@ use axum::Extension; -use axum::extract::{FromRef, Json, State}; +use axum::extract::{FromRef, Json, Path, State}; use axum::response::Json as JsonResponse; use validator::Validate; @@ -12,11 +12,9 @@ use crate::config_state::ConfigState; use crate::dto::{BatchSaveMenusReq, CreateMenuReq, MenuResp}; use crate::service::menu_service::MenuService; -/// GET /api/v1/menus +/// GET /api/v1/config/menus /// /// 获取当前租户下当前用户角色可见的菜单树。 -/// 根据用户关联的角色过滤菜单可见性。 -/// 需要 `menu.list` 权限。 pub async fn get_menus( State(state): State, Extension(ctx): Extension, @@ -38,11 +36,75 @@ where Ok(JsonResponse(ApiResponse::ok(menus))) } -/// PUT /api/v1/menus/batch +/// POST /api/v1/config/menus +/// +/// 创建单个菜单项。 +pub async fn create_menu( + State(state): State, + Extension(ctx): Extension, + Json(req): Json, +) -> Result>, AppError> +where + ConfigState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "menu.update")?; + + req.validate() + .map_err(|e| AppError::Validation(e.to_string()))?; + + let resp = MenuService::create( + ctx.tenant_id, + ctx.user_id, + &req, + &state.db, + &state.event_bus, + ) + .await?; + + Ok(JsonResponse(ApiResponse::ok(resp))) +} + +/// PUT /api/v1/config/menus/{id} +/// +/// 更新单个菜单项。 +pub async fn update_menu( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, + Json(req): Json, +) -> Result>, AppError> +where + ConfigState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "menu.update")?; + + let resp = MenuService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; + Ok(JsonResponse(ApiResponse::ok(resp))) +} + +/// DELETE /api/v1/config/menus/{id} +/// +/// 软删除单个菜单项。 +pub async fn delete_menu( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, AppError> +where + ConfigState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "menu.update")?; + + MenuService::delete(id, ctx.tenant_id, ctx.user_id, &state.db, &state.event_bus).await?; + Ok(JsonResponse(ApiResponse::ok(()))) +} + +/// PUT /api/v1/config/menus/batch /// /// 批量保存菜单列表。 -/// 对每个菜单项:有 id 的执行更新,没有 id 的执行创建。 -/// 需要 `menu.update` 权限。 pub async fn batch_save_menus( State(state): State, Extension(ctx): Extension, diff --git a/crates/erp-config/src/module.rs b/crates/erp-config/src/module.rs index 6c3e8b3..65d16ea 100644 --- a/crates/erp-config/src/module.rs +++ b/crates/erp-config/src/module.rs @@ -56,7 +56,14 @@ impl ConfigModule { // Menu routes .route( "/config/menus", - get(menu_handler::get_menus).put(menu_handler::batch_save_menus), + get(menu_handler::get_menus) + .post(menu_handler::create_menu) + .put(menu_handler::batch_save_menus), + ) + .route( + "/config/menus/{id}", + put(menu_handler::update_menu) + .delete(menu_handler::delete_menu), ) // Setting routes .route(