NotificationSettings.jsx 34 KB

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