diff --git a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue index ad062150..c1c64efe 100644 --- a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue +++ b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue @@ -34,16 +34,173 @@

加载中...

- +

暂无 API Key

-
+ +
+ +
+ +
+ +
+ +
+ + 筛选: +
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ +
+ 批量操作: + + +
+ + +
+ + +
+ + + 显示 {{ filteredApiKeys.length }} 个 + +
+
+
+
-
+
@@ -238,21 +392,66 @@ const deleting = ref(null) const resetting = ref(null) const apiKeys = ref([]) const currentPage = ref(1) -const pageSize = ref(18) +const pageSize = ref(15) + +// 新增:筛选和搜索相关状态 +const statusFilter = ref('all') // 'all' | 'active' | 'error' +const searchQuery = ref('') +const searchMode = ref('fuzzy') // 'fuzzy' | 'exact' +const batchDeleting = ref(false) + +// 掩码显示 API Key(提前声明供 computed 使用) +const maskApiKey = (key) => { + if (!key || key.length < 12) { + return key + } + return `${key.substring(0, 8)}...${key.substring(key.length - 4)}` +} + +// 计算属性:筛选后的 API Keys +const filteredApiKeys = computed(() => { + let filtered = apiKeys.value + + // 状态筛选 + if (statusFilter.value !== 'all') { + filtered = filtered.filter((key) => key.status === statusFilter.value) + } + + // 搜索筛选(使用完整的 key 进行搜索) + if (searchQuery.value.trim()) { + const query = searchQuery.value.trim() + filtered = filtered.filter((key) => { + const fullKey = key.key // 使用完整的 key + if (searchMode.value === 'exact') { + // 精确搜索:完全匹配完整的 key + return fullKey === query + } else { + // 模糊搜索:完整 key 包含查询字符串(不区分大小写) + return fullKey.toLowerCase().includes(query.toLowerCase()) + } + }) + } + + return filtered +}) // 计算属性 -const totalItems = computed(() => apiKeys.value.length) +const totalItems = computed(() => filteredApiKeys.value.length) const totalPages = computed(() => Math.ceil(totalItems.value / pageSize.value)) const paginatedApiKeys = computed(() => { const start = (currentPage.value - 1) * pageSize.value const end = start + pageSize.value - return apiKeys.value.slice(start, end) + return filteredApiKeys.value.slice(start, end) }) -// 获取原始索引的方法 -const getOriginalIndex = (paginatedIndex) => { - return (currentPage.value - 1) * pageSize.value + paginatedIndex -} +// 统计数量 +const activeKeysCount = computed(() => { + return apiKeys.value.filter((key) => key.status === 'active').length +}) + +const errorKeysCount = computed(() => { + return apiKeys.value.filter((key) => key.status === 'error').length +}) // 加载 API Keys const loadApiKeys = async () => { @@ -332,12 +531,12 @@ const loadApiKeys = async () => { } // 删除 API Key -const deleteApiKey = async (apiKey, index) => { +const deleteApiKey = async (apiKey) => { if (!confirm(`确定要删除 API Key "${maskApiKey(apiKey.key)}" 吗?`)) { return } - deleting.value = index + deleting.value = apiKey.key try { // 准备更新数据:删除指定的 key const updateData = { @@ -359,7 +558,7 @@ const deleteApiKey = async (apiKey, index) => { } // 重置 API Key 状态 -const resetApiKeyStatus = async (apiKey, index) => { +const resetApiKeyStatus = async (apiKey) => { if ( !confirm( `确定要重置 API Key "${maskApiKey(apiKey.key)}" 的状态吗?这将清除错误信息并恢复为正常状态。` @@ -368,7 +567,7 @@ const resetApiKeyStatus = async (apiKey, index) => { return } - resetting.value = index + resetting.value = apiKey.key try { // 准备更新数据:重置指定 key 的状态 const updateData = { @@ -395,12 +594,113 @@ const resetApiKeyStatus = async (apiKey, index) => { } } -// 掩码显示 API Key -const maskApiKey = (key) => { - if (!key || key.length < 12) { - return key +// 批量删除所有异常状态的 Key +const deleteAllErrorKeys = async () => { + const errorKeys = apiKeys.value.filter((key) => key.status === 'error') + if (errorKeys.length === 0) { + showToast('没有异常状态的 API Key', 'warning') + return } - return `${key.substring(0, 8)}...${key.substring(key.length - 4)}` + + if (!confirm(`确定要删除所有 ${errorKeys.length} 个异常状态的 API Key 吗?此操作不可恢复!`)) { + return + } + + batchDeleting.value = true + try { + const keysToDelete = errorKeys.map((key) => key.key) + const updateData = { + removeApiKeys: keysToDelete, + apiKeyUpdateMode: 'delete' + } + + await apiClient.put(`/admin/droid-accounts/${props.accountId}`, updateData) + + showToast(`成功删除 ${errorKeys.length} 个异常 API Key`, 'success') + await loadApiKeys() + emit('refresh') + } catch (error) { + console.error('Failed to delete error API keys:', error) + showToast(error.response?.data?.error || '批量删除失败', 'error') + } finally { + batchDeleting.value = false + } +} + +// 批量删除所有 Key +const deleteAllKeys = async () => { + if (apiKeys.value.length === 0) { + showToast('没有可删除的 API Key', 'warning') + return + } + + if ( + !confirm( + `确定要删除所有 ${apiKeys.value.length} 个 API Key 吗?此操作不可恢复!\n\n请再次确认:这将删除该账户下的所有 API Key。` + ) + ) { + return + } + + // 二次确认 + if (!confirm('最后确认:真的要删除所有 API Key 吗?')) { + return + } + + batchDeleting.value = true + try { + const keysToDelete = apiKeys.value.map((key) => key.key) + const updateData = { + removeApiKeys: keysToDelete, + apiKeyUpdateMode: 'delete' + } + + await apiClient.put(`/admin/droid-accounts/${props.accountId}`, updateData) + + showToast(`成功删除所有 ${keysToDelete.length} 个 API Key`, 'success') + await loadApiKeys() + emit('refresh') + } catch (error) { + console.error('Failed to delete all API keys:', error) + showToast(error.response?.data?.error || '批量删除失败', 'error') + } finally { + batchDeleting.value = false + } +} + +// 导出 Key +const exportKeys = (type) => { + let keysToExport = [] + let filename = '' + + if (type === 'error') { + keysToExport = apiKeys.value.filter((key) => key.status === 'error') + filename = `error_api_keys_${props.accountName}_${new Date().toISOString().split('T')[0]}.txt` + } else { + keysToExport = apiKeys.value + filename = `all_api_keys_${props.accountName}_${new Date().toISOString().split('T')[0]}.txt` + } + + if (keysToExport.length === 0) { + showToast('没有可导出的 API Key', 'warning') + return + } + + // 生成 TXT 内容(每行一个完整的 key) + const content = keysToExport.map((key) => key.key).join('\n') + + // 创建下载 + const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + + showToast(`成功导出 ${keysToExport.length} 个 API Key`, 'success') } // 复制 API Key