PersonalSetting.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. import React, {useContext, useEffect, useState} from 'react';
  2. import {Link, useNavigate} from 'react-router-dom';
  3. import {API, copy, isRoot, showError, showInfo, showNotice, showSuccess} from '../helpers';
  4. import Turnstile from 'react-turnstile';
  5. import {UserContext} from '../context/User';
  6. import {onGitHubOAuthClicked} from './utils';
  7. import {
  8. Avatar, Banner,
  9. Button,
  10. Card,
  11. Descriptions,
  12. Divider, Image,
  13. Input, InputNumber,
  14. Layout,
  15. Modal,
  16. Space,
  17. Tag,
  18. Typography
  19. } from "@douyinfe/semi-ui";
  20. import {getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor} from "../helpers/render";
  21. import EditToken from "../pages/Token/EditToken";
  22. import EditUser from "../pages/User/EditUser";
  23. import passwordResetConfirm from "./PasswordResetConfirm";
  24. const PersonalSetting = () => {
  25. const [userState, userDispatch] = useContext(UserContext);
  26. let navigate = useNavigate();
  27. const [inputs, setInputs] = useState({
  28. wechat_verification_code: '',
  29. email_verification_code: '',
  30. email: '',
  31. self_account_deletion_confirmation: '',
  32. set_new_password: '',
  33. set_new_password_confirmation: '',
  34. });
  35. const [status, setStatus] = useState({});
  36. const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
  37. const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
  38. const [showEmailBindModal, setShowEmailBindModal] = useState(false);
  39. const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
  40. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  41. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  42. const [turnstileToken, setTurnstileToken] = useState('');
  43. const [loading, setLoading] = useState(false);
  44. const [disableButton, setDisableButton] = useState(false);
  45. const [countdown, setCountdown] = useState(30);
  46. const [affLink, setAffLink] = useState("");
  47. const [systemToken, setSystemToken] = useState("");
  48. const [models, setModels] = useState([]);
  49. const [openTransfer, setOpenTransfer] = useState(false);
  50. const [transferAmount, setTransferAmount] = useState(0);
  51. useEffect(() => {
  52. // let user = localStorage.getItem('user');
  53. // if (user) {
  54. // userDispatch({ type: 'login', payload: user });
  55. // }
  56. // console.log(localStorage.getItem('user'))
  57. let status = localStorage.getItem('status');
  58. if (status) {
  59. status = JSON.parse(status);
  60. setStatus(status);
  61. if (status.turnstile_check) {
  62. setTurnstileEnabled(true);
  63. setTurnstileSiteKey(status.turnstile_site_key);
  64. }
  65. }
  66. getUserData().then(
  67. (res) => {
  68. console.log(userState)
  69. }
  70. );
  71. loadModels().then();
  72. getAffLink().then();
  73. setTransferAmount(getQuotaPerUnit())
  74. }, []);
  75. useEffect(() => {
  76. let countdownInterval = null;
  77. if (disableButton && countdown > 0) {
  78. countdownInterval = setInterval(() => {
  79. setCountdown(countdown - 1);
  80. }, 1000);
  81. } else if (countdown === 0) {
  82. setDisableButton(false);
  83. setCountdown(30);
  84. }
  85. return () => clearInterval(countdownInterval); // Clean up on unmount
  86. }, [disableButton, countdown]);
  87. const handleInputChange = (name, value) => {
  88. setInputs((inputs) => ({...inputs, [name]: value}));
  89. };
  90. const generateAccessToken = async () => {
  91. const res = await API.get('/api/user/token');
  92. const {success, message, data} = res.data;
  93. if (success) {
  94. setSystemToken(data);
  95. await copy(data);
  96. showSuccess(`令牌已重置并已复制到剪贴板`);
  97. } else {
  98. showError(message);
  99. }
  100. };
  101. const getAffLink = async () => {
  102. const res = await API.get('/api/user/aff');
  103. const {success, message, data} = res.data;
  104. if (success) {
  105. let link = `${window.location.origin}/register?aff=${data}`;
  106. setAffLink(link);
  107. } else {
  108. showError(message);
  109. }
  110. };
  111. const getUserData = async () => {
  112. let res = await API.get(`/api/user/self`);
  113. const {success, message, data} = res.data;
  114. if (success) {
  115. userDispatch({type: 'login', payload: data});
  116. } else {
  117. showError(message);
  118. }
  119. }
  120. const loadModels = async () => {
  121. let res = await API.get(`/api/user/models`);
  122. const {success, message, data} = res.data;
  123. if (success) {
  124. setModels(data);
  125. console.log(data)
  126. } else {
  127. showError(message);
  128. }
  129. }
  130. const handleAffLinkClick = async (e) => {
  131. e.target.select();
  132. await copy(e.target.value);
  133. showSuccess(`邀请链接已复制到剪切板`);
  134. };
  135. const handleSystemTokenClick = async (e) => {
  136. e.target.select();
  137. await copy(e.target.value);
  138. showSuccess(`系统令牌已复制到剪切板`);
  139. };
  140. const deleteAccount = async () => {
  141. if (inputs.self_account_deletion_confirmation !== userState.user.username) {
  142. showError('请输入你的账户名以确认删除!');
  143. return;
  144. }
  145. const res = await API.delete('/api/user/self');
  146. const {success, message} = res.data;
  147. if (success) {
  148. showSuccess('账户已删除!');
  149. await API.get('/api/user/logout');
  150. userDispatch({type: 'logout'});
  151. localStorage.removeItem('user');
  152. navigate('/login');
  153. } else {
  154. showError(message);
  155. }
  156. };
  157. const bindWeChat = async () => {
  158. if (inputs.wechat_verification_code === '') return;
  159. const res = await API.get(
  160. `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
  161. );
  162. const {success, message} = res.data;
  163. if (success) {
  164. showSuccess('微信账户绑定成功!');
  165. setShowWeChatBindModal(false);
  166. } else {
  167. showError(message);
  168. }
  169. };
  170. const changePassword = async () => {
  171. if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
  172. showError('两次输入的密码不一致!');
  173. return;
  174. }
  175. const res = await API.put(
  176. `/api/user/self`,
  177. {
  178. password: inputs.set_new_password
  179. }
  180. );
  181. const {success, message} = res.data;
  182. if (success) {
  183. showSuccess('密码修改成功!');
  184. setShowWeChatBindModal(false);
  185. } else {
  186. showError(message);
  187. }
  188. setShowChangePasswordModal(false);
  189. };
  190. const transfer = async () => {
  191. if (transferAmount < getQuotaPerUnit()) {
  192. showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
  193. return;
  194. }
  195. const res = await API.post(
  196. `/api/user/aff_transfer`,
  197. {
  198. quota: transferAmount
  199. }
  200. );
  201. const {success, message} = res.data;
  202. if (success) {
  203. showSuccess(message);
  204. setOpenTransfer(false);
  205. getUserData().then();
  206. } else {
  207. showError(message);
  208. }
  209. };
  210. const sendVerificationCode = async () => {
  211. if (inputs.email === '') {
  212. showError('请输入邮箱!');
  213. return;
  214. }
  215. setDisableButton(true);
  216. if (turnstileEnabled && turnstileToken === '') {
  217. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  218. return;
  219. }
  220. setLoading(true);
  221. const res = await API.get(
  222. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
  223. );
  224. const {success, message} = res.data;
  225. if (success) {
  226. showSuccess('验证码发送成功,请检查邮箱!');
  227. } else {
  228. showError(message);
  229. }
  230. setLoading(false);
  231. };
  232. const bindEmail = async () => {
  233. if (inputs.email_verification_code === '') {
  234. showError('请输入邮箱验证码!');
  235. return;
  236. }
  237. setLoading(true);
  238. const res = await API.get(
  239. `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
  240. );
  241. const {success, message} = res.data;
  242. if (success) {
  243. showSuccess('邮箱账户绑定成功!');
  244. setShowEmailBindModal(false);
  245. userState.user.email = inputs.email;
  246. } else {
  247. showError(message);
  248. }
  249. setLoading(false);
  250. };
  251. const getUsername = () => {
  252. if (userState.user) {
  253. return userState.user.username;
  254. } else {
  255. return 'null';
  256. }
  257. }
  258. const handleCancel = () => {
  259. setOpenTransfer(false);
  260. }
  261. const copyText = async (text) => {
  262. if (await copy(text)) {
  263. showSuccess('已复制:' + text);
  264. } else {
  265. // setSearchKeyword(text);
  266. Modal.error({title: '无法复制到剪贴板,请手动复制', content: text});
  267. }
  268. }
  269. return (
  270. <div>
  271. <Layout>
  272. <Layout.Content>
  273. <Modal
  274. title="请输入要划转的数量"
  275. visible={openTransfer}
  276. onOk={transfer}
  277. onCancel={handleCancel}
  278. maskClosable={false}
  279. size={'small'}
  280. centered={true}
  281. >
  282. <div style={{marginTop: 20}}>
  283. <Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text>
  284. <Input style={{marginTop: 5}} value={userState?.user?.aff_quota} disabled={true}></Input>
  285. </div>
  286. <div style={{marginTop: 20}}>
  287. <Typography.Text>{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` + renderQuota(getQuotaPerUnit())}</Typography.Text>
  288. <div>
  289. <InputNumber min={0} style={{marginTop: 5}} value={transferAmount} onChange={(value)=>setTransferAmount(value)} disabled={false}></InputNumber>
  290. </div>
  291. </div>
  292. </Modal>
  293. <div style={{marginTop: 20}}>
  294. <Card
  295. title={
  296. <Card.Meta
  297. avatar={<Avatar size="default" color={stringToColor(getUsername())}
  298. style={{marginRight: 4}}>
  299. {typeof getUsername() === 'string' && getUsername().slice(0, 1)}
  300. </Avatar>}
  301. title={<Typography.Text>{getUsername()}</Typography.Text>}
  302. description={isRoot()?<Tag color="red">管理员</Tag>:<Tag color="blue">普通用户</Tag>}
  303. ></Card.Meta>
  304. }
  305. headerExtraContent={
  306. <>
  307. <Space vertical align="start">
  308. <Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
  309. <Tag color="blue">{userState?.user?.group}</Tag>
  310. </Space>
  311. </>
  312. }
  313. footer={
  314. <Descriptions row>
  315. <Descriptions.Item itemKey="当前余额">{renderQuota(userState?.user?.quota)}</Descriptions.Item>
  316. <Descriptions.Item itemKey="历史消耗">{renderQuota(userState?.user?.used_quota)}</Descriptions.Item>
  317. <Descriptions.Item itemKey="请求次数">{userState.user?.request_count}</Descriptions.Item>
  318. </Descriptions>
  319. }
  320. >
  321. <Typography.Title heading={6}>可用模型</Typography.Title>
  322. <div style={{marginTop: 10}}>
  323. <Space wrap>
  324. {models.map((model) => (
  325. <Tag key={model} color="cyan" onClick={() => {
  326. copyText(model)
  327. }}>
  328. {model}
  329. </Tag>
  330. ))}
  331. </Space>
  332. </div>
  333. </Card>
  334. <Card
  335. footer={
  336. <div>
  337. <Typography.Text>邀请链接</Typography.Text>
  338. <Input
  339. style={{marginTop: 10}}
  340. value={affLink}
  341. onClick={handleAffLinkClick}
  342. readOnly
  343. />
  344. </div>
  345. }
  346. >
  347. <Typography.Title heading={6}>邀请信息</Typography.Title>
  348. <div style={{marginTop: 10}}>
  349. <Descriptions row>
  350. <Descriptions.Item itemKey="待使用收益">
  351. <span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
  352. {
  353. renderQuota(userState?.user?.aff_quota)
  354. }
  355. </span>
  356. <Button type={'secondary'} onClick={()=>setOpenTransfer(true)} size={'small'} style={{marginLeft: 10}}>划转</Button>
  357. </Descriptions.Item>
  358. <Descriptions.Item itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item>
  359. <Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item>
  360. </Descriptions>
  361. </div>
  362. </Card>
  363. <Card>
  364. <Typography.Title heading={6}>个人信息</Typography.Title>
  365. <div style={{marginTop: 20}}>
  366. <Typography.Text strong>邮箱</Typography.Text>
  367. <div style={{display: 'flex', justifyContent: 'space-between'}}>
  368. <div>
  369. <Input
  370. value={userState.user && userState.user.email !== ''?userState.user.email:'未绑定'}
  371. readonly={true}
  372. ></Input>
  373. </div>
  374. <div>
  375. <Button onClick={()=>{setShowEmailBindModal(true)}} disabled={userState.user && userState.user.email !== ''}>绑定邮箱</Button>
  376. </div>
  377. </div>
  378. </div>
  379. <div style={{marginTop: 10}}>
  380. <Typography.Text strong>微信</Typography.Text>
  381. <div style={{display: 'flex', justifyContent: 'space-between'}}>
  382. <div>
  383. <Input
  384. value={userState.user && userState.user.wechat_id !== ''?'已绑定':'未绑定'}
  385. readonly={true}
  386. ></Input>
  387. </div>
  388. <div>
  389. <Button disabled={(userState.user && userState.user.wechat_id !== '') || !status.wechat_login}>
  390. {
  391. status.wechat_login?'绑定':'未启用'
  392. }
  393. </Button>
  394. </div>
  395. </div>
  396. </div>
  397. <div style={{marginTop: 10}}>
  398. <Typography.Text strong>GitHub</Typography.Text>
  399. <div style={{display: 'flex', justifyContent: 'space-between'}}>
  400. <div>
  401. <Input
  402. value={userState.user && userState.user.github_id !== ''?userState.user.github_id:'未绑定'}
  403. readonly={true}
  404. ></Input>
  405. </div>
  406. <div>
  407. <Button
  408. onClick={() => {onGitHubOAuthClicked(status.github_client_id)}}
  409. disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
  410. >
  411. {
  412. status.github_oauth?'绑定':'未启用'
  413. }
  414. </Button>
  415. </div>
  416. </div>
  417. </div>
  418. <div style={{marginTop: 10}}>
  419. <Space>
  420. <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
  421. <Button onClick={() => {
  422. setShowChangePasswordModal(true);
  423. }}>修改密码</Button>
  424. <Button type={'danger'} onClick={() => {
  425. setShowAccountDeleteModal(true);
  426. }}>删除个人账户</Button>
  427. </Space>
  428. {systemToken && (
  429. <Input
  430. readOnly
  431. value={systemToken}
  432. onClick={handleSystemTokenClick}
  433. style={{marginTop: '10px'}}
  434. />
  435. )}
  436. {
  437. status.wechat_login && (
  438. <Button
  439. onClick={() => {
  440. setShowWeChatBindModal(true);
  441. }}
  442. >
  443. 绑定微信账号
  444. </Button>
  445. )
  446. }
  447. <Modal
  448. onCancel={() => setShowWeChatBindModal(false)}
  449. // onOpen={() => setShowWeChatBindModal(true)}
  450. visible={showWeChatBindModal}
  451. size={'mini'}
  452. >
  453. <Image src={status.wechat_qrcode}/>
  454. <div style={{textAlign: 'center'}}>
  455. <p>
  456. 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
  457. </p>
  458. </div>
  459. <Input
  460. placeholder='验证码'
  461. name='wechat_verification_code'
  462. value={inputs.wechat_verification_code}
  463. onChange={(v)=>handleInputChange('wechat_verification_code', v)}
  464. />
  465. <Button color='' fluid size='large' onClick={bindWeChat}>
  466. 绑定
  467. </Button>
  468. </Modal>
  469. </div>
  470. </Card>
  471. <Modal
  472. onCancel={() => setShowEmailBindModal(false)}
  473. // onOpen={() => setShowEmailBindModal(true)}
  474. onOk={bindEmail}
  475. visible={showEmailBindModal}
  476. size={'small'}
  477. centered={true}
  478. maskClosable={false}
  479. >
  480. <Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
  481. <div style={{marginTop: 20, display: 'flex', justifyContent: 'space-between'}}>
  482. <Input
  483. fluid
  484. placeholder='输入邮箱地址'
  485. onChange={(value)=>handleInputChange('email', value)}
  486. name='email'
  487. type='email'
  488. />
  489. <Button onClick={sendVerificationCode}
  490. disabled={disableButton || loading}>
  491. {disableButton ? `重新发送(${countdown})` : '获取验证码'}
  492. </Button>
  493. </div>
  494. <div style={{marginTop: 10}}>
  495. <Input
  496. fluid
  497. placeholder='验证码'
  498. name='email_verification_code'
  499. value={inputs.email_verification_code}
  500. onChange={(value)=>handleInputChange('email_verification_code', value)}
  501. />
  502. </div>
  503. {turnstileEnabled ? (
  504. <Turnstile
  505. sitekey={turnstileSiteKey}
  506. onVerify={(token) => {
  507. setTurnstileToken(token);
  508. }}
  509. />
  510. ) : (
  511. <></>
  512. )}
  513. </Modal>
  514. <Modal
  515. onCancel={() => setShowAccountDeleteModal(false)}
  516. visible={showAccountDeleteModal}
  517. size={'small'}
  518. centered={true}
  519. onOk={deleteAccount}
  520. >
  521. <div style={{marginTop: 20}}>
  522. <Banner
  523. type="danger"
  524. description="您正在删除自己的帐户,将清空所有数据且不可恢复"
  525. closeIcon={null}
  526. />
  527. </div>
  528. <div style={{marginTop: 20}}>
  529. <Input
  530. placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
  531. name='self_account_deletion_confirmation'
  532. value={inputs.self_account_deletion_confirmation}
  533. onChange={(value)=>handleInputChange('self_account_deletion_confirmation', value)}
  534. />
  535. {turnstileEnabled ? (
  536. <Turnstile
  537. sitekey={turnstileSiteKey}
  538. onVerify={(token) => {
  539. setTurnstileToken(token);
  540. }}
  541. />
  542. ) : (
  543. <></>
  544. )}
  545. </div>
  546. </Modal>
  547. <Modal
  548. onCancel={() => setShowChangePasswordModal(false)}
  549. visible={showChangePasswordModal}
  550. size={'small'}
  551. centered={true}
  552. onOk={changePassword}
  553. >
  554. <div style={{marginTop: 20}}>
  555. <Input
  556. name='set_new_password'
  557. placeholder='新密码'
  558. value={inputs.set_new_password}
  559. onChange={(value)=>handleInputChange('set_new_password', value)}
  560. />
  561. <Input
  562. style={{marginTop: 20}}
  563. name='set_new_password_confirmation'
  564. placeholder='确认新密码'
  565. value={inputs.set_new_password_confirmation}
  566. onChange={(value)=>handleInputChange('set_new_password_confirmation', value)}
  567. />
  568. {turnstileEnabled ? (
  569. <Turnstile
  570. sitekey={turnstileSiteKey}
  571. onVerify={(token) => {
  572. setTurnstileToken(token);
  573. }}
  574. />
  575. ) : (
  576. <></>
  577. )}
  578. </div>
  579. </Modal>
  580. </div>
  581. </Layout.Content>
  582. </Layout>
  583. </div>
  584. );
  585. };
  586. export default PersonalSetting;