From 8809c44443af90a518b720cd13681a79d5ae6477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E3=80=82?= Date: Sun, 31 Aug 2025 07:07:40 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B6=E6=A0=8F=E5=92=8C=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加用户体验 --- controller/misc.go | 4 + controller/user.go | 185 +++++++- dto/user_settings.go | 1 + model/user.go | 86 ++++ web/src/App.jsx | 40 +- .../layout/HeaderBar/Navigation.jsx | 5 +- web/src/components/layout/HeaderBar/index.jsx | 5 +- web/src/components/layout/SiderBar.jsx | 286 ++++++++----- .../components/settings/OperationSetting.jsx | 16 + .../components/settings/PersonalSetting.jsx | 4 + .../personal/cards/NotificationSettings.jsx | 360 +++++++++++++++- web/src/hooks/common/useHeaderBar.js | 40 +- web/src/hooks/common/useNavigation.js | 94 ++-- web/src/hooks/common/useSidebar.js | 220 ++++++++++ web/src/hooks/common/useUserPermissions.js | 100 +++++ web/src/i18n/locales/en.json | 60 ++- .../Operation/SettingsHeaderNavModules.jsx | 326 ++++++++++++++ .../Operation/SettingsSidebarModulesAdmin.jsx | 362 ++++++++++++++++ .../Personal/SettingsSidebarModulesUser.jsx | 404 ++++++++++++++++++ 19 files changed, 2428 insertions(+), 170 deletions(-) create mode 100644 web/src/hooks/common/useSidebar.js create mode 100644 web/src/hooks/common/useUserPermissions.js create mode 100644 web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx create mode 100644 web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx create mode 100644 web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx diff --git a/controller/misc.go b/controller/misc.go index f30ab8c79..dfe3091b5 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -89,6 +89,10 @@ func GetStatus(c *gin.Context) { "announcements_enabled": cs.AnnouncementsEnabled, "faq_enabled": cs.FAQEnabled, + // 模块管理配置 + "HeaderNavModules": common.OptionMap["HeaderNavModules"], + "SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"], + "oidc_enabled": system_setting.GetOIDCSettings().Enabled, "oidc_client_id": system_setting.GetOIDCSettings().ClientId, "oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint, diff --git a/controller/user.go b/controller/user.go index c9795c0cb..91fdce4cf 100644 --- a/controller/user.go +++ b/controller/user.go @@ -210,6 +210,7 @@ func Register(c *gin.Context) { Password: user.Password, DisplayName: user.Username, InviterId: inviterId, + Role: common.RoleCommonUser, // 明确设置角色为普通用户 } if common.EmailVerificationEnabled { cleanUser.Email = user.Email @@ -426,6 +427,7 @@ func GetAffCode(c *gin.Context) { func GetSelf(c *gin.Context) { id := c.GetInt("id") + userRole := c.GetInt("role") user, err := model.GetUserById(id, false) if err != nil { common.ApiError(c, err) @@ -434,14 +436,136 @@ func GetSelf(c *gin.Context) { // Hide admin remarks: set to empty to trigger omitempty tag, ensuring the remark field is not included in JSON returned to regular users user.Remark = "" + // 计算用户权限信息 + permissions := calculateUserPermissions(userRole) + + // 获取用户设置并提取sidebar_modules + userSetting := user.GetSetting() + + // 构建响应数据,包含用户信息和权限 + responseData := map[string]interface{}{ + "id": user.Id, + "username": user.Username, + "display_name": user.DisplayName, + "role": user.Role, + "status": user.Status, + "email": user.Email, + "group": user.Group, + "quota": user.Quota, + "used_quota": user.UsedQuota, + "request_count": user.RequestCount, + "aff_code": user.AffCode, + "aff_count": user.AffCount, + "aff_quota": user.AffQuota, + "aff_history_quota": user.AffHistoryQuota, + "inviter_id": user.InviterId, + "linux_do_id": user.LinuxDOId, + "setting": user.Setting, + "stripe_customer": user.StripeCustomer, + "sidebar_modules": userSetting.SidebarModules, // 正确提取sidebar_modules字段 + "permissions": permissions, // 新增权限字段 + } + c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", - "data": user, + "data": responseData, }) return } +// 计算用户权限的辅助函数 +func calculateUserPermissions(userRole int) map[string]interface{} { + permissions := map[string]interface{}{} + + // 根据用户角色计算权限 + if userRole == common.RoleRootUser { + // 超级管理员不需要边栏设置功能 + permissions["sidebar_settings"] = false + permissions["sidebar_modules"] = map[string]interface{}{} + } else if userRole == common.RoleAdminUser { + // 管理员可以设置边栏,但不包含系统设置功能 + permissions["sidebar_settings"] = true + permissions["sidebar_modules"] = map[string]interface{}{ + "admin": map[string]interface{}{ + "setting": false, // 管理员不能访问系统设置 + }, + } + } else { + // 普通用户只能设置个人功能,不包含管理员区域 + permissions["sidebar_settings"] = true + permissions["sidebar_modules"] = map[string]interface{}{ + "admin": false, // 普通用户不能访问管理员区域 + } + } + + return permissions +} + +// 根据用户角色生成默认的边栏配置 +func generateDefaultSidebarConfig(userRole int) string { + defaultConfig := map[string]interface{}{} + + // 聊天区域 - 所有用户都可以访问 + defaultConfig["chat"] = map[string]interface{}{ + "enabled": true, + "playground": true, + "chat": true, + } + + // 控制台区域 - 所有用户都可以访问 + defaultConfig["console"] = map[string]interface{}{ + "enabled": true, + "detail": true, + "token": true, + "log": true, + "midjourney": true, + "task": true, + } + + // 个人中心区域 - 所有用户都可以访问 + defaultConfig["personal"] = map[string]interface{}{ + "enabled": true, + "topup": true, + "personal": true, + } + + // 管理员区域 - 根据角色决定 + if userRole == common.RoleAdminUser { + // 管理员可以访问管理员区域,但不能访问系统设置 + defaultConfig["admin"] = map[string]interface{}{ + "enabled": true, + "channel": true, + "models": true, + "redemption": true, + "user": true, + "setting": false, // 管理员不能访问系统设置 + } + } else if userRole == common.RoleRootUser { + // 超级管理员可以访问所有功能 + defaultConfig["admin"] = map[string]interface{}{ + "enabled": true, + "channel": true, + "models": true, + "redemption": true, + "user": true, + "setting": true, + } + } + // 普通用户不包含admin区域 + + // 转换为JSON字符串 + configBytes, err := json.Marshal(defaultConfig) + if err != nil { + common.SysLog("生成默认边栏配置失败: " + err.Error()) + return "" + } + + return string(configBytes) +} + + + func GetUserModels(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -528,8 +652,8 @@ func UpdateUser(c *gin.Context) { } func UpdateSelf(c *gin.Context) { - var user model.User - err := json.NewDecoder(c.Request.Body).Decode(&user) + var requestData map[string]interface{} + err := json.NewDecoder(c.Request.Body).Decode(&requestData) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -537,6 +661,60 @@ func UpdateSelf(c *gin.Context) { }) 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 + } + + // 原有的用户信息更新逻辑 + var user model.User + requestDataBytes, err := json.Marshal(requestData) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无效的参数", + }) + return + } + err = json.Unmarshal(requestDataBytes, &user) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无效的参数", + }) + return + } + if user.Password == "" { user.Password = "$I_LOVE_U" // make Validator happy :) } @@ -679,6 +857,7 @@ func CreateUser(c *gin.Context) { Username: user.Username, Password: user.Password, DisplayName: user.DisplayName, + Role: user.Role, // 保持管理员设置的角色 } if err := cleanUser.Insert(0); err != nil { common.ApiError(c, err) diff --git a/dto/user_settings.go b/dto/user_settings.go index 2e1a15418..56beb7118 100644 --- a/dto/user_settings.go +++ b/dto/user_settings.go @@ -8,6 +8,7 @@ type UserSetting struct { NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址 AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型 RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP + SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置 } var ( diff --git a/model/user.go b/model/user.go index 29d7a4462..ea0584c5a 100644 --- a/model/user.go +++ b/model/user.go @@ -91,6 +91,68 @@ func (user *User) SetSetting(setting dto.UserSetting) { user.Setting = string(settingBytes) } +// 根据用户角色生成默认的边栏配置 +func generateDefaultSidebarConfigForRole(userRole int) string { + defaultConfig := map[string]interface{}{} + + // 聊天区域 - 所有用户都可以访问 + defaultConfig["chat"] = map[string]interface{}{ + "enabled": true, + "playground": true, + "chat": true, + } + + // 控制台区域 - 所有用户都可以访问 + defaultConfig["console"] = map[string]interface{}{ + "enabled": true, + "detail": true, + "token": true, + "log": true, + "midjourney": true, + "task": true, + } + + // 个人中心区域 - 所有用户都可以访问 + defaultConfig["personal"] = map[string]interface{}{ + "enabled": true, + "topup": true, + "personal": true, + } + + // 管理员区域 - 根据角色决定 + if userRole == common.RoleAdminUser { + // 管理员可以访问管理员区域,但不能访问系统设置 + defaultConfig["admin"] = map[string]interface{}{ + "enabled": true, + "channel": true, + "models": true, + "redemption": true, + "user": true, + "setting": false, // 管理员不能访问系统设置 + } + } else if userRole == common.RoleRootUser { + // 超级管理员可以访问所有功能 + defaultConfig["admin"] = map[string]interface{}{ + "enabled": true, + "channel": true, + "models": true, + "redemption": true, + "user": true, + "setting": true, + } + } + // 普通用户不包含admin区域 + + // 转换为JSON字符串 + configBytes, err := json.Marshal(defaultConfig) + if err != nil { + common.SysLog("生成默认边栏配置失败: " + err.Error()) + return "" + } + + return string(configBytes) +} + // CheckUserExistOrDeleted check if user exist or deleted, if not exist, return false, nil, if deleted or exist, return true, nil func CheckUserExistOrDeleted(username string, email string) (bool, error) { var user User @@ -320,10 +382,34 @@ func (user *User) Insert(inviterId int) error { user.Quota = common.QuotaForNewUser //user.SetAccessToken(common.GetUUID()) user.AffCode = common.GetRandomString(4) + + // 初始化用户设置,包括默认的边栏配置 + if user.Setting == "" { + defaultSetting := dto.UserSetting{} + // 这里暂时不设置SidebarModules,因为需要在用户创建后根据角色设置 + user.SetSetting(defaultSetting) + } + result := DB.Create(user) if result.Error != nil { return result.Error } + + // 用户创建成功后,根据角色初始化边栏配置 + // 需要重新获取用户以确保有正确的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)) + } + } + if common.QuotaForNewUser > 0 { RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", logger.LogQuota(common.QuotaForNewUser))) } diff --git a/web/src/App.jsx b/web/src/App.jsx index e3bd7db85..cb9245244 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { lazy, Suspense } from 'react'; +import React, { lazy, Suspense, useContext, useMemo } from 'react'; import { Route, Routes, useLocation } from 'react-router-dom'; import Loading from './components/common/ui/Loading'; import User from './pages/User'; @@ -27,6 +27,7 @@ import LoginForm from './components/auth/LoginForm'; import NotFound from './pages/NotFound'; import Forbidden from './pages/Forbidden'; import Setting from './pages/Setting'; +import { StatusContext } from './context/Status'; import PasswordResetForm from './components/auth/PasswordResetForm'; import PasswordResetConfirm from './components/auth/PasswordResetConfirm'; @@ -53,6 +54,29 @@ const About = lazy(() => import('./pages/About')); function App() { const location = useLocation(); + const [statusState] = useContext(StatusContext); + + // 获取模型广场权限配置 + const pricingRequireAuth = useMemo(() => { + const headerNavModulesConfig = statusState?.status?.HeaderNavModules; + if (headerNavModulesConfig) { + try { + const modules = JSON.parse(headerNavModulesConfig); + + // 处理向后兼容性:如果pricing是boolean,默认不需要登录 + if (typeof modules.pricing === 'boolean') { + return false; // 默认不需要登录鉴权 + } + + // 如果是对象格式,使用requireAuth配置 + return modules.pricing?.requireAuth === true; + } catch (error) { + console.error('解析顶栏模块配置失败:', error); + return false; // 默认不需要登录 + } + } + return false; // 默认不需要登录 + }, [statusState?.status?.HeaderNavModules]); return ( @@ -253,9 +277,17 @@ function App() { } key={location.pathname}> - - + pricingRequireAuth ? ( + + } key={location.pathname}> + + + + ) : ( + } key={location.pathname}> + + + ) } /> { +const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth }) => { const renderNavLinks = () => { const baseClasses = 'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out'; @@ -51,6 +51,9 @@ const Navigation = ({ mainNavLinks, isMobile, isLoading, userState }) => { if (link.itemKey === 'console' && !userState.user) { targetPath = '/login'; } + if (link.itemKey === 'pricing' && pricingRequireAuth && !userState.user) { + targetPath = '/login'; + } return ( diff --git a/web/src/components/layout/HeaderBar/index.jsx b/web/src/components/layout/HeaderBar/index.jsx index db104de43..81b51d7fe 100644 --- a/web/src/components/layout/HeaderBar/index.jsx +++ b/web/src/components/layout/HeaderBar/index.jsx @@ -44,6 +44,8 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { isDemoSiteMode, isConsoleRoute, theme, + headerNavModules, + pricingRequireAuth, logout, handleLanguageChange, handleThemeToggle, @@ -60,7 +62,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { getUnreadKeys, } = useNotifications(statusState); - const { mainNavLinks } = useNavigation(t, docsLink); + const { mainNavLinks } = useNavigation(t, docsLink, headerNavModules); return (
@@ -102,6 +104,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { isMobile={isMobile} isLoading={isLoading} userState={userState} + pricingRequireAuth={pricingRequireAuth} /> {} }) => { const { t } = useTranslation(); const [collapsed, toggleCollapsed] = useSidebarCollapsed(); + const { isModuleVisible, hasSectionVisibleModules, loading: sidebarLoading } = useSidebar(); const [selectedKeys, setSelectedKeys] = useState(['home']); const [chatItems, setChatItems] = useState([]); @@ -57,117 +59,158 @@ const SiderBar = ({ onNavigate = () => {} }) => { const [routerMapState, setRouterMapState] = useState(routerMap); const workspaceItems = useMemo( - () => [ - { - text: t('数据看板'), - itemKey: 'detail', - to: '/detail', - className: - localStorage.getItem('enable_data_export') === 'true' - ? '' - : 'tableHiddle', - }, - { - text: t('令牌管理'), - itemKey: 'token', - to: '/token', - }, - { - text: t('使用日志'), - itemKey: 'log', - to: '/log', - }, - { - text: t('绘图日志'), - itemKey: 'midjourney', - to: '/midjourney', - className: - localStorage.getItem('enable_drawing') === 'true' - ? '' - : 'tableHiddle', - }, - { - text: t('任务日志'), - itemKey: 'task', - to: '/task', - className: - localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle', - }, - ], + () => { + const items = [ + { + text: t('数据看板'), + itemKey: 'detail', + to: '/detail', + className: + localStorage.getItem('enable_data_export') === 'true' + ? '' + : 'tableHiddle', + }, + { + text: t('令牌管理'), + itemKey: 'token', + to: '/token', + }, + { + text: t('使用日志'), + itemKey: 'log', + to: '/log', + }, + { + text: t('绘图日志'), + itemKey: 'midjourney', + to: '/midjourney', + className: + localStorage.getItem('enable_drawing') === 'true' + ? '' + : 'tableHiddle', + }, + { + text: t('任务日志'), + itemKey: 'task', + to: '/task', + className: + localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle', + }, + ]; + + // 根据配置过滤项目 + const filteredItems = items.filter(item => { + const configVisible = isModuleVisible('console', item.itemKey); + return configVisible; + }); + + return filteredItems; + }, [ localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'), localStorage.getItem('enable_task'), t, + isModuleVisible, ], ); const financeItems = useMemo( - () => [ - { - text: t('钱包管理'), - itemKey: 'topup', - to: '/topup', - }, - { - text: t('个人设置'), - itemKey: 'personal', - to: '/personal', - }, - ], - [t], + () => { + const items = [ + { + text: t('钱包管理'), + itemKey: 'topup', + to: '/topup', + }, + { + text: t('个人设置'), + itemKey: 'personal', + to: '/personal', + }, + ]; + + // 根据配置过滤项目 + const filteredItems = items.filter(item => { + const configVisible = isModuleVisible('personal', item.itemKey); + return configVisible; + }); + + return filteredItems; + }, + [t, isModuleVisible], ); const adminItems = useMemo( - () => [ - { - text: t('渠道管理'), - itemKey: 'channel', - to: '/channel', - className: isAdmin() ? '' : 'tableHiddle', - }, - { - text: t('模型管理'), - itemKey: 'models', - to: '/console/models', - className: isAdmin() ? '' : 'tableHiddle', - }, - { - text: t('兑换码管理'), - itemKey: 'redemption', - to: '/redemption', - className: isAdmin() ? '' : 'tableHiddle', - }, - { - text: t('用户管理'), - itemKey: 'user', - to: '/user', - className: isAdmin() ? '' : 'tableHiddle', - }, - { - text: t('系统设置'), - itemKey: 'setting', - to: '/setting', - className: isRoot() ? '' : 'tableHiddle', - }, - ], - [isAdmin(), isRoot(), t], + () => { + const items = [ + { + text: t('渠道管理'), + itemKey: 'channel', + to: '/channel', + className: isAdmin() ? '' : 'tableHiddle', + }, + { + text: t('模型管理'), + itemKey: 'models', + to: '/console/models', + className: isAdmin() ? '' : 'tableHiddle', + }, + { + text: t('兑换码管理'), + itemKey: 'redemption', + to: '/redemption', + className: isAdmin() ? '' : 'tableHiddle', + }, + { + text: t('用户管理'), + itemKey: 'user', + to: '/user', + className: isAdmin() ? '' : 'tableHiddle', + }, + { + text: t('系统设置'), + itemKey: 'setting', + to: '/setting', + className: isRoot() ? '' : 'tableHiddle', + }, + ]; + + // 根据配置过滤项目 + const filteredItems = items.filter(item => { + const configVisible = isModuleVisible('admin', item.itemKey); + return configVisible; + }); + + return filteredItems; + }, + [isAdmin(), isRoot(), t, isModuleVisible], ); const chatMenuItems = useMemo( - () => [ - { - text: t('操练场'), - itemKey: 'playground', - to: '/playground', - }, - { - text: t('聊天'), - itemKey: 'chat', - items: chatItems, - }, - ], - [chatItems, t], + () => { + const items = [ + { + text: t('操练场'), + itemKey: 'playground', + to: '/playground', + }, + { + text: t('聊天'), + itemKey: 'chat', + items: chatItems, + }, + ]; + + // 根据配置过滤项目 + const filteredItems = items.filter(item => { + const configVisible = isModuleVisible('chat', item.itemKey); + return configVisible; + }); + + return filteredItems; + }, + [chatItems, t, isModuleVisible], ); // 更新路由映射,添加聊天路由 @@ -213,7 +256,6 @@ const SiderBar = ({ onNavigate = () => {} }) => { updateRouterMapWithChats(chats); } } catch (e) { - console.error(e); showError('聊天数据解析失败'); } } @@ -382,31 +424,41 @@ const SiderBar = ({ onNavigate = () => {} }) => { }} > {/* 聊天区域 */} -
- {!collapsed &&
{t('聊天')}
} - {chatMenuItems.map((item) => renderSubItem(item))} -
+ {hasSectionVisibleModules('chat') && ( +
+ {!collapsed &&
{t('聊天')}
} + {chatMenuItems.map((item) => renderSubItem(item))} +
+ )} {/* 控制台区域 */} - -
- {!collapsed && ( -
{t('控制台')}
- )} - {workspaceItems.map((item) => renderNavItem(item))} -
+ {hasSectionVisibleModules('console') && ( + <> + +
+ {!collapsed && ( +
{t('控制台')}
+ )} + {workspaceItems.map((item) => renderNavItem(item))} +
+ + )} {/* 个人中心区域 */} - -
- {!collapsed && ( -
{t('个人中心')}
- )} - {financeItems.map((item) => renderNavItem(item))} -
+ {hasSectionVisibleModules('personal') && ( + <> + +
+ {!collapsed && ( +
{t('个人中心')}
+ )} + {financeItems.map((item) => renderNavItem(item))} +
+ + )} - {/* 管理员区域 - 只在管理员时显示 */} - {isAdmin() && ( + {/* 管理员区域 - 只在管理员时显示且配置允许时显示 */} + {isAdmin() && hasSectionVisibleModules('admin') && ( <>
diff --git a/web/src/components/settings/OperationSetting.jsx b/web/src/components/settings/OperationSetting.jsx index d2669c643..05bda1528 100644 --- a/web/src/components/settings/OperationSetting.jsx +++ b/web/src/components/settings/OperationSetting.jsx @@ -20,6 +20,8 @@ For commercial licensing, please contact support@quantumnous.com import React, { useEffect, useState } from 'react'; import { Card, Spin } from '@douyinfe/semi-ui'; import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral'; +import SettingsHeaderNavModules from '../../pages/Setting/Operation/SettingsHeaderNavModules'; +import SettingsSidebarModulesAdmin from '../../pages/Setting/Operation/SettingsSidebarModulesAdmin'; import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords'; import SettingsLog from '../../pages/Setting/Operation/SettingsLog'; import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring'; @@ -46,6 +48,12 @@ const OperationSetting = () => { DemoSiteEnabled: false, SelfUseModeEnabled: false, + /* 顶栏模块管理 */ + HeaderNavModules: '', + + /* 左侧边栏模块管理(管理员) */ + SidebarModulesAdmin: '', + /* 敏感词设置 */ CheckSensitiveEnabled: false, CheckSensitiveOnPromptEnabled: false, @@ -108,6 +116,14 @@ const OperationSetting = () => { + {/* 顶栏模块管理 */} +
+ +
+ {/* 左侧边栏模块管理(管理员) */} +
+ +
{/* 屏蔽词过滤设置 */} diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index cdeb1f511..07f2b8c48 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -38,6 +38,8 @@ const PersonalSetting = () => { let navigate = useNavigate(); const { t } = useTranslation(); + + const [inputs, setInputs] = useState({ wechat_verification_code: '', email_verification_code: '', @@ -330,6 +332,8 @@ const PersonalSetting = () => { saveNotificationSettings={saveNotificationSettings} />
+ + diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index 6caffde7b..d76706c55 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState, useContext } from 'react'; import { Button, Typography, @@ -28,11 +28,17 @@ import { Toast, Tabs, TabPane, + Switch, + Row, + Col, } from '@douyinfe/semi-ui'; import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons'; -import { ShieldCheck, Bell, DollarSign } from 'lucide-react'; -import { renderQuotaWithPrompt } from '../../../../helpers'; +import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react'; +import { renderQuotaWithPrompt, API, showSuccess, showError } from '../../../../helpers'; import CodeViewer from '../../../playground/CodeViewer'; +import { StatusContext } from '../../../../context/Status'; +import { UserContext } from '../../../../context/User'; +import { useUserPermissions } from '../../../../hooks/common/useUserPermissions'; const NotificationSettings = ({ t, @@ -41,6 +47,128 @@ const NotificationSettings = ({ saveNotificationSettings, }) => { const formApiRef = useRef(null); + const [statusState] = useContext(StatusContext); + const [userState] = useContext(UserContext); + + // 左侧边栏设置相关状态 + const [sidebarLoading, setSidebarLoading] = useState(false); + const [activeTabKey, setActiveTabKey] = useState('notification'); + const [sidebarModulesUser, setSidebarModulesUser] = 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 [adminConfig, setAdminConfig] = useState(null); + + // 使用后端权限验证替代前端角色判断 + const { + permissions, + loading: permissionsLoading, + hasSidebarSettingsPermission, + isSidebarSectionAllowed, + isSidebarModuleAllowed, + } = useUserPermissions(); + + // 左侧边栏设置处理函数 + const handleSectionChange = (sectionKey) => { + return (checked) => { + const newModules = { + ...sidebarModulesUser, + [sectionKey]: { + ...sidebarModulesUser[sectionKey], + enabled: checked + } + }; + setSidebarModulesUser(newModules); + }; + }; + + const handleModuleChange = (sectionKey, moduleKey) => { + return (checked) => { + const newModules = { + ...sidebarModulesUser, + [sectionKey]: { + ...sidebarModulesUser[sectionKey], + [moduleKey]: checked + } + }; + setSidebarModulesUser(newModules); + }; + }; + + const saveSidebarSettings = async () => { + setSidebarLoading(true); + try { + const res = await API.put('/api/user/self', { + sidebar_modules: JSON.stringify(sidebarModulesUser) + }); + if (res.data.success) { + showSuccess(t('侧边栏设置保存成功')); + } else { + showError(res.data.message); + } + } catch (error) { + showError(t('保存失败')); + } + setSidebarLoading(false); + }; + + const resetSidebarModules = () => { + const defaultConfig = { + 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 } + }; + setSidebarModulesUser(defaultConfig); + }; + + // 加载左侧边栏配置 + useEffect(() => { + const loadSidebarConfigs = async () => { + try { + // 获取管理员全局配置 + 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); + } + } catch (error) { + console.error('加载边栏配置失败:', error); + } + }; + + loadSidebarConfigs(); + }, [statusState]); // 初始化表单值 useEffect(() => { @@ -54,6 +182,75 @@ const NotificationSettings = ({ handleNotificationSettingChange(field, value); }; + // 检查功能是否被管理员允许 + const isAllowedByAdmin = (sectionKey, moduleKey = null) => { + if (!adminConfig) return true; + + if (moduleKey) { + return adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]; + } else { + return adminConfig[sectionKey]?.enabled; + } + }; + + // 区域配置数据(根据权限过滤) + const sectionConfigs = [ + { + key: 'chat', + title: t('聊天区域'), + description: t('操练场和聊天功能'), + modules: [ + { key: 'playground', title: t('操练场'), description: t('AI模型测试环境') }, + { key: 'chat', title: t('聊天'), description: t('聊天会话管理') } + ] + }, + { + key: 'console', + title: t('控制台区域'), + description: t('数据管理和日志查看'), + modules: [ + { key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, + { key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, + { key: 'log', title: t('使用日志'), description: t('API使用记录') }, + { key: 'midjourney', title: t('绘图日志'), description: t('绘图任务记录') }, + { key: 'task', title: t('任务日志'), description: t('系统任务记录') } + ] + }, + { + key: 'personal', + title: t('个人中心区域'), + description: t('用户个人功能'), + modules: [ + { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') }, + { key: 'personal', title: t('个人设置'), description: t('个人信息设置') } + ] + }, + // 管理员区域:根据后端权限控制显示 + { + key: 'admin', + title: t('管理员区域'), + description: t('系统管理功能'), + modules: [ + { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, + { key: 'models', title: t('模型管理'), description: t('AI模型配置') }, + { key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') }, + { key: 'user', title: t('用户管理'), description: t('用户账户管理') }, + { key: 'setting', title: t('系统设置'), description: t('系统参数配置') } + ] + } + ].filter(section => { + // 使用后端权限验证替代前端角色判断 + return isSidebarSectionAllowed(section.key); + }).map(section => ({ + ...section, + modules: section.modules.filter(module => + isSidebarModuleAllowed(section.key, module.key) + ) + })).filter(section => + // 过滤掉没有可用模块的区域 + section.modules.length > 0 && isAllowedByAdmin(section.key) + ); + // 表单提交 const handleSubmit = () => { if (formApiRef.current) { @@ -75,10 +272,32 @@ const NotificationSettings = ({ - +
+ {activeTabKey === 'sidebar' ? ( + // 边栏设置标签页的按钮 + <> + + + + ) : ( + // 其他标签页的通用保存按钮 + + )}
} > @@ -103,7 +322,11 @@ const NotificationSettings = ({ onSubmit={handleSubmit} > {() => ( - + setActiveTabKey(key)} + > {/* 通知配置 Tab */} + + {/* 左侧边栏设置 Tab - 根据后端权限控制显示 */} + {hasSidebarSettingsPermission() && ( + + + {t('边栏设置')} + + } + itemKey='sidebar' + > +
+
+ + {t('您可以个性化设置侧边栏的要显示功能')} + +
+ + {/* 边栏设置功能区域容器 */} +
+ + {sectionConfigs.map((section) => ( +
+ {/* 区域标题和总开关 */} +
+
+
+ {section.title} +
+ + {section.description} + +
+ +
+ + {/* 功能模块网格 */} + + {section.modules + .filter(module => isAllowedByAdmin(section.key, module.key)) + .map((module) => ( + + +
+
+
+ {module.title} +
+ + {module.description} + +
+
+ +
+
+
+ + ))} +
+
+ ))} +
{/* 关闭边栏设置功能区域容器 */} +
+
+ )}
)} diff --git a/web/src/hooks/common/useHeaderBar.js b/web/src/hooks/common/useHeaderBar.js index 1b91511ba..9f95a9b9a 100644 --- a/web/src/hooks/common/useHeaderBar.js +++ b/web/src/hooks/common/useHeaderBar.js @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useState, useEffect, useContext, useCallback } from 'react'; +import { useState, useEffect, useContext, useCallback, useMemo } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { UserContext } from '../../context/User'; @@ -51,6 +51,42 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { const docsLink = statusState?.status?.docs_link || ''; const isDemoSiteMode = statusState?.status?.demo_site_enabled || false; + // 获取顶栏模块配置 + const headerNavModulesConfig = statusState?.status?.HeaderNavModules; + + // 使用useMemo确保headerNavModules正确响应statusState变化 + const headerNavModules = useMemo(() => { + if (headerNavModulesConfig) { + try { + const modules = JSON.parse(headerNavModulesConfig); + + // 处理向后兼容性:如果pricing是boolean,转换为对象格式 + if (typeof modules.pricing === 'boolean') { + modules.pricing = { + enabled: modules.pricing, + requireAuth: false // 默认不需要登录鉴权 + }; + } + + return modules; + } catch (error) { + console.error('解析顶栏模块配置失败:', error); + return null; + } + } + return null; + }, [headerNavModulesConfig]); + + // 获取模型广场权限配置 + const pricingRequireAuth = useMemo(() => { + if (headerNavModules?.pricing) { + return typeof headerNavModules.pricing === 'object' + ? headerNavModules.pricing.requireAuth + : false; // 默认不需要登录 + } + return false; // 默认不需要登录 + }, [headerNavModules]); + const isConsoleRoute = location.pathname.startsWith('/console'); const theme = useTheme(); @@ -156,6 +192,8 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { isConsoleRoute, theme, drawerOpen, + headerNavModules, + pricingRequireAuth, // Actions logout, diff --git a/web/src/hooks/common/useNavigation.js b/web/src/hooks/common/useNavigation.js index e0bac5521..43d9024ea 100644 --- a/web/src/hooks/common/useNavigation.js +++ b/web/src/hooks/common/useNavigation.js @@ -19,41 +19,67 @@ For commercial licensing, please contact support@quantumnous.com import { useMemo } from 'react'; -export const useNavigation = (t, docsLink) => { +export const useNavigation = (t, docsLink, headerNavModules) => { const mainNavLinks = useMemo( - () => [ - { - text: t('首页'), - itemKey: 'home', - to: '/', - }, - { - text: t('控制台'), - itemKey: 'console', - to: '/console', - }, - { - text: t('模型广场'), - itemKey: 'pricing', - to: '/pricing', - }, - ...(docsLink - ? [ - { - text: t('文档'), - itemKey: 'docs', - isExternal: true, - externalLink: docsLink, - }, - ] - : []), - { - text: t('关于'), - itemKey: 'about', - to: '/about', - }, - ], - [t, docsLink], + () => { + // 默认配置,如果没有传入配置则显示所有模块 + const defaultModules = { + home: true, + console: true, + pricing: true, + docs: true, + about: true, + }; + + // 使用传入的配置或默认配置 + const modules = headerNavModules || defaultModules; + + const allLinks = [ + { + text: t('首页'), + itemKey: 'home', + to: '/', + }, + { + text: t('控制台'), + itemKey: 'console', + to: '/console', + }, + { + text: t('模型广场'), + itemKey: 'pricing', + to: '/pricing', + }, + ...(docsLink + ? [ + { + text: t('文档'), + itemKey: 'docs', + isExternal: true, + externalLink: docsLink, + }, + ] + : []), + { + text: t('关于'), + itemKey: 'about', + to: '/about', + }, + ]; + + // 根据配置过滤导航链接 + return allLinks.filter(link => { + if (link.itemKey === 'docs') { + return docsLink && modules.docs; + } + if (link.itemKey === 'pricing') { + // 支持新的pricing配置格式 + return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing; + } + return modules[link.itemKey] === true; + }); + }, + [t, docsLink, headerNavModules], ); return { diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js new file mode 100644 index 000000000..0a695bbd4 --- /dev/null +++ b/web/src/hooks/common/useSidebar.js @@ -0,0 +1,220 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import { useState, useEffect, useMemo, useContext } from 'react'; +import { StatusContext } from '../../context/Status'; +import { API } from '../../helpers'; + +export const useSidebar = () => { + const [statusState] = useContext(StatusContext); + const [userConfig, setUserConfig] = useState(null); + const [loading, setLoading] = useState(true); + + // 默认配置 + const defaultAdminConfig = { + 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 adminConfig = useMemo(() => { + if (statusState?.status?.SidebarModulesAdmin) { + try { + const config = JSON.parse(statusState.status.SidebarModulesAdmin); + return config; + } catch (error) { + return defaultAdminConfig; + } + } + return defaultAdminConfig; + }, [statusState?.status?.SidebarModulesAdmin]); + + // 加载用户配置的通用方法 + const loadUserConfig = async () => { + try { + setLoading(true); + const res = await API.get('/api/user/self'); + if (res.data.success && res.data.data.sidebar_modules) { + let config; + // 检查sidebar_modules是字符串还是对象 + if (typeof res.data.data.sidebar_modules === 'string') { + config = JSON.parse(res.data.data.sidebar_modules); + } else { + config = res.data.data.sidebar_modules; + } + setUserConfig(config); + } 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); + } + } 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); + } finally { + setLoading(false); + } + }; + + // 刷新用户配置的方法(供外部调用) + const refreshUserConfig = async () => { + if (Object.keys(adminConfig).length > 0) { + await loadUserConfig(); + } + }; + + // 加载用户配置 + useEffect(() => { + // 只有当管理员配置加载完成后才加载用户配置 + if (Object.keys(adminConfig).length > 0) { + loadUserConfig(); + } + }, [adminConfig]); + + // 计算最终的显示配置 + const finalConfig = useMemo(() => { + const result = {}; + + // 确保adminConfig已加载 + if (!adminConfig || Object.keys(adminConfig).length === 0) { + return result; + } + + // 如果userConfig未加载,等待加载完成 + if (!userConfig) { + return result; + } + + // 遍历所有区域 + Object.keys(adminConfig).forEach(sectionKey => { + const adminSection = adminConfig[sectionKey]; + const userSection = userConfig[sectionKey]; + + // 如果管理员禁用了整个区域,则该区域不显示 + if (!adminSection?.enabled) { + result[sectionKey] = { enabled: false }; + return; + } + + // 区域级别:用户可以选择隐藏管理员允许的区域 + // 当userSection存在时检查enabled状态,否则默认为true + const sectionEnabled = userSection ? (userSection.enabled !== false) : true; + result[sectionKey] = { enabled: sectionEnabled }; + + // 功能级别:只有管理员和用户都允许的功能才显示 + Object.keys(adminSection).forEach(moduleKey => { + if (moduleKey === 'enabled') return; + + const adminAllowed = adminSection[moduleKey]; + // 当userSection存在时检查模块状态,否则默认为true + const userAllowed = userSection ? (userSection[moduleKey] !== false) : true; + + result[sectionKey][moduleKey] = adminAllowed && userAllowed && sectionEnabled; + }); + }); + + return result; + }, [adminConfig, userConfig]); + + // 检查特定功能是否应该显示 + const isModuleVisible = (sectionKey, moduleKey = null) => { + if (moduleKey) { + return finalConfig[sectionKey]?.[moduleKey] === true; + } else { + return finalConfig[sectionKey]?.enabled === true; + } + }; + + // 检查区域是否有任何可见的功能 + const hasSectionVisibleModules = (sectionKey) => { + const section = finalConfig[sectionKey]; + if (!section?.enabled) return false; + + return Object.keys(section).some(key => + key !== 'enabled' && section[key] === true + ); + }; + + // 获取区域的可见功能列表 + const getVisibleModules = (sectionKey) => { + const section = finalConfig[sectionKey]; + if (!section?.enabled) return []; + + return Object.keys(section) + .filter(key => key !== 'enabled' && section[key] === true); + }; + + return { + loading, + adminConfig, + userConfig, + finalConfig, + isModuleVisible, + hasSectionVisibleModules, + getVisibleModules, + refreshUserConfig + }; +}; diff --git a/web/src/hooks/common/useUserPermissions.js b/web/src/hooks/common/useUserPermissions.js new file mode 100644 index 000000000..743594353 --- /dev/null +++ b/web/src/hooks/common/useUserPermissions.js @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import { API } from '../../helpers'; + +/** + * 用户权限钩子 - 从后端获取用户权限,替代前端角色判断 + * 确保权限控制的安全性,防止前端绕过 + */ +export const useUserPermissions = () => { + const [permissions, setPermissions] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // 加载用户权限(从用户信息接口获取) + const loadPermissions = async () => { + try { + setLoading(true); + setError(null); + const res = await API.get('/api/user/self'); + if (res.data.success) { + const userPermissions = res.data.data.permissions; + setPermissions(userPermissions); + console.log('用户权限加载成功:', userPermissions); + } else { + setError(res.data.message || '获取权限失败'); + console.error('获取权限失败:', res.data.message); + } + } catch (error) { + setError('网络错误,请重试'); + console.error('加载用户权限异常:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadPermissions(); + }, []); + + // 检查是否有边栏设置权限 + const hasSidebarSettingsPermission = () => { + return permissions?.sidebar_settings === true; + }; + + // 检查是否允许访问特定的边栏区域 + const isSidebarSectionAllowed = (sectionKey) => { + if (!permissions?.sidebar_modules) return true; + const sectionPerms = permissions.sidebar_modules[sectionKey]; + return sectionPerms !== false; + }; + + // 检查是否允许访问特定的边栏模块 + const isSidebarModuleAllowed = (sectionKey, moduleKey) => { + if (!permissions?.sidebar_modules) return true; + const sectionPerms = permissions.sidebar_modules[sectionKey]; + + // 如果整个区域被禁用 + if (sectionPerms === 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 getAllowedSidebarModules = (sectionKey) => { + if (!permissions?.sidebar_modules) return []; + const sectionPerms = permissions.sidebar_modules[sectionKey]; + + if (sectionPerms === false) return []; + if (!sectionPerms || typeof sectionPerms !== 'object') return []; + + return Object.keys(sectionPerms).filter(moduleKey => + moduleKey !== 'enabled' && sectionPerms[moduleKey] === true + ); + }; + + return { + permissions, + loading, + error, + loadPermissions, + hasSidebarSettingsPermission, + isSidebarSectionAllowed, + isSidebarModuleAllowed, + getAllowedSidebarSections, + getAllowedSidebarModules, + }; +}; + +export default useUserPermissions; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 877fa44fe..7b308b9b7 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2017,5 +2017,63 @@ "查看密钥": "View key", "查看渠道密钥": "View channel key", "渠道密钥信息": "Channel key information", - "密钥获取成功": "Key acquisition successful" + "密钥获取成功": "Key acquisition successful", + "顶栏管理": "Header Management", + "控制顶栏模块显示状态,全局生效": "Control header module display status, global effect", + "用户主页,展示系统信息": "User homepage, displaying system information", + "用户控制面板,管理账户": "User control panel for account management", + "模型广场": "Model Marketplace", + "模型定价,需要登录访问": "Model pricing, requires login to access", + "文档": "Documentation", + "系统文档和帮助信息": "System documentation and help information", + "关于系统的详细信息": "Detailed information about the system", + "重置为默认": "Reset to Default", + "保存设置": "Save Settings", + "已重置为默认配置": "Reset to default configuration", + "保存成功": "Saved successfully", + "保存失败,请重试": "Save failed, please try again", + "侧边栏管理(全局控制)": "Sidebar Management (Global Control)", + "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Global control of sidebar areas and functions, users cannot enable functions hidden by administrators", + "聊天区域": "Chat Area", + "操练场和聊天功能": "Playground and chat functions", + "操练场": "Playground", + "AI模型测试环境": "AI model testing environment", + "聊天": "Chat", + "聊天会话管理": "Chat session management", + "控制台区域": "Console Area", + "数据管理和日志查看": "Data management and log viewing", + "数据看板": "Dashboard", + "系统数据统计": "System data statistics", + "令牌管理": "Token Management", + "API令牌管理": "API token management", + "使用日志": "Usage Logs", + "API使用记录": "API usage records", + "绘图日志": "Drawing Logs", + "绘图任务记录": "Drawing task records", + "任务日志": "Task Logs", + "系统任务记录": "System task records", + "个人中心区域": "Personal Center Area", + "用户个人功能": "User personal functions", + "钱包管理": "Wallet Management", + "余额充值管理": "Balance recharge management", + "个人设置": "Personal Settings", + "个人信息设置": "Personal information settings", + "管理员区域": "Administrator Area", + "系统管理功能": "System management functions", + "渠道管理": "Channel Management", + "API渠道配置": "API channel configuration", + "模型管理": "Model Management", + "AI模型配置": "AI model configuration", + "兑换码管理": "Redemption Code Management", + "兑换码生成管理": "Redemption code generation management", + "用户管理": "User Management", + "用户账户管理": "User account management", + "系统设置": "System Settings", + "系统参数配置": "System parameter configuration", + "边栏设置": "Sidebar Settings", + "您可以个性化设置侧边栏的要显示功能": "You can customize the sidebar functions to display", + "保存边栏设置": "Save Sidebar Settings", + "侧边栏设置保存成功": "Sidebar settings saved successfully", + "需要登录访问": "Require Login", + "开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace" } diff --git a/web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx b/web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx new file mode 100644 index 000000000..623accefb --- /dev/null +++ b/web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx @@ -0,0 +1,326 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useEffect, useState, useContext } from 'react'; +import { Button, Card, Col, Form, Row, Switch, Typography } from '@douyinfe/semi-ui'; +import { API, showError, showSuccess } from '../../../helpers'; +import { useTranslation } from 'react-i18next'; +import { StatusContext } from '../../../context/Status'; + +const { Text } = Typography; + +export default function SettingsHeaderNavModules(props) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [statusState, statusDispatch] = useContext(StatusContext); + + // 顶栏模块管理状态 + const [headerNavModules, setHeaderNavModules] = useState({ + home: true, + console: true, + pricing: { + enabled: true, + requireAuth: false // 默认不需要登录鉴权 + }, + docs: true, + about: true, + }); + + // 处理顶栏模块配置变更 + 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); + }; + } + + // 处理模型广场权限控制变更 + function handlePricingAuthChange(checked) { + const newModules = { ...headerNavModules }; + newModules.pricing = { + ...newModules.pricing, + requireAuth: checked + }; + setHeaderNavModules(newModules); + } + + // 重置顶栏模块为默认配置 + function resetHeaderNavModules() { + const defaultModules = { + home: true, + console: true, + pricing: { + enabled: true, + requireAuth: false + }, + docs: true, + about: true, + }; + setHeaderNavModules(defaultModules); + showSuccess(t('已重置为默认配置')); + } + + // 保存配置 + async function onSubmit() { + setLoading(true); + try { + const res = await API.put('/api/option/', { + key: 'HeaderNavModules', + value: JSON.stringify(headerNavModules), + }); + const { success, message } = res.data; + if (success) { + showSuccess(t('保存成功')); + + // 立即更新StatusContext中的状态 + statusDispatch({ + type: 'set', + payload: { + ...statusState.status, + HeaderNavModules: JSON.stringify(headerNavModules) + } + }); + + // 刷新父组件状态 + if (props.refresh) { + await props.refresh(); + } + } else { + showError(message); + } + } catch (error) { + showError(t('保存失败,请重试')); + } finally { + setLoading(false); + } + } + + useEffect(() => { + // 从 props.options 中获取配置 + if (props.options && props.options.HeaderNavModules) { + try { + const modules = JSON.parse(props.options.HeaderNavModules); + + // 处理向后兼容性:如果pricing是boolean,转换为对象格式 + if (typeof modules.pricing === 'boolean') { + modules.pricing = { + enabled: modules.pricing, + requireAuth: false // 默认不需要登录鉴权 + }; + } + + setHeaderNavModules(modules); + } catch (error) { + // 使用默认配置 + const defaultModules = { + home: true, + console: true, + pricing: { + enabled: true, + requireAuth: false + }, + docs: true, + about: true, + }; + setHeaderNavModules(defaultModules); + } + } + }, [props.options]); + + // 模块配置数据 + const moduleConfigs = [ + { + key: 'home', + title: t('首页'), + description: t('用户主页,展示系统信息') + }, + { + key: 'console', + title: t('控制台'), + description: t('用户控制面板,管理账户') + }, + { + key: 'pricing', + title: t('模型广场'), + description: t('模型定价,需要登录访问'), + hasSubConfig: true // 标识该模块有子配置 + }, + { + key: 'docs', + title: t('文档'), + description: t('系统文档和帮助信息') + }, + { + key: 'about', + title: t('关于'), + description: t('关于系统的详细信息') + } + ]; + + return ( + + + + + {moduleConfigs.map((module) => ( + + +
+
+
+ {module.title} +
+ + {module.description} + +
+
+ +
+
+ + {/* 为模型广场添加权限控制子开关 */} + {module.key === 'pricing' && (module.key === 'pricing' ? headerNavModules[module.key]?.enabled : headerNavModules[module.key]) && ( +
+
+
+
+ {t('需要登录访问')} +
+ + {t('开启后未登录用户无法访问模型广场')} + +
+
+ +
+
+
+ )} +
+ + ))} + +
+ +
+ + +
+
+
+ ); +} diff --git a/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx b/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx new file mode 100644 index 000000000..ec8485fba --- /dev/null +++ b/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx @@ -0,0 +1,362 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useEffect, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Card, Form, Button, Switch, Row, Col, Typography } from '@douyinfe/semi-ui'; +import { API, showSuccess, showError } from '../../../helpers'; +import { StatusContext } from '../../../context/Status'; + +const { Text } = Typography; + +export default function SettingsSidebarModulesAdmin(props) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [statusState, statusDispatch] = useContext(StatusContext); + + // 左侧边栏模块管理状态(管理员全局控制) + 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 + } + }); + + // 处理区域级别开关变更 + function handleSectionChange(sectionKey) { + return (checked) => { + const newModules = { + ...sidebarModulesAdmin, + [sectionKey]: { + ...sidebarModulesAdmin[sectionKey], + enabled: checked + } + }; + setSidebarModulesAdmin(newModules); + }; + } + + // 处理功能级别开关变更 + function handleModuleChange(sectionKey, moduleKey) { + return (checked) => { + const newModules = { + ...sidebarModulesAdmin, + [sectionKey]: { + ...sidebarModulesAdmin[sectionKey], + [moduleKey]: checked + } + }; + setSidebarModulesAdmin(newModules); + }; + } + + // 重置为默认配置 + 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); + showSuccess(t('已重置为默认配置')); + } + + // 保存配置 + async function onSubmit() { + setLoading(true); + try { + const res = await API.put('/api/option/', { + key: 'SidebarModulesAdmin', + value: JSON.stringify(sidebarModulesAdmin), + }); + const { success, message } = res.data; + if (success) { + showSuccess(t('保存成功')); + + // 立即更新StatusContext中的状态 + statusDispatch({ + type: 'set', + payload: { + ...statusState.status, + SidebarModulesAdmin: JSON.stringify(sidebarModulesAdmin) + } + }); + + // 刷新父组件状态 + if (props.refresh) { + await props.refresh(); + } + } else { + showError(message); + } + } catch (error) { + showError(t('保存失败,请重试')); + } finally { + setLoading(false); + } + } + + useEffect(() => { + // 从 props.options 中获取配置 + if (props.options && props.options.SidebarModulesAdmin) { + try { + const modules = JSON.parse(props.options.SidebarModulesAdmin); + setSidebarModulesAdmin(modules); + } catch (error) { + // 使用默认配置 + 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); + } + } + }, [props.options]); + + // 区域配置数据 + const sectionConfigs = [ + { + key: 'chat', + title: t('聊天区域'), + description: t('操练场和聊天功能'), + modules: [ + { key: 'playground', title: t('操练场'), description: t('AI模型测试环境') }, + { key: 'chat', title: t('聊天'), description: t('聊天会话管理') } + ] + }, + { + key: 'console', + title: t('控制台区域'), + description: t('数据管理和日志查看'), + modules: [ + { key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, + { key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, + { key: 'log', title: t('使用日志'), description: t('API使用记录') }, + { key: 'midjourney', title: t('绘图日志'), description: t('绘图任务记录') }, + { key: 'task', title: t('任务日志'), description: t('系统任务记录') } + ] + }, + { + key: 'personal', + title: t('个人中心区域'), + description: t('用户个人功能'), + modules: [ + { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') }, + { key: 'personal', title: t('个人设置'), description: t('个人信息设置') } + ] + }, + { + key: 'admin', + title: t('管理员区域'), + description: t('系统管理功能'), + modules: [ + { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, + { key: 'models', title: t('模型管理'), description: t('AI模型配置') }, + { key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') }, + { key: 'user', title: t('用户管理'), description: t('用户账户管理') }, + { key: 'setting', title: t('系统设置'), description: t('系统参数配置') } + ] + } + ]; + + return ( + + + + {sectionConfigs.map((section) => ( +
+ {/* 区域标题和总开关 */} +
+
+
+ {section.title} +
+ + {section.description} + +
+ +
+ + {/* 功能模块网格 */} + + {section.modules.map((module) => ( + + +
+
+
+ {module.title} +
+ + {module.description} + +
+
+ +
+
+
+ + ))} +
+
+ ))} + +
+ + +
+
+
+ ); +} diff --git a/web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx b/web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx new file mode 100644 index 000000000..bb779c7ea --- /dev/null +++ b/web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx @@ -0,0 +1,404 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import { useState, useEffect, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Card, Button, Switch, Typography, Row, Col, Avatar } from '@douyinfe/semi-ui'; +import { API, showSuccess, showError } from '../../../helpers'; +import { StatusContext } from '../../../context/Status'; +import { UserContext } from '../../../context/User'; +import { useUserPermissions } from '../../../hooks/common/useUserPermissions'; +import { useSidebar } from '../../../hooks/common/useSidebar'; +import { Settings } from 'lucide-react'; + +const { Text } = Typography; + +export default function SettingsSidebarModulesUser() { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [statusState] = useContext(StatusContext); + + // 使用后端权限验证替代前端角色判断 + const { + permissions, + loading: permissionsLoading, + hasSidebarSettingsPermission, + isSidebarSectionAllowed, + isSidebarModuleAllowed, + } = useUserPermissions(); + + // 使用useSidebar钩子获取刷新方法 + const { refreshUserConfig } = useSidebar(); + + // 如果没有边栏设置权限,不显示此组件 + if (!permissionsLoading && !hasSidebarSettingsPermission()) { + return null; + } + + // 权限加载中,显示加载状态 + if (permissionsLoading) { + return null; + } + + // 根据用户权限生成默认配置 + const generateDefaultConfig = () => { + const defaultConfig = {}; + + // 聊天区域 - 所有用户都可以访问 + if (isSidebarSectionAllowed('chat')) { + defaultConfig.chat = { + enabled: true, + playground: isSidebarModuleAllowed('chat', 'playground'), + chat: isSidebarModuleAllowed('chat', 'chat') + }; + } + + // 控制台区域 - 所有用户都可以访问 + if (isSidebarSectionAllowed('console')) { + defaultConfig.console = { + enabled: true, + detail: isSidebarModuleAllowed('console', 'detail'), + token: isSidebarModuleAllowed('console', 'token'), + log: isSidebarModuleAllowed('console', 'log'), + midjourney: isSidebarModuleAllowed('console', 'midjourney'), + task: isSidebarModuleAllowed('console', 'task') + }; + } + + // 个人中心区域 - 所有用户都可以访问 + if (isSidebarSectionAllowed('personal')) { + defaultConfig.personal = { + enabled: true, + topup: isSidebarModuleAllowed('personal', 'topup'), + personal: isSidebarModuleAllowed('personal', 'personal') + }; + } + + // 管理员区域 - 只有管理员可以访问 + if (isSidebarSectionAllowed('admin')) { + defaultConfig.admin = { + enabled: true, + channel: isSidebarModuleAllowed('admin', 'channel'), + models: isSidebarModuleAllowed('admin', 'models'), + redemption: isSidebarModuleAllowed('admin', 'redemption'), + user: isSidebarModuleAllowed('admin', 'user'), + setting: isSidebarModuleAllowed('admin', 'setting') + }; + } + + return defaultConfig; + }; + + // 用户个人左侧边栏模块设置 + const [sidebarModulesUser, setSidebarModulesUser] = useState({}); + + // 管理员全局配置 + const [adminConfig, setAdminConfig] = useState(null); + + // 处理区域级别开关变更 + function handleSectionChange(sectionKey) { + return (checked) => { + const newModules = { + ...sidebarModulesUser, + [sectionKey]: { + ...sidebarModulesUser[sectionKey], + enabled: checked + } + }; + setSidebarModulesUser(newModules); + console.log('用户边栏区域配置变更:', sectionKey, checked, newModules); + }; + } + + // 处理功能级别开关变更 + function handleModuleChange(sectionKey, moduleKey) { + return (checked) => { + const newModules = { + ...sidebarModulesUser, + [sectionKey]: { + ...sidebarModulesUser[sectionKey], + [moduleKey]: checked + } + }; + setSidebarModulesUser(newModules); + console.log('用户边栏功能配置变更:', sectionKey, moduleKey, checked, newModules); + }; + } + + // 重置为默认配置(基于权限过滤) + function resetSidebarModules() { + const defaultConfig = generateDefaultConfig(); + setSidebarModulesUser(defaultConfig); + showSuccess(t('已重置为默认配置')); + console.log('用户边栏配置重置为默认:', defaultConfig); + } + + // 保存配置 + async function onSubmit() { + setLoading(true); + try { + console.log('保存用户边栏配置:', sidebarModulesUser); + const res = await API.put('/api/user/self', { + sidebar_modules: JSON.stringify(sidebarModulesUser), + }); + const { success, message } = res.data; + if (success) { + showSuccess(t('保存成功')); + console.log('用户边栏配置保存成功'); + + // 刷新useSidebar钩子中的用户配置,实现实时更新 + await refreshUserConfig(); + console.log('用户边栏配置已刷新,边栏将立即更新'); + } else { + showError(message); + console.error('用户边栏配置保存失败:', message); + } + } catch (error) { + showError(t('保存失败,请重试')); + console.error('用户边栏配置保存异常:', error); + } finally { + setLoading(false); + } + } + + // 统一的配置加载逻辑 + 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]); + + // 检查功能是否被管理员允许 + const isAllowedByAdmin = (sectionKey, moduleKey = null) => { + if (!adminConfig) return true; + + if (moduleKey) { + return adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]; + } else { + return adminConfig[sectionKey]?.enabled; + } + }; + + // 区域配置数据(根据后端权限过滤) + const sectionConfigs = [ + { + key: 'chat', + title: t('聊天区域'), + description: t('操练场和聊天功能'), + modules: [ + { key: 'playground', title: t('操练场'), description: t('AI模型测试环境') }, + { key: 'chat', title: t('聊天'), description: t('聊天会话管理') } + ] + }, + { + key: 'console', + title: t('控制台区域'), + description: t('数据管理和日志查看'), + modules: [ + { key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, + { key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, + { key: 'log', title: t('使用日志'), description: t('API使用记录') }, + { key: 'midjourney', title: t('绘图日志'), description: t('绘图任务记录') }, + { key: 'task', title: t('任务日志'), description: t('系统任务记录') } + ] + }, + { + key: 'personal', + title: t('个人中心区域'), + description: t('用户个人功能'), + modules: [ + { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') }, + { key: 'personal', title: t('个人设置'), description: t('个人信息设置') } + ] + }, + { + key: 'admin', + title: t('管理员区域'), + description: t('系统管理功能'), + modules: [ + { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, + { key: 'models', title: t('模型管理'), description: t('AI模型配置') }, + { key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') }, + { key: 'user', title: t('用户管理'), description: t('用户账户管理') }, + { key: 'setting', title: t('系统设置'), description: t('系统参数配置') } + ] + } + ].filter(section => { + // 使用后端权限验证替代前端角色判断 + return isSidebarSectionAllowed(section.key); + }).map(section => ({ + ...section, + modules: section.modules.filter(module => + isSidebarModuleAllowed(section.key, module.key) + ) + })).filter(section => + // 过滤掉没有可用模块的区域 + section.modules.length > 0 && isAllowedByAdmin(section.key) + ); + + return ( + + {/* 卡片头部 */} +
+ + + +
+ + {t('左侧边栏个人设置')} + +
+ {t('个性化设置左侧边栏的显示内容')} +
+
+
+ +
+ + {t('您可以个性化设置侧边栏的要显示功能')} + +
+ + {sectionConfigs.map((section) => ( +
+ {/* 区域标题和总开关 */} +
+
+
+ {section.title} +
+ + {section.description} + +
+ +
+ + {/* 功能模块网格 */} + + {section.modules.map((module) => ( + + +
+
+
+ {module.title} +
+ + {module.description} + +
+
+ +
+
+
+ + ))} +
+
+ ))} + + {/* 底部按钮 */} +
+ + +
+
+ ); +}