PersonalSetting.js 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. import React, {useContext, useEffect, useState} from 'react';
  2. import {useNavigate} from 'react-router-dom';
  3. import {
  4. API,
  5. copy,
  6. isRoot,
  7. showError,
  8. showInfo,
  9. showSuccess,
  10. } from '../helpers';
  11. import Turnstile from 'react-turnstile';
  12. import {UserContext} from '../context/User';
  13. import {onGitHubOAuthClicked, onLinuxDOOAuthClicked} from './utils';
  14. import {
  15. Avatar,
  16. Banner,
  17. Button,
  18. Card,
  19. Descriptions,
  20. Image,
  21. Input,
  22. InputNumber,
  23. Layout,
  24. Modal,
  25. Space,
  26. Tag,
  27. Typography,
  28. Collapsible,
  29. Select,
  30. Radio,
  31. RadioGroup,
  32. AutoComplete,
  33. } from '@douyinfe/semi-ui';
  34. import {
  35. getQuotaPerUnit,
  36. renderQuota,
  37. renderQuotaWithPrompt,
  38. stringToColor,
  39. } from '../helpers/render';
  40. import TelegramLoginButton from 'react-telegram-login';
  41. import { useTranslation } from 'react-i18next';
  42. const PersonalSetting = () => {
  43. const [userState, userDispatch] = useContext(UserContext);
  44. let navigate = useNavigate();
  45. const { t } = useTranslation();
  46. const [inputs, setInputs] = useState({
  47. wechat_verification_code: '',
  48. email_verification_code: '',
  49. email: '',
  50. self_account_deletion_confirmation: '',
  51. set_new_password: '',
  52. set_new_password_confirmation: '',
  53. });
  54. const [status, setStatus] = useState({});
  55. const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
  56. const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
  57. const [showEmailBindModal, setShowEmailBindModal] = useState(false);
  58. const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
  59. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  60. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  61. const [turnstileToken, setTurnstileToken] = useState('');
  62. const [loading, setLoading] = useState(false);
  63. const [disableButton, setDisableButton] = useState(false);
  64. const [countdown, setCountdown] = useState(30);
  65. const [affLink, setAffLink] = useState('');
  66. const [systemToken, setSystemToken] = useState('');
  67. const [models, setModels] = useState([]);
  68. const [openTransfer, setOpenTransfer] = useState(false);
  69. const [transferAmount, setTransferAmount] = useState(0);
  70. const [isModelsExpanded, setIsModelsExpanded] = useState(false);
  71. const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
  72. const [notificationSettings, setNotificationSettings] = useState({
  73. warningType: 'email',
  74. warningThreshold: 100000,
  75. webhookUrl: '',
  76. webhookSecret: '',
  77. notificationEmail: ''
  78. });
  79. const [showWebhookDocs, setShowWebhookDocs] = useState(false);
  80. useEffect(() => {
  81. let status = localStorage.getItem('status');
  82. if (status) {
  83. status = JSON.parse(status);
  84. setStatus(status);
  85. if (status.turnstile_check) {
  86. setTurnstileEnabled(true);
  87. setTurnstileSiteKey(status.turnstile_site_key);
  88. }
  89. }
  90. getUserData().then((res) => {
  91. console.log(userState);
  92. });
  93. loadModels().then();
  94. getAffLink().then();
  95. setTransferAmount(getQuotaPerUnit());
  96. }, []);
  97. useEffect(() => {
  98. let countdownInterval = null;
  99. if (disableButton && countdown > 0) {
  100. countdownInterval = setInterval(() => {
  101. setCountdown(countdown - 1);
  102. }, 1000);
  103. } else if (countdown === 0) {
  104. setDisableButton(false);
  105. setCountdown(30);
  106. }
  107. return () => clearInterval(countdownInterval); // Clean up on unmount
  108. }, [disableButton, countdown]);
  109. useEffect(() => {
  110. if (userState?.user?.setting) {
  111. const settings = JSON.parse(userState.user.setting);
  112. setNotificationSettings({
  113. warningType: settings.notify_type || 'email',
  114. warningThreshold: settings.quota_warning_threshold || 500000,
  115. webhookUrl: settings.webhook_url || '',
  116. webhookSecret: settings.webhook_secret || '',
  117. notificationEmail: settings.notification_email || ''
  118. });
  119. }
  120. }, [userState?.user?.setting]);
  121. const handleInputChange = (name, value) => {
  122. setInputs((inputs) => ({...inputs, [name]: value}));
  123. };
  124. const generateAccessToken = async () => {
  125. const res = await API.get('/api/user/token');
  126. const {success, message, data} = res.data;
  127. if (success) {
  128. setSystemToken(data);
  129. await copy(data);
  130. showSuccess(t('令牌已重置并已复制到剪贴板'));
  131. } else {
  132. showError(message);
  133. }
  134. };
  135. const getAffLink = async () => {
  136. const res = await API.get('/api/user/aff');
  137. const {success, message, data} = res.data;
  138. if (success) {
  139. let link = `${window.location.origin}/register?aff=${data}`;
  140. setAffLink(link);
  141. } else {
  142. showError(message);
  143. }
  144. };
  145. const getUserData = async () => {
  146. let res = await API.get(`/api/user/self`);
  147. const {success, message, data} = res.data;
  148. if (success) {
  149. userDispatch({type: 'login', payload: data});
  150. } else {
  151. showError(message);
  152. }
  153. };
  154. const loadModels = async () => {
  155. let res = await API.get(`/api/user/models`);
  156. const {success, message, data} = res.data;
  157. if (success) {
  158. if (data != null) {
  159. setModels(data);
  160. }
  161. } else {
  162. showError(message);
  163. }
  164. };
  165. const handleAffLinkClick = async (e) => {
  166. e.target.select();
  167. await copy(e.target.value);
  168. showSuccess(t('邀请链接已复制到剪切板'));
  169. };
  170. const handleSystemTokenClick = async (e) => {
  171. e.target.select();
  172. await copy(e.target.value);
  173. showSuccess(t('系统令牌已复制到剪切板'));
  174. };
  175. const deleteAccount = async () => {
  176. if (inputs.self_account_deletion_confirmation !== userState.user.username) {
  177. showError(t('请输入你的账户名以确认删除!'));
  178. return;
  179. }
  180. const res = await API.delete('/api/user/self');
  181. const {success, message} = res.data;
  182. if (success) {
  183. showSuccess(t('账户已删除!'));
  184. await API.get('/api/user/logout');
  185. userDispatch({type: 'logout'});
  186. localStorage.removeItem('user');
  187. navigate('/login');
  188. } else {
  189. showError(message);
  190. }
  191. };
  192. const bindWeChat = async () => {
  193. if (inputs.wechat_verification_code === '') return;
  194. const res = await API.get(
  195. `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
  196. );
  197. const {success, message} = res.data;
  198. if (success) {
  199. showSuccess(t('微信账户绑定成功!'));
  200. setShowWeChatBindModal(false);
  201. } else {
  202. showError(message);
  203. }
  204. };
  205. const changePassword = async () => {
  206. if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
  207. showError(t('两次输入的密码不一致!'));
  208. return;
  209. }
  210. const res = await API.put(`/api/user/self`, {
  211. password: inputs.set_new_password,
  212. });
  213. const {success, message} = res.data;
  214. if (success) {
  215. showSuccess(t('密码修改成功!'));
  216. setShowWeChatBindModal(false);
  217. } else {
  218. showError(message);
  219. }
  220. setShowChangePasswordModal(false);
  221. };
  222. const transfer = async () => {
  223. if (transferAmount < getQuotaPerUnit()) {
  224. showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
  225. return;
  226. }
  227. const res = await API.post(`/api/user/aff_transfer`, {
  228. quota: transferAmount,
  229. });
  230. const {success, message} = res.data;
  231. if (success) {
  232. showSuccess(message);
  233. setOpenTransfer(false);
  234. getUserData().then();
  235. } else {
  236. showError(message);
  237. }
  238. };
  239. const sendVerificationCode = async () => {
  240. if (inputs.email === '') {
  241. showError(t('请输入邮箱!'));
  242. return;
  243. }
  244. setDisableButton(true);
  245. if (turnstileEnabled && turnstileToken === '') {
  246. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  247. return;
  248. }
  249. setLoading(true);
  250. const res = await API.get(
  251. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
  252. );
  253. const {success, message} = res.data;
  254. if (success) {
  255. showSuccess(t('验证码发送成功,请检查邮箱!'));
  256. } else {
  257. showError(message);
  258. }
  259. setLoading(false);
  260. };
  261. const bindEmail = async () => {
  262. if (inputs.email_verification_code === '') {
  263. showError(t('请输入邮箱验证码!'));
  264. return;
  265. }
  266. setLoading(true);
  267. const res = await API.get(
  268. `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
  269. );
  270. const {success, message} = res.data;
  271. if (success) {
  272. showSuccess(t('邮箱账户绑定成功!'));
  273. setShowEmailBindModal(false);
  274. userState.user.email = inputs.email;
  275. } else {
  276. showError(message);
  277. }
  278. setLoading(false);
  279. };
  280. const getUsername = () => {
  281. if (userState.user) {
  282. return userState.user.username;
  283. } else {
  284. return 'null';
  285. }
  286. };
  287. const handleCancel = () => {
  288. setOpenTransfer(false);
  289. };
  290. const copyText = async (text) => {
  291. if (await copy(text)) {
  292. showSuccess(t('已复制:') + text);
  293. } else {
  294. // setSearchKeyword(text);
  295. Modal.error({title: t('无法复制到剪贴板,请手动复制'), content: text});
  296. }
  297. };
  298. const handleNotificationSettingChange = (type, value) => {
  299. setNotificationSettings(prev => ({
  300. ...prev,
  301. [type]: value.target ? value.target.value : value // 处理 Radio 事件对象
  302. }));
  303. };
  304. const saveNotificationSettings = async () => {
  305. try {
  306. const res = await API.put('/api/user/setting', {
  307. notify_type: notificationSettings.warningType,
  308. quota_warning_threshold: notificationSettings.warningThreshold,
  309. webhook_url: notificationSettings.webhookUrl,
  310. webhook_secret: notificationSettings.webhookSecret,
  311. notification_email: notificationSettings.notificationEmail
  312. });
  313. if (res.data.success) {
  314. showSuccess(t('通知设置已更新'));
  315. await getUserData();
  316. } else {
  317. showError(res.data.message);
  318. }
  319. } catch (error) {
  320. showError(t('更新通知设置失败'));
  321. }
  322. };
  323. return (
  324. <div>
  325. <Layout>
  326. <Layout.Content>
  327. <Modal
  328. title={t('请输入要划转的数量')}
  329. visible={openTransfer}
  330. onOk={transfer}
  331. onCancel={handleCancel}
  332. maskClosable={false}
  333. size={'small'}
  334. centered={true}
  335. >
  336. <div style={{marginTop: 20}}>
  337. <Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
  338. <Input
  339. style={{marginTop: 5}}
  340. value={userState?.user?.aff_quota}
  341. disabled={true}
  342. ></Input>
  343. </div>
  344. <div style={{marginTop: 20}}>
  345. <Typography.Text>
  346. {t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
  347. </Typography.Text>
  348. <div>
  349. <InputNumber
  350. min={0}
  351. style={{marginTop: 5}}
  352. value={transferAmount}
  353. onChange={(value) => setTransferAmount(value)}
  354. disabled={false}
  355. ></InputNumber>
  356. </div>
  357. </div>
  358. </Modal>
  359. <div style={{marginTop: 20}}>
  360. <Card
  361. title={
  362. <Card.Meta
  363. avatar={
  364. <Avatar
  365. size='default'
  366. color={stringToColor(getUsername())}
  367. style={{marginRight: 4}}
  368. >
  369. {typeof getUsername() === 'string' &&
  370. getUsername().slice(0, 1)}
  371. </Avatar>
  372. }
  373. title={<Typography.Text>{getUsername()}</Typography.Text>}
  374. description={
  375. isRoot() ? (
  376. <Tag color='red'>{t('管理员')}</Tag>
  377. ) : (
  378. <Tag color='blue'>{t('普通用户')}</Tag>
  379. )
  380. }
  381. ></Card.Meta>
  382. }
  383. headerExtraContent={
  384. <>
  385. <Space vertical align='start'>
  386. <Tag color='green'>{'ID: ' + userState?.user?.id}</Tag>
  387. <Tag color='blue'>{userState?.user?.group}</Tag>
  388. </Space>
  389. </>
  390. }
  391. footer={
  392. <>
  393. <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
  394. <Typography.Title heading={6}>{t('可用模型')}</Typography.Title>
  395. </div>
  396. <div style={{marginTop: 10}}>
  397. {models.length <= MODELS_DISPLAY_COUNT ? (
  398. <Space wrap>
  399. {models.map((model) => (
  400. <Tag
  401. key={model}
  402. color='cyan'
  403. onClick={() => {
  404. copyText(model);
  405. }}
  406. >
  407. {model}
  408. </Tag>
  409. ))}
  410. </Space>
  411. ) : (
  412. <>
  413. <Collapsible isOpen={isModelsExpanded}>
  414. <Space wrap>
  415. {models.map((model) => (
  416. <Tag
  417. key={model}
  418. color='cyan'
  419. onClick={() => {
  420. copyText(model);
  421. }}
  422. >
  423. {model}
  424. </Tag>
  425. ))}
  426. <Tag
  427. color='blue'
  428. type="light"
  429. style={{ cursor: 'pointer' }}
  430. onClick={() => setIsModelsExpanded(false)}
  431. >
  432. {t('收起')}
  433. </Tag>
  434. </Space>
  435. </Collapsible>
  436. {!isModelsExpanded && (
  437. <Space wrap>
  438. {models.slice(0, MODELS_DISPLAY_COUNT).map((model) => (
  439. <Tag
  440. key={model}
  441. color='cyan'
  442. onClick={() => {
  443. copyText(model);
  444. }}
  445. >
  446. {model}
  447. </Tag>
  448. ))}
  449. <Tag
  450. color='blue'
  451. type="light"
  452. style={{ cursor: 'pointer' }}
  453. onClick={() => setIsModelsExpanded(true)}
  454. >
  455. {t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
  456. </Tag>
  457. </Space>
  458. )}
  459. </>
  460. )}
  461. </div>
  462. </>
  463. }
  464. >
  465. <Descriptions row>
  466. <Descriptions.Item itemKey={t('当前余额')}>
  467. {renderQuota(userState?.user?.quota)}
  468. </Descriptions.Item>
  469. <Descriptions.Item itemKey={t('历史消耗')}>
  470. {renderQuota(userState?.user?.used_quota)}
  471. </Descriptions.Item>
  472. <Descriptions.Item itemKey={t('请求次数')}>
  473. {userState.user?.request_count}
  474. </Descriptions.Item>
  475. </Descriptions>
  476. </Card>
  477. <Card
  478. style={{marginTop: 10}}
  479. footer={
  480. <div>
  481. <Typography.Text>{t('邀请链接')}</Typography.Text>
  482. <Input
  483. style={{marginTop: 10}}
  484. value={affLink}
  485. onClick={handleAffLinkClick}
  486. readOnly
  487. />
  488. </div>
  489. }
  490. >
  491. <Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
  492. <div style={{marginTop: 10}}>
  493. <Descriptions row>
  494. <Descriptions.Item itemKey={t('待使用收益')}>
  495. <span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
  496. {renderQuota(userState?.user?.aff_quota)}
  497. </span>
  498. <Button
  499. type={'secondary'}
  500. onClick={() => setOpenTransfer(true)}
  501. size={'small'}
  502. style={{marginLeft: 10}}
  503. >
  504. {t('划转')}
  505. </Button>
  506. </Descriptions.Item>
  507. <Descriptions.Item itemKey={t('总收益')}>
  508. {renderQuota(userState?.user?.aff_history_quota)}
  509. </Descriptions.Item>
  510. <Descriptions.Item itemKey={t('邀请人数')}>
  511. {userState?.user?.aff_count}
  512. </Descriptions.Item>
  513. </Descriptions>
  514. </div>
  515. </Card>
  516. <Card style={{marginTop: 10}}>
  517. <Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
  518. <div style={{marginTop: 20}}>
  519. <Typography.Text strong>{t('邮箱')}</Typography.Text>
  520. <div
  521. style={{display: 'flex', justifyContent: 'space-between'}}
  522. >
  523. <div>
  524. <Input
  525. value={
  526. userState.user && userState.user.email !== ''
  527. ? userState.user.email
  528. : t('未绑定')
  529. }
  530. readonly={true}
  531. ></Input>
  532. </div>
  533. <div>
  534. <Button
  535. onClick={() => {
  536. setShowEmailBindModal(true);
  537. }}
  538. >
  539. {userState.user && userState.user.email !== ''
  540. ? t('修改绑定')
  541. : t('绑定邮箱')}
  542. </Button>
  543. </div>
  544. </div>
  545. </div>
  546. <div style={{marginTop: 10}}>
  547. <Typography.Text strong>{t('微信')}</Typography.Text>
  548. <div style={{display: 'flex', justifyContent: 'space-between'}}>
  549. <div>
  550. <Input
  551. value={
  552. userState.user && userState.user.wechat_id !== ''
  553. ? t('已绑定')
  554. : t('未绑定')
  555. }
  556. readonly={true}
  557. ></Input>
  558. </div>
  559. <div>
  560. <Button
  561. disabled={!status.wechat_login}
  562. onClick={() => {
  563. setShowWeChatBindModal(true);
  564. }}
  565. >
  566. {userState.user && userState.user.wechat_id !== ''
  567. ? t('修改绑定')
  568. : status.wechat_login
  569. ? t('绑定')
  570. : t('未启用')}
  571. </Button>
  572. </div>
  573. </div>
  574. </div>
  575. <div style={{marginTop: 10}}>
  576. <Typography.Text strong>{t('GitHub')}</Typography.Text>
  577. <div
  578. style={{display: 'flex', justifyContent: 'space-between'}}
  579. >
  580. <div>
  581. <Input
  582. value={
  583. userState.user && userState.user.github_id !== ''
  584. ? userState.user.github_id
  585. : t('未绑定')
  586. }
  587. readonly={true}
  588. ></Input>
  589. </div>
  590. <div>
  591. <Button
  592. onClick={() => {
  593. onGitHubOAuthClicked(status.github_client_id);
  594. }}
  595. disabled={
  596. (userState.user && userState.user.github_id !== '') ||
  597. !status.github_oauth
  598. }
  599. >
  600. {status.github_oauth ? t('绑定') : t('未启用')}
  601. </Button>
  602. </div>
  603. </div>
  604. </div>
  605. <div style={{marginTop: 10}}>
  606. <Typography.Text strong>{t('Telegram')}</Typography.Text>
  607. <div
  608. style={{display: 'flex', justifyContent: 'space-between'}}
  609. >
  610. <div>
  611. <Input
  612. value={
  613. userState.user && userState.user.telegram_id !== ''
  614. ? userState.user.telegram_id
  615. : t('未绑定')
  616. }
  617. readonly={true}
  618. ></Input>
  619. </div>
  620. <div>
  621. {status.telegram_oauth ? (
  622. userState.user.telegram_id !== '' ? (
  623. <Button disabled={true}>{t('已绑定')}</Button>
  624. ) : (
  625. <TelegramLoginButton
  626. dataAuthUrl='/api/oauth/telegram/bind'
  627. botName={status.telegram_bot_name}
  628. />
  629. )
  630. ) : (
  631. <Button disabled={true}>{t('未启用')}</Button>
  632. )}
  633. </div>
  634. </div>
  635. </div>
  636. <div style={{marginTop: 10}}>
  637. <Typography.Text strong>{t('LinuxDO')}</Typography.Text>
  638. <div
  639. style={{display: 'flex', justifyContent: 'space-between'}}
  640. >
  641. <div>
  642. <Input
  643. value={
  644. userState.user && userState.user.linux_do_id !== ''
  645. ? userState.user.linux_do_id
  646. : t('未绑定')
  647. }
  648. readonly={true}
  649. ></Input>
  650. </div>
  651. <div>
  652. <Button
  653. onClick={() => {
  654. onLinuxDOOAuthClicked(status.linuxdo_client_id);
  655. }}
  656. disabled={
  657. (userState.user && userState.user.linux_do_id !== '') ||
  658. !status.linuxdo_oauth
  659. }
  660. >
  661. {status.linuxdo_oauth ? t('绑定') : t('未启用')}
  662. </Button>
  663. </div>
  664. </div>
  665. </div>
  666. <div style={{marginTop: 10}}>
  667. <Space>
  668. <Button onClick={generateAccessToken}>
  669. {t('生成系统访问令牌')}
  670. </Button>
  671. <Button
  672. onClick={() => {
  673. setShowChangePasswordModal(true);
  674. }}
  675. >
  676. {t('修改密码')}
  677. </Button>
  678. <Button
  679. type={'danger'}
  680. onClick={() => {
  681. setShowAccountDeleteModal(true);
  682. }}
  683. >
  684. {t('删除个人账户')}
  685. </Button>
  686. </Space>
  687. {systemToken && (
  688. <Input
  689. readOnly
  690. value={systemToken}
  691. onClick={handleSystemTokenClick}
  692. style={{marginTop: '10px'}}
  693. />
  694. )}
  695. <Modal
  696. onCancel={() => setShowWeChatBindModal(false)}
  697. visible={showWeChatBindModal}
  698. size={'small'}
  699. >
  700. <Image src={status.wechat_qrcode}/>
  701. <div style={{textAlign: 'center'}}>
  702. <p>
  703. 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
  704. </p>
  705. </div>
  706. <Input
  707. placeholder='验证码'
  708. name='wechat_verification_code'
  709. value={inputs.wechat_verification_code}
  710. onChange={(v) =>
  711. handleInputChange('wechat_verification_code', v)
  712. }
  713. />
  714. <Button color='' fluid size='large' onClick={bindWeChat}>
  715. {t('绑定')}
  716. </Button>
  717. </Modal>
  718. </div>
  719. </Card>
  720. <Card style={{marginTop: 10}}>
  721. <Typography.Title heading={6}>{t('通知设置')}</Typography.Title>
  722. <div style={{marginTop: 20}}>
  723. <Typography.Text strong>{t('通知方式')}</Typography.Text>
  724. <div style={{marginTop: 10}}>
  725. <RadioGroup
  726. value={notificationSettings.warningType}
  727. onChange={value => handleNotificationSettingChange('warningType', value)}
  728. >
  729. <Radio value="email">{t('邮件通知')}</Radio>
  730. <Radio value="webhook">{t('Webhook通知')}</Radio>
  731. </RadioGroup>
  732. </div>
  733. </div>
  734. {notificationSettings.warningType === 'webhook' && (
  735. <>
  736. <div style={{marginTop: 20}}>
  737. <Typography.Text strong>{t('Webhook地址')}</Typography.Text>
  738. <div style={{marginTop: 10}}>
  739. <Input
  740. value={notificationSettings.webhookUrl}
  741. onChange={val => handleNotificationSettingChange('webhookUrl', val)}
  742. placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
  743. />
  744. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  745. {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
  746. </Typography.Text>
  747. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  748. <div style={{cursor: 'pointer'}} onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
  749. {t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'}
  750. </div>
  751. <Collapsible isOpen={showWebhookDocs}>
  752. <pre style={{marginTop: 4, background: 'var(--semi-color-fill-0)', padding: 8, borderRadius: 4}}>
  753. {`{
  754. "type": "quota_exceed", // 通知类型
  755. "title": "标题", // 通知标题
  756. "content": "通知内容", // 通知内容,支持 {{value}} 变量占位符
  757. "values": ["值1", "值2"], // 按顺序替换content中的 {{value}} 占位符
  758. "timestamp": 1739950503 // 时间戳
  759. }
  760. 示例:
  761. {
  762. "type": "quota_exceed",
  763. "title": "额度预警通知",
  764. "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
  765. "values": ["$0.99"],
  766. "timestamp": 1739950503
  767. }`}
  768. </pre>
  769. </Collapsible>
  770. </Typography.Text>
  771. </div>
  772. </div>
  773. <div style={{marginTop: 20}}>
  774. <Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text>
  775. <div style={{marginTop: 10}}>
  776. <Input
  777. value={notificationSettings.webhookSecret}
  778. onChange={val => handleNotificationSettingChange('webhookSecret', val)}
  779. placeholder={t('请输入密钥')}
  780. />
  781. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  782. {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
  783. </Typography.Text>
  784. <Typography.Text type="secondary" style={{marginTop: 4, display: 'block'}}>
  785. {t('Authorization: Bearer your-secret-key')}
  786. </Typography.Text>
  787. </div>
  788. </div>
  789. </>
  790. )}
  791. {notificationSettings.warningType === 'email' && (
  792. <div style={{marginTop: 20}}>
  793. <Typography.Text strong>{t('通知邮箱')}</Typography.Text>
  794. <div style={{marginTop: 10}}>
  795. <Input
  796. value={notificationSettings.notificationEmail}
  797. onChange={val => handleNotificationSettingChange('notificationEmail', val)}
  798. placeholder={t('留空则使用账号绑定的邮箱')}
  799. />
  800. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  801. {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
  802. </Typography.Text>
  803. </div>
  804. </div>
  805. )}
  806. <div style={{marginTop: 20}}>
  807. <Typography.Text strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
  808. <div style={{marginTop: 10}}>
  809. <AutoComplete
  810. value={notificationSettings.warningThreshold}
  811. onChange={val => handleNotificationSettingChange('warningThreshold', val)}
  812. style={{width: 200}}
  813. placeholder={t('请输入预警额度')}
  814. data={[
  815. { value: 100000, label: '0.2$' },
  816. { value: 500000, label: '1$' },
  817. { value: 1000000, label: '5$' },
  818. { value: 5000000, label: '10$' }
  819. ]}
  820. />
  821. </div>
  822. <Typography.Text type="secondary" style={{marginTop: 10, display: 'block'}}>
  823. {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
  824. </Typography.Text>
  825. </div>
  826. <div style={{marginTop: 20}}>
  827. <Button type="primary" onClick={saveNotificationSettings}>
  828. {t('保存设置')}
  829. </Button>
  830. </div>
  831. </Card>
  832. <Modal
  833. onCancel={() => setShowEmailBindModal(false)}
  834. onOk={bindEmail}
  835. visible={showEmailBindModal}
  836. size={'small'}
  837. centered={true}
  838. maskClosable={false}
  839. >
  840. <Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
  841. <div
  842. style={{
  843. marginTop: 20,
  844. display: 'flex',
  845. justifyContent: 'space-between',
  846. }}
  847. >
  848. <Input
  849. fluid
  850. placeholder='输入邮箱地址'
  851. onChange={(value) => handleInputChange('email', value)}
  852. name='email'
  853. type='email'
  854. />
  855. <Button
  856. onClick={sendVerificationCode}
  857. disabled={disableButton || loading}
  858. >
  859. {disableButton ? `重新发送 (${countdown})` : '获取验证码'}
  860. </Button>
  861. </div>
  862. <div style={{marginTop: 10}}>
  863. <Input
  864. fluid
  865. placeholder='验证码'
  866. name='email_verification_code'
  867. value={inputs.email_verification_code}
  868. onChange={(value) =>
  869. handleInputChange('email_verification_code', value)
  870. }
  871. />
  872. </div>
  873. {turnstileEnabled ? (
  874. <Turnstile
  875. sitekey={turnstileSiteKey}
  876. onVerify={(token) => {
  877. setTurnstileToken(token);
  878. }}
  879. />
  880. ) : (
  881. <></>
  882. )}
  883. </Modal>
  884. <Modal
  885. onCancel={() => setShowAccountDeleteModal(false)}
  886. visible={showAccountDeleteModal}
  887. size={'small'}
  888. centered={true}
  889. onOk={deleteAccount}
  890. >
  891. <div style={{marginTop: 20}}>
  892. <Banner
  893. type='danger'
  894. description='您正在删除自己的帐户,将清空所有数据且不可恢复'
  895. closeIcon={null}
  896. />
  897. </div>
  898. <div style={{marginTop: 20}}>
  899. <Input
  900. placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
  901. name='self_account_deletion_confirmation'
  902. value={inputs.self_account_deletion_confirmation}
  903. onChange={(value) =>
  904. handleInputChange(
  905. 'self_account_deletion_confirmation',
  906. value,
  907. )
  908. }
  909. />
  910. {turnstileEnabled ? (
  911. <Turnstile
  912. sitekey={turnstileSiteKey}
  913. onVerify={(token) => {
  914. setTurnstileToken(token);
  915. }}
  916. />
  917. ) : (
  918. <></>
  919. )}
  920. </div>
  921. </Modal>
  922. <Modal
  923. onCancel={() => setShowChangePasswordModal(false)}
  924. visible={showChangePasswordModal}
  925. size={'small'}
  926. centered={true}
  927. onOk={changePassword}
  928. >
  929. <div style={{marginTop: 20}}>
  930. <Input
  931. name='set_new_password'
  932. placeholder={t('新密码')}
  933. value={inputs.set_new_password}
  934. onChange={(value) =>
  935. handleInputChange('set_new_password', value)
  936. }
  937. />
  938. <Input
  939. style={{marginTop: 20}}
  940. name='set_new_password_confirmation'
  941. placeholder={t('确认新密码')}
  942. value={inputs.set_new_password_confirmation}
  943. onChange={(value) =>
  944. handleInputChange('set_new_password_confirmation', value)
  945. }
  946. />
  947. {turnstileEnabled ? (
  948. <Turnstile
  949. sitekey={turnstileSiteKey}
  950. onVerify={(token) => {
  951. setTurnstileToken(token);
  952. }}
  953. />
  954. ) : (
  955. <></>
  956. )}
  957. </div>
  958. </Modal>
  959. </div>
  960. </Layout.Content>
  961. </Layout>
  962. </div>
  963. );
  964. };
  965. export default PersonalSetting;