Просмотр исходного кода

🎨 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
t0ng7u 7 месяцев назад
Родитель
Сommit
ada434fb20

+ 1 - 1
web/src/components/table/channels/ChannelsColumnDefs.js

@@ -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);

+ 216 - 216
web/src/components/table/channels/modals/MultiKeyManageModal.jsx

@@ -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 { 
-  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';
 import { API, showError, showSuccess, timestamp2string } from '../../../../helpers/index.js';
 
 
-const { Text, Title } = Typography;
+const { Text } = Typography;
 
 
 const MultiKeyManageModal = ({
 const MultiKeyManageModal = ({
   visible,
   visible,
@@ -55,13 +52,13 @@ const MultiKeyManageModal = ({
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [keyStatusList, setKeyStatusList] = useState([]);
   const [keyStatusList, setKeyStatusList] = useState([]);
   const [operationLoading, setOperationLoading] = useState({});
   const [operationLoading, setOperationLoading] = useState({});
-  
+
   // 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);
-  
+
   // Statistics states
   // Statistics states
   const [enabledCount, setEnabledCount] = useState(0);
   const [enabledCount, setEnabledCount] = useState(0);
   const [manualDisabledCount, setManualDisabledCount] = useState(0);
   const [manualDisabledCount, setManualDisabledCount] = useState(0);
@@ -73,7 +70,7 @@ const MultiKeyManageModal = ({
   // Load key status data
   // Load key status data
   const loadKeyStatus = async (page = currentPage, size = pageSize, status = statusFilter) => {
   const loadKeyStatus = async (page = currentPage, size = pageSize, status = statusFilter) => {
     if (!channel?.id) return;
     if (!channel?.id) return;
-    
+
     setLoading(true);
     setLoading(true);
     try {
     try {
       const requestData = {
       const requestData = {
@@ -82,22 +79,22 @@ const MultiKeyManageModal = ({
         page: page,
         page: page,
         page_size: size
         page_size: size
       };
       };
-      
+
       // Add status filter if specified
       // Add status filter if specified
       if (status !== null) {
       if (status !== null) {
         requestData.status = status;
         requestData.status = status;
       }
       }
-      
+
       const res = await API.post('/api/channel/multi_key/manage', requestData);
       const res = await API.post('/api/channel/multi_key/manage', requestData);
-      
+
       if (res.data.success) {
       if (res.data.success) {
         const data = res.data.data;
         const data = res.data.data;
         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)
         setEnabledCount(data.enabled_count || 0);
         setEnabledCount(data.enabled_count || 0);
         setManualDisabledCount(data.manual_disabled_count || 0);
         setManualDisabledCount(data.manual_disabled_count || 0);
@@ -117,14 +114,14 @@ const MultiKeyManageModal = ({
   const handleDisableKey = async (keyIndex) => {
   const handleDisableKey = async (keyIndex) => {
     const operationId = `disable_${keyIndex}`;
     const operationId = `disable_${keyIndex}`;
     setOperationLoading(prev => ({ ...prev, [operationId]: true }));
     setOperationLoading(prev => ({ ...prev, [operationId]: true }));
-    
+
     try {
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
         channel_id: channel.id,
         action: 'disable_key',
         action: 'disable_key',
         key_index: keyIndex
         key_index: keyIndex
       });
       });
-      
+
       if (res.data.success) {
       if (res.data.success) {
         showSuccess(t('密钥已禁用'));
         showSuccess(t('密钥已禁用'));
         await loadKeyStatus(currentPage, pageSize); // Reload current page
         await loadKeyStatus(currentPage, pageSize); // Reload current page
@@ -143,14 +140,14 @@ const MultiKeyManageModal = ({
   const handleEnableKey = async (keyIndex) => {
   const handleEnableKey = async (keyIndex) => {
     const operationId = `enable_${keyIndex}`;
     const operationId = `enable_${keyIndex}`;
     setOperationLoading(prev => ({ ...prev, [operationId]: true }));
     setOperationLoading(prev => ({ ...prev, [operationId]: true }));
-    
+
     try {
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
         channel_id: channel.id,
         action: 'enable_key',
         action: 'enable_key',
         key_index: keyIndex
         key_index: keyIndex
       });
       });
-      
+
       if (res.data.success) {
       if (res.data.success) {
         showSuccess(t('密钥已启用'));
         showSuccess(t('密钥已启用'));
         await loadKeyStatus(currentPage, pageSize); // Reload current page
         await loadKeyStatus(currentPage, pageSize); // Reload current page
@@ -168,13 +165,13 @@ const MultiKeyManageModal = ({
   // Enable all disabled keys
   // Enable all disabled keys
   const handleEnableAll = async () => {
   const handleEnableAll = async () => {
     setOperationLoading(prev => ({ ...prev, enable_all: true }));
     setOperationLoading(prev => ({ ...prev, enable_all: true }));
-    
+
     try {
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
         channel_id: channel.id,
         action: 'enable_all_keys'
         action: 'enable_all_keys'
       });
       });
-      
+
       if (res.data.success) {
       if (res.data.success) {
         showSuccess(res.data.message || t('已启用所有密钥'));
         showSuccess(res.data.message || t('已启用所有密钥'));
         // Reset to first page after bulk operation
         // Reset to first page after bulk operation
@@ -194,13 +191,13 @@ const MultiKeyManageModal = ({
   // Disable all enabled keys
   // Disable all enabled keys
   const handleDisableAll = async () => {
   const handleDisableAll = async () => {
     setOperationLoading(prev => ({ ...prev, disable_all: true }));
     setOperationLoading(prev => ({ ...prev, disable_all: true }));
-    
+
     try {
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
         channel_id: channel.id,
         action: 'disable_all_keys'
         action: 'disable_all_keys'
       });
       });
-      
+
       if (res.data.success) {
       if (res.data.success) {
         showSuccess(res.data.message || t('已禁用所有密钥'));
         showSuccess(res.data.message || t('已禁用所有密钥'));
         // Reset to first page after bulk operation
         // Reset to first page after bulk operation
@@ -220,13 +217,13 @@ const MultiKeyManageModal = ({
   // Delete all disabled keys
   // Delete all disabled keys
   const handleDeleteDisabledKeys = async () => {
   const handleDeleteDisabledKeys = async () => {
     setOperationLoading(prev => ({ ...prev, delete_disabled: true }));
     setOperationLoading(prev => ({ ...prev, delete_disabled: true }));
-    
+
     try {
     try {
       const res = await API.post('/api/channel/multi_key/manage', {
       const res = await API.post('/api/channel/multi_key/manage', {
         channel_id: channel.id,
         channel_id: channel.id,
         action: 'delete_disabled_keys'
         action: 'delete_disabled_keys'
       });
       });
-      
+
       if (res.data.success) {
       if (res.data.success) {
         showSuccess(res.data.message);
         showSuccess(res.data.message);
         // Reset to first page after deletion as data structure might change
         // Reset to first page after deletion as data structure might change
@@ -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,196 +391,194 @@ const MultiKeyManageModal = ({
     <Modal
     <Modal
       title={
       title={
         <Space>
         <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>
         </Space>
       }
       }
       visible={visible}
       visible={visible}
       onCancel={onCancel}
       onCancel={onCancel}
       width={900}
       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>
-              )}
-            </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>
         </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}>
           <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>
           </Spin>
         </div>
         </div>
       </div>
       </div>

+ 18 - 1
web/src/i18n/locales/en.json

@@ -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"
 }
 }