🎨 refactor(TokensTable): refactor TokensTable UI & UX for clearer data and inline actions

This commit overhauls the `TokensTable` component to deliver a cleaner, more intuitive experience.

Key changes
1. Quota
   • Merged “Used” & “Remaining” into a single “Quota” column.
   • Uses a circular `Progress` with %-label; full details shown on tooltip.

2. Status
   • Tag now embeds a small `Switch` (prefixIcon) to enable/disable a token in-place.
   • Removed enable/disable actions from the old dropdown.

3. Columns & layout
   • Added dedicated “Group” column (moved from Status).
   • Added “Key” column:
     – Read-only `Input` styled like Home page base-URL field.
     – Masked value (`sk-abc********xyz`) by default.
     – Eye button toggles reveal/hide; Copy button copies full key (without modal).
   • Dropped “More” menu; Delete is now a direct button in the action area.

4. Model limits
   • Shows vendor icons inside an `AvatarGroup`; tooltip lists the exact models.

5. IP restriction
   • Displays first IP, extra count as “+N” Tag with tooltip.
   • Unlimited shows white Tag.

6. Cleanup / misc.
   • Removed unused helpers (`getQuotaPerUnit`), icons (`IconMore`, eye/copy duplicates, etc.).
   • Replaced legacy modal view of key with inline input behaviour.
   • Tweaked paddings, themes, sizes to align with design system.

BREAKING CHANGE: Table column order & names have changed; update any tests or docs referencing the old structure.
This commit is contained in:
t0ng7u
2025-07-12 03:35:19 +08:00
parent cf711d55a5
commit 7c4b83a430
14 changed files with 394 additions and 311 deletions

View File

@@ -34,7 +34,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Temperature
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.temperature}
</Tag>
</div>
@@ -70,7 +70,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Top P
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.top_p}
</Tag>
</div>
@@ -106,7 +106,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Frequency Penalty
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.frequency_penalty}
</Tag>
</div>
@@ -142,7 +142,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Presence Penalty
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.presence_penalty}
</Tag>
</div>

View File

