|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useContext, useEffect, useState } from 'react'; |
|
|
import { useNavigate } from 'react-router-dom'; |
|
|
import { |
|
|
API, |
|
|
copy, |
|
|
showError, |
|
|
showInfo, |
|
|
showSuccess, |
|
|
setStatusData, |
|
|
prepareCredentialCreationOptions, |
|
|
buildRegistrationResult, |
|
|
isPasskeySupported, |
|
|
setUserData, |
|
|
} 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 [passkeyStatus, setPasskeyStatus] = useState({ enabled: false }); |
|
|
const [passkeyRegisterLoading, setPasskeyRegisterLoading] = useState(false); |
|
|
const [passkeyDeleteLoading, setPasskeyDeleteLoading] = useState(false); |
|
|
const [passkeySupported, setPasskeySupported] = useState(false); |
|
|
const [notificationSettings, setNotificationSettings] = useState({ |
|
|
warningType: 'email', |
|
|
warningThreshold: 100000, |
|
|
webhookUrl: '', |
|
|
webhookSecret: '', |
|
|
notificationEmail: '', |
|
|
barkUrl: '', |
|
|
gotifyUrl: '', |
|
|
gotifyToken: '', |
|
|
gotifyPriority: 5, |
|
|
acceptUnsetModelRatioModel: false, |
|
|
recordIpLog: false, |
|
|
}); |
|
|
|
|
|
useEffect(() => { |
|
|
let saved = localStorage.getItem('status'); |
|
|
if (saved) { |
|
|
const parsed = JSON.parse(saved); |
|
|
setStatus(parsed); |
|
|
if (parsed.turnstile_check) { |
|
|
setTurnstileEnabled(true); |
|
|
setTurnstileSiteKey(parsed.turnstile_site_key); |
|
|
} else { |
|
|
setTurnstileEnabled(false); |
|
|
setTurnstileSiteKey(''); |
|
|
} |
|
|
} |
|
|
|
|
|
(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); |
|
|
} else { |
|
|
setTurnstileEnabled(false); |
|
|
setTurnstileSiteKey(''); |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
|
|
|
} |
|
|
})(); |
|
|
|
|
|
getUserData(); |
|
|
|
|
|
isPasskeySupported() |
|
|
.then(setPasskeySupported) |
|
|
.catch(() => setPasskeySupported(false)); |
|
|
}, []); |
|
|
|
|
|
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); |
|
|
}, [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 || '', |
|
|
barkUrl: settings.bark_url || '', |
|
|
gotifyUrl: settings.gotify_url || '', |
|
|
gotifyToken: settings.gotify_token || '', |
|
|
gotifyPriority: |
|
|
settings.gotify_priority !== undefined ? settings.gotify_priority : 5, |
|
|
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 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); |
|
|
} |
|
|
}; |
|
|
|
|
|
const getUserData = async () => { |
|
|
let res = await API.get(`/api/user/self`); |
|
|
const { success, message, data } = res.data; |
|
|
if (success) { |
|
|
userDispatch({ type: 'login', payload: data }); |
|
|
setUserData(data); |
|
|
await loadPasskeyStatus(); |
|
|
} else { |
|
|
showError(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
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 { |
|
|
|
|
|
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, |
|
|
})); |
|
|
}; |
|
|
|
|
|
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, |
|
|
bark_url: notificationSettings.barkUrl, |
|
|
gotify_url: notificationSettings.gotifyUrl, |
|
|
gotify_token: notificationSettings.gotifyToken, |
|
|
gotify_priority: (() => { |
|
|
const parsed = parseInt(notificationSettings.gotifyPriority); |
|
|
return isNaN(parsed) ? 5 : parsed; |
|
|
})(), |
|
|
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-2'> |
|
|
{/* 顶部用户信息区域 */} |
|
|
<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} |
|
|
passkeyStatus={passkeyStatus} |
|
|
passkeySupported={passkeySupported} |
|
|
passkeyRegisterLoading={passkeyRegisterLoading} |
|
|
passkeyDeleteLoading={passkeyDeleteLoading} |
|
|
onPasskeyRegister={handleRegisterPasskey} |
|
|
onPasskeyDelete={handleRemovePasskey} |
|
|
/> |
|
|
|
|
|
{/* 右侧:其他设置 */} |
|
|
<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; |
|
|
|