✨ style(ui): Replace switches with buttons; add quota column with Popover; cleanup
- Tokens/Users tables: - Replaced status Switch with explicit Enable/Disable buttons in the operation column - Unified button styles with Channels/Models (Disable: danger + small; Enable: default + small) - Status column now shows a small Tag only; standardized labels (Enabled/Disabled/etc.); removed usage info - New "Remaining/Total Quota" column: - Wrapped in a white Tag; shows Remaining/Total with a progress bar - Replaced Tooltip with Popover; contents use Typography.Paragraph with copyable values - Copyable content excludes percentages (only numeric quota values are copied) - Added padding to Popover content for better readability - Tokens specifics: - For unlimited quota, show a white Tag "Unlimited quota" with a Popover that displays copyable "Used quota" - Cleanup: - Removed Switch imports/handlers and unused code paths - Eliminated console logs and redundant flags; simplified chats parsing - Removed quota calculations from status renderers Files: - web/src/components/table/tokens/TokensColumnDefs.js - web/src/components/table/users/UsersColumnDefs.js
This commit is contained in:
@@ -28,7 +28,8 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Progress,
|
Progress,
|
||||||
Switch,
|
Popover,
|
||||||
|
Typography,
|
||||||
Input,
|
Input,
|
||||||
Modal
|
Modal
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
@@ -46,21 +47,22 @@ import {
|
|||||||
IconEyeClosed,
|
IconEyeClosed,
|
||||||
} from '@douyinfe/semi-icons';
|
} 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
|
// Render functions
|
||||||
function renderTimestamp(timestamp) {
|
function renderTimestamp(timestamp) {
|
||||||
return <>{timestamp2string(timestamp)}</>;
|
return <>{timestamp2string(timestamp)}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render status column with switch and progress bar
|
// Render status column only (no usage)
|
||||||
const renderStatus = (text, record, manageToken, t) => {
|
const renderStatus = (text, record, t) => {
|
||||||
const enabled = text === 1;
|
const enabled = text === 1;
|
||||||
const handleToggle = (checked) => {
|
|
||||||
if (checked) {
|
|
||||||
manageToken(record.id, 'enable', record);
|
|
||||||
} else {
|
|
||||||
manageToken(record.id, 'disable', record);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tagColor = 'black';
|
let tagColor = 'black';
|
||||||
let tagText = t('未知状态');
|
let tagText = t('未知状态');
|
||||||
@@ -78,69 +80,11 @@ const renderStatus = (text, record, manageToken, t) => {
|
|||||||
tagText = t('已耗尽');
|
tagText = t('已耗尽');
|
||||||
}
|
}
|
||||||
|
|
||||||
const used = parseInt(record.used_quota) || 0;
|
return (
|
||||||
const remain = parseInt(record.remain_quota) || 0;
|
<Tag color={tagColor} shape='circle' size='small'>
|
||||||
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}
|
|
||||||
>
|
|
||||||
{tagText}
|
{tagText}
|
||||||
</Tag>
|
</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
|
// Render group column
|
||||||
@@ -292,13 +236,65 @@ const renderAllowIps = (text, t) => {
|
|||||||
return <Space wrap>{ipTags}</Space>;
|
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
|
// Render operations column
|
||||||
const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit, manageToken, refresh, t) => {
|
const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit, manageToken, refresh, t) => {
|
||||||
let chats = localStorage.getItem('chats');
|
let chats = localStorage.getItem('chats');
|
||||||
let chatsArray = [];
|
let chatsArray = [];
|
||||||
let shouldUseCustom = true;
|
if (true) {
|
||||||
|
|
||||||
if (shouldUseCustom) {
|
|
||||||
try {
|
try {
|
||||||
chats = JSON.parse(chats);
|
chats = JSON.parse(chats);
|
||||||
if (Array.isArray(chats)) {
|
if (Array.isArray(chats)) {
|
||||||
@@ -318,7 +314,6 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
|
||||||
showError(t('聊天链接配置错误,请联系管理员'));
|
showError(t('聊天链接配置错误,请联系管理员'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,6 +354,29 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</SplitButtonGroup>
|
</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
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size="small"
|
||||||
@@ -412,7 +430,12 @@ export const getTokensColumns = ({
|
|||||||
title: t('状态'),
|
title: t('状态'),
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
key: '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('分组'),
|
title: t('分组'),
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Progress,
|
Progress,
|
||||||
Switch,
|
Popover,
|
||||||
|
Typography,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { renderGroup, renderNumber, renderQuota } from '../../../helpers';
|
import { renderGroup, renderNumber, renderQuota } from '../../../helpers';
|
||||||
|
|
||||||
@@ -89,7 +90,6 @@ const renderUsername = (text, record) => {
|
|||||||
* Render user statistics
|
* Render user statistics
|
||||||
*/
|
*/
|
||||||
const renderStatistics = (text, record, showEnableDisableModal, t) => {
|
const renderStatistics = (text, record, showEnableDisableModal, t) => {
|
||||||
const enabled = record.status === 1;
|
|
||||||
const isDeleted = record.DeletedAt !== null;
|
const isDeleted = record.DeletedAt !== null;
|
||||||
|
|
||||||
// Determine tag text & color like original status column
|
// Determine tag text & color like original status column
|
||||||
@@ -100,60 +100,17 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
|
|||||||
tagText = t('已注销');
|
tagText = t('已注销');
|
||||||
} else if (record.status === 1) {
|
} else if (record.status === 1) {
|
||||||
tagColor = 'green';
|
tagColor = 'green';
|
||||||
tagText = t('已激活');
|
tagText = t('已启用');
|
||||||
} else if (record.status === 2) {
|
} else if (record.status === 2) {
|
||||||
tagColor = 'red';
|
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 = (
|
const content = (
|
||||||
<Tag
|
<Tag
|
||||||
color={tagColor}
|
color={tagColor}
|
||||||
shape='circle'
|
shape='circle'
|
||||||
size='large'
|
size='small'
|
||||||
prefixIcon={
|
|
||||||
<Switch
|
|
||||||
size='small'
|
|
||||||
checked={enabled}
|
|
||||||
onChange={handleToggle}
|
|
||||||
disabled={isDeleted}
|
|
||||||
aria-label='user status switch'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
suffixIcon={quotaSuffix}
|
|
||||||
>
|
>
|
||||||
{tagText}
|
{tagText}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -161,9 +118,6 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
|
|||||||
|
|
||||||
const tooltipContent = (
|
const tooltipContent = (
|
||||||
<div className='text-xs'>
|
<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>{t('调用次数')}: {renderNumber(record.request_count)}</div>
|
||||||
</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
|
* Render invite information
|
||||||
*/
|
*/
|
||||||
@@ -204,6 +195,7 @@ const renderOperations = (text, record, {
|
|||||||
setShowEditUser,
|
setShowEditUser,
|
||||||
showPromoteModal,
|
showPromoteModal,
|
||||||
showDemoteModal,
|
showDemoteModal,
|
||||||
|
showEnableDisableModal,
|
||||||
showDeleteModal,
|
showDeleteModal,
|
||||||
t
|
t
|
||||||
}) => {
|
}) => {
|
||||||
@@ -213,6 +205,22 @@ const renderOperations = (text, record, {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Space>
|
<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
|
<Button
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
size="small"
|
size="small"
|
||||||
@@ -270,6 +278,16 @@ export const getUsersColumns = ({
|
|||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
render: (text, record) => renderUsername(text, record),
|
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('分组'),
|
title: t('分组'),
|
||||||
dataIndex: 'group',
|
dataIndex: 'group',
|
||||||
@@ -284,11 +302,6 @@ export const getUsersColumns = ({
|
|||||||
return <div>{renderRole(text, t)}</div>;
|
return <div>{renderRole(text, t)}</div>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('状态'),
|
|
||||||
dataIndex: 'info',
|
|
||||||
render: (text, record, index) => renderStatistics(text, record, showEnableDisableModal, t),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('邀请信息'),
|
title: t('邀请信息'),
|
||||||
dataIndex: 'invite',
|
dataIndex: 'invite',
|
||||||
|
|||||||
Reference in New Issue
Block a user