PersonalSetting.js 13 KB

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