From dd1576862317d5d5be84568de0c2e624d4a4ebfc Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Wed, 17 Jun 2026 23:23:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:[FL-160][=E8=A7=92=E8=89=B2=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2=E8=A1=A8=E5=8D=95=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E6=94=B9=E6=88=90=E5=90=8E=E5=8F=B0=E6=9F=A5=E8=AF=A2]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端API新增keyword查询参数支持角色搜索 - 在数据库层面实现基础过滤优化性能 - 前端移除客户端过滤逻辑改为调用后端API - 添加搜索按钮和回车搜索功能 Co-Authored-By: Claude Sonnet 4.6 Co-authored-by: multica-agent --- api/app/api/v1/admin.py | 3 +- api/app/services/legacy_admin_rbac_service.py | 43 ++++++++++++++++--- web/src/app/admin/roles/page.tsx | 41 +++++++++--------- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/api/app/api/v1/admin.py b/api/app/api/v1/admin.py index 55ce7d1..48bc7c6 100644 --- a/api/app/api/v1/admin.py +++ b/api/app/api/v1/admin.py @@ -43,10 +43,11 @@ router = APIRouter(prefix="/admin", tags=["admin"]) @router.get("/roles", response_model=RoleListResponse) def get_roles( + keyword: str | None = Query(default=None), _: CurrentUser = Depends(require_any_permission("role.read", "role.manage")), db: Session = Depends(get_db), ) -> RoleListResponse: - return list_roles(db) + return list_roles(db, keyword=keyword) @router.post("/roles", response_model=RolePublic) diff --git a/api/app/services/legacy_admin_rbac_service.py b/api/app/services/legacy_admin_rbac_service.py index 77ff0b3..f854203 100644 --- a/api/app/services/legacy_admin_rbac_service.py +++ b/api/app/services/legacy_admin_rbac_service.py @@ -101,9 +101,9 @@ PROTECTED_MENU_CODES = { } -def list_roles(db: Session) -> RoleListResponse: +def list_roles(db: Session, keyword: str | None = None) -> RoleListResponse: role_source = "legacy" if _legacy_role_table_exists(db) else "modern" - rows = _load_role_rows(db, role_source=role_source) + rows = _load_role_rows(db, role_source=role_source, keyword=keyword) role_ids = [str(row["id"]) for row in rows] role_menu_ids = _load_role_menu_ids_map(db, role_ids, role_source=role_source) menu_rows = _load_menus_map( @@ -120,6 +120,21 @@ def list_roles(db: Session) -> RoleListResponse: menu_ids = sorted(menu_id for menu_id in role_menu_ids.get(role_id, []) if menu_id in menu_rows) permission_codes = set(role_permission_codes.get(role_id, [])) permission_codes.update(_permission_codes_from_menu_rows(menu_rows, menu_ids)) + + # Apply keyword filter on menu names if keyword provided + if keyword: + normalized_keyword = keyword.strip().lower() + # Collect menu names for this role + menu_names = " ".join( + str(menu_rows.get(menu_id, {}).get("menu_name", "")) + for menu_id in menu_ids + ) + # Build search haystack + haystack = f"{role_code} {row.get('name', '')} {menu_names}".lower() + # Skip if keyword not found + if normalized_keyword not in haystack: + continue + items.append( RolePublic( id=role_id, @@ -923,26 +938,40 @@ def _load_menus_map(db: Session, menu_ids: list[str], *, role_source: str = "leg } -def _load_role_rows(db: Session, *, role_source: str) -> list[dict[str, object]]: +def _load_role_rows(db: Session, *, role_source: str, keyword: str | None = None) -> list[dict[str, object]]: + where_clause = "" + params = {} + + if keyword: + normalized_keyword = f"%{keyword.strip().lower()}%" + where_clause = "WHERE LOWER(id) LIKE :keyword OR LOWER(name) LIKE :keyword" + params["keyword"] = normalized_keyword + if role_source == "legacy": rows = db.execute( text( - """ + f""" SELECT id, name FROM user_role + {where_clause} ORDER BY create_date DESC NULLS LAST, id ASC """ - ) + ), + params ).mappings().all() else: + if keyword: + where_clause = "WHERE LOWER(code) LIKE :keyword OR LOWER(name) LIKE :keyword" rows = db.execute( text( - """ + f""" SELECT id::text AS id, code, name FROM roles + {where_clause} ORDER BY id ASC """ - ) + ), + params ).mappings().all() return [dict(row) for row in rows] diff --git a/web/src/app/admin/roles/page.tsx b/web/src/app/admin/roles/page.tsx index 3d4c4a8..8c95239 100644 --- a/web/src/app/admin/roles/page.tsx +++ b/web/src/app/admin/roles/page.tsx @@ -80,21 +80,6 @@ export default function AdminRolesPage() { return new Map(menus.map((menu) => [menu.id, `${menu.name} (${menu.code})`])); }, [menus]); - const filteredRoles = useMemo(() => { - const keyword = searchKeyword.trim().toLowerCase(); - if (!keyword) { - return roles; - } - - return roles.filter((role) => { - const menuNames = role.menu_ids - .map((menuId) => menuNameById.get(menuId) ?? String(menuId)) - .join(" "); - const haystack = [role.code, role.name, menuNames].join(" ").toLowerCase(); - return haystack.includes(keyword); - }); - }, [menuNameById, roles, searchKeyword]); - const loadData = useCallback(async () => { if (!canRead) { setLoading(false); @@ -104,8 +89,13 @@ export default function AdminRolesPage() { setLoading(true); setError(""); try { + const keyword = searchKeyword.trim(); + const roleUrl = keyword + ? `/api/v1/admin/roles?keyword=${encodeURIComponent(keyword)}` + : "/api/v1/admin/roles"; + const [roleRes, menuRes] = await Promise.all([ - fetchWithAuth("/api/v1/admin/roles"), + fetchWithAuth(roleUrl), fetchWithAuth("/api/v1/admin/menus"), ]); @@ -126,7 +116,7 @@ export default function AdminRolesPage() { } finally { setLoading(false); } - }, [canRead, fetchWithAuth]); + }, [canRead, fetchWithAuth, searchKeyword]); useEffect(() => { if (!user || !canRead) { @@ -347,7 +337,7 @@ export default function AdminRolesPage() { useEffect(() => { updateTableScrollY(); - }, [error, filteredRoles.length, loading, updateTableScrollY]); + }, [error, roles.length, loading, updateTableScrollY]); useEffect(() => { if (typeof window === "undefined") { @@ -440,10 +430,21 @@ export default function AdminRolesPage() { placeholder="搜索角色编码、名称或菜单" value={searchKeyword} onChange={(event) => setSearchKeyword(event.currentTarget.value)} + onPressEnter={() => void loadData()} /> - + + + +
rowKey="id" columns={columns} - dataSource={filteredRoles} + dataSource={roles} loading={loading} scroll={{ x: 1400, y: tableScrollY }} pagination={{