PersonalSetting.js 24 KB

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