| | 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(() => { |
| | |
| | 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); |
| |
|
| | 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); |
| | }, [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]); |
| |
|
| | |
| | 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 { |
| | |
| | Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); |
| | } |
| | }; |
| |
|
| | const handleNotificationSettingChange = (type, value) => { |
| | setNotificationSettings((prev) => ({ |
| | ...prev, |
| | [type]: value.target ? value.target.value : 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, |
| | 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 ( |
| | <div className="bg-gray-50"> |
| | <Layout> |
| | <Layout.Content> |
| | |
| | <div className="flex justify-center"> |
| | <div className="w-full"> |
| | {/* 主卡片容器 */} |
| | <Card className="!rounded-2xl shadow-lg border-0"> |
| | {/* 顶部用户信息区域 */} |
| | <Card |
| | className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden" |
| | style={{ |
| | background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 25%, #a855f7 50%, #c084fc 75%, #d8b4fe 100%)', |
| | position: 'relative' |
| | }} |
| | bodyStyle={{ padding: 0 }} |
| | > |
| | {/* 装饰性背景元素 */} |
| | <div className="absolute inset-0 overflow-hidden"> |
| | <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div> |
| | <div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-3 rounded-full"></div> |
| | <div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div> |
| | </div> |
| | |
| | <div className="relative p-4 sm:p-6 md:p-8" style={{ color: 'white' }}> |
| | <div className="flex justify-between items-start mb-4 sm:mb-6"> |
| | <div className="flex items-center flex-1 min-w-0"> |
| | <Avatar |
| | size='large' |
| | color={stringToColor(getUsername())} |
| | border={{ motion: true }} |
| | contentMotion={true} |
| | className="mr-3 sm:mr-4 shadow-lg flex-shrink-0" |
| | > |
| | {getAvatarText()} |
| | </Avatar> |
| | <div className="flex-1 min-w-0"> |
| | <div className="text-base sm:text-lg font-semibold truncate" style={{ color: 'white' }}> |
| | {getUsername()} |
| | </div> |
| | <div className="mt-1 flex flex-wrap gap-1 sm:gap-2"> |
| | {isRoot() ? ( |
| | <Tag |
| | color='red' |
| | size='small' |
| | style={{ |
| | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | color: '#dc2626', |
| | fontWeight: '600' |
| | }} |
| | className="!rounded-full" |
| | > |
| | {t('超级管理员')} |
| | </Tag> |
| | ) : isAdmin() ? ( |
| | <Tag |
| | color='orange' |
| | size='small' |
| | style={{ |
| | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | color: '#ea580c', |
| | fontWeight: '600' |
| | }} |
| | className="!rounded-full" |
| | > |
| | {t('管理员')} |
| | </Tag> |
| | ) : ( |
| | <Tag |
| | color='blue' |
| | size='small' |
| | style={{ |
| | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | color: '#2563eb', |
| | fontWeight: '600' |
| | }} |
| | className="!rounded-full" |
| | > |
| | {t('普通用户')} |
| | </Tag> |
| | )} |
| | <Tag |
| | color='green' |
| | size='small' |
| | className="!rounded-full" |
| | style={{ |
| | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | color: '#16a34a', |
| | fontWeight: '600' |
| | }} |
| | > |
| | ID: {userState?.user?.id} |
| | </Tag> |
| | </div> |
| | </div> |
| | </div> |
| | <div |
| | className="w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-lg flex-shrink-0 ml-2" |
| | style={{ |
| | background: `linear-gradient(135deg, ${stringToColor(getUsername())} 0%, #f59e0b 100%)` |
| | }} |
| | > |
| | <IconUser size="default" style={{ color: 'white' }} /> |
| | </div> |
| | </div> |
| | |
| | <div className="mb-4 sm:mb-6"> |
| | <div className="text-xs sm:text-sm mb-1 sm:mb-2" style={{ color: 'rgba(255, 255, 255, 0.7)' }}> |
| | {t('当前余额')} |
| | </div> |
| | <div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide" style={{ color: 'white' }}> |
| | {renderQuota(userState?.user?.quota)} |
| | </div> |
| | </div> |
| | |
| | <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end"> |
| | <div className="grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0"> |
| | <div className="text-center sm:text-left"> |
| | <div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}> |
| | {t('历史消耗')} |
| | </div> |
| | <div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}> |
| | {renderQuota(userState?.user?.used_quota)} |
| | </div> |
| | </div> |
| | <div className="text-center sm:text-left"> |
| | <div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}> |
| | {t('请求次数')} |
| | </div> |
| | <div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}> |
| | {userState.user?.request_count || 0} |
| | </div> |
| | </div> |
| | <div className="text-center sm:text-left"> |
| | <div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}> |
| | {t('用户分组')} |
| | </div> |
| | <div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}> |
| | {userState?.user?.group || t('默认')} |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-yellow-400 via-orange-400 to-red-400" style={{ opacity: 0.6 }}></div> |
| | </div> |
| | </Card> |
| | |
| | {/* 主内容区域 - 使用Tabs组织不同功能模块 */} |
| | <div className="p-4"> |
| | <Tabs type='line' defaultActiveKey='models' className="modern-tabs"> |
| | {/* 可用模型Tab */} |
| | <TabPane |
| | tab={ |
| | <div className="flex items-center"> |
| | <Settings size={16} className="mr-2" /> |
| | {t('可用模型')} |
| | </div> |
| | } |
| | itemKey='models' |
| | > |
| | <div className="gap-6 py-4"> |
| | {/* 可用模型部分 */} |
| | <div className="bg-gray-50 rounded-xl"> |
| | <div className="flex items-center mb-4"> |
| | <div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-3"> |
| | <Settings size={20} className="text-purple-500" /> |
| | </div> |
| | <div> |
| | <Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title> |
| | <div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div> |
| | </div> |
| | </div> |
| | |
| | {modelsLoading ? ( |
| | // 骨架屏加载状态 - 模拟实际加载后的布局 |
| | <div className="space-y-4"> |
| | {/* 模拟分类标签 */} |
| | <div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}> |
| | <div className="flex overflow-x-auto py-2 gap-2"> |
| | {Array.from({ length: 8 }).map((_, index) => ( |
| | <Skeleton.Button key={`cat-${index}`} style={{ |
| | width: index === 0 ? 130 : 100 + Math.random() * 50, |
| | height: 36, |
| | borderRadius: 8 |
| | }} /> |
| | ))} |
| | </div> |
| | </div> |
| | |
| | {/* 模拟模型标签列表 */} |
| | <div className="flex flex-wrap gap-2"> |
| | {Array.from({ length: 20 }).map((_, index) => ( |
| | <Skeleton.Button |
| | key={`model-${index}`} |
| | style={{ |
| | width: 100 + Math.random() * 100, |
| | height: 32, |
| | borderRadius: 16, |
| | margin: '4px' |
| | }} |
| | /> |
| | ))} |
| | </div> |
| | </div> |
| | ) : models.length === 0 ? ( |
| | <div className="py-8"> |
| | <Empty |
| | image={<IllustrationNoContent style={{ width: 150, height: 150 }} />} |
| | darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />} |
| | description={t('没有可用模型')} |
| | style={{ padding: '24px 0' }} |
| | /> |
| | </div> |
| | ) : ( |
| | <> |
| | {/* 模型分类标签页 */} |
| | <div className="mb-4"> |
| | <Tabs |
| | type="card" |
| | activeKey={activeModelCategory} |
| | onChange={key => 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 ( |
| | <TabPane |
| | tab={ |
| | <span className="flex items-center gap-2"> |
| | {category.icon && <span className="w-4 h-4">{category.icon}</span>} |
| | {category.label} |
| | <Tag |
| | color={activeModelCategory === key ? 'red' : 'grey'} |
| | size='small' |
| | shape='circle' |
| | > |
| | {modelCount} |
| | </Tag> |
| | </span> |
| | } |
| | itemKey={key} |
| | key={key} |
| | /> |
| | ); |
| | })} |
| | </Tabs> |
| | </div> |
| | |
| | <div className="bg-white rounded-lg p-3"> |
| | {(() => { |
| | // 根据当前选中的分类过滤模型 |
| | const categories = getModelCategories(t); |
| | const filteredModels = activeModelCategory === 'all' |
| | ? models |
| | : models.filter(model => categories[activeModelCategory].filter({ model_name: model })); |
| | |
| | // 如果过滤后没有模型,显示空状态 |
| | if (filteredModels.length === 0) { |
| | return ( |
| | <Empty |
| | image={<IllustrationNoContent style={{ width: 120, height: 120 }} />} |
| | darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />} |
| | description={t('该分类下没有可用模型')} |
| | style={{ padding: '16px 0' }} |
| | /> |
| | ); |
| | } |
| | |
| | if (filteredModels.length <= MODELS_DISPLAY_COUNT) { |
| | return ( |
| | <Space wrap> |
| | {filteredModels.map((model) => ( |
| | renderModelTag(model, { |
| | size: 'large', |
| | shape: 'circle', |
| | onClick: () => copyText(model), |
| | }) |
| | ))} |
| | </Space> |
| | ); |
| | } else { |
| | return ( |
| | <> |
| | <Collapsible isOpen={isModelsExpanded}> |
| | <Space wrap> |
| | {filteredModels.map((model) => ( |
| | renderModelTag(model, { |
| | size: 'large', |
| | shape: 'circle', |
| | onClick: () => copyText(model), |
| | }) |
| | ))} |
| | <Tag |
| | color='grey' |
| | type='light' |
| | className="cursor-pointer !rounded-lg" |
| | onClick={() => setIsModelsExpanded(false)} |
| | icon={<IconChevronUp />} |
| | > |
| | {t('收起')} |
| | </Tag> |
| | </Space> |
| | </Collapsible> |
| | {!isModelsExpanded && ( |
| | <Space wrap> |
| | {filteredModels |
| | .slice(0, MODELS_DISPLAY_COUNT) |
| | .map((model) => ( |
| | renderModelTag(model, { |
| | size: 'large', |
| | shape: 'circle', |
| | onClick: () => copyText(model), |
| | }) |
| | ))} |
| | <Tag |
| | color='grey' |
| | type='light' |
| | className="cursor-pointer !rounded-lg" |
| | onClick={() => setIsModelsExpanded(true)} |
| | icon={<IconChevronDown />} |
| | > |
| | {t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')} |
| | </Tag> |
| | </Space> |
| | )} |
| | </> |
| | ); |
| | } |
| | })()} |
| | </div> |
| | </> |
| | )} |
| | </div> |
| | </div> |
| | </TabPane> |
| | |
| | {/* 账户绑定Tab */} |
| | <TabPane |
| | tab={ |
| | <div className="flex items-center"> |
| | <UserPlus size={16} className="mr-2" /> |
| | {t('账户绑定')} |
| | </div> |
| | } |
| | itemKey='account' |
| | > |
| | <div className="py-4"> |
| | <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> |
| | {/* 邮箱绑定 */} |
| | <Card |
| | className="!rounded-xl transition-shadow" |
| | bodyStyle={{ padding: '16px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex items-center justify-between"> |
| | <div className="flex items-center flex-1"> |
| | <div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mr-3"> |
| | <IconMail size="default" className="text-red-500" /> |
| | </div> |
| | <div className="flex-1 min-w-0"> |
| | <div className="font-medium text-gray-900">{t('邮箱')}</div> |
| | <div className="text-sm text-gray-500 truncate"> |
| | {userState.user && userState.user.email !== '' |
| | ? userState.user.email |
| | : t('未绑定')} |
| | </div> |
| | </div> |
| | </div> |
| | <Button |
| | type="primary" |
| | theme="outline" |
| | size="small" |
| | onClick={() => setShowEmailBindModal(true)} |
| | className="!rounded-lg" |
| | > |
| | {userState.user && userState.user.email !== '' |
| | ? t('修改绑定') |
| | : t('绑定')} |
| | </Button> |
| | </div> |
| | </Card> |
| | |
| | {/* 微信绑定 */} |
| | <Card |
| | className="!rounded-xl transition-shadow" |
| | bodyStyle={{ padding: '16px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex items-center justify-between"> |
| | <div className="flex items-center flex-1"> |
| | <div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-3"> |
| | <SiWechat size={20} className="text-green-500" /> |
| | </div> |
| | <div className="flex-1 min-w-0"> |
| | <div className="font-medium text-gray-900">{t('微信')}</div> |
| | <div className="text-sm text-gray-500 truncate"> |
| | {userState.user && userState.user.wechat_id !== '' |
| | ? t('已绑定') |
| | : t('未绑定')} |
| | </div> |
| | </div> |
| | </div> |
| | <Button |
| | type="primary" |
| | theme="outline" |
| | size="small" |
| | disabled={!status.wechat_login} |
| | onClick={() => setShowWeChatBindModal(true)} |
| | className="!rounded-lg" |
| | > |
| | {userState.user && userState.user.wechat_id !== '' |
| | ? t('修改绑定') |
| | : status.wechat_login |
| | ? t('绑定') |
| | : t('未启用')} |
| | </Button> |
| | </div> |
| | </Card> |
| | |
| | {/* GitHub绑定 */} |
| | <Card |
| | className="!rounded-xl transition-shadow" |
| | bodyStyle={{ padding: '16px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex items-center justify-between"> |
| | <div className="flex items-center flex-1"> |
| | <div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mr-3"> |
| | <IconGithubLogo size="default" className="text-gray-700" /> |
| | </div> |
| | <div className="flex-1 min-w-0"> |
| | <div className="font-medium text-gray-900">{t('GitHub')}</div> |
| | <div className="text-sm text-gray-500 truncate"> |
| | {userState.user && userState.user.github_id !== '' |
| | ? userState.user.github_id |
| | : t('未绑定')} |
| | </div> |
| | </div> |
| | </div> |
| | <Button |
| | type="primary" |
| | theme="outline" |
| | size="small" |
| | onClick={() => onGitHubOAuthClicked(status.github_client_id)} |
| | disabled={ |
| | (userState.user && userState.user.github_id !== '') || |
| | !status.github_oauth |
| | } |
| | className="!rounded-lg" |
| | > |
| | {status.github_oauth ? t('绑定') : t('未启用')} |
| | </Button> |
| | </div> |
| | </Card> |
| | |
| | {/* OIDC绑定 */} |
| | <Card |
| | className="!rounded-xl transition-shadow" |
| | bodyStyle={{ padding: '16px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex items-center justify-between"> |
| | <div className="flex items-center flex-1"> |
| | <div className="w-10 h-10 rounded-full bg-indigo-50 flex items-center justify-center mr-3"> |
| | <IconShield size="default" className="text-indigo-500" /> |
| | </div> |
| | <div className="flex-1 min-w-0"> |
| | <div className="font-medium text-gray-900">{t('OIDC')}</div> |
| | <div className="text-sm text-gray-500 truncate"> |
| | {userState.user && userState.user.oidc_id !== '' |
| | ? userState.user.oidc_id |
| | : t('未绑定')} |
| | </div> |
| | </div> |
| | </div> |
| | <Button |
| | type="primary" |
| | theme="outline" |
| | size="small" |
| | onClick={() => onOIDCClicked( |
| | status.oidc_authorization_endpoint, |
| | status.oidc_client_id, |
| | )} |
| | disabled={ |
| | (userState.user && userState.user.oidc_id !== '') || |
| | !status.oidc_enabled |
| | } |
| | className="!rounded-lg" |
| | > |
| | {status.oidc_enabled ? t('绑定') : t('未启用')} |
| | </Button> |
| | </div> |
| | </Card> |
| | |
| | {/* Telegram绑定 */} |
| | <Card |
| | className="!rounded-xl transition-shadow" |
| | bodyStyle={{ padding: '16px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex items-center justify-between"> |
| | <div className="flex items-center flex-1"> |
| | <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-3"> |
| | <SiTelegram size={20} className="text-blue-500" /> |
| | </div> |
| | <div className="flex-1 min-w-0"> |
| | <div className="font-medium text-gray-900">{t('Telegram')}</div> |
| | <div className="text-sm text-gray-500 truncate"> |
| | {userState.user && userState.user.telegram_id !== '' |
| | ? userState.user.telegram_id |
| | : t('未绑定')} |
| | </div> |
| | </div> |
| | </div> |
| | <div className="flex-shrink-0"> |
| | {status.telegram_oauth ? ( |
| | userState.user.telegram_id !== '' ? ( |
| | <Button disabled={true} size="small" className="!rounded-lg"> |
| | {t('已绑定')} |
| | </Button> |
| | ) : ( |
| | <div className="scale-75"> |
| | <TelegramLoginButton |
| | dataAuthUrl='/api/oauth/telegram/bind' |
| | botName={status.telegram_bot_name} |
| | /> |
| | </div> |
| | ) |
| | ) : ( |
| | <Button disabled={true} size="small" className="!rounded-lg"> |
| | {t('未启用')} |
| | </Button> |
| | )} |
| | </div> |
| | </div> |
| | </Card> |
| | |
| | {/* LinuxDO绑定 */} |
| | <Card |
| | className="!rounded-xl transition-shadow" |
| | bodyStyle={{ padding: '16px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex items-center justify-between"> |
| | <div className="flex items-center flex-1"> |
| | <div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-3"> |
| | <SiLinux size={20} className="text-orange-500" /> |
| | </div> |
| | <div className="flex-1 min-w-0"> |
| | <div className="font-medium text-gray-900">{t('LinuxDO')}</div> |
| | <div className="text-sm text-gray-500 truncate"> |
| | {userState.user && userState.user.linux_do_id !== '' |
| | ? userState.user.linux_do_id |
| | : t('未绑定')} |
| | </div> |
| | </div> |
| | </div> |
| | <Button |
| | type="primary" |
| | theme="outline" |
| | size="small" |
| | onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)} |
| | disabled={ |
| | (userState.user && userState.user.linux_do_id !== '') || |
| | !status.linuxdo_oauth |
| | } |
| | className="!rounded-lg" |
| | > |
| | {status.linuxdo_oauth ? t('绑定') : t('未启用')} |
| | </Button> |
| | </div> |
| | </Card> |
| | </div> |
| | </div> |
| | </TabPane> |
| | |
| | {/* 安全设置Tab */} |
| | <TabPane |
| | tab={ |
| | <div className="flex items-center"> |
| | <ShieldCheck size={16} className="mr-2" /> |
| | {t('安全设置')} |
| | </div> |
| | } |
| | itemKey='security' |
| | > |
| | <div className="py-4"> |
| | <div className="space-y-6"> |
| | <Space vertical className='w-full'> |
| | {/* 系统访问令牌 */} |
| | <Card |
| | className="!rounded-xl w-full" |
| | bodyStyle={{ padding: '20px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4"> |
| | <div className="flex items-start w-full sm:w-auto"> |
| | <div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mr-4 flex-shrink-0"> |
| | <IconKey size="large" className="text-blue-500" /> |
| | </div> |
| | <div className="flex-1"> |
| | <Typography.Title heading={6} className="mb-1"> |
| | {t('系统访问令牌')} |
| | </Typography.Title> |
| | <Typography.Text type="tertiary" className="text-sm"> |
| | {t('用于API调用的身份验证令牌,请妥善保管')} |
| | </Typography.Text> |
| | {systemToken && ( |
| | <div className="mt-3"> |
| | <Input |
| | readOnly |
| | value={systemToken} |
| | onClick={handleSystemTokenClick} |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconKey />} |
| | /> |
| | </div> |
| | )} |
| | </div> |
| | </div> |
| | <Button |
| | type="primary" |
| | theme="solid" |
| | onClick={generateAccessToken} |
| | className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 w-full sm:w-auto" |
| | icon={<IconKey />} |
| | > |
| | {systemToken ? t('重新生成') : t('生成令牌')} |
| | </Button> |
| | </div> |
| | </Card> |
| | |
| | {/* 密码管理 */} |
| | <Card |
| | className="!rounded-xl w-full" |
| | bodyStyle={{ padding: '20px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4"> |
| | <div className="flex items-start w-full sm:w-auto"> |
| | <div className="w-12 h-12 rounded-full bg-orange-50 flex items-center justify-center mr-4 flex-shrink-0"> |
| | <IconLock size="large" className="text-orange-500" /> |
| | </div> |
| | <div> |
| | <Typography.Title heading={6} className="mb-1"> |
| | {t('密码管理')} |
| | </Typography.Title> |
| | <Typography.Text type="tertiary" className="text-sm"> |
| | {t('定期更改密码可以提高账户安全性')} |
| | </Typography.Text> |
| | </div> |
| | </div> |
| | <Button |
| | type="primary" |
| | theme="solid" |
| | onClick={() => setShowChangePasswordModal(true)} |
| | className="!rounded-lg !bg-orange-500 hover:!bg-orange-600 w-full sm:w-auto" |
| | icon={<IconLock />} |
| | > |
| | {t('修改密码')} |
| | </Button> |
| | </div> |
| | </Card> |
| | |
| | {/* 危险区域 */} |
| | <Card |
| | className="!rounded-xl border-red-200 w-full" |
| | bodyStyle={{ padding: '20px' }} |
| | shadows='hover' |
| | > |
| | <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4"> |
| | <div className="flex items-start w-full sm:w-auto"> |
| | <div className="w-12 h-12 rounded-full bg-red-50 flex items-center justify-center mr-4 flex-shrink-0"> |
| | <IconDelete size="large" className="text-red-500" /> |
| | </div> |
| | <div> |
| | <Typography.Title heading={6} className="mb-1 text-red-600"> |
| | {t('删除账户')} |
| | </Typography.Title> |
| | <Typography.Text type="tertiary" className="text-sm"> |
| | {t('此操作不可逆,所有数据将被永久删除')} |
| | </Typography.Text> |
| | </div> |
| | </div> |
| | <Button |
| | type="danger" |
| | theme="solid" |
| | onClick={() => setShowAccountDeleteModal(true)} |
| | className="!rounded-lg w-full sm:w-auto" |
| | icon={<IconDelete />} |
| | > |
| | {t('删除账户')} |
| | </Button> |
| | </div> |
| | </Card> |
| | </Space> |
| | </div> |
| | </div> |
| | </TabPane> |
| | |
| | {/* 通知设置Tab */} |
| | <TabPane |
| | tab={ |
| | <div className="flex items-center"> |
| | <Bell size={16} className="mr-2" /> |
| | {t('通知设置')} |
| | </div> |
| | } |
| | itemKey='notification' |
| | > |
| | <div className="py-4"> |
| | <Tabs type='card' defaultActiveKey='notify' className="!rounded-lg"> |
| | <TabPane |
| | tab={t('通知设置')} |
| | itemKey='notify' |
| | > |
| | <div className="space-y-6"> |
| | {/* 通知方式选择 */} |
| | <div className="bg-gray-50 rounded-xl"> |
| | <Typography.Text strong className="block mb-4 pt-4">{t('通知方式')}</Typography.Text> |
| | <RadioGroup |
| | value={notificationSettings.warningType} |
| | onChange={(value) => |
| | handleNotificationSettingChange('warningType', value) |
| | } |
| | type="pureCard" |
| | > |
| | <Radio value='email' className="!p-4 !rounded-lg"> |
| | <div className="flex items-center"> |
| | <IconMail className="mr-2 text-blue-500" /> |
| | <div> |
| | <div className="font-medium">{t('邮件通知')}</div> |
| | <div className="text-sm text-gray-500">{t('通过邮件接收通知')}</div> |
| | </div> |
| | </div> |
| | </Radio> |
| | <Radio value='webhook' className="!p-4 !rounded-lg"> |
| | <div className="flex items-center"> |
| | <Webhook size={16} className="mr-2 text-green-500" /> |
| | <div> |
| | <div className="font-medium">{t('Webhook通知')}</div> |
| | <div className="text-sm text-gray-500">{t('通过HTTP请求接收通知')}</div> |
| | </div> |
| | </div> |
| | </Radio> |
| | </RadioGroup> |
| | </div> |
| | |
| | {/* Webhook设置 */} |
| | {notificationSettings.warningType === 'webhook' && ( |
| | <div className="space-y-4"> |
| | <div className="bg-white rounded-xl"> |
| | <Typography.Text strong className="block mb-3">{t('Webhook地址')}</Typography.Text> |
| | <Input |
| | value={notificationSettings.webhookUrl} |
| | onChange={(val) => |
| | handleNotificationSettingChange('webhookUrl', val) |
| | } |
| | placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')} |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<Webhook size={16} className="m-2" />} |
| | /> |
| | <div className="text-gray-500 text-sm mt-2"> |
| | {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')} |
| | </div> |
| | </div> |
| | |
| | <div className="bg-white rounded-xl"> |
| | <Typography.Text strong className="block mb-3">{t('接口凭证(可选)')}</Typography.Text> |
| | <Input |
| | value={notificationSettings.webhookSecret} |
| | onChange={(val) => |
| | handleNotificationSettingChange('webhookSecret', val) |
| | } |
| | placeholder={t('请输入密钥')} |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconKey />} |
| | /> |
| | <div className="text-gray-500 text-sm mt-2"> |
| | {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')} |
| | </div> |
| | </div> |
| | |
| | <div className="bg-yellow-50 rounded-xl"> |
| | <div className="flex items-center justify-between cursor-pointer" onClick={() => setShowWebhookDocs(!showWebhookDocs)}> |
| | <div className="flex items-center"> |
| | <Globe size={16} className="mr-2 text-yellow-600" /> |
| | <Typography.Text strong className="text-yellow-800"> |
| | {t('Webhook请求结构')} |
| | </Typography.Text> |
| | </div> |
| | {showWebhookDocs ? <IconChevronUp /> : <IconChevronDown />} |
| | </div> |
| | <Collapsible isOpen={showWebhookDocs}> |
| | <pre className="mt-4 bg-gray-800 text-gray-100 rounded-lg text-sm overflow-x-auto"> |
| | {`{ |
| | "type": "quota_exceed", // 通知类型 |
| | "title": "标题", // 通知标题 |
| | "content": "通知内容", // 通知内容,支持 {{value}} 变量占位符 |
| | "values": ["值1", "值2"], // 按顺序替换content中的 {{value}} 占位符 |
| | "timestamp": 1739950503 // 时间戳 |
| | } |
| | |
| | 示例: |
| | { |
| | "type": "quota_exceed", |
| | "title": "额度预警通知", |
| | "content": "您的额度即将用尽,当前剩余额度为 {{value}}", |
| | "values": ["$0.99"], |
| | "timestamp": 1739950503 |
| | }`} |
| | </pre> |
| | </Collapsible> |
| | </div> |
| | </div> |
| | )} |
| | |
| | {/* 邮件设置 */} |
| | {notificationSettings.warningType === 'email' && ( |
| | <div className="bg-white rounded-xl"> |
| | <Typography.Text strong className="block mb-3">{t('通知邮箱')}</Typography.Text> |
| | <Input |
| | value={notificationSettings.notificationEmail} |
| | onChange={(val) => |
| | handleNotificationSettingChange('notificationEmail', val) |
| | } |
| | placeholder={t('留空则使用账号绑定的邮箱')} |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconMail />} |
| | /> |
| | <div className="text-gray-500 text-sm mt-2"> |
| | {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')} |
| | </div> |
| | </div> |
| | )} |
| | |
| | {/* 预警阈值 */} |
| | <div className="bg-white rounded-xl"> |
| | <Typography.Text strong className="block mb-3"> |
| | {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)} |
| | </Typography.Text> |
| | <AutoComplete |
| | value={notificationSettings.warningThreshold} |
| | onChange={(val) => |
| | 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={<IconBell />} |
| | /> |
| | <div className="text-gray-500 text-sm mt-2"> |
| | {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')} |
| | </div> |
| | </div> |
| | </div> |
| | </TabPane> |
| | |
| | <TabPane |
| | tab={t('价格设置')} |
| | itemKey='price' |
| | > |
| | <div className="py-4"> |
| | <div className="bg-white rounded-xl"> |
| | <div className="flex items-start"> |
| | <div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mt-1"> |
| | <Shield size={20} className="text-orange-500" /> |
| | </div> |
| | <div className="flex-1"> |
| | <div className="flex items-center justify-between"> |
| | <div> |
| | <Typography.Text strong className="block mb-2"> |
| | {t('接受未设置价格模型')} |
| | </Typography.Text> |
| | <div className="text-gray-500 text-sm"> |
| | {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} |
| | </div> |
| | </div> |
| | <Checkbox |
| | checked={notificationSettings.acceptUnsetModelRatioModel} |
| | onChange={(e) => |
| | handleNotificationSettingChange( |
| | 'acceptUnsetModelRatioModel', |
| | e.target.checked, |
| | ) |
| | } |
| | className="ml-4" |
| | /> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </TabPane> |
| | </Tabs> |
| | |
| | <div className="mt-6 flex justify-end"> |
| | <Button |
| | type='primary' |
| | onClick={saveNotificationSettings} |
| | size="large" |
| | className="!rounded-lg !bg-purple-500 hover:!bg-purple-600" |
| | icon={<IconSetting />} |
| | > |
| | {t('保存设置')} |
| | </Button> |
| | </div> |
| | </div> |
| | </TabPane> |
| | </Tabs> |
| | </div> |
| | </Card> |
| | </div> |
| | </div> |
| | </Layout.Content> |
| | </Layout> |
| |
|
| | {} |
| | <Modal |
| | title={ |
| | <div className="flex items-center"> |
| | <IconMail className="mr-2 text-blue-500" /> |
| | {t('绑定邮箱地址')} |
| | </div> |
| | } |
| | visible={showEmailBindModal} |
| | onCancel={() => setShowEmailBindModal(false)} |
| | onOk={bindEmail} |
| | size={'small'} |
| | centered={true} |
| | maskClosable={false} |
| | className="modern-modal" |
| | > |
| | <div className="space-y-4 py-4"> |
| | <div className="flex gap-3"> |
| | <Input |
| | placeholder={t('输入邮箱地址')} |
| | onChange={(value) => handleInputChange('email', value)} |
| | name='email' |
| | type='email' |
| | size="large" |
| | className="!rounded-lg flex-1" |
| | prefix={<IconMail />} |
| | /> |
| | <Button |
| | onClick={sendVerificationCode} |
| | disabled={disableButton || loading} |
| | className="!rounded-lg" |
| | type="primary" |
| | theme="outline" |
| | size='large' |
| | > |
| | {disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')} |
| | </Button> |
| | </div> |
| | |
| | <Input |
| | placeholder={t('验证码')} |
| | name='email_verification_code' |
| | value={inputs.email_verification_code} |
| | onChange={(value) => |
| | handleInputChange('email_verification_code', value) |
| | } |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconKey />} |
| | /> |
| | |
| | {turnstileEnabled && ( |
| | <div className="flex justify-center"> |
| | <Turnstile |
| | sitekey={turnstileSiteKey} |
| | onVerify={(token) => { |
| | setTurnstileToken(token); |
| | }} |
| | /> |
| | </div> |
| | )} |
| | </div> |
| | </Modal> |
| |
|
| | {} |
| | <Modal |
| | title={ |
| | <div className="flex items-center"> |
| | <SiWechat className="mr-2 text-green-500" size={20} /> |
| | {t('绑定微信账户')} |
| | </div> |
| | } |
| | visible={showWeChatBindModal} |
| | onCancel={() => setShowWeChatBindModal(false)} |
| | footer={null} |
| | size={'small'} |
| | centered={true} |
| | className="modern-modal" |
| | > |
| | <div className="space-y-4 py-4 text-center"> |
| | <Image src={status.wechat_qrcode} className="mx-auto" /> |
| | <div className="text-gray-600"> |
| | <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p> |
| | </div> |
| | <Input |
| | placeholder={t('验证码')} |
| | name='wechat_verification_code' |
| | value={inputs.wechat_verification_code} |
| | onChange={(v) => |
| | handleInputChange('wechat_verification_code', v) |
| | } |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconKey />} |
| | /> |
| | <Button |
| | type="primary" |
| | theme="solid" |
| | size='large' |
| | onClick={bindWeChat} |
| | className="!rounded-lg w-full !bg-green-500 hover:!bg-green-600" |
| | icon={<SiWechat size={16} />} |
| | > |
| | {t('绑定')} |
| | </Button> |
| | </div> |
| | </Modal> |
| |
|
| | {} |
| | <Modal |
| | title={ |
| | <div className="flex items-center"> |
| | <IconDelete className="mr-2 text-red-500" /> |
| | {t('删除账户确认')} |
| | </div> |
| | } |
| | visible={showAccountDeleteModal} |
| | onCancel={() => setShowAccountDeleteModal(false)} |
| | onOk={deleteAccount} |
| | size={'small'} |
| | centered={true} |
| | className="modern-modal" |
| | > |
| | <div className="space-y-4 py-4"> |
| | <Banner |
| | type='danger' |
| | description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')} |
| | closeIcon={null} |
| | className="!rounded-lg" |
| | /> |
| | |
| | <div> |
| | <Typography.Text strong className="block mb-2 text-red-600"> |
| | {t('请输入您的用户名以确认删除')} |
| | </Typography.Text> |
| | <Input |
| | placeholder={t('输入你的账户名{{username}}以确认删除', { username: ` ${userState?.user?.username} ` })} |
| | name='self_account_deletion_confirmation' |
| | value={inputs.self_account_deletion_confirmation} |
| | onChange={(value) => |
| | handleInputChange('self_account_deletion_confirmation', value) |
| | } |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconUser />} |
| | /> |
| | </div> |
| | |
| | {turnstileEnabled && ( |
| | <div className="flex justify-center"> |
| | <Turnstile |
| | sitekey={turnstileSiteKey} |
| | onVerify={(token) => { |
| | setTurnstileToken(token); |
| | }} |
| | /> |
| | </div> |
| | )} |
| | </div> |
| | </Modal> |
| |
|
| | {} |
| | <Modal |
| | title={ |
| | <div className="flex items-center"> |
| | <IconLock className="mr-2 text-orange-500" /> |
| | {t('修改密码')} |
| | </div> |
| | } |
| | visible={showChangePasswordModal} |
| | onCancel={() => setShowChangePasswordModal(false)} |
| | onOk={changePassword} |
| | size={'small'} |
| | centered={true} |
| | className="modern-modal" |
| | > |
| | <div className="space-y-4 py-4"> |
| | <div> |
| | <Typography.Text strong className="block mb-2">{t('原密码')}</Typography.Text> |
| | <Input |
| | name='original_password' |
| | placeholder={t('请输入原密码')} |
| | type='password' |
| | value={inputs.original_password} |
| | onChange={(value) => |
| | handleInputChange('original_password', value) |
| | } |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconLock />} |
| | /> |
| | </div> |
| | |
| | <div> |
| | <Typography.Text strong className="block mb-2">{t('新密码')}</Typography.Text> |
| | <Input |
| | name='set_new_password' |
| | placeholder={t('请输入新密码')} |
| | type='password' |
| | value={inputs.set_new_password} |
| | onChange={(value) => |
| | handleInputChange('set_new_password', value) |
| | } |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconLock />} |
| | /> |
| | </div> |
| | |
| | <div> |
| | <Typography.Text strong className="block mb-2">{t('确认新密码')}</Typography.Text> |
| | <Input |
| | name='set_new_password_confirmation' |
| | placeholder={t('请再次输入新密码')} |
| | type='password' |
| | value={inputs.set_new_password_confirmation} |
| | onChange={(value) => |
| | handleInputChange('set_new_password_confirmation', value) |
| | } |
| | size="large" |
| | className="!rounded-lg" |
| | prefix={<IconLock />} |
| | /> |
| | </div> |
| | |
| | {turnstileEnabled && ( |
| | <div className="flex justify-center"> |
| | <Turnstile |
| | sitekey={turnstileSiteKey} |
| | onVerify={(token) => { |
| | setTurnstileToken(token); |
| | }} |
| | /> |
| | </div> |
| | )} |
| | </div> |
| | </Modal> |
| | </div> |
| | ); |
| | }; |
| |
|
| | export default PersonalSetting; |
| |
|