2025-07-19 03:30:44 +08:00
|
|
|
|
/*
|
|
|
|
|
|
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
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
import React, { useContext, useEffect, useState } from 'react';
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-09-28 17:31:38 +08:00
|
|
|
|
import {
|
|
|
|
|
|
API,
|
|
|
|
|
|
copy,
|
|
|
|
|
|
showError,
|
|
|
|
|
|
showInfo,
|
|
|
|
|
|
showSuccess,
|
|
|
|
|
|
setStatusData,
|
2025-09-29 17:45:09 +08:00
|
|
|
|
prepareCredentialCreationOptions,
|
|
|
|
|
|
buildRegistrationResult,
|
|
|
|
|
|
isPasskeySupported,
|
2025-09-29 19:23:42 +08:00
|
|
|
|
setUserData,
|
2025-09-28 17:31:38 +08:00
|
|
|
|
} from '../../helpers';
|
2025-06-04 00:42:06 +08:00
|
|
|
|
import { UserContext } from '../../context/User';
|
2025-08-17 00:49:54 +08:00
|
|
|
|
import { Modal } from '@douyinfe/semi-ui';
|
2024-12-13 19:03:14 +08:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2023-04-22 20:39:27 +08:00
|
|
|
|
|
2025-08-17 00:49:54 +08:00
|
|
|
|
// 导入子组件
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
2023-04-22 20:39:27 +08:00
|
|
|
|
const PersonalSetting = () => {
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const [userState, userDispatch] = useContext(UserContext);
|
|
|
|
|
|
let navigate = useNavigate();
|
|
|
|
|
|
const { t } = useTranslation();
|
2023-07-23 13:37:32 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const [inputs, setInputs] = useState({
|
|
|
|
|
|
wechat_verification_code: '',
|
|
|
|
|
|
email_verification_code: '',
|
|
|
|
|
|
email: '',
|
|
|
|
|
|
self_account_deletion_confirmation: '',
|
2025-05-06 14:18:15 +08:00
|
|
|
|
original_password: '',
|
2025-04-03 17:32:48 +08:00
|
|
|
|
set_new_password: '',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
set_new_password_confirmation: '',
|
2025-04-03 17:32:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
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('');
|
2025-09-29 17:45:09 +08:00
|
|
|
|
const [passkeyStatus, setPasskeyStatus] = useState({ enabled: false });
|
|
|
|
|
|
const [passkeyRegisterLoading, setPasskeyRegisterLoading] = useState(false);
|
|
|
|
|
|
const [passkeyDeleteLoading, setPasskeyDeleteLoading] = useState(false);
|
|
|
|
|
|
const [passkeySupported, setPasskeySupported] = useState(false);
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const [notificationSettings, setNotificationSettings] = useState({
|
|
|
|
|
|
warningType: 'email',
|
|
|
|
|
|
warningThreshold: 100000,
|
|
|
|
|
|
webhookUrl: '',
|
|
|
|
|
|
webhookSecret: '',
|
|
|
|
|
|
notificationEmail: '',
|
2025-09-01 15:57:23 +08:00
|
|
|
|
barkUrl: '',
|
2025-10-01 19:15:00 +08:00
|
|
|
|
gotifyUrl: '',
|
|
|
|
|
|
gotifyToken: '',
|
|
|
|
|
|
gotifyPriority: 5,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
acceptUnsetModelRatioModel: false,
|
2025-06-13 01:34:01 +08:00
|
|
|
|
recordIpLog: false,
|
2025-04-03 17:32:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-09-28 17:31:38 +08:00
|
|
|
|
let saved = localStorage.getItem('status');
|
|
|
|
|
|
if (saved) {
|
|
|
|
|
|
const parsed = JSON.parse(saved);
|
|
|
|
|
|
setStatus(parsed);
|
|
|
|
|
|
if (parsed.turnstile_check) {
|
2025-04-03 17:32:48 +08:00
|
|
|
|
setTurnstileEnabled(true);
|
2025-09-28 17:31:38 +08:00
|
|
|
|
setTurnstileSiteKey(parsed.turnstile_site_key);
|
2025-09-28 17:38:56 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
setTurnstileEnabled(false);
|
|
|
|
|
|
setTurnstileSiteKey('');
|
2025-04-03 17:32:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-28 17:31:38 +08:00
|
|
|
|
// Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.get('/api/status');
|
|
|
|
|
|
const { success, data } = res.data;
|
|
|
|
|
|
if (success && data) {
|
|
|
|
|
|
setStatus(data);
|
|
|
|
|
|
setStatusData(data);
|
|
|
|
|
|
if (data.turnstile_check) {
|
|
|
|
|
|
setTurnstileEnabled(true);
|
|
|
|
|
|
setTurnstileSiteKey(data.turnstile_site_key);
|
2025-09-28 17:38:56 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
setTurnstileEnabled(false);
|
|
|
|
|
|
setTurnstileSiteKey('');
|
2025-09-28 17:31:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// ignore and keep local status
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
getUserData();
|
2025-09-29 17:45:09 +08:00
|
|
|
|
|
|
|
|
|
|
isPasskeySupported()
|
|
|
|
|
|
.then(setPasskeySupported)
|
|
|
|
|
|
.catch(() => setPasskeySupported(false));
|
2025-04-03 17:32:48 +08:00
|
|
|
|
}, []);
|
2023-04-22 20:39:27 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
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]);
|
2023-11-21 16:35:51 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
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 || '',
|
2025-09-01 15:57:23 +08:00
|
|
|
|
barkUrl: settings.bark_url || '',
|
2025-10-01 19:15:00 +08:00
|
|
|
|
gotifyUrl: settings.gotify_url || '',
|
|
|
|
|
|
gotifyToken: settings.gotify_token || '',
|
|
|
|
|
|
gotifyPriority:
|
|
|
|
|
|
settings.gotify_priority !== undefined
|
|
|
|
|
|
? settings.gotify_priority
|
|
|
|
|
|
: 5,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
acceptUnsetModelRatioModel:
|
|
|
|
|
|
settings.accept_unset_model_ratio_model || false,
|
2025-06-13 01:34:01 +08:00
|
|
|
|
recordIpLog: settings.record_ip_log || false,
|
2025-04-03 17:32:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [userState?.user?.setting]);
|
2023-11-21 16:35:51 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const handleInputChange = (name, value) => {
|
|
|
|
|
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
|
|
|
|
};
|
2025-03-02 01:35:50 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2023-07-23 13:25:28 +08:00
|
|
|
|
|
2025-09-29 17:45:09 +08:00
|
|
|
|
const loadPasskeyStatus = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.get('/api/user/passkey');
|
|
|
|
|
|
const { success, data, message } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
setPasskeyStatus({
|
|
|
|
|
|
enabled: data?.enabled || false,
|
|
|
|
|
|
last_used_at: data?.last_used_at || null,
|
|
|
|
|
|
backup_eligible: data?.backup_eligible || false,
|
|
|
|
|
|
backup_state: data?.backup_state || false,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 忽略错误,保留默认状态
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRegisterPasskey = async () => {
|
|
|
|
|
|
if (!passkeySupported || !window.PublicKeyCredential) {
|
|
|
|
|
|
showInfo(t('当前设备不支持 Passkey'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setPasskeyRegisterLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const beginRes = await API.post('/api/user/passkey/register/begin');
|
|
|
|
|
|
const { success, message, data } = beginRes.data;
|
|
|
|
|
|
if (!success) {
|
|
|
|
|
|
showError(message || t('无法发起 Passkey 注册'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const publicKey = prepareCredentialCreationOptions(data?.options || data?.publicKey || data);
|
|
|
|
|
|
const credential = await navigator.credentials.create({ publicKey });
|
|
|
|
|
|
const payload = buildRegistrationResult(credential);
|
|
|
|
|
|
if (!payload) {
|
|
|
|
|
|
showError(t('Passkey 注册失败,请重试'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const finishRes = await API.post('/api/user/passkey/register/finish', payload);
|
|
|
|
|
|
if (finishRes.data.success) {
|
|
|
|
|
|
showSuccess(t('Passkey 注册成功'));
|
|
|
|
|
|
await loadPasskeyStatus();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(finishRes.data.message || t('Passkey 注册失败,请重试'));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error?.name === 'AbortError') {
|
|
|
|
|
|
showInfo(t('已取消 Passkey 注册'));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(t('Passkey 注册失败,请重试'));
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setPasskeyRegisterLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRemovePasskey = async () => {
|
|
|
|
|
|
setPasskeyDeleteLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.delete('/api/user/passkey');
|
|
|
|
|
|
const { success, message } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(t('Passkey 已解绑'));
|
|
|
|
|
|
await loadPasskeyStatus();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message || t('操作失败,请重试'));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
showError(t('操作失败,请重试'));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setPasskeyDeleteLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const getUserData = async () => {
|
|
|
|
|
|
let res = await API.get(`/api/user/self`);
|
|
|
|
|
|
const { success, message, data } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
userDispatch({ type: 'login', payload: data });
|
2025-09-29 19:23:42 +08:00
|
|
|
|
setUserData(data);
|
2025-09-29 17:45:09 +08:00
|
|
|
|
await loadPasskeyStatus();
|
2025-04-03 17:32:48 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2023-04-26 20:54:39 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const handleSystemTokenClick = async (e) => {
|
|
|
|
|
|
e.target.select();
|
|
|
|
|
|
await copy(e.target.value);
|
|
|
|
|
|
showSuccess(t('系统令牌已复制到剪切板'));
|
|
|
|
|
|
};
|
2023-08-06 22:02:58 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const deleteAccount = async () => {
|
|
|
|
|
|
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
|
|
|
|
|
showError(t('请输入你的账户名以确认删除!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2023-07-23 13:37:32 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const res = await API.delete('/api/user/self');
|
|
|
|
|
|
const { success, message } = res.data;
|
2023-07-23 13:37:32 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(t('账户已删除!'));
|
|
|
|
|
|
await API.get('/api/user/logout');
|
|
|
|
|
|
userDispatch({ type: 'logout' });
|
|
|
|
|
|
localStorage.removeItem('user');
|
|
|
|
|
|
navigate('/login');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2023-07-23 13:37:32 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const bindWeChat = async () => {
|
|
|
|
|
|
if (inputs.wechat_verification_code === '') return;
|
|
|
|
|
|
const res = await API.get(
|
2025-04-04 12:00:38 +08:00
|
|
|
|
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
|
2025-04-03 17:32:48 +08:00
|
|
|
|
);
|
|
|
|
|
|
const { success, message } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(t('微信账户绑定成功!'));
|
|
|
|
|
|
setShowWeChatBindModal(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2023-04-22 20:39:27 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const changePassword = async () => {
|
2025-05-06 14:18:15 +08:00
|
|
|
|
if (inputs.original_password === '') {
|
|
|
|
|
|
showError(t('请输入原密码!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-05-06 22:28:32 +08:00
|
|
|
|
if (inputs.set_new_password === '') {
|
|
|
|
|
|
showError(t('请输入新密码!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-05-06 14:18:15 +08:00
|
|
|
|
if (inputs.original_password === inputs.set_new_password) {
|
|
|
|
|
|
showError(t('新密码需要和原密码不一致!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-04-03 17:32:48 +08:00
|
|
|
|
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
|
|
|
|
|
|
showError(t('两次输入的密码不一致!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await API.put(`/api/user/self`, {
|
2025-05-06 14:18:15 +08:00
|
|
|
|
original_password: inputs.original_password,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
password: inputs.set_new_password,
|
2025-04-03 17:32:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
const { success, message } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(t('密码修改成功!'));
|
|
|
|
|
|
setShowWeChatBindModal(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
setShowChangePasswordModal(false);
|
|
|
|
|
|
};
|
2023-12-20 20:53:15 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const sendVerificationCode = async () => {
|
|
|
|
|
|
if (inputs.email === '') {
|
|
|
|
|
|
showError(t('请输入邮箱!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setDisableButton(true);
|
|
|
|
|
|
if (turnstileEnabled && turnstileToken === '') {
|
2025-05-23 23:53:10 +08:00
|
|
|
|
showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
|
2025-04-03 17:32:48 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const res = await API.get(
|
2025-04-04 12:00:38 +08:00
|
|
|
|
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
|
2025-04-03 17:32:48 +08:00
|
|
|
|
);
|
|
|
|
|
|
const { success, message } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(t('验证码发送成功,请检查邮箱!'));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
};
|
2023-11-21 16:35:51 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const bindEmail = async () => {
|
|
|
|
|
|
if (inputs.email_verification_code === '') {
|
|
|
|
|
|
showError(t('请输入邮箱验证码!'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const res = await API.get(
|
2025-04-04 12:00:38 +08:00
|
|
|
|
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
|
2025-04-03 17:32:48 +08:00
|
|
|
|
);
|
|
|
|
|
|
const { success, message } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
showSuccess(t('邮箱账户绑定成功!'));
|
|
|
|
|
|
setShowEmailBindModal(false);
|
|
|
|
|
|
userState.user.email = inputs.email;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
};
|
2023-11-21 16:35:51 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const copyText = async (text) => {
|
|
|
|
|
|
if (await copy(text)) {
|
|
|
|
|
|
showSuccess(t('已复制:') + text);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// setSearchKeyword(text);
|
|
|
|
|
|
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2024-03-15 16:05:33 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const handleNotificationSettingChange = (type, value) => {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
setNotificationSettings((prev) => ({
|
2025-04-03 17:32:48 +08:00
|
|
|
|
...prev,
|
2025-08-30 21:15:10 +08:00
|
|
|
|
[type]: value.target
|
|
|
|
|
|
? value.target.value !== undefined
|
|
|
|
|
|
? value.target.value
|
|
|
|
|
|
: value.target.checked
|
|
|
|
|
|
: value, // handle checkbox properly
|
2025-04-03 17:32:48 +08:00
|
|
|
|
}));
|
|
|
|
|
|
};
|
2023-11-27 20:12:10 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
const saveNotificationSettings = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.put('/api/user/setting', {
|
|
|
|
|
|
notify_type: notificationSettings.warningType,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
quota_warning_threshold: parseFloat(
|
|
|
|
|
|
notificationSettings.warningThreshold,
|
|
|
|
|
|
),
|
2025-04-03 17:32:48 +08:00
|
|
|
|
webhook_url: notificationSettings.webhookUrl,
|
|
|
|
|
|
webhook_secret: notificationSettings.webhookSecret,
|
|
|
|
|
|
notification_email: notificationSettings.notificationEmail,
|
2025-09-01 15:57:23 +08:00
|
|
|
|
bark_url: notificationSettings.barkUrl,
|
2025-10-01 19:15:00 +08:00
|
|
|
|
gotify_url: notificationSettings.gotifyUrl,
|
|
|
|
|
|
gotify_token: notificationSettings.gotifyToken,
|
|
|
|
|
|
gotify_priority: (() => {
|
|
|
|
|
|
const parsed = parseInt(notificationSettings.gotifyPriority);
|
|
|
|
|
|
return isNaN(parsed) ? 5 : parsed;
|
|
|
|
|
|
})(),
|
2025-04-04 12:00:38 +08:00
|
|
|
|
accept_unset_model_ratio_model:
|
|
|
|
|
|
notificationSettings.acceptUnsetModelRatioModel,
|
2025-06-13 01:34:01 +08:00
|
|
|
|
record_ip_log: notificationSettings.recordIpLog,
|
2025-04-03 17:32:48 +08:00
|
|
|
|
});
|
2025-02-18 14:54:21 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
if (res.data.success) {
|
2025-06-13 01:43:43 +08:00
|
|
|
|
showSuccess(t('设置保存成功'));
|
2025-04-03 17:32:48 +08:00
|
|
|
|
await getUserData();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(res.data.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-06-13 01:43:43 +08:00
|
|
|
|
showError(t('设置保存失败'));
|
2025-04-03 17:32:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-02-18 14:54:21 +08:00
|
|
|
|
|
2025-04-03 17:32:48 +08:00
|
|
|
|
return (
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='mt-[60px]'>
|
|
|
|
|
|
<div className='flex justify-center'>
|
|
|
|
|
|
<div className='w-full max-w-7xl mx-auto px-2'>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{/* 顶部用户信息区域 */}
|
|
|
|
|
|
<UserInfoHeader t={t} userState={userState} />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 账户管理和其他设置 */}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{/* 左侧:账户管理设置 */}
|
|
|
|
|
|
<AccountManagement
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
userState={userState}
|
|
|
|
|
|
status={status}
|
|
|
|
|
|
systemToken={systemToken}
|
|
|
|
|
|
setShowEmailBindModal={setShowEmailBindModal}
|
|
|
|
|
|
setShowWeChatBindModal={setShowWeChatBindModal}
|
|
|
|
|
|
generateAccessToken={generateAccessToken}
|
|
|
|
|
|
handleSystemTokenClick={handleSystemTokenClick}
|
|
|
|
|
|
setShowChangePasswordModal={setShowChangePasswordModal}
|
|
|
|
|
|
setShowAccountDeleteModal={setShowAccountDeleteModal}
|
2025-09-29 17:45:09 +08:00
|
|
|
|
passkeyStatus={passkeyStatus}
|
|
|
|
|
|
passkeySupported={passkeySupported}
|
|
|
|
|
|
passkeyRegisterLoading={passkeyRegisterLoading}
|
|
|
|
|
|
passkeyDeleteLoading={passkeyDeleteLoading}
|
|
|
|
|
|
onPasskeyRegister={handleRegisterPasskey}
|
|
|
|
|
|
onPasskeyDelete={handleRemovePasskey}
|
2025-05-23 23:53:10 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{/* 右侧:其他设置 */}
|
|
|
|
|
|
<NotificationSettings
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
notificationSettings={notificationSettings}
|
|
|
|
|
|
handleNotificationSettingChange={handleNotificationSettingChange}
|
|
|
|
|
|
saveNotificationSettings={saveNotificationSettings}
|
2025-05-23 23:53:10 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-17 00:49:54 +08:00
|
|
|
|
</div>
|
2025-05-23 23:53:10 +08:00
|
|
|
|
|
2025-08-17 00:49:54 +08:00
|
|
|
|
{/* 模态框组件 */}
|
|
|
|
|
|
<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}
|
|
|
|
|
|
/>
|
2025-04-03 17:32:48 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2023-04-22 20:39:27 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default PersonalSetting;
|