NotificationSettings.jsx 34 KB

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