新增"顶栏"、"侧边栏"管理功能 - #1701
Conversation
WalkthroughAdds configurable header and sidebar modules across backend and frontend. Backend now returns HeaderNavModules and SidebarModulesAdmin in status, manages user roles, permissions, and per-user sidebar_modules with defaults and updates. Frontend consumes these configs to gate navigation (pricing auth), render dynamic sidebar visibility, and provide admin/user UIs to edit and persist configurations. Changes
Sequence Diagram(s)sequenceDiagram
participant Server as Server (GetStatus)
participant App as App/useHeaderBar
participant Nav as Header Navigation
participant Router as Router
Server-->>App: status.HeaderNavModules (JSON)
App->>App: parse modules, derive pricingRequireAuth
App-->>Nav: pricingRequireAuth
Router->>App: Route /pricing
App->>Router: Use PrivateRoute if pricingRequireAuth else public Pricing
Nav->>Nav: If unauth && pricingRequireAuth => link to /login
sequenceDiagram
participant Server as Server
participant Sidebar as useSidebar
participant UI as SiderBar.jsx
Server-->>Sidebar: Status.SidebarModulesAdmin (JSON)
Sidebar->>Server: GET /api/user/self
Server-->>Sidebar: sidebar_modules (string|object)
Sidebar->>Sidebar: merge adminConfig + userConfig => finalConfig
Sidebar-->>UI: isModuleVisible(), hasSectionVisibleModules()
UI->>UI: Conditionally render sections and items
sequenceDiagram
participant Client as Settings (User)
participant API as /api/user/self
participant Model as model.User
Client->>API: PUT { sidebar_modules: JSON-string }
API->>Model: Persist user.Setting.SidebarModules
Model-->>API: OK
API-->>Client: Success response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ 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 (
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (28)
web/src/hooks/common/useUserPermissions.js (1)
22-23: Clean up dev logs and localize error.Remove console.log in production; return localized error from consumers instead of hard-coded zh message in the hook.
- console.log('用户权限加载成功:', userPermissions); + // no-op ... - setError('网络错误,请重试'); + setError('NETWORK_ERROR'); // let UI map to i18nAlso applies to: 28-29
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
34-61: DRY: extract DEFAULT_SIDEBAR_MODULES to avoid triple duplication.Same default object appears in initial state, reset, and fallback. Centralize to prevent drift.
+const DEFAULT_SIDEBAR_MODULES = { + chat: { enabled: true, playground: true, chat: true }, + console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true }, + personal: { enabled: true, topup: true, personal: true }, + admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true }, +}; ... - const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState({ - chat: { - enabled: true, - playground: true, - chat: true - }, - console: { - enabled: true, - detail: true, - token: true, - log: true, - midjourney: true, - task: true - }, - personal: { - enabled: true, - topup: true, - personal: true - }, - admin: { - enabled: true, - channel: true, - models: true, - redemption: true, - user: true, - setting: true - } - }); + const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState(DEFAULT_SIDEBAR_MODULES); ... - function resetSidebarModules() { - const defaultModules = { - chat: { - enabled: true, - playground: true, - chat: true - }, - console: { - enabled: true, - detail: true, - token: true, - log: true, - midjourney: true, - task: true - }, - personal: { - enabled: true, - topup: true, - personal: true - }, - admin: { - enabled: true, - channel: true, - models: true, - redemption: true, - user: true, - setting: true - } - }; - setSidebarModulesAdmin(defaultModules); + function resetSidebarModules() { + setSidebarModulesAdmin(DEFAULT_SIDEBAR_MODULES); showSuccess(t('已重置为默认配置')); } ... - const defaultModules = { - chat: { enabled: true, playground: true, chat: true }, - console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true }, - personal: { enabled: true, topup: true, personal: true }, - admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true } - }; - setSidebarModulesAdmin(defaultModules); + setSidebarModulesAdmin(DEFAULT_SIDEBAR_MODULES);Also applies to: 92-123, 168-173
dto/user_settings.go (1)
11-11: Avoid stringly-typed JSON for SidebarModules; validate or use RawMessageStoring JSON as string is brittle. At minimum, validate it server-side before persisting. Optionally, switch the field to json.RawMessage to avoid double-encoding and enable schema validation later.
If you choose RawMessage, update the type as below and adjust assignments accordingly:
- SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置 + SidebarModules json.RawMessage `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置Add import in this file:
import "encoding/json"Would you like me to add a small validator that rejects malformed JSON for this field in the update path?
web/src/components/settings/OperationSetting.jsx (1)
119-126: UI wiring looks good; consider consistent Card wrappers (nit)
Wrap these sections in Card like others for uniform look-and-feel.model/user.go (2)
94-154: Default config generator is fine; consider constants and tighter typing (optional)
- Define module/section keys as constants to avoid drift with frontend.
- Optionally return json.RawMessage and let callers assign directly without string conversions.
Example constants (outside this hunk):
const ( sectionChat = "chat" sectionConsole = "console" sectionPersonal = "personal" sectionAdmin = "admin" modPlayground = "playground" modChat = "chat" modDetail = "detail" modToken = "token" modLog = "log" modMidjourney = "midjourney" modTask = "task" modTopup = "topup" modPersonal = "personal" modChannel = "channel" modModels = "models" modRedemption = "redemption" modUser = "user" modSetting = "setting" )If you adopt RawMessage (optional), apply:
-func generateDefaultSidebarConfigForRole(userRole int) string { +func generateDefaultSidebarConfigForRole(userRole int) json.RawMessage { @@ - configBytes, err := json.Marshal(defaultConfig) + configBytes, err := json.Marshal(defaultConfig) if err != nil { common.SysLog("生成默认边栏配置失败: " + err.Error()) - return "" + return nil } - return string(configBytes) + return configBytes }
386-392: Initialize defaults early to avoid a second write (optional)You can set SidebarModules in the initial defaultSetting (role is already known) and persist once.
- defaultSetting := dto.UserSetting{} - // 这里暂时不设置SidebarModules,因为需要在用户创建后根据角色设置 - user.SetSetting(defaultSetting) + defaultSetting := dto.UserSetting{} + // 可直接根据当前 user.Role 生成默认边栏配置,减少后续一次更新 + if cfg := generateDefaultSidebarConfigForRole(user.Role); cfg != nil { + defaultSetting.SidebarModules = cfg // json.RawMessage if adopted; else keep string + } + user.SetSetting(defaultSetting)web/src/components/layout/HeaderBar/Navigation.jsx (2)
24-24: Default prop to avoid undefined and ease usage (optional)Provide a default false to pricingRequireAuth to keep call sites simple.
-const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth }) => { +const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth = false }) => {
55-56: Preserve post-login redirect when gating pricing (optional)Retain intended destination after login to improve UX.
- targetPath = '/login'; + targetPath = `/login?redirect=${encodeURIComponent(link.to)}`;Please confirm your login page honors a redirect query param; if not, I can wire that up.
web/src/hooks/common/useNavigation.js (1)
35-36: Merge with defaults to avoid accidental hiding when keys are missingIf HeaderNavModules omits a key, that item is currently hidden. Merge with defaults so unspecified modules inherit default visibility.
Apply:
- const modules = headerNavModules || defaultModules; + const modules = headerNavModules + ? { ...defaultModules, ...headerNavModules } + : defaultModules;web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (5)
34-43: Deduplicate default config into a single constantThe default modules object is repeated 3x. Centralize to prevent drift.
+// 顶栏默认配置(集中定义,避免重复) +const DEFAULT_HEADER_NAV_MODULES = Object.freeze({ + home: true, + console: true, + pricing: { enabled: true, requireAuth: false }, + docs: true, + about: true, +}); - const [headerNavModules, setHeaderNavModules] = useState({ - home: true, - console: true, - pricing: { - enabled: true, - requireAuth: false // 默认不需要登录鉴权 - }, - docs: true, - about: true, - }); + const [headerNavModules, setHeaderNavModules] = useState(DEFAULT_HEADER_NAV_MODULES);- function resetHeaderNavModules() { - const defaultModules = { - home: true, - console: true, - pricing: { - enabled: true, - requireAuth: false - }, - docs: true, - about: true, - }; - setHeaderNavModules(defaultModules); + function resetHeaderNavModules() { + setHeaderNavModules(DEFAULT_HEADER_NAV_MODULES); showSuccess(t('已重置为默认配置')); }- // 使用默认配置 - const defaultModules = { - home: true, - console: true, - pricing: { - enabled: true, - requireAuth: false - }, - docs: true, - about: true, - }; - setHeaderNavModules(defaultModules); + // 使用默认配置 + setHeaderNavModules(DEFAULT_HEADER_NAV_MODULES);Also applies to: 74-83, 140-149
46-60: Use functional setState to avoid stale closures during rapid togglesSafer under quick interactions.
- function handleHeaderNavModuleChange(moduleKey) { - return (checked) => { - const newModules = { ...headerNavModules }; + function handleHeaderNavModuleChange(moduleKey) { + return (checked) => { + const newModules = { ...headerNavModules }; if (moduleKey === 'pricing') { // 对于pricing模块,只更新enabled属性 newModules[moduleKey] = { ...newModules[moduleKey], enabled: checked }; } else { newModules[moduleKey] = checked; } - setHeaderNavModules(newModules); + setHeaderNavModules(() => newModules); }; }Alternatively, purely functional:
function handleHeaderNavModuleChange(moduleKey) { return (checked) => { - const newModules = { ...headerNavModules }; - if (moduleKey === 'pricing') { - newModules[moduleKey] = { ...newModules[moduleKey], enabled: checked }; - } else { - newModules[moduleKey] = checked; - } - setHeaderNavModules(() => newModules); + setHeaderNavModules(prev => { + if (moduleKey === 'pricing') { + return { ...prev, pricing: { ...prev.pricing, enabled: checked } }; + } + return { ...prev, [moduleKey]: checked }; + }); }; }
63-70: Functional update for pricing auth toggleMatch the pattern above to avoid stale state.
- function handlePricingAuthChange(checked) { - const newModules = { ...headerNavModules }; - newModules.pricing = { - ...newModules.pricing, - requireAuth: checked - }; - setHeaderNavModules(newModules); - } + function handlePricingAuthChange(checked) { + setHeaderNavModules(prev => ({ + ...prev, + pricing: { ...prev.pricing, requireAuth: checked }, + })); + }
116-121: Preserve original error for diagnosticsPassing a translated string to showError loses the original stack/response. Forward the actual error; UI will still show a friendly toast.
- } catch (error) { - showError(t('保存失败,请重试')); + } catch (error) { + showError(error);Optionally add a user-facing toast after logging if needed:
// showSuccess/Toast.info here if you still need a localized generic message
168-172: Copy tweak: reflect that login requirement is configurableThe description currently implies login is always required.
- description: t('模型定价,需要登录访问'), + description: t('模型定价,可配置是否需要登录访问'),web/src/hooks/common/useHeaderBar.js (2)
80-88: Be defensive when reading requireAuthGuard against non-boolean/undefined and coerce to boolean.
- const pricingRequireAuth = useMemo(() => { - if (headerNavModules?.pricing) { - return typeof headerNavModules.pricing === 'object' - ? headerNavModules.pricing.requireAuth - : false; // 默认不需要登录 - } - return false; // 默认不需要登录 - }, [headerNavModules]); + const pricingRequireAuth = useMemo(() => { + const p = headerNavModules?.pricing; + if (p && typeof p === 'object') return !!p.requireAuth; + return false; + }, [headerNavModules]);
54-79: Extract shared parser to avoid duplication with App.jsxBoth here and App.jsx parse HeaderNavModules and handle legacy booleans. Consider a helper (e.g., helpers/headerNavModules.js) exporting parseHeaderNavModules(configStr) and getPricingRequireAuth(modules).
I can draft the helper if you want it in this PR.
web/src/App.jsx (1)
59-79: De-duplicate pricingRequireAuth computationLogic here mirrors useHeaderBar. Prefer a shared helper or import from a central util to prevent drift.
Example:
- const pricingRequireAuth = useMemo(() => { - const headerNavModulesConfig = statusState?.status?.HeaderNavModules; - if (headerNavModulesConfig) { - try { - const modules = JSON.parse(headerNavModulesConfig); - if (typeof modules.pricing === 'boolean') { - return false; - } - return modules.pricing?.requireAuth === true; - } catch (error) { - console.error('解析顶栏模块配置失败:', error); - return false; - } - } - return false; - }, [statusState?.status?.HeaderNavModules]); + const pricingRequireAuth = useMemo(() => { + return getPricingRequireAuth(statusState?.status?.HeaderNavModules); + }, [statusState?.status?.HeaderNavModules]);Where getPricingRequireAuth comes from a new shared helper.
web/src/components/layout/SiderBar.jsx (2)
145-188: Avoid calling isAdmin()/isRoot() inside deps; compute once per renderCalling functions in dependency arrays is brittle and harder to reason about. Compute values, use them in items and deps.
- const adminItems = useMemo( - () => { + const admin = isAdmin(); + const root = isRoot(); + const adminItems = useMemo(() => { const items = [ { text: t('渠道管理'), itemKey: 'channel', to: '/channel', - className: isAdmin() ? '' : 'tableHiddle', + className: admin ? '' : 'tableHiddle', }, { text: t('模型管理'), itemKey: 'models', to: '/console/models', - className: isAdmin() ? '' : 'tableHiddle', + className: admin ? '' : 'tableHiddle', }, { text: t('兑换码管理'), itemKey: 'redemption', to: '/redemption', - className: isAdmin() ? '' : 'tableHiddle', + className: admin ? '' : 'tableHiddle', }, { text: t('用户管理'), itemKey: 'user', to: '/user', - className: isAdmin() ? '' : 'tableHiddle', + className: admin ? '' : 'tableHiddle', }, { text: t('系统设置'), itemKey: 'setting', to: '/setting', - className: isRoot() ? '' : 'tableHiddle', + className: root ? '' : 'tableHiddle', }, ]; // 根据配置过滤项目 const filteredItems = items.filter(item => { const configVisible = isModuleVisible('admin', item.itemKey); return configVisible; }); return filteredItems; - }, - [isAdmin(), isRoot(), t, isModuleVisible], - ); + }, [admin, root, t, isModuleVisible]);
101-109: localStorage in deps is non-reactive; consider caching readsDirect localStorage.getItem calls in dependency arrays won’t react to storage changes without a re-render from elsewhere. Cache into variables or state for clarity.
Example for workspaceItems:
- [ - localStorage.getItem('enable_data_export'), - localStorage.getItem('enable_drawing'), - localStorage.getItem('enable_task'), - t, - isModuleVisible, - ], + [t, isModuleVisible], // read localStorage inside the memo body once per renderOr lift them to useState/useEffect listening to the 'storage' event if you need cross-tab reactivity.
Also applies to: 118-141, 190-214
web/src/hooks/common/useSidebar.js (3)
59-70: Harden adminConfig parsing and shape validationIf SidebarModulesAdmin is invalid JSON or not an object, you silently fall back; also consider non-object cases. Add a simple type guard to avoid downstream surprises.
- const adminConfig = useMemo(() => { + const adminConfig = useMemo(() => { if (statusState?.status?.SidebarModulesAdmin) { try { - const config = JSON.parse(statusState.status.SidebarModulesAdmin); - return config; + const config = JSON.parse(statusState.status.SidebarModulesAdmin); + if (config && typeof config === 'object') return config; + return defaultAdminConfig; } catch (error) { return defaultAdminConfig; } } return defaultAdminConfig; }, [statusState?.status?.SidebarModulesAdmin]);
72-120: De-duplicate default user-config builderThe same “build defaultUserConfig” logic appears in try and catch. Factor it into a helper to reduce drift.
+ const buildDefaultUserConfig = (admin) => { + const cfg = {}; + Object.keys(admin).forEach(sectionKey => { + if (admin[sectionKey]?.enabled) { + cfg[sectionKey] = { enabled: true }; + Object.keys(admin[sectionKey]).forEach(moduleKey => { + if (moduleKey !== 'enabled' && admin[sectionKey][moduleKey]) { + cfg[sectionKey][moduleKey] = true; + } + }); + } + }); + return cfg; + }; @@ - } else { - // 当用户没有配置时,生成一个基于管理员配置的默认用户配置 - // 这样可以确保权限控制正确生效 - const defaultUserConfig = {}; - Object.keys(adminConfig).forEach(sectionKey => { - if (adminConfig[sectionKey]?.enabled) { - defaultUserConfig[sectionKey] = { enabled: true }; - // 为每个管理员允许的模块设置默认值为true - Object.keys(adminConfig[sectionKey]).forEach(moduleKey => { - if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) { - defaultUserConfig[sectionKey][moduleKey] = true; - } - }); - } - }); - setUserConfig(defaultUserConfig); - } + } else { + setUserConfig(buildDefaultUserConfig(adminConfig)); + } @@ - } catch (error) { - // 出错时也生成默认配置,而不是设置为空对象 - const defaultUserConfig = {}; - Object.keys(adminConfig).forEach(sectionKey => { - if (adminConfig[sectionKey]?.enabled) { - defaultUserConfig[sectionKey] = { enabled: true }; - Object.keys(adminConfig[sectionKey]).forEach(moduleKey => { - if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) { - defaultUserConfig[sectionKey][moduleKey] = true; - } - }); - } - }); - setUserConfig(defaultUserConfig); + } catch (error) { + setUserConfig(buildDefaultUserConfig(adminConfig));
129-135: Prevent setState on unmounted componentAdd a mounted flag to avoid state updates after unmount during slow API calls.
- useEffect(() => { - // 只有当管理员配置加载完成后才加载用户配置 - if (Object.keys(adminConfig).length > 0) { - loadUserConfig(); - } - }, [adminConfig]); + useEffect(() => { + let active = true; + const run = async () => { + if (Object.keys(adminConfig).length > 0) { + await loadUserConfig(); + } + }; + if (active) run(); + return () => { active = false; }; + }, [adminConfig]);web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (2)
341-347: Coerce Switch “checked” to booleanAvoid controlled/uncontrolled warnings when keys are absent.
- checked={sidebarModulesUser[section.key]?.enabled} + checked={!!sidebarModulesUser[section.key]?.enabled} @@ - checked={sidebarModulesUser[section.key]?.[module.key]} + checked={!!sidebarModulesUser[section.key]?.[module.key]}Also applies to: 369-375
152-178: Rename loading → saving for clarity“loading” here indicates a save-in-progress. Rename to avoid confusion with permissionsLoading.
- const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); @@ - setLoading(true); + setSaving(true); @@ - setLoading(false); + setSaving(false); @@ - loading={loading} + loading={saving}web/src/components/settings/personal/cards/NotificationSettings.jsx (3)
154-157: Guard adminConfig parseSame as above—protect JSON.parse with try/catch and type checks.
- if (statusState?.status?.SidebarModulesAdmin) { - const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); - setAdminConfig(adminConf); + if (statusState?.status?.SidebarModulesAdmin) { + try { + const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); + if (adminConf && typeof adminConf === 'object') setAdminConfig(adminConf); + } catch {} }
602-606: Coerce Switch “checked” props to booleansPrevents UI warnings when state keys are absent.
- checked={sidebarModulesUser[section.key]?.enabled} + checked={!!sidebarModulesUser[section.key]?.enabled} @@ - checked={sidebarModulesUser[section.key]?.[module.key]} + checked={!!sidebarModulesUser[section.key]?.[module.key]}Also applies to: 642-646
122-137: Avoid duplicated sidebar editor logicThis component reimplements the editor already added in SettingsSidebarModulesUser and logic now lives in useSidebar. Consider extracting a shared SidebarSettingsPanel or reuse the hook to DRY parsing, defaults, filtering, and saving.
Also applies to: 275-301, 539-658
controller/user.go (1)
505-566: generateDefaultSidebarConfig placementThis helper belongs in model/service to avoid duplication with model-side defaults; otherwise it’s dead code here. Consider moving or removing.
📜 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 by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (19)
controller/misc.go(1 hunks)controller/user.go(5 hunks)dto/user_settings.go(1 hunks)model/user.go(2 hunks)web/src/App.jsx(4 hunks)web/src/components/layout/HeaderBar/Navigation.jsx(2 hunks)web/src/components/layout/HeaderBar/index.jsx(3 hunks)web/src/components/layout/SiderBar.jsx(4 hunks)web/src/components/settings/OperationSetting.jsx(3 hunks)web/src/components/settings/PersonalSetting.jsx(2 hunks)web/src/components/settings/personal/cards/NotificationSettings.jsx(7 hunks)web/src/hooks/common/useHeaderBar.js(3 hunks)web/src/hooks/common/useNavigation.js(1 hunks)web/src/hooks/common/useSidebar.js(1 hunks)web/src/hooks/common/useUserPermissions.js(1 hunks)web/src/i18n/locales/en.json(1 hunks)web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx(1 hunks)web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx(1 hunks)web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
web/src/components/settings/OperationSetting.jsx (2)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
SettingsHeaderNavModules(28-326)web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)
SettingsSidebarModulesAdmin(28-362)
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (2)
web/src/hooks/common/useHeaderBar.js (1)
headerNavModules(58-78)web/src/helpers/utils.jsx (2)
showSuccess(153-155)showError(118-147)
controller/misc.go (1)
common/constants.go (1)
OptionMap(36-36)
web/src/components/layout/HeaderBar/Navigation.jsx (2)
web/src/App.jsx (1)
pricingRequireAuth(60-79)web/src/hooks/common/useHeaderBar.js (1)
pricingRequireAuth(81-88)
web/src/hooks/common/useUserPermissions.js (2)
web/src/hooks/common/useHeaderBar.js (1)
loading(42-42)web/src/hooks/common/useSidebar.js (1)
loading(27-27)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (2)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (5)
Typography(30-30)useTranslation(33-33)loading(34-34)statusState(35-35)sectionConfigs(250-304)web/src/helpers/utils.jsx (2)
showSuccess(153-155)showError(118-147)
web/src/hooks/common/useSidebar.js (1)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (4)
useSidebar(47-47)statusState(35-35)loading(34-34)adminConfig(112-112)
model/user.go (4)
common/constants.go (2)
RoleAdminUser(135-135)RoleRootUser(136-136)common/sys_log.go (1)
SysLog(10-13)dto/user_settings.go (1)
UserSetting(3-12)model/main.go (1)
DB(63-63)
web/src/components/settings/personal/cards/NotificationSettings.jsx (4)
web/src/App.jsx (1)
statusState(57-57)web/src/hooks/common/useSidebar.js (2)
statusState(25-25)adminConfig(60-70)web/src/hooks/common/useUserPermissions.js (6)
useUserPermissions(8-98)useUserPermissions(8-98)error(11-11)isSidebarSectionAllowed(45-49)isSidebarModuleAllowed(52-63)hasSidebarSettingsPermission(40-42)web/src/helpers/utils.jsx (2)
showSuccess(153-155)showError(118-147)
web/src/App.jsx (2)
web/src/hooks/common/useHeaderBar.js (3)
statusState(34-34)pricingRequireAuth(81-88)headerNavModulesConfig(55-55)web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
statusState(31-31)
web/src/hooks/common/useNavigation.js (3)
web/src/components/layout/HeaderBar/index.jsx (1)
useNavigation(65-65)web/src/hooks/common/useHeaderBar.js (2)
docsLink(51-51)headerNavModules(58-78)web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
headerNavModules(34-43)
web/src/hooks/common/useHeaderBar.js (2)
web/src/App.jsx (2)
statusState(57-57)pricingRequireAuth(60-79)web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (2)
statusState(31-31)headerNavModules(34-43)
controller/user.go (5)
common/constants.go (3)
RoleCommonUser(134-134)RoleRootUser(136-136)RoleAdminUser(135-135)common/json.go (2)
Marshal(20-22)Unmarshal(8-10)common/sys_log.go (1)
SysLog(10-13)model/user.go (2)
GetUserById(287-299)User(19-48)common/gin.go (1)
ApiError(95-100)
web/src/components/layout/SiderBar.jsx (3)
web/src/hooks/common/useSidebar.js (4)
useSidebar(24-220)useSidebar(24-220)isModuleVisible(183-189)hasSectionVisibleModules(192-199)web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (1)
useSidebar(47-47)web/src/helpers/utils.jsx (2)
isAdmin(35-40)isRoot(42-47)
web/src/components/layout/HeaderBar/index.jsx (4)
web/src/hooks/common/useNavigation.js (2)
useNavigation(22-88)useNavigation(22-88)web/src/hooks/common/useHeaderBar.js (3)
docsLink(51-51)headerNavModules(58-78)pricingRequireAuth(81-88)web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
headerNavModules(34-43)web/src/App.jsx (1)
pricingRequireAuth(60-79)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx (3)
web/src/hooks/common/useSidebar.js (6)
loading(27-27)statusState(25-25)useSidebar(24-220)useSidebar(24-220)adminConfig(60-70)refreshUserConfig(123-127)web/src/hooks/common/useUserPermissions.js (7)
loading(10-10)useUserPermissions(8-98)useUserPermissions(8-98)hasSidebarSettingsPermission(40-42)isSidebarSectionAllowed(45-49)isSidebarModuleAllowed(52-63)error(11-11)web/src/helpers/utils.jsx (2)
showSuccess(153-155)showError(118-147)
🪛 Biome (2.1.2)
web/src/i18n/locales/en.json
[error] 2064-2064: The key 操练场 was already declared.
This where a duplicated key was declared again.
If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.
(lint/suspicious/noDuplicateObjectKeys)
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx
[error] 120-120: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
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)
[error] 124-124: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
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)
[error] 197-197: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
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)
🔇 Additional comments (13)
web/src/i18n/locales/en.json (1)
2021-2078: Ignore duplicate‐keys suggestion – a scan of web/src/i18n/locales/en.json shows no repeated keys; the linter error no longer applies.Likely an incorrect or invalid review comment.
web/src/components/settings/PersonalSetting.jsx (1)
41-42: Whitespace-only changes are fine.No functional impact. LGTM.
Also applies to: 335-336
web/src/components/settings/OperationSetting.jsx (2)
23-24: LGTM: new settings panels imported correctly
51-56: LGTM: state keys for new options are wired
Values come from /api/option; empty-string defaults are acceptable since children parse JSON.web/src/components/layout/HeaderBar/index.jsx (3)
47-49: LGTM: header config and auth flag plumbed from hook
65-65: LGTM: useNavigation now correctly receives headerNavModules
107-107: LGTM: pricingRequireAuth forwarded to Navigationweb/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx (1)
100-107: Dispatch payload shape matches reducer contract
ThestatusReducer’s'set'case appliesaction.payloaddirectly tostate.status, so passing the flattened status object is correct.web/src/App.jsx (1)
278-291: Clarify behavior when pricing module is disabledCurrently, the /pricing route remains accessible even if modules.pricing.enabled is false (only the header link hides). Confirm product intent: should the route also be disabled/redirected when admin turns it off?
If it should be blocked, I can provide a small guard that redirects to 404 or Home when disabled.
web/src/components/layout/SiderBar.jsx (1)
258-261: Good: consistent error surface for chat parsingSwitching to showError('聊天数据解析失败') keeps UX consistent and avoids noisy console logs.
controller/user.go (3)
213-214: Explicit default role assignment on Register looks goodSetting Role to common.RoleCommonUser during registration is correct and prevents privilege escalation at sign-up.
428-473: GetSelf: include permissions and sidebar_modules — LGTMThe response shape aligns with frontend needs and hides remark. Keep it stable for clients.
856-861: CreateUser preserves role — LGTMRespects admin-provided role with upstream checks already in place.
| // 模块管理配置 | ||
| "HeaderNavModules": common.OptionMap["HeaderNavModules"], | ||
| "SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"], | ||
|
|
There was a problem hiding this comment.
Data race on common.OptionMap reads in GetStatus.
OptionMap is a map guarded elsewhere with OptionMapRWMutex; reading it here without the lock can race with writers.
Apply:
- // 模块管理配置
- "HeaderNavModules": common.OptionMap["HeaderNavModules"],
- "SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"],
+ // 模块管理配置(下方加锁后设置)Then, after data is constructed:
}
+ // 模块管理配置(加读锁)
+ common.OptionMapRWMutex.RLock()
+ data["HeaderNavModules"] = common.OptionMap["HeaderNavModules"]
+ data["SidebarModulesAdmin"] = common.OptionMap["SidebarModulesAdmin"]
+ common.OptionMapRWMutex.RUnlock()
+
// 根据启用状态注入可选内容🤖 Prompt for AI Agents
In controller/misc.go around lines 92-95, reads of common.OptionMap
(HeaderNavModules and SidebarModulesAdmin) are unprotected and can race with
writers; wrap the reads with common.OptionMapRWMutex.RLock() before accessing
OptionMap and RUnlock() afterwards, either by copying the needed values into
local variables under the read-lock or by populating the response map entries
while the lock is held, then release the lock once those values have been
captured/inserted.
| var requestData map[string]interface{} | ||
| err := json.NewDecoder(c.Request.Body).Decode(&requestData) | ||
| if err != nil { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": "无效的参数", | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| // 检查是否是sidebar_modules更新请求 | ||
| if sidebarModules, exists := requestData["sidebar_modules"]; exists { | ||
| userId := c.GetInt("id") | ||
| user, err := model.GetUserById(userId, false) | ||
| if err != nil { | ||
| common.ApiError(c, err) | ||
| return | ||
| } | ||
|
|
||
| // 获取当前用户设置 | ||
| currentSetting := user.GetSetting() | ||
|
|
||
| // 更新sidebar_modules字段 | ||
| if sidebarModulesStr, ok := sidebarModules.(string); ok { | ||
| currentSetting.SidebarModules = sidebarModulesStr | ||
| } | ||
|
|
||
| // 保存更新后的设置 | ||
| user.SetSetting(currentSetting) | ||
| if err := user.Update(false); err != nil { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": "更新设置失败: " + err.Error(), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "message": "设置更新成功", | ||
| }) | ||
| return | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
UpdateSelf lacks server-side authorization and input validation for sidebar_modules
Anyone can POST sidebar_modules regardless of role; also raw strings are accepted without JSON validation. Enforce RBAC and validate JSON; accept object input too.
func UpdateSelf(c *gin.Context) {
var requestData map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&requestData)
@@
// 检查是否是sidebar_modules更新请求
if sidebarModules, exists := requestData["sidebar_modules"]; exists {
- userId := c.GetInt("id")
+ userId := c.GetInt("id")
+ userRole := c.GetInt("role")
+ // 基于后端权限控制,避免绕过前端限制
+ perms := calculateUserPermissions(userRole)
+ if allow, ok := perms["sidebar_settings"].(bool); !ok || !allow {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "无权更新边栏设置",
+ })
+ return
+ }
user, err := model.GetUserById(userId, false)
@@
- // 更新sidebar_modules字段
- if sidebarModulesStr, ok := sidebarModules.(string); ok {
- currentSetting.SidebarModules = sidebarModulesStr
- }
+ // 更新sidebar_modules字段(支持字符串或对象)
+ switch v := sidebarModules.(type) {
+ case string:
+ // 验证JSON格式
+ var tmp map[string]interface{}
+ if err := json.Unmarshal([]byte(v), &tmp); err != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "message": "无效的边栏配置"})
+ return
+ }
+ currentSetting.SidebarModules = v
+ case map[string]interface{}:
+ // 统一存为字符串
+ b, _ := json.Marshal(v)
+ currentSetting.SidebarModules = string(b)
+ default:
+ c.JSON(http.StatusOK, gin.H{"success": false, "message": "无效的边栏配置类型"})
+ return
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In controller/user.go around lines 655-697, enforce server-side RBAC and
stricter JSON validation for sidebar_modules: before loading the target user,
verify the requester is authorized (e.g., requester ID equals target ID OR
requester has an admin role/permission via c.GetInt("id") vs target id and
c.GetString("role") or model permission check) and return 403 if not allowed;
then accept sidebar_modules as either a JSON string or an object/array — if it's
a string attempt to json.Unmarshal to a concrete structure (e.g., []string or
map[string]bool) to validate contents, if it's an object/array validate its
shape and items, sanitize/whitelist allowed module keys, enforce size limits,
then json.Marshal the validated structure to store as the setting string; on any
validation error return 400 with an explanatory message and do not update,
otherwise set the new setting and persist as before.
| // 用户创建成功后,根据角色初始化边栏配置 | ||
| // 需要重新获取用户以确保有正确的ID和Role | ||
| var createdUser User | ||
| if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil { | ||
| // 生成基于角色的默认边栏配置 | ||
| defaultSidebarConfig := generateDefaultSidebarConfigForRole(createdUser.Role) | ||
| if defaultSidebarConfig != "" { | ||
| currentSetting := createdUser.GetSetting() | ||
| currentSetting.SidebarModules = defaultSidebarConfig | ||
| createdUser.SetSetting(currentSetting) | ||
| createdUser.Update(false) | ||
| common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role)) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid re-query by username; update by ID and handle errors
Re-fetching by username is unnecessary and error-prone. Also, errors from Update are ignored.
- // 用户创建成功后,根据角色初始化边栏配置
- // 需要重新获取用户以确保有正确的ID和Role
- var createdUser User
- if err := DB.Where("username = ?", user.Username).First(&createdUser).Error; err == nil {
- // 生成基于角色的默认边栏配置
- defaultSidebarConfig := generateDefaultSidebarConfigForRole(createdUser.Role)
- if defaultSidebarConfig != "" {
- currentSetting := createdUser.GetSetting()
- currentSetting.SidebarModules = defaultSidebarConfig
- createdUser.SetSetting(currentSetting)
- createdUser.Update(false)
- common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", createdUser.Username, createdUser.Role))
- }
- }
+ // 用户创建成功后,根据角色初始化边栏配置(使用已获取的 user.Id / user.Role)
+ if cfg := generateDefaultSidebarConfigForRole(user.Role); cfg != nil {
+ current := user.GetSetting()
+ current.SidebarModules = cfg // json.RawMessage if adopted; else keep string
+ user.SetSetting(current)
+ if err := DB.Model(&User{}).Where("id = ?", user.Id).Update("setting", user.Setting).Error; err != nil {
+ common.SysLog("初始化边栏配置失败: " + err.Error())
+ } else {
+ common.SysLog(fmt.Sprintf("为新用户 %s (角色: %d) 初始化边栏配置", user.Username, user.Role))
+ }
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In model/user.go around lines 398 to 411, remove the re-query by username and
instead load/update the record by the new user's ID (e.g.,
DB.First(&createdUser, user.ID) or use the created user instance that has the
ID), set the role-based SidebarModules on that record, call Update and check its
returned error (handle/log/return it instead of ignoring), and ensure DB
lookup/update use the primary key to avoid ambiguity and race conditions.
| if (statusState?.status?.SidebarModulesAdmin) { | ||
| const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); | ||
| setAdminConfig(adminConf); | ||
| } | ||
|
|
||
| // 获取用户个人配置 | ||
| const userRes = await API.get('/api/user/self'); | ||
| if (userRes.data.success && userRes.data.data.sidebar_modules) { | ||
| const userConf = JSON.parse(userRes.data.data.sidebar_modules); | ||
| setSidebarModulesUser(userConf); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Handle non-string sidebar_modules and parse failures
APIs may return an object or malformed JSON. Add guards to avoid runtime crashes.
- if (userRes.data.success && userRes.data.data.sidebar_modules) {
- const userConf = JSON.parse(userRes.data.data.sidebar_modules);
- setSidebarModulesUser(userConf);
+ if (userRes.data.success && userRes.data.data.sidebar_modules) {
+ let userConf = userRes.data.data.sidebar_modules;
+ try {
+ if (typeof userConf === 'string') userConf = JSON.parse(userConf);
+ if (userConf && typeof userConf === 'object') setSidebarModulesUser(userConf);
+ } catch (e) {
+ console.warn('用户边栏配置解析失败,保留默认:', e);
+ }
}Also applies to: 160-164
🤖 Prompt for AI Agents
In web/src/components/settings/personal/cards/NotificationSettings.jsx around
lines 154 to 164, the code blindly calls JSON.parse on
statusState.status.SidebarModulesAdmin and userRes.data.data.sidebar_modules
which can be non-strings or malformed JSON; update the logic to (1) check typeof
value === 'string' before parsing, (2) if the value is already an object use it
directly, (3) wrap JSON.parse in try/catch to handle parse failures and fall
back to a safe default (e.g., {} or skip setting), and (4) verify the resulting
value is an object before calling setAdminConfig or setSidebarModulesUser so
runtime crashes are avoided.
| if (link.itemKey === 'pricing') { | ||
| // 支持新的pricing配置格式 | ||
| return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing; | ||
| } | ||
| return modules[link.itemKey] === true; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Harden pricing check to avoid null-dereference
typeof null === 'object'. If pricing is null, reading .enabled throws. Guard for truthiness and coerce to boolean.
- if (link.itemKey === 'pricing') {
- // 支持新的pricing配置格式
- return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing;
- }
+ if (link.itemKey === 'pricing') {
+ // 支持新的pricing配置格式并防御 null
+ return modules.pricing && typeof modules.pricing === 'object'
+ ? !!modules.pricing.enabled
+ : !!modules.pricing;
+ }📝 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.
| if (link.itemKey === 'pricing') { | |
| // 支持新的pricing配置格式 | |
| return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing; | |
| } | |
| return modules[link.itemKey] === true; | |
| if (link.itemKey === 'pricing') { | |
| // 支持新的pricing配置格式并防御 null | |
| return modules.pricing && typeof modules.pricing === 'object' | |
| ? !!modules.pricing.enabled | |
| : !!modules.pricing; | |
| } | |
| return modules[link.itemKey] === true; |
🤖 Prompt for AI Agents
In web/src/hooks/common/useNavigation.js around lines 75 to 79, the pricing
check uses typeof modules.pricing === 'object' which treats null as 'object' and
leads to a null-dereference when accessing .enabled; change the guard to first
ensure modules.pricing is truthy (e.g., modules.pricing && typeof
modules.pricing === 'object') and then return the boolean value (coerce enabled
or the pricing value to a true/false) so null/undefined are handled safely.
| const isSidebarSectionAllowed = (sectionKey) => { | ||
| if (!permissions?.sidebar_modules) return true; | ||
| const sectionPerms = permissions.sidebar_modules[sectionKey]; | ||
| return sectionPerms !== false; | ||
| }; |
There was a problem hiding this comment.
Section “enabled” flag is ignored → modules/sections can render when disabled.
Admin config uses { enabled: false, ... }, but checks here only compare sectionPerms === false. Respect the enabled flag at both section and module levels.
Apply:
- const isSidebarSectionAllowed = (sectionKey) => {
- if (!permissions?.sidebar_modules) return true;
- const sectionPerms = permissions.sidebar_modules[sectionKey];
- return sectionPerms !== false;
- };
+ const isSidebarSectionAllowed = (sectionKey) => {
+ if (!permissions?.sidebar_modules) return true;
+ const sectionPerms = permissions.sidebar_modules[sectionKey];
+ if (sectionPerms === false) return false;
+ if (sectionPerms && typeof sectionPerms === 'object' && sectionPerms.enabled === false) return false;
+ return true;
+ };
...
- const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
+ const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
if (!permissions?.sidebar_modules) return true;
const sectionPerms = permissions.sidebar_modules[sectionKey];
-
- // 如果整个区域被禁用
- if (sectionPerms === false) return false;
+ // 如果整个区域被禁用
+ if (sectionPerms === false) return false;
+ if (sectionPerms && typeof sectionPerms === 'object' && sectionPerms.enabled === false) return false;
// 如果区域存在但模块被禁用
if (sectionPerms && sectionPerms[moduleKey] === false) return false;
return true;
};
...
- const getAllowedSidebarSections = () => {
- if (!permissions?.sidebar_modules) return [];
-
- return Object.keys(permissions.sidebar_modules).filter(sectionKey =>
- isSidebarSectionAllowed(sectionKey)
- );
- };
+ const getAllowedSidebarSections = () => {
+ if (!permissions?.sidebar_modules) return [];
+ return Object.keys(permissions.sidebar_modules).filter(isSidebarSectionAllowed);
+ };
...
- const getAllowedSidebarModules = (sectionKey) => {
+ const getAllowedSidebarModules = (sectionKey) => {
if (!permissions?.sidebar_modules) return [];
const sectionPerms = permissions.sidebar_modules[sectionKey];
- if (sectionPerms === false) return [];
+ if (sectionPerms === false) return [];
+ if (sectionPerms && typeof sectionPerms === 'object' && sectionPerms.enabled === false) return [];
if (!sectionPerms || typeof sectionPerms !== 'object') return [];
return Object.keys(sectionPerms).filter(moduleKey =>
moduleKey !== 'enabled' && sectionPerms[moduleKey] === true
);
};Also applies to: 52-63, 66-72, 75-85
🤖 Prompt for AI Agents
In web/src/hooks/common/useUserPermissions.js around lines 45-49 (and also apply
the same fix to 52-63, 66-72, 75-85), the code currently treats a section as
disabled only when the value is strictly false and ignores objects like {
enabled: false, ... }. Update each permission check to treat a section/module as
disallowed when the permission entry is strictly false OR when it's an object
with enabled === false (e.g., sectionPerms === false || sectionPerms?.enabled
=== false), and preserve the current behavior of allowing access when the
permission entry is undefined; apply this same enabled check at both section and
module levels wherever similar logic appears in the referenced line ranges.
| // 如果没有边栏设置权限,不显示此组件 | ||
| if (!permissionsLoading && !hasSidebarSettingsPermission()) { | ||
| return null; | ||
| } | ||
|
|
||
| // 权限加载中,显示加载状态 | ||
| if (permissionsLoading) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Fix conditional hook order violations (early returns before hooks)
Hooks must run unconditionally. Move these early returns below all hooks (after useState/useEffect) or gate inside render.
- // 如果没有边栏设置权限,不显示此组件
- if (!permissionsLoading && !hasSidebarSettingsPermission()) {
- return null;
- }
-
- // 权限加载中,显示加载状态
- if (permissionsLoading) {
- return null;
- }
+ // ... keep all hooks above ...
+ // Defer early returns until after all hooks are declared
+ const shouldHide = permissionsLoading || !hasSidebarSettingsPermission();
+ if (shouldHide) return null;📝 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.
| // 如果没有边栏设置权限,不显示此组件 | |
| if (!permissionsLoading && !hasSidebarSettingsPermission()) { | |
| return null; | |
| } | |
| // 权限加载中,显示加载状态 | |
| if (permissionsLoading) { | |
| return null; | |
| } | |
| // …keep all hooks above… | |
| // Defer early returns until after all hooks are declared | |
| const shouldHide = permissionsLoading || !hasSidebarSettingsPermission(); | |
| if (shouldHide) return null; |
🤖 Prompt for AI Agents
In web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx around lines
49-57, the current early returns for permission/loading state run before React
hooks causing hook-order violations; move these conditional returns so all hooks
(useState/useEffect/useMemo/useContext etc.) are called first, then check
permissionsLoading and hasSidebarSettingsPermission() to return null after hooks
have executed, or alternatively keep the returns but wrap them inside the
component's render JSX (e.g., conditional rendering) rather than exiting before
hooks.
| useEffect(() => { | ||
| const loadConfigs = async () => { | ||
| try { | ||
| // 获取管理员全局配置 | ||
| if (statusState?.status?.SidebarModulesAdmin) { | ||
| const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); | ||
| setAdminConfig(adminConf); | ||
| console.log('加载管理员边栏配置:', adminConf); | ||
| } | ||
|
|
||
| // 获取用户个人配置 | ||
| const userRes = await API.get('/api/user/self'); | ||
| if (userRes.data.success && userRes.data.data.sidebar_modules) { | ||
| let userConf; | ||
| // 检查sidebar_modules是字符串还是对象 | ||
| if (typeof userRes.data.data.sidebar_modules === 'string') { | ||
| userConf = JSON.parse(userRes.data.data.sidebar_modules); | ||
| } else { | ||
| userConf = userRes.data.data.sidebar_modules; | ||
| } | ||
| console.log('从API加载的用户配置:', userConf); | ||
|
|
||
| // 确保用户配置也经过权限过滤 | ||
| const filteredUserConf = {}; | ||
| Object.keys(userConf).forEach(sectionKey => { | ||
| if (isSidebarSectionAllowed(sectionKey)) { | ||
| filteredUserConf[sectionKey] = { ...userConf[sectionKey] }; | ||
| // 过滤不允许的模块 | ||
| Object.keys(userConf[sectionKey]).forEach(moduleKey => { | ||
| if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) { | ||
| delete filteredUserConf[sectionKey][moduleKey]; | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| setSidebarModulesUser(filteredUserConf); | ||
| console.log('权限过滤后的用户配置:', filteredUserConf); | ||
| } else { | ||
| // 如果用户没有配置,使用权限过滤后的默认配置 | ||
| const defaultConfig = generateDefaultConfig(); | ||
| setSidebarModulesUser(defaultConfig); | ||
| console.log('用户无配置,使用默认配置:', defaultConfig); | ||
| } | ||
| } catch (error) { | ||
| console.error('加载边栏配置失败:', error); | ||
| // 出错时也使用默认配置 | ||
| const defaultConfig = generateDefaultConfig(); | ||
| setSidebarModulesUser(defaultConfig); | ||
| } | ||
| }; | ||
|
|
||
| // 只有权限加载完成且有边栏设置权限时才加载配置 | ||
| if (!permissionsLoading && hasSidebarSettingsPermission()) { | ||
| loadConfigs(); | ||
| } | ||
| }, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Effect re-renders due to unstable function deps
Depending on function refs from useUserPermissions causes reload loops. Depend on stable values (permissions) or memoize functions in the hook.
- }, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]);
+ }, [statusState, permissionsLoading, permissions]);Optionally, memoize functions inside useUserPermissions with useCallback for stability.
📝 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.
| useEffect(() => { | |
| const loadConfigs = async () => { | |
| try { | |
| // 获取管理员全局配置 | |
| if (statusState?.status?.SidebarModulesAdmin) { | |
| const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); | |
| setAdminConfig(adminConf); | |
| console.log('加载管理员边栏配置:', adminConf); | |
| } | |
| // 获取用户个人配置 | |
| const userRes = await API.get('/api/user/self'); | |
| if (userRes.data.success && userRes.data.data.sidebar_modules) { | |
| let userConf; | |
| // 检查sidebar_modules是字符串还是对象 | |
| if (typeof userRes.data.data.sidebar_modules === 'string') { | |
| userConf = JSON.parse(userRes.data.data.sidebar_modules); | |
| } else { | |
| userConf = userRes.data.data.sidebar_modules; | |
| } | |
| console.log('从API加载的用户配置:', userConf); | |
| // 确保用户配置也经过权限过滤 | |
| const filteredUserConf = {}; | |
| Object.keys(userConf).forEach(sectionKey => { | |
| if (isSidebarSectionAllowed(sectionKey)) { | |
| filteredUserConf[sectionKey] = { ...userConf[sectionKey] }; | |
| // 过滤不允许的模块 | |
| Object.keys(userConf[sectionKey]).forEach(moduleKey => { | |
| if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) { | |
| delete filteredUserConf[sectionKey][moduleKey]; | |
| } | |
| }); | |
| } | |
| }); | |
| setSidebarModulesUser(filteredUserConf); | |
| console.log('权限过滤后的用户配置:', filteredUserConf); | |
| } else { | |
| // 如果用户没有配置,使用权限过滤后的默认配置 | |
| const defaultConfig = generateDefaultConfig(); | |
| setSidebarModulesUser(defaultConfig); | |
| console.log('用户无配置,使用默认配置:', defaultConfig); | |
| } | |
| } catch (error) { | |
| console.error('加载边栏配置失败:', error); | |
| // 出错时也使用默认配置 | |
| const defaultConfig = generateDefaultConfig(); | |
| setSidebarModulesUser(defaultConfig); | |
| } | |
| }; | |
| // 只有权限加载完成且有边栏设置权限时才加载配置 | |
| if (!permissionsLoading && hasSidebarSettingsPermission()) { | |
| loadConfigs(); | |
| } | |
| }, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]); | |
| useEffect(() => { | |
| const loadConfigs = async () => { | |
| try { | |
| // 获取管理员全局配置 | |
| if (statusState?.status?.SidebarModulesAdmin) { | |
| const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); | |
| setAdminConfig(adminConf); | |
| console.log('加载管理员边栏配置:', adminConf); | |
| } | |
| // 获取用户个人配置 | |
| const userRes = await API.get('/api/user/self'); | |
| if (userRes.data.success && userRes.data.data.sidebar_modules) { | |
| let userConf; | |
| // 检查sidebar_modules是字符串还是对象 | |
| if (typeof userRes.data.data.sidebar_modules === 'string') { | |
| userConf = JSON.parse(userRes.data.data.sidebar_modules); | |
| } else { | |
| userConf = userRes.data.data.sidebar_modules; | |
| } | |
| console.log('从API加载的用户配置:', userConf); | |
| // 确保用户配置也经过权限过滤 | |
| const filteredUserConf = {}; | |
| Object.keys(userConf).forEach(sectionKey => { | |
| if (isSidebarSectionAllowed(sectionKey)) { | |
| filteredUserConf[sectionKey] = { ...userConf[sectionKey] }; | |
| // 过滤不允许的模块 | |
| Object.keys(userConf[sectionKey]).forEach(moduleKey => { | |
| if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) { | |
| delete filteredUserConf[sectionKey][moduleKey]; | |
| } | |
| }); | |
| } | |
| }); | |
| setSidebarModulesUser(filteredUserConf); | |
| console.log('权限过滤后的用户配置:', filteredUserConf); | |
| } else { | |
| // 如果用户没有配置,使用权限过滤后的默认配置 | |
| const defaultConfig = generateDefaultConfig(); | |
| setSidebarModulesUser(defaultConfig); | |
| console.log('用户无配置,使用默认配置:', defaultConfig); | |
| } | |
| } catch (error) { | |
| console.error('加载边栏配置失败:', error); | |
| // 出错时也使用默认配置 | |
| const defaultConfig = generateDefaultConfig(); | |
| setSidebarModulesUser(defaultConfig); | |
| } | |
| }; | |
| // 只有权限加载完成且有边栏设置权限时才加载配置 | |
| if (!permissionsLoading && hasSidebarSettingsPermission()) { | |
| loadConfigs(); | |
| } | |
| }, [statusState, permissionsLoading, permissions]); |
🧰 Tools
🪛 Biome (2.1.2)
[error] 197-197: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
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 web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx around lines
181-236, the useEffect depends on unstable function references
isSidebarSectionAllowed and isSidebarModuleAllowed which causes re-renders;
replace these function references in the dependency array with stable values
derived from permissions (e.g., a memoized permissions object/array or specific
permission booleans) or ensure those functions are memoized at their source
(inside useUserPermissions) with useCallback so their identity is stable; update
the effect deps to only include statusState, permissionsLoading,
hasSidebarSettingsPermission and the stable memoized permission values (or
memoized functions), and/or memoize any computed default/filtered configs used
inside the effect to prevent reload loops.
| if (statusState?.status?.SidebarModulesAdmin) { | ||
| const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); | ||
| setAdminConfig(adminConf); | ||
| console.log('加载管理员边栏配置:', adminConf); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Guard JSON.parse for adminConfig
A malformed SidebarModulesAdmin will crash the page. Wrap in try/catch with fallback.
- if (statusState?.status?.SidebarModulesAdmin) {
- const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
- setAdminConfig(adminConf);
- console.log('加载管理员边栏配置:', adminConf);
+ if (statusState?.status?.SidebarModulesAdmin) {
+ try {
+ const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
+ if (adminConf && typeof adminConf === 'object') {
+ setAdminConfig(adminConf);
+ console.log('加载管理员边栏配置:', adminConf);
+ }
+ } catch (e) {
+ console.warn('管理员边栏配置解析失败,使用空配置:', e);
+ setAdminConfig({});
+ }
}📝 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.
| if (statusState?.status?.SidebarModulesAdmin) { | |
| const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); | |
| setAdminConfig(adminConf); | |
| console.log('加载管理员边栏配置:', adminConf); | |
| if (statusState?.status?.SidebarModulesAdmin) { | |
| try { | |
| const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); | |
| if (adminConf && typeof adminConf === 'object') { | |
| setAdminConfig(adminConf); | |
| console.log('加载管理员边栏配置:', adminConf); | |
| } | |
| } catch (e) { | |
| console.warn('管理员边栏配置解析失败,使用空配置:', e); | |
| setAdminConfig({}); | |
| } | |
| } |
🤖 Prompt for AI Agents
In web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx around lines
185 to 188, the code directly JSON.parse's
statusState.status.SidebarModulesAdmin which will throw on malformed JSON; wrap
the parse in a try/catch, on success call setAdminConfig with the parsed object
and keep the console.log, on failure setAdminConfig to a safe fallback (e.g., {}
or existing default) and optionally log a warning/error with the raw value;
ensure you only call JSON.parse if the value is a non-empty string and avoid
letting the exception bubble up.
新增设置 "顶栏"、"侧边栏"管理功能,提高用户体验
Summary by CodeRabbit