NotificationSettings.jsx 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useRef, useEffect, useState, useContext } from 'react';
  16. import {
  17. Button,
  18. Typography,
  19. Card,
  20. Avatar,
  21. Form,
  22. Radio,
  23. Toast,
  24. Tabs,
  25. TabPane,
  26. Switch,
  27. Row,
  28. Col,
  29. } from '@douyinfe/semi-ui';
  30. import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
  31. import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react';
  32. import {
  33. renderQuotaWithPrompt,
  34. API,
  35. showSuccess,
  36. showError,
  37. } from '../../../../helpers';
  38. import CodeViewer from '../../../playground/CodeViewer';
  39. import { StatusContext } from '../../../../context/Status';
  40. import { UserContext } from '../../../../context/User';
  41. import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
  42. import { useSidebar } from '../../../../hooks/common/useSidebar';
  43. const NotificationSettings = ({
  44. t,
  45. notificationSettings,
  46. handleNotificationSettingChange,
  47. saveNotificationSettings,
  48. }) => {
  49. const formApiRef = useRef(null);
  50. const [statusState] = useContext(StatusContext);
  51. const [userState] = useContext(UserContext);
  52. // 左侧边栏设置相关状态
  53. const [sidebarLoading, setSidebarLoading] = useState(false);
  54. const [activeTabKey, setActiveTabKey] = useState('notification');
  55. const [sidebarModulesUser, setSidebarModulesUser] = useState({
  56. chat: {
  57. enabled: true,
  58. playground: true,
  59. chat: true,
  60. },
  61. console: {
  62. enabled: true,
  63. detail: true,
  64. token: true,
  65. log: true,
  66. midjourney: true,
  67. task: true,
  68. },
  69. personal: {
  70. enabled: true,
  71. topup: true,
  72. personal: true,
  73. },
  74. admin: {
  75. enabled: true,
  76. channel: true,
  77. models: true,
  78. redemption: true,
  79. user: true,
  80. setting: true,
  81. },
  82. });
  83. const [adminConfig, setAdminConfig] = useState(null);
  84. // 使用后端权限验证替代前端角色判断
  85. const {
  86. permissions,
  87. loading: permissionsLoading,
  88. hasSidebarSettingsPermission,
  89. isSidebarSectionAllowed,
  90. isSidebarModuleAllowed,
  91. } = useUserPermissions();
  92. // 使用useSidebar钩子获取刷新方法
  93. const { refreshUserConfig } = useSidebar();
  94. // 左侧边栏设置处理函数
  95. const handleSectionChange = (sectionKey) => {
  96. return (checked) => {
  97. const newModules = {
  98. ...sidebarModulesUser,
  99. [sectionKey]: {
  100. ...sidebarModulesUser[sectionKey],
  101. enabled: checked,
  102. },
  103. };
  104. setSidebarModulesUser(newModules);
  105. };
  106. };
  107. const handleModuleChange = (sectionKey, moduleKey) => {
  108. return (checked) => {
  109. const newModules = {
  110. ...sidebarModulesUser,
  111. [sectionKey]: {
  112. ...sidebarModulesUser[sectionKey],
  113. [moduleKey]: checked,
  114. },
  115. };
  116. setSidebarModulesUser(newModules);
  117. };
  118. };
  119. const saveSidebarSettings = async () => {
  120. setSidebarLoading(true);
  121. try {
  122. const res = await API.put('/api/user/self', {
  123. sidebar_modules: JSON.stringify(sidebarModulesUser),
  124. });
  125. if (res.data.success) {
  126. showSuccess(t('侧边栏设置保存成功'));
  127. // 刷新useSidebar钩子中的用户配置,实现实时更新
  128. await refreshUserConfig();
  129. } else {
  130. showError(res.data.message);
  131. }
  132. } catch (error) {
  133. showError(t('保存失败'));
  134. }
  135. setSidebarLoading(false);
  136. };
  137. const resetSidebarModules = () => {
  138. const defaultConfig = {
  139. chat: { enabled: true, playground: true, chat: true },
  140. console: {
  141. enabled: true,
  142. detail: true,
  143. token: true,
  144. log: true,
  145. midjourney: true,
  146. task: true,
  147. },
  148. personal: { enabled: true, topup: true, personal: true },
  149. admin: {
  150. enabled: true,
  151. channel: true,
  152. models: true,
  153. redemption: true,
  154. user: true,
  155. setting: true,
  156. },
  157. };
  158. setSidebarModulesUser(defaultConfig);
  159. };
  160. // 加载左侧边栏配置
  161. useEffect(() => {
  162. const loadSidebarConfigs = async () => {
  163. try {
  164. // 获取管理员全局配置
  165. if (statusState?.status?.SidebarModulesAdmin) {
  166. const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
  167. setAdminConfig(adminConf);
  168. }
  169. // 获取用户个人配置
  170. const userRes = await API.get('/api/user/self');
  171. if (userRes.data.success && userRes.data.data.sidebar_modules) {
  172. const userConf = JSON.parse(userRes.data.data.sidebar_modules);
  173. setSidebarModulesUser(userConf);
  174. }
  175. } catch (error) {
  176. console.error('加载边栏配置失败:', error);
  177. }
  178. };
  179. loadSidebarConfigs();
  180. }, [statusState]);
  181. // 初始化表单值
  182. useEffect(() => {
  183. if (formApiRef.current && notificationSettings) {
  184. formApiRef.current.setValues(notificationSettings);
  185. }
  186. }, [notificationSettings]);
  187. // 处理表单字段变化
  188. const handleFormChange = (field, value) => {
  189. handleNotificationSettingChange(field, value);
  190. };
  191. // 检查功能是否被管理员允许
  192. const isAllowedByAdmin = (sectionKey, moduleKey = null) => {
  193. if (!adminConfig) return true;
  194. if (moduleKey) {
  195. return (
  196. adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]
  197. );
  198. } else {
  199. return adminConfig[sectionKey]?.enabled;
  200. }
  201. };
  202. // 区域配置数据(根据权限过滤)
  203. const sectionConfigs = [
  204. {
  205. key: 'chat',
  206. title: t('聊天区域'),
  207. description: t('操练场和聊天功能'),
  208. modules: [
  209. {
  210. key: 'playground',
  211. title: t('操练场'),
  212. description: t('AI模型测试环境'),
  213. },
  214. { key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
  215. ],
  216. },
  217. {
  218. key: 'console',
  219. title: t('控制台区域'),
  220. description: t('数据管理和日志查看'),
  221. modules: [
  222. { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
  223. { key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
  224. { key: 'log', title: t('使用日志'), description: t('API使用记录') },
  225. {
  226. key: 'midjourney',
  227. title: t('绘图日志'),
  228. description: t('绘图任务记录'),
  229. },
  230. { key: 'task', title: t('任务日志'), description: t('系统任务记录') },
  231. ],
  232. },
  233. {
  234. key: 'personal',
  235. title: t('个人中心区域'),
  236. description: t('用户个人功能'),
  237. modules: [
  238. { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
  239. {
  240. key: 'personal',
  241. title: t('个人设置'),
  242. description: t('个人信息设置'),
  243. },
  244. ],
  245. },
  246. // 管理员区域:根据后端权限控制显示
  247. {
  248. key: 'admin',
  249. title: t('管理员区域'),
  250. description: t('系统管理功能'),
  251. modules: [
  252. { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
  253. { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
  254. {
  255. key: 'redemption',
  256. title: t('兑换码管理'),
  257. description: t('兑换码生成管理'),
  258. },
  259. { key: 'user', title: t('用户管理'), description: t('用户账户管理') },
  260. {
  261. key: 'setting',
  262. title: t('系统设置'),
  263. description: t('系统参数配置'),
  264. },
  265. ],
  266. },
  267. ]
  268. .filter((section) => {
  269. // 使用后端权限验证替代前端角色判断
  270. return isSidebarSectionAllowed(section.key);
  271. })
  272. .map((section) => ({
  273. ...section,
  274. modules: section.modules.filter((module) =>
  275. isSidebarModuleAllowed(section.key, module.key),
  276. ),
  277. }))
  278. .filter(
  279. (section) =>
  280. // 过滤掉没有可用模块的区域
  281. section.modules.length > 0 && isAllowedByAdmin(section.key),
  282. );
  283. // 表单提交
  284. const handleSubmit = () => {
  285. if (formApiRef.current) {
  286. formApiRef.current
  287. .validate()
  288. .then(() => {
  289. saveNotificationSettings();
  290. })
  291. .catch((errors) => {
  292. console.log('表单验证失败:', errors);
  293. Toast.error(t('请检查表单填写是否正确'));
  294. });
  295. } else {
  296. saveNotificationSettings();
  297. }
  298. };
  299. return (
  300. <Card
  301. className='!rounded-2xl shadow-sm border-0'
  302. footer={
  303. <div className='flex justify-end gap-3'>
  304. {activeTabKey === 'sidebar' ? (
  305. // 边栏设置标签页的按钮
  306. <>
  307. <Button
  308. type='tertiary'
  309. onClick={resetSidebarModules}
  310. className='!rounded-lg'
  311. >
  312. {t('重置为默认')}
  313. </Button>
  314. <Button
  315. type='primary'
  316. onClick={saveSidebarSettings}
  317. loading={sidebarLoading}
  318. className='!rounded-lg'
  319. >
  320. {t('保存设置')}
  321. </Button>
  322. </>
  323. ) : (
  324. // 其他标签页的通用保存按钮
  325. <Button type='primary' onClick={handleSubmit}>
  326. {t('保存设置')}
  327. </Button>
  328. )}
  329. </div>
  330. }
  331. >
  332. {/* 卡片头部 */}
  333. <div className='flex items-center mb-4'>
  334. <Avatar size='small' color='blue' className='mr-3 shadow-md'>
  335. <Bell size={16} />
  336. </Avatar>
  337. <div>
  338. <Typography.Text className='text-lg font-medium'>
  339. {t('其他设置')}
  340. </Typography.Text>
  341. <div className='text-xs text-gray-600'>
  342. {t('通知、价格和隐私相关设置')}
  343. </div>
  344. </div>
  345. </div>
  346. <Form
  347. getFormApi={(api) => (formApiRef.current = api)}
  348. initValues={notificationSettings}
  349. onSubmit={handleSubmit}
  350. >
  351. {() => (
  352. <Tabs
  353. type='card'
  354. defaultActiveKey='notification'
  355. onChange={(key) => setActiveTabKey(key)}
  356. >
  357. {/* 通知配置 Tab */}
  358. <TabPane
  359. tab={
  360. <div className='flex items-center'>
  361. <Bell size={16} className='mr-2' />
  362. {t('通知配置')}
  363. </div>
  364. }
  365. itemKey='notification'
  366. >
  367. <div className='py-4'>
  368. <Form.RadioGroup
  369. field='warningType'
  370. label={t('通知方式')}
  371. initValue={notificationSettings.warningType}
  372. onChange={(value) => handleFormChange('warningType', value)}
  373. rules={[{ required: true, message: t('请选择通知方式') }]}
  374. >
  375. <Radio value='email'>{t('邮件通知')}</Radio>
  376. <Radio value='webhook'>{t('Webhook通知')}</Radio>
  377. <Radio value='bark'>{t('Bark通知')}</Radio>
  378. <Radio value='gotify'>{t('Gotify通知')}</Radio>
  379. </Form.RadioGroup>
  380. <Form.AutoComplete
  381. field='warningThreshold'
  382. label={
  383. <span>
  384. {t('额度预警阈值')}{' '}
  385. {renderQuotaWithPrompt(
  386. notificationSettings.warningThreshold,
  387. )}
  388. </span>
  389. }
  390. placeholder={t('请输入预警额度')}
  391. data={[
  392. { value: 100000, label: '0.2$' },
  393. { value: 500000, label: '1$' },
  394. { value: 1000000, label: '5$' },
  395. { value: 5000000, label: '10$' },
  396. ]}
  397. onChange={(val) => handleFormChange('warningThreshold', val)}
  398. prefix={<IconBell />}
  399. extraText={t(
  400. '当剩余额度低于此数值时,系统将通过选择的方式发送通知',
  401. )}
  402. style={{ width: '100%', maxWidth: '300px' }}
  403. rules={[
  404. { required: true, message: t('请输入预警阈值') },
  405. {
  406. validator: (rule, value) => {
  407. const numValue = Number(value);
  408. if (isNaN(numValue) || numValue <= 0) {
  409. return Promise.reject(t('预警阈值必须为正数'));
  410. }
  411. return Promise.resolve();
  412. },
  413. },
  414. ]}
  415. />
  416. {/* 邮件通知设置 */}
  417. {notificationSettings.warningType === 'email' && (
  418. <Form.Input
  419. field='notificationEmail'
  420. label={t('通知邮箱')}
  421. placeholder={t('留空则使用账号绑定的邮箱')}
  422. onChange={(val) =>
  423. handleFormChange('notificationEmail', val)
  424. }
  425. prefix={<IconMail />}
  426. extraText={t(
  427. '设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
  428. )}
  429. showClear
  430. />
  431. )}
  432. {/* Webhook通知设置 */}
  433. {notificationSettings.warningType === 'webhook' && (
  434. <>
  435. <Form.Input
  436. field='webhookUrl'
  437. label={t('Webhook地址')}
  438. placeholder={t(
  439. '请输入Webhook地址,例如: https://example.com/webhook',
  440. )}
  441. onChange={(val) => handleFormChange('webhookUrl', val)}
  442. prefix={<IconLink />}
  443. extraText={t(
  444. '只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求',
  445. )}
  446. showClear
  447. rules={[
  448. {
  449. required:
  450. notificationSettings.warningType === 'webhook',
  451. message: t('请输入Webhook地址'),
  452. },
  453. {
  454. pattern: /^https:\/\/.+/,
  455. message: t('Webhook地址必须以https://开头'),
  456. },
  457. ]}
  458. />
  459. <Form.Input
  460. field='webhookSecret'
  461. label={t('接口凭证')}
  462. placeholder={t('请输入密钥')}
  463. onChange={(val) => handleFormChange('webhookSecret', val)}
  464. prefix={<IconKey />}
  465. extraText={t(
  466. '密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性',
  467. )}
  468. showClear
  469. />
  470. <Form.Slot label={t('Webhook请求结构说明')}>
  471. <div>
  472. <div style={{ height: '200px', marginBottom: '12px' }}>
  473. <CodeViewer
  474. content={{
  475. type: 'quota_exceed',
  476. title: '额度预警通知',
  477. content:
  478. '您的额度即将用尽,当前剩余额度为 {{value}}',
  479. values: ['$0.99'],
  480. timestamp: 1739950503,
  481. }}
  482. title='webhook'
  483. language='json'
  484. />
  485. </div>
  486. <div className='text-xs text-gray-500 leading-relaxed'>
  487. <div>
  488. <strong>type:</strong>{' '}
  489. {t('通知类型 (quota_exceed: 额度预警)')}{' '}
  490. </div>
  491. <div>
  492. <strong>title:</strong> {t('通知标题')}
  493. </div>
  494. <div>
  495. <strong>content:</strong>{' '}
  496. {t('通知内容,支持 {{value}} 变量占位符')}
  497. </div>
  498. <div>
  499. <strong>values:</strong>{' '}
  500. {t('按顺序替换content中的变量占位符')}
  501. </div>
  502. <div>
  503. <strong>timestamp:</strong> {t('Unix时间戳')}
  504. </div>
  505. </div>
  506. </div>
  507. </Form.Slot>
  508. </>
  509. )}
  510. {/* Bark推送设置 */}
  511. {notificationSettings.warningType === 'bark' && (
  512. <>
  513. <Form.Input
  514. field='barkUrl'
  515. label={t('Bark推送URL')}
  516. placeholder={t(
  517. '请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}',
  518. )}
  519. onChange={(val) => handleFormChange('barkUrl', val)}
  520. prefix={<IconLink />}
  521. extraText={t(
  522. '支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)',
  523. )}
  524. showClear
  525. rules={[
  526. {
  527. required: notificationSettings.warningType === 'bark',
  528. message: t('请输入Bark推送URL'),
  529. },
  530. {
  531. pattern: /^https?:\/\/.+/,
  532. message: t('Bark推送URL必须以http://或https://开头'),
  533. },
  534. ]}
  535. />
  536. <div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
  537. <div className='text-sm text-gray-700 mb-3'>
  538. <strong>{t('模板示例')}</strong>
  539. </div>
  540. <div className='text-xs text-gray-600 font-mono bg-white p-3 rounded-lg shadow-sm mb-4'>
  541. https://api.day.app/yourkey/{'{{title}}'}/
  542. {'{{content}}'}?sound=alarm&group=quota
  543. </div>
  544. <div className='text-xs text-gray-500 space-y-2'>
  545. <div>
  546. • <strong>{'title'}:</strong> {t('通知标题')}
  547. </div>
  548. <div>
  549. • <strong>{'content'}:</strong> {t('通知内容')}
  550. </div>
  551. <div className='mt-3 pt-3 border-t border-gray-200'>
  552. <span className='text-gray-400'>
  553. {t('更多参数请参考')}
  554. </span>{' '}
  555. <a
  556. href='https://github.com/Finb/Bark'
  557. target='_blank'
  558. rel='noopener noreferrer'
  559. className='text-blue-500 hover:text-blue-600 font-medium'
  560. >
  561. Bark {t('官方文档')}
  562. </a>
  563. </div>
  564. </div>
  565. </div>
  566. </>
  567. )}
  568. {/* Gotify推送设置 */}
  569. {notificationSettings.warningType === 'gotify' && (
  570. <>
  571. <Form.Input
  572. field='gotifyUrl'
  573. label={t('Gotify服务器地址')}
  574. placeholder={t(
  575. '请输入Gotify服务器地址,例如: https://gotify.example.com',
  576. )}
  577. onChange={(val) => handleFormChange('gotifyUrl', val)}
  578. prefix={<IconLink />}
  579. extraText={t(
  580. '支持HTTP和HTTPS,填写Gotify服务器的完整URL地址',
  581. )}
  582. showClear
  583. rules={[
  584. {
  585. required:
  586. notificationSettings.warningType === 'gotify',
  587. message: t('请输入Gotify服务器地址'),
  588. },
  589. {
  590. pattern: /^https?:\/\/.+/,
  591. message: t(
  592. 'Gotify服务器地址必须以http://或https://开头',
  593. ),
  594. },
  595. ]}
  596. />
  597. <Form.Input
  598. field='gotifyToken'
  599. label={t('Gotify应用令牌')}
  600. placeholder={t('请输入Gotify应用令牌')}
  601. onChange={(val) => handleFormChange('gotifyToken', val)}
  602. prefix={<IconKey />}
  603. extraText={t(
  604. '在Gotify服务器创建应用后获得的令牌,用于发送通知',
  605. )}
  606. showClear
  607. rules={[
  608. {
  609. required:
  610. notificationSettings.warningType === 'gotify',
  611. message: t('请输入Gotify应用令牌'),
  612. },
  613. ]}
  614. />
  615. <Form.AutoComplete
  616. field='gotifyPriority'
  617. label={t('消息优先级')}
  618. placeholder={t('请选择消息优先级')}
  619. data={[
  620. { value: 0, label: t('0 - 最低') },
  621. { value: 2, label: t('2 - 低') },
  622. { value: 5, label: t('5 - 正常(默认)') },
  623. { value: 8, label: t('8 - 高') },
  624. { value: 10, label: t('10 - 最高') },
  625. ]}
  626. onChange={(val) =>
  627. handleFormChange('gotifyPriority', val)
  628. }
  629. prefix={<IconBell />}
  630. extraText={t('消息优先级,范围0-10,默认为5')}
  631. style={{ width: '100%', maxWidth: '300px' }}
  632. />
  633. <div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
  634. <div className='text-sm text-gray-700 mb-3'>
  635. <strong>{t('配置说明')}</strong>
  636. </div>
  637. <div className='text-xs text-gray-500 space-y-2'>
  638. <div>
  639. 1. {t('在Gotify服务器的应用管理中创建新应用')}
  640. </div>
  641. <div>
  642. 2.{' '}
  643. {t(
  644. '复制应用的令牌(Token)并填写到上方的应用令牌字段',
  645. )}
  646. </div>
  647. <div>3. {t('填写Gotify服务器的完整URL地址')}</div>
  648. <div className='mt-3 pt-3 border-t border-gray-200'>
  649. <span className='text-gray-400'>
  650. {t('更多信息请参考')}
  651. </span>{' '}
  652. <a
  653. href='https://gotify.net/'
  654. target='_blank'
  655. rel='noopener noreferrer'
  656. className='text-blue-500 hover:text-blue-600 font-medium'
  657. >
  658. Gotify {t('官方文档')}
  659. </a>
  660. </div>
  661. </div>
  662. </div>
  663. </>
  664. )}
  665. </div>
  666. </TabPane>
  667. {/* 价格设置 Tab */}
  668. <TabPane
  669. tab={
  670. <div className='flex items-center'>
  671. <DollarSign size={16} className='mr-2' />
  672. {t('价格设置')}
  673. </div>
  674. }
  675. itemKey='pricing'
  676. >
  677. <div className='py-4'>
  678. <Form.Switch
  679. field='acceptUnsetModelRatioModel'
  680. label={t('接受未设置价格模型')}
  681. checkedText={t('开')}
  682. uncheckedText={t('关')}
  683. onChange={(value) =>
  684. handleFormChange('acceptUnsetModelRatioModel', value)
  685. }
  686. extraText={t(
  687. '当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
  688. )}
  689. />
  690. </div>
  691. </TabPane>
  692. {/* 隐私设置 Tab */}
  693. <TabPane
  694. tab={
  695. <div className='flex items-center'>
  696. <ShieldCheck size={16} className='mr-2' />
  697. {t('隐私设置')}
  698. </div>
  699. }
  700. itemKey='privacy'
  701. >
  702. <div className='py-4'>
  703. <Form.Switch
  704. field='recordIpLog'
  705. label={t('记录请求与错误日志IP')}
  706. checkedText={t('开')}
  707. uncheckedText={t('关')}
  708. onChange={(value) => handleFormChange('recordIpLog', value)}
  709. extraText={t(
  710. '开启后,仅"消费"和"错误"日志将记录您的客户端IP地址',
  711. )}
  712. />
  713. </div>
  714. </TabPane>
  715. {/* 左侧边栏设置 Tab - 根据后端权限控制显示 */}
  716. {hasSidebarSettingsPermission() && (
  717. <TabPane
  718. tab={
  719. <div className='flex items-center'>
  720. <Settings size={16} className='mr-2' />
  721. {t('边栏设置')}
  722. </div>
  723. }
  724. itemKey='sidebar'
  725. >
  726. <div className='py-4'>
  727. <div className='mb-4'>
  728. <Typography.Text
  729. type='secondary'
  730. size='small'
  731. style={{
  732. fontSize: '12px',
  733. lineHeight: '1.5',
  734. color: 'var(--semi-color-text-2)',
  735. }}
  736. >
  737. {t('您可以个性化设置侧边栏的要显示功能')}
  738. </Typography.Text>
  739. </div>
  740. {/* 边栏设置功能区域容器 */}
  741. <div
  742. className='border rounded-xl p-4'
  743. style={{
  744. borderColor: 'var(--semi-color-border)',
  745. backgroundColor: 'var(--semi-color-bg-1)',
  746. }}
  747. >
  748. {sectionConfigs.map((section) => (
  749. <div key={section.key} className='mb-6'>
  750. {/* 区域标题和总开关 */}
  751. <div
  752. className='flex justify-between items-center mb-4 p-4 rounded-lg'
  753. style={{
  754. backgroundColor: 'var(--semi-color-fill-0)',
  755. border: '1px solid var(--semi-color-border-light)',
  756. borderColor: 'var(--semi-color-fill-1)',
  757. }}
  758. >
  759. <div>
  760. <div className='font-semibold text-base text-gray-900 mb-1'>
  761. {section.title}
  762. </div>
  763. <Typography.Text
  764. type='secondary'
  765. size='small'
  766. style={{
  767. fontSize: '12px',
  768. lineHeight: '1.5',
  769. color: 'var(--semi-color-text-2)',
  770. }}
  771. >
  772. {section.description}
  773. </Typography.Text>
  774. </div>
  775. <Switch
  776. checked={sidebarModulesUser[section.key]?.enabled}
  777. onChange={handleSectionChange(section.key)}
  778. size='default'
  779. />
  780. </div>
  781. {/* 功能模块网格 */}
  782. <Row gutter={[12, 12]}>
  783. {section.modules
  784. .filter((module) =>
  785. isAllowedByAdmin(section.key, module.key),
  786. )
  787. .map((module) => (
  788. <Col
  789. key={module.key}
  790. xs={24}
  791. sm={24}
  792. md={12}
  793. lg={8}
  794. xl={8}
  795. >
  796. <Card
  797. className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
  798. sidebarModulesUser[section.key]?.enabled
  799. ? ''
  800. : 'opacity-50'
  801. }`}
  802. bodyStyle={{ padding: '16px' }}
  803. hoverable
  804. >
  805. <div className='flex justify-between items-center h-full'>
  806. <div className='flex-1 text-left'>
  807. <div className='font-semibold text-sm text-gray-900 mb-1'>
  808. {module.title}
  809. </div>
  810. <Typography.Text
  811. type='secondary'
  812. size='small'
  813. className='block'
  814. style={{
  815. fontSize: '12px',
  816. lineHeight: '1.5',
  817. color: 'var(--semi-color-text-2)',
  818. marginTop: '4px',
  819. }}
  820. >
  821. {module.description}
  822. </Typography.Text>
  823. </div>
  824. <div className='ml-4'>
  825. <Switch
  826. checked={
  827. sidebarModulesUser[section.key]?.[
  828. module.key
  829. ]
  830. }
  831. onChange={handleModuleChange(
  832. section.key,
  833. module.key,
  834. )}
  835. size='default'
  836. disabled={
  837. !sidebarModulesUser[section.key]
  838. ?.enabled
  839. }
  840. />
  841. </div>
  842. </div>
  843. </Card>
  844. </Col>
  845. ))}
  846. </Row>
  847. </div>
  848. ))}
  849. </div>{' '}
  850. {/* 关闭边栏设置功能区域容器 */}
  851. </div>
  852. </TabPane>
  853. )}
  854. </Tabs>
  855. )}
  856. </Form>
  857. </Card>
  858. );
  859. };
  860. export default NotificationSettings;