PersonalSetting.js 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  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, onOIDCClicked, 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>
  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('OIDC')}</Typography.Text>
  615. <div
  616. style={{display: 'flex', justifyContent: 'space-between'}}
  617. >
  618. <div>
  619. <Input
  620. value={
  621. userState.user && userState.user.oidc_id !== ''
  622. ? userState.user.oidc_id
  623. : t('未绑定')
  624. }
  625. readonly={true}
  626. ></Input>
  627. </div>
  628. <div>
  629. <Button
  630. onClick={() => {
  631. onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
  632. }}
  633. disabled={
  634. (userState.user && userState.user.oidc_id !== '') ||
  635. !status.oidc_enabled
  636. }
  637. >
  638. {status.oidc_enabled ? t('绑定') : t('未启用')}
  639. </Button>
  640. </div>
  641. </div>
  642. </div>
  643. <div style={{marginTop: 10}}>
  644. <Typography.Text strong>{t('Telegram')}</Typography.Text>
  645. <div
  646. style={{display: 'flex', justifyContent: 'space-between'}}
  647. >
  648. <div>
  649. <Input
  650. value={
  651. userState.user && userState.user.telegram_id !== ''
  652. ? userState.user.telegram_id
  653. : t('未绑定')
  654. }
  655. readonly={true}
  656. ></Input>
  657. </div>
  658. <div>
  659. {status.telegram_oauth ? (
  660. userState.user.telegram_id !== '' ? (
  661. <Button disabled={true}>{t('已绑定')}</Button>
  662. ) : (
  663. <TelegramLoginButton
  664. dataAuthUrl='/api/oauth/telegram/bind'
  665. botName={status.telegram_bot_name}
  666. />
  667. )
  668. ) : (
  669. <Button disabled={true}>{t('未启用')}</Button>
  670. )}
  671. </div>
  672. </div>
  673. </div>
  674. <div style={{marginTop: 10}}>
  675. <Typography.Text strong>{t('LinuxDO')}</Typography.Text>
  676. <div
  677. style={{display: 'flex', justifyContent: 'space-between'}}
  678. >
  679. <div>
  680. <Input
  681. value={
  682. userState.user && userState.user.linux_do_id !== ''
  683. ? userState.user.linux_do_id
  684. : t('未绑定')
  685. }
  686. readonly={true}
  687. ></Input>
  688. </div>
  689. <div>
  690. <Button
  691. onClick={() => {
  692. onLinuxDOOAuthClicked(status.linuxdo_client_id);
  693. }}
  694. disabled={
  695. (userState.user && userState.user.linux_do_id !== '') ||
  696. !status.linuxdo_oauth
  697. }
  698. >
  699. {status.linuxdo_oauth ? t('绑定') : t('未启用')}
  700. </Button>
  701. </div>
  702. </div>
  703. </div>
  704. <div style={{marginTop: 10}}>
  705. <Space>
  706. <Button onClick={generateAccessToken}>
  707. {t('生成系统访问令牌')}
  708. </Button>
  709. <Button
  710. onClick={() => {
  711. setShowChangePasswordModal(true);
  712. }}
  713. >
  714. {t('修改密码')}
  715. </Button>
  716. <Button
  717. type={'danger'}
  718. onClick={() => {
  719. setShowAccountDeleteModal(true);
  720. }}
  721. >
  722. {t('删除个人账户')}
  723. </Button>
  724. </Space>
  725. {systemToken && (
  726. <Input
  727. readOnly
  728. value={systemToken}
  729. onClick={handleSystemTokenClick}
  730. style={{marginTop: '10px'}}
  731. />
  732. )}
  733. <Modal
  734. onCancel={() => setShowWeChatBindModal(false)}
  735. visible={showWeChatBindModal}
  736. size={'small'}
  737. >
  738. <Image src={status.wechat_qrcode}/>
  739. <div style={{textAlign: 'center'}}>
  740. <p>
  741. 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
  742. </p>
  743. </div>
  744. <Input
  745. placeholder='验证码'
  746. name='wechat_verification_code'
  747. value={inputs.wechat_verification_code}
  748. onChange={(v) =>
  749. handleInputChange('wechat_verification_code', v)
  750. }
  751. />
  752. <Button color='' fluid size='large' onClick={bindWeChat}>
  753. {t('绑定')}
  754. </Button>
  755. </Modal>
  756. </div>
  757. </Card>
  758. <Card style={{marginTop: 10}}>
  759. <Typography.Title heading={6}>{t('通知设置')}</Typography.Title>
  760. <div style={{marginTop: 20}}>
  761. <Typography.Text strong>{t('通知方式')}</Typography.Text>
  762. <div style={{marginTop: 10}}>
  763. <RadioGroup
  764. value={notificationSettings.warningType}
  765. onChange={value => handleNotificationSettingChange('warningType', value)}
  766. >
  767. <Radio value="email">{t('邮件通知')}</Radio>
  768. <Radio value="webhook">{t('Webhook通知')}</Radio>
  769. </RadioGroup>
  770. </div>
  771. </div>
  772. {notificationSettings.warningType === 'webhook' && (
  773. <>
  774. <div style={{marginTop: 20}}>
  775. <Typography.Text strong>{t('Webhook地址')}</Typography.Text>
  776. <div style={{marginTop: 10}}>
  777. <Input
  778. value={notificationSettings.webhookUrl}
  779. onChange={val => handleNotificationSettingChange('webhookUrl', val)}
  780. placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
  781. />
  782. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  783. {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
  784. </Typography.Text>
  785. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  786. <div style={{cursor: 'pointer'}} onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
  787. {t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'}
  788. </div>
  789. <Collapsible isOpen={showWebhookDocs}>
  790. <pre style={{marginTop: 4, background: 'var(--semi-color-fill-0)', padding: 8, borderRadius: 4}}>
  791. {`{
  792. "type": "quota_exceed", // 通知类型
  793. "title": "标题", // 通知标题
  794. "content": "通知内容", // 通知内容,支持 {{value}} 变量占位符
  795. "values": ["值1", "值2"], // 按顺序替换content中的 {{value}} 占位符
  796. "timestamp": 1739950503 // 时间戳
  797. }
  798. 示例:
  799. {
  800. "type": "quota_exceed",
  801. "title": "额度预警通知",
  802. "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
  803. "values": ["$0.99"],
  804. "timestamp": 1739950503
  805. }`}
  806. </pre>
  807. </Collapsible>
  808. </Typography.Text>
  809. </div>
  810. </div>
  811. <div style={{marginTop: 20}}>
  812. <Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text>
  813. <div style={{marginTop: 10}}>
  814. <Input
  815. value={notificationSettings.webhookSecret}
  816. onChange={val => handleNotificationSettingChange('webhookSecret', val)}
  817. placeholder={t('请输入密钥')}
  818. />
  819. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  820. {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
  821. </Typography.Text>
  822. <Typography.Text type="secondary" style={{marginTop: 4, display: 'block'}}>
  823. {t('Authorization: Bearer your-secret-key')}
  824. </Typography.Text>
  825. </div>
  826. </div>
  827. </>
  828. )}
  829. {notificationSettings.warningType === 'email' && (
  830. <div style={{marginTop: 20}}>
  831. <Typography.Text strong>{t('通知邮箱')}</Typography.Text>
  832. <div style={{marginTop: 10}}>
  833. <Input
  834. value={notificationSettings.notificationEmail}
  835. onChange={val => handleNotificationSettingChange('notificationEmail', val)}
  836. placeholder={t('留空则使用账号绑定的邮箱')}
  837. />
  838. <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
  839. {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
  840. </Typography.Text>
  841. </div>
  842. </div>
  843. )}
  844. <div style={{marginTop: 20}}>
  845. <Typography.Text strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
  846. <div style={{marginTop: 10}}>
  847. <AutoComplete
  848. value={notificationSettings.warningThreshold}
  849. onChange={val => handleNotificationSettingChange('warningThreshold', val)}
  850. style={{width: 200}}
  851. placeholder={t('请输入预警额度')}
  852. data={[
  853. { value: 100000, label: '0.2$' },
  854. { value: 500000, label: '1$' },
  855. { value: 1000000, label: '5$' },
  856. { value: 5000000, label: '10$' }
  857. ]}
  858. />
  859. </div>
  860. <Typography.Text type="secondary" style={{marginTop: 10, display: 'block'}}>
  861. {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
  862. </Typography.Text>
  863. </div>
  864. <div style={{marginTop: 20}}>
  865. <Button type="primary" onClick={saveNotificationSettings}>
  866. {t('保存设置')}
  867. </Button>
  868. </div>
  869. </Card>
  870. <Modal
  871. onCancel={() => setShowEmailBindModal(false)}
  872. onOk={bindEmail}
  873. visible={showEmailBindModal}
  874. size={'small'}
  875. centered={true}
  876. maskClosable={false}
  877. >
  878. <Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
  879. <div
  880. style={{
  881. marginTop: 20,
  882. display: 'flex',
  883. justifyContent: 'space-between',
  884. }}
  885. >
  886. <Input
  887. fluid
  888. placeholder='输入邮箱地址'
  889. onChange={(value) => handleInputChange('email', value)}
  890. name='email'
  891. type='email'
  892. />
  893. <Button
  894. onClick={sendVerificationCode}
  895. disabled={disableButton || loading}
  896. >
  897. {disableButton ? `重新发送 (${countdown})` : '获取验证码'}
  898. </Button>
  899. </div>
  900. <div style={{marginTop: 10}}>
  901. <Input
  902. fluid
  903. placeholder='验证码'
  904. name='email_verification_code'
  905. value={inputs.email_verification_code}
  906. onChange={(value) =>
  907. handleInputChange('email_verification_code', value)
  908. }
  909. />
  910. </div>
  911. {turnstileEnabled ? (
  912. <Turnstile
  913. sitekey={turnstileSiteKey}
  914. onVerify={(token) => {
  915. setTurnstileToken(token);
  916. }}
  917. />
  918. ) : (
  919. <></>
  920. )}
  921. </Modal>
  922. <Modal
  923. onCancel={() => setShowAccountDeleteModal(false)}
  924. visible={showAccountDeleteModal}
  925. size={'small'}
  926. centered={true}
  927. onOk={deleteAccount}
  928. >
  929. <div style={{marginTop: 20}}>
  930. <Banner
  931. type='danger'
  932. description='您正在删除自己的帐户,将清空所有数据且不可恢复'
  933. closeIcon={null}
  934. />
  935. </div>
  936. <div style={{marginTop: 20}}>
  937. <Input
  938. placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
  939. name='self_account_deletion_confirmation'
  940. value={inputs.self_account_deletion_confirmation}
  941. onChange={(value) =>
  942. handleInputChange(
  943. 'self_account_deletion_confirmation',
  944. value,
  945. )
  946. }
  947. />
  948. {turnstileEnabled ? (
  949. <Turnstile
  950. sitekey={turnstileSiteKey}
  951. onVerify={(token) => {
  952. setTurnstileToken(token);
  953. }}
  954. />
  955. ) : (
  956. <></>
  957. )}
  958. </div>
  959. </Modal>
  960. <Modal
  961. onCancel={() => setShowChangePasswordModal(false)}
  962. visible={showChangePasswordModal}
  963. size={'small'}
  964. centered={true}
  965. onOk={changePassword}
  966. >
  967. <div style={{marginTop: 20}}>
  968. <Input
  969. name='set_new_password'
  970. placeholder={t('新密码')}
  971. value={inputs.set_new_password}
  972. onChange={(value) =>
  973. handleInputChange('set_new_password', value)
  974. }
  975. />
  976. <Input
  977. style={{marginTop: 20}}
  978. name='set_new_password_confirmation'
  979. placeholder={t('确认新密码')}
  980. value={inputs.set_new_password_confirmation}
  981. onChange={(value) =>
  982. handleInputChange('set_new_password_confirmation', value)
  983. }
  984. />
  985. {turnstileEnabled ? (
  986. <Turnstile
  987. sitekey={turnstileSiteKey}
  988. onVerify={(token) => {
  989. setTurnstileToken(token);
  990. }}
  991. />
  992. ) : (
  993. <></>
  994. )}
  995. </div>
  996. </Modal>
  997. </div>
  998. </Layout.Content>
  999. </Layout>
  1000. </div>
  1001. );
  1002. };
  1003. export default PersonalSetting;