-
Notifications
You must be signed in to change notification settings - Fork 106
feat(menu): 实现自定义菜单功能,包括菜单编辑器、权限控制和动态展示 #280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
新增菜单类型定义和接口,实现菜单数据的存储与获取 添加菜单编辑器页面,支持可视化编辑和JSON配置 实现基于用户角色和集群状态的菜单动态展示逻辑 扩展用户组管理,支持为不同用户组配置自定义菜单 添加菜单可见性控制,支持表达式条件判断
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 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. ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (2)
📝 WalkthroughSummary by CodeRabbit
Walkthrough在后端新增 Menu 模型与 Admin 菜单控制器并注册路由;扩展 UserGroup(新增 MenuData)及用户组保存菜单接口;用户角色接口返回 menu_data。前端新增菜单编辑器页面、图标选择器、菜单预览与历史,Sidebar 改为基于 menu_data + CRD 能力动态渲染并新增相关 Hooks 与可见性解析。 Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
添加LDAP配置菜单项,支持跳转到LDAP配置页面 修正关于菜单的标题文字错误
There was a problem hiding this 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 的必填/互斥字段,提升类型安全目前
url
与customEvent
均为可选,运行期才会发现缺失。建议使用可判别联合,静态约束字段的存在性与互斥关系。参考改造示例(如不影响现有调用):
-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 传递,建议改为id
或menuKey
。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
并赋值,如果后端某字段缺失,可能传入undefined
给useState<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.
📒 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]
。- 前端:
- 在
ui/src/routes/index.tsx
第 25 行定义了<Route path='/MenuEditor' element={<MenuEditor/>}/>
。- 在
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 |
There was a problem hiding this comment.
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.
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.
pkg/controller/param/user.go
Outdated
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 | ||
} |
There was a problem hiding this comment.
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.
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.
pkg/models/menu.go
Outdated
for _, f := range queryFuncs { | ||
db = f(db) | ||
} | ||
err := db.Order("version desc").First(&menu).Error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
查询条件使用了不存在的字段
GetLatest
方法中使用 ORDER BY version desc
,但 Menu
结构体中没有 version
字段,这会导致数据库查询错误。
应该使用 created_at
或 id
字段来获取最新记录:
- 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.
{ | ||
"name": "menu_data", | ||
"label": "菜单状态", | ||
"type": "mapping", | ||
"map": { | ||
"": "", | ||
"*": "<span class='label label-success'>已设置</span>" | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
空值展示逻辑可能误判为“已设置”,建议改为表达式渲染
当后端返回 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'; |
There was a problem hiding this comment.
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.
ui/src/pages/MenuEditor/Preview.tsx
Outdated
try { | ||
const routerNavigate = useNavigate(); | ||
navigateFunc = propNavigate || routerNavigate; | ||
} catch (error) { | ||
navigateFunc = propNavigate || (path => { | ||
console.warn('Navigation attempted outside Router context to:', path); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
修复 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.
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.
ui/src/pages/MenuEditor/Preview.tsx
Outdated
// 构建并执行自定义函数 | ||
const func = new Function(...Object.keys(context), `return ${item.customEvent}`); | ||
const result = func(...Object.values(context)); | ||
|
||
// 如果是函数,执行它 | ||
if (typeof result === 'function') { | ||
result(); | ||
} | ||
} catch (error) { |
There was a problem hiding this comment.
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.
// 构建并执行自定义函数 | |
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")'); | |
} |
ui/src/pages/MenuEditor/Preview.tsx
Outdated
<span onClick={() => handleClick(item.key)}> | ||
{item.icon && <i className={`fa-solid ${item.icon}`} style={{ marginRight: '4px' }}></i>} | ||
{item.title} | ||
</span> |
There was a problem hiding this comment.
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.
<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.
ui/src/utils/iconOptions.ts
Outdated
{ value: 'fa-wifi' }, | ||
{ value: 'fa-wifi' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
移除重复的 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.
{ 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.
ui/src/utils/menuVisibility.ts
Outdated
parser.functions.contains = function (str: string | string[], substr: string) { | ||
if (typeof str !== 'string' || typeof substr !== 'string') { | ||
return false; | ||
} | ||
return str.includes(substr); | ||
}; | ||
|
There was a problem hiding this comment.
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.
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.
新增菜单类型定义和接口,实现菜单数据的存储与获取
添加菜单编辑器页面,支持可视化编辑和JSON配置
实现基于用户角色和集群状态的菜单动态展示逻辑
扩展用户组管理,支持为不同用户组配置自定义菜单
添加菜单可见性控制,支持表达式条件判断