🎨 refactor: MultiKeyManageModal: cleaner stats UI, remove chart, integrate toolbar/pagination, and improve UX

- Replace custom dots with Semi Badge types (success/danger/warning); add compact Progress bars
- Remove pie chart and related deps/config; move total key count and mode tags into the modal title
- Rework header using Row/Col; three equal stat cards (enabled/manual-disabled/auto-disabled)
- Integrate toolbar into Table title; wrap content with Card; use Table’s native empty state
- Make “Enable All” conditional (hidden when all keys are enabled), mirroring “Disable All”
- Unify numeric typography (current/total same size) for better readability
- Default page size set to 10; fallback to 10 when backend page_size is absent; page-size options: 10/20/50/100
- Cleanup imports and dead code (remove VChart and pie-spec logic)
- Minor spacing polish (extra bottom margin before table), no footer buttons
This commit is contained in:
t0ng7u
2025-08-10 00:55:18 +08:00
parent 71ba3fa310
commit ada434fb20
3 changed files with 235 additions and 218 deletions

View File

@@ -544,7 +544,7 @@ export const getChannelsColumns = ({
menu={[
{
node: 'item',
name: t('多key管理'),
name: t('多密钥管理'),
onClick: () => {
setCurrentMultiKeyChannel(record);
setShowMultiKeyManageModal(true);

View File

@@ -30,20 +30,17 @@ import {
Popconfirm,
Empty,
Spin,
Banner,
Select,
Pagination
Row,
Col,
Badge,
Progress,
Card
} from '@douyinfe/semi-ui';
import {
IconRefresh,
IconDelete,
IconClose,
IconSave,
IconSetting
} from '@douyinfe/semi-icons';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import { API, showError, showSuccess, timestamp2string } from '../../../../helpers/index.js';
const { Text, Title } = Typography;
const { Text } = Typography;
const MultiKeyManageModal = ({
visible,
@@ -55,13 +52,13 @@ const MultiKeyManageModal = ({
const [loading, setLoading] = useState(false);
const [keyStatusList, setKeyStatusList] = useState([]);
const [operationLoading, setOperationLoading] = useState({});
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(0);
// Statistics states
const [enabledCount, setEnabledCount] = useState(0);
const [manualDisabledCount, setManualDisabledCount] = useState(0);
@@ -73,7 +70,7 @@ const MultiKeyManageModal = ({
// Load key status data
const loadKeyStatus = async (page = currentPage, size = pageSize, status = statusFilter) => {
if (!channel?.id) return;
setLoading(true);
try {
const requestData = {
@@ -82,22 +79,22 @@ const MultiKeyManageModal = ({
page: page,
page_size: size
};
// Add status filter if specified
if (status !== null) {
requestData.status = status;
}
const res = await API.post('/api/channel/multi_key/manage', requestData);
if (res.data.success) {
const data = res.data.data;
setKeyStatusList(data.keys || []);
setTotal(data.total || 0);
setCurrentPage(data.page || 1);
setPageSize(data.page_size || 50);
setPageSize(data.page_size || 10);
setTotalPages(data.total_pages || 0);
// Update statistics (these are always the overall statistics)
setEnabledCount(data.enabled_count || 0);
setManualDisabledCount(data.manual_disabled_count || 0);
@@ -117,14 +114,14 @@ const MultiKeyManageModal = ({
const handleDisableKey = async (keyIndex) => {
const operationId = `disable_${keyIndex}`;
setOperationLoading(prev => ({ ...prev, [operationId]: true }));
try {
const res = await API.post('/api/channel/multi_key/manage', {
channel_id: channel.id,
action: 'disable_key',
key_index: keyIndex
});
if (res.data.success) {
showSuccess(t('密钥已禁用'));
await loadKeyStatus(currentPage, pageSize); // Reload current page
@@ -143,14 +140,14 @@ const MultiKeyManageModal = ({
const handleEnableKey = async (keyIndex) => {
const operationId = `enable_${keyIndex}`;
setOperationLoading(prev => ({ ...prev, [operationId]: true }));
try {
const res = await API.post('/api/channel/multi_key/manage', {
channel_id: channel.id,
action: 'enable_key',
key_index: keyIndex
});
if (res.data.success) {
showSuccess(t('密钥已启用'));
await loadKeyStatus(currentPage, pageSize); // Reload current page
@@ -168,13 +165,13 @@ const MultiKeyManageModal = ({
// Enable all disabled keys
const handleEnableAll = async () => {
setOperationLoading(prev => ({ ...prev, enable_all: true }));
try {
const res = await API.post('/api/channel/multi_key/manage', {
channel_id: channel.id,
action: 'enable_all_keys'
});
if (res.data.success) {
showSuccess(res.data.message || t('已启用所有密钥'));
// Reset to first page after bulk operation
@@ -194,13 +191,13 @@ const MultiKeyManageModal = ({
// Disable all enabled keys
const handleDisableAll = async () => {
setOperationLoading(prev => ({ ...prev, disable_all: true }));
try {
const res = await API.post('/api/channel/multi_key/manage', {
channel_id: channel.id,
action: 'disable_all_keys'
});
if (res.data.success) {
showSuccess(res.data.message || t('已禁用所有密钥'));
// Reset to first page after bulk operation
@@ -220,13 +217,13 @@ const MultiKeyManageModal = ({
// Delete all disabled keys
const handleDeleteDisabledKeys = async () => {
setOperationLoading(prev => ({ ...prev, delete_disabled: true }));
try {
const res = await API.post('/api/channel/multi_key/manage', {
channel_id: channel.id,
action: 'delete_disabled_keys'
});
if (res.data.success) {
showSuccess(res.data.message);
// Reset to first page after deletion as data structure might change
@@ -285,17 +282,24 @@ const MultiKeyManageModal = ({
}
}, [visible]);
// Percentages for progress display
const enabledPercent = total > 0 ? Math.round((enabledCount / total) * 100) : 0;
const manualDisabledPercent = total > 0 ? Math.round((manualDisabledCount / total) * 100) : 0;
const autoDisabledPercent = total > 0 ? Math.round((autoDisabledCount / total) * 100) : 0;
// 取消饼图:不再需要图表数据与配置
// Get status tag component
const renderStatusTag = (status) => {
switch (status) {
case 1:
return <Tag color='green' shape='circle'>{t('已启用')}</Tag>;
return <Tag color='green' shape='circle' size='small'>{t('已启用')}</Tag>;
case 2:
return <Tag color='red' shape='circle'>{t('已禁用')}</Tag>;
return <Tag color='red' shape='circle' size='small'>{t('已禁用')}</Tag>;
case 3:
return <Tag color='orange' shape='circle'>{t('自动禁用')}</Tag>;
return <Tag color='orange' shape='circle' size='small'>{t('自动禁用')}</Tag>;
default:
return <Tag color='grey' shape='circle'>{t('未知状态')}</Tag>;
return <Tag color='grey' shape='circle' size='small'>{t('未知状态')}</Tag>;
}
};
@@ -318,13 +322,11 @@ const MultiKeyManageModal = ({
{
title: t('状态'),
dataIndex: 'status',
width: 100,
render: (status) => renderStatusTag(status),
},
{
title: t('禁用原因'),
dataIndex: 'reason',
width: 220,
render: (reason, record) => {
if (record.status === 1 || !reason) {
return <Text type='quaternary'>-</Text>;
@@ -341,7 +343,6 @@ const MultiKeyManageModal = ({
{
title: t('禁用时间'),
dataIndex: 'disabled_time',
width: 150,
render: (time, record) => {
if (record.status === 1 || !time) {
return <Text type='quaternary'>-</Text>;
@@ -358,7 +359,8 @@ const MultiKeyManageModal = ({
{
title: t('操作'),
key: 'action',
width: 120,
fixed: 'right',
width: 100,
render: (_, record) => (
<Space>
{record.status === 1 ? (
@@ -389,196 +391,194 @@ const MultiKeyManageModal = ({
<Modal
title={
<Space>
<IconSetting />
<span>{t('多密钥管理')} - {channel?.name}</span>
<Text>{t('多密钥管理')}</Text>
{channel?.name && (
<Tag size='small' shape='circle' color='white'>{channel.name}</Tag>
)}
<Tag size='small' shape='circle' color='white'>
{t('总密钥数')}: {total}
</Tag>
{channel?.channel_info?.multi_key_mode && (
<Tag size='small' shape='circle' color='white'>
{channel.channel_info.multi_key_mode === 'random' ? t('随机模式') : t('轮询模式')}
</Tag>
)}
</Space>
}
visible={visible}
onCancel={onCancel}
width={900}
footer={
<Space>
<Button onClick={onCancel}>{t('关闭')}</Button>
<Button
icon={<IconRefresh />}
onClick={() => loadKeyStatus(currentPage, pageSize)}
loading={loading}
>
{t('刷新')}
</Button>
<Popconfirm
title={t('确定要启用所有密钥吗?')}
onConfirm={handleEnableAll}
position={'topRight'}
>
<Button
type='primary'
loading={operationLoading.enable_all}
>
{t('启用全部')}
</Button>
</Popconfirm>
{enabledCount > 0 && (
<Popconfirm
title={t('确定要禁用所有的密钥吗?')}
onConfirm={handleDisableAll}
okType={'danger'}
position={'topRight'}
>
<Button
type='danger'
loading={operationLoading.disable_all}
>
{t('禁用全部')}
</Button>
</Popconfirm>
)}
<Popconfirm
title={t('确定要删除所有已自动禁用的密钥吗?')}
content={t('此操作不可撤销,将永久删除已自动禁用的密钥')}
onConfirm={handleDeleteDisabledKeys}
okType={'danger'}
position={'topRight'}
>
<Button
type='danger'
icon={<IconDelete />}
loading={operationLoading.delete_disabled}
>
{t('删除自动禁用密钥')}
</Button>
</Popconfirm>
</Space>
}
footer={null}
>
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* Statistics Banner */}
<Banner
type='info'
style={{ marginBottom: '16px', flexShrink: 0 }}
description={
<div>
<Text>
{t('总共 {{total}} 个密钥,{{enabled}} 个已启用,{{manual}} 个手动禁用,{{auto}} 个自动禁用', {
total: total,
enabled: enabledCount,
manual: manualDisabledCount,
auto: autoDisabledCount
})}
</Text>
{channel?.channel_info?.multi_key_mode && (
<div style={{ marginTop: '4px' }}>
<Text type='quaternary' style={{ fontSize: '12px' }}>
{t('多密钥模式')}: {channel.channel_info.multi_key_mode === 'random' ? t('随机') : t('轮询')}
</Text>
<div className="flex flex-col mb-5">
{/* Stats & Mode */}
<div
className="rounded-xl p-4 mb-3"
style={{
background: 'var(--semi-color-bg-1)',
border: '1px solid var(--semi-color-border)'
}}
>
<Row gutter={16} align="middle">
<Col span={8}>
<div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
<div className="flex items-center gap-2 mb-2">
<Badge dot type='success' />
<Text type='tertiary'>{t('已启用')}</Text>
</div>
)}
</div>
}
/>
{/* Filter Controls */}
<div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px', flexShrink: 0 }}>
<Text style={{ fontSize: '14px', fontWeight: '500' }}>{t('状态筛选')}:</Text>
<Select
value={statusFilter}
onChange={handleStatusFilterChange}
style={{ width: '120px' }}
size='small'
placeholder={t('全部状态')}
>
<Select.Option value={null}>{t('全部状态')}</Select.Option>
<Select.Option value={1}>{t('已启用')}</Select.Option>
<Select.Option value={2}>{t('手动禁用')}</Select.Option>
<Select.Option value={3}>{t('自动禁用')}</Select.Option>
</Select>
{statusFilter !== null && (
<Text type='quaternary' style={{ fontSize: '12px' }}>
{t('当前显示 {{count}} 条筛选结果', { count: total })}
</Text>
)}
<div className="flex items-end gap-2 mb-2">
<Text style={{ fontSize: 18, fontWeight: 700, color: '#22c55e' }}>{enabledCount}</Text>
<Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
</div>
<Progress percent={enabledPercent} showInfo={false} size="small" stroke="#22c55e" style={{ height: 6, borderRadius: 999 }} />
</div>
</Col>
<Col span={8}>
<div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
<div className="flex items-center gap-2 mb-2">
<Badge dot type='danger' />
<Text type='tertiary'>{t('手动禁用')}</Text>
</div>
<div className="flex items-end gap-2 mb-2">
<Text style={{ fontSize: 18, fontWeight: 700, color: '#ef4444' }}>{manualDisabledCount}</Text>
<Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
</div>
<Progress percent={manualDisabledPercent} showInfo={false} size="small" stroke="#ef4444" style={{ height: 6, borderRadius: 999 }} />
</div>
</Col>
<Col span={8}>
<div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
<div className="flex items-center gap-2 mb-2">
<Badge dot type='warning' />
<Text type='tertiary'>{t('自动禁用')}</Text>
</div>
<div className="flex items-end gap-2 mb-2">
<Text style={{ fontSize: 18, fontWeight: 700, color: '#f59e0b' }}>{autoDisabledCount}</Text>
<Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
</div>
<Progress percent={autoDisabledPercent} showInfo={false} size="small" stroke="#f59e0b" style={{ height: 6, borderRadius: 999 }} />
</div>
</Col>
</Row>
</div>
{/* Key Status Table */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
{/* Table */}
<div className="flex-1 flex flex-col min-h-0">
<Spin spinning={loading}>
{keyStatusList.length > 0 ? (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'auto', marginBottom: '16px' }}>
<Table
columns={columns}
dataSource={keyStatusList}
pagination={false}
size='small'
bordered
rowKey='index'
scroll={{ y: 'calc(100vh - 400px)' }}
/>
</div>
{/* Pagination */}
{total > 0 && (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexShrink: 0,
padding: '12px 0',
borderTop: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-1)'
}}>
<Text type='quaternary' style={{ fontSize: '12px' }}>
{t('显示第 {{start}}-{{end}} 条,共 {{total}} 条', {
start: (currentPage - 1) * pageSize + 1,
end: Math.min(currentPage * pageSize, total),
total: total
})}
</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Text type='quaternary' style={{ fontSize: '12px' }}>
{t('每页显示')}:
</Text>
<Select
value={pageSize}
onChange={handlePageSizeChange}
size='small'
style={{ width: '80px' }}
>
<Select.Option value={50}>50</Select.Option>
<Select.Option value={100}>100</Select.Option>
<Select.Option value={500}>500</Select.Option>
<Select.Option value={1000}>1000</Select.Option>
</Select>
<Pagination
current={currentPage}
total={total}
pageSize={pageSize}
showSizeChanger={false}
showQuickJumper
size='small'
onChange={handlePageChange}
showTotal={(total, range) =>
t('第 {{current}} / {{total}} 页', {
current: currentPage,
total: totalPages
})
}
/>
</div>
</div>
<Card className='!rounded-xl'>
<Table
title={() => (
<Row gutter={12} style={{ width: '100%' }}>
<Col span={14}>
<Row gutter={12} style={{ alignItems: 'center' }}>
<Col>
<Select
value={statusFilter}
onChange={handleStatusFilterChange}
size='small'
placeholder={t('全部状态')}
>
<Select.Option value={null}>{t('全部状态')}</Select.Option>
<Select.Option value={1}>{t('已启用')}</Select.Option>
<Select.Option value={2}>{t('手动禁用')}</Select.Option>
<Select.Option value={3}>{t('自动禁用')}</Select.Option>
</Select>
</Col>
</Row>
</Col>
<Col span={10} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button
size='small'
type='tertiary'
onClick={() => loadKeyStatus(currentPage, pageSize)}
loading={loading}
>
{t('刷新')}
</Button>
{(manualDisabledCount + autoDisabledCount) > 0 && (
<Popconfirm
title={t('确定要启用所有密钥吗?')}
onConfirm={handleEnableAll}
position={'topRight'}
>
<Button
size='small'
type='primary'
loading={operationLoading.enable_all}
>
{t('启用全部')}
</Button>
</Popconfirm>
)}
{enabledCount > 0 && (
<Popconfirm
title={t('确定要禁用所有的密钥吗?')}
onConfirm={handleDisableAll}
okType={'danger'}
position={'topRight'}
>
<Button
size='small'
type='danger'
loading={operationLoading.disable_all}
>
{t('禁用全部')}
</Button>
</Popconfirm>
)}
<Popconfirm
title={t('确定要删除所有已自动禁用的密钥吗?')}
content={t('此操作不可撤销,将永久删除已自动禁用的密钥')}
onConfirm={handleDeleteDisabledKeys}
okType={'danger'}
position={'topRight'}
>
<Button
size='small'
type='warning'
loading={operationLoading.delete_disabled}
>
{t('删除自动禁用密钥')}
</Button>
</Popconfirm>
</Space>
</Col>
</Row>
)}
</div>
) : (
!loading && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
title={t('暂无密钥数据')}
description={t('请检查渠道配置或刷新重试')}
/>
)
)}
columns={columns}
dataSource={keyStatusList}
pagination={{
currentPage: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, size) => {
setCurrentPage(page);
loadKeyStatus(page, size);
},
onShowSizeChange: (current, size) => {
setCurrentPage(1);
handlePageSizeChange(size);
}
}}
size='small'
bordered={false}
rowKey='index'
scroll={{ x: 'max-content' }}
empty={
<Empty
image={<IllustrationNoResult style={{ width: 140, height: 140 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 140, height: 140 }} />}
title={t('暂无密钥数据')}
description={t('请检查渠道配置或刷新重试')}
style={{ padding: 30 }}
/>
}
/>
</Card>
</Spin>
</div>
</div>

View File

@@ -1890,5 +1890,22 @@
"未知供应商": "Unknown",
"共 {{count}} 个模型": "{{count}} models",
"倍率信息": "Ratio information",
"倍率是用于系统计算不同模型的最终价格用的,如果您不理解倍率,请忽略": "The ratio is used to calculate the final price of different models in the system. If you do not understand the ratio, please ignore it."
"倍率是用于系统计算不同模型的最终价格用的,如果您不理解倍率,请忽略": "The ratio is used to calculate the final price of different models in the system. If you do not understand the ratio, please ignore it.",
"多密钥管理": "Multi-key management",
"总密钥数": "Total key count",
"随机模式": "Random mode",
"轮询模式": "Polling mode",
"手动禁用": "Manually disabled",
"自动禁用": "Auto disabled",
"暂无密钥数据": "No key data",
"请检查渠道配置或刷新重试": "Please check the channel configuration or refresh and try again",
"全部状态": "All status",
"索引": "Index",
"禁用原因": "Disable reason",
"禁用时间": "Disable time",
"确定要启用所有密钥吗?": "Are you sure you want to enable all keys?",
"确定要禁用所有的密钥吗?": "Are you sure you want to disable all keys?",
"确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?",
"此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.",
"删除自动禁用密钥": "Delete auto disabled keys"
}