Merge remote-tracking branch 'origin/alpha' into alpha

This commit is contained in:
CaIon
2025-08-09 17:02:20 +08:00
4 changed files with 260 additions and 188 deletions

View File

@@ -28,7 +28,8 @@ import {
Avatar,
Tooltip,
Progress,
Switch,
Popover,
Typography,
Input,
Modal
} from '@douyinfe/semi-ui';
@@ -46,21 +47,22 @@ import {
IconEyeClosed,
} from '@douyinfe/semi-icons';
// progress color helper
const getProgressColor = (pct) => {
if (pct === 100) return 'var(--semi-color-success)';
if (pct <= 10) return 'var(--semi-color-danger)';
if (pct <= 30) return 'var(--semi-color-warning)';
return undefined;
};
// Render functions
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
// Render status column with switch and progress bar
const renderStatus = (text, record, manageToken, t) => {
// Render status column only (no usage)
const renderStatus = (text, record, t) => {
const enabled = text === 1;
const handleToggle = (checked) => {
if (checked) {
manageToken(record.id, 'enable', record);
} else {
manageToken(record.id, 'disable', record);
}
};
let tagColor = 'black';
let tagText = t('未知状态');
@@ -78,69 +80,11 @@ const renderStatus = (text, record, manageToken, t) => {
tagText = t('已耗尽');
}
const used = parseInt(record.used_quota) || 0;
const remain = parseInt(record.remain_quota) || 0;
const total = used + remain;
const percent = total > 0 ? (remain / total) * 100 : 0;
const getProgressColor = (pct) => {
if (pct === 100) return 'var(--semi-color-success)';
if (pct <= 10) return 'var(--semi-color-danger)';
if (pct <= 30) return 'var(--semi-color-warning)';
return undefined;
};
const quotaSuffix = record.unlimited_quota ? (
<div className='text-xs'>{t('无限额度')}</div>
) : (
<div className='flex flex-col items-end'>
<span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
<Progress
percent={percent}
stroke={getProgressColor(percent)}
aria-label='quota usage'
format={() => `${percent.toFixed(0)}%`}
style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
/>
</div>
);
const content = (
<Tag
color={tagColor}
shape='circle'
size='large'
prefixIcon={
<Switch
size='small'
checked={enabled}
onChange={handleToggle}
aria-label='token status switch'
/>
}
suffixIcon={quotaSuffix}
>
return (
<Tag color={tagColor} shape='circle' size='small'>
{tagText}
</Tag>
);
const tooltipContent = record.unlimited_quota ? (
<div className='text-xs'>
<div>{t('已用额度')}: {renderQuota(used)}</div>
</div>
) : (
<div className='text-xs'>
<div>{t('已用额度')}: {renderQuota(used)}</div>
<div>{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)</div>
<div>{t('总额度')}: {renderQuota(total)}</div>
</div>
);
return (
<Tooltip content={tooltipContent}>
{content}
</Tooltip>
);
};
// Render group column
@@ -292,35 +236,81 @@ const renderAllowIps = (text, t) => {
return <Space wrap>{ipTags}</Space>;
};
// Render separate quota usage column
const renderQuotaUsage = (text, record, t) => {
const { Paragraph } = Typography;
const used = parseInt(record.used_quota) || 0;
const remain = parseInt(record.remain_quota) || 0;
const total = used + remain;
if (record.unlimited_quota) {
const popoverContent = (
<div className='text-xs p-2'>
<Paragraph copyable={{ content: renderQuota(used) }}>
{t('已用额度')}: {renderQuota(used)}
</Paragraph>
</div>
);
return (
<Popover content={popoverContent} position='top'>
<Tag color='white' shape='circle'>
{t('无限额度')}
</Tag>
</Popover>
);
}
const percent = total > 0 ? (remain / total) * 100 : 0;
const popoverContent = (
<div className='text-xs p-2'>
<Paragraph copyable={{ content: renderQuota(used) }}>
{t('已用额度')}: {renderQuota(used)}
</Paragraph>
<Paragraph copyable={{ content: renderQuota(remain) }}>
{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)
</Paragraph>
<Paragraph copyable={{ content: renderQuota(total) }}>
{t('总额度')}: {renderQuota(total)}
</Paragraph>
</div>
);
return (
<Popover content={popoverContent} position='top'>
<Tag color='white' shape='circle'>
<div className='flex flex-col items-end'>
<span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
<Progress
percent={percent}
stroke={getProgressColor(percent)}
aria-label='quota usage'
format={() => `${percent.toFixed(0)}%`}
style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
/>
</div>
</Tag>
</Popover>
);
};
// Render operations column
const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit, manageToken, refresh, t) => {
let chats = localStorage.getItem('chats');
let chatsArray = [];
let shouldUseCustom = true;
if (shouldUseCustom) {
try {
chats = JSON.parse(chats);
if (Array.isArray(chats)) {
for (let i = 0; i < chats.length; i++) {
let chat = {};
chat.node = 'item';
for (let key in chats[i]) {
if (chats[i].hasOwnProperty(key)) {
chat.key = i;
chat.name = key;
chat.onClick = () => {
onOpenLink(key, chats[i][key], record);
};
}
}
chatsArray.push(chat);
}
try {
const raw = localStorage.getItem('chats');
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
for (let i = 0; i < parsed.length; i++) {
const item = parsed[i];
const name = Object.keys(item)[0];
if (!name) continue;
chatsArray.push({
node: 'item',
key: i,
name,
onClick: () => onOpenLink(name, item[name], record),
});
}
} catch (e) {
console.log(e);
showError(t('聊天链接配置错误,请联系管理员'));
}
} catch (_) {
showError(t('聊天链接配置错误,请联系管理员'));
}
return (
@@ -338,7 +328,7 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
} else {
onOpenLink(
'default',
chats[0][Object.keys(chats[0])[0]],
chatsArray[0].name ? (parsed => parsed)(localStorage.getItem('chats')) : '',
record,
);
}
@@ -359,6 +349,29 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
</Dropdown>
</SplitButtonGroup>
{record.status === 1 ? (
<Button
type='danger'
size="small"
onClick={async () => {
await manageToken(record.id, 'disable', record);
await refresh();
}}
>
{t('禁用')}
</Button>
) : (
<Button
size="small"
onClick={async () => {
await manageToken(record.id, 'enable', record);
await refresh();
}}
>
{t('启用')}
</Button>
)}
<Button
type='tertiary'
size="small"
@@ -412,7 +425,12 @@ export const getTokensColumns = ({
title: t('状态'),
dataIndex: 'status',
key: 'status',
render: (text, record) => renderStatus(text, record, manageToken, t),
render: (text, record) => renderStatus(text, record, t),
},
{
title: t('剩余额度/总额度'),
key: 'quota_usage',
render: (text, record) => renderQuotaUsage(text, record, t),
},
{
title: t('分组'),

View File

@@ -24,7 +24,8 @@ import {
Tag,
Tooltip,
Progress,
Switch,
Popover,
Typography,
} from '@douyinfe/semi-ui';
import { renderGroup, renderNumber, renderQuota } from '../../../helpers';
@@ -89,7 +90,6 @@ const renderUsername = (text, record) => {
* Render user statistics
*/
const renderStatistics = (text, record, showEnableDisableModal, t) => {
const enabled = record.status === 1;
const isDeleted = record.DeletedAt !== null;
// Determine tag text & color like original status column
@@ -100,60 +100,17 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
tagText = t('已注销');
} else if (record.status === 1) {
tagColor = 'green';
tagText = t('已激活');
tagText = t('已启用');
} else if (record.status === 2) {
tagColor = 'red';
tagText = t('已禁');
tagText = t('已禁');
}
const handleToggle = (checked) => {
if (checked) {
showEnableDisableModal(record, 'enable');
} else {
showEnableDisableModal(record, 'disable');
}
};
const used = parseInt(record.used_quota) || 0;
const remain = parseInt(record.quota) || 0;
const total = used + remain;
const percent = total > 0 ? (remain / total) * 100 : 0;
const getProgressColor = (pct) => {
if (pct === 100) return 'var(--semi-color-success)';
if (pct <= 10) return 'var(--semi-color-danger)';
if (pct <= 30) return 'var(--semi-color-warning)';
return undefined;
};
const quotaSuffix = (
<div className='flex flex-col items-end'>
<span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
<Progress
percent={percent}
stroke={getProgressColor(percent)}
aria-label='quota usage'
format={() => `${percent.toFixed(0)}%`}
style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
/>
</div>
);
const content = (
<Tag
color={tagColor}
shape='circle'
size='large'
prefixIcon={
<Switch
size='small'
checked={enabled}
onChange={handleToggle}
disabled={isDeleted}
aria-label='user status switch'
/>
}
suffixIcon={quotaSuffix}
size='small'
>
{tagText}
</Tag>
@@ -161,9 +118,6 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
const tooltipContent = (
<div className='text-xs'>
<div>{t('已用额度')}: {renderQuota(used)}</div>
<div>{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)</div>
<div>{t('总额度')}: {renderQuota(total)}</div>
<div>{t('调用次数')}: {renderNumber(record.request_count)}</div>
</div>
);
@@ -175,6 +129,43 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
);
};
// Render separate quota usage column
const renderQuotaUsage = (text, record, t) => {
const { Paragraph } = Typography;
const used = parseInt(record.used_quota) || 0;
const remain = parseInt(record.quota) || 0;
const total = used + remain;
const percent = total > 0 ? (remain / total) * 100 : 0;
const popoverContent = (
<div className='text-xs p-2'>
<Paragraph copyable={{ content: renderQuota(used) }}>
{t('已用额度')}: {renderQuota(used)}
</Paragraph>
<Paragraph copyable={{ content: renderQuota(remain) }}>
{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)
</Paragraph>
<Paragraph copyable={{ content: renderQuota(total) }}>
{t('总额度')}: {renderQuota(total)}
</Paragraph>
</div>
);
return (
<Popover content={popoverContent} position='top'>
<Tag color='white' shape='circle'>
<div className='flex flex-col items-end'>
<span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
<Progress
percent={percent}
aria-label='quota usage'
format={() => `${percent.toFixed(0)}%`}
style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
/>
</div>
</Tag>
</Popover>
);
};
/**
* Render invite information
*/
@@ -204,6 +195,7 @@ const renderOperations = (text, record, {
setShowEditUser,
showPromoteModal,
showDemoteModal,
showEnableDisableModal,
showDeleteModal,
t
}) => {
@@ -213,6 +205,22 @@ const renderOperations = (text, record, {
return (
<Space>
{record.status === 1 ? (
<Button
type='danger'
size="small"
onClick={() => showEnableDisableModal(record, 'disable')}
>
{t('禁用')}
</Button>
) : (
<Button
size="small"
onClick={() => showEnableDisableModal(record, 'enable')}
>
{t('启用')}
</Button>
)}
<Button
type='tertiary'
size="small"
@@ -270,6 +278,16 @@ export const getUsersColumns = ({
dataIndex: 'username',
render: (text, record) => renderUsername(text, record),
},
{
title: t('状态'),
dataIndex: 'info',
render: (text, record, index) => renderStatistics(text, record, showEnableDisableModal, t),
},
{
title: t('剩余额度/总额度'),
key: 'quota_usage',
render: (text, record) => renderQuotaUsage(text, record, t),
},
{
title: t('分组'),
dataIndex: 'group',
@@ -284,11 +302,6 @@ export const getUsersColumns = ({
return <div>{renderRole(text, t)}</div>;
},
},
{
title: t('状态'),
dataIndex: 'info',
render: (text, record, index) => renderStatistics(text, record, showEnableDisableModal, t),
},
{
title: t('邀请信息'),
dataIndex: 'invite',