2025-08-17 00:49:54 +08:00
|
|
|
|
/*
|
|
|
|
|
|
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
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-08-31 07:07:40 +08:00
|
|
|
|
import React, { useRef, useEffect, useState, useContext } from 'react';
|
2025-08-17 00:49:54 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Avatar,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Radio,
|
|
|
|
|
|
Toast,
|
|
|
|
|
|
Tabs,
|
2025-08-30 21:15:10 +08:00
|
|
|
|
TabPane,
|
2025-08-31 07:07:40 +08:00
|
|
|
|
Switch,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
2025-08-17 00:49:54 +08:00
|
|
|
|
} from '@douyinfe/semi-ui';
|
2025-08-30 21:15:10 +08:00
|
|
|
|
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
|
2025-08-31 07:07:40 +08:00
|
|
|
|
import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react';
|
2025-09-01 23:43:39 +08:00
|
|
|
|
import {
|
|
|
|
|
|
renderQuotaWithPrompt,
|
|
|
|
|
|
API,
|
|
|
|
|
|
showSuccess,
|
|
|
|
|
|
showError,
|
|
|
|
|
|
} from '../../../../helpers';
|
2025-08-17 00:49:54 +08:00
|
|
|
|
import CodeViewer from '../../../playground/CodeViewer';
|
2025-08-31 07:07:40 +08:00
|
|
|
|
import { StatusContext } from '../../../../context/Status';
|
|
|
|
|
|
import { UserContext } from '../../../../context/User';
|
|
|
|
|
|
import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
|
2026-01-03 12:37:50 +08:00
|
|
|
|
import {
|
|
|
|
|
|
mergeAdminConfig,
|
|
|
|
|
|
useSidebar,
|
|
|
|
|
|
} from '../../../../hooks/common/useSidebar';
|
2025-08-17 00:49:54 +08:00
|
|
|
|
|
|
|
|
|
|
const NotificationSettings = ({
|
|
|
|
|
|
t,
|
|
|
|
|
|
notificationSettings,
|
|
|
|
|
|
handleNotificationSettingChange,
|
2025-08-30 21:15:10 +08:00
|
|
|
|
saveNotificationSettings,
|
2025-08-17 00:49:54 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
const formApiRef = useRef(null);
|
2025-08-31 07:07:40 +08:00
|
|
|
|
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,
|
2025-09-01 23:43:39 +08:00
|
|
|
|
chat: true,
|
2025-08-31 07:07:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
console: {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
detail: true,
|
|
|
|
|
|
token: true,
|
|
|
|
|
|
log: true,
|
|
|
|
|
|
midjourney: true,
|
2025-09-01 23:43:39 +08:00
|
|
|
|
task: true,
|
2025-08-31 07:07:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
personal: {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
topup: true,
|
2025-09-01 23:43:39 +08:00
|
|
|
|
personal: true,
|
2025-08-31 07:07:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
admin: {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
channel: true,
|
|
|
|
|
|
models: true,
|
2026-01-03 12:37:50 +08:00
|
|
|
|
deployment: true,
|
2025-08-31 07:07:40 +08:00
|
|
|
|
redemption: true,
|
|
|
|
|
|
user: true,
|
2025-09-01 23:43:39 +08:00
|
|
|
|
setting: true,
|
|
|
|
|
|
},
|
2025-08-31 07:07:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
const [adminConfig, setAdminConfig] = useState(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用后端权限验证替代前端角色判断
|
|
|
|
|
|
const {
|
|
|
|
|
|
permissions,
|
|
|
|
|
|
loading: permissionsLoading,
|
|
|
|
|
|
hasSidebarSettingsPermission,
|
|
|
|
|
|
isSidebarSectionAllowed,
|
|
|
|
|
|
isSidebarModuleAllowed,
|
|
|
|
|
|
} = useUserPermissions();
|
|
|
|
|
|
|
2025-09-01 09:52:52 +08:00
|
|
|
|
// 使用useSidebar钩子获取刷新方法
|
|
|
|
|
|
const { refreshUserConfig } = useSidebar();
|
|
|
|
|
|
|
2025-08-31 07:07:40 +08:00
|
|
|
|
// 左侧边栏设置处理函数
|
|
|
|
|
|
const handleSectionChange = (sectionKey) => {
|
|
|
|
|
|
return (checked) => {
|
|
|
|
|
|
const newModules = {
|
|
|
|
|
|
...sidebarModulesUser,
|
|
|
|
|
|
[sectionKey]: {
|
|
|
|
|
|
...sidebarModulesUser[sectionKey],
|
2025-09-01 23:43:39 +08:00
|
|
|
|
enabled: checked,
|
|
|
|
|
|
},
|
2025-08-31 07:07:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
setSidebarModulesUser(newModules);
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleModuleChange = (sectionKey, moduleKey) => {
|
|
|
|
|
|
return (checked) => {
|
|
|
|
|
|
const newModules = {
|
|
|
|
|
|
...sidebarModulesUser,
|
|
|
|
|
|
[sectionKey]: {
|
|
|
|
|
|
...sidebarModulesUser[sectionKey],
|
2025-09-01 23:43:39 +08:00
|
|
|
|
[moduleKey]: checked,
|
|
|
|
|
|
},
|
2025-08-31 07:07:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
setSidebarModulesUser(newModules);
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const saveSidebarSettings = async () => {
|
|
|
|
|
|
setSidebarLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.put('/api/user/self', {
|
2025-09-01 23:43:39 +08:00
|
|
|
|
sidebar_modules: JSON.stringify(sidebarModulesUser),
|
2025-08-31 07:07:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
if (res.data.success) {
|
|
|
|
|
|
showSuccess(t('侧边栏设置保存成功'));
|
2025-09-01 09:52:52 +08:00
|
|
|
|
|
|
|
|
|
|
// 刷新useSidebar钩子中的用户配置,实现实时更新
|
|
|
|
|
|
await refreshUserConfig();
|
2025-08-31 07:07:40 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
showError(res.data.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
showError(t('保存失败'));
|
|
|
|
|
|
}
|
|
|
|
|
|
setSidebarLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resetSidebarModules = () => {
|
|
|
|
|
|
const defaultConfig = {
|
|
|
|
|
|
chat: { enabled: true, playground: true, chat: true },
|
2025-09-01 23:43:39 +08:00
|
|
|
|
console: {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
detail: true,
|
|
|
|
|
|
token: true,
|
|
|
|
|
|
log: true,
|
|
|
|
|
|
midjourney: true,
|
|
|
|
|
|
task: true,
|
|
|
|
|
|
},
|
2025-08-31 07:07:40 +08:00
|
|
|
|
personal: { enabled: true, topup: true, personal: true },
|
2025-09-01 23:43:39 +08:00
|
|
|
|
admin: {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
channel: true,
|
|
|
|
|
|
models: true,
|
2026-01-03 12:37:50 +08:00
|
|
|
|
deployment: true,
|
2025-09-01 23:43:39 +08:00
|
|
|
|
redemption: true,
|
|
|
|
|
|
user: true,
|
|
|
|
|
|
setting: true,
|
|
|
|
|
|
},
|
2025-08-31 07:07:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
setSidebarModulesUser(defaultConfig);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 加载左侧边栏配置
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadSidebarConfigs = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取管理员全局配置
|
|
|
|
|
|
if (statusState?.status?.SidebarModulesAdmin) {
|
2026-01-03 12:37:50 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const adminConf = JSON.parse(
|
|
|
|
|
|
statusState.status.SidebarModulesAdmin,
|
|
|
|
|
|
);
|
|
|
|
|
|
setAdminConfig(mergeAdminConfig(adminConf));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setAdminConfig(mergeAdminConfig(null));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setAdminConfig(mergeAdminConfig(null));
|
2025-08-31 07:07:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户个人配置
|
|
|
|
|
|
const userRes = await API.get('/api/user/self');
|
|
|
|
|
|
if (userRes.data.success && userRes.data.data.sidebar_modules) {
|
2026-01-03 12:37:50 +08:00
|
|
|
|
let userConf;
|
|
|
|
|
|
if (typeof userRes.data.data.sidebar_modules === 'string') {
|
|
|
|
|
|
userConf = JSON.parse(userRes.data.data.sidebar_modules);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
userConf = userRes.data.data.sidebar_modules;
|
|
|
|
|
|
}
|
2025-08-31 07:07:40 +08:00
|
|
|
|
setSidebarModulesUser(userConf);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载边栏配置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadSidebarConfigs();
|
|
|
|
|
|
}, [statusState]);
|
2025-08-17 00:49:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化表单值
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (formApiRef.current && notificationSettings) {
|
|
|
|
|
|
formApiRef.current.setValues(notificationSettings);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [notificationSettings]);
|
|
|
|
|
|
|
|
|
|
|
|
// 处理表单字段变化
|
|
|
|
|
|
const handleFormChange = (field, value) => {
|
|
|
|
|
|
handleNotificationSettingChange(field, value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-31 07:07:40 +08:00
|
|
|
|
// 检查功能是否被管理员允许
|
|
|
|
|
|
const isAllowedByAdmin = (sectionKey, moduleKey = null) => {
|
|
|
|
|
|
if (!adminConfig) return true;
|
|
|
|
|
|
|
|
|
|
|
|
if (moduleKey) {
|
2025-09-01 23:43:39 +08:00
|
|
|
|
return (
|
|
|
|
|
|
adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]
|
|
|
|
|
|
);
|
2025-08-31 07:07:40 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
return adminConfig[sectionKey]?.enabled;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 区域配置数据(根据权限过滤)
|
|
|
|
|
|
const sectionConfigs = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'chat',
|
|
|
|
|
|
title: t('聊天区域'),
|
|
|
|
|
|
description: t('操练场和聊天功能'),
|
|
|
|
|
|
modules: [
|
2025-09-01 23:43:39 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'playground',
|
|
|
|
|
|
title: t('操练场'),
|
|
|
|
|
|
description: t('AI模型测试环境'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
|
|
|
|
|
|
],
|
2025-08-31 07:07:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
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使用记录') },
|
2025-09-01 23:43:39 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'midjourney',
|
|
|
|
|
|
title: t('绘图日志'),
|
|
|
|
|
|
description: t('绘图任务记录'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{ key: 'task', title: t('任务日志'), description: t('系统任务记录') },
|
|
|
|
|
|
],
|
2025-08-31 07:07:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'personal',
|
|
|
|
|
|
title: t('个人中心区域'),
|
|
|
|
|
|
description: t('用户个人功能'),
|
|
|
|
|
|
modules: [
|
|
|
|
|
|
{ key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
|
2025-09-01 23:43:39 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'personal',
|
|
|
|
|
|
title: t('个人设置'),
|
|
|
|
|
|
description: t('个人信息设置'),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-08-31 07:07:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 管理员区域:根据后端权限控制显示
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'admin',
|
|
|
|
|
|
title: t('管理员区域'),
|
|
|
|
|
|
description: t('系统管理功能'),
|
|
|
|
|
|
modules: [
|
|
|
|
|
|
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
|
|
|
|
|
|
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
|
2026-01-03 12:37:50 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'deployment',
|
|
|
|
|
|
title: t('模型部署'),
|
|
|
|
|
|
description: t('模型部署管理'),
|
|
|
|
|
|
},
|
2025-09-01 23:43:39 +08:00
|
|
|
|
{
|
|
|
|
|
|
key: 'redemption',
|
|
|
|
|
|
title: t('兑换码管理'),
|
|
|
|
|
|
description: t('兑换码生成管理'),
|
|
|
|
|
|
},
|
2025-08-31 07:07:40 +08:00
|
|
|
|
{ key: 'user', title: t('用户管理'), description: t('用户账户管理') },
|
2025-09-01 23:43:39 +08:00
|
|
|
|
{
|
|
|
|
|
|
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),
|
|
|
|
|
|
);
|
2025-08-31 07:07:40 +08:00
|
|
|
|
|
2025-08-17 00:49:54 +08:00
|
|
|
|
// 表单提交
|
|
|
|
|
|
const handleSubmit = () => {
|
|
|
|
|
|
if (formApiRef.current) {
|
2025-08-30 21:15:10 +08:00
|
|
|
|
formApiRef.current
|
|
|
|
|
|
.validate()
|
2025-08-17 00:49:54 +08:00
|
|
|
|
.then(() => {
|
|
|
|
|
|
saveNotificationSettings();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((errors) => {
|
|
|
|
|
|
console.log('表单验证失败:', errors);
|
|
|
|
|
|
Toast.error(t('请检查表单填写是否正确'));
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
saveNotificationSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card
|
2025-08-30 21:15:10 +08:00
|
|
|
|
className='!rounded-2xl shadow-sm border-0'
|
2025-08-17 00:49:54 +08:00
|
|
|
|
footer={
|
2025-08-31 07:07:40 +08:00
|
|
|
|
<div className='flex justify-end gap-3'>
|
|
|
|
|
|
{activeTabKey === 'sidebar' ? (
|
|
|
|
|
|
// 边栏设置标签页的按钮
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type='tertiary'
|
|
|
|
|
|
onClick={resetSidebarModules}
|
|
|
|
|
|
className='!rounded-lg'
|
|
|
|
|
|
>
|
|
|
|
|
|
{t('重置为默认')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type='primary'
|
|
|
|
|
|
onClick={saveSidebarSettings}
|
|
|
|
|
|
loading={sidebarLoading}
|
|
|
|
|
|
className='!rounded-lg'
|
|
|
|
|
|
>
|
2025-09-01 09:52:52 +08:00
|
|
|
|
{t('保存设置')}
|
2025-08-31 07:07:40 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
// 其他标签页的通用保存按钮
|
|
|
|
|
|
<Button type='primary' onClick={handleSubmit}>
|
|
|
|
|
|
{t('保存设置')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 卡片头部 */}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='flex items-center mb-4'>
|
|
|
|
|
|
<Avatar size='small' color='blue' className='mr-3 shadow-md'>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
<Bell size={16} />
|
|
|
|
|
|
</Avatar>
|
|
|
|
|
|
<div>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<Typography.Text className='text-lg font-medium'>
|
|
|
|
|
|
{t('其他设置')}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
<div className='text-xs text-gray-600'>
|
|
|
|
|
|
{t('通知、价格和隐私相关设置')}
|
|
|
|
|
|
</div>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Form
|
|
|
|
|
|
getFormApi={(api) => (formApiRef.current = api)}
|
|
|
|
|
|
initValues={notificationSettings}
|
|
|
|
|
|
onSubmit={handleSubmit}
|
|
|
|
|
|
>
|
|
|
|
|
|
{() => (
|
2025-08-31 07:07:40 +08:00
|
|
|
|
<Tabs
|
|
|
|
|
|
type='card'
|
|
|
|
|
|
defaultActiveKey='notification'
|
|
|
|
|
|
onChange={(key) => setActiveTabKey(key)}
|
|
|
|
|
|
>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{/* 通知配置 Tab */}
|
|
|
|
|
|
<TabPane
|
|
|
|
|
|
tab={
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='flex items-center'>
|
|
|
|
|
|
<Bell size={16} className='mr-2' />
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{t('通知配置')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
itemKey='notification'
|
2025-08-17 00:49:54 +08:00
|
|
|
|
>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='py-4'>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
<Form.RadioGroup
|
|
|
|
|
|
field='warningType'
|
|
|
|
|
|
label={t('通知方式')}
|
|
|
|
|
|
initValue={notificationSettings.warningType}
|
|
|
|
|
|
onChange={(value) => handleFormChange('warningType', value)}
|
|
|
|
|
|
rules={[{ required: true, message: t('请选择通知方式') }]}
|
|
|
|
|
|
>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<Radio value='email'>{t('邮件通知')}</Radio>
|
|
|
|
|
|
<Radio value='webhook'>{t('Webhook通知')}</Radio>
|
2025-09-01 15:57:23 +08:00
|
|
|
|
<Radio value='bark'>{t('Bark通知')}</Radio>
|
2025-10-01 19:15:00 +08:00
|
|
|
|
<Radio value='gotify'>{t('Gotify通知')}</Radio>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</Form.RadioGroup>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.AutoComplete
|
|
|
|
|
|
field='warningThreshold'
|
|
|
|
|
|
label={
|
|
|
|
|
|
<span>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
{t('额度预警阈值')}{' '}
|
|
|
|
|
|
{renderQuotaWithPrompt(
|
|
|
|
|
|
notificationSettings.warningThreshold,
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder={t('请输入预警额度')}
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ value: 100000, label: '0.2$' },
|
|
|
|
|
|
{ value: 500000, label: '1$' },
|
2026-01-19 12:57:51 +08:00
|
|
|
|
{ value: 1000000, label: '2$' },
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{ value: 5000000, label: '10$' },
|
|
|
|
|
|
]}
|
|
|
|
|
|
onChange={(val) => handleFormChange('warningThreshold', val)}
|
|
|
|
|
|
prefix={<IconBell />}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
extraText={t(
|
2026-02-07 01:15:59 +08:00
|
|
|
|
'当钱包或订阅剩余额度低于此数值时,系统将通过选择的方式发送通知',
|
2025-08-30 21:15:10 +08:00
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
style={{ width: '100%', maxWidth: '300px' }}
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: t('请输入预警阈值') },
|
|
|
|
|
|
{
|
|
|
|
|
|
validator: (rule, value) => {
|
|
|
|
|
|
const numValue = Number(value);
|
|
|
|
|
|
if (isNaN(numValue) || numValue <= 0) {
|
|
|
|
|
|
return Promise.reject(t('预警阈值必须为正数'));
|
|
|
|
|
|
}
|
|
|
|
|
|
return Promise.resolve();
|
2025-08-30 21:15:10 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-08-17 00:49:54 +08:00
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 邮件通知设置 */}
|
|
|
|
|
|
{notificationSettings.warningType === 'email' && (
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field='notificationEmail'
|
|
|
|
|
|
label={t('通知邮箱')}
|
|
|
|
|
|
placeholder={t('留空则使用账号绑定的邮箱')}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
onChange={(val) =>
|
|
|
|
|
|
handleFormChange('notificationEmail', val)
|
|
|
|
|
|
}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
prefix={<IconMail />}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
extraText={t(
|
|
|
|
|
|
'设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
showClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Webhook通知设置 */}
|
|
|
|
|
|
{notificationSettings.warningType === 'webhook' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field='webhookUrl'
|
|
|
|
|
|
label={t('Webhook地址')}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
placeholder={t(
|
|
|
|
|
|
'请输入Webhook地址,例如: https://example.com/webhook',
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
onChange={(val) => handleFormChange('webhookUrl', val)}
|
|
|
|
|
|
prefix={<IconLink />}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
extraText={t(
|
|
|
|
|
|
'只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求',
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
showClear
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{
|
2025-08-30 21:15:10 +08:00
|
|
|
|
required:
|
|
|
|
|
|
notificationSettings.warningType === 'webhook',
|
|
|
|
|
|
message: t('请输入Webhook地址'),
|
2025-08-17 00:49:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
pattern: /^https:\/\/.+/,
|
2025-08-30 21:15:10 +08:00
|
|
|
|
message: t('Webhook地址必须以https://开头'),
|
|
|
|
|
|
},
|
2025-08-17 00:49:54 +08:00
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field='webhookSecret'
|
|
|
|
|
|
label={t('接口凭证')}
|
|
|
|
|
|
placeholder={t('请输入密钥')}
|
|
|
|
|
|
onChange={(val) => handleFormChange('webhookSecret', val)}
|
|
|
|
|
|
prefix={<IconKey />}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
extraText={t(
|
|
|
|
|
|
'密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性',
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
showClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Slot label={t('Webhook请求结构说明')}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ height: '200px', marginBottom: '12px' }}>
|
|
|
|
|
|
<CodeViewer
|
|
|
|
|
|
content={{
|
2025-08-30 21:15:10 +08:00
|
|
|
|
type: 'quota_exceed',
|
|
|
|
|
|
title: '额度预警通知',
|
|
|
|
|
|
content:
|
|
|
|
|
|
'您的额度即将用尽,当前剩余额度为 {{value}}',
|
|
|
|
|
|
values: ['$0.99'],
|
|
|
|
|
|
timestamp: 1739950503,
|
2025-08-17 00:49:54 +08:00
|
|
|
|
}}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
title='webhook'
|
|
|
|
|
|
language='json'
|
2025-08-17 00:49:54 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='text-xs text-gray-500 leading-relaxed'>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>type:</strong>{' '}
|
|
|
|
|
|
{t('通知类型 (quota_exceed: 额度预警)')}{' '}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>title:</strong> {t('通知标题')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>content:</strong>{' '}
|
|
|
|
|
|
{t('通知内容,支持 {{value}} 变量占位符')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>values:</strong>{' '}
|
|
|
|
|
|
{t('按顺序替换content中的变量占位符')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>timestamp:</strong> {t('Unix时间戳')}
|
|
|
|
|
|
</div>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form.Slot>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-09-01 15:57:23 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Bark推送设置 */}
|
|
|
|
|
|
{notificationSettings.warningType === 'bark' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field='barkUrl'
|
|
|
|
|
|
label={t('Bark推送URL')}
|
2025-09-01 23:43:39 +08:00
|
|
|
|
placeholder={t(
|
|
|
|
|
|
'请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}',
|
|
|
|
|
|
)}
|
2025-09-01 15:57:23 +08:00
|
|
|
|
onChange={(val) => handleFormChange('barkUrl', val)}
|
|
|
|
|
|
prefix={<IconLink />}
|
|
|
|
|
|
extraText={t(
|
|
|
|
|
|
'支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)',
|
|
|
|
|
|
)}
|
|
|
|
|
|
showClear
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{
|
2025-09-01 23:43:39 +08:00
|
|
|
|
required: notificationSettings.warningType === 'bark',
|
2025-09-01 15:57:23 +08:00
|
|
|
|
message: t('请输入Bark推送URL'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
pattern: /^https?:\/\/.+/,
|
|
|
|
|
|
message: t('Bark推送URL必须以http://或https://开头'),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
|
|
|
|
|
|
<div className='text-sm text-gray-700 mb-3'>
|
|
|
|
|
|
<strong>{t('模板示例')}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className='text-xs text-gray-600 font-mono bg-white p-3 rounded-lg shadow-sm mb-4'>
|
2025-09-01 23:43:39 +08:00
|
|
|
|
https://api.day.app/yourkey/{'{{title}}'}/
|
|
|
|
|
|
{'{{content}}'}?sound=alarm&group=quota
|
2025-09-01 15:57:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className='text-xs text-gray-500 space-y-2'>
|
2025-09-01 23:43:39 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>{'title'}:</strong> {t('通知标题')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>{'content'}:</strong> {t('通知内容')}
|
|
|
|
|
|
</div>
|
2025-09-01 15:57:23 +08:00
|
|
|
|
<div className='mt-3 pt-3 border-t border-gray-200'>
|
2025-09-01 23:43:39 +08:00
|
|
|
|
<span className='text-gray-400'>
|
|
|
|
|
|
{t('更多参数请参考')}
|
|
|
|
|
|
</span>{' '}
|
|
|
|
|
|
<a
|
|
|
|
|
|
href='https://github.com/Finb/Bark'
|
|
|
|
|
|
target='_blank'
|
2025-09-01 15:57:23 +08:00
|
|
|
|
rel='noopener noreferrer'
|
|
|
|
|
|
className='text-blue-500 hover:text-blue-600 font-medium'
|
|
|
|
|
|
>
|
2025-10-01 19:36:19 +08:00
|
|
|
|
Bark {t('官方文档')}
|
2025-09-01 15:57:23 +08:00
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-10-01 19:15:00 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Gotify推送设置 */}
|
|
|
|
|
|
{notificationSettings.warningType === 'gotify' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field='gotifyUrl'
|
|
|
|
|
|
label={t('Gotify服务器地址')}
|
|
|
|
|
|
placeholder={t(
|
|
|
|
|
|
'请输入Gotify服务器地址,例如: https://gotify.example.com',
|
|
|
|
|
|
)}
|
|
|
|
|
|
onChange={(val) => handleFormChange('gotifyUrl', val)}
|
|
|
|
|
|
prefix={<IconLink />}
|
|
|
|
|
|
extraText={t(
|
|
|
|
|
|
'支持HTTP和HTTPS,填写Gotify服务器的完整URL地址',
|
|
|
|
|
|
)}
|
|
|
|
|
|
showClear
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{
|
|
|
|
|
|
required:
|
|
|
|
|
|
notificationSettings.warningType === 'gotify',
|
|
|
|
|
|
message: t('请输入Gotify服务器地址'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
pattern: /^https?:\/\/.+/,
|
✨ feat: Add topup billing history with admin manual completion
Implement comprehensive topup billing system with user history viewing and admin management capabilities.
## Features Added
### Frontend
- Add topup history modal with paginated billing records
- Display order details: trade number, payment method, amount, money, status, create time
- Implement empty state with proper illustrations
- Add payment method column with localized display (Stripe, Alipay, WeChat)
- Add admin manual completion feature for pending orders
- Add Coins icon for recharge amount display
- Integrate "Bills" button in RechargeCard header
- Optimize code quality by using shared utility functions (isAdmin)
- Extract constants for status and payment method mappings
- Use React.useMemo for performance optimization
### Backend
- Create GET `/api/user/topup/self` endpoint for user topup history with pagination
- Create POST `/api/user/topup/complete` endpoint for admin manual order completion
- Add `payment_method` field to TopUp model for tracking payment types
- Implement `GetUserTopUps` method with proper pagination and ordering
- Implement `ManualCompleteTopUp` with transaction safety and row-level locking
- Add application-level mutex locks to prevent concurrent order processing
- Record payment method in Epay and Stripe payment flows
- Ensure idempotency and data consistency with proper error handling
### Internationalization
- Add i18n keys for Chinese (zh), English (en), and French (fr)
- Support for billing-related UI text and status messages
## Technical Improvements
- Use database transactions with FOR UPDATE row-level locking
- Implement sync.Map-based mutex for order-level concurrency control
- Proper error handling and user-friendly toast notifications
- Follow existing codebase patterns for empty states and modals
- Maintain code quality with extracted render functions and constants
## Files Changed
- Backend: controller/topup.go, controller/topup_stripe.go, model/topup.go, router/api-router.go
- Frontend: web/src/components/topup/modals/TopupHistoryModal.jsx (new), web/src/components/topup/RechargeCard.jsx, web/src/components/topup/index.jsx
- i18n: web/src/i18n/locales/{zh,en,fr}.json
2025-10-07 00:22:45 +08:00
|
|
|
|
message: t(
|
|
|
|
|
|
'Gotify服务器地址必须以http://或https://开头',
|
|
|
|
|
|
),
|
2025-10-01 19:15:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field='gotifyToken'
|
|
|
|
|
|
label={t('Gotify应用令牌')}
|
|
|
|
|
|
placeholder={t('请输入Gotify应用令牌')}
|
|
|
|
|
|
onChange={(val) => handleFormChange('gotifyToken', val)}
|
|
|
|
|
|
prefix={<IconKey />}
|
|
|
|
|
|
extraText={t(
|
|
|
|
|
|
'在Gotify服务器创建应用后获得的令牌,用于发送通知',
|
|
|
|
|
|
)}
|
|
|
|
|
|
showClear
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{
|
|
|
|
|
|
required:
|
|
|
|
|
|
notificationSettings.warningType === 'gotify',
|
|
|
|
|
|
message: t('请输入Gotify应用令牌'),
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.AutoComplete
|
|
|
|
|
|
field='gotifyPriority'
|
|
|
|
|
|
label={t('消息优先级')}
|
|
|
|
|
|
placeholder={t('请选择消息优先级')}
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ value: 0, label: t('0 - 最低') },
|
|
|
|
|
|
{ value: 2, label: t('2 - 低') },
|
|
|
|
|
|
{ value: 5, label: t('5 - 正常(默认)') },
|
|
|
|
|
|
{ value: 8, label: t('8 - 高') },
|
|
|
|
|
|
{ value: 10, label: t('10 - 最高') },
|
|
|
|
|
|
]}
|
|
|
|
|
|
onChange={(val) =>
|
|
|
|
|
|
handleFormChange('gotifyPriority', val)
|
|
|
|
|
|
}
|
|
|
|
|
|
prefix={<IconBell />}
|
|
|
|
|
|
extraText={t('消息优先级,范围0-10,默认为5')}
|
|
|
|
|
|
style={{ width: '100%', maxWidth: '300px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
|
|
|
|
|
|
<div className='text-sm text-gray-700 mb-3'>
|
|
|
|
|
|
<strong>{t('配置说明')}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className='text-xs text-gray-500 space-y-2'>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
1. {t('在Gotify服务器的应用管理中创建新应用')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
2.{' '}
|
|
|
|
|
|
{t(
|
|
|
|
|
|
'复制应用的令牌(Token)并填写到上方的应用令牌字段',
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
✨ feat: Add topup billing history with admin manual completion
Implement comprehensive topup billing system with user history viewing and admin management capabilities.
## Features Added
### Frontend
- Add topup history modal with paginated billing records
- Display order details: trade number, payment method, amount, money, status, create time
- Implement empty state with proper illustrations
- Add payment method column with localized display (Stripe, Alipay, WeChat)
- Add admin manual completion feature for pending orders
- Add Coins icon for recharge amount display
- Integrate "Bills" button in RechargeCard header
- Optimize code quality by using shared utility functions (isAdmin)
- Extract constants for status and payment method mappings
- Use React.useMemo for performance optimization
### Backend
- Create GET `/api/user/topup/self` endpoint for user topup history with pagination
- Create POST `/api/user/topup/complete` endpoint for admin manual order completion
- Add `payment_method` field to TopUp model for tracking payment types
- Implement `GetUserTopUps` method with proper pagination and ordering
- Implement `ManualCompleteTopUp` with transaction safety and row-level locking
- Add application-level mutex locks to prevent concurrent order processing
- Record payment method in Epay and Stripe payment flows
- Ensure idempotency and data consistency with proper error handling
### Internationalization
- Add i18n keys for Chinese (zh), English (en), and French (fr)
- Support for billing-related UI text and status messages
## Technical Improvements
- Use database transactions with FOR UPDATE row-level locking
- Implement sync.Map-based mutex for order-level concurrency control
- Proper error handling and user-friendly toast notifications
- Follow existing codebase patterns for empty states and modals
- Maintain code quality with extracted render functions and constants
## Files Changed
- Backend: controller/topup.go, controller/topup_stripe.go, model/topup.go, router/api-router.go
- Frontend: web/src/components/topup/modals/TopupHistoryModal.jsx (new), web/src/components/topup/RechargeCard.jsx, web/src/components/topup/index.jsx
- i18n: web/src/i18n/locales/{zh,en,fr}.json
2025-10-07 00:22:45 +08:00
|
|
|
|
<div>3. {t('填写Gotify服务器的完整URL地址')}</div>
|
2025-10-01 19:15:00 +08:00
|
|
|
|
<div className='mt-3 pt-3 border-t border-gray-200'>
|
|
|
|
|
|
<span className='text-gray-400'>
|
|
|
|
|
|
{t('更多信息请参考')}
|
|
|
|
|
|
</span>{' '}
|
|
|
|
|
|
<a
|
|
|
|
|
|
href='https://gotify.net/'
|
|
|
|
|
|
target='_blank'
|
|
|
|
|
|
rel='noopener noreferrer'
|
|
|
|
|
|
className='text-blue-500 hover:text-blue-600 font-medium'
|
|
|
|
|
|
>
|
2025-10-01 19:36:19 +08:00
|
|
|
|
Gotify {t('官方文档')}
|
2025-10-01 19:15:00 +08:00
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</TabPane>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 价格设置 Tab */}
|
|
|
|
|
|
<TabPane
|
|
|
|
|
|
tab={
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='flex items-center'>
|
|
|
|
|
|
<DollarSign size={16} className='mr-2' />
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{t('价格设置')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
itemKey='pricing'
|
2025-08-17 00:49:54 +08:00
|
|
|
|
>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='py-4'>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
<Form.Switch
|
|
|
|
|
|
field='acceptUnsetModelRatioModel'
|
|
|
|
|
|
label={t('接受未设置价格模型')}
|
|
|
|
|
|
checkedText={t('开')}
|
|
|
|
|
|
uncheckedText={t('关')}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
onChange={(value) =>
|
|
|
|
|
|
handleFormChange('acceptUnsetModelRatioModel', value)
|
|
|
|
|
|
}
|
|
|
|
|
|
extraText={t(
|
|
|
|
|
|
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TabPane>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 隐私设置 Tab */}
|
|
|
|
|
|
<TabPane
|
|
|
|
|
|
tab={
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='flex items-center'>
|
|
|
|
|
|
<ShieldCheck size={16} className='mr-2' />
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{t('隐私设置')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
itemKey='privacy'
|
2025-08-17 00:49:54 +08:00
|
|
|
|
>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='py-4'>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
<Form.Switch
|
|
|
|
|
|
field='recordIpLog'
|
|
|
|
|
|
label={t('记录请求与错误日志IP')}
|
|
|
|
|
|
checkedText={t('开')}
|
|
|
|
|
|
uncheckedText={t('关')}
|
|
|
|
|
|
onChange={(value) => handleFormChange('recordIpLog', value)}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
extraText={t(
|
|
|
|
|
|
'开启后,仅"消费"和"错误"日志将记录您的客户端IP地址',
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TabPane>
|
2025-08-31 07:07:40 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 左侧边栏设置 Tab - 根据后端权限控制显示 */}
|
|
|
|
|
|
{hasSidebarSettingsPermission() && (
|
|
|
|
|
|
<TabPane
|
|
|
|
|
|
tab={
|
|
|
|
|
|
<div className='flex items-center'>
|
|
|
|
|
|
<Settings size={16} className='mr-2' />
|
|
|
|
|
|
{t('边栏设置')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
itemKey='sidebar'
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className='py-4'>
|
|
|
|
|
|
<div className='mb-4'>
|
|
|
|
|
|
<Typography.Text
|
2025-09-01 23:43:39 +08:00
|
|
|
|
type='secondary'
|
|
|
|
|
|
size='small'
|
2025-08-31 07:07:40 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
lineHeight: '1.5',
|
2025-09-01 23:43:39 +08:00
|
|
|
|
color: 'var(--semi-color-text-2)',
|
2025-08-31 07:07:40 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t('您可以个性化设置侧边栏的要显示功能')}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/* 边栏设置功能区域容器 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className='border rounded-xl p-4'
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderColor: 'var(--semi-color-border)',
|
2025-09-01 23:43:39 +08:00
|
|
|
|
backgroundColor: 'var(--semi-color-bg-1)',
|
2025-08-31 07:07:40 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{sectionConfigs.map((section) => (
|
|
|
|
|
|
<div key={section.key} className='mb-6'>
|
|
|
|
|
|
{/* 区域标题和总开关 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className='flex justify-between items-center mb-4 p-4 rounded-lg'
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: 'var(--semi-color-fill-0)',
|
|
|
|
|
|
border: '1px solid var(--semi-color-border-light)',
|
2025-09-01 23:43:39 +08:00
|
|
|
|
borderColor: 'var(--semi-color-fill-1)',
|
2025-08-31 07:07:40 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-01 23:43:39 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div className='font-semibold text-base text-gray-900 mb-1'>
|
|
|
|
|
|
{section.title}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Typography.Text
|
|
|
|
|
|
type='secondary'
|
|
|
|
|
|
size='small'
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
lineHeight: '1.5',
|
|
|
|
|
|
color: 'var(--semi-color-text-2)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{section.description}
|
|
|
|
|
|
</Typography.Text>
|
2025-08-31 07:07:40 +08:00
|
|
|
|
</div>
|
2025-09-01 23:43:39 +08:00
|
|
|
|
<Switch
|
2026-01-03 12:37:50 +08:00
|
|
|
|
checked={
|
|
|
|
|
|
sidebarModulesUser[section.key]?.enabled !== false
|
|
|
|
|
|
}
|
2025-09-01 23:43:39 +08:00
|
|
|
|
onChange={handleSectionChange(section.key)}
|
|
|
|
|
|
size='default'
|
|
|
|
|
|
/>
|
2025-08-31 07:07:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-01 23:43:39 +08:00
|
|
|
|
{/* 功能模块网格 */}
|
|
|
|
|
|
<Row gutter={[12, 12]}>
|
|
|
|
|
|
{section.modules
|
|
|
|
|
|
.filter((module) =>
|
|
|
|
|
|
isAllowedByAdmin(section.key, module.key),
|
|
|
|
|
|
)
|
|
|
|
|
|
.map((module) => (
|
|
|
|
|
|
<Col
|
|
|
|
|
|
key={module.key}
|
|
|
|
|
|
xs={24}
|
|
|
|
|
|
sm={24}
|
|
|
|
|
|
md={12}
|
|
|
|
|
|
lg={8}
|
|
|
|
|
|
xl={8}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
|
2026-01-03 12:37:50 +08:00
|
|
|
|
sidebarModulesUser[section.key]?.enabled !==
|
|
|
|
|
|
false
|
2025-09-01 23:43:39 +08:00
|
|
|
|
? ''
|
|
|
|
|
|
: 'opacity-50'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
bodyStyle={{ padding: '16px' }}
|
|
|
|
|
|
hoverable
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className='flex justify-between items-center h-full'>
|
|
|
|
|
|
<div className='flex-1 text-left'>
|
|
|
|
|
|
<div className='font-semibold text-sm text-gray-900 mb-1'>
|
|
|
|
|
|
{module.title}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Typography.Text
|
|
|
|
|
|
type='secondary'
|
|
|
|
|
|
size='small'
|
|
|
|
|
|
className='block'
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
lineHeight: '1.5',
|
|
|
|
|
|
color: 'var(--semi-color-text-2)',
|
|
|
|
|
|
marginTop: '4px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{module.description}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className='ml-4'>
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
checked={
|
|
|
|
|
|
sidebarModulesUser[section.key]?.[
|
|
|
|
|
|
module.key
|
2026-01-03 12:37:50 +08:00
|
|
|
|
] !== false
|
2025-09-01 23:43:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
onChange={handleModuleChange(
|
|
|
|
|
|
section.key,
|
|
|
|
|
|
module.key,
|
|
|
|
|
|
)}
|
|
|
|
|
|
size='default'
|
|
|
|
|
|
disabled={
|
2026-01-03 12:37:50 +08:00
|
|
|
|
sidebarModulesUser[section.key]
|
|
|
|
|
|
?.enabled === false
|
2025-09-01 23:43:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-31 07:07:40 +08:00
|
|
|
|
</div>
|
2025-09-01 23:43:39 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Row>
|
2025-08-31 07:07:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
2025-09-01 23:43:39 +08:00
|
|
|
|
</div>{' '}
|
|
|
|
|
|
{/* 关闭边栏设置功能区域容器 */}
|
2025-08-31 07:07:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</TabPane>
|
|
|
|
|
|
)}
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</Tabs>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default NotificationSettings;
|