import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { API, copy, isRoot, isAdmin, showError, showInfo, showSuccess, renderQuota, renderQuotaWithPrompt, stringToColor, onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked, renderModelTag, getModelCategories } from '../../helpers'; import Turnstile from 'react-turnstile'; import { UserContext } from '../../context/User'; import { Avatar, Banner, Button, Card, Empty, Image, Input, Layout, Modal, Skeleton, Space, Tag, Typography, Collapsible, Radio, RadioGroup, AutoComplete, Checkbox, Tabs, TabPane, } from '@douyinfe/semi-ui'; import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations'; import { IconMail, IconLock, IconShield, IconUser, IconSetting, IconBell, IconGithubLogo, IconKey, IconDelete, IconChevronDown, IconChevronUp, } from '@douyinfe/semi-icons'; import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si'; import { Bell, Shield, Webhook, Globe, Settings, UserPlus, ShieldCheck } from 'lucide-react'; import TelegramLoginButton from 'react-telegram-login'; import { useTranslation } from 'react-i18next'; 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 [isModelsExpanded, setIsModelsExpanded] = useState(() => { // Initialize from localStorage if available const savedState = localStorage.getItem('modelsExpanded'); return savedState ? JSON.parse(savedState) : false; }); const [activeModelCategory, setActiveModelCategory] = useState('all'); const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量 const [notificationSettings, setNotificationSettings] = useState({ warningType: 'email', warningThreshold: 100000, webhookUrl: '', webhookSecret: '', notificationEmail: '', acceptUnsetModelRatioModel: false, }); const [modelsLoading, setModelsLoading] = useState(true); const [showWebhookDocs, setShowWebhookDocs] = useState(true); const [isDarkMode, setIsDarkMode] = useState(false); // 检测暗色模式 useEffect(() => { const checkDarkMode = () => { const isDark = document.documentElement.classList.contains('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches; setIsDarkMode(isDark); }; checkDarkMode(); // 监听主题变化 const observer = new MutationObserver(checkDarkMode); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addListener(checkDarkMode); return () => { observer.disconnect(); mediaQuery.removeListener(checkDarkMode); }; }, []); 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, }); } }, [userState?.user?.setting]); // Save models expanded state to localStorage whenever it changes useEffect(() => { localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded)); }, [isModelsExpanded]); 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 getUsername = () => { if (userState.user) { return userState.user.username; } else { return 'null'; } }; const getAvatarText = () => { const username = getUsername(); if (username && username.length > 0) { // 获取前两个字符,支持中文和英文 return username.slice(0, 2).toUpperCase(); } return 'NA'; }; 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 : value, // 处理 Radio 事件对象 })); }; 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, }); if (res.data.success) { showSuccess(t('通知设置已更新')); await getUserData(); } else { showError(res.data.message); } } catch (error) { showError(t('更新通知设置失败')); } }; return (
{/* 主卡片容器 */} {/* 顶部用户信息区域 */} {/* 装饰性背景元素 */}
{getAvatarText()}
{getUsername()}
{isRoot() ? ( {t('超级管理员')} ) : isAdmin() ? ( {t('管理员')} ) : ( {t('普通用户')} )} ID: {userState?.user?.id}
{t('当前余额')}
{renderQuota(userState?.user?.quota)}
{t('历史消耗')}
{renderQuota(userState?.user?.used_quota)}
{t('请求次数')}
{userState.user?.request_count || 0}
{t('用户分组')}
{userState?.user?.group || t('默认')}
{/* 主内容区域 - 使用Tabs组织不同功能模块 */}
{/* 可用模型Tab */} {t('可用模型')}
} itemKey='models' >
{/* 可用模型部分 */}
{t('模型列表')}
{t('点击模型名称可复制')}
{modelsLoading ? ( // 骨架屏加载状态 - 模拟实际加载后的布局
{/* 模拟分类标签 */}
{Array.from({ length: 8 }).map((_, index) => ( ))}
{/* 模拟模型标签列表 */}
{Array.from({ length: 20 }).map((_, index) => ( ))}
) : models.length === 0 ? (
} darkModeImage={} description={t('没有可用模型')} style={{ padding: '24px 0' }} />
) : ( <> {/* 模型分类标签页 */}
setActiveModelCategory(key)} className="mt-2" > {Object.entries(getModelCategories(t)).map(([key, category]) => { // 计算该分类下的模型数量 const modelCount = key === 'all' ? models.length : models.filter(model => category.filter({ model_name: model })).length; if (modelCount === 0 && key !== 'all') return null; return ( {category.icon && {category.icon}} {category.label} {modelCount} } itemKey={key} key={key} /> ); })}
{(() => { // 根据当前选中的分类过滤模型 const categories = getModelCategories(t); const filteredModels = activeModelCategory === 'all' ? models : models.filter(model => categories[activeModelCategory].filter({ model_name: model })); // 如果过滤后没有模型,显示空状态 if (filteredModels.length === 0) { return ( } darkModeImage={} description={t('该分类下没有可用模型')} style={{ padding: '16px 0' }} /> ); } if (filteredModels.length <= MODELS_DISPLAY_COUNT) { return ( {filteredModels.map((model) => ( renderModelTag(model, { size: 'large', shape: 'circle', onClick: () => copyText(model), }) ))} ); } else { return ( <> {filteredModels.map((model) => ( renderModelTag(model, { size: 'large', shape: 'circle', onClick: () => copyText(model), }) ))} setIsModelsExpanded(false)} icon={} > {t('收起')} {!isModelsExpanded && ( {filteredModels .slice(0, MODELS_DISPLAY_COUNT) .map((model) => ( renderModelTag(model, { size: 'large', shape: 'circle', onClick: () => copyText(model), }) ))} setIsModelsExpanded(true)} icon={} > {t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')} )} ); } })()}
)}
{/* 账户绑定Tab */} {t('账户绑定')}
} itemKey='account' >
{/* 邮箱绑定 */}
{t('邮箱')}
{userState.user && userState.user.email !== '' ? userState.user.email : t('未绑定')}
{/* 微信绑定 */}
{t('微信')}
{userState.user && userState.user.wechat_id !== '' ? t('已绑定') : t('未绑定')}
{/* GitHub绑定 */}
{t('GitHub')}
{userState.user && userState.user.github_id !== '' ? userState.user.github_id : t('未绑定')}
{/* OIDC绑定 */}
{t('OIDC')}
{userState.user && userState.user.oidc_id !== '' ? userState.user.oidc_id : t('未绑定')}
{/* Telegram绑定 */}
{t('Telegram')}
{userState.user && userState.user.telegram_id !== '' ? userState.user.telegram_id : t('未绑定')}
{status.telegram_oauth ? ( userState.user.telegram_id !== '' ? ( ) : (
) ) : ( )}
{/* LinuxDO绑定 */}
{t('LinuxDO')}
{userState.user && userState.user.linux_do_id !== '' ? userState.user.linux_do_id : t('未绑定')}
{/* 安全设置Tab */} {t('安全设置')}
} itemKey='security' >
{/* 系统访问令牌 */}
{t('系统访问令牌')} {t('用于API调用的身份验证令牌,请妥善保管')} {systemToken && (
} />
)}
{/* 密码管理 */}
{t('密码管理')} {t('定期更改密码可以提高账户安全性')}
{/* 危险区域 */}
{t('删除账户')} {t('此操作不可逆,所有数据将被永久删除')}
{/* 通知设置Tab */} {t('通知设置')}
} itemKey='notification' >
{/* 通知方式选择 */}
{t('通知方式')} handleNotificationSettingChange('warningType', value) } type="pureCard" >
{t('邮件通知')}
{t('通过邮件接收通知')}
{t('Webhook通知')}
{t('通过HTTP请求接收通知')}
{/* Webhook设置 */} {notificationSettings.warningType === 'webhook' && (
{t('Webhook地址')} handleNotificationSettingChange('webhookUrl', val) } placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')} size="large" className="!rounded-lg" prefix={} />
{t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
{t('接口凭证(可选)')} handleNotificationSettingChange('webhookSecret', val) } placeholder={t('请输入密钥')} size="large" className="!rounded-lg" prefix={} />
{t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
setShowWebhookDocs(!showWebhookDocs)}>
{t('Webhook请求结构')}
{showWebhookDocs ? : }
                                        {`{
  "type": "quota_exceed",      // 通知类型
  "title": "标题",             // 通知标题
  "content": "通知内容",       // 通知内容,支持 {{value}} 变量占位符
  "values": ["值1", "值2"],    // 按顺序替换content中的 {{value}} 占位符
  "timestamp": 1739950503      // 时间戳
}

示例:
{
  "type": "quota_exceed",
  "title": "额度预警通知",
  "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
  "values": ["$0.99"],
  "timestamp": 1739950503
}`}
                                      
)} {/* 邮件设置 */} {notificationSettings.warningType === 'email' && (
{t('通知邮箱')} handleNotificationSettingChange('notificationEmail', val) } placeholder={t('留空则使用账号绑定的邮箱')} size="large" className="!rounded-lg" prefix={} />
{t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
)} {/* 预警阈值 */}
{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)} handleNotificationSettingChange('warningThreshold', val) } size="large" className="!rounded-lg w-full max-w-xs" placeholder={t('请输入预警额度')} data={[ { value: 100000, label: '0.2$' }, { value: 500000, label: '1$' }, { value: 1000000, label: '5$' }, { value: 5000000, label: '10$' }, ]} prefix={} />
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
{t('接受未设置价格模型')}
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
handleNotificationSettingChange( 'acceptUnsetModelRatioModel', e.target.checked, ) } className="ml-4" />
{/* 邮箱绑定模态框 */} {t('绑定邮箱地址')} } visible={showEmailBindModal} onCancel={() => setShowEmailBindModal(false)} onOk={bindEmail} size={'small'} centered={true} maskClosable={false} className="modern-modal" >
handleInputChange('email', value)} name='email' type='email' size="large" className="!rounded-lg flex-1" prefix={} />
handleInputChange('email_verification_code', value) } size="large" className="!rounded-lg" prefix={} /> {turnstileEnabled && (
{ setTurnstileToken(token); }} />
)}
{/* 微信绑定模态框 */} {t('绑定微信账户')} } visible={showWeChatBindModal} onCancel={() => setShowWeChatBindModal(false)} footer={null} size={'small'} centered={true} className="modern-modal" >

{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}

handleInputChange('wechat_verification_code', v) } size="large" className="!rounded-lg" prefix={} />
{/* 账户删除模态框 */} {t('删除账户确认')} } visible={showAccountDeleteModal} onCancel={() => setShowAccountDeleteModal(false)} onOk={deleteAccount} size={'small'} centered={true} className="modern-modal" >
{t('请输入您的用户名以确认删除')} handleInputChange('self_account_deletion_confirmation', value) } size="large" className="!rounded-lg" prefix={} />
{turnstileEnabled && (
{ setTurnstileToken(token); }} />
)}
{/* 修改密码模态框 */} {t('修改密码')} } visible={showChangePasswordModal} onCancel={() => setShowChangePasswordModal(false)} onOk={changePassword} size={'small'} centered={true} className="modern-modal" >
{t('原密码')} handleInputChange('original_password', value) } size="large" className="!rounded-lg" prefix={} />
{t('新密码')} handleInputChange('set_new_password', value) } size="large" className="!rounded-lg" prefix={} />
{t('确认新密码')} handleInputChange('set_new_password_confirmation', value) } size="large" className="!rounded-lg" prefix={} />
{turnstileEnabled && (
{ setTurnstileToken(token); }} />
)}
); }; export default PersonalSetting;