From f17b4f0760c6e7865a6335d15aa248bc9b2571ce Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Fri, 6 Jun 2025 04:20:57 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20feat(ui):=20enhance=20model=20di?= =?UTF-8?q?splay=20with=20category=20tabs=20and=20loading=20states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve the model list section in PersonalSetting component with the following enhancements: - Add category-based tabs for filtering models by provider (OpenAI, Anthropic, etc.) - Implement skeleton loading states using Semi UI components - Add empty state illustrations when no models are available - Use Semi UI design tokens for consistent styling - Optimize display for both expanded and collapsed model lists - Simplify some button text labels for better UI consistency --- .../components/settings/PersonalSetting.js | 431 ++++++++---------- web/src/i18n/locales/en.json | 3 +- web/src/pages/TopUp/index.js | 175 ++++++- 3 files changed, 365 insertions(+), 244 deletions(-) diff --git a/web/src/components/settings/PersonalSetting.js b/web/src/components/settings/PersonalSetting.js index f8cbc022..935143ab 100644 --- a/web/src/components/settings/PersonalSetting.js +++ b/web/src/components/settings/PersonalSetting.js @@ -8,13 +8,14 @@ import { showError, showInfo, showSuccess, - getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor, onGitHubOAuthClicked, onOIDCClicked, - onLinuxDOOAuthClicked + onLinuxDOOAuthClicked, + renderModelTag, + getModelCategories } from '../../helpers'; import Turnstile from 'react-turnstile'; import { UserContext } from '../../context/User'; @@ -23,11 +24,12 @@ import { Banner, Button, Card, + Empty, Image, Input, - InputNumber, Layout, Modal, + Skeleton, Space, Tag, Typography, @@ -39,6 +41,7 @@ import { Tabs, TabPane, } from '@douyinfe/semi-ui'; +import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations'; import { IconMail, IconLock, @@ -48,8 +51,6 @@ import { IconBell, IconGithubLogo, IconKey, - IconCreditCard, - IconLink, IconDelete, IconChevronDown, IconChevronUp, @@ -84,16 +85,14 @@ const PersonalSetting = () => { const [loading, setLoading] = useState(false); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); - const [affLink, setAffLink] = useState(''); const [systemToken, setSystemToken] = useState(''); const [models, setModels] = useState([]); - const [openTransfer, setOpenTransfer] = useState(false); - const [transferAmount, setTransferAmount] = useState(0); 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', @@ -103,6 +102,7 @@ const PersonalSetting = () => { notificationEmail: '', acceptUnsetModelRatioModel: false, }); + const [modelsLoading, setModelsLoading] = useState(true); const [showWebhookDocs, setShowWebhookDocs] = useState(true); useEffect(() => { @@ -119,8 +119,6 @@ const PersonalSetting = () => { console.log(userState); }); loadModels().then(); - getAffLink().then(); - setTransferAmount(getQuotaPerUnit()); }, []); useEffect(() => { @@ -172,17 +170,6 @@ const PersonalSetting = () => { } }; - const getAffLink = async () => { - const res = await API.get('/api/user/aff'); - const { success, message, data } = res.data; - if (success) { - let link = `${window.location.origin}/register?aff=${data}`; - setAffLink(link); - } else { - showError(message); - } - }; - const getUserData = async () => { let res = await API.get(`/api/user/self`); const { success, message, data } = res.data; @@ -194,21 +181,24 @@ const PersonalSetting = () => { }; const loadModels = async () => { - let res = await API.get(`/api/user/models`); - const { success, message, data } = res.data; - if (success) { - if (data != null) { - setModels(data); - } - } else { - showError(message); - } - }; + setModelsLoading(true); - const handleAffLinkClick = async (e) => { - e.target.select(); - await copy(e.target.value); - showSuccess(t('邀请链接已复制到剪切板')); + 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) => { @@ -282,24 +272,6 @@ const PersonalSetting = () => { setShowChangePasswordModal(false); }; - const transfer = async () => { - if (transferAmount < getQuotaPerUnit()) { - showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit())); - return; - } - const res = await API.post(`/api/user/aff_transfer`, { - quota: transferAmount, - }); - const { success, message } = res.data; - if (success) { - showSuccess(message); - setOpenTransfer(false); - getUserData().then(); - } else { - showError(message); - } - }; - const sendVerificationCode = async () => { if (inputs.email === '') { showError(t('请输入邮箱!')); @@ -360,10 +332,6 @@ const PersonalSetting = () => { return 'NA'; }; - const handleCancel = () => { - setOpenTransfer(false); - }; - const copyText = async (text) => { if (await copy(text)) { showSuccess(t('已复制:') + text); @@ -409,52 +377,9 @@ const PersonalSetting = () => {
- {/* 划转模态框 */} - - - {t('请输入要划转的数量')} -
- } - visible={openTransfer} - onOk={transfer} - onCancel={handleCancel} - maskClosable={false} - size={'small'} - centered={true} - > -
-
- - {t('可用额度')} {renderQuotaWithPrompt(userState?.user?.aff_quota)} - - -
-
- - {t('划转额度')} {renderQuotaWithPrompt(transferAmount)}{' '} - {t('最低') + renderQuota(getQuotaPerUnit())} - - setTransferAmount(value)} - disabled={false} - size="large" - className="!rounded-lg w-full" - /> -
-
-
-
+
{/* 主卡片容器 */} {/* 顶部用户信息区域 */} @@ -600,17 +525,17 @@ const PersonalSetting = () => { {/* 主内容区域 - 使用Tabs组织不同功能模块 */}
- {/* 模型与邀请Tab */} + {/* 可用模型Tab */} - {t('模型与邀请')} + {t('可用模型')}
} itemKey='models' > -
+
{/* 可用模型部分 */}
@@ -618,148 +543,176 @@ const PersonalSetting = () => {
- {t('可用模型')} + {t('模型列表')}
{t('点击模型名称可复制')}
-
- {models.length <= MODELS_DISPLAY_COUNT ? ( - - {models.map((model) => ( - copyText(model)} - className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg" - > - {model} - + {modelsLoading ? ( + // 骨架屏加载状态 - 模拟实际加载后的布局 +
+ {/* 模拟分类标签 */} +
+
+ {Array.from({ length: 8 }).map((_, index) => ( + + ))} +
+
+ + {/* 模拟模型标签列表 */} +
+ {Array.from({ length: 20 }).map((_, index) => ( + ))} - - ) : ( - <> - - - {models.map((model) => ( - copyText(model)} - className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg" - > - {model} - - ))} - setIsModelsExpanded(false)} - icon={} - > - {t('收起')} - - - - {!isModelsExpanded && ( - - {models - .slice(0, MODELS_DISPLAY_COUNT) - .map((model) => ( - copyText(model)} - className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg" - > - {model} - - ))} - setIsModelsExpanded(true)} - icon={} - > - {t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')} - - - )} - - )} -
-
- - {/* 邀请信息部分 */} -
-
-
- +
-
- {t('邀请信息')} -
{t('管理您的邀请链接和收益')}
-
-
- -
-
- -
{t('待使用收益')}
-
- {renderQuota(userState?.user?.aff_quota)} -
- -
- -
{t('总收益')}
-
- {renderQuota(userState?.user?.aff_history_quota)} -
-
- -
{t('邀请人数')}
-
- {userState?.user?.aff_count || 0} -
-
-
- -
- {t('邀请链接')} - } + ) : 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('个模型')} + + + )} + + ); + } + })()} +
+ + )}
@@ -805,7 +758,7 @@ const PersonalSetting = () => { > {userState.user && userState.user.email !== '' ? t('修改绑定') - : t('绑定邮箱')} + : t('绑定')}
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index eccd8b69..82949ef4 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -743,7 +743,6 @@ "无效的用户单独并发限制数据": "Invalid user individual concurrency limit data", "未绑定": "Not bound", "修改绑定": "Modify binding", - "绑定邮箱": "Bind email", "确认新密码": "Confirm new password", "历史消耗": "Consumption", "查看": "Check", @@ -1458,7 +1457,7 @@ "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "The administrator has not enabled the online recharge function, please contact the administrator to enable it or recharge with a redemption code.", "点击模型名称可复制": "Click the model name to copy", "管理您的邀请链接和收益": "Manage your invitation link and earnings", - "模型与邀请": "Model and Invitation", + "没有可用模型": "No available models", "账户绑定": "Account Binding", "安全设置": "Security Settings", "系统访问令牌": "System Access Token", diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index 2f920647..bff83791 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -6,7 +6,9 @@ import { showSuccess, renderQuota, renderQuotaWithAmount, - stringToColor + stringToColor, + copy, + getQuotaPerUnit } from '../../helpers'; import { Layout, @@ -54,6 +56,11 @@ const TopUp = () => { const [paymentLoading, setPaymentLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false); + // 邀请相关状态 + const [affLink, setAffLink] = useState(''); + const [openTransfer, setOpenTransfer] = useState(false); + const [transferAmount, setTransferAmount] = useState(0); + const getUsername = () => { if (userState.user) { return userState.user.username; @@ -211,6 +218,44 @@ const TopUp = () => { setUserDataLoading(false); }; + // 获取邀请链接 + const getAffLink = async () => { + const res = await API.get('/api/user/aff'); + const { success, message, data } = res.data; + if (success) { + let link = `${window.location.origin}/register?aff=${data}`; + setAffLink(link); + } else { + showError(message); + } + }; + + // 划转邀请额度 + const transfer = async () => { + if (transferAmount < getQuotaPerUnit()) { + showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit())); + return; + } + const res = await API.post(`/api/user/aff_transfer`, { + quota: transferAmount, + }); + const { success, message } = res.data; + if (success) { + showSuccess(message); + setOpenTransfer(false); + getUserQuota().then(); + } else { + showError(message); + } + }; + + // 复制邀请链接 + const handleAffLinkClick = async (e) => { + e.target.select(); + await copy(e.target.value); + showSuccess(t('邀请链接已复制到剪切板')); + }; + useEffect(() => { if (userState?.user?.id) { setUserDataLoading(false); @@ -218,6 +263,8 @@ const TopUp = () => { } else { getUserQuota().then(); } + getAffLink().then(); + setTransferAmount(getQuotaPerUnit()); }, []); useEffect(() => { @@ -264,10 +311,58 @@ const TopUp = () => { setOpen(false); }; + const handleTransferCancel = () => { + setOpenTransfer(false); + }; + return (
+ {/* 划转模态框 */} + + + {t('请输入要划转的数量')} +
+ } + visible={openTransfer} + onOk={transfer} + onCancel={handleTransferCancel} + maskClosable={false} + size={'small'} + centered={true} + > +
+
+ + {t('可用额度')} {renderQuota(userState?.user?.aff_quota)} + + +
+
+ + {t('划转额度')} {renderQuota(transferAmount)}{' '} + {t('最低') + renderQuota(getQuotaPerUnit())} + + setTransferAmount(value)} + disabled={false} + size="large" + className="!rounded-lg w-full" + /> +
+
+ + @@ -300,7 +395,7 @@ const TopUp = () => {
-
+
{
-
+
+ {/* 邀请信息部分 */} +
+
+
+
+ +
+
+
+ {t('邀请信息')} + +
+
{t('管理您的邀请链接和收益')}
+
+
+
+ +
+
+ +
{t('待使用收益')}
+
+ {renderQuota(userState?.user?.aff_quota)} +
+ +
+ +
{t('总收益')}
+
+ {renderQuota(userState?.user?.aff_history_quota)} +
+
+ +
{t('邀请人数')}
+
+ {userState?.user?.aff_count || 0} +
+
+
+ +
+ {t('邀请链接')} + } + /> +
+
+