🎨 feat(ui): enhance model display with category tabs and loading states

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
This commit is contained in:
Apple\Apple
2025-06-06 04:20:57 +08:00
parent a13f80f15b
commit f17b4f0760
3 changed files with 365 additions and 244 deletions

View File

@@ -8,13 +8,14 @@ import {
showError, showError,
showInfo, showInfo,
showSuccess, showSuccess,
getQuotaPerUnit,
renderQuota, renderQuota,
renderQuotaWithPrompt, renderQuotaWithPrompt,
stringToColor, stringToColor,
onGitHubOAuthClicked, onGitHubOAuthClicked,
onOIDCClicked, onOIDCClicked,
onLinuxDOOAuthClicked onLinuxDOOAuthClicked,
renderModelTag,
getModelCategories
} from '../../helpers'; } from '../../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { UserContext } from '../../context/User'; import { UserContext } from '../../context/User';
@@ -23,11 +24,12 @@ import {
Banner, Banner,
Button, Button,
Card, Card,
Empty,
Image, Image,
Input, Input,
InputNumber,
Layout, Layout,
Modal, Modal,
Skeleton,
Space, Space,
Tag, Tag,
Typography, Typography,
@@ -39,6 +41,7 @@ import {
Tabs, Tabs,
TabPane, TabPane,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
import { import {
IconMail, IconMail,
IconLock, IconLock,
@@ -48,8 +51,6 @@ import {
IconBell, IconBell,
IconGithubLogo, IconGithubLogo,
IconKey, IconKey,
IconCreditCard,
IconLink,
IconDelete, IconDelete,
IconChevronDown, IconChevronDown,
IconChevronUp, IconChevronUp,
@@ -84,16 +85,14 @@ const PersonalSetting = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false); const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30); const [countdown, setCountdown] = useState(30);
const [affLink, setAffLink] = useState('');
const [systemToken, setSystemToken] = useState(''); const [systemToken, setSystemToken] = useState('');
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
const [openTransfer, setOpenTransfer] = useState(false);
const [transferAmount, setTransferAmount] = useState(0);
const [isModelsExpanded, setIsModelsExpanded] = useState(() => { const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
// Initialize from localStorage if available // Initialize from localStorage if available
const savedState = localStorage.getItem('modelsExpanded'); const savedState = localStorage.getItem('modelsExpanded');
return savedState ? JSON.parse(savedState) : false; return savedState ? JSON.parse(savedState) : false;
}); });
const [activeModelCategory, setActiveModelCategory] = useState('all');
const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量 const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
const [notificationSettings, setNotificationSettings] = useState({ const [notificationSettings, setNotificationSettings] = useState({
warningType: 'email', warningType: 'email',
@@ -103,6 +102,7 @@ const PersonalSetting = () => {
notificationEmail: '', notificationEmail: '',
acceptUnsetModelRatioModel: false, acceptUnsetModelRatioModel: false,
}); });
const [modelsLoading, setModelsLoading] = useState(true);
const [showWebhookDocs, setShowWebhookDocs] = useState(true); const [showWebhookDocs, setShowWebhookDocs] = useState(true);
useEffect(() => { useEffect(() => {
@@ -119,8 +119,6 @@ const PersonalSetting = () => {
console.log(userState); console.log(userState);
}); });
loadModels().then(); loadModels().then();
getAffLink().then();
setTransferAmount(getQuotaPerUnit());
}, []); }, []);
useEffect(() => { 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 () => { const getUserData = async () => {
let res = await API.get(`/api/user/self`); let res = await API.get(`/api/user/self`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
@@ -194,21 +181,24 @@ const PersonalSetting = () => {
}; };
const loadModels = async () => { const loadModels = async () => {
let res = await API.get(`/api/user/models`); setModelsLoading(true);
const { success, message, data } = res.data;
if (success) {
if (data != null) {
setModels(data);
}
} else {
showError(message);
}
};
const handleAffLinkClick = async (e) => { try {
e.target.select(); let res = await API.get(`/api/user/models`);
await copy(e.target.value); const { success, message, data } = res.data;
showSuccess(t('邀请链接已复制到剪切板'));
if (success) {
if (data != null) {
setModels(data);
}
} else {
showError(message);
}
} catch (error) {
showError(t('加载模型列表失败'));
} finally {
setModelsLoading(false);
}
}; };
const handleSystemTokenClick = async (e) => { const handleSystemTokenClick = async (e) => {
@@ -282,24 +272,6 @@ const PersonalSetting = () => {
setShowChangePasswordModal(false); 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 () => { const sendVerificationCode = async () => {
if (inputs.email === '') { if (inputs.email === '') {
showError(t('请输入邮箱!')); showError(t('请输入邮箱!'));
@@ -360,10 +332,6 @@ const PersonalSetting = () => {
return 'NA'; return 'NA';
}; };
const handleCancel = () => {
setOpenTransfer(false);
};
const copyText = async (text) => { const copyText = async (text) => {
if (await copy(text)) { if (await copy(text)) {
showSuccess(t('已复制:') + text); showSuccess(t('已复制:') + text);
@@ -409,52 +377,9 @@ const PersonalSetting = () => {
<div className="bg-gray-50"> <div className="bg-gray-50">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
{/* 划转模态框 */}
<Modal
title={
<div className="flex items-center">
<IconCreditCard className="mr-2" />
{t('请输入要划转的数量')}
</div>
}
visible={openTransfer}
onOk={transfer}
onCancel={handleCancel}
maskClosable={false}
size={'small'}
centered={true}
>
<div className="space-y-4 py-4">
<div>
<Typography.Text strong className="block mb-2">
{t('可用额度')} {renderQuotaWithPrompt(userState?.user?.aff_quota)}
</Typography.Text>
<Input
value={userState?.user?.aff_quota}
disabled={true}
size="large"
className="!rounded-lg"
/>
</div>
<div>
<Typography.Text strong className="block mb-2">
{t('划转额度')} {renderQuotaWithPrompt(transferAmount)}{' '}
{t('最低') + renderQuota(getQuotaPerUnit())}
</Typography.Text>
<InputNumber
min={0}
value={transferAmount}
onChange={(value) => setTransferAmount(value)}
disabled={false}
size="large"
className="!rounded-lg w-full"
/>
</div>
</div>
</Modal>
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-full max-w-6xl"> <div className="w-full">
{/* 主卡片容器 */} {/* 主卡片容器 */}
<Card className="!rounded-2xl shadow-lg border-0"> <Card className="!rounded-2xl shadow-lg border-0">
{/* 顶部用户信息区域 */} {/* 顶部用户信息区域 */}
@@ -600,17 +525,17 @@ const PersonalSetting = () => {
{/* 主内容区域 - 使用Tabs组织不同功能模块 */} {/* 主内容区域 - 使用Tabs组织不同功能模块 */}
<div className="p-4"> <div className="p-4">
<Tabs type='line' defaultActiveKey='models' className="modern-tabs"> <Tabs type='line' defaultActiveKey='models' className="modern-tabs">
{/* 模型与邀请Tab */} {/* 可用模型Tab */}
<TabPane <TabPane
tab={ tab={
<div className="flex items-center"> <div className="flex items-center">
<Settings size={16} className="mr-2" /> <Settings size={16} className="mr-2" />
{t('模型与邀请')} {t('可用模型')}
</div> </div>
} }
itemKey='models' itemKey='models'
> >
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 py-4"> <div className="gap-6 py-4">
{/* 可用模型部分 */} {/* 可用模型部分 */}
<div className="bg-gray-50 rounded-xl"> <div className="bg-gray-50 rounded-xl">
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
@@ -618,148 +543,176 @@ const PersonalSetting = () => {
<Settings size={20} className="text-purple-500" /> <Settings size={20} className="text-purple-500" />
</div> </div>
<div> <div>
<Typography.Title heading={6} className="mb-0">{t('可用模型')}</Typography.Title> <Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title>
<div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div> <div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div>
</div> </div>
</div> </div>
<div className="bg-white rounded-lg p-3"> {modelsLoading ? (
{models.length <= MODELS_DISPLAY_COUNT ? ( // 骨架屏加载状态 - 模拟实际加载后的布局
<Space wrap> <div className="space-y-4">
{models.map((model) => ( {/* 模拟分类标签 */}
<Tag <div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
key={model} <div className="flex overflow-x-auto py-2 gap-2">
color={stringToColor(model)} {Array.from({ length: 8 }).map((_, index) => (
onClick={() => copyText(model)} <Skeleton.Button key={`cat-${index}`} style={{
className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg" width: index === 0 ? 130 : 100 + Math.random() * 50,
> height: 36,
{model} borderRadius: 8
</Tag> }} />
))}
</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'
}}
/>
))} ))}
</Space> </div>
) : (
<>
<Collapsible isOpen={isModelsExpanded}>
<Space wrap>
{models.map((model) => (
<Tag
key={model}
color={stringToColor(model)}
onClick={() => copyText(model)}
className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
>
{model}
</Tag>
))}
<Tag
color='grey'
type='light'
className="cursor-pointer !rounded-lg"
onClick={() => setIsModelsExpanded(false)}
icon={<IconChevronUp />}
>
{t('收起')}
</Tag>
</Space>
</Collapsible>
{!isModelsExpanded && (
<Space wrap>
{models
.slice(0, MODELS_DISPLAY_COUNT)
.map((model) => (
<Tag
key={model}
color={stringToColor(model)}
onClick={() => copyText(model)}
className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
>
{model}
</Tag>
))}
<Tag
color='grey'
type='light'
className="cursor-pointer !rounded-lg"
onClick={() => setIsModelsExpanded(true)}
icon={<IconChevronDown />}
>
{t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
</Tag>
</Space>
)}
</>
)}
</div>
</div>
{/* 邀请信息部分 */}
<div className="bg-gray-50 rounded-xl">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-3">
<IconLink size={20} className="text-orange-500" />
</div> </div>
<div> ) : models.length === 0 ? (
<Typography.Title heading={6} className="mb-0">{t('邀请信息')}</Typography.Title> <div className="py-8">
<div className="text-gray-500 text-sm">{t('管理您的邀请链接和收益')}</div> <Empty
</div> image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
</div> darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
description={t('没有可用模型')}
<div className="space-y-3"> style={{ padding: '24px 0' }}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="text-gray-600 text-xs font-medium">{t('待使用收益')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{renderQuota(userState?.user?.aff_quota)}
</div>
<Button
type="primary"
theme="solid"
onClick={() => setOpenTransfer(true)}
size="small"
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 mt-2 w-full"
icon={<IconCreditCard />}
>
{t('划转')}
</Button>
</Card>
<Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="text-gray-600 text-xs font-medium">{t('总收益')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{renderQuota(userState?.user?.aff_history_quota)}
</div>
</Card>
<Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="text-gray-600 text-xs font-medium">{t('邀请人数')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{userState?.user?.aff_count || 0}
</div>
</Card>
</div>
<div className="bg-white rounded-lg p-3">
<Typography.Text strong className="block mb-2 text-sm">{t('邀请链接')}</Typography.Text>
<Input
value={affLink}
onClick={handleAffLinkClick}
readOnly
size="large"
className="!rounded-lg"
prefix={<IconLink />}
/> />
</div> </div>
</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>
</div> </div>
</TabPane> </TabPane>
@@ -805,7 +758,7 @@ const PersonalSetting = () => {
> >
{userState.user && userState.user.email !== '' {userState.user && userState.user.email !== ''
? t('修改绑定') ? t('修改绑定')
: t('绑定邮箱')} : t('绑定')}
</Button> </Button>
</div> </div>
</Card> </Card>

View File

@@ -743,7 +743,6 @@
"无效的用户单独并发限制数据": "Invalid user individual concurrency limit data", "无效的用户单独并发限制数据": "Invalid user individual concurrency limit data",
"未绑定": "Not bound", "未绑定": "Not bound",
"修改绑定": "Modify binding", "修改绑定": "Modify binding",
"绑定邮箱": "Bind email",
"确认新密码": "Confirm new password", "确认新密码": "Confirm new password",
"历史消耗": "Consumption", "历史消耗": "Consumption",
"查看": "Check", "查看": "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.", "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "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", "点击模型名称可复制": "Click the model name to copy",
"管理您的邀请链接和收益": "Manage your invitation link and earnings", "管理您的邀请链接和收益": "Manage your invitation link and earnings",
"模型与邀请": "Model and Invitation", "没有可用模型": "No available models",
"账户绑定": "Account Binding", "账户绑定": "Account Binding",
"安全设置": "Security Settings", "安全设置": "Security Settings",
"系统访问令牌": "System Access Token", "系统访问令牌": "System Access Token",

View File

@@ -6,7 +6,9 @@ import {
showSuccess, showSuccess,
renderQuota, renderQuota,
renderQuotaWithAmount, renderQuotaWithAmount,
stringToColor stringToColor,
copy,
getQuotaPerUnit
} from '../../helpers'; } from '../../helpers';
import { import {
Layout, Layout,
@@ -54,6 +56,11 @@ const TopUp = () => {
const [paymentLoading, setPaymentLoading] = useState(false); const [paymentLoading, setPaymentLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
// 邀请相关状态
const [affLink, setAffLink] = useState('');
const [openTransfer, setOpenTransfer] = useState(false);
const [transferAmount, setTransferAmount] = useState(0);
const getUsername = () => { const getUsername = () => {
if (userState.user) { if (userState.user) {
return userState.user.username; return userState.user.username;
@@ -211,6 +218,44 @@ const TopUp = () => {
setUserDataLoading(false); 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(() => { useEffect(() => {
if (userState?.user?.id) { if (userState?.user?.id) {
setUserDataLoading(false); setUserDataLoading(false);
@@ -218,6 +263,8 @@ const TopUp = () => {
} else { } else {
getUserQuota().then(); getUserQuota().then();
} }
getAffLink().then();
setTransferAmount(getQuotaPerUnit());
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -264,10 +311,58 @@ const TopUp = () => {
setOpen(false); setOpen(false);
}; };
const handleTransferCancel = () => {
setOpenTransfer(false);
};
return ( return (
<div className="bg-gray-50"> <div className="bg-gray-50">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
{/* 划转模态框 */}
<Modal
title={
<div className="flex items-center">
<IconCreditCard className="mr-2" />
{t('请输入要划转的数量')}
</div>
}
visible={openTransfer}
onOk={transfer}
onCancel={handleTransferCancel}
maskClosable={false}
size={'small'}
centered={true}
>
<div className="space-y-4 py-4">
<div>
<Typography.Text strong className="block mb-2">
{t('可用额度')} {renderQuota(userState?.user?.aff_quota)}
</Typography.Text>
<Input
value={userState?.user?.aff_quota}
disabled={true}
size="large"
className="!rounded-lg"
/>
</div>
<div>
<Typography.Text strong className="block mb-2">
{t('划转额度')} {renderQuota(transferAmount)}{' '}
{t('最低') + renderQuota(getQuotaPerUnit())}
</Typography.Text>
<InputNumber
min={0}
value={transferAmount}
onChange={(value) => setTransferAmount(value)}
disabled={false}
size="large"
className="!rounded-lg w-full"
/>
</div>
</div>
</Modal>
<Modal <Modal
title={ title={
<div className="flex items-center"> <div className="flex items-center">
@@ -300,7 +395,7 @@ const TopUp = () => {
</Modal> </Modal>
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-full max-w-4xl"> <div className="w-full">
<Card className="!rounded-2xl shadow-lg border-0"> <Card className="!rounded-2xl shadow-lg border-0">
<Card <Card
className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden" className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden"
@@ -413,7 +508,81 @@ const TopUp = () => {
</Card> </Card>
<div className="p-6"> <div className="p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* 邀请信息部分 */}
<div>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<div className="w-12 h-12 rounded-full bg-orange-50 flex items-center justify-center mr-4">
<IconLink size="large" className="text-orange-500" />
</div>
<div>
<div className="flex items-center gap-3">
<Text className="text-xl font-semibold">{t('邀请信息')}</Text>
<Button
type="primary"
theme="solid"
onClick={() => setOpenTransfer(true)}
size="small"
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600"
icon={<IconCreditCard />}
>
{t('划转')}
</Button>
</div>
<div className="text-gray-500 text-sm">{t('管理您的邀请链接和收益')}</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="text-gray-600 text-xs font-medium">{t('待使用收益')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{renderQuota(userState?.user?.aff_quota)}
</div>
</Card>
<Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="text-gray-600 text-xs font-medium">{t('总收益')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{renderQuota(userState?.user?.aff_history_quota)}
</div>
</Card>
<Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="text-gray-600 text-xs font-medium">{t('邀请人数')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{userState?.user?.aff_count || 0}
</div>
</Card>
</div>
<div className="bg-white rounded-lg p-3">
<Typography.Text strong className="block mb-2 text-sm">{t('邀请链接')}</Typography.Text>
<Input
value={affLink}
onClick={handleAffLinkClick}
readOnly
size="large"
className="!rounded-lg"
prefix={<IconLink />}
/>
</div>
</div>
</div>
<div> <div>
<div className="flex items-center mb-6"> <div className="flex items-center mb-6">
<div className="w-12 h-12 rounded-full bg-green-50 flex items-center justify-center mr-4"> <div className="w-12 h-12 rounded-full bg-green-50 flex items-center justify-center mr-4">