Skip to content

Conversation

weibaohui
Copy link
Owner

新增菜单类型定义和接口,实现菜单数据的存储与获取
添加菜单编辑器页面,支持可视化编辑和JSON配置
实现基于用户角色和集群状态的菜单动态展示逻辑
扩展用户组管理,支持为不同用户组配置自定义菜单
添加菜单可见性控制,支持表达式条件判断

新增菜单类型定义和接口,实现菜单数据的存储与获取
添加菜单编辑器页面,支持可视化编辑和JSON配置
实现基于用户角色和集群状态的菜单动态展示逻辑
扩展用户组管理,支持为不同用户组配置自定义菜单
添加菜单可见性控制,支持表达式条件判断
Copy link
Contributor

coderabbitai bot commented Aug 16, 2025

Warning

Rate limit exceeded

@weibaohui has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 24 minutes and 35 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 8cbf1e3 and 5145baf.

⛔ Files ignored due to path filters (5)
  • ui/.DS_Store is excluded by !**/.DS_Store
  • ui/public/pages/.DS_Store is excluded by !**/.DS_Store
  • ui/src/.DS_Store is excluded by !**/.DS_Store
  • ui/src/components/.DS_Store is excluded by !**/.DS_Store
  • ui/src/pages/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (2)
  • ui/src/components/Sidebar/menu.tsx (0 hunks)
  • ui/src/pages/MenuEditor/menuData.ts (1 hunks)
📝 Walkthrough

Summary by CodeRabbit

  • 新功能
    • 新增后台菜单管理:支持列表、保存、历史查看与删除。
    • 支持为用户组配置专属菜单,保存后自动生效并缓存优化。
    • 新增菜单编辑器:拖拽编辑、预览、导入/导出、历史版本管理与回滚。
    • 新增图标选择器,便捷挑选图标。
  • 界面
    • 用户组页面新增“设置菜单”对话框与“菜单状态”列。
    • 侧边栏基于用户角色、CRD 支持与自定义菜单动态展示与排序。
  • 重构
    • 侧边栏从静态配置迁移为数据驱动,支持自定义规则与可见性表达式。

Walkthrough

在后端新增 Menu 模型与 Admin 菜单控制器并注册路由;扩展 UserGroup(新增 MenuData)及用户组保存菜单接口;用户角色接口返回 menu_data。前端新增菜单编辑器页面、图标选择器、菜单预览与历史,Sidebar 改为基于 menu_data + CRD 能力动态渲染并新增相关 Hooks 与可见性解析。

Changes

