顶栏和侧边栏管理

增加用户体验
This commit is contained in:
F。
2025-08-31 07:07:40 +08:00
parent 0d57b1acd4
commit d0d6168e2f
19 changed files with 2428 additions and 170 deletions

View File

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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,

View File

@@ -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 {

View File

@@ -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 <https://www.gnu.org/licenses/>.
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
};
};

View File

@@ -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;