PersonalSetting.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useContext, useEffect, useState } from 'react';
  16. import { useNavigate } from 'react-router-dom';
  17. import { API, copy, showError, showInfo, showSuccess } from '../../helpers';
  18. import { UserContext } from '../../context/User';
  19. import { Modal } from '@douyinfe/semi-ui';
  20. import { useTranslation } from 'react-i18next';
  21. // 导入子组件
  22. import UserInfoHeader from './personal/components/UserInfoHeader';
  23. import AccountManagement from './personal/cards/AccountManagement';
  24. import NotificationSettings from './personal/cards/NotificationSettings';
  25. import EmailBindModal from './personal/modals/EmailBindModal';
  26. import WeChatBindModal from './personal/modals/WeChatBindModal';
  27. import AccountDeleteModal from './personal/modals/AccountDeleteModal';
  28. import ChangePasswordModal from './personal/modals/ChangePasswordModal';
  29. const PersonalSetting = () => {
  30. const [userState, userDispatch] = useContext(UserContext);
  31. let navigate = useNavigate();
  32. const { t } = useTranslation();
  33. const [inputs, setInputs] = useState({
  34. wechat_verification_code: '',
  35. email_verification_code: '',
  36. email: '',
  37. self_account_deletion_confirmation: '',
  38. original_password: '',
  39. set_new_password: '',
  40. set_new_password_confirmation: '',
  41. });
  42. const [status, setStatus] = useState({});
  43. const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
  44. const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
  45. const [showEmailBindModal, setShowEmailBindModal] = useState(false);
  46. const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
  47. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  48. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  49. const [turnstileToken, setTurnstileToken] = useState('');
  50. const [loading, setLoading] = useState(false);
  51. const [disableButton, setDisableButton] = useState(false);
  52. const [countdown, setCountdown] = useState(30);
  53. const [systemToken, setSystemToken] = useState('');
  54. const [notificationSettings, setNotificationSettings] = useState({
  55. warningType: 'email',
  56. warningThreshold: 100000,
  57. webhookUrl: '',
  58. webhookSecret: '',
  59. notificationEmail: '',
  60. barkUrl: '',
  61. acceptUnsetModelRatioModel: false,
  62. recordIpLog: false,
  63. });
  64. useEffect(() => {
  65. let status = localStorage.getItem('status');
  66. if (status) {
  67. status = JSON.parse(status);
  68. setStatus(status);
  69. if (status.turnstile_check) {
  70. setTurnstileEnabled(true);
  71. setTurnstileSiteKey(status.turnstile_site_key);
  72. }
  73. }
  74. getUserData().then((res) => {
  75. console.log(userState);
  76. });
  77. }, []);
  78. useEffect(() => {
  79. let countdownInterval = null;
  80. if (disableButton && countdown > 0) {
  81. countdownInterval = setInterval(() => {
  82. setCountdown(countdown - 1);
  83. }, 1000);
  84. } else if (countdown === 0) {
  85. setDisableButton(false);
  86. setCountdown(30);
  87. }
  88. return () => clearInterval(countdownInterval); // Clean up on unmount
  89. }, [disableButton, countdown]);
  90. useEffect(() => {
  91. if (userState?.user?.setting) {
  92. const settings = JSON.parse(userState.user.setting);
  93. setNotificationSettings({
  94. warningType: settings.notify_type || 'email',
  95. warningThreshold: settings.quota_warning_threshold || 500000,
  96. webhookUrl: settings.webhook_url || '',
  97. webhookSecret: settings.webhook_secret || '',
  98. notificationEmail: settings.notification_email || '',
  99. barkUrl: settings.bark_url || '',
  100. acceptUnsetModelRatioModel:
  101. settings.accept_unset_model_ratio_model || false,
  102. recordIpLog: settings.record_ip_log || false,
  103. });
  104. }
  105. }, [userState?.user?.setting]);
  106. const handleInputChange = (name, value) => {
  107. setInputs((inputs) => ({ ...inputs, [name]: value }));
  108. };
  109. const generateAccessToken = async () => {
  110. const res = await API.get('/api/user/token');
  111. const { success, message, data } = res.data;
  112. if (success) {
  113. setSystemToken(data);
  114. await copy(data);
  115. showSuccess(t('令牌已重置并已复制到剪贴板'));
  116. } else {
  117. showError(message);
  118. }
  119. };
  120. const getUserData = async () => {
  121. let res = await API.get(`/api/user/self`);
  122. const { success, message, data } = res.data;
  123. if (success) {
  124. userDispatch({ type: 'login', payload: data });
  125. } else {
  126. showError(message);
  127. }
  128. };
  129. const handleSystemTokenClick = async (e) => {
  130. e.target.select();
  131. await copy(e.target.value);
  132. showSuccess(t('系统令牌已复制到剪切板'));
  133. };
  134. const deleteAccount = async () => {
  135. if (inputs.self_account_deletion_confirmation !== userState.user.username) {
  136. showError(t('请输入你的账户名以确认删除!'));
  137. return;
  138. }
  139. const res = await API.delete('/api/user/self');
  140. const { success, message } = res.data;
  141. if (success) {
  142. showSuccess(t('账户已删除!'));
  143. await API.get('/api/user/logout');
  144. userDispatch({ type: 'logout' });
  145. localStorage.removeItem('user');
  146. navigate('/login');
  147. } else {
  148. showError(message);
  149. }
  150. };
  151. const bindWeChat = async () => {
  152. if (inputs.wechat_verification_code === '') return;
  153. const res = await API.get(
  154. `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
  155. );
  156. const { success, message } = res.data;
  157. if (success) {
  158. showSuccess(t('微信账户绑定成功!'));
  159. setShowWeChatBindModal(false);
  160. } else {
  161. showError(message);
  162. }
  163. };
  164. const changePassword = async () => {
  165. if (inputs.original_password === '') {
  166. showError(t('请输入原密码!'));
  167. return;
  168. }
  169. if (inputs.set_new_password === '') {
  170. showError(t('请输入新密码!'));
  171. return;
  172. }
  173. if (inputs.original_password === inputs.set_new_password) {
  174. showError(t('新密码需要和原密码不一致!'));
  175. return;
  176. }
  177. if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
  178. showError(t('两次输入的密码不一致!'));
  179. return;
  180. }
  181. const res = await API.put(`/api/user/self`, {
  182. original_password: inputs.original_password,
  183. password: inputs.set_new_password,
  184. });
  185. const { success, message } = res.data;
  186. if (success) {
  187. showSuccess(t('密码修改成功!'));
  188. setShowWeChatBindModal(false);
  189. } else {
  190. showError(message);
  191. }
  192. setShowChangePasswordModal(false);
  193. };
  194. const sendVerificationCode = async () => {
  195. if (inputs.email === '') {
  196. showError(t('请输入邮箱!'));
  197. return;
  198. }
  199. setDisableButton(true);
  200. if (turnstileEnabled && turnstileToken === '') {
  201. showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
  202. return;
  203. }
  204. setLoading(true);
  205. const res = await API.get(
  206. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
  207. );
  208. const { success, message } = res.data;
  209. if (success) {
  210. showSuccess(t('验证码发送成功,请检查邮箱!'));
  211. } else {
  212. showError(message);
  213. }
  214. setLoading(false);
  215. };
  216. const bindEmail = async () => {
  217. if (inputs.email_verification_code === '') {
  218. showError(t('请输入邮箱验证码!'));
  219. return;
  220. }
  221. setLoading(true);
  222. const res = await API.get(
  223. `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
  224. );
  225. const { success, message } = res.data;
  226. if (success) {
  227. showSuccess(t('邮箱账户绑定成功!'));
  228. setShowEmailBindModal(false);
  229. userState.user.email = inputs.email;
  230. } else {
  231. showError(message);
  232. }
  233. setLoading(false);
  234. };
  235. const copyText = async (text) => {
  236. if (await copy(text)) {
  237. showSuccess(t('已复制:') + text);
  238. } else {
  239. // setSearchKeyword(text);
  240. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  241. }
  242. };
  243. const handleNotificationSettingChange = (type, value) => {
  244. setNotificationSettings((prev) => ({
  245. ...prev,
  246. [type]: value.target
  247. ? value.target.value !== undefined
  248. ? value.target.value
  249. : value.target.checked
  250. : value, // handle checkbox properly
  251. }));
  252. };
  253. const saveNotificationSettings = async () => {
  254. try {
  255. const res = await API.put('/api/user/setting', {
  256. notify_type: notificationSettings.warningType,
  257. quota_warning_threshold: parseFloat(
  258. notificationSettings.warningThreshold,
  259. ),
  260. webhook_url: notificationSettings.webhookUrl,
  261. webhook_secret: notificationSettings.webhookSecret,
  262. notification_email: notificationSettings.notificationEmail,
  263. bark_url: notificationSettings.barkUrl,
  264. accept_unset_model_ratio_model:
  265. notificationSettings.acceptUnsetModelRatioModel,
  266. record_ip_log: notificationSettings.recordIpLog,
  267. });
  268. if (res.data.success) {
  269. showSuccess(t('设置保存成功'));
  270. await getUserData();
  271. } else {
  272. showError(res.data.message);
  273. }
  274. } catch (error) {
  275. showError(t('设置保存失败'));
  276. }
  277. };
  278. return (
  279. <div className='mt-[60px]'>
  280. <div className='flex justify-center'>
  281. <div className='w-full max-w-7xl mx-auto px-2'>
  282. {/* 顶部用户信息区域 */}
  283. <UserInfoHeader t={t} userState={userState} />
  284. {/* 账户管理和其他设置 */}
  285. <div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
  286. {/* 左侧:账户管理设置 */}
  287. <AccountManagement
  288. t={t}
  289. userState={userState}
  290. status={status}
  291. systemToken={systemToken}
  292. setShowEmailBindModal={setShowEmailBindModal}
  293. setShowWeChatBindModal={setShowWeChatBindModal}
  294. generateAccessToken={generateAccessToken}
  295. handleSystemTokenClick={handleSystemTokenClick}
  296. setShowChangePasswordModal={setShowChangePasswordModal}
  297. setShowAccountDeleteModal={setShowAccountDeleteModal}
  298. />
  299. {/* 右侧:其他设置 */}
  300. <NotificationSettings
  301. t={t}
  302. notificationSettings={notificationSettings}
  303. handleNotificationSettingChange={handleNotificationSettingChange}
  304. saveNotificationSettings={saveNotificationSettings}
  305. />
  306. </div>
  307. </div>
  308. </div>
  309. {/* 模态框组件 */}
  310. <EmailBindModal
  311. t={t}
  312. showEmailBindModal={showEmailBindModal}
  313. setShowEmailBindModal={setShowEmailBindModal}
  314. inputs={inputs}
  315. handleInputChange={handleInputChange}
  316. sendVerificationCode={sendVerificationCode}
  317. bindEmail={bindEmail}
  318. disableButton={disableButton}
  319. loading={loading}
  320. countdown={countdown}
  321. turnstileEnabled={turnstileEnabled}
  322. turnstileSiteKey={turnstileSiteKey}
  323. setTurnstileToken={setTurnstileToken}
  324. />
  325. <WeChatBindModal
  326. t={t}
  327. showWeChatBindModal={showWeChatBindModal}
  328. setShowWeChatBindModal={setShowWeChatBindModal}
  329. inputs={inputs}
  330. handleInputChange={handleInputChange}
  331. bindWeChat={bindWeChat}
  332. status={status}
  333. />
  334. <AccountDeleteModal
  335. t={t}
  336. showAccountDeleteModal={showAccountDeleteModal}
  337. setShowAccountDeleteModal={setShowAccountDeleteModal}
  338. inputs={inputs}
  339. handleInputChange={handleInputChange}
  340. deleteAccount={deleteAccount}
  341. userState={userState}
  342. turnstileEnabled={turnstileEnabled}
  343. turnstileSiteKey={turnstileSiteKey}
  344. setTurnstileToken={setTurnstileToken}
  345. />
  346. <ChangePasswordModal
  347. t={t}
  348. showChangePasswordModal={showChangePasswordModal}
  349. setShowChangePasswordModal={setShowChangePasswordModal}
  350. inputs={inputs}
  351. handleInputChange={handleInputChange}
  352. changePassword={changePassword}
  353. turnstileEnabled={turnstileEnabled}
  354. turnstileSiteKey={turnstileSiteKey}
  355. setTurnstileToken={setTurnstileToken}
  356. />
  357. </div>
  358. );
  359. };
  360. export default PersonalSetting;