feat:[FL-165][角色管理页面查询时合并角色和菜单请求]

将角色管理页面的角色和菜单两个独立API请求合并为单个请求,减少网络开销。

后端改动:
- 新增 RolesWithMenusResponse 响应模型
- 新增 list_roles_with_menus 服务函数
- 新增 GET /api/v1/admin/roles-with-menus 接口

前端改动:
- 更新 loadData 函数使用新的合并接口
- 减少从两个并发请求改为单个请求

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-18 00:04:55 +08:00
parent 22f3bdd438
commit cdc0b4b054
4 changed files with 47 additions and 16 deletions
+11
View File
@@ -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,
+7
View File
@@ -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()
@@ -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:
+16 -16
View File
@@ -32,6 +32,13 @@ const AntCard = Card as unknown as ComponentType<CardProps>;
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 {