Cohort / File(s) Change Summary
后端:Admin 菜单与路由
main.go, pkg/controller/admin/menu/menu.go, pkg/models/menu.go
注册 admin 菜单路由;新增 AdminMenuController(List/History/Save/Delete/DeleteHistory);新增 Menu 模型及 JSON 编解码与 DAO 方法(List/Save/Delete/GetOne/GetLatest/DeleteByID)。
后端:用户组与用户服务
pkg/controller/admin/user/user_group.go, pkg/models/user_group.go, pkg/service/user.go
为用户组增加 MenuData 字段;新增保存用户组菜单接口 SaveUserGroupMenu(更新 MenuData 并清理缓存);新增服务方法 GetGroupMenuData(groupNames)(缓存读取组菜单数据)。
后端:用户参数
pkg/controller/param/user.go
扩展 UserRole 响应:读取当前登录用户组名并返回 menu_data 字段(通过服务获取并在错误时返回 JSON 错误)。
前端:Sidebar 与可见性
ui/src/components/Sidebar/index.tsx, ui/src/utils/menuVisibility.ts, ui/src/types/menu.ts, ui/src/components/Sidebar/menu.tsx (删除)
删除旧的静态 menu 构建;Sidebar 改为从 menu_datainitialMenu 解析生成 Antd Menu;新增 MenuItem 类型与 shouldShowMenuItem 表达式评估逻辑(expr-eval);引入基于上下文的可见性过滤与导航处理。
前端:用户角色与 CRD 状态 Hook
ui/src/hooks/useUserRole.ts, ui/src/hooks/useCRDStatus.ts
新增 useUserRole(获取 /params/user/role,提供 userRole 与 menuData)和 useCRDStatus(获取 /k8s/crd/status,提供三项 CRD 支持标志)。
前端:菜单编辑器及相关组件
ui/src/pages/MenuEditor/*, ui/src/routes/index.tsx, ui/src/pages/MenuEditor/menuData.ts, ui/src/pages/MenuEditor/CustomEventTags.tsx, ui/src/pages/MenuEditor/Preview.tsx
新增完整 MenuEditor 页面:树编辑、拖拽、导入/导出、历史、预览、保存 API 集成;新增初始菜单数据与自定义事件标签组件与预览组件;路由新增 /MenuEditor
前端:图标选择器与图标列表
ui/src/components/IconPicker/index.tsx, ui/src/components/IconPicker/index.module.scss, ui/src/utils/iconOptions.ts
新增 IconPicker 组件及样式;新增大量 iconOptions 用于选择。
前端:用户组 UI 更新
ui/public/pages/admin/user/user_group.json
在用户组管理页添加信息提示,新增“设置菜单”行操作(弹窗编辑 menu_data)和菜单状态列映射,表单提交调用 /admin/user_group/save_menu

Sequence Diagram(s)

sequenceDiagram
  participant AdminUI as Admin UI
  participant Router as Gin Router
  participant AMC as AdminMenuController
  participant UGCtrl as AdminUserGroupController
  participant MenuModel as Menu model
  participant UGModel as UserGroup model
  participant UserSvc as UserService
  participant DB as Database
  participant Cache as Cache

  AdminUI->>Router: POST /admin/menu/save
  Router->>AMC: Save
  AMC->>MenuModel: Save(params)
  MenuModel->>DB: INSERT/UPDATE
  DB-->>MenuModel: OK
  MenuModel-->>AMC: OK
  AMC-->>AdminUI: JSON OK

  AdminUI->>Router: GET /admin/menu/history
  Router->>AMC: History
  AMC->>MenuModel: List(params order by created_at DESC)
  MenuModel->>DB: SELECT
  DB-->>MenuModel: Rows
  MenuModel-->>AMC: Data
  AMC-->>AdminUI: JSON Data

  AdminUI->>Router: POST /admin/user_group/save_menu
  Router->>UGCtrl: SaveUserGroupMenu
  UGCtrl->>UGModel: GetOne(id)
  UGModel->>DB: SELECT
  DB-->>UGModel: Row
  UGCtrl->>UGModel: Update MenuData
  UGModel->>DB: UPDATE
  DB-->>UGModel: OK
  UGCtrl->>UserSvc: Clear group cache
  UserSvc->>Cache: Invalidate
  Cache-->>UserSvc: OK
  UGCtrl-->>AdminUI: JSON OK
Loading
sequenceDiagram
  participant Sidebar as Sidebar UI
  participant HookRole as useUserRole
  participant HookCRD as useCRDStatus
  participant API as Backend
  participant Vis as shouldShowMenuItem
  participant Router as react-router

  Sidebar->>HookRole: fetch /params/user/role
  HookRole->>API: GET /params/user/role
  API-->>HookRole: { role, cluster, menu_data }

  Sidebar->>HookCRD: fetch /k8s/crd/status
  HookCRD->>API: GET /k8s/crd/status
  API-->>HookCRD: { IsGatewayAPISupported, IsOpenKruiseSupported, IsIstioSupported }

  Sidebar->>Vis: shouldShowMenuItem(item, context)
  Vis-->>Sidebar: boolean
  Sidebar->>Router: navigate(path) on item click
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • feat(ui): 添加工作负载分布菜单项和页面配置 #212 — 修改/扩展 Sidebar 菜单及 OpenKruise 条目,可能与本次将菜单数据外置并重构 Sidebar 的改动产生冲突或重叠。
  • OpenKruise支持 #213 — 另一个对 Sidebar 菜单内容与 OpenKruise 支持的改动,与本次将静态菜单替换为 data-driven 初始菜单密切相关。
  • add swagger api #265 — 修改 pkg/controller/param/user.go::UserRole 相关逻辑;本 PR 更改该接口以返回 menu_data,二者在接口契约上直接相关。

Poem

我是小兔写配置,菜单枝头任意移。
拖拽一跳树成行,图标点亮像星驰。
存历史,开预览,不怕回滚与纷争。
鼓掌保存又一刻,侧栏随心换新姿。 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch menu-editor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@weibaohui weibaohui merged commit 2afb718 into main Aug 16, 2025
5 checks passed
@weibaohui weibaohui deleted the menu-editor branch August 16, 2025 08:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (35)
pkg/models/user_group.go (3)

16-16: MenuData JSON 序列化策略与现有字段不一致,建议补充 omitempty 减少冗余输出

同一结构体内其他字段普遍使用了 json:",omitempty",而 menu_data 未加该选项。空字符串会始终下发到前端,占用带宽且易引起语义歧义。

可以考虑如下调整:

-    MenuData    string    `gorm:"type:text" json:"menu_data"`
+    MenuData    string    `gorm:"type:text" json:"menu_data,omitempty"`

16-16: 持久化类型为 text 可能不够稳妥,建议评估 datatypes.JSON 或更大容量类型

  • text 在 MySQL/MariaDB 上最大约 64KB;若菜单配置较大(历史版本、注释、图标等堆叠),存在溢出风险。
  • 为兼容多数据库方言与更清晰的语义,可考虑使用 gorm.io/datatypes.JSON;或至少在 MySQL 上改为 longtext

可选方案 A(推荐,跨方言更健壮):

- MenuData    string    `gorm:"type:text" json:"menu_data,omitempty"`
+ // 如需跨方言可靠的 JSON 存储与查询能力,考虑直接用 JSON 类型
+ MenuData    datatypes.JSON `json:"menu_data,omitempty"`

可选方案 B(仅 MySQL/MariaDB 提升容量):

- MenuData    string    `gorm:"type:text" json:"menu_data,omitempty"`
+ MenuData    string    `gorm:"type:longtext" json:"menu_data,omitempty"`

16-16: 接口层建议对 MenuData 做 JSON 校验与限流/限长,避免无效或过大入库

该字段为前端提交的 JSON 字符串,建议在 controller/service 层:

  • 校验 JSON 结构与大小(例如限制 256KB/1MB,按需设定)。
  • 拒绝无效 JSON,避免脏数据。
  • 对历史版本(若有)做留存数量控制,防止无限增长。

如需,我可以补一段保存前的校验逻辑示例。

ui/src/types/menu.ts (3)

6-8: 用类型区分不同 eventType 的必填/互斥字段,提升类型安全

目前 urlcustomEvent 均为可选,运行期才会发现缺失。建议使用可判别联合,静态约束字段的存在性与互斥关系。

参考改造示例(如不影响现有调用):

-export interface MenuItem {
-    key: string;
-    title: string;
-    icon?: string;
-    url?: string;
-    eventType?: 'url' | 'custom';
-    customEvent?: string;
-    order?: number;
-    children?: MenuItem[];
-    show?: string; // 修改这里,只保留字符串类型
-}
+type BaseMenuItem = {
+  key: string;
+  title: string;
+  icon?: string;
+  order?: number;
+  children?: MenuItem[];
+  show?: string;
+}
+
+type UrlMenuItem = BaseMenuItem & {
+  eventType: 'url';
+  url: string;
+  customEvent?: never;
+}
+
+type CustomEventMenuItem = BaseMenuItem & {
+  eventType: 'custom';
+  customEvent: string;
+  url?: never;
+}
+
+type GroupMenuItem = BaseMenuItem & {
+  eventType?: undefined; // 纯分组节点
+  url?: never;
+  customEvent?: never;
+}
+
+export type MenuItem = UrlMenuItem | CustomEventMenuItem | GroupMenuItem;

10-10: 为 show 表达式定义类型别名并补充注释,约定语法,降低使用歧义

当前 show?: string 缺少语法约束(如可用变量/函数、返回值语义)。建议引入别名并在注释中描述语法约定,便于全局复用和 IDE 提示。

-    show?: string; // 修改这里,只保留字符串类型
+    // show 表达式,返回 truthy 即显示。建议:限定可用变量(如 userRole、clusterStatus 等)与函数。
+    show?: VisibilityExpr;

文件头部新增:

export type VisibilityExpr = string; // 统一的可见性表达式类型别名

2-2: 命名建议:如该结构常在 JSX 中透传,考虑用 id 替代 key

当对象被直接展开到组件 props 时,key 是 React 保留属性,不会出现在组件 props 内,易引起困惑。若 MenuItem 会被直接作为 props 传递,建议改为 idmenuKey

ui/src/components/IconPicker/index.module.scss (2)

18-40: .selected 写法在 CSS Modules 下是“全局类”,建议改为导出类,避免样式泄漏

&.selected 生成的 selected 不会哈希,需在组件里加原生类名,且可能与全局样式冲突。建议导出一个选择态类并在组件中组合使用。

可修改为:

 .icon-item {
   display: flex;
   align-items: center;
   justify-content: center;
   width: 48px;
   height: 48px;
   border: 1px solid #eee;
   border-radius: 4px;
   cursor: pointer;
   transition: all 0.2s;
   font-size: 24px;

   &:hover {
     background-color: #f0f7ff;
     border-color: #1890ff;
   }

-  &.selected {
-    background-color: #e6f7ff;
-    border-color: #1890ff;
-    color: #1890ff;
-  }
 }
+
+/* 导出的“选中态”类,在 TSX 中组合:className={`${styles['icon-item']} ${selected ? styles.isSelected : ''}`} */ 
+.isSelected {
+  background-color: #e6f7ff;
+  border-color: #1890ff;
+  color: #1890ff;
+}

同时把组件里的类名从字符串 'selected' 切换为 styles.isSelected


