PersonalSetting.js 39 KB

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