@@ -118,25 +118,25 @@ const ChannelSelectorModal = forwardRef(({
switch (status) {
case 1:
return (
<Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知状态')}
</Tag>
);

View File

@@ -63,7 +63,6 @@ const ChannelsTable = () => {
}
return (
<Tag
size='large'
color={type2label[type]?.color}
shape='circle'
prefixIcon={getChannelIcon(type)}
@@ -77,7 +76,6 @@ const ChannelsTable = () => {
return (
<Tag
color='light-blue'
size='large'
shape='circle'
type='light'
>
@@ -90,25 +88,25 @@ const ChannelsTable = () => {
switch (status) {
case 1:
return (
<Tag size='large' color='green' shape='circle'>
<Tag color='green' shape='circle'>
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size='large' color='red' shape='circle'>
<Tag color='red' shape='circle'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size='large' color='yellow' shape='circle'>
<Tag color='yellow' shape='circle'>
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size='large' color='grey' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未知状态')}
</Tag>
);
@@ -120,31 +118,31 @@ const ChannelsTable = () => {
time = time.toFixed(2) + t(' 秒');
if (responseTime === 0) {
return (
<Tag size='large' color='grey' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未测试')}
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size='large' color='green' shape='circle'>
<Tag color='green' shape='circle'>
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size='large' color='lime' shape='circle'>
<Tag color='lime' shape='circle'>
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size='large' color='yellow' shape='circle'>
<Tag color='yellow' shape='circle'>
{time}
</Tag>
);
} else {
return (
<Tag size='large' color='red' shape='circle'>
<Tag color='red' shape='circle'>
{time}
</Tag>
);
@@ -331,7 +329,7 @@ const ChannelsTable = () => {
<div>
<Space spacing={1}>
<Tooltip content={t('已用额度')}>
<Tag color='white' type='ghost' size='large' shape='circle'>
<Tag color='white' type='ghost' shape='circle'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
@@ -339,7 +337,6 @@ const ChannelsTable = () => {
<Tag
color='white'
type='ghost'
size='large'
shape='circle'
onClick={() => updateChannelBalance(record)}
>
@@ -352,7 +349,7 @@ const ChannelsTable = () => {
} else {
return (
<Tooltip content={t('已用额度')}>
<Tag color='white' type='ghost' size='large' shape='circle'>
<Tag color='white' type='ghost' shape='circle'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
@@ -1240,7 +1237,7 @@ const ChannelsTable = () => {
tab={
<span className="flex items-center gap-2">
{t('全部')}
<Tag color={activeTypeKey === 'all' ? 'red' : 'grey'} size='small' shape='circle'>
<Tag color={activeTypeKey === 'all' ? 'red' : 'grey'} shape='circle'>
{channelTypeCounts['all'] || 0}
</Tag>
</span>
@@ -1258,7 +1255,7 @@ const ChannelsTable = () => {
<span className="flex items-center gap-2">
{getChannelIcon(option.value)}
{option.label}
<Tag color={activeTypeKey === key ? 'red' : 'grey'} size='small' shape='circle'>
<Tag color={activeTypeKey === key ? 'red' : 'grey'} shape='circle'>
{count}
</Tag>
</span>
@@ -1461,7 +1458,7 @@ const ChannelsTable = () => {
const fixChannelsAbilities = async () => {
const res = await API.post(`/api/channel/fix`);
const { success, message, data } = res.data;
const { success, message, data } = res.data;
if (success) {
showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails));
await refresh();
@@ -2033,7 +2030,7 @@ const ChannelsTable = () => {
if (isTesting) {
return (
<Tag size='large' color='blue' shape='circle'>
<Tag color='blue' shape='circle'>
{t('测试中')}
</Tag>
);
@@ -2041,7 +2038,7 @@ const ChannelsTable = () => {
if (!testResult) {
return (
<Tag size='large' color='grey' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未开始')}
</Tag>
);
@@ -2050,7 +2047,6 @@ const ChannelsTable = () => {
return (
<div className="flex items-center gap-2">
<Tag
size='large'
color={testResult.success ? 'green' : 'red'}
shape='circle'
>

View File

@@ -78,37 +78,37 @@ const LogsTable = () => {
switch (type) {
case 1:
return (
<Tag color='cyan' size='large' shape='circle'>
<Tag color='cyan' shape='circle'>
{t('充值')}
</Tag>
);
case 2:
return (
<Tag color='lime' size='large' shape='circle'>
<Tag color='lime' shape='circle'>
{t('消费')}
</Tag>
);
case 3:
return (
<Tag color='orange' size='large' shape='circle'>
<Tag color='orange' shape='circle'>
{t('管理')}
</Tag>
);
case 4:
return (
<Tag color='purple' size='large' shape='circle'>
<Tag color='purple' shape='circle'>
{t('系统')}
</Tag>
);
case 5:
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{t('错误')}
</Tag>
);
default:
return (
<Tag color='grey' size='large' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未知')}
</Tag>
);
@@ -118,13 +118,13 @@ const LogsTable = () => {
function renderIsStream(bool) {
if (bool) {
return (
<Tag color='blue' size='large' shape='circle'>
<Tag color='blue' shape='circle'>
{t('流')}
</Tag>
);
} else {
return (
<Tag color='purple' size='large' shape='circle'>
<Tag color='purple' shape='circle'>
{t('非流')}
</Tag>
);
@@ -135,21 +135,21 @@ const LogsTable = () => {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='green' size='large' shape='circle'>
<Tag color='green' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 300) {
return (
<Tag color='orange' size='large' shape='circle'>
<Tag color='orange' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{' '}
{time} s{' '}
</Tag>
@@ -162,21 +162,21 @@ const LogsTable = () => {
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='green' size='large' shape='circle'>
<Tag color='green' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 10) {
return (
<Tag color='orange' size='large' shape='circle'>
<Tag color='orange' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{' '}
{time} s{' '}
</Tag>
@@ -363,7 +363,6 @@ const LogsTable = () => {
<Tooltip content={record.channel_name || '[未知]'}>
<Tag
color={colors[parseInt(text) % colors.length]}
size='large'
shape='circle'
>
{' '}
@@ -415,7 +414,6 @@ const LogsTable = () => {
<div>
<Tag
color='grey'
size='large'
shape='circle'
onClick={(event) => {
//cancel the row click event
@@ -567,7 +565,6 @@ const LogsTable = () => {
<Tooltip content={text}>
<Tag
color='orange'
size='large'
shape='circle'
onClick={(event) => {
copyText(event, text);

View File

@@ -185,115 +185,115 @@ const LogsTable = () => {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Palette size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
{t('绘图')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<ZoomIn size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
{t('放大')}
</Tag>
);
case 'VIDEO':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('视频')}
</Tag>
);
case 'EDITS':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('编辑')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('变换')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('强变换')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('弱变换')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large' shape='circle' prefixIcon={<Move size={14} />}>
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
{t('平移')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
{t('图生文')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large' shape='circle' prefixIcon={<Blend size={14} />}>
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
{t('图混合')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Upload size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large' shape='circle' prefixIcon={<Minimize2 size={14} />}>
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
{t('缩词')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large' shape='circle' prefixIcon={<RotateCcw size={14} />}>
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
{t('重绘')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large' shape='circle' prefixIcon={<PaintBucket size={14} />}>
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
{t('局部重绘-提交')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large' shape='circle' prefixIcon={<Focus size={14} />}>
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
{t('变焦')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large' shape='circle' prefixIcon={<Move3D size={14} />}>
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
{t('自定义变焦-提交')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<Monitor size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
{t('窗口处理')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large' shape='circle' prefixIcon={<UserCheck size={14} />}>
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
{t('换脸')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -304,31 +304,31 @@ const LogsTable = () => {
switch (code) {
case 1:
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('已提交')}
</Tag>
);
case 21:
return (
<Tag color='lime' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
{t('等待中')}
</Tag>
);
case 22:
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Copy size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
{t('重复提交')}
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<FileX size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
{t('未提交')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -339,43 +339,43 @@ const LogsTable = () => {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('失败')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<AlertCircle size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('窗口等待')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -405,7 +405,7 @@ const LogsTable = () => {
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
{durationSec} {t('秒')}
</Tag>
);
@@ -439,7 +439,6 @@ const LogsTable = () => {
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
size='large'
shape='circle'
prefixIcon={<Hash size={14} />}
onClick={() => {

View File

@@ -76,13 +76,13 @@ const ModelPricing = () => {
switch (type) {
case 1:
return (
<Tag color='teal' size='large' shape='circle'>
<Tag color='teal' shape='circle'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag color='violet' size='large' shape='circle'>
<Tag color='violet' shape='circle'>
{t('按量计费')}
</Tag>
);
@@ -116,7 +116,6 @@ const ModelPricing = () => {
<Tag
key={endpoint}
color={stringToColor(endpoint)}
size='large'
shape='circle'
>
{endpoint}
@@ -179,7 +178,7 @@ const ModelPricing = () => {
if (usableGroup[group]) {
if (group === selectedGroup) {
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<IconVerify />}>
<Tag color='blue' shape='circle' prefixIcon={<IconVerify />}>
{group}
</Tag>
);
@@ -187,7 +186,6 @@ const ModelPricing = () => {
return (
<Tag
color='blue'
size='large'
shape='circle'
onClick={() => {
setSelectedGroup(group);
@@ -392,7 +390,6 @@ const ModelPricing = () => {
{category.label}
<Tag
color={activeKey === key ? 'red' : 'grey'}
size='small'
shape='circle'
>
{modelCount}
@@ -436,7 +433,6 @@ const ModelPricing = () => {
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
size="large"
/>
</div>
<Button
@@ -446,7 +442,6 @@ const ModelPricing = () => {
onClick={() => copyText(selectedRowKeys)}
disabled={selectedRowKeys.length === 0}
className="!bg-blue-500 hover:!bg-blue-600 text-white"
size="large"
>
{t('复制选中模型')}
</Button>

View File

@@ -53,31 +53,31 @@ const RedemptionsTable = () => {
const renderStatus = (status, record) => {
if (isExpired(record)) {
return (
<Tag color='orange' size='large' shape='circle'>{t('已过期')}</Tag>
<Tag color='orange' shape='circle'>{t('已过期')}</Tag>
);
}
switch (status) {
case 1:
return (
<Tag color='green' size='large' shape='circle'>
<Tag color='green' shape='circle'>
{t('未使用')}
</Tag>
);
case 2:
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large' shape='circle'>
<Tag color='grey' shape='circle'>
{t('已使用')}
</Tag>
);
default:
return (
<Tag color='black' size='large' shape='circle'>
<Tag color='black' shape='circle'>
{t('未知状态')}
</Tag>
);
@@ -107,7 +107,7 @@ const RedemptionsTable = () => {
render: (text, record, index) => {
return (
<div>
<Tag size={'large'} color={'grey'} shape='circle'>
<Tag color='grey' shape='circle'>
{renderQuota(parseInt(text))}
</Tag>
</div>

View File

@@ -106,7 +106,7 @@ function renderDuration(submit_time, finishTime) {
// 返回带有样式的颜色标签
return (
<Tag color={color} size='large' prefixIcon={<Clock size={14} />}>
<Tag color={color} prefixIcon={<Clock size={14} />}>
{durationSec}
</Tag>
);
@@ -198,31 +198,31 @@ const LogsTable = () => {
switch (type) {
case 'MUSIC':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Music size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Music size={14} />}>
{t('生成音乐')}
</Tag>
);
case 'LYRICS':
return (
<Tag color='pink' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
<Tag color='pink' shape='circle' prefixIcon={<FileText size={14} />}>
{t('生成歌词')}
</Tag>
);
case TASK_ACTION_GENERATE:
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Sparkles size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
{t('图生视频')}
</Tag>
);
case TASK_ACTION_TEXT_GENERATE:
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Sparkles size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
{t('文生视频')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -233,25 +233,25 @@ const LogsTable = () => {
switch (platform) {
case 'suno':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<Music size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<Music size={14} />}>
Suno
</Tag>
);
case 'kling':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
Kling
</Tag>
);
case 'jimeng':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Video size={14} />}>
Jimeng
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -262,55 +262,55 @@ const LogsTable = () => {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Play size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Play size={14} />}>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('失败')}
</Tag>
);
case 'QUEUED':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<List size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<List size={14} />}>
{t('排队中')}
</Tag>
);
case 'UNKNOWN':
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
case '':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Loader size={14} />}>
{t('正在提交')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);

View File

@@ -7,7 +7,7 @@ import {
timestamp2string,
renderGroup,
renderQuota,
getQuotaPerUnit
getModelCategories
} from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import {
@@ -22,6 +22,12 @@ import {
SplitButtonGroup,
Table,
Tag,
AvatarGroup,
Avatar,
Tooltip,
Progress,
Switch,
Input,
Typography
} from '@douyinfe/semi-ui';
import {
@@ -31,7 +37,10 @@ import {
import {
IconSearch,
IconTreeTriangleDown,
IconMore,
IconCopy,
IconEyeOpened,
IconEyeClosed,
IconBolt,
} from '@douyinfe/semi-icons';
import { Key } from 'lucide-react';
import EditToken from '../../pages/Token/EditToken';
@@ -47,49 +56,6 @@ function renderTimestamp(timestamp) {
const TokensTable = () => {
const { t } = useTranslation();
const renderStatus = (status, model_limits_enabled = false) => {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large' shape='circle' >
{t('已启用:限制模型')}
</Tag>
);
} else {
return (
<Tag color='green' size='large' shape='circle' >
{t('已启用')}
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large' shape='circle' >
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large' shape='circle' >
{t('已过期')}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large' shape='circle' >
{t('已耗尽')}
</Tag>
);
default:
return (
<Tag color='black' size='large' shape='circle' >
{t('未知状态')}
</Tag>
);
}
};
const columns = [
{
title: t('名称'),
@@ -99,66 +65,249 @@ const TokensTable = () => {
title: t('状态'),
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
return (
<div>
<Space>
{renderStatus(text, record.model_limits_enabled)}
{renderGroup(record.group)}
</Space>
</div>
);
},
},
{
title: t('已用额度'),
dataIndex: 'used_quota',
render: (text, record, index) => {
return (
<div>
<Tag size={'large'} color={'grey'} shape='circle' >
{renderQuota(parseInt(text))}
</Tag>
</div>
);
},
},
{
title: t('剩余额度'),
dataIndex: 'remain_quota',
render: (text, record, index) => {
const getQuotaColor = (quotaValue) => {
const quotaPerUnit = getQuotaPerUnit();
const dollarAmount = quotaValue / quotaPerUnit;
if (dollarAmount <= 0) {
return 'red';
} else if (dollarAmount <= 100) {
return 'yellow';
render: (text, record) => {
const enabled = text === 1;
const handleToggle = (checked) => {
if (checked) {
manageToken(record.id, 'enable', record);
} else {
return 'green';
manageToken(record.id, 'disable', record);
}
};
let tagColor = 'black';
let tagText = t('未知状态');
if (enabled) {
tagColor = 'green';
tagText = t('已启用');
} else if (text === 2) {
tagColor = 'red';
tagText = t('已禁用');
} else if (text === 3) {
tagColor = 'yellow';
tagText = t('已过期');
} else if (text === 4) {
tagColor = 'grey';
tagText = t('已耗尽');
}
return (
<div>
{record.unlimited_quota ? (
<Tag size={'large'} color={'white'} shape='circle' >
{t('无限制')}
</Tag>
) : (
<Tag
size={'large'}
color={getQuotaColor(parseInt(text))}
shape='circle'
>
{renderQuota(parseInt(text))}
</Tag>
)}
<Tag
color={tagColor}
shape='circle'
prefixIcon={
<Switch
size='small'
checked={enabled}
onChange={handleToggle}
aria-label='token status switch'
/>
}
>
{tagText}
</Tag>
);
},
},
{
title: t('分组'),
dataIndex: 'group',
key: 'group',
render: (text) => {
if (text === 'auto') {
return (
<Tooltip
content={t('当前分组为 auto会自动选择最优分组当一个组不可用时自动降级到下一个组熔断机制')}
position='top'
>
<Tag color='blue' shape='circle' prefixIcon={<IconBolt />}> {t('智能熔断')} </Tag>
</Tooltip>
);
}
return renderGroup(text);
},
},
{
title: t('密钥'),
key: 'token_key',
render: (text, record) => {
const fullKey = 'sk-' + record.key;
const maskedKey = 'sk-' + record.key.slice(0, 3) + '********' + record.key.slice(-3);
const revealed = !!showKeys[record.id];
return (
<div className='w-[200px]'>
<Input
readOnly
value={revealed ? fullKey : maskedKey}
size='small'
suffix={
<div className='flex items-center'>
<Button
theme='borderless'
size='small'
type='tertiary'
icon={revealed ? <IconEyeClosed /> : <IconEyeOpened />}
aria-label='toggle token visibility'
onClick={(e) => {
e.stopPropagation();
setShowKeys(prev => ({ ...prev, [record.id]: !revealed }));
}}
/>
<Button
theme='borderless'
size='small'
type='tertiary'
icon={<IconCopy />}
aria-label='copy token key'
onClick={async (e) => {
e.stopPropagation();
await copyText(fullKey);
}}
/>
</div>
}
/>
</div>
);
},
},
{
title: t('额度'),
key: 'quota',
render: (text, record) => {
if (record.unlimited_quota) {
return <Tag color='white' shape='circle'>{t('无限制')}</Tag>;
}
const used = parseInt(record.used_quota) || 0;
const remain = parseInt(record.remain_quota) || 0;
const total = used + remain;
const percent = total > 0 ? (used / total) * 100 : 0;
const getProgressColor = (pct) => {
if (pct >= 90) return 'var(--semi-color-danger)';
if (pct >= 70) return 'var(--semi-color-warning)';
return undefined; // default color
};
return (
<Tooltip
content={
<div className='text-xs'>
<div>{t('已用额度')}: {renderQuota(used)}</div>
<div>{t('剩余额度')}: {renderQuota(remain)}</div>
<div>{t('总额度')}: {renderQuota(total)}</div>
</div>
}
>
<div className='w-[30px]'>
<Progress
percent={percent}
stroke={getProgressColor(percent)}
showInfo
aria-label='quota usage'
format={percent => <span className="text-xs">{percent.toFixed(0)}%</span>}
type="circle"
width={30}
/>
</div>
</Tooltip>
);
},
},
{
title: t('可用模型'),
dataIndex: 'model_limits',
render: (text, record) => {
if (record.model_limits_enabled && text) {
const models = text.split(',').filter(Boolean);
const categories = getModelCategories(t);
const vendorAvatars = [];
Object.entries(categories).forEach(([key, category]) => {
if (key === 'all') return;
if (!category.icon || !category.filter) return;
const vendorModels = models.filter((m) => category.filter({ model_name: m }));
if (vendorModels.length > 0) {
vendorAvatars.push(
<Tooltip key={key} content={vendorModels.join(', ')} position='top' showArrow>
<Avatar size='extra-extra-small' alt={category.label} color='transparent'>
{category.icon}
</Avatar>
</Tooltip>
);
}
});
if (vendorAvatars.length === 0) {
vendorAvatars.push(
<Tooltip key='default' content={models.join(', ')} position='top' showArrow>
<Avatar size='extra-extra-small' alt='models'>
{models[0].slice(0, 2).toUpperCase()}
</Avatar>
</Tooltip>
);
}
return (
<AvatarGroup size='extra-extra-small'>
{vendorAvatars}
</AvatarGroup>
);
} else {
return (
<Tag color='white' shape='circle'>
{t('无限制')}
</Tag>
);
}
},
},
{
title: t('IP限制'),
dataIndex: 'allow_ips',
render: (text) => {
if (!text || text.trim() === '') {
return (
<Tag color='white' shape='circle'>
{t('无限制')}
</Tag>
);
}
const ips = text
.split('\n')
.map((ip) => ip.trim())
.filter(Boolean);
const displayIps = ips.slice(0, 1);
const extraCount = ips.length - displayIps.length;
const ipTags = displayIps.map((ip, idx) => (
<Tag key={idx} shape='circle'>
{ip}
</Tag>
));
if (extraCount > 0) {
ipTags.push(
<Tooltip
key='extra'
content={ips.slice(2).join(', ')}
position='top'
showArrow
>
<Tag shape='circle'>
{'+' + extraCount}
</Tag>
</Tooltip>
);
}
return <Space wrap>{ipTags}</Space>;
},
},
{
title: t('创建时间'),
dataIndex: 'created_time',
@@ -211,58 +360,6 @@ const TokensTable = () => {
}
}
// 创建更多操作的下拉菜单项
const moreMenuItems = [
{
node: 'item',
name: t('查看'),
onClick: () => {
Modal.info({
title: t('令牌详情'),
content: 'sk-' + record.key,
size: 'large',
});
},
},
{
node: 'item',
name: t('删除'),
type: 'danger',
onClick: () => {
Modal.confirm({
title: t('确定是否要删除此令牌?'),
content: t('此修改将不可逆'),
onOk: () => {
manageToken(record.id, 'delete', record).then(() => {
removeRecord(record.key);
});
},
});
},
}
];
// 动态添加启用/禁用按钮
if (record.status === 1) {
moreMenuItems.push({
node: 'item',
name: t('禁用'),
type: 'warning',
onClick: () => {
manageToken(record.id, 'disable', record);
},
});
} else {
moreMenuItems.push({
node: 'item',
name: t('启用'),
type: 'secondary',
onClick: () => {
manageToken(record.id, 'enable', record);
},
});
}
return (
<Space wrap>
<SplitButtonGroup
@@ -304,17 +401,6 @@ const TokensTable = () => {
</Dropdown>
</SplitButtonGroup>
<Button
theme='light'
type='secondary'
size="small"
onClick={async (text) => {
await copyText('sk-' + record.key);
}}
>
{t('复制')}
</Button>
<Button
theme='light'
type='tertiary'
@@ -327,18 +413,24 @@ const TokensTable = () => {
{t('编辑')}
</Button>
<Dropdown
trigger='click'
position='bottomRight'
menu={moreMenuItems}
<Button
theme='light'
type='danger'
size="small"
onClick={() => {
Modal.confirm({
title: t('确定是否要删除此令牌?'),
content: t('此修改将不可逆'),
onOk: () => {
manageToken(record.id, 'delete', record).then(() => {
removeRecord(record.key);
});
},
});
}}
>
<Button
icon={<IconMore />}
theme='light'
type='tertiary'
size="small"
/>
</Dropdown>
{t('删除')}
</Button>
</Space>
);
},
@@ -357,6 +449,7 @@ const TokensTable = () => {
id: undefined,
});
const [compactMode, setCompactMode] = useTableCompactMode('tokens');
const [showKeys, setShowKeys] = useState({});
// Form 初始值
const formInitValues = {

View File

@@ -54,25 +54,25 @@ const UsersTable = () => {
switch (role) {
case 1:
return (
<Tag size='large' color='blue' shape='circle' prefixIcon={<User size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<User size={14} />}>
{t('普通用户')}
</Tag>
);
case 10:
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<Shield size={14} />}>
{t('管理员')}
</Tag>
);
case 100:
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Crown size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Crown size={14} />}>
{t('超级管理员')}
</Tag>
);
default:
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知身份')}
</Tag>
);
@@ -82,16 +82,16 @@ const UsersTable = () => {
const renderStatus = (status) => {
switch (status) {
case 1:
return <Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
return <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
case 2:
return (
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('已封禁')}
</Tag>
);
default:
return (
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知状态')}
</Tag>
);
@@ -117,7 +117,7 @@ const UsersTable = () => {
<Space spacing={2}>
<span>{text}</span>
<Tooltip content={remark} position="top" showArrow>
<Tag color='white' size='large' shape='circle' className="!text-xs">
<Tag color='white' shape='circle' className="!text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 flex-shrink-0 rounded-full" style={{ backgroundColor: '#10b981' }} />
{displayRemark}
@@ -142,13 +142,13 @@ const UsersTable = () => {
return (
<div>
<Space spacing={1}>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
{t('剩余')}: {renderQuota(record.quota)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
{t('已用')}: {renderQuota(record.used_quota)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
{t('调用')}: {renderNumber(record.request_count)}
</Tag>
</Space>
@@ -163,13 +163,13 @@ const UsersTable = () => {
return (
<div>
<Space spacing={1}>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Users size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Users size={14} />}>
{t('邀请')}: {renderNumber(record.aff_count)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<DollarSign size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<DollarSign size={14} />}>
{t('收益')}: {renderQuota(record.aff_history_quota)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
{record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
</Tag>
</Space>

View File

@@ -539,7 +539,7 @@ export function stringToColor(str) {
export function renderModelTag(modelName, options = {}) {
const {
color,
size = 'large',
size = 'default',
shape = 'circle',
onClick,
suffixIcon,
@@ -584,7 +584,7 @@ export function renderText(text, limit) {
export function renderGroup(group) {
if (group === '') {
return (
<Tag size='large' key='default' color='orange' shape='circle'>
<Tag key='default' color='orange' shape='circle'>
{i18next.t('用户分组')}
</Tag>
);
@@ -603,7 +603,6 @@ export function renderGroup(group) {
<span key={group}>
{groups.map((group) => (
<Tag
size='large'
color={tagColors[group] || stringToColor(group)}
key={group}
shape='circle'

View File

@@ -373,6 +373,9 @@
"搜索令牌的名称 ...": "Search for the name of the token...",
"已用额度": "Quota used",
"剩余额度": "Remaining quota",
"总额度": "Total quota",
"智能熔断": "Smart fallback",
"当前分组为 auto会自动选择最优分组当一个组不可用时自动降级到下一个组熔断机制": "The current group is auto, it will automatically select the optimal group, and automatically downgrade to the next group when a group is unavailable (breakage mechanism)",
"过期时间": "Expiration time",
"无": "None",
"无限制": "Unlimited",
@@ -962,6 +965,7 @@
"启用突发备用号池(建议勾选,极大降低故障率)": "Enable burst backup number pool (it is recommended to check this box to greatly reduce the failure rate)",
"查看说明": "View instructions",
"添加令牌": "Create token",
"IP限制": "IP restrictions",
"令牌纬度控制 Midjouney 配置,设置优先级:令牌 > 路径参数 > 系统默认": "Token latitude controls Midjouney configuration, setting priority: token > path parameter > system default",
"启用速率限制": "Enable rate limiting",
"复制BaseURL": "Copy BaseURL",

View File

@@ -1381,7 +1381,7 @@ const Detail = (props) => {
<div className="flex items-center gap-2">
<Bell size={16} />
{t('系统公告')}
<Tag size="small" color="grey" shape="circle">
<Tag color="grey" shape="circle">
{t('显示最新20条')}
</Tag>
</div>

View File

@@ -183,7 +183,7 @@ const Setup = () => {
title={
<div className="flex items-center">
<span className="font-medium">{t('数据库警告')}</span>
<Tag color='orange' size='small' className="ml-2 !rounded-full">
<Tag color='orange' shape='circle' className="ml-2">
SQLite
</Tag>
</div>
@@ -222,7 +222,7 @@ const Setup = () => {
title={
<div className="flex items-center">
<span className="font-medium">{t('数据库信息')}</span>
<Tag color='blue' size='small' className="ml-2 !rounded-full">
<Tag color='blue' shape='circle' className="ml-2">
MySQL
</Tag>
</div>
@@ -256,7 +256,7 @@ const Setup = () => {
title={
<div className="flex items-center">
<span className="font-medium">{t('数据库信息')}</span>
<Tag color='green' size='small' className="ml-2 !rounded-full">
<Tag color='green' shape='circle' className="ml-2">
PostgreSQL
</Tag>
</div>
@@ -425,7 +425,7 @@ const Setup = () => {
<div className="flex-1">
<div className="font-medium text-gray-900 mb-1">{t('对外运营模式')}</div>
<div className="text-sm text-gray-500">{t('适用于为多个用户提供服务的场景')}</div>
<Tag color='blue' size='small' className="!rounded-full mt-2">
<Tag color='blue' shape='circle' className="mt-2">
{t('默认模式')}
</Tag>
</div>
@@ -443,7 +443,7 @@ const Setup = () => {
<div className="flex-1">
<div className="font-medium text-gray-900 mb-1">{t('自用模式')}</div>
<div className="text-sm text-gray-500">{t('适用于个人使用的场景,不需要设置模型价格')}</div>
<Tag color='green' size='small' className="!rounded-full mt-2">
<Tag color='green' shape='circle' className="mt-2">
{t('无需计费')}
</Tag>
</div>
@@ -461,7 +461,7 @@ const Setup = () => {
<div className="flex-1">
<div className="font-medium text-gray-900 mb-1">{t('演示站点模式')}</div>
<div className="text-sm text-gray-500">{t('适用于展示系统功能的场景,提供基础功能演示')}</div>
<Tag color='purple' size='small' className="!rounded-full mt-2">
<Tag color='purple' shape='circle' className="mt-2">
{t('演示体验')}
</Tag>
</div>
@@ -522,8 +522,8 @@ const Setup = () => {
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
<div className="mt-3">
<Tag color='blue' className="!rounded-full mr-2">{t('计费模式')}</Tag>
<Tag color='blue' className="!rounded-full">{t('多用户支持')}</Tag>
<Tag color='blue' shape='circle' className="mr-2">{t('计费模式')}</Tag>
<Tag color='blue' shape='circle'>{t('多用户支持')}</Tag>
</div>
</div>
</div>
@@ -542,8 +542,8 @@ const Setup = () => {
<p>{t('适用于个人使用的场景。')}</p>
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
<div className="mt-3">
<Tag color='green' className="!rounded-full mr-2">{t('无需计费')}</Tag>
<Tag color='green' className="!rounded-full">{t('个人使用')}</Tag>
<Tag color='green' shape='circle' className="mr-2">{t('无需计费')}</Tag>
<Tag color='green' shape='circle'>{t('个人使用')}</Tag>
</div>
</div>
</div>
@@ -562,8 +562,8 @@ const Setup = () => {
<p>{t('适用于展示系统功能的场景。')}</p>
<p>{t('提供基础功能演示,方便用户了解系统特性。')}</p>
<div className="mt-3">
<Tag color='purple' className="!rounded-full mr-2">{t('功能演示')}</Tag>
<Tag color='purple' className="!rounded-full">{t('体验试用')}</Tag>
<Tag color='purple' shape='circle' className="mr-2">{t('功能演示')}</Tag>
<Tag color='purple' shape='circle'>{t('体验试用')}</Tag>
</div>
</div>
</div>