8-15: 提升栅格适配性:使用 auto-fill/minmax 或固定列宽,避免 tile 与单元格尺寸错位

当前列宽为 1fr,而 .icon-item 固定 48px,导致单元格宽度和 tile 宽度不一致。两种可选方案:

方案 A(自适应列数,最小 48px):

-  grid-template-columns: repeat(8, 1fr);
+  grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));

方案 B(固定 8 列且 tile 撑满单元格):

-  grid-template-columns: repeat(8, 1fr);
+  grid-template-columns: repeat(8, 1fr);

并将 .icon-item 的宽高改为百分比:

-  width: 48px;
-  height: 48px;
+  width: 100%;
+  height: 100%;

二选一即可,取决于是否需要响应式列数。

ui/src/routes/index.tsx (2)

9-9: MenuEditor 建议按需加载,避免首屏包体增大

当前直接静态引入。与后文 lazyLoad 工具保持一致,可懒加载该页面,减小首屏体积。

-import MenuEditor from '@/pages/MenuEditor'
+const MenuEditor = lazy(() => import('@/pages/MenuEditor'));

25-25: 将路由元素改为 lazy 包装,统一懒加载写法

复用现有 lazyLoad 辅助函数。

-            <Route path='/MenuEditor' element={<MenuEditor/>}></Route>
+            <Route path='/MenuEditor' element={lazyLoad(MenuEditor)}></Route>
ui/src/utils/iconOptions.ts (2)

1-3: 为 iconOptions 增加显式类型与只读限定,提升可维护性与类型安全

当前是隐式 any[],建议加上类型声明并标记只读,避免后续被误改。

-// 所有Font Awesome实心图标选项
-const iconOptions = [
+// 所有Font Awesome实心图标选项
+export type IconOption = { value: string };
+const iconOptions: ReadonlyArray<IconOption> = [

1-1006: 发现重复的图标项:fa-wifi

  • 当前 ui/src/utils/iconOptions.ts 中存在重复项:fa-wifi
  • 建议:
    • 在构建或导出阶段对 iconOptions 做去重,移除重复值
    • 将超大静态列表抽离为独立 JSON 文件,并在 IconPicker 首次打开时按需加载,以降低首屏包体积
    • 或者在运行时对加载的数据做懒加载/分页,避免整个列表常驻主包

[optional_refactors_recommended]

pkg/service/user.go (2)

26-64: 返回 any 导致前后端类型不一致,建议改为 string 并用空串表示缺省

当前返回类型为 any,Controller 无法感知并最终把 null 下发到前端,导致 useUserRole 以 string 处理时潜在运行时问题。建议改为 string,并在未配置时返回空串。

-func (u *userService) GetGroupMenuData(groupNames string) (any, error) {
+func (u *userService) GetGroupMenuData(groupNames string) (string, error) {
 	if groupNames == "" {
-		return nil, nil
+		return "", nil
 	}
 
 	// 按逗号分割,取第一个组名
 	groupNameList := strings.Split(groupNames, ",")
 	firstGroupName := strings.TrimSpace(groupNameList[0])
 
 	if firstGroupName == "" {
-		return nil, nil
+		return "", nil
 	}
 
 	cacheKey := u.formatCacheKey("user:groupmenu:%s", firstGroupName)
 
-	result, err := utils.GetOrSetCache(CacheService().CacheInstance(), cacheKey, 5*time.Minute, func() (any, error) {
+	result, err := utils.GetOrSetCache(CacheService().CacheInstance(), cacheKey, 5*time.Minute, func() (string, error) {
 		params := &dao.Params{}
 		userGroup := &models.UserGroup{}
 		queryFunc := func(db *gorm.DB) *gorm.DB {
 			return db.Select("menu_data").Where("group_name = ?", firstGroupName)
 		}
 
 		item, err := userGroup.GetOne(params, queryFunc)
 		if err != nil {
 			if errors.Is(err, gorm.ErrRecordNotFound) {
-				return nil, nil
+				return "", nil
 			}
-			return nil, err
+			return "", err
 		}
 
 		if item.MenuData == "" {
-			return nil, nil
+			return "", nil
 		}
 
 		return item.MenuData, nil
 	})
 
 	return result, err
 }

如采纳上述改动,请同步更新调用处 Controller 的变量类型。


39-41: TTL 常量化建议

5 分钟 TTL 多处硬编码,建议抽到常量或配置,便于统一调整和可测试性。

pkg/controller/admin/user/user_group.go (1)

82-116: 仅更新 menu_data 字段,避免误更新其他字段

当前使用 Save 可能将未预期字段一并写回(取决于 Save 实现)。本处仅需更新 menu_data,建议使用 UpdateColumn 精确更新。

-    // 更新菜单数据
-    userGroup.MenuData = requestData.MenuData
-    err = userGroup.Save(params)
+    // 仅更新菜单字段,避免误改其他字段
+    err = userGroup.UpdateColumn("menu_data", requestData.MenuData)
     if err != nil {
         amis.WriteJsonError(c, err)
         return
     }
ui/src/components/IconPicker/index.tsx (2)

15-15: 非变动数据不需要放入 state,简化为常量并减少不必要的 effect 依赖

iconsPerPage 不会动态改变,没必要放入 React state。简化为常量也能减少 useEffect 的依赖与无效触发。

-  const [iconsPerPage] = useState(64); // 8x8网格
+  const iconsPerPage = 64; // 8x8网格
-  }, [currentPage, iconsPerPage]);
+  }, [currentPage]);

Also applies to: 23-23


18-24: 打开弹窗时重置页码到第一页

从 UX 上讲,用户每次打开选择器看到第 1 页更自然。

   useEffect(() => {
     const startIndex = (currentPage - 1) * iconsPerPage;
     const endIndex = startIndex + iconsPerPage;
     setDisplayIcons(iconOptions.slice(startIndex, endIndex));
   }, [currentPage, iconsPerPage]);
 
+  // 打开时重置页码
+  useEffect(() => {
+    if (open) {
+      setCurrentPage(1);
+    }
+  }, [open]);
ui/src/hooks/useCRDStatus.ts (2)

23-28: 提高健壮性:后端字段缺失时避免向 setState 传入 undefined

当前直接解构 response.data.data 并赋值,如果后端某字段缺失,可能传入 undefineduseState<boolean> 的 setter(尽管 TS 可能放过,运行时仍有隐患)。建议做布尔化处理。

-                    const status = response.data.data as CRDSupportedStatus;
-                    setIsGatewayAPISupported(status.IsGatewayAPISupported);
-                    setIsOpenKruiseSupported(status.IsOpenKruiseSupported);
-                    setIsIstioSupported(status.IsIstioSupported);
+                    const status = (response.data?.data || {}) as Partial<CRDSupportedStatus>;
+                    setIsGatewayAPISupported(!!status.IsGatewayAPISupported);
+                    setIsOpenKruiseSupported(!!status.IsOpenKruiseSupported);
+                    setIsIstioSupported(!!status.IsIstioSupported);

15-35: 避免组件卸载后 setState 引发内存泄漏或控制台警告

为异步请求添加取消标记,防止卸载后仍更新状态。

   useEffect(() => {
-        const fetchCRDSupportedStatus = async () => {
+        let cancelled = false;
+        const fetchCRDSupportedStatus = async () => {
             try {
                 const response = await fetcher({
                     url: '/k8s/crd/status',
                     method: 'get'
                 });
                 
                 if (response.data && typeof response.data === 'object') {
-                    const status = response.data.data as CRDSupportedStatus;
-                    setIsGatewayAPISupported(status.IsGatewayAPISupported);
-                    setIsOpenKruiseSupported(status.IsOpenKruiseSupported);
-                    setIsIstioSupported(status.IsIstioSupported);
+                    if (cancelled) return;
+                    const status = (response.data?.data || {}) as Partial<CRDSupportedStatus>;
+                    setIsGatewayAPISupported(!!status.IsGatewayAPISupported);
+                    setIsOpenKruiseSupported(!!status.IsOpenKruiseSupported);
+                    setIsIstioSupported(!!status.IsIstioSupported);
                 }
             } catch (error) {
                 console.error('Failed to fetch CRD status:', error);
             }
         };
 
         fetchCRDSupportedStatus();
-    }, []);
+        return () => { cancelled = true; };
+    }, []);
ui/src/pages/MenuEditor/menuData.ts (3)

