| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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; |
| |
|