From 8ab3c76c6fda28cee9a387b64eb08d7ca12d00e3 Mon Sep 17 00:00:00 2001 From: AAEE86 Date: Wed, 15 Oct 2025 15:35:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20API=20Key=20?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E5=92=8C=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20-=20=E7=AD=9B=E9=80=89key=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=AD=9B=E9=80=89=E6=AD=A3=E5=B8=B8=E5=92=8C?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E7=8A=B6=E6=80=81=E7=9A=84key=20-=20?= =?UTF-8?q?=E6=90=9C=E7=B4=A2key=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=A8=A1=E7=B3=8A/=E7=B2=BE=E7=A1=AE=E6=90=9C?= =?UTF-8?q?=E7=B4=A2key=20-=20=E5=88=A0=E9=99=A4key=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=94=AF=E6=8C=81=E4=B8=80=E9=94=AE=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=89=80=E6=9C=89=E5=BC=82=E5=B8=B8=E7=8A=B6=E6=80=81=E7=9A=84?= =?UTF-8?q?key=E6=88=96=E8=80=85=E5=88=A0=E9=99=A4=E6=89=80=E6=9C=89key=20?= =?UTF-8?q?-=20=E5=AF=BC=E5=87=BAkey=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=80=E9=94=AE=E5=AF=BC=E5=87=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E7=8A=B6=E6=80=81=E7=9A=84key=E6=88=96?= =?UTF-8?q?=E8=80=85=E5=AF=BC=E5=87=BA=E6=89=80=E6=9C=89key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../accounts/ApiKeyManagementModal.vue | 356 ++++++++++++++++-- 1 file changed, 328 insertions(+), 28 deletions(-) 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