924-926: 清理测试/占位文案

“关于333”疑似调试残留,建议改回正式文案“关于”。

-        title: '关于333',
+        title: '关于',

125-131: 精简可见性表达式:移除多余的 ==true

expr-eval 中函数已返回布尔值,可直接写 isOpenKruiseSupported() 等,提升可读性。

示例:

-        show: 'isOpenKruiseSupported()==true',
+        show: 'isOpenKruiseSupported()',

类似处:Istio、GatewayAPI、平台设置、Helm 仓库等。

Also applies to: 246-251, 472-477, 745-751, 371-381


7-8: 统一图标类名约定(与渲染端一致)

当前数据中多处直接存储完整的 FA 类(如 fa-solid fa-cube),而渲染端(Preview/Picker)有的会再加 fa-solid 前缀,导致重复。建议约定 MenuItem.icon 始终为“完整类名字符串”,渲染时直接使用 className={item.icon},避免重复与歧义。

如需我统一生成数据内的类名规范(或脚本检测不规范项),请告知。

Also applies to: 15-16, 31-32, 247-249, 368-376, 918-921

ui/src/utils/menuVisibility.ts (2)

22-25: 移除未使用的 evalContext,或补充必要变量

evalContext 仅含 user.role,但表达式并未用到该变量,保持为空对象即可,避免误导。

-            const evalContext = {
-                user: { role: 'user' },
-            };
+            // 无需传递上下文变量,均通过注入函数获得
-            const result = expr.evaluate(evalContext);
+            const result = expr.evaluate({});

Also applies to: 57-59


16-18: 类型与实现不一致:show 已限定为 string,分支冗余

MenuItem.show 在类型定义中已限定为字符串(见 types/menu.ts 注释“只保留字符串类型”)。这里保留 boolean 分支会造成心智负担。建议二选一:

  • 要兼容旧数据:更新类型为 string | boolean 并标注迁移计划;
  • 若不再兼容:去掉 boolean 分支,简化逻辑。

我可以按你的意向提交相应重构补丁。

ui/src/pages/MenuEditor/Preview.tsx (2)

34-40: 移除未使用的 _menuData 字段,保持上下文语义纯净

visibilityContext 中的 _menuData 未被 shouldShowMenuItem 使用,易引起误解。

     const visibilityContext = {
         userRole,
-        _menuData,
         isGatewayAPISupported,
         isOpenKruiseSupported,
         isIstioSupported
     };

108-114: 可提升交互的一点建议:用 Tree 的 onSelect 统一处理点击

当前通过包裹 span 的 onClick 处理,容易与 Tree 的选择态脱节。可以考虑改为 onSelect={(keys) => handleClick(keys[0] as string)},让整行节点点击都一致触发。

这是可选优化,不影响功能。

ui/src/pages/MenuEditor/CustomEventTags.tsx (1)

91-94: 事件处理逻辑可能存在浏览器兼容性问题

使用 tagName.toLowerCase() 检查 SVG 元素来阻止事件冒泡的方式可能不够可靠,因为关闭按钮的实现细节可能会变化。

建议使用更可靠的方式来区分点击事件:

-                        onClick={(e) => {
-                            // 防止点击关闭按钮时触发 tag 的点击事件
-                            if ((e.target as HTMLElement).tagName.toLowerCase() !== 'svg') {
-                                onChange?.(tag.value);
-                            }
+                        onClick={(e) => {
+                            // 使用 currentTarget 和 target 的比较更可靠
+                            if (e.target === e.currentTarget) {
+                                onChange?.(tag.value);
+                            }
ui/src/components/Sidebar/index.tsx (2)

45-46: 正则表达式可能无法匹配所有有效的路径格式

当前的正则表达式 /loadJsonPage\("([^"]+)"\)/ 只能匹配双引号格式,如果将来 customEvent 使用单引号或模板字符串,将无法正确提取路径。

建议使用更灵活的正则表达式:

-                    const match = customEvent.match(/loadJsonPage\("([^"]+)"\)/);
+                    const match = customEvent.match(/loadJsonPage\(["'`]([^"'`]+)["'`]\)/);

104-111: useMemo 依赖项中包含 navigate 可能导致不必要的重新计算

navigate 函数在每次组件重新渲染时都会创建新的引用,这会导致 menuItems 频繁重新计算。

建议将 navigate 从依赖项中移除,因为它的功能不会改变:

     const menuItems = useMemo(() => {
         const menuDataToUse = getMenuData();
         return convertMenuItems(menuDataToUse);
     }, [
-        navigate,
         userRole,
         menuData,
         isGatewayAPISupported,
         isOpenKruiseSupported,
         isIstioSupported
     ]);
pkg/controller/admin/menu/menu.go (1)

117-122: 缺少输入验证

strconv.Atoi 转换失败时会返回错误,但没有验证 ID 是否为正数。

建议添加 ID 有效性检查:

     id, err := strconv.Atoi(idStr)
     if err != nil {
         amis.WriteJsonError(c, err)
         return
     }
+    if id <= 0 {
+        amis.WriteJsonError(c, fmt.Errorf("invalid ID: %d", id))
+        return
+    }
pkg/models/menu.go (1)

79-81: 方法接收者命名不一致

Save 方法的接收者命名为 c,而其他方法都使用 m,这种不一致可能会造成困惑。

-func (c *Menu) Save(params *dao.Params, queryFuncs ...func(*gorm.DB) *gorm.DB) error {
-    return dao.GenericSave(params, c, queryFuncs...)
+func (m *Menu) Save(params *dao.Params, queryFuncs ...func(*gorm.DB) *gorm.DB) error {
+    return dao.GenericSave(params, m, queryFuncs...)
 }
ui/public/pages/admin/user/user_group.json (4)

5-10: 信息提示内容可读性与一致性微调(避免内联重复 alert 类、统一括号风格)

  • 当前在 Alert 组件内部再次包裹 <div class='alert alert-info'>,样式重复,容易导致不一致展示。
  • 标题中使用了全角括号“(ESC 关闭)”,而页面其他处使用半角括号“(ESC 关闭)”。建议统一。

可考虑直接让 Alert 渲染纯内容,移除内层 div 包裹;同时开启 showIcon 增强可读性。

       "type": "alert",
       "level": "info",
-      "body": "<div class='alert alert-info'><p><strong>菜单配置说明:</strong></p><ul><li><strong>自定义菜单:</strong>如果用户组设置了菜单数据,系统将使用该用户组的自定义菜单配置,覆盖默认菜单</li><li><strong>默认菜单:</strong>如果用户组未设置菜单数据,系统将使用默认的菜单配置</li><li><strong>多用户组冲突:</strong>当用户归属多个用户组且出现菜单冲突时,按照用户第一个用户组的菜单配置为准</li><li><strong>菜单优先级:</strong>用户组自定义菜单 > 系统默认菜单</li></ul></div>",
+      "showIcon": true,
+      "body": "<p><strong>菜单配置说明:</strong></p><ul><li><strong>自定义菜单:</strong>如果用户组设置了菜单数据,系统将使用该用户组的自定义菜单配置,覆盖默认菜单</li><li><strong>默认菜单:</strong>如果用户组未设置菜单数据,系统将使用默认的菜单配置</li><li><strong>多用户组冲突:</strong>当用户归属多个用户组且出现菜单冲突时,按照用户第一个用户组的菜单配置为准</li><li><strong>菜单优先级:</strong>用户组自定义菜单 > 系统默认菜单</li></ul>",
       "className": "mb-3"

195-209: 将通用编辑器替换为 JSON 专用控件,确保类型安全与校验体验

当前使用 type: editor + language: json。如果后端返回的是对象(而非字符串),Monaco 可能收到非字符串初始值,存在兼容风险。建议改为 amis 的 input-json 组件,内置格式化与校验,更贴合场景;同时对初始值使用 json 过滤器,保证为字符串。

-                    {
-                      "type": "editor",
-                      "name": "menu_data",
-                      "size": "xl",
-                      "required": true,
-                      "allowFullscreen": true,
-                      "placeholder": "请粘贴菜单JSON配置",
-                      "language": "json",
-                      "value": "${menu_data}",
-                      "options": {
-                        "wordWrap": "on",
-                        "scrollbar": {
-                          "vertical": "auto"
-                        }
-                      }
-                    },
+                    {
+                      "type": "input-json",
+                      "name": "menu_data",
+                      "height": 420,
+                      "required": true,
+                      "placeholder": "请粘贴菜单 JSON 配置",
+                      "value": "${menu_data|json}",
+                      "validateOnChange": true
+                    },

218-231: 建议补充失败态提示,提升可观测性与可用性

保存失败时目前无反馈。补充 submitFail 事件,toast 提示具体错误,有助于定位 JSON 校验/后端报错。

                   "onEvent": {
                     "submitSucc": {
                       "actions": [
                         {
                           "actionType": "reload",
                           "componentId": "groupCRUD"
                         },
                         {
                           "actionType": "closeDialog"
                         }
                       ]
                     }
+                  ,
+                    "submitFail": {
+                      "actions": [
+                        {
+                          "actionType": "toast",
+                          "args": {
+                            "msgType": "error",
+                            "msg": "保存失败:${lastError || '请检查 JSON 是否为合法格式,或联系管理员查看后端日志'}"
+                          }
+                        }
+                      ]
+                    }
                   }

183-191: 安全细节:target=_blank 打开新标签的反制措施

使用 "blank": true 可能仅设置 target=_blank。为防止反向标签劫持(reverse tabnabbing),通常需要 rel="noopener noreferrer"。请确认 amis 的 url 动作是否默认添加 rel 属性;若未添加,建议升级 amis 或采用受控链接组件以携带 rel。

是否需要我帮忙检索 amis 文档并确认该属性的默认行为?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled
  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9678bda and 8cbf1e3.

📒 Files selected for processing (21)
  • main.go (2 hunks)
  • pkg/controller/admin/menu/menu.go (1 hunks)
  • pkg/controller/admin/user/user_group.go (2 hunks)
  • pkg/controller/param/user.go (2 hunks)
  • pkg/models/menu.go (1 hunks)
  • pkg/models/user_group.go (1 hunks)
  • pkg/service/user.go (1 hunks)
  • ui/public/pages/admin/user/user_group.json (3 hunks)
  • ui/src/components/IconPicker/index.module.scss (1 hunks)
  • ui/src/components/IconPicker/index.tsx (1 hunks)
  • ui/src/components/Sidebar/index.tsx (1 hunks)
  • ui/src/hooks/useCRDStatus.ts (1 hunks)
  • ui/src/hooks/useUserRole.ts (1 hunks)
  • ui/src/pages/MenuEditor/CustomEventTags.tsx (1 hunks)
  • ui/src/pages/MenuEditor/Preview.tsx (1 hunks)
  • ui/src/pages/MenuEditor/index.tsx (1 hunks)
  • ui/src/pages/MenuEditor/menuData.ts (1 hunks)
  • ui/src/routes/index.tsx (2 hunks)
  • ui/src/types/menu.ts (1 hunks)
  • ui/src/utils/iconOptions.ts (1 hunks)
  • ui/src/utils/menuVisibility.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (13)
main.go (1)
pkg/controller/admin/menu/menu.go (1)
  • RegisterAdminMenuRoutes (18-28)
ui/src/hooks/useUserRole.ts (1)
ui/src/components/Amis/fetcher.ts (1)
  • fetcher (8-60)
pkg/controller/admin/menu/menu.go (3)
internal/dao/param.go (1)
  • BuildParams (30-72)
pkg/models/menu.go (1)
  • Menu (14-19)
internal/dao/conn.go (1)
  • DB (184-190)
ui/src/pages/MenuEditor/menuData.ts (1)
ui/src/types/menu.ts (1)
  • MenuItem (1-11)
ui/src/pages/MenuEditor/index.tsx (3)
ui/src/types/menu.ts (1)
  • MenuItem (1-11)
ui/src/pages/MenuEditor/menuData.ts (1)
  • initialMenu (3-930)
ui/src/components/Amis/fetcher.ts (1)
  • fetcher (8-60)
pkg/controller/param/user.go (2)
pkg/comm/utils/amis/gin.go (1)
  • GetLoginUser (31-43)
pkg/service/service.go (1)
  • UserService (67-69)
ui/src/hooks/useCRDStatus.ts (1)
ui/src/components/Amis/fetcher.ts (1)
  • fetcher (8-60)
ui/src/components/Sidebar/index.tsx (6)
ui/src/hooks/useUserRole.ts (1)
  • useUserRole (10-41)
ui/src/hooks/useCRDStatus.ts (1)
  • useCRDStatus (10-42)
ui/src/types/menu.ts (1)
  • MenuItem (1-11)
ui/src/utils/menuVisibility.ts (1)
  • shouldShowMenuItem (11-67)
ui/src/pages/MenuEditor/menuData.ts (1)
  • initialMenu (3-930)
pkg/models/menu.go (1)
  • Menu (14-19)
ui/src/utils/menuVisibility.ts (1)
ui/src/types/menu.ts (1)
  • MenuItem (1-11)
pkg/service/user.go (5)
pkg/comm/utils/cache.go (1)
  • GetOrSetCache (10-34)
pkg/service/cache.go (1)
  • CacheService (15-27)
internal/dao/param.go (1)
  • Params (11-19)
pkg/models/user_group.go (1)
  • UserGroup (11-19)
internal/dao/conn.go (1)
  • DB (184-190)
pkg/models/menu.go (3)
internal/dao/param.go (1)
  • Params (11-19)
internal/dao/conn.go (1)
  • DB (184-190)
internal/dao/base.go (4)
  • GenericQuery (13-77)
  • GenericSave (110-136)
  • GenericDelete (139-167)
  • GenericGetOne (79-101)
pkg/controller/admin/user/user_group.go (4)
internal/dao/param.go (1)
  • BuildParams (30-72)
pkg/models/user_group.go (1)
  • UserGroup (11-19)
pkg/service/service.go (1)
  • UserService (67-69)
pkg/comm/utils/cache.go (1)
  • ClearCacheByKey (37-44)
ui/src/pages/MenuEditor/Preview.tsx (4)
ui/src/types/menu.ts (1)
  • MenuItem (1-11)
ui/src/hooks/useUserRole.ts (1)
  • useUserRole (10-41)
ui/src/hooks/useCRDStatus.ts (1)
  • useCRDStatus (10-42)
ui/src/utils/menuVisibility.ts (1)
  • shouldShowMenuItem (11-67)
🪛 Biome (2.1.2)
ui/src/pages/MenuEditor/Preview.tsx

[error] 22-22: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (11)
ui/src/routes/index.tsx (1)

25-25: 确认是否有意将 /MenuEditor 放在 Layout 之外及访问控制策略

当前与 /NodeExec/PodExec/PodLog 一样放在根路由下。请确认:

  • 是否需要在该页面内或路由层进行权限校验(仅平台管理员可见)?
  • 是否应放入 <Layout/> 下以复用侧边栏/顶栏等布局?

若已有全局守卫/组件内校验可忽略。

main.go (2)

23-23: 引入 admin/menu 包用于路由注册 — LGTM

新增 import 合理,未引入未用依赖问题。


374-376: 在 /admin 分组下注册菜单路由,受 PlatformAuthMiddleware 保护 — LGTM

放置位置与其他 admin 能力保持一致,权限边界清晰。

pkg/controller/param/user.go (1)

32-36: 多组用户仅取首个组的菜单配置,需求确认

GetGroupMenuData 会拆分 groupNames 并仅取第一个组。若用户同时属于多个组,这种策略是否符合预期(优先级/合并规则)?

如果需按优先级合并或覆盖,建议明确策略并在 service 层实现(例如按组优先级从高到低找首个非空,或进行合并)。

pkg/service/user.go (1)

31-37: 仅取第一个组名,建议在注释中明确策略或支持优先级

当前策略是“按逗号分割,仅取第一个组名”。如业务确实以“首个组为主”,建议在方法注释中明确,避免后续误改;如需优先级/合并,建议扩展。

pkg/controller/admin/user/user_group.go (1)

23-23: 路由注册符合预期

新增保存菜单接口注册到 /admin/user_group/save_menu,路径与职责清晰。

ui/src/components/IconPicker/index.tsx (1)

79-79: 无需调整:当前前缀添加逻辑正确

经验证,ui/src/utils/iconOptions.ts 中的 value 仅包含图标名(如 fa-angle-down),不含 fa-solid 前缀;渲染时统一加上 fa-solid,不会出现重复。现有写法保持不变即可。

ui/src/components/Sidebar/index.tsx (1)

86-98: 菜单数据解析失败时的降级处理很好

使用 try-catch 处理 JSON 解析错误,并在失败时优雅地降级到 initialMenu,这种错误处理方式很稳健。

pkg/models/menu.go (1)

31-48: JSON 序列化处理很稳健

MarshalJSON 方法能够优雅地处理 JSON 解析失败的情况,在解析失败时返回原始字符串而不是抛出错误,这种容错设计很好。

ui/public/pages/admin/user/user_group.json (2)

161-233: “设置菜单”弹窗整体交互清晰,流程合理

  • 行为与后端接口的对接直观,包含打开可视化编辑器与粘贴 JSON 的双路径,用户成本低。
  • 保存成功后 reload + 关闭弹窗的联动符合预期。

173-174: 已验证后端 save_menu 接口及前端 MenuEditor 路由存在

  • 后端:在 pkg/controller/admin/user/user_group.go 中已注册
    admin.POST("/user_group/save_menu", ctrl.SaveUserGroupMenu)(行 23),并在 Swagger 注释中标注 @Router /admin/user_group/save_menu [post]
  • 前端:
    1. ui/src/routes/index.tsx 第 25 行定义了 <Route path='/MenuEditor' element={<MenuEditor/>}/>
    2. ui/public/pages/admin/user/user_group.json 第 188 行使用了 "url": "/#/MenuEditor"

无需对这部分代码做额外调整。

func (a *AdminMenuController) History(c *gin.Context) {
params := dao.BuildParams(c)
m := &models.Menu{}
params.PerPage = 100000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

PerPage 硬编码值过大可能影响性能

PerPage 设置为 100000 可能会导致内存占用过大和响应时间过长,特别是当历史记录增多时。

建议添加合理的分页支持或限制最大返回数量:

-    params.PerPage = 100000
+    params.PerPage = 1000 // 限制最大返回1000条记录

或者根据前端需求实现真正的分页加载。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
params.PerPage = 100000
params.PerPage = 1000 // 限制最大返回1000条记录
🤖 Prompt for AI Agents
In pkg/controller/admin/menu/menu.go around line 80, PerPage is hardcoded to
100000 which can cause high memory and slow responses; change this to implement
proper pagination and guard the page size: read PerPage from request parameters
(with a sensible default like 20–50), validate and clamp it to a safe maximum
(e.g., 1000), and return results and metadata (total, page, per_page) so the
frontend can request further pages; alternatively, if full export is required
keep an explicit export flag that bypasses pagination after
authorization/limits.

Comment on lines +27 to +36
groupNames, err := service.UserService().GetGroupNames(user)
if err != nil {
amis.WriteJsonError(c, err)
return
}
menuData, err := service.UserService().GetGroupMenuData(groupNames)
if err != nil {
amis.WriteJsonError(c, err)
return
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

menu_data 可能为 nil,前端 useUserRole 以 string 处理,建议统一为空串

GetGroupMenuData 在未配置时返回 nil,序列化后为 JSON null。前端 hook 将其当作 string 使用,可能引发后续 JSON.parse 或拼接错误。建议在控制器层兜底为空串,保持返回类型稳定。

 	menuData, err := service.UserService().GetGroupMenuData(groupNames)
 	if err != nil {
 		amis.WriteJsonError(c, err)
 		return
 	}
+	// 统一 menu_data 类型为字符串,避免前端拿到 null
+	if menuData == nil {
+		menuData = ""
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
groupNames, err := service.UserService().GetGroupNames(user)
if err != nil {
amis.WriteJsonError(c, err)
return
}
menuData, err := service.UserService().GetGroupMenuData(groupNames)
if err != nil {
amis.WriteJsonError(c, err)
return
}
groupNames, err := service.UserService().GetGroupNames(user)
if err != nil {
amis.WriteJsonError(c, err)
return
}
menuData, err := service.UserService().GetGroupMenuData(groupNames)
if err != nil {
amis.WriteJsonError(c, err)
return
}
// 统一 menu_data 类型为字符串,避免前端拿到 null
if menuData == nil {
menuData = ""
}
🤖 Prompt for AI Agents
In pkg/controller/param/user.go around lines 27 to 36, GetGroupMenuData can
return nil which serializes to JSON null but the frontend expects a string;
ensure menuData is normalized to an empty string when nil before writing the
response. After calling GetGroupMenuData, check if menuData == nil (or
empty/zero value depending on its type) and if so set menuData = "" (string) so
the controller always returns a stable string value to the client, then continue
to write the JSON response as before.

for _, f := range queryFuncs {
db = f(db)
}
err := db.Order("version desc").First(&menu).Error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

查询条件使用了不存在的字段

GetLatest 方法中使用 ORDER BY version desc,但 Menu 结构体中没有 version 字段,这会导致数据库查询错误。

应该使用 created_atid 字段来获取最新记录:

-    err := db.Order("version desc").First(&menu).Error
+    err := db.Order("created_at desc").First(&menu).Error
🤖 Prompt for AI Agents
In pkg/models/menu.go around line 100, the query uses Order("version desc") but
the Menu struct has no version field causing the DB query to fail; replace the
ordering with an existing column such as Order("created_at desc") (or Order("id
desc") if created_at is not present) and ensure the column name matches the
actual DB column/struct tag (e.g., created_at or id) so GetLatest returns the
newest record.

Comment on lines +282 to +290
{
"name": "menu_data",
"label": "菜单状态",
"type": "mapping",
"map": {
"": "",
"*": "<span class='label label-success'>已设置</span>"
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

空值展示逻辑可能误判为“已设置”,建议改为表达式渲染

当后端返回 null/undefined 时,mapping 的 "*" 兜底会把它当成未匹配而仍渲染“已设置”。这会导致全新用户组也显示“已设置”。建议改为 tpl + 表达式判断并开启 html 渲染。

-        {
-          "name": "menu_data",
-          "label": "菜单状态",
-          "type": "mapping",
-          "map": {
-            "": "",
-            "*": "<span class='label label-success'>已设置</span>"
-          }
-        },
+        {
+          "name": "menu_data",
+          "label": "菜单状态",
+          "type": "tpl",
+          "html": true,
+          "tpl": "${menu_data ? '<span class=\"label label-success\">已设置</span>' : ''}"
+        },

如需更严格区分空对象,也可约定后端未配置时返回空字符串或 null,以避免 {} 被判为“已设置”。需要的话我可以同步提供后端返回值规范与前端表达式的配套实现。

🤖 Prompt for AI Agents
In ui/public/pages/admin/user/user_group.json around lines 282 to 290, the
mapping for "menu_data" treats null/undefined via the "*" fallback and
incorrectly shows "已设置"; replace the mapping with a tpl expression that
explicitly checks for empty/null/undefined (and optionally empty object) and
returns an empty string or a "未设置" label otherwise, enable html rendering so the
success badge is rendered when truly set, and document that backend should
return null/"" when not configured if you need stricter distinction.

import React, { useState, useEffect } from 'react';
import { Modal, Pagination } from 'antd';
import iconOptions from '@/utils/iconOptions';
import './index.module.scss';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

修复 CSS Modules 未生效问题,当前类名不会匹配样式

你以 side-effect 方式引入了 index.module.scss,但下方使用的是普通字符串类名,这在 CSS Modules 下会被哈希化导致无法匹配,样式不生效。建议改为通过映射对象引用模块类名。

可应用如下补丁:

-import './index.module.scss';
+import styles from './index.module.scss';
-      <div className="icon-picker-container" style={{ padding: '16px', width: '100%', boxSizing: 'border-box' }}>
+      <div className={styles['icon-picker-container']} style={{ padding: '16px', width: '100%', boxSizing: 'border-box' }}>
-        <div
-          className="icon-grid"
+        <div
+          className={styles['icon-grid']}
-              className={`icon-item ${selectedIcon === icon.value ? 'selected' : ''}`}
+              className={`${styles['icon-item']} ${selectedIcon === icon.value ? styles['selected'] : ''}`}
-        <div className="pagination-container" style={{ display: 'flex', justifyContent: 'center', marginTop: '16px' }}>
+        <div className={styles['pagination-container']} style={{ display: 'flex', justifyContent: 'center', marginTop: '16px' }}>

Also applies to: 44-47, 60-61, 84-86

🤖 Prompt for AI Agents
In ui/src/components/IconPicker/index.tsx around lines 4 (and also affecting
lines 44-47, 60-61, 84-86), the module SCSS is imported as a side-effect so
className strings won't match hashed CSS Modules names; change the import to a
named mapping (e.g. import styles from './index.module.scss') and replace all
plain string className values with the corresponding properties on that styles
object (e.g. className={styles.someClass}), ensuring conditional classes use
helpers or template literals that reference styles.* rather than raw strings.

Comment on lines +20 to +27
try {
const routerNavigate = useNavigate();
navigateFunc = propNavigate || routerNavigate;
} catch (error) {
navigateFunc = propNavigate || (path => {
console.warn('Navigation attempted outside Router context to:', path);
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

修复 Hooks 规则违规:useNavigate 不能放在 try/catch 中条件调用

静态分析已报错(lint/correctness/useHookAtTopLevel)。请将 useNavigate 提升到组件顶层无条件调用,并用 prop 作为优先值。该组件应在 Router 环境内使用;若需要在非 Router 环境使用,请由调用方传入 navigate 实现。

-    // 尝试使用传入的 navigate 或者 useNavigate hook
-    let navigateFunc: NavigateFunction | undefined;
-
-    try {
-        const routerNavigate = useNavigate();
-        navigateFunc = propNavigate || routerNavigate;
-    } catch (error) {
-        navigateFunc = propNavigate || (path => {
-            console.warn('Navigation attempted outside Router context to:', path);
-        });
-    }
+    // Router 环境内使用;如需在非 Router 环境使用,请通过 propNavigate 传入
+    const routerNavigate = useNavigate();
+    const navigateFunc: NavigateFunction = propNavigate || routerNavigate;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const routerNavigate = useNavigate();
navigateFunc = propNavigate || routerNavigate;
} catch (error) {
navigateFunc = propNavigate || (path => {
console.warn('Navigation attempted outside Router context to:', path);
});
}
// Router 环境内使用;如需在非 Router 环境使用,请通过 propNavigate 传入
const routerNavigate = useNavigate();
const navigateFunc: NavigateFunction = propNavigate || routerNavigate;
🧰 Tools
🪛 Biome (2.1.2)

[error] 22-22: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In ui/src/pages/MenuEditor/Preview.tsx around lines 20 to 27, useNavigate is
being called conditionally inside a try/catch which violates Hooks rules; move
the call to useNavigate to the component top-level (unconditional), then set
navigateFunc = propNavigate || routerNavigate and remove the try/catch and the
in-function fallback; if this component must work outside a Router, require the
caller to pass a navigate prop (propNavigate) instead of providing a local
console-warning fallback.

Comment on lines +61 to +69
// 构建并执行自定义函数
const func = new Function(...Object.keys(context), `return ${item.customEvent}`);
const result = func(...Object.values(context));

// 如果是函数,执行它
if (typeof result === 'function') {
result();
}
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

避免使用 new Function 执行任意代码,存在 XSS/代码注入风险

new Function 会执行任意字符串代码。建议白名单化支持的事件格式(当前仅需 () => loadJsonPage("/path")),用正则解析出路径并调用 loadJsonPage,不执行任意脚本。

-                    // 构建并执行自定义函数
-                    const func = new Function(...Object.keys(context), `return ${item.customEvent}`);
-                    const result = func(...Object.values(context));
-
-                    // 如果是函数,执行它
-                    if (typeof result === 'function') {
-                        result();
-                    }
+                    // 安全解析支持的格式:`() => loadJsonPage("/path")`
+                    const match = item.customEvent.match(/loadJsonPage\((['"])(.+?)\1\)/);
+                    if (match) {
+                        context.loadJsonPage(match[2]);
+                    } else {
+                        message.warning('不支持的自定义事件格式,仅支持 loadJsonPage("/path")');
+                    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 构建并执行自定义函数
const func = new Function(...Object.keys(context), `return ${item.customEvent}`);
const result = func(...Object.values(context));
// 如果是函数,执行它
if (typeof result === 'function') {
result();
}
} catch (error) {
// 安全解析支持的格式:`() => loadJsonPage("/path")`
const match = item.customEvent.match(/loadJsonPage\((['"])(.+?)\1\)/);
if (match) {
context.loadJsonPage(match[2]);
} else {
message.warning('不支持的自定义事件格式,仅支持 loadJsonPage("/path")');
}

Comment on lines +99 to +102
<span onClick={() => handleClick(item.key)}>
{item.icon && <i className={`fa-solid ${item.icon}`} style={{ marginRight: '4px' }}></i>}
{item.title}
</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

避免重复添加 fa-solid,直接使用完整类名

为与数据保持一致,直接使用 item.icon 作为完整类名,避免出现 fa-solid fa-solid ...

-                    {item.icon && <i className={`fa-solid ${item.icon}`} style={{ marginRight: '4px' }}></i>}
+                    {item.icon && <i className={item.icon} style={{ marginRight: '4px' }}></i>}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span onClick={() => handleClick(item.key)}>
{item.icon && <i className={`fa-solid ${item.icon}`} style={{ marginRight: '4px' }}></i>}
{item.title}
</span>
<span onClick={() => handleClick(item.key)}>
{item.icon && <i className={item.icon} style={{ marginRight: '4px' }}></i>}
{item.title}
</span>
🤖 Prompt for AI Agents
In ui/src/pages/MenuEditor/Preview.tsx around lines 99 to 102, the code prepends
a hardcoded "fa-solid" which can duplicate classes if item.icon already contains
the full FontAwesome class string; update the span's icon element to use
item.icon directly as the className (preserving the inline style/marginRight)
instead of `fa-solid ${item.icon}`, and ensure the conditional still guards
null/undefined item.icon so no empty class is rendered.

Comment on lines +988 to +989
{ value: 'fa-wifi' },
{ value: 'fa-wifi' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

移除重复的 fa-wifi 项,避免图标选择器重复显示

这里 fa-wifi 连续出现了两次,会导致 UI 出现重复条目。

建议删除其中一个重复项:

         { value: 'fa-wifi' },
-        { value: 'fa-wifi' },
         { value: 'fa-wind' },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ value: 'fa-wifi' },
{ value: 'fa-wifi' },
{ value: 'fa-wifi' },
{ value: 'fa-wind' },
🤖 Prompt for AI Agents
In ui/src/utils/iconOptions.ts around lines 988 to 989 there are two identical
entries for { value: 'fa-wifi' } causing duplicate items in the icon picker;
remove one of the duplicate fa-wifi entries so each icon value is unique,
leaving a single { value: 'fa-wifi' } in the array.

Comment on lines +29 to +35
parser.functions.contains = function (str: string | string[], substr: string) {
if (typeof str !== 'string' || typeof substr !== 'string') {
return false;
}
return str.includes(substr);
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

contains 函数未按注释支持数组,补齐数组场景

注释声明支持 string | string[],但当前仅处理了字符串。建议补齐数组判断:

-            parser.functions.contains = function (str: string | string[], substr: string) {
-                if (typeof str !== 'string' || typeof substr !== 'string') {
-                    return false;
-                }
-                return str.includes(substr);
-            };
+            parser.functions.contains = function (str: string | string[], substr: string) {
+                if (typeof substr !== 'string') return false;
+                if (typeof str === 'string') return str.includes(substr);
+                if (Array.isArray(str)) return str.includes(substr);
+                return false;
+            };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
parser.functions.contains = function (str: string | string[], substr: string) {
if (typeof str !== 'string' || typeof substr !== 'string') {
return false;
}
return str.includes(substr);
};
parser.functions.contains = function (str: string | string[], substr: string) {
if (typeof substr !== 'string') return false;
if (typeof str === 'string') return str.includes(substr);
if (Array.isArray(str)) return str.includes(substr);
return false;
};
🤖 Prompt for AI Agents
In ui/src/utils/menuVisibility.ts around lines 29 to 35, the contains function
currently only handles string inputs despite its signature and comment claiming
support for string | string[]; update the function to handle the array case by
first validating substr is a string, then if str is an array use Array.isArray
to iterate over elements and return true if any element is a string and
includes(substr), otherwise fall back to the existing string branch and return
false for any unsupported types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant