🎨 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={[ menu={[
{ {
node: 'item', node: 'item',
name: t('多key管理'), name: t('多密钥管理'),
onClick: () => { onClick: () => {
setCurrentMultiKeyChannel(record); setCurrentMultiKeyChannel(record);
setShowMultiKeyManageModal(true); setShowMultiKeyManageModal(true);

View File

@@ -30,20 +30,17 @@ import {
Popconfirm, Popconfirm,
Empty, Empty,
Spin, Spin,
Banner,
Select, Select,
Pagination Row,
Col,
Badge,
Progress,
Card
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
IconRefresh,
IconDelete,
IconClose,
IconSave,
IconSetting
} from '@douyinfe/semi-icons';
import { API, showError, showSuccess, timestamp2string } from '../../../../helpers/index.js'; import { API, showError, showSuccess, timestamp2string } from '../../../../helpers/index.js';
const { Text, Title } = Typography; const { Text } = Typography;
const MultiKeyManageModal = ({ const MultiKeyManageModal = ({
visible, visible,
@@ -58,7 +55,7 @@ const MultiKeyManageModal = ({
// Pagination states // Pagination states
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(50); const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
@@ -95,7 +92,7 @@ const MultiKeyManageModal = ({
setKeyStatusList(data.keys || []); setKeyStatusList(data.keys || []);
setTotal(data.total || 0); setTotal(data.total || 0);
setCurrentPage(data.page || 1); setCurrentPage(data.page || 1);
setPageSize(data.page_size || 50); setPageSize(data.page_size || 10);
setTotalPages(data.total_pages || 0); setTotalPages(data.total_pages || 0);
// Update statistics (these are always the overall statistics) // Update statistics (these are always the overall statistics)
@@ -285,17 +282,24 @@ const MultiKeyManageModal = ({
} }
}, [visible]); }, [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 // Get status tag component
const renderStatusTag = (status) => { const renderStatusTag = (status) => {
switch (status) { switch (status) {
case 1: case 1:
return <Tag color='green' shape='circle'>{t('已启用')}</Tag>; return <Tag color='green' shape='circle' size='small'>{t('已启用')}</Tag>;
case 2: case 2:
return <Tag color='red' shape='circle'>{t('已禁用')}</Tag>; return <Tag color='red' shape='circle' size='small'>{t('已禁用')}</Tag>;
case 3: case 3:
return <Tag color='orange' shape='circle'>{t('自动禁用')}</Tag>; return <Tag color='orange' shape='circle' size='small'>{t('自动禁用')}</Tag>;
default: 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('状态'), title: t('状态'),
dataIndex: 'status', dataIndex: 'status',
width: 100,
render: (status) => renderStatusTag(status), render: (status) => renderStatusTag(status),
}, },
{ {
title: t('禁用原因'), title: t('禁用原因'),
dataIndex: 'reason', dataIndex: 'reason',
width: 220,
render: (reason, record) => { render: (reason, record) => {
if (record.status === 1 || !reason) { if (record.status === 1 || !reason) {
return <Text type='quaternary'>-</Text>; return <Text type='quaternary'>-</Text>;
@@ -341,7 +343,6 @@ const MultiKeyManageModal = ({
{ {
title: t('禁用时间'), title: t('禁用时间'),
dataIndex: 'disabled_time', dataIndex: 'disabled_time',
width: 150,
render: (time, record) => { render: (time, record) => {
if (record.status === 1 || !time) { if (record.status === 1 || !time) {
return <Text type='quaternary'>-</Text>; return <Text type='quaternary'>-</Text>;
@@ -358,7 +359,8 @@ const MultiKeyManageModal = ({
{ {
title: t('操作'), title: t('操作'),
key: 'action', key: 'action',
width: 120, fixed: 'right',
width: 100,
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
{record.status === 1 ? ( {record.status === 1 ? (
@@ -389,35 +391,126 @@ const MultiKeyManageModal = ({
<Modal <Modal
title={ title={
<Space> <Space>
<IconSetting /> <Text>{t('多密钥管理')}</Text>
<span>{t('多密钥管理')} - {channel?.name}</span> {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> </Space>
} }
visible={visible} visible={visible}
onCancel={onCancel} onCancel={onCancel}
width={900} width={900}
footer={ footer={null}
>
<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 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>
{/* Table */}
<div className="flex-1 flex flex-col min-h-0">
<Spin spinning={loading}>
<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> <Space>
<Button onClick={onCancel}>{t('关闭')}</Button>
<Button <Button
icon={<IconRefresh />} size='small'
type='tertiary'
onClick={() => loadKeyStatus(currentPage, pageSize)} onClick={() => loadKeyStatus(currentPage, pageSize)}
loading={loading} loading={loading}
> >
{t('刷新')} {t('刷新')}
</Button> </Button>
{(manualDisabledCount + autoDisabledCount) > 0 && (
<Popconfirm <Popconfirm
title={t('确定要启用所有密钥吗?')} title={t('确定要启用所有密钥吗?')}
onConfirm={handleEnableAll} onConfirm={handleEnableAll}
position={'topRight'} position={'topRight'}
> >
<Button <Button
size='small'
type='primary' type='primary'
loading={operationLoading.enable_all} loading={operationLoading.enable_all}
> >
{t('启用全部')} {t('启用全部')}
</Button> </Button>
</Popconfirm> </Popconfirm>
)}
{enabledCount > 0 && ( {enabledCount > 0 && (
<Popconfirm <Popconfirm
title={t('确定要禁用所有的密钥吗?')} title={t('确定要禁用所有的密钥吗?')}
@@ -426,6 +519,7 @@ const MultiKeyManageModal = ({
position={'topRight'} position={'topRight'}
> >
<Button <Button
size='small'
type='danger' type='danger'
loading={operationLoading.disable_all} loading={operationLoading.disable_all}
> >
@@ -441,144 +535,50 @@ const MultiKeyManageModal = ({
position={'topRight'} position={'topRight'}
> >
<Button <Button
type='danger' size='small'
icon={<IconDelete />} type='warning'
loading={operationLoading.delete_disabled} loading={operationLoading.delete_disabled}
> >
{t('删除自动禁用密钥')} {t('删除自动禁用密钥')}
</Button> </Button>
</Popconfirm> </Popconfirm>
</Space> </Space>
} </Col>
> </Row>
<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>
)} )}
</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>
{/* Key Status Table */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 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} columns={columns}
dataSource={keyStatusList} dataSource={keyStatusList}
pagination={false} pagination={{
size='small' currentPage: currentPage,
bordered pageSize: pageSize,
rowKey='index' total: total,
scroll={{ y: 'calc(100vh - 400px)' }} showSizeChanger: true,
/> showQuickJumper: true,
</div> pageSizeOptions: ['10', '20', '50', '100'],
onChange: (page, size) => {
{/* Pagination */} setCurrentPage(page);
{total > 0 && ( loadKeyStatus(page, size);
<div style={{ },
display: 'flex', onShowSizeChange: (current, size) => {
justifyContent: 'space-between', setCurrentPage(1);
alignItems: 'center', handlePageSizeChange(size);
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> size='small'
</div> bordered={false}
)} rowKey='index'
</div> scroll={{ x: 'max-content' }}
) : ( empty={
!loading && (
<Empty <Empty
image={Empty.PRESENTED_IMAGE_SIMPLE} image={<IllustrationNoResult style={{ width: 140, height: 140 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 140, height: 140 }} />}
title={t('暂无密钥数据')} title={t('暂无密钥数据')}
description={t('请检查渠道配置或刷新重试')} description={t('请检查渠道配置或刷新重试')}
style={{ padding: 30 }}
/> />
) }
)} />
</Card>
</Spin> </Spin>
</div> </div>
</div> </div>

View File

@@ -1890,5 +1890,22 @@
"未知供应商": "Unknown", "未知供应商": "Unknown",
"共 {{count}} 个模型": "{{count}} models", "共 {{count}} 个模型": "{{count}} models",
"倍率信息": "Ratio information", "倍率信息": "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"
} }