diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 0bea28a8..da6a169e 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -4841,11 +4841,23 @@ const handleGroupRefresh = async () => { // 处理 API Key 管理模态框刷新 const handleApiKeyRefresh = async () => { // 刷新账户信息以更新 API Key 数量 - if (props.account?.id) { + if (!props.account?.id) { + return + } + + const refreshers = [ + typeof accountsStore.fetchDroidAccounts === 'function' + ? accountsStore.fetchDroidAccounts + : null, + typeof accountsStore.fetchAllAccounts === 'function' ? accountsStore.fetchAllAccounts : null + ].filter(Boolean) + + for (const refresher of refreshers) { try { - await accountsStore.fetchAccounts() + await refresher() + return } catch (error) { - console.error('Failed to refresh account data:', error) + console.error('刷新账户列表失败:', error) } } } diff --git a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue index ad062150..dc6f95c3 100644 --- a/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue +++ b/web/admin-spa/src/components/accounts/ApiKeyManagementModal.vue @@ -20,12 +20,28 @@

- +
+ + +
@@ -239,6 +255,7 @@ const resetting = ref(null) const apiKeys = ref([]) const currentPage = ref(1) const pageSize = ref(18) +const copyingAll = ref(false) // 计算属性 const totalItems = computed(() => apiKeys.value.length) @@ -403,14 +420,71 @@ const maskApiKey = (key) => { return `${key.substring(0, 8)}...${key.substring(key.length - 4)}` } +// 写入剪贴板(带回退逻辑) +const writeToClipboard = async (text) => { + const canUseClipboardApi = + typeof navigator !== 'undefined' && + navigator.clipboard && + typeof navigator.clipboard.writeText === 'function' && + (typeof window === 'undefined' || window.isSecureContext !== false) + + if (canUseClipboardApi) { + await navigator.clipboard.writeText(text) + return + } + + if (typeof document === 'undefined') { + throw new Error('clipboard unavailable') + } + + const textarea = document.createElement('textarea') + textarea.value = text + textarea.setAttribute('readonly', '') + textarea.style.position = 'fixed' + textarea.style.opacity = '0' + textarea.style.pointerEvents = 'none' + document.body.appendChild(textarea) + textarea.select() + + try { + const success = document.execCommand('copy') + document.body.removeChild(textarea) + if (!success) { + throw new Error('execCommand failed') + } + } catch (error) { + document.body.removeChild(textarea) + throw error + } +} + // 复制 API Key const copyApiKey = async (key) => { try { - await navigator.clipboard.writeText(key) + await writeToClipboard(key) showToast('API Key 已复制', 'success') } catch (error) { console.error('Failed to copy:', error) - showToast('复制失败', 'error') + showToast('复制失败,请手动复制', 'error') + } +} + +// 复制全部 API Key +const copyAllApiKeys = async () => { + if (!apiKeys.value.length || copyingAll.value) { + return + } + + copyingAll.value = true + try { + const allKeysText = apiKeys.value.map((item) => item.key).join('\n') + await writeToClipboard(allKeysText) + showToast(`已复制 ${apiKeys.value.length} 条 API Key`, 'success') + } catch (error) { + console.error('Failed to copy all keys:', error) + showToast('复制全部 API Key 失败,请手动复制', 'error') + } finally { + copyingAll.value = false } }