tokenFactory/web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin...

487 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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>
);
}