From 9805d35a5d9572bf72b14a6e8e037fef583e31b4 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 17 Aug 2025 00:49:54 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(personal-settings?= =?UTF-8?q?):=20Break=20down=20PersonalSetting.js=20into=20modular=20compo?= =?UTF-8?q?nents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split the 1554-line PersonalSetting.js file into smaller, maintainable components - Created organized folder structure under personal/: - components/: UserInfoHeader for shared user info display - tabs/: ModelsList, AccountBinding, SecuritySettings, NotificationSettings - modals/: EmailBindModal, WeChatBindModal, AccountDeleteModal, ChangePasswordModal - Refactored main PersonalSetting component to use composition pattern - Improved code maintainability and separation of concerns - Added collapsible prop to ModelsList tabs for better UX - Fixed import path for TwoFASetting component in SecuritySettings - Preserved all existing functionality and user interactions This refactoring reduces the main file from 1554 to 484 lines and makes the codebase more modular, testable, and easier to maintain. --- .../components/settings/PersonalSetting.js | 1344 ++--------------- .../personal/cards/AccountManagement.js | 411 +++++ .../settings/personal/cards/ModelsList.js | 240 +++ .../personal/cards/NotificationSettings.js | 289 ++++ .../{ => personal/components}/TwoFASetting.js | 8 +- .../personal/components/UserInfoHeader.js | 192 +++ .../personal/modals/AccountDeleteModal.js | 92 ++ .../personal/modals/ChangePasswordModal.js | 115 ++ .../personal/modals/EmailBindModal.js | 106 ++ .../personal/modals/WeChatBindModal.js | 80 + web/src/i18n/locales/en.json | 18 +- 11 files changed, 1629 insertions(+), 1266 deletions(-) create mode 100644 web/src/components/settings/personal/cards/AccountManagement.js create mode 100644 web/src/components/settings/personal/cards/ModelsList.js create mode 100644 web/src/components/settings/personal/cards/NotificationSettings.js rename web/src/components/settings/{ => personal/components}/TwoFASetting.js (99%) create mode 100644 web/src/components/settings/personal/components/UserInfoHeader.js create mode 100644 web/src/components/settings/personal/modals/AccountDeleteModal.js create mode 100644 web/src/components/settings/personal/modals/ChangePasswordModal.js create mode 100644 web/src/components/settings/personal/modals/EmailBindModal.js create mode 100644 web/src/components/settings/personal/modals/WeChatBindModal.js diff --git a/web/src/components/settings/PersonalSetting.js b/web/src/components/settings/PersonalSetting.js index 010b5051..75401c1c 100644 --- a/web/src/components/settings/PersonalSetting.js +++ b/web/src/components/settings/PersonalSetting.js @@ -22,70 +22,27 @@ import { useNavigate } from 'react-router-dom'; import { API, copy, - isRoot, - isAdmin, showError, showInfo, - showSuccess, - renderQuota, - renderQuotaWithPrompt, - stringToColor, - onGitHubOAuthClicked, - onOIDCClicked, - onLinuxDOOAuthClicked, - renderModelTag, - getModelCategories + showSuccess } from '../../helpers'; -import TwoFASetting from './TwoFASetting'; -import Turnstile from 'react-turnstile'; import { UserContext } from '../../context/User'; -import { useTheme } from '../../context/Theme'; -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 { 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 theme = useTheme(); const [inputs, setInputs] = useState({ wechat_verification_code: '', @@ -109,13 +66,6 @@ const PersonalSetting = () => { 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, @@ -126,7 +76,6 @@ const PersonalSetting = () => { recordIpLog: false, }); const [modelsLoading, setModelsLoading] = useState(true); - const [showWebhookDocs, setShowWebhookDocs] = useState(true); useEffect(() => { let status = localStorage.getItem('status'); @@ -173,11 +122,6 @@ const PersonalSetting = () => { } }, [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 })); }; @@ -339,23 +283,6 @@ const PersonalSetting = () => { 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); @@ -399,1189 +326,90 @@ const PersonalSetting = () => { }; 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('记录请求与错误日志 IP')} - -
- {t('开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址')} -
-
- - handleNotificationSettingChange( - 'recordIpLog', - 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); - }} - /> -
- )} -
-
+ ); }; diff --git a/web/src/components/settings/personal/cards/AccountManagement.js b/web/src/components/settings/personal/cards/AccountManagement.js new file mode 100644 index 00000000..b2c9017e --- /dev/null +++ b/web/src/components/settings/personal/cards/AccountManagement.js @@ -0,0 +1,411 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { + Button, + Card, + Input, + Space, + Typography, + Avatar, + Tabs, + TabPane +} from '@douyinfe/semi-ui'; +import { + IconMail, + IconShield, + IconGithubLogo, + IconKey, + IconLock, + IconDelete +} from '@douyinfe/semi-icons'; +import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si'; +import { UserPlus, ShieldCheck } from 'lucide-react'; +import TelegramLoginButton from 'react-telegram-login'; +import { + onGitHubOAuthClicked, + onOIDCClicked, + onLinuxDOOAuthClicked +} from '../../../../helpers'; +import TwoFASetting from '../components/TwoFASetting'; + +const AccountManagement = ({ + t, + userState, + status, + systemToken, + setShowEmailBindModal, + setShowWeChatBindModal, + generateAccessToken, + handleSystemTokenClick, + setShowChangePasswordModal, + setShowAccountDeleteModal +}) => { + return ( + + {/* 卡片头部 */} +
+ + + +
+ {t('账户管理')} +
{t('账户绑定、安全设置和身份验证')}
+
+
+ + + {/* 账户绑定 Tab */} + + + {t('账户绑定')} + + } + itemKey="binding" + > +
+
+ {/* 邮箱绑定 */} + +
+
+
+ +
+
+
{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('此操作不可逆,所有数据将被永久删除')} + +
+
+ +
+
+
+
+
+
+
+
+ ); +}; + +export default AccountManagement; diff --git a/web/src/components/settings/personal/cards/ModelsList.js b/web/src/components/settings/personal/cards/ModelsList.js new file mode 100644 index 00000000..f69c605f --- /dev/null +++ b/web/src/components/settings/personal/cards/ModelsList.js @@ -0,0 +1,240 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useEffect } from 'react'; +import { + Empty, + Skeleton, + Space, + Tag, + Collapsible, + Tabs, + TabPane, + Typography, + Avatar +} from '@douyinfe/semi-ui'; +import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations'; +import { + IconChevronDown, + IconChevronUp +} from '@douyinfe/semi-icons'; +import { Settings } from 'lucide-react'; +import { renderModelTag, getModelCategories } from '../../../../helpers'; + +const ModelsList = ({ t, models, modelsLoading, copyText }) => { + 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; // 默认显示的模型数量 + + // Save models expanded state to localStorage whenever it changes + useEffect(() => { + localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded)); + }, [isModelsExpanded]); + + return ( +
+ {/* 卡片头部 */} +
+ + + +
+ {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" + collapsible + > + {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: 'small', + shape: 'circle', + onClick: () => copyText(model), + }) + ))} + + ); + } else { + return ( + <> + + + {filteredModels.map((model) => ( + renderModelTag(model, { + size: 'small', + shape: 'circle', + onClick: () => copyText(model), + }) + ))} + setIsModelsExpanded(false)} + icon={} + > + {t('收起')} + + + + {!isModelsExpanded && ( + + {filteredModels + .slice(0, MODELS_DISPLAY_COUNT) + .map((model) => ( + renderModelTag(model, { + size: 'small', + shape: 'circle', + onClick: () => copyText(model), + }) + ))} + setIsModelsExpanded(true)} + icon={} + > + {t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')} + + + )} + + ); + } + })()} +
+ + )} +
+
+ ); +}; + +export default ModelsList; diff --git a/web/src/components/settings/personal/cards/NotificationSettings.js b/web/src/components/settings/personal/cards/NotificationSettings.js new file mode 100644 index 00000000..dfa37e1b --- /dev/null +++ b/web/src/components/settings/personal/cards/NotificationSettings.js @@ -0,0 +1,289 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useRef, useEffect } from 'react'; +import { + Button, + Typography, + Card, + Avatar, + Form, + Radio, + Toast, + Tabs, + TabPane +} from '@douyinfe/semi-ui'; +import { + IconMail, + IconKey, + IconBell, + IconLink +} from '@douyinfe/semi-icons'; +import { ShieldCheck, Bell, DollarSign } from 'lucide-react'; +import { renderQuotaWithPrompt } from '../../../../helpers'; +import CodeViewer from '../../../playground/CodeViewer'; + +const NotificationSettings = ({ + t, + notificationSettings, + handleNotificationSettingChange, + saveNotificationSettings +}) => { + const formApiRef = useRef(null); + + // 初始化表单值 + useEffect(() => { + if (formApiRef.current && notificationSettings) { + formApiRef.current.setValues(notificationSettings); + } + }, [notificationSettings]); + + // 处理表单字段变化 + const handleFormChange = (field, value) => { + handleNotificationSettingChange(field, value); + }; + + // 表单提交 + const handleSubmit = () => { + if (formApiRef.current) { + formApiRef.current.validate() + .then(() => { + saveNotificationSettings(); + }) + .catch((errors) => { + console.log('表单验证失败:', errors); + Toast.error(t('请检查表单填写是否正确')); + }); + } else { + saveNotificationSettings(); + } + }; + + return ( + + + + } + > + {/* 卡片头部 */} +
+ + + +
+ {t('其他设置')} +
{t('通知、价格和隐私相关设置')}
+
+
+ +
(formApiRef.current = api)} + initValues={notificationSettings} + onSubmit={handleSubmit} + > + {() => ( + + {/* 通知配置 Tab */} + + + {t('通知配置')} + + } + itemKey="notification" + > +
+ handleFormChange('warningType', value)} + rules={[{ required: true, message: t('请选择通知方式') }]} + > + {t('邮件通知')} + {t('Webhook通知')} + + + + {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)} + + } + placeholder={t('请输入预警额度')} + data={[ + { value: 100000, label: '0.2$' }, + { value: 500000, label: '1$' }, + { value: 1000000, label: '5$' }, + { value: 5000000, label: '10$' }, + ]} + onChange={(val) => handleFormChange('warningThreshold', val)} + prefix={} + extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')} + style={{ width: '100%', maxWidth: '300px' }} + rules={[ + { required: true, message: t('请输入预警阈值') }, + { + validator: (rule, value) => { + const numValue = Number(value); + if (isNaN(numValue) || numValue <= 0) { + return Promise.reject(t('预警阈值必须为正数')); + } + return Promise.resolve(); + } + } + ]} + /> + + {/* 邮件通知设置 */} + {notificationSettings.warningType === 'email' && ( + handleFormChange('notificationEmail', val)} + prefix={} + extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')} + showClear + /> + )} + + {/* Webhook通知设置 */} + {notificationSettings.warningType === 'webhook' && ( + <> + handleFormChange('webhookUrl', val)} + prefix={} + extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')} + showClear + rules={[ + { + required: notificationSettings.warningType === 'webhook', + message: t('请输入Webhook地址') + }, + { + pattern: /^https:\/\/.+/, + message: t('Webhook地址必须以https://开头') + } + ]} + /> + + handleFormChange('webhookSecret', val)} + prefix={} + extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')} + showClear + /> + + +
+
+ +
+
+
type: 通知类型 (quota_exceed: 额度预警)
+
title: 通知标题
+
content: 通知内容,支持 {`{{value}}`} 变量占位符
+
values: 按顺序替换content中的变量占位符
+
timestamp: Unix时间戳
+
+
+
+ + )} +
+
+ + {/* 价格设置 Tab */} + + + {t('价格设置')} + + } + itemKey="pricing" + > +
+ handleFormChange('acceptUnsetModelRatioModel', value)} + extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} + /> +
+
+ + {/* 隐私设置 Tab */} + + + {t('隐私设置')} + + } + itemKey="privacy" + > +
+ handleFormChange('recordIpLog', value)} + extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')} + /> +
+
+
+ )} +
+
+ ); +}; + +export default NotificationSettings; diff --git a/web/src/components/settings/TwoFASetting.js b/web/src/components/settings/personal/components/TwoFASetting.js similarity index 99% rename from web/src/components/settings/TwoFASetting.js rename to web/src/components/settings/personal/components/TwoFASetting.js index 15a56780..6b1f96bd 100644 --- a/web/src/components/settings/TwoFASetting.js +++ b/web/src/components/settings/personal/components/TwoFASetting.js @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { API, showError, showSuccess, showWarning } from '../../helpers'; +import { API, showError, showSuccess, showWarning } from '../../../../helpers'; import { Banner, Button, Card, Checkbox, Divider, Input, Modal, Tag, Typography, Steps, Space, Badge } from '@douyinfe/semi-ui'; import { IconShield, @@ -353,11 +353,7 @@ const TwoFASetting = ({ t }) => { return ( <> - +
diff --git a/web/src/components/settings/personal/components/UserInfoHeader.js b/web/src/components/settings/personal/components/UserInfoHeader.js new file mode 100644 index 00000000..e1843d00 --- /dev/null +++ b/web/src/components/settings/personal/components/UserInfoHeader.js @@ -0,0 +1,192 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Avatar, Card, Tag, Divider, Typography } from '@douyinfe/semi-ui'; +import { isRoot, isAdmin, renderQuota } from '../../../../helpers'; +import { useTheme } from '../../../../context/Theme'; +import { Coins, BarChart2, Users } from 'lucide-react'; + +const UserInfoHeader = ({ t, userState }) => { + const theme = useTheme(); + + 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'; + }; + + return ( + + {/* 装饰性背景元素 */} +
+
+
+
+
+ +
+
+
+ + {getAvatarText()} + +
+
+ {getUsername()} +
+
+ {isRoot() ? ( + + {t('超级管理员')} + + ) : isAdmin() ? ( + + {t('管理员')} + + ) : ( + + {t('普通用户')} + + )} + + ID: {userState?.user?.id} + +
+
+
+ + {/* 右上角统计信息(Semi UI 卡片) */} +
+ +
+
+ +
+ {t('历史消耗')} +
{renderQuota(userState?.user?.used_quota)}
+
+
+ +
+ +
+ {t('请求次数')} +
{userState.user?.request_count || 0}
+
+
+ +
+ +
+ {t('用户分组')} +
{userState?.user?.group || t('默认')}
+
+
+
+
+
+
+ +
+
+ {t('当前余额')} +
+
+ {renderQuota(userState?.user?.quota)} +
+
+ + {/* 移动端统计信息卡片(仅 xs 可见) */} +
+ +
+
+
+ + {t('历史消耗')} +
+
{renderQuota(userState?.user?.used_quota)}
+
+ +
+
+ + {t('请求次数')} +
+
{userState.user?.request_count || 0}
+
+ +
+
+ + {t('用户分组')} +
+
{userState?.user?.group || t('默认')}
+
+
+
+
+ +
+
+
+ ); +}; + +export default UserInfoHeader; diff --git a/web/src/components/settings/personal/modals/AccountDeleteModal.js b/web/src/components/settings/personal/modals/AccountDeleteModal.js new file mode 100644 index 00000000..22e785e7 --- /dev/null +++ b/web/src/components/settings/personal/modals/AccountDeleteModal.js @@ -0,0 +1,92 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Banner, Input, Modal, Typography } from '@douyinfe/semi-ui'; +import { IconDelete, IconUser } from '@douyinfe/semi-icons'; +import Turnstile from 'react-turnstile'; + +const AccountDeleteModal = ({ + t, + showAccountDeleteModal, + setShowAccountDeleteModal, + inputs, + handleInputChange, + deleteAccount, + userState, + turnstileEnabled, + turnstileSiteKey, + setTurnstileToken +}) => { + return ( + + + {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); + }} + /> +
+ )} +
+ + ); +}; + +export default AccountDeleteModal; diff --git a/web/src/components/settings/personal/modals/ChangePasswordModal.js b/web/src/components/settings/personal/modals/ChangePasswordModal.js new file mode 100644 index 00000000..09d570cc --- /dev/null +++ b/web/src/components/settings/personal/modals/ChangePasswordModal.js @@ -0,0 +1,115 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Input, Modal, Typography } from '@douyinfe/semi-ui'; +import { IconLock } from '@douyinfe/semi-icons'; +import Turnstile from 'react-turnstile'; + +const ChangePasswordModal = ({ + t, + showChangePasswordModal, + setShowChangePasswordModal, + inputs, + handleInputChange, + changePassword, + turnstileEnabled, + turnstileSiteKey, + setTurnstileToken +}) => { + return ( + + + {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 ChangePasswordModal; diff --git a/web/src/components/settings/personal/modals/EmailBindModal.js b/web/src/components/settings/personal/modals/EmailBindModal.js new file mode 100644 index 00000000..fe3b485c --- /dev/null +++ b/web/src/components/settings/personal/modals/EmailBindModal.js @@ -0,0 +1,106 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Button, Input, Modal } from '@douyinfe/semi-ui'; +import { IconMail, IconKey } from '@douyinfe/semi-icons'; +import Turnstile from 'react-turnstile'; + +const EmailBindModal = ({ + t, + showEmailBindModal, + setShowEmailBindModal, + inputs, + handleInputChange, + sendVerificationCode, + bindEmail, + disableButton, + loading, + countdown, + turnstileEnabled, + turnstileSiteKey, + setTurnstileToken +}) => { + return ( + + + {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); + }} + /> +
+ )} +
+ + ); +}; + +export default EmailBindModal; diff --git a/web/src/components/settings/personal/modals/WeChatBindModal.js b/web/src/components/settings/personal/modals/WeChatBindModal.js new file mode 100644 index 00000000..a276aeeb --- /dev/null +++ b/web/src/components/settings/personal/modals/WeChatBindModal.js @@ -0,0 +1,80 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Button, Input, Modal, Image } from '@douyinfe/semi-ui'; +import { IconKey } from '@douyinfe/semi-icons'; +import { SiWechat } from 'react-icons/si'; + +const WeChatBindModal = ({ + t, + showWeChatBindModal, + setShowWeChatBindModal, + inputs, + handleInputChange, + bindWeChat, + status +}) => { + return ( + + + {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={} + /> + +
+
+ ); +}; + +export default WeChatBindModal; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 14870045..1d8f144f 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1691,7 +1691,7 @@ "暂无监控数据": "No monitoring data", "IP记录": "IP Record", "记录请求与错误日志 IP": "Record request and error log IP", - "开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址": "After enabling, only \"consumption\" and \"error\" logs will record your client IP address", + "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "After enabling, only \"consumption\" and \"error\" logs will record your client IP address", "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Only when the user sets IP recording, the IP recording of request and error type logs will be performed", "设置保存成功": "Settings saved successfully", "设置保存失败": "Settings save failed", @@ -1973,5 +1973,19 @@ "禁用2FA失败": "Failed to disable 2FA", "备用码重新生成成功": "Backup codes regenerated successfully", "重新生成备用码失败": "Failed to regenerate backup codes", - "备用码已复制到剪贴板": "Backup codes copied to clipboard" + "备用码已复制到剪贴板": "Backup codes copied to clipboard", + "账户管理": "Account management", + "账户绑定、安全设置和身份验证": "Account binding, security settings and identity verification", + "通知、价格和隐私相关设置": "Notification, price and privacy related settings", + "通知配置": "Notification configuration", + "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "Only HTTPS is supported, the system will send notifications via POST, please ensure that the address can receive POST requests", + "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "The key will be added to the request header as Bearer to verify the legitimacy of the webhook request", + "Webhook请求结构说明": "Webhook request structure description", + "通知类型 (quota_exceed: 额度预警)": "Notification type (quota_exceed: quota warning)", + "通知标题": "Notification title", + "通知内容,支持 {{value}} 变量占位符": "Notification content, supports {{value}} variable placeholders", + "按顺序替换content中的变量占位符": "Replace variable placeholders in content in order", + "Unix时间戳": "Unix timestamp", + "隐私设置": "Privacy settings", + "记录请求与错误日志IP": "Record request and error log IP" } \ No newline at end of file