487 lines
15 KiB
JavaScript
487 lines
15 KiB
JavaScript
/*
|
||
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 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 React, { useState, useEffect, useContext } from 'react';
|
||
import { useTranslation } from 'react-i18next';
|
||
import {
|
||
Card,
|
||
Form,
|
||
Button,
|
||
Switch,
|
||
Row,
|
||
Col,
|
||
Typography,
|
||
RadioGroup,
|
||
Radio,
|
||
} from '@douyinfe/semi-ui';
|
||
import { API, showSuccess, showError } from '../../../helpers';
|
||
import { StatusContext } from '../../../context/Status';
|
||
import { USER_ROLES, USER_ROLE_NAMES } from '../../../constants/user.constants';
|
||
|
||
const { Text } = Typography;
|
||
|
||
// 区域配置数据(不依赖组件 state/props,提取到外部避免 Temporal Dead Zone)
|
||
const sectionConfigs = [
|
||
{
|
||
key: 'chat',
|
||
title_key: '聊天区域',
|
||
desc_key: '操练场和聊天功能',
|
||
modules: [
|
||
{
|
||
key: 'playground',
|
||
title_key: '操练场',
|
||
desc_key: 'AI模型测试环境',
|
||
},
|
||
{ key: 'chat', title_key: '聊天', desc_key: '聊天会话管理' },
|
||
],
|
||
},
|
||
{
|
||
key: 'console',
|
||
title_key: '控制台区域',
|
||
desc_key: '数据管理和日志查看',
|
||
modules: [
|
||
{ key: 'detail', title_key: '数据看板', desc_key: '系统数据统计' },
|
||
{ key: 'token', title_key: '令牌管理', desc_key: 'API令牌管理' },
|
||
{ key: 'log', title_key: '使用日志', desc_key: 'API使用记录' },
|
||
{
|
||
key: 'midjourney',
|
||
title_key: '绘图日志',
|
||
desc_key: '绘图任务记录',
|
||
},
|
||
{ key: 'task', title_key: '任务日志', desc_key: '系统任务记录' },
|
||
],
|
||
},
|
||
{
|
||
key: 'personal',
|
||
title_key: '个人中心区域',
|
||
desc_key: '用户个人功能',
|
||
modules: [
|
||
{ key: 'topup', title_key: '钱包管理', desc_key: '余额充值管理' },
|
||
{
|
||
key: 'personal',
|
||
title_key: '个人设置',
|
||
desc_key: '个人信息设置',
|
||
},
|
||
{
|
||
key: 'supplier',
|
||
title_key: '供应商',
|
||
desc_key: '供应商申请',
|
||
},
|
||
{
|
||
key: 'supplier-apply',
|
||
title_key: '供应商-申请',
|
||
desc_key: '供应商申请',
|
||
},
|
||
{
|
||
key: 'supplier-channel',
|
||
title_key: '供应商-渠道管理',
|
||
desc_key: '供应商渠道管理',
|
||
},
|
||
{
|
||
key: 'supplier-pricing-settings',
|
||
title_key: '供应商-定价设置',
|
||
desc_key: '供应商定价设置',
|
||
},
|
||
{
|
||
key: 'supplier-dashboard',
|
||
title_key: '供应商-数据看板',
|
||
desc_key: '供应商数据看板',
|
||
},
|
||
{
|
||
key: 'distributor_center',
|
||
title_key: '代理分销',
|
||
desc_key: '代理邀请与收益',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'admin',
|
||
title_key: '管理员区域',
|
||
desc_key: '系统管理功能',
|
||
adminOnly: true,
|
||
modules: [
|
||
{ key: 'channel', title_key: '渠道管理', desc_key: 'API渠道配置' },
|
||
{ key: 'models', title_key: '模型管理', desc_key: 'AI模型配置' },
|
||
{
|
||
key: 'deployment',
|
||
title_key: '模型部署',
|
||
desc_key: '模型部署管理',
|
||
},
|
||
{
|
||
key: 'subscription',
|
||
title_key: '订阅管理',
|
||
desc_key: '订阅套餐管理',
|
||
},
|
||
{
|
||
key: 'redemption',
|
||
title_key: '兑换码管理',
|
||
desc_key: '兑换码生成管理',
|
||
},
|
||
{ key: 'user', title_key: '用户管理', desc_key: '用户账户管理' },
|
||
{
|
||
key: 'operation-log',
|
||
title_key: '操作记录',
|
||
desc_key: '系统操作日志审计',
|
||
},
|
||
{
|
||
key: 'model-heat',
|
||
title_key: '热度配置',
|
||
desc_key: '模型热度排行配置',
|
||
},
|
||
{
|
||
key: 'distributor',
|
||
title_key: '代理管理',
|
||
desc_key: '代理申请与人员管理',
|
||
},
|
||
{
|
||
key: 'supplier-management',
|
||
title_key: '供应商管理',
|
||
desc_key: '供应商相关管理',
|
||
},
|
||
{
|
||
key: 'supplier-application-approval',
|
||
title_key: '申请审批',
|
||
desc_key: '供应商申请审批管理',
|
||
},
|
||
{
|
||
key: 'supplier-list',
|
||
title_key: '供应商列表',
|
||
desc_key: '管理所有供应商信息',
|
||
},
|
||
{
|
||
key: 'supplier-dashboard',
|
||
title_key: '数据看板',
|
||
desc_key: '查看供应商模型聚合统计',
|
||
},
|
||
{
|
||
key: 'setting',
|
||
title_key: '系统设置',
|
||
desc_key: '系统参数配置',
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
// 基于区域配置生成默认值(所有模块开启)
|
||
const buildDefaultRoleConfig = () => {
|
||
const config = {};
|
||
sectionConfigs.forEach((section) => {
|
||
config[section.key] = { enabled: true };
|
||
section.modules.forEach((module) => {
|
||
config[section.key][module.key] = true;
|
||
});
|
||
});
|
||
return config;
|
||
};
|
||
|
||
export default function SettingsSidebarModulesAdmin(props) {
|
||
const { t } = useTranslation();
|
||
const [loading, setLoading] = useState(false);
|
||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||
|
||
// 翻译 sectionConfigs 的 title/description
|
||
const translatedSections = sectionConfigs.map((section) => ({
|
||
...section,
|
||
title: t(section.title_key),
|
||
description: t(section.desc_key),
|
||
modules: section.modules.map((module) => ({
|
||
...module,
|
||
title: t(module.title_key),
|
||
description: t(module.desc_key),
|
||
})),
|
||
}));
|
||
|
||
// 角色配置
|
||
const [roleModulesConfig, setRoleModulesConfig] = useState({
|
||
[USER_ROLES.USER]: buildDefaultRoleConfig(),
|
||
[USER_ROLES.ADMIN]: buildDefaultRoleConfig(),
|
||
[USER_ROLES.ROOT]: buildDefaultRoleConfig(),
|
||
});
|
||
const [selectedRole, setSelectedRole] = useState(String(USER_ROLES.USER));
|
||
|
||
// 将已保存的角色配置与默认配置合并(确保新增模块自动出现)
|
||
const mergeWithDefaults = (saved) => {
|
||
const defaults = buildDefaultRoleConfig();
|
||
if (!saved || typeof saved !== 'object') return defaults;
|
||
const merged = { ...defaults };
|
||
for (const [sectionKey, sectionConfig] of Object.entries(saved)) {
|
||
if (!sectionConfig || typeof sectionConfig !== 'object') continue;
|
||
merged[sectionKey] = { ...(merged[sectionKey] || {}), ...sectionConfig };
|
||
}
|
||
return merged;
|
||
};
|
||
|
||
// 加载角色配置
|
||
useEffect(() => {
|
||
if (props.options && props.options.SidebarModulesByRole) {
|
||
try {
|
||
const config = JSON.parse(props.options.SidebarModulesByRole);
|
||
setRoleModulesConfig({
|
||
[USER_ROLES.USER]: mergeWithDefaults(config[USER_ROLES.USER]),
|
||
[USER_ROLES.ADMIN]: mergeWithDefaults(config[USER_ROLES.ADMIN]),
|
||
[USER_ROLES.ROOT]: mergeWithDefaults(config[USER_ROLES.ROOT]),
|
||
});
|
||
} catch (error) {
|
||
// 使用默认值
|
||
}
|
||
}
|
||
}, [props.options]);
|
||
|
||
// 当前选中角色
|
||
const currentRole = Number(selectedRole);
|
||
const currentUserRoleConfig = roleModulesConfig[currentRole] || {};
|
||
|
||
// 是否为管理员/超级管理员角色 — 显示管理员区域
|
||
const showAdminSection = currentRole === USER_ROLES.ADMIN || currentRole === USER_ROLES.ROOT;
|
||
|
||
// 处理角色模块开关变更
|
||
function handleRoleModuleChange(sectionKey, moduleKey) {
|
||
return (checked) => {
|
||
setRoleModulesConfig((prev) => ({
|
||
...prev,
|
||
[currentRole]: {
|
||
...(prev[currentRole] || {}),
|
||
[sectionKey]: {
|
||
...(prev[currentRole]?.[sectionKey] || {}),
|
||
[moduleKey]: checked,
|
||
},
|
||
},
|
||
}));
|
||
};
|
||
}
|
||
|
||
// 重置当前角色配置为默认
|
||
function resetCurrentRoleConfig() {
|
||
setRoleModulesConfig((prev) => ({
|
||
...prev,
|
||
[currentRole]: buildDefaultRoleConfig(),
|
||
}));
|
||
showSuccess(t('已重置角色配置'));
|
||
}
|
||
|
||
// 保存角色配置(唯一保存入口)
|
||
async function onSubmit() {
|
||
setLoading(true);
|
||
try {
|
||
const res = await API.put('/api/option/', {
|
||
key: 'SidebarModulesByRole',
|
||
value: JSON.stringify(roleModulesConfig),
|
||
});
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('保存成功'));
|
||
statusDispatch({
|
||
type: 'set',
|
||
payload: {
|
||
...statusState.status,
|
||
SidebarModulesByRole: JSON.stringify(roleModulesConfig),
|
||
},
|
||
});
|
||
if (props.refresh) {
|
||
await props.refresh();
|
||
}
|
||
} else {
|
||
showError(message);
|
||
}
|
||
} catch (error) {
|
||
showError(t('保存失败,请重试'));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
// 根据当前角色过滤需要显示的区域(普通用户不显示admin区域)
|
||
const visibleSections = translatedSections.filter(
|
||
(section) => !section.adminOnly || showAdminSection,
|
||
);
|
||
|
||
return (
|
||
<Card>
|
||
<Form.Section
|
||
text={t('角色菜单权限配置')}
|
||
extraText={t(
|
||
'针对不同角色配置菜单可见性,关闭后该角色用户无法看到对应模块。管理员区域仅对管理员和超级管理员角色可见。',
|
||
)}
|
||
>
|
||
{/* 角色选择 */}
|
||
<div
|
||
style={{
|
||
padding: '12px 16px',
|
||
backgroundColor: 'var(--semi-color-fill-0)',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--semi-color-border)',
|
||
marginBottom: '24px',
|
||
}}
|
||
>
|
||
<Text style={{ marginRight: '12px', fontWeight: '500' }}>{t('选择角色')}:</Text>
|
||
<RadioGroup
|
||
type='button'
|
||
buttonStyle='solid'
|
||
value={selectedRole}
|
||
onChange={(e) => setSelectedRole(e.target.value)}
|
||
>
|
||
<Radio value={String(USER_ROLES.USER)}>{USER_ROLE_NAMES[USER_ROLES.USER]}</Radio>
|
||
<Radio value={String(USER_ROLES.ADMIN)}>{USER_ROLE_NAMES[USER_ROLES.ADMIN]}</Radio>
|
||
<Radio value={String(USER_ROLES.ROOT)}>{USER_ROLE_NAMES[USER_ROLES.ROOT]}</Radio>
|
||
</RadioGroup>
|
||
</div>
|
||
|
||
{/* 角色模块配置 */}
|
||
{visibleSections.map((section) => (
|
||
<div key={section.key} style={{ marginBottom: '24px' }}>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: '16px',
|
||
padding: '12px 16px',
|
||
backgroundColor: 'var(--semi-color-fill-0)',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--semi-color-border)',
|
||
}}
|
||
>
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontWeight: '600',
|
||
fontSize: '16px',
|
||
color: 'var(--semi-color-text-0)',
|
||
marginBottom: '4px',
|
||
}}
|
||
>
|
||
{section.title}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
}}
|
||
>
|
||
{section.description}
|
||
</Text>
|
||
</div>
|
||
<Switch
|
||
checked={currentUserRoleConfig?.[section.key]?.enabled !== false}
|
||
onChange={handleRoleModuleChange(section.key, 'enabled')}
|
||
size='default'
|
||
/>
|
||
</div>
|
||
|
||
<Row gutter={[16, 16]}>
|
||
{section.modules.map((module) => (
|
||
<Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
|
||
<Card
|
||
bodyStyle={{ padding: '16px' }}
|
||
style={{
|
||
opacity: currentUserRoleConfig?.[section.key]?.enabled !== false
|
||
? 1
|
||
: 0.5,
|
||
transition: 'opacity 0.2s',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
height: '100%',
|
||
}}
|
||
>
|
||
<div style={{ flex: 1, textAlign: 'left' }}>
|
||
<div
|
||
style={{
|
||
fontWeight: '600',
|
||
fontSize: '14px',
|
||
color: 'var(--semi-color-text-0)',
|
||
marginBottom: '4px',
|
||
}}
|
||
>
|
||
{module.title}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
}}
|
||
>
|
||
{module.description}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginLeft: '16px' }}>
|
||
<Switch
|
||
checked={
|
||
currentUserRoleConfig?.[section.key]?.[module.key] !== false
|
||
}
|
||
onChange={handleRoleModuleChange(section.key, module.key)}
|
||
size='default'
|
||
disabled={
|
||
currentUserRoleConfig?.[section.key]?.enabled === false
|
||
}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
</div>
|
||
))}
|
||
|
||
{/* 操作按钮 */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
gap: '12px',
|
||
justifyContent: 'flex-start',
|
||
alignItems: 'center',
|
||
paddingTop: '8px',
|
||
borderTop: '1px solid var(--semi-color-border)',
|
||
marginTop: '16px',
|
||
}}
|
||
>
|
||
<Button
|
||
size='default'
|
||
type='tertiary'
|
||
onClick={resetCurrentRoleConfig}
|
||
style={{ borderRadius: '6px', fontWeight: '500' }}
|
||
>
|
||
{t('重置当前角色')}
|
||
</Button>
|
||
<Button
|
||
size='default'
|
||
type='primary'
|
||
onClick={onSubmit}
|
||
loading={loading}
|
||
style={{ borderRadius: '6px', fontWeight: '500', minWidth: '100px' }}
|
||
>
|
||
{t('保存设置')}
|
||
</Button>
|
||
</div>
|
||
</Form.Section>
|
||
</Card>
|
||
);
|
||
}
|