From cdc0b4b0544ae3443d527bdd128b3433eca2d24a Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Thu, 18 Jun 2026 00:04:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:[FL-165][=E8=A7=92=E8=89=B2=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2=E6=9F=A5=E8=AF=A2=E6=97=B6=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E8=A7=92=E8=89=B2=E5=92=8C=E8=8F=9C=E5=8D=95=E8=AF=B7?= =?UTF-8?q?=E6=B1=82]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将角色管理页面的角色和菜单两个独立API请求合并为单个请求,减少网络开销。 后端改动: - 新增 RolesWithMenusResponse 响应模型 - 新增 list_roles_with_menus 服务函数 - 新增 GET /api/v1/admin/roles-with-menus 接口 前端改动: - 更新 loadData 函数使用新的合并接口 - 减少从两个并发请求改为单个请求 Co-Authored-By: Claude Sonnet 4.6 Co-authored-by: multica-agent --- api/app/api/v1/admin.py | 11 +++++++ api/app/schemas/admin.py | 7 ++++ api/app/services/legacy_admin_rbac_service.py | 13 ++++++++ web/src/app/admin/roles/page.tsx | 32 +++++++++---------- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/api/app/api/v1/admin.py b/api/app/api/v1/admin.py index 508b582..9a73f30 100644 --- a/api/app/api/v1/admin.py +++ b/api/app/api/v1/admin.py @@ -14,6 +14,7 @@ from ...schemas.admin import ( RoleListResponse, RoleMenuUpdateRequest, RolePublic, + RolesWithMenusResponse, SeedDefaultsResponse, RoleUpdateRequest, ) @@ -32,6 +33,7 @@ from ...services.legacy_admin_rbac_service import ( list_permissions, list_role_menu_ids, list_roles, + list_roles_with_menus, replace_role_menus, update_menu, update_role, @@ -50,6 +52,15 @@ def get_roles( return list_roles(db, keyword=keyword) +@router.get("/roles-with-menus", response_model=RolesWithMenusResponse) +def get_roles_with_menus( + keyword: str | None = Query(default=None), + _: CurrentUser = Depends(require_any_permission("role.read", "role.manage")), + db: Session = Depends(get_db), +) -> RolesWithMenusResponse: + return list_roles_with_menus(db, keyword=keyword) + + @router.post("/roles", response_model=RolePublic) def create_role_endpoint( payload: RoleCreateRequest, diff --git a/api/app/schemas/admin.py b/api/app/schemas/admin.py index 6cd4cbf..fc97b1b 100644 --- a/api/app/schemas/admin.py +++ b/api/app/schemas/admin.py @@ -128,4 +128,11 @@ class SeedDefaultsResponse(BaseModel): summary: dict[str, SeedCategorySummary] = Field(default_factory=dict) +class RolesWithMenusResponse(BaseModel): + roles: list[RolePublic] + roles_total: int + menus: list[MenuPublic] + menus_total: int + + MenuTreeItem.model_rebuild() diff --git a/api/app/services/legacy_admin_rbac_service.py b/api/app/services/legacy_admin_rbac_service.py index f854203..85f8909 100644 --- a/api/app/services/legacy_admin_rbac_service.py +++ b/api/app/services/legacy_admin_rbac_service.py @@ -17,6 +17,7 @@ from ..schemas.admin import ( RoleCreateRequest, RoleListResponse, RolePublic, + RolesWithMenusResponse, RoleUpdateRequest, ) from .audit_service import compose_audit_detail, describe_changed_fields, summarize_values, write_audit_log @@ -462,6 +463,18 @@ def list_menus(db: Session, keyword: str | None = None, status: str | None = Non return MenuListResponse(items=items, total=len(items)) +def list_roles_with_menus(db: Session, keyword: str | None = None) -> RolesWithMenusResponse: + """Get roles and menus in a single request to reduce network calls.""" + roles_response = list_roles(db, keyword=keyword) + menus_response = list_menus(db) + return RolesWithMenusResponse( + roles=roles_response.items, + roles_total=roles_response.total, + menus=menus_response.items, + menus_total=menus_response.total, + ) + + def get_menu_by_id(db: Session, menu_id: str) -> MenuPublic | None: normalized_menu_id = menu_id.strip() if not normalized_menu_id: diff --git a/web/src/app/admin/roles/page.tsx b/web/src/app/admin/roles/page.tsx index cc9b47f..32f7543 100644 --- a/web/src/app/admin/roles/page.tsx +++ b/web/src/app/admin/roles/page.tsx @@ -32,6 +32,13 @@ const AntCard = Card as unknown as ComponentType; type MenuListResponse = { items: MenuItem[]; total: number }; +type RolesWithMenusResponse = { + roles: RoleItem[]; + roles_total: number; + menus: MenuItem[]; + menus_total: number; +}; + type RoleFormValues = { code: string; name: string; @@ -90,27 +97,20 @@ export default function AdminRolesPage() { setError(""); try { const keyword = searchKeyword.trim(); - const roleUrl = keyword - ? `/api/v1/admin/roles?keyword=${encodeURIComponent(keyword)}` - : "/api/v1/admin/roles"; + const url = keyword + ? `/api/v1/admin/roles-with-menus?keyword=${encodeURIComponent(keyword)}` + : "/api/v1/admin/roles-with-menus"; - const [roleRes, menuRes] = await Promise.all([ - fetchWithAuth(roleUrl), - fetchWithAuth("/api/v1/admin/menus"), - ]); + const response = await fetchWithAuth(url); - if (!roleRes.ok) { - throw new Error(await readApiError(roleRes)); - } - if (!menuRes.ok) { - throw new Error(await readApiError(menuRes)); + if (!response.ok) { + throw new Error(await readApiError(response)); } - const rolePayload = (await roleRes.json()) as RoleListResponse; - const menuPayload = (await menuRes.json()) as MenuListResponse; + const payload = (await response.json()) as RolesWithMenusResponse; - setRoles(rolePayload.items); - setMenus(menuPayload.items); + setRoles(payload.roles); + setMenus(payload.menus); } catch (candidate) { setError(candidate instanceof Error ? candidate.message : "角色数据加载失败"); } finally {