| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901 |
- /*
- 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 React, { useRef, useEffect, useState, useContext } from 'react';
- import {
- Button,
- Typography,
- Card,
- Avatar,
- Form,
- Radio,
- Toast,
- Tabs,
- TabPane,
- Switch,
- Row,
- Col,
- } from '@douyinfe/semi-ui';
- import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
- 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';
- import { useSidebar } from '../../../../hooks/common/useSidebar';
- const NotificationSettings = ({
- t,
- notificationSettings,
- handleNotificationSettingChange,
- 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();
- // 使用useSidebar钩子获取刷新方法
- const { refreshUserConfig } = useSidebar();
- // 左侧边栏设置处理函数
- 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('侧边栏设置保存成功'));
- // 刷新useSidebar钩子中的用户配置,实现实时更新
- await refreshUserConfig();
- } 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(() => {
- if (formApiRef.current && notificationSettings) {
- formApiRef.current.setValues(notificationSettings);
- }
- }, [notificationSettings]);
- // 处理表单字段变化
- const handleFormChange = (field, value) => {
- 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) {
- formApiRef.current
- .validate()
- .then(() => {
- saveNotificationSettings();
- })
- .catch((errors) => {
- console.log('表单验证失败:', errors);
- Toast.error(t('请检查表单填写是否正确'));
- });
- } else {
- saveNotificationSettings();
- }
- };
- return (
- <Card
- className='!rounded-2xl shadow-sm border-0'
- footer={
- <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'
- >
- {t('保存设置')}
- </Button>
- </>
- ) : (
- // 其他标签页的通用保存按钮
- <Button type='primary' onClick={handleSubmit}>
- {t('保存设置')}
- </Button>
- )}
- </div>
- }
- >
- {/* 卡片头部 */}
- <div className='flex items-center mb-4'>
- <Avatar size='small' color='blue' className='mr-3 shadow-md'>
- <Bell size={16} />
- </Avatar>
- <div>
- <Typography.Text className='text-lg font-medium'>
- {t('其他设置')}
- </Typography.Text>
- <div className='text-xs text-gray-600'>
- {t('通知、价格和隐私相关设置')}
- </div>
- </div>
- </div>
- <Form
- getFormApi={(api) => (formApiRef.current = api)}
- initValues={notificationSettings}
- onSubmit={handleSubmit}
- >
- {() => (
- <Tabs
- type='card'
- defaultActiveKey='notification'
- onChange={(key) => setActiveTabKey(key)}
- >
- {/* 通知配置 Tab */}
- <TabPane
- tab={
- <div className='flex items-center'>
- <Bell size={16} className='mr-2' />
- {t('通知配置')}
- </div>
- }
- itemKey='notification'
- >
- <div className='py-4'>
- <Form.RadioGroup
- field='warningType'
- label={t('通知方式')}
- initValue={notificationSettings.warningType}
- onChange={(value) => handleFormChange('warningType', value)}
- rules={[{ required: true, message: t('请选择通知方式') }]}
- >
- <Radio value='email'>{t('邮件通知')}</Radio>
- <Radio value='webhook'>{t('Webhook通知')}</Radio>
- <Radio value='bark'>{t('Bark通知')}</Radio>
- <Radio value='gotify'>{t('Gotify通知')}</Radio>
- </Form.RadioGroup>
- <Form.AutoComplete
- field='warningThreshold'
- label={
- <span>
- {t('额度预警阈值')}{' '}
- {renderQuotaWithPrompt(
- notificationSettings.warningThreshold,
- )}
- </span>
- }
- placeholder={t('请输入预警额度')}
- data={[
- { value: 100000, label: '0.2$' },
- { value: 500000, label: '1$' },
- { value: 1000000, label: '5$' },
- { value: 5000000, label: '10$' },
- ]}
- onChange={(val) => handleFormChange('warningThreshold', val)}
- prefix={<IconBell />}
- extraText={t(
- '当剩余额度低于此数值时,系统将通过选择的方式发送通知',
- )}
- 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();
- },
- },
- ]}
- />
- {/* 邮件通知设置 */}
- {notificationSettings.warningType === 'email' && (
- <Form.Input
- field='notificationEmail'
- label={t('通知邮箱')}
- placeholder={t('留空则使用账号绑定的邮箱')}
- onChange={(val) =>
- handleFormChange('notificationEmail', val)
- }
- prefix={<IconMail />}
- extraText={t(
- '设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
- )}
- showClear
- />
- )}
- {/* Webhook通知设置 */}
- {notificationSettings.warningType === 'webhook' && (
- <>
- <Form.Input
- field='webhookUrl'
- label={t('Webhook地址')}
- placeholder={t(
- '请输入Webhook地址,例如: https://example.com/webhook',
- )}
- onChange={(val) => handleFormChange('webhookUrl', val)}
- prefix={<IconLink />}
- extraText={t(
- '只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求',
- )}
- showClear
- rules={[
- {
- required:
- notificationSettings.warningType === 'webhook',
- message: t('请输入Webhook地址'),
- },
- {
- pattern: /^https:\/\/.+/,
- message: t('Webhook地址必须以https://开头'),
- },
- ]}
- />
- <Form.Input
- field='webhookSecret'
- label={t('接口凭证')}
- placeholder={t('请输入密钥')}
- onChange={(val) => handleFormChange('webhookSecret', val)}
- prefix={<IconKey />}
- extraText={t(
- '密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性',
- )}
- showClear
- />
- <Form.Slot label={t('Webhook请求结构说明')}>
- <div>
- <div style={{ height: '200px', marginBottom: '12px' }}>
- <CodeViewer
- content={{
- type: 'quota_exceed',
- title: '额度预警通知',
- content:
- '您的额度即将用尽,当前剩余额度为 {{value}}',
- values: ['$0.99'],
- timestamp: 1739950503,
- }}
- title='webhook'
- language='json'
- />
- </div>
- <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>
- </div>
- </div>
- </Form.Slot>
- </>
- )}
- {/* Bark推送设置 */}
- {notificationSettings.warningType === 'bark' && (
- <>
- <Form.Input
- field='barkUrl'
- label={t('Bark推送URL')}
- placeholder={t(
- '请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}',
- )}
- onChange={(val) => handleFormChange('barkUrl', val)}
- prefix={<IconLink />}
- extraText={t(
- '支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)',
- )}
- showClear
- rules={[
- {
- required: notificationSettings.warningType === 'bark',
- 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'>
- https://api.day.app/yourkey/{'{{title}}'}/
- {'{{content}}'}?sound=alarm&group=quota
- </div>
- <div className='text-xs text-gray-500 space-y-2'>
- <div>
- • <strong>{'title'}:</strong> {t('通知标题')}
- </div>
- <div>
- • <strong>{'content'}:</strong> {t('通知内容')}
- </div>
- <div className='mt-3 pt-3 border-t border-gray-200'>
- <span className='text-gray-400'>
- {t('更多参数请参考')}
- </span>{' '}
- <a
- href='https://github.com/Finb/Bark'
- target='_blank'
- rel='noopener noreferrer'
- className='text-blue-500 hover:text-blue-600 font-medium'
- >
- Bark {t('官方文档')}
- </a>
- </div>
- </div>
- </div>
- </>
- )}
- {/* 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?:\/\/.+/,
- message: t(
- 'Gotify服务器地址必须以http://或https://开头',
- ),
- },
- ]}
- />
- <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>
- <div>3. {t('填写Gotify服务器的完整URL地址')}</div>
- <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'
- >
- Gotify {t('官方文档')}
- </a>
- </div>
- </div>
- </div>
- </>
- )}
- </div>
- </TabPane>
- {/* 价格设置 Tab */}
- <TabPane
- tab={
- <div className='flex items-center'>
- <DollarSign size={16} className='mr-2' />
- {t('价格设置')}
- </div>
- }
- itemKey='pricing'
- >
- <div className='py-4'>
- <Form.Switch
- field='acceptUnsetModelRatioModel'
- label={t('接受未设置价格模型')}
- checkedText={t('开')}
- uncheckedText={t('关')}
- onChange={(value) =>
- handleFormChange('acceptUnsetModelRatioModel', value)
- }
- extraText={t(
- '当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
- )}
- />
- </div>
- </TabPane>
- {/* 隐私设置 Tab */}
- <TabPane
- tab={
- <div className='flex items-center'>
- <ShieldCheck size={16} className='mr-2' />
- {t('隐私设置')}
- </div>
- }
- itemKey='privacy'
- >
- <div className='py-4'>
- <Form.Switch
- field='recordIpLog'
- label={t('记录请求与错误日志IP')}
- checkedText={t('开')}
- uncheckedText={t('关')}
- onChange={(value) => handleFormChange('recordIpLog', value)}
- extraText={t(
- '开启后,仅"消费"和"错误"日志将记录您的客户端IP地址',
- )}
- />
- </div>
- </TabPane>
- {/* 左侧边栏设置 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
- type='secondary'
- size='small'
- style={{
- fontSize: '12px',
- lineHeight: '1.5',
- color: 'var(--semi-color-text-2)',
- }}
- >
- {t('您可以个性化设置侧边栏的要显示功能')}
- </Typography.Text>
- </div>
- {/* 边栏设置功能区域容器 */}
- <div
- className='border rounded-xl p-4'
- style={{
- borderColor: 'var(--semi-color-border)',
- backgroundColor: 'var(--semi-color-bg-1)',
- }}
- >
- {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)',
- borderColor: 'var(--semi-color-fill-1)',
- }}
- >
- <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>
- </div>
- <Switch
- checked={sidebarModulesUser[section.key]?.enabled}
- onChange={handleSectionChange(section.key)}
- size='default'
- />
- </div>
- {/* 功能模块网格 */}
- <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 ${
- sidebarModulesUser[section.key]?.enabled
- ? ''
- : '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
- ]
- }
- onChange={handleModuleChange(
- section.key,
- module.key,
- )}
- size='default'
- disabled={
- !sidebarModulesUser[section.key]
- ?.enabled
- }
- />
- </div>
- </div>
- </Card>
- </Col>
- ))}
- </Row>
- </div>
- ))}
- </div>{' '}
- {/* 关闭边栏设置功能区域容器 */}
- </div>
- </TabPane>
- )}
- </Tabs>
- )}
- </Form>
- </Card>
- );
- };
- export default NotificationSettings;
|