From ecdd9d1ccbe2ce0ccdf452549bff5846dc50e022 Mon Sep 17 00:00:00 2001 From: CaIon Date: Mon, 4 Aug 2025 16:52:31 +0800 Subject: [PATCH 1/4] feat: add multi-key management --- controller/channel.go | 258 ++++++++++++ model/channel.go | 33 +- model/channel_cache.go | 2 +- router/api-router.go | 1 + .../table/channels/ChannelsColumnDefs.js | 105 +++-- .../table/channels/ChannelsTable.jsx | 7 + web/src/components/table/channels/index.jsx | 7 + .../channels/modals/MultiKeyManageModal.jsx | 372 ++++++++++++++++++ web/src/hooks/channels/useChannelsData.js | 10 + 9 files changed, 730 insertions(+), 65 deletions(-) create mode 100644 web/src/components/table/channels/modals/MultiKeyManageModal.jsx diff --git a/controller/channel.go b/controller/channel.go index d9e4d422..a2ee5743 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -1030,3 +1030,261 @@ func CopyChannel(c *gin.Context) { // success c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": gin.H{"id": clone.Id}}) } + +// MultiKeyManageRequest represents the request for multi-key management operations +type MultiKeyManageRequest struct { + ChannelId int `json:"channel_id"` + Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status" + KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions +} + +// MultiKeyStatusResponse represents the response for key status query +type MultiKeyStatusResponse struct { + Keys []KeyStatus `json:"keys"` +} + +type KeyStatus struct { + Index int `json:"index"` + Status int `json:"status"` // 1: enabled, 2: disabled + DisabledTime int64 `json:"disabled_time,omitempty"` + Reason string `json:"reason,omitempty"` + KeyPreview string `json:"key_preview"` // first 10 chars of key for identification +} + +// ManageMultiKeys handles multi-key management operations +func ManageMultiKeys(c *gin.Context) { + request := MultiKeyManageRequest{} + err := c.ShouldBindJSON(&request) + if err != nil { + common.ApiError(c, err) + return + } + + channel, err := model.GetChannelById(request.ChannelId, true) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "渠道不存在", + }) + return + } + + if !channel.ChannelInfo.IsMultiKey { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "该渠道不是多密钥模式", + }) + return + } + + switch request.Action { + case "get_key_status": + keys := channel.GetKeys() + var keyStatusList []KeyStatus + + for i, key := range keys { + status := 1 // default enabled + var disabledTime int64 + var reason string + + if channel.ChannelInfo.MultiKeyStatusList != nil { + if s, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists { + status = s + } + } + + if status != 1 { + if channel.ChannelInfo.MultiKeyDisabledTime != nil { + disabledTime = channel.ChannelInfo.MultiKeyDisabledTime[i] + } + if channel.ChannelInfo.MultiKeyDisabledReason != nil { + reason = channel.ChannelInfo.MultiKeyDisabledReason[i] + } + } + + // Create key preview (first 10 chars) + keyPreview := key + if len(key) > 10 { + keyPreview = key[:10] + "..." + } + + keyStatusList = append(keyStatusList, KeyStatus{ + Index: i, + Status: status, + DisabledTime: disabledTime, + Reason: reason, + KeyPreview: keyPreview, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": MultiKeyStatusResponse{Keys: keyStatusList}, + }) + return + + case "disable_key": + if request.KeyIndex == nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "未指定要禁用的密钥索引", + }) + return + } + + keyIndex := *request.KeyIndex + if keyIndex < 0 || keyIndex >= channel.ChannelInfo.MultiKeySize { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "密钥索引超出范围", + }) + return + } + + 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) + } + + channel.ChannelInfo.MultiKeyStatusList[keyIndex] = 2 // disabled + channel.ChannelInfo.MultiKeyDisabledTime[keyIndex] = common.GetTimestamp() + channel.ChannelInfo.MultiKeyDisabledReason[keyIndex] = "手动禁用" + + err = channel.Update() + if err != nil { + common.ApiError(c, err) + return + } + + model.InitChannelCache() + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "密钥已禁用", + }) + return + + case "enable_key": + if request.KeyIndex == nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "未指定要启用的密钥索引", + }) + return + } + + keyIndex := *request.KeyIndex + if keyIndex < 0 || keyIndex >= channel.ChannelInfo.MultiKeySize { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "密钥索引超出范围", + }) + return + } + + // 从状态列表中删除该密钥的记录,使其回到默认启用状态 + if channel.ChannelInfo.MultiKeyStatusList != nil { + delete(channel.ChannelInfo.MultiKeyStatusList, keyIndex) + } + if channel.ChannelInfo.MultiKeyDisabledTime != nil { + delete(channel.ChannelInfo.MultiKeyDisabledTime, keyIndex) + } + if channel.ChannelInfo.MultiKeyDisabledReason != nil { + delete(channel.ChannelInfo.MultiKeyDisabledReason, keyIndex) + } + + err = channel.Update() + if err != nil { + common.ApiError(c, err) + return + } + + model.InitChannelCache() + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "密钥已启用", + }) + return + + case "delete_disabled_keys": + keys := channel.GetKeys() + var remainingKeys []string + var deletedCount int + var newStatusList = make(map[int]int) + var newDisabledTime = make(map[int]int64) + var newDisabledReason = make(map[int]string) + + newIndex := 0 + for i, key := range keys { + status := 1 // default enabled + if channel.ChannelInfo.MultiKeyStatusList != nil { + if s, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists { + status = s + } + } + + // 只删除自动禁用(status == 3)的密钥,保留启用(status == 1)和手动禁用(status == 2)的密钥 + if status == 3 { + deletedCount++ + } else { + remainingKeys = append(remainingKeys, key) + // 保留非自动禁用密钥的状态信息,重新索引 + if status != 1 { + newStatusList[newIndex] = status + if channel.ChannelInfo.MultiKeyDisabledTime != nil { + if t, exists := channel.ChannelInfo.MultiKeyDisabledTime[i]; exists { + newDisabledTime[newIndex] = t + } + } + if channel.ChannelInfo.MultiKeyDisabledReason != nil { + if r, exists := channel.ChannelInfo.MultiKeyDisabledReason[i]; exists { + newDisabledReason[newIndex] = r + } + } + } + newIndex++ + } + } + + if deletedCount == 0 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "没有需要删除的自动禁用密钥", + }) + return + } + + // Update channel with remaining keys + channel.Key = strings.Join(remainingKeys, "\n") + channel.ChannelInfo.MultiKeySize = len(remainingKeys) + channel.ChannelInfo.MultiKeyStatusList = newStatusList + channel.ChannelInfo.MultiKeyDisabledTime = newDisabledTime + channel.ChannelInfo.MultiKeyDisabledReason = newDisabledReason + + 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 个自动禁用的密钥", deletedCount), + "data": deletedCount, + }) + return + + default: + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "不支持的操作", + }) + return + } +} diff --git a/model/channel.go b/model/channel.go index bcffc102..502171fa 100644 --- a/model/channel.go +++ b/model/channel.go @@ -41,6 +41,7 @@ type Channel struct { Priority *int64 `json:"priority" gorm:"bigint;default:0"` AutoBan *int `json:"auto_ban" gorm:"default:1"` OtherInfo string `json:"other_info"` + Settings string `json:"settings"` Tag *string `json:"tag" gorm:"index"` Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置 ParamOverride *string `json:"param_override" gorm:"type:text"` @@ -52,11 +53,13 @@ type Channel struct { } type ChannelInfo struct { - IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式 - MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量 - MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status - MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引 - MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"` + IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式 + MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量 + MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status + MultiKeyDisabledReason map[int]string `json:"multi_key_disabled_reason"` // key禁用原因列表,key index -> reason + MultiKeyDisabledTime map[int]int64 `json:"multi_key_disabled_time"` // key禁用时间列表,key index -> time + MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引 + MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"` } // Value implements driver.Valuer interface @@ -70,7 +73,7 @@ func (c *ChannelInfo) Scan(value interface{}) error { return common.Unmarshal(bytesValue, c) } -func (channel *Channel) getKeys() []string { +func (channel *Channel) GetKeys() []string { if channel.Key == "" { return []string{} } @@ -101,7 +104,7 @@ func (channel *Channel) GetNextEnabledKey() (string, int, *types.NewAPIError) { } // Obtain all keys (split by \n) - keys := channel.getKeys() + keys := channel.GetKeys() if len(keys) == 0 { // No keys available, return error, should disable the channel return "", 0, types.NewError(errors.New("no keys available"), types.ErrorCodeChannelNoAvailableKey) @@ -528,8 +531,8 @@ func CleanupChannelPollingLocks() { }) } -func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int) { - keys := channel.getKeys() +func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int, reason string) { + keys := channel.GetKeys() if len(keys) == 0 { channel.Status = status } else { @@ -547,6 +550,14 @@ func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int) { delete(channel.ChannelInfo.MultiKeyStatusList, keyIndex) } else { channel.ChannelInfo.MultiKeyStatusList[keyIndex] = status + if channel.ChannelInfo.MultiKeyDisabledReason == nil { + channel.ChannelInfo.MultiKeyDisabledReason = make(map[int]string) + } + if channel.ChannelInfo.MultiKeyDisabledTime == nil { + channel.ChannelInfo.MultiKeyDisabledTime = make(map[int]int64) + } + channel.ChannelInfo.MultiKeyDisabledReason[keyIndex] = reason + channel.ChannelInfo.MultiKeyDisabledTime[keyIndex] = common.GetTimestamp() } if len(channel.ChannelInfo.MultiKeyStatusList) >= channel.ChannelInfo.MultiKeySize { channel.Status = common.ChannelStatusAutoDisabled @@ -569,7 +580,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri } if channelCache.ChannelInfo.IsMultiKey { // 如果是多Key模式,更新缓存中的状态 - handlerMultiKeyUpdate(channelCache, usingKey, status) + handlerMultiKeyUpdate(channelCache, usingKey, status, reason) //CacheUpdateChannel(channelCache) //return true } else { @@ -600,7 +611,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri if channel.ChannelInfo.IsMultiKey { beforeStatus := channel.Status - handlerMultiKeyUpdate(channel, usingKey, status) + handlerMultiKeyUpdate(channel, usingKey, status, reason) if beforeStatus != channel.Status { shouldUpdateAbilities = true } diff --git a/model/channel_cache.go b/model/channel_cache.go index ecd87607..6ca23cf9 100644 --- a/model/channel_cache.go +++ b/model/channel_cache.go @@ -70,7 +70,7 @@ func InitChannelCache() { //channelsIDM = newChannelId2channel for i, channel := range newChannelId2channel { if channel.ChannelInfo.IsMultiKey { - channel.Keys = channel.getKeys() + channel.Keys = channel.GetKeys() if channel.ChannelInfo.MultiKeyMode == constant.MultiKeyModePolling { if oldChannel, ok := channelsIDM[i]; ok { // 存在旧的渠道,如果是多key且轮询,保留轮询索引信息 diff --git a/router/api-router.go b/router/api-router.go index bc49803a..12846012 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -120,6 +120,7 @@ func SetApiRouter(router *gin.Engine) { channelRoute.POST("/batch/tag", controller.BatchSetChannelTag) channelRoute.GET("/tag/models", controller.GetTagModels) channelRoute.POST("/copy/:id", controller.CopyChannel) + channelRoute.POST("/multi_key/manage", controller.ManageMultiKeys) } tokenRoute := apiRouter.Group("/token") tokenRoute.Use(middleware.UserAuth()) diff --git a/web/src/components/table/channels/ChannelsColumnDefs.js b/web/src/components/table/channels/ChannelsColumnDefs.js index beb5fe55..18cb5700 100644 --- a/web/src/components/table/channels/ChannelsColumnDefs.js +++ b/web/src/components/table/channels/ChannelsColumnDefs.js @@ -210,7 +210,9 @@ export const getChannelsColumns = ({ copySelectedChannel, refresh, activePage, - channels + channels, + setShowMultiKeyManageModal, + setCurrentMultiKeyChannel }) => { return [ { @@ -503,47 +505,7 @@ export const getChannelsColumns = ({ /> - {record.channel_info?.is_multi_key ? ( - - { - record.status === 1 ? ( - - ) : ( - - ) - } - manageChannel(record.id, 'enable_all', record), - } - ]} - > - + {record.channel_info?.is_multi_key ? ( + + + { + setCurrentMultiKeyChannel(record); + setShowMultiKeyManageModal(true); + }, + } + ]} + > + + )} { setEditingTag, copySelectedChannel, refresh, + // Multi-key management + setShowMultiKeyManageModal, + setCurrentMultiKeyChannel, } = channelsData; // Get all columns @@ -79,6 +82,8 @@ const ChannelsTable = (channelsData) => { refresh, activePage, channels, + setShowMultiKeyManageModal, + setCurrentMultiKeyChannel, }); }, [ t, @@ -98,6 +103,8 @@ const ChannelsTable = (channelsData) => { refresh, activePage, channels, + setShowMultiKeyManageModal, + setCurrentMultiKeyChannel, ]); // Filter columns based on visibility settings diff --git a/web/src/components/table/channels/index.jsx b/web/src/components/table/channels/index.jsx index b0106b4e..66e2d72d 100644 --- a/web/src/components/table/channels/index.jsx +++ b/web/src/components/table/channels/index.jsx @@ -30,6 +30,7 @@ import ModelTestModal from './modals/ModelTestModal.jsx'; import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx'; import EditChannelModal from './modals/EditChannelModal.jsx'; import EditTagModal from './modals/EditTagModal.jsx'; +import MultiKeyManageModal from './modals/MultiKeyManageModal.jsx'; import { createCardProPagination } from '../../../helpers/utils'; const ChannelsPage = () => { @@ -54,6 +55,12 @@ const ChannelsPage = () => { /> + channelsData.setShowMultiKeyManageModal(false)} + channel={channelsData.currentMultiKeyChannel} + onRefresh={channelsData.refresh} + /> {/* Main Content */} . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Modal, + Button, + Table, + Tag, + Typography, + Space, + Tooltip, + Popconfirm, + Empty, + Spin, + Banner +} from '@douyinfe/semi-ui'; +import { + IconRefresh, + IconDelete, + IconClose, + IconSave, + IconSetting +} from '@douyinfe/semi-icons'; +import { API, showError, showSuccess, timestamp2string } from '../../../../helpers/index.js'; + +const { Text, Title } = Typography; + +const MultiKeyManageModal = ({ + visible, + onCancel, + channel, + onRefresh +}) => { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [keyStatusList, setKeyStatusList] = useState([]); + const [operationLoading, setOperationLoading] = useState({}); + + // Load key status data + const loadKeyStatus = async () => { + if (!channel?.id) return; + + setLoading(true); + try { + const res = await API.post('/api/channel/multi_key/manage', { + channel_id: channel.id, + action: 'get_key_status' + }); + + if (res.data.success) { + setKeyStatusList(res.data.data.keys || []); + } else { + showError(res.data.message); + } + } catch (error) { + showError(t('获取密钥状态失败')); + } finally { + setLoading(false); + } + }; + + // Disable a specific key + 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(); // Reload data + onRefresh && onRefresh(); // Refresh parent component + } else { + showError(res.data.message); + } + } catch (error) { + showError(t('禁用密钥失败')); + } finally { + setOperationLoading(prev => ({ ...prev, [operationId]: false })); + } + }; + + // Enable a specific key + 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(); // Reload data + onRefresh && onRefresh(); // Refresh parent component + } else { + showError(res.data.message); + } + } catch (error) { + showError(t('启用密钥失败')); + } finally { + setOperationLoading(prev => ({ ...prev, [operationId]: false })); + } + }; + + // 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); + await loadKeyStatus(); // Reload data + onRefresh && onRefresh(); // Refresh parent component + } else { + showError(res.data.message); + } + } catch (error) { + showError(t('删除禁用密钥失败')); + } finally { + setOperationLoading(prev => ({ ...prev, delete_disabled: false })); + } + }; + + // Effect to load data when modal opens + useEffect(() => { + if (visible && channel?.id) { + loadKeyStatus(); + } + }, [visible, channel?.id]); + + // Get status tag component + const renderStatusTag = (status) => { + switch (status) { + case 1: + return {t('已启用')}; + case 2: + return {t('已禁用')}; + case 3: + return {t('自动禁用')}; + default: + return {t('未知状态')}; + } + }; + + // Table columns definition + const columns = [ + { + title: t('索引'), + dataIndex: 'index', + render: (text) => `#${text}`, + }, + { + title: t('密钥预览'), + dataIndex: 'key_preview', + render: (text) => ( + + {text} + + ), + }, + { + 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 -; + } + return ( + + + {reason} + + + ); + }, + }, + { + title: t('禁用时间'), + dataIndex: 'disabled_time', + width: 150, + render: (time, record) => { + if (record.status === 1 || !time) { + return -; + } + return ( + + + {timestamp2string(time)} + + + ); + }, + }, + { + title: t('操作'), + key: 'action', + width: 120, + render: (_, record) => ( + + {record.status === 1 ? ( + handleDisableKey(record.index)} + > + + + ) : ( + handleEnableKey(record.index)} + > + + + )} + + ), + }, + ]; + + // Calculate statistics + const enabledCount = keyStatusList.filter(key => key.status === 1).length; + const manualDisabledCount = keyStatusList.filter(key => key.status === 2).length; + const autoDisabledCount = keyStatusList.filter(key => key.status === 3).length; + const totalCount = keyStatusList.length; + + return ( + + + {t('多密钥管理')} - {channel?.name} + + } + visible={visible} + onCancel={onCancel} + width={800} + height={600} + footer={ + + + + {autoDisabledCount > 0 && ( + + + + )} + + } + > +
+ {/* Statistics Banner */} + + + {t('总共 {{total}} 个密钥,{{enabled}} 个已启用,{{manual}} 个手动禁用,{{auto}} 个自动禁用', { + total: totalCount, + enabled: enabledCount, + manual: manualDisabledCount, + auto: autoDisabledCount + })} + + {channel?.channel_info?.multi_key_mode && ( +
+ + {t('多密钥模式')}: {channel.channel_info.multi_key_mode === 'random' ? t('随机') : t('轮询')} + +
+ )} +
+ } + /> + + {/* Key Status Table */} + + {keyStatusList.length > 0 ? ( + + ) : ( + !loading && ( + + ) + )} + + + + ); +}; + +export default MultiKeyManageModal; \ No newline at end of file diff --git a/web/src/hooks/channels/useChannelsData.js b/web/src/hooks/channels/useChannelsData.js index d188c9fe..8f1f8c29 100644 --- a/web/src/hooks/channels/useChannelsData.js +++ b/web/src/hooks/channels/useChannelsData.js @@ -83,6 +83,10 @@ export const useChannelsData = () => { const [isProcessingQueue, setIsProcessingQueue] = useState(false); const [modelTablePage, setModelTablePage] = useState(1); + // Multi-key management states + const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false); + const [currentMultiKeyChannel, setCurrentMultiKeyChannel] = useState(null); + // Refs const requestCounter = useRef(0); const allSelectingRef = useRef(false); @@ -885,6 +889,12 @@ export const useChannelsData = () => { setModelTablePage, allSelectingRef, + // Multi-key management states + showMultiKeyManageModal, + setShowMultiKeyManageModal, + currentMultiKeyChannel, + setCurrentMultiKeyChannel, + // Form formApi, setFormApi, From 8357b15fec0a3e1a6c6607be12b641ee050fd6c0 Mon Sep 17 00:00:00 2001 From: CaIon Date: Mon, 4 Aug 2025 17:15:32 +0800 Subject: [PATCH 2/4] feat: enhance multi-key management with pagination and statistics --- controller/channel.go | 122 ++++++++++++--- model/channel.go | 12 +- .../channels/modals/MultiKeyManageModal.jsx | 147 +++++++++++++++--- 3 files changed, 228 insertions(+), 53 deletions(-) diff --git a/controller/channel.go b/controller/channel.go index a2ee5743..440815cc 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -71,6 +71,13 @@ func parseStatusFilter(statusParam string) int { } } +func clearChannelInfo(channel *model.Channel) { + if channel.ChannelInfo.IsMultiKey { + channel.ChannelInfo.MultiKeyDisabledReason = nil + channel.ChannelInfo.MultiKeyDisabledTime = nil + } +} + func GetAllChannels(c *gin.Context) { pageInfo := common.GetPageQuery(c) channelData := make([]*model.Channel, 0) @@ -145,6 +152,10 @@ func GetAllChannels(c *gin.Context) { } } + for _, datum := range channelData { + clearChannelInfo(datum) + } + countQuery := model.DB.Model(&model.Channel{}) if statusFilter == common.ChannelStatusEnabled { countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled) @@ -371,6 +382,10 @@ func SearchChannels(c *gin.Context) { pagedData := channelData[startIdx:endIdx] + for _, datum := range pagedData { + clearChannelInfo(datum) + } + c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", @@ -394,6 +409,9 @@ func GetChannel(c *gin.Context) { common.ApiError(c, err) return } + if channel != nil { + clearChannelInfo(channel) + } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", @@ -827,6 +845,7 @@ func UpdateChannel(c *gin.Context) { } model.InitChannelCache() channel.Key = "" + clearChannelInfo(&channel.Channel) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", @@ -1036,11 +1055,21 @@ type MultiKeyManageRequest struct { ChannelId int `json:"channel_id"` Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status" 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 } // MultiKeyStatusResponse represents the response for key status query type MultiKeyStatusResponse struct { - Keys []KeyStatus `json:"keys"` + Keys []KeyStatus `json:"keys"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` + // Statistics + EnabledCount int `json:"enabled_count"` + ManualDisabledCount int `json:"manual_disabled_count"` + AutoDisabledCount int `json:"auto_disabled_count"` } type KeyStatus struct { @@ -1080,8 +1109,35 @@ func ManageMultiKeys(c *gin.Context) { switch request.Action { case "get_key_status": keys := channel.GetKeys() - var keyStatusList []KeyStatus + total := len(keys) + // Default pagination parameters + page := request.Page + pageSize := request.PageSize + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + 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 + var enabledCount, manualDisabledCount, autoDisabledCount int + + var keyStatusList []KeyStatus for i, key := range keys { status := 1 // default enabled var disabledTime int64 @@ -1093,34 +1149,56 @@ func ManageMultiKeys(c *gin.Context) { } } - if status != 1 { - if channel.ChannelInfo.MultiKeyDisabledTime != nil { - disabledTime = channel.ChannelInfo.MultiKeyDisabledTime[i] - } - if channel.ChannelInfo.MultiKeyDisabledReason != nil { - reason = channel.ChannelInfo.MultiKeyDisabledReason[i] - } + // Count for statistics + switch status { + case 1: + enabledCount++ + case 2: + manualDisabledCount++ + case 3: + autoDisabledCount++ } - // Create key preview (first 10 chars) - keyPreview := key - if len(key) > 10 { - keyPreview = key[:10] + "..." - } + // 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] + } + } - 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] + "..." + } + + keyStatusList = append(keyStatusList, KeyStatus{ + Index: i, + Status: status, + DisabledTime: disabledTime, + Reason: reason, + KeyPreview: keyPreview, + }) + } } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", - "data": MultiKeyStatusResponse{Keys: keyStatusList}, + "data": MultiKeyStatusResponse{ + Keys: keyStatusList, + Total: total, + Page: page, + PageSize: pageSize, + TotalPages: totalPages, + EnabledCount: enabledCount, + ManualDisabledCount: manualDisabledCount, + AutoDisabledCount: autoDisabledCount, + }, }) return diff --git a/model/channel.go b/model/channel.go index 502171fa..280781f1 100644 --- a/model/channel.go +++ b/model/channel.go @@ -53,12 +53,12 @@ type Channel struct { } type ChannelInfo struct { - IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式 - MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量 - MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status - MultiKeyDisabledReason map[int]string `json:"multi_key_disabled_reason"` // key禁用原因列表,key index -> reason - MultiKeyDisabledTime map[int]int64 `json:"multi_key_disabled_time"` // key禁用时间列表,key index -> time - MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引 + IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式 + MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量 + MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status + MultiKeyDisabledReason map[int]string `json:"multi_key_disabled_reason,omitempty"` // key禁用原因列表,key index -> reason + MultiKeyDisabledTime map[int]int64 `json:"multi_key_disabled_time,omitempty"` // key禁用时间列表,key index -> time + MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引 MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"` } diff --git a/web/src/components/table/channels/modals/MultiKeyManageModal.jsx b/web/src/components/table/channels/modals/MultiKeyManageModal.jsx index 9ae46ea3..44f16c03 100644 --- a/web/src/components/table/channels/modals/MultiKeyManageModal.jsx +++ b/web/src/components/table/channels/modals/MultiKeyManageModal.jsx @@ -30,7 +30,9 @@ import { Popconfirm, Empty, Spin, - Banner + Banner, + Select, + Pagination } from '@douyinfe/semi-ui'; import { IconRefresh, @@ -53,24 +55,48 @@ 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 [total, setTotal] = useState(0); + const [totalPages, setTotalPages] = useState(0); + + // Statistics states + const [enabledCount, setEnabledCount] = useState(0); + const [manualDisabledCount, setManualDisabledCount] = useState(0); + const [autoDisabledCount, setAutoDisabledCount] = useState(0); // Load key status data - const loadKeyStatus = async () => { + const loadKeyStatus = async (page = currentPage, size = pageSize) => { if (!channel?.id) return; setLoading(true); try { const res = await API.post('/api/channel/multi_key/manage', { channel_id: channel.id, - action: 'get_key_status' + action: 'get_key_status', + page: page, + page_size: size }); if (res.data.success) { - setKeyStatusList(res.data.data.keys || []); + const data = res.data.data; + setKeyStatusList(data.keys || []); + setTotal(data.total || 0); + setCurrentPage(data.page || 1); + setPageSize(data.page_size || 50); + setTotalPages(data.total_pages || 0); + + // Update statistics + setEnabledCount(data.enabled_count || 0); + setManualDisabledCount(data.manual_disabled_count || 0); + setAutoDisabledCount(data.auto_disabled_count || 0); } else { showError(res.data.message); } } catch (error) { + console.error(error); showError(t('获取密钥状态失败')); } finally { setLoading(false); @@ -91,7 +117,7 @@ const MultiKeyManageModal = ({ if (res.data.success) { showSuccess(t('密钥已禁用')); - await loadKeyStatus(); // Reload data + await loadKeyStatus(currentPage, pageSize); // Reload current page onRefresh && onRefresh(); // Refresh parent component } else { showError(res.data.message); @@ -117,7 +143,7 @@ const MultiKeyManageModal = ({ if (res.data.success) { showSuccess(t('密钥已启用')); - await loadKeyStatus(); // Reload data + await loadKeyStatus(currentPage, pageSize); // Reload current page onRefresh && onRefresh(); // Refresh parent component } else { showError(res.data.message); @@ -141,7 +167,9 @@ const MultiKeyManageModal = ({ if (res.data.success) { showSuccess(res.data.message); - await loadKeyStatus(); // Reload data + // Reset to first page after deletion as data structure might change + setCurrentPage(1); + await loadKeyStatus(1, pageSize); onRefresh && onRefresh(); // Refresh parent component } else { showError(res.data.message); @@ -153,13 +181,40 @@ const MultiKeyManageModal = ({ } }; + // Handle page change + const handlePageChange = (page) => { + setCurrentPage(page); + loadKeyStatus(page, pageSize); + }; + + // Handle page size change + const handlePageSizeChange = (size) => { + setPageSize(size); + setCurrentPage(1); // Reset to first page + loadKeyStatus(1, size); + }; + // Effect to load data when modal opens useEffect(() => { if (visible && channel?.id) { - loadKeyStatus(); + setCurrentPage(1); // Reset to first page when opening + loadKeyStatus(1, pageSize); } }, [visible, channel?.id]); + // Reset pagination when modal closes + useEffect(() => { + if (!visible) { + setCurrentPage(1); + setKeyStatusList([]); + setTotal(0); + setTotalPages(0); + setEnabledCount(0); + setManualDisabledCount(0); + setAutoDisabledCount(0); + } + }, [visible]); + // Get status tag component const renderStatusTag = (status) => { switch (status) { @@ -270,12 +325,6 @@ const MultiKeyManageModal = ({ }, ]; - // Calculate statistics - const enabledCount = keyStatusList.filter(key => key.status === 1).length; - const manualDisabledCount = keyStatusList.filter(key => key.status === 2).length; - const autoDisabledCount = keyStatusList.filter(key => key.status === 3).length; - const totalCount = keyStatusList.length; - return ( {t('关闭')}
+ <> +
+ + {/* Pagination */} + {total > 0 && ( +
+ + {t('显示第 {{start}}-{{end}} 条,共 {{total}} 条', { + start: (currentPage - 1) * pageSize + 1, + end: Math.min(currentPage * pageSize, total), + total: total + })} + + +
+ + {t('每页显示')}: + + + + + t('第 {{current}} / {{total}} 页', { + current: currentPage, + total: totalPages + }) + } + /> +
+
+ )} + ) : ( !loading && ( Date: Mon, 4 Aug 2025 19:33:24 +0800 Subject: [PATCH 3/4] fix: correct option value for pagination in MultiKeyManageModal --- .../components/table/channels/modals/MultiKeyManageModal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/table/channels/modals/MultiKeyManageModal.jsx b/web/src/components/table/channels/modals/MultiKeyManageModal.jsx index 44f16c03..6bb14184 100644 --- a/web/src/components/table/channels/modals/MultiKeyManageModal.jsx +++ b/web/src/components/table/channels/modals/MultiKeyManageModal.jsx @@ -428,7 +428,7 @@ const MultiKeyManageModal = ({ > 50 100 - 500 + 500 1000 From 12b4e80d4b242d79153687b4e4a93ccaafc96cbf Mon Sep 17 00:00:00 2001 From: CaIon Date: Mon, 4 Aug 2025 19:51:58 +0800 Subject: [PATCH 4/4] feat: add status filtering and bulk enable/disable functionality in multi-key management --- controller/channel.go | 181 ++++++++++++---- .../channels/modals/MultiKeyManageModal.jsx | 197 ++++++++++++++---- 2 files changed, 288 insertions(+), 90 deletions(-) diff --git a/controller/channel.go b/controller/channel.go index 440815cc..7756e18f 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -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 diff --git a/web/src/components/table/channels/modals/MultiKeyManageModal.jsx b/web/src/components/table/channels/modals/MultiKeyManageModal.jsx index 6bb14184..161da1cc 100644 --- a/web/src/components/table/channels/modals/MultiKeyManageModal.jsx +++ b/web/src/components/table/channels/modals/MultiKeyManageModal.jsx @@ -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} - - ), - }, + // { + // title: t('密钥预览'), + // dataIndex: 'key_preview', + // render: (text) => ( + // + // {text} + // + // ), + // }, { title: t('状态'), dataIndex: 'status', @@ -292,33 +362,23 @@ const MultiKeyManageModal = ({ render: (_, record) => ( {record.status === 1 ? ( - handleDisableKey(record.index)} + - + {t('禁用')} + ) : ( - handleEnableKey(record.index)} + - + {t('启用')} + )} ), @@ -347,21 +407,48 @@ const MultiKeyManageModal = ({ > {t('刷新')} - {autoDisabledCount > 0 && ( + + + + {enabledCount > 0 && ( )} + + + } > @@ -391,6 +478,28 @@ const MultiKeyManageModal = ({ } /> + {/* Filter Controls */} +
+ {t('状态筛选')}: + + {statusFilter !== null && ( + + {t('当前显示 {{count}} 条筛选结果', { count: total })} + + )} +
+ {/* Key Status Table */} {keyStatusList.length > 0 ? (