feat: add status filtering and bulk enable/disable functionality in multi-key management

This commit is contained in:
CaIon
2025-08-04 19:51:58 +08:00
parent 6e2a04f374
commit 12b4e80d4b
2 changed files with 288 additions and 90 deletions

View File

@@ -1057,6 +1057,7 @@ type MultiKeyManageRequest struct {
KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions
Page int `json:"page,omitempty"` // for get_key_status pagination
PageSize int `json:"page_size,omitempty"` // for get_key_status pagination
Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all
}
// MultiKeyStatusResponse represents the response for key status query
@@ -1109,7 +1110,6 @@ func ManageMultiKeys(c *gin.Context) {
switch request.Action {
case "get_key_status":
keys := channel.GetKeys()
total := len(keys)
// Default pagination parameters
page := request.Page
@@ -1121,23 +1121,11 @@ func ManageMultiKeys(c *gin.Context) {
pageSize = 50 // Default page size
}
// Calculate pagination
totalPages := (total + pageSize - 1) / pageSize
if page > totalPages && totalPages > 0 {
page = totalPages
}
// Calculate range
start := (page - 1) * pageSize
end := start + pageSize
if end > total {
end = total
}
// Statistics for all keys
// Statistics for all keys (unchanged by filtering)
var enabledCount, manualDisabledCount, autoDisabledCount int
var keyStatusList []KeyStatus
// Build all key status data first
var allKeyStatusList []KeyStatus
for i, key := range keys {
status := 1 // default enabled
var disabledTime int64
@@ -1149,7 +1137,7 @@ func ManageMultiKeys(c *gin.Context) {
}
}
// Count for statistics
// Count for statistics (all keys)
switch status {
case 1:
enabledCount++
@@ -1159,45 +1147,77 @@ func ManageMultiKeys(c *gin.Context) {
autoDisabledCount++
}
// Only include keys in current page
if i >= start && i < end {
if status != 1 {
if channel.ChannelInfo.MultiKeyDisabledTime != nil {
disabledTime = channel.ChannelInfo.MultiKeyDisabledTime[i]
}
if channel.ChannelInfo.MultiKeyDisabledReason != nil {
reason = channel.ChannelInfo.MultiKeyDisabledReason[i]
}
if status != 1 {
if channel.ChannelInfo.MultiKeyDisabledTime != nil {
disabledTime = channel.ChannelInfo.MultiKeyDisabledTime[i]
}
// Create key preview (first 10 chars)
keyPreview := key
if len(key) > 10 {
keyPreview = key[:10] + "..."
if channel.ChannelInfo.MultiKeyDisabledReason != nil {
reason = channel.ChannelInfo.MultiKeyDisabledReason[i]
}
keyStatusList = append(keyStatusList, KeyStatus{
Index: i,
Status: status,
DisabledTime: disabledTime,
Reason: reason,
KeyPreview: keyPreview,
})
}
// Create key preview (first 10 chars)
keyPreview := key
if len(key) > 10 {
keyPreview = key[:10] + "..."
}
allKeyStatusList = append(allKeyStatusList, KeyStatus{
Index: i,
Status: status,
DisabledTime: disabledTime,
Reason: reason,
KeyPreview: keyPreview,
})
}
// Apply status filter if specified
var filteredKeyStatusList []KeyStatus
if request.Status != nil {
for _, keyStatus := range allKeyStatusList {
if keyStatus.Status == *request.Status {
filteredKeyStatusList = append(filteredKeyStatusList, keyStatus)
}
}
} else {
filteredKeyStatusList = allKeyStatusList
}
// Calculate pagination based on filtered results
filteredTotal := len(filteredKeyStatusList)
totalPages := (filteredTotal + pageSize - 1) / pageSize
if totalPages == 0 {
totalPages = 1
}
if page > totalPages {
page = totalPages
}
// Calculate range for current page
start := (page - 1) * pageSize
end := start + pageSize
if end > filteredTotal {
end = filteredTotal
}
// Get the page data
var pageKeyStatusList []KeyStatus
if start < filteredTotal {
pageKeyStatusList = filteredKeyStatusList[start:end]
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": MultiKeyStatusResponse{
Keys: keyStatusList,
Total: total,
Keys: pageKeyStatusList,
Total: filteredTotal, // Total of filtered results
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
EnabledCount: enabledCount,
ManualDisabledCount: manualDisabledCount,
AutoDisabledCount: autoDisabledCount,
EnabledCount: enabledCount, // Overall statistics
ManualDisabledCount: manualDisabledCount, // Overall statistics
AutoDisabledCount: autoDisabledCount, // Overall statistics
},
})
return
@@ -1231,8 +1251,6 @@ func ManageMultiKeys(c *gin.Context) {
}
channel.ChannelInfo.MultiKeyStatusList[keyIndex] = 2 // disabled
channel.ChannelInfo.MultiKeyDisabledTime[keyIndex] = common.GetTimestamp()
channel.ChannelInfo.MultiKeyDisabledReason[keyIndex] = "手动禁用"
err = channel.Update()
if err != nil {
@@ -1289,6 +1307,77 @@ func ManageMultiKeys(c *gin.Context) {
})
return
case "enable_all_keys":
// 清空所有禁用状态,使所有密钥回到默认启用状态
var enabledCount int
if channel.ChannelInfo.MultiKeyStatusList != nil {
enabledCount = len(channel.ChannelInfo.MultiKeyStatusList)
}
channel.ChannelInfo.MultiKeyStatusList = make(map[int]int)
channel.ChannelInfo.MultiKeyDisabledTime = make(map[int]int64)
channel.ChannelInfo.MultiKeyDisabledReason = make(map[int]string)
err = channel.Update()
if err != nil {
common.ApiError(c, err)
return
}
model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": fmt.Sprintf("已启用 %d 个密钥", enabledCount),
})
return
case "disable_all_keys":
// 禁用所有启用的密钥
if channel.ChannelInfo.MultiKeyStatusList == nil {
channel.ChannelInfo.MultiKeyStatusList = make(map[int]int)
}
if channel.ChannelInfo.MultiKeyDisabledTime == nil {
channel.ChannelInfo.MultiKeyDisabledTime = make(map[int]int64)
}
if channel.ChannelInfo.MultiKeyDisabledReason == nil {
channel.ChannelInfo.MultiKeyDisabledReason = make(map[int]string)
}
var disabledCount int
for i := 0; i < channel.ChannelInfo.MultiKeySize; i++ {
status := 1 // default enabled
if s, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists {
status = s
}
// 只禁用当前启用的密钥
if status == 1 {
channel.ChannelInfo.MultiKeyStatusList[i] = 2 // disabled
disabledCount++
}
}
if disabledCount == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "没有可禁用的密钥",
})
return
}
err = channel.Update()
if err != nil {
common.ApiError(c, err)
return
}
model.InitChannelCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": fmt.Sprintf("已禁用 %d 个密钥", disabledCount),
})
return
case "delete_disabled_keys":
keys := channel.GetKeys()
var remainingKeys []string

View File

@@ -67,18 +67,28 @@ const MultiKeyManageModal = ({
const [manualDisabledCount, setManualDisabledCount] = useState(0);
const [autoDisabledCount, setAutoDisabledCount] = useState(0);
// Filter states
const [statusFilter, setStatusFilter] = useState(null); // null=all, 1=enabled, 2=manual_disabled, 3=auto_disabled
// Load key status data
const loadKeyStatus = async (page = currentPage, size = pageSize) => {
const loadKeyStatus = async (page = currentPage, size = pageSize, status = statusFilter) => {
if (!channel?.id) return;
setLoading(true);
try {
const res = await API.post('/api/channel/multi_key/manage', {
const requestData = {
channel_id: channel.id,
action: 'get_key_status',
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;
@@ -88,7 +98,7 @@ const MultiKeyManageModal = ({
setPageSize(data.page_size || 50);
setTotalPages(data.total_pages || 0);
// Update statistics
// Update statistics (these are always the overall statistics)
setEnabledCount(data.enabled_count || 0);
setManualDisabledCount(data.manual_disabled_count || 0);
setAutoDisabledCount(data.auto_disabled_count || 0);
@@ -155,6 +165,58 @@ 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
setCurrentPage(1);
await loadKeyStatus(1, pageSize);
onRefresh && onRefresh(); // Refresh parent component
} else {
showError(res.data.message);
}
} catch (error) {
showError(t('启用所有密钥失败'));
} finally {
setOperationLoading(prev => ({ ...prev, enable_all: false }));
}
};
// 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
setCurrentPage(1);
await loadKeyStatus(1, pageSize);
onRefresh && onRefresh(); // Refresh parent component
} else {
showError(res.data.message);
}
} catch (error) {
showError(t('禁用所有密钥失败'));
} finally {
setOperationLoading(prev => ({ ...prev, disable_all: false }));
}
};
// Delete all disabled keys
const handleDeleteDisabledKeys = async () => {
setOperationLoading(prev => ({ ...prev, delete_disabled: true }));
@@ -194,6 +256,13 @@ const MultiKeyManageModal = ({
loadKeyStatus(1, size);
};
// Handle status filter change
const handleStatusFilterChange = (status) => {
setStatusFilter(status);
setCurrentPage(1); // Reset to first page when filter changes
loadKeyStatus(1, pageSize, status);
};
// Effect to load data when modal opens
useEffect(() => {
if (visible && channel?.id) {
@@ -212,6 +281,7 @@ const MultiKeyManageModal = ({
setEnabledCount(0);
setManualDisabledCount(0);
setAutoDisabledCount(0);
setStatusFilter(null); // Reset filter
}
}, [visible]);
@@ -236,15 +306,15 @@ const MultiKeyManageModal = ({
dataIndex: 'index',
render: (text) => `#${text}`,
},
{
title: t('密钥预览'),
dataIndex: 'key_preview',
render: (text) => (
<Text code style={{ fontSize: '12px' }}>
{text}
</Text>
),
},
// {
// title: t('密钥预览'),
// dataIndex: 'key_preview',
// render: (text) => (
// <Text code style={{ fontSize: '12px' }}>
// {text}
// </Text>
// ),
// },
{
title: t('状态'),
dataIndex: 'status',
@@ -292,33 +362,23 @@ const MultiKeyManageModal = ({
render: (_, record) => (
<Space>
{record.status === 1 ? (
<Popconfirm
title={t('确定要禁用此密钥吗?')}
content={t('禁用后该密钥将不再被使用')}
onConfirm={() => handleDisableKey(record.index)}
<Button
type='danger'
size='small'
loading={operationLoading[`disable_${record.index}`]}
onClick={() => handleDisableKey(record.index)}
>
<Button
type='danger'
size='small'
loading={operationLoading[`disable_${record.index}`]}
>
{t('禁用')}
</Button>
</Popconfirm>
{t('禁用')}
</Button>
) : (
<Popconfirm
title={t('确定要启用此密钥吗?')}
content={t('启用后该密钥将重新被使用')}
onConfirm={() => handleEnableKey(record.index)}
<Button
type='primary'
size='small'
loading={operationLoading[`enable_${record.index}`]}
onClick={() => handleEnableKey(record.index)}
>
<Button
type='primary'
size='small'
loading={operationLoading[`enable_${record.index}`]}
>
{t('启用')}
</Button>
</Popconfirm>
{t('启用')}
</Button>
)}
</Space>
),
@@ -347,21 +407,48 @@ const MultiKeyManageModal = ({
>
{t('刷新')}
</Button>
{autoDisabledCount > 0 && (
<Popconfirm
title={t('确定要启用所有密钥吗?')}
onConfirm={handleEnableAll}
position={'topRight'}
>
<Button
type='primary'
loading={operationLoading.enable_all}
>
{t('启用全部')}
</Button>
</Popconfirm>
{enabledCount > 0 && (
<Popconfirm
title={t('确定要删除所有已自动禁用的密钥吗?')}
content={t('此操作不可撤销,将永久删除已自动禁用的密钥')}
onConfirm={handleDeleteDisabledKeys}
title={t('确定要禁用所有的密钥吗?')}
onConfirm={handleDisableAll}
okType={'danger'}
position={'topRight'}
>
<Button
type='danger'
icon={<IconDelete />}
loading={operationLoading.delete_disabled}
loading={operationLoading.disable_all}
>
{t('删除自动禁用密钥')}
{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>
}
>
@@ -391,6 +478,28 @@ const MultiKeyManageModal = ({
}
/>
{/* Filter Controls */}
<div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<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 */}
<Spin spinning={loading}>
{keyStatusList.length > 0 ? (