- Split the 1554-line PersonalSetting.js file into smaller, maintainable components - Created organized folder structure under personal/: - components/: UserInfoHeader for shared user info display - tabs/: ModelsList, AccountBinding, SecuritySettings, NotificationSettings - modals/: EmailBindModal, WeChatBindModal, AccountDeleteModal, ChangePasswordModal - Refactored main PersonalSetting component to use composition pattern - Improved code maintainability and separation of concerns - Added collapsible prop to ModelsList tabs for better UX - Fixed import path for TwoFASetting component in SecuritySettings - Preserved all existing functionality and user interactions This refactoring reduces the main file from 1554 to 484 lines and makes the codebase more modular, testable, and easier to maintain.
418 lines
13 KiB
JavaScript
418 lines
13 KiB
JavaScript
/*
|
||
Copyright (C) 2025 QuantumNous
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU Affero General Public License as
|
||
published by the Free Software Foundation, either version 3 of the
|
||
License, or (at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU Affero General Public License for more details.
|
||
|
||
You should have received a copy of the GNU Affero General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
For commercial licensing, please contact support@quantumnous.com
|
||
*/
|
||
|
||
import React, { useContext, useEffect, useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import {
|
||
API,
|
||
copy,
|
||
showError,
|
||
showInfo,
|
||
showSuccess
|
||
} from '../../helpers';
|
||
import { UserContext } from '../../context/User';
|
||
import { Modal } from '@douyinfe/semi-ui';
|
||
import { useTranslation } from 'react-i18next';
|
||
|
||
// 导入子组件
|
||
import UserInfoHeader from './personal/components/UserInfoHeader';
|
||
import AccountManagement from './personal/cards/AccountManagement';
|
||
import NotificationSettings from './personal/cards/NotificationSettings';
|
||
import EmailBindModal from './personal/modals/EmailBindModal';
|
||
import WeChatBindModal from './personal/modals/WeChatBindModal';
|
||
import AccountDeleteModal from './personal/modals/AccountDeleteModal';
|
||
import ChangePasswordModal from './personal/modals/ChangePasswordModal';
|
||
|
||
const PersonalSetting = () => {
|
||
const [userState, userDispatch] = useContext(UserContext);
|
||
let navigate = useNavigate();
|
||
const { t } = useTranslation();
|
||
|
||
const [inputs, setInputs] = useState({
|
||
wechat_verification_code: '',
|
||
email_verification_code: '',
|
||
email: '',
|
||
self_account_deletion_confirmation: '',
|
||
original_password: '',
|
||
set_new_password: '',
|
||
set_new_password_confirmation: '',
|
||
});
|
||
const [status, setStatus] = useState({});
|
||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
||
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
|
||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||
const [turnstileToken, setTurnstileToken] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
const [disableButton, setDisableButton] = useState(false);
|
||
const [countdown, setCountdown] = useState(30);
|
||
const [systemToken, setSystemToken] = useState('');
|
||
const [models, setModels] = useState([]);
|
||
const [notificationSettings, setNotificationSettings] = useState({
|
||
warningType: 'email',
|
||
warningThreshold: 100000,
|
||
webhookUrl: '',
|
||
webhookSecret: '',
|
||
notificationEmail: '',
|
||
acceptUnsetModelRatioModel: false,
|
||
recordIpLog: false,
|
||
});
|
||
const [modelsLoading, setModelsLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
let status = localStorage.getItem('status');
|
||
if (status) {
|
||
status = JSON.parse(status);
|
||
setStatus(status);
|
||
if (status.turnstile_check) {
|
||
setTurnstileEnabled(true);
|
||
setTurnstileSiteKey(status.turnstile_site_key);
|
||
}
|
||
}
|
||
getUserData().then((res) => {
|
||
console.log(userState);
|
||
});
|
||
loadModels().then();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
let countdownInterval = null;
|
||
if (disableButton && countdown > 0) {
|
||
countdownInterval = setInterval(() => {
|
||
setCountdown(countdown - 1);
|
||
}, 1000);
|
||
} else if (countdown === 0) {
|
||
setDisableButton(false);
|
||
setCountdown(30);
|
||
}
|
||
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||
}, [disableButton, countdown]);
|
||
|
||
useEffect(() => {
|
||
if (userState?.user?.setting) {
|
||
const settings = JSON.parse(userState.user.setting);
|
||
setNotificationSettings({
|
||
warningType: settings.notify_type || 'email',
|
||
warningThreshold: settings.quota_warning_threshold || 500000,
|
||
webhookUrl: settings.webhook_url || '',
|
||
webhookSecret: settings.webhook_secret || '',
|
||
notificationEmail: settings.notification_email || '',
|
||
acceptUnsetModelRatioModel:
|
||
settings.accept_unset_model_ratio_model || false,
|
||
recordIpLog: settings.record_ip_log || false,
|
||
});
|
||
}
|
||
}, [userState?.user?.setting]);
|
||
|
||
const handleInputChange = (name, value) => {
|
||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||
};
|
||
|
||
const generateAccessToken = async () => {
|
||
const res = await API.get('/api/user/token');
|
||
const { success, message, data } = res.data;
|
||
if (success) {
|
||
setSystemToken(data);
|
||
await copy(data);
|
||
showSuccess(t('令牌已重置并已复制到剪贴板'));
|
||
} else {
|
||
showError(message);
|
||
}
|
||
};
|
||
|
||
const getUserData = async () => {
|
||
let res = await API.get(`/api/user/self`);
|
||
const { success, message, data } = res.data;
|
||
if (success) {
|
||
userDispatch({ type: 'login', payload: data });
|
||
} else {
|
||
showError(message);
|
||
}
|
||
};
|
||
|
||
const loadModels = async () => {
|
||
setModelsLoading(true);
|
||
|
||
try {
|
||
let res = await API.get(`/api/user/models`);
|
||
const { success, message, data } = res.data;
|
||
|
||
if (success) {
|
||
if (data != null) {
|
||
setModels(data);
|
||
}
|
||
} else {
|
||
showError(message);
|
||
}
|
||
} catch (error) {
|
||
showError(t('加载模型列表失败'));
|
||
} finally {
|
||
setModelsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSystemTokenClick = async (e) => {
|
||
e.target.select();
|
||
await copy(e.target.value);
|
||
showSuccess(t('系统令牌已复制到剪切板'));
|
||
};
|
||
|
||
const deleteAccount = async () => {
|
||
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||
showError(t('请输入你的账户名以确认删除!'));
|
||
return;
|
||
}
|
||
|
||
const res = await API.delete('/api/user/self');
|
||
const { success, message } = res.data;
|
||
|
||
if (success) {
|
||
showSuccess(t('账户已删除!'));
|
||
await API.get('/api/user/logout');
|
||
userDispatch({ type: 'logout' });
|
||
localStorage.removeItem('user');
|
||
navigate('/login');
|
||
} else {
|
||
showError(message);
|
||
}
|
||
};
|
||
|
||
const bindWeChat = async () => {
|
||
if (inputs.wechat_verification_code === '') return;
|
||
const res = await API.get(
|
||
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
|
||
);
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('微信账户绑定成功!'));
|
||
setShowWeChatBindModal(false);
|
||
} else {
|
||
showError(message);
|
||
}
|
||
};
|
||
|
||
const changePassword = async () => {
|
||
if (inputs.original_password === '') {
|
||
showError(t('请输入原密码!'));
|
||
return;
|
||
}
|
||
if (inputs.set_new_password === '') {
|
||
showError(t('请输入新密码!'));
|
||
return;
|
||
}
|
||
if (inputs.original_password === inputs.set_new_password) {
|
||
showError(t('新密码需要和原密码不一致!'));
|
||
return;
|
||
}
|
||
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
|
||
showError(t('两次输入的密码不一致!'));
|
||
return;
|
||
}
|
||
const res = await API.put(`/api/user/self`, {
|
||
original_password: inputs.original_password,
|
||
password: inputs.set_new_password,
|
||
});
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('密码修改成功!'));
|
||
setShowWeChatBindModal(false);
|
||
} else {
|
||
showError(message);
|
||
}
|
||
setShowChangePasswordModal(false);
|
||
};
|
||
|
||
const sendVerificationCode = async () => {
|
||
if (inputs.email === '') {
|
||
showError(t('请输入邮箱!'));
|
||
return;
|
||
}
|
||
setDisableButton(true);
|
||
if (turnstileEnabled && turnstileToken === '') {
|
||
showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
const res = await API.get(
|
||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
|
||
);
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('验证码发送成功,请检查邮箱!'));
|
||
} else {
|
||
showError(message);
|
||
}
|
||
setLoading(false);
|
||
};
|
||
|
||
const bindEmail = async () => {
|
||
if (inputs.email_verification_code === '') {
|
||
showError(t('请输入邮箱验证码!'));
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
const res = await API.get(
|
||
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
|
||
);
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('邮箱账户绑定成功!'));
|
||
setShowEmailBindModal(false);
|
||
userState.user.email = inputs.email;
|
||
} else {
|
||
showError(message);
|
||
}
|
||
setLoading(false);
|
||
};
|
||
|
||
const copyText = async (text) => {
|
||
if (await copy(text)) {
|
||
showSuccess(t('已复制:') + text);
|
||
} else {
|
||
// setSearchKeyword(text);
|
||
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
||
}
|
||
};
|
||
|
||
const handleNotificationSettingChange = (type, value) => {
|
||
setNotificationSettings((prev) => ({
|
||
...prev,
|
||
[type]: value.target ? value.target.value !== undefined ? value.target.value : value.target.checked : value, // handle checkbox properly
|
||
}));
|
||
};
|
||
|
||
const saveNotificationSettings = async () => {
|
||
try {
|
||
const res = await API.put('/api/user/setting', {
|
||
notify_type: notificationSettings.warningType,
|
||
quota_warning_threshold: parseFloat(
|
||
notificationSettings.warningThreshold,
|
||
),
|
||
webhook_url: notificationSettings.webhookUrl,
|
||
webhook_secret: notificationSettings.webhookSecret,
|
||
notification_email: notificationSettings.notificationEmail,
|
||
accept_unset_model_ratio_model:
|
||
notificationSettings.acceptUnsetModelRatioModel,
|
||
record_ip_log: notificationSettings.recordIpLog,
|
||
});
|
||
|
||
if (res.data.success) {
|
||
showSuccess(t('设置保存成功'));
|
||
await getUserData();
|
||
} else {
|
||
showError(res.data.message);
|
||
}
|
||
} catch (error) {
|
||
showError(t('设置保存失败'));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="mt-[60px]">
|
||
<div className="flex justify-center">
|
||
<div className="w-full max-w-7xl mx-auto px-4">
|
||
{/* 顶部用户信息区域 */}
|
||
<UserInfoHeader t={t} userState={userState} />
|
||
|
||
{/* 账户管理和其他设置 */}
|
||
<div className="grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6">
|
||
{/* 左侧:账户管理设置 */}
|
||
<AccountManagement
|
||
t={t}
|
||
userState={userState}
|
||
status={status}
|
||
systemToken={systemToken}
|
||
setShowEmailBindModal={setShowEmailBindModal}
|
||
setShowWeChatBindModal={setShowWeChatBindModal}
|
||
generateAccessToken={generateAccessToken}
|
||
handleSystemTokenClick={handleSystemTokenClick}
|
||
setShowChangePasswordModal={setShowChangePasswordModal}
|
||
setShowAccountDeleteModal={setShowAccountDeleteModal}
|
||
/>
|
||
|
||
{/* 右侧:其他设置 */}
|
||
<NotificationSettings
|
||
t={t}
|
||
notificationSettings={notificationSettings}
|
||
handleNotificationSettingChange={handleNotificationSettingChange}
|
||
saveNotificationSettings={saveNotificationSettings}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 模态框组件 */}
|
||
<EmailBindModal
|
||
t={t}
|
||
showEmailBindModal={showEmailBindModal}
|
||
setShowEmailBindModal={setShowEmailBindModal}
|
||
inputs={inputs}
|
||
handleInputChange={handleInputChange}
|
||
sendVerificationCode={sendVerificationCode}
|
||
bindEmail={bindEmail}
|
||
disableButton={disableButton}
|
||
loading={loading}
|
||
countdown={countdown}
|
||
turnstileEnabled={turnstileEnabled}
|
||
turnstileSiteKey={turnstileSiteKey}
|
||
setTurnstileToken={setTurnstileToken}
|
||
/>
|
||
|
||
<WeChatBindModal
|
||
t={t}
|
||
showWeChatBindModal={showWeChatBindModal}
|
||
setShowWeChatBindModal={setShowWeChatBindModal}
|
||
inputs={inputs}
|
||
handleInputChange={handleInputChange}
|
||
bindWeChat={bindWeChat}
|
||
status={status}
|
||
/>
|
||
|
||
<AccountDeleteModal
|
||
t={t}
|
||
showAccountDeleteModal={showAccountDeleteModal}
|
||
setShowAccountDeleteModal={setShowAccountDeleteModal}
|
||
inputs={inputs}
|
||
handleInputChange={handleInputChange}
|
||
deleteAccount={deleteAccount}
|
||
userState={userState}
|
||
turnstileEnabled={turnstileEnabled}
|
||
turnstileSiteKey={turnstileSiteKey}
|
||
setTurnstileToken={setTurnstileToken}
|
||
/>
|
||
|
||
<ChangePasswordModal
|
||
t={t}
|
||
showChangePasswordModal={showChangePasswordModal}
|
||
setShowChangePasswordModal={setShowChangePasswordModal}
|
||
inputs={inputs}
|
||
handleInputChange={handleInputChange}
|
||
changePassword={changePassword}
|
||
turnstileEnabled={turnstileEnabled}
|
||
turnstileSiteKey={turnstileSiteKey}
|
||
setTurnstileToken={setTurnstileToken}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PersonalSetting;
|