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('邀请链接')}
+ }
+ />
+
+
+