PersonalSetting.js 48 KB

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