From b5c0f85dcad2fbfdcd300349338cc5bb4b8cdf5d Mon Sep 17 00:00:00 2001 From: fawney19 Date: Sun, 28 Dec 2025 20:41:52 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E5=89=AA?= =?UTF-8?q?=E8=B4=B4=E6=9D=BF=E5=A4=8D=E5=88=B6=E5=8A=9F=E8=83=BD=E5=88=B0?= =?UTF-8?q?=20useClipboard=20=E7=BB=84=E5=90=88=E5=BC=8F=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将各个组件和视图中重复的剪贴板复制逻辑提取到 useClipboard 组合式函数。 增加 showToast 参数支持静默复制,减少代码重复,提高维护性。 --- frontend/src/composables/useClipboard.ts | 10 +++++----- .../features/models/components/ModelDetailDrawer.vue | 12 ++---------- .../providers/components/ProviderDetailDrawer.vue | 12 ++---------- .../providers/components/provider-tabs/ModelsTab.vue | 9 +++------ .../usage/components/RequestDetailDrawer.vue | 4 +++- frontend/src/views/admin/ApiKeys.vue | 12 ++++-------- frontend/src/views/admin/ModelManagement.vue | 12 ++---------- frontend/src/views/admin/Users.vue | 12 ++++-------- frontend/src/views/user/ModelCatalog.vue | 12 ++---------- .../views/user/components/UserModelDetailDrawer.vue | 11 ++--------- 10 files changed, 29 insertions(+), 77 deletions(-) diff --git a/frontend/src/composables/useClipboard.ts b/frontend/src/composables/useClipboard.ts index 1847339..1964bbe 100644 --- a/frontend/src/composables/useClipboard.ts +++ b/frontend/src/composables/useClipboard.ts @@ -4,11 +4,11 @@ import { log } from '@/utils/logger' export function useClipboard() { const { success, error: showError } = useToast() - async function copyToClipboard(text: string): Promise { + async function copyToClipboard(text: string, showToast = true): Promise { try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text) - success('已复制到剪贴板') + if (showToast) success('已复制到剪贴板') return true } @@ -25,17 +25,17 @@ export function useClipboard() { try { const successful = document.execCommand('copy') if (successful) { - success('已复制到剪贴板') + if (showToast) success('已复制到剪贴板') return true } - showError('复制失败,请手动复制') + if (showToast) showError('复制失败,请手动复制') return false } finally { document.body.removeChild(textArea) } } catch (err) { log.error('复制失败:', err) - showError('复制失败,请手动选择文本进行复制') + if (showToast) showError('复制失败,请手动选择文本进行复制') return false } } diff --git a/frontend/src/features/models/components/ModelDetailDrawer.vue b/frontend/src/features/models/components/ModelDetailDrawer.vue index ebbd39d..b36427c 100644 --- a/frontend/src/features/models/components/ModelDetailDrawer.vue +++ b/frontend/src/features/models/components/ModelDetailDrawer.vue @@ -700,6 +700,7 @@ import { } from 'lucide-vue-next' import { useEscapeKey } from '@/composables/useEscapeKey' import { useToast } from '@/composables/useToast' +import { useClipboard } from '@/composables/useClipboard' import Card from '@/components/ui/card.vue' import Badge from '@/components/ui/badge.vue' import Button from '@/components/ui/button.vue' @@ -731,6 +732,7 @@ const emit = defineEmits<{ 'refreshProviders': [] }>() const { success: showSuccess, error: showError } = useToast() +const { copyToClipboard } = useClipboard() interface Props { model: GlobalModelResponse | null @@ -763,16 +765,6 @@ function handleClose() { } } -// 复制到剪贴板 -async function copyToClipboard(text: string) { - try { - await navigator.clipboard.writeText(text) - showSuccess('已复制') - } catch { - showError('复制失败') - } -} - // 格式化日期 function formatDate(dateStr: string): string { if (!dateStr) return '-' diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index fdde2a9..efb15af 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -661,6 +661,7 @@ import Button from '@/components/ui/button.vue' import Badge from '@/components/ui/badge.vue' import Card from '@/components/ui/card.vue' import { useToast } from '@/composables/useToast' +import { useClipboard } from '@/composables/useClipboard' import { getProvider, getProviderEndpoints } from '@/api/endpoints' import { KeyFormDialog, @@ -706,6 +707,7 @@ const emit = defineEmits<{ }>() const { error: showError, success: showSuccess } = useToast() +const { copyToClipboard } = useClipboard() const loading = ref(false) const provider = ref(null) @@ -1250,16 +1252,6 @@ function getHealthScoreBarColor(score: number): string { return 'bg-red-500 dark:bg-red-400' } -// 复制到剪贴板 -async function copyToClipboard(text: string) { - try { - await navigator.clipboard.writeText(text) - showSuccess('已复制到剪贴板') - } catch { - showError('复制失败', '错误') - } -} - // 加载 Provider 信息 async function loadProvider() { if (!props.providerId) return diff --git a/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue b/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue index f5c35ba..8a875e2 100644 --- a/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue +++ b/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue @@ -224,6 +224,7 @@ import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image, import Card from '@/components/ui/card.vue' import Button from '@/components/ui/button.vue' import { useToast } from '@/composables/useToast' +import { useClipboard } from '@/composables/useClipboard' import { getProviderModels, testModel, type Model } from '@/api/endpoints' import { updateModel } from '@/api/endpoints/models' import { parseTestModelError } from '@/utils/errorParser' @@ -239,6 +240,7 @@ const emit = defineEmits<{ }>() const { error: showError, success: showSuccess } = useToast() +const { copyToClipboard } = useClipboard() // 状态 const loading = ref(false) @@ -257,12 +259,7 @@ const sortedModels = computed(() => { // 复制模型 ID 到剪贴板 async function copyModelId(modelId: string) { - try { - await navigator.clipboard.writeText(modelId) - showSuccess('已复制到剪贴板') - } catch { - showError('复制失败', '错误') - } + await copyToClipboard(modelId) } // 加载模型 diff --git a/frontend/src/features/usage/components/RequestDetailDrawer.vue b/frontend/src/features/usage/components/RequestDetailDrawer.vue index 23e38c6..28527ee 100644 --- a/frontend/src/features/usage/components/RequestDetailDrawer.vue +++ b/frontend/src/features/usage/components/RequestDetailDrawer.vue @@ -473,6 +473,7 @@ import { ref, watch, computed } from 'vue' import Button from '@/components/ui/button.vue' import { useEscapeKey } from '@/composables/useEscapeKey' +import { useClipboard } from '@/composables/useClipboard' import Card from '@/components/ui/card.vue' import Badge from '@/components/ui/badge.vue' import Separator from '@/components/ui/separator.vue' @@ -505,6 +506,7 @@ const copiedStates = ref>({}) const viewMode = ref<'compare' | 'formatted' | 'raw'>('compare') const currentExpandDepth = ref(1) const dataSource = ref<'client' | 'provider'>('client') +const { copyToClipboard } = useClipboard() const historicalPricing = ref<{ input_price: string output_price: string @@ -784,7 +786,7 @@ function copyJsonToClipboard(tabName: string) { } if (data) { - navigator.clipboard.writeText(JSON.stringify(data, null, 2)) + copyToClipboard(JSON.stringify(data, null, 2), false) copiedStates.value[tabName] = true setTimeout(() => { copiedStates.value[tabName] = false diff --git a/frontend/src/views/admin/ApiKeys.vue b/frontend/src/views/admin/ApiKeys.vue index bf07f03..8b85d5d 100644 --- a/frontend/src/views/admin/ApiKeys.vue +++ b/frontend/src/views/admin/ApiKeys.vue @@ -650,6 +650,7 @@ import { ref, computed, onMounted } from 'vue' import { useToast } from '@/composables/useToast' import { useConfirm } from '@/composables/useConfirm' +import { useClipboard } from '@/composables/useClipboard' import { adminApi, type AdminApiKey, type CreateStandaloneApiKeyRequest } from '@/api/admin' import { @@ -693,6 +694,7 @@ import { log } from '@/utils/logger' const { success, error } = useToast() const { confirmDanger } = useConfirm() +const { copyToClipboard } = useClipboard() const apiKeys = ref([]) const loading = ref(false) @@ -927,20 +929,14 @@ function selectKey() { } async function copyKey() { - try { - await navigator.clipboard.writeText(newKeyValue.value) - success('API Key 已复制到剪贴板') - } catch { - error('复制失败,请手动复制') - } + await copyToClipboard(newKeyValue.value) } async function copyKeyPrefix(apiKey: AdminApiKey) { try { // 调用后端 API 获取完整密钥 const response = await adminApi.getFullApiKey(apiKey.id) - await navigator.clipboard.writeText(response.key) - success('完整密钥已复制到剪贴板') + await copyToClipboard(response.key) } catch (err) { log.error('复制密钥失败:', err) error('复制失败,请重试') diff --git a/frontend/src/views/admin/ModelManagement.vue b/frontend/src/views/admin/ModelManagement.vue index 5caba49..123c5da 100644 --- a/frontend/src/views/admin/ModelManagement.vue +++ b/frontend/src/views/admin/ModelManagement.vue @@ -713,6 +713,7 @@ import ProviderModelFormDialog from '@/features/providers/components/ProviderMod import type { Model } from '@/api/endpoints' import { useToast } from '@/composables/useToast' import { useConfirm } from '@/composables/useConfirm' +import { useClipboard } from '@/composables/useClipboard' import { useRowClick } from '@/composables/useRowClick' import { parseApiError } from '@/utils/errorParser' import { @@ -743,6 +744,7 @@ import { getProvidersSummary } from '@/api/endpoints/providers' import { getAllCapabilities, type CapabilityDefinition } from '@/api/endpoints' const { success, error: showError } = useToast() +const { copyToClipboard } = useClipboard() // 状态 const loading = ref(false) @@ -1066,16 +1068,6 @@ function handleRowClick(event: MouseEvent, model: GlobalModelResponse) { selectModel(model) } -// 复制到剪贴板 -async function copyToClipboard(text: string) { - try { - await navigator.clipboard.writeText(text) - success('已复制') - } catch { - showError('复制失败') - } -} - async function selectModel(model: GlobalModelResponse) { selectedModel.value = model detailTab.value = 'basic' diff --git a/frontend/src/views/admin/Users.vue b/frontend/src/views/admin/Users.vue index 3ee627a..0f0abbb 100644 --- a/frontend/src/views/admin/Users.vue +++ b/frontend/src/views/admin/Users.vue @@ -701,6 +701,7 @@ import { ref, computed, onMounted, watch } from 'vue' import { useUsersStore } from '@/stores/users' import { useToast } from '@/composables/useToast' import { useConfirm } from '@/composables/useConfirm' +import { useClipboard } from '@/composables/useClipboard' import { usageApi, type UsageByUser } from '@/api/usage' import { adminApi } from '@/api/admin' @@ -748,6 +749,7 @@ import { log } from '@/utils/logger' const { success, error } = useToast() const { confirmDanger, confirmWarning } = useConfirm() +const { copyToClipboard } = useClipboard() const usersStore = useUsersStore() // 用户表单对话框状态 @@ -1001,12 +1003,7 @@ function selectApiKey() { } async function copyApiKey() { - try { - await navigator.clipboard.writeText(newApiKey.value) - success('API Key已复制到剪贴板') - } catch { - error('复制失败,请手动复制') - } + await copyToClipboard(newApiKey.value) } async function closeNewApiKeyDialog() { @@ -1035,8 +1032,7 @@ async function copyFullKey(apiKey: any) { try { // 调用后端 API 获取完整密钥 const response = await adminApi.getFullApiKey(apiKey.id) - await navigator.clipboard.writeText(response.key) - success('完整密钥已复制到剪贴板') + await copyToClipboard(response.key) } catch (err: any) { log.error('复制密钥失败:', err) error(err.response?.data?.error?.message || err.response?.data?.detail || '未知错误', '复制密钥失败') diff --git a/frontend/src/views/user/ModelCatalog.vue b/frontend/src/views/user/ModelCatalog.vue index 6ed0f7d..ebe6451 100644 --- a/frontend/src/views/user/ModelCatalog.vue +++ b/frontend/src/views/user/ModelCatalog.vue @@ -342,6 +342,7 @@ import { Plus, } from 'lucide-vue-next' import { useToast } from '@/composables/useToast' +import { useClipboard } from '@/composables/useClipboard' import { Card, Table, @@ -370,6 +371,7 @@ import { useRowClick } from '@/composables/useRowClick' import { log } from '@/utils/logger' const { success, error: showError } = useToast() +const { copyToClipboard } = useClipboard() // 状态 const loading = ref(false) @@ -565,16 +567,6 @@ function hasTieredPricing(model: PublicGlobalModel): boolean { return (tiered?.tiers?.length || 0) > 1 } -async function copyToClipboard(text: string) { - try { - await navigator.clipboard.writeText(text) - success('已复制') - } catch (err) { - log.error('复制失败:', err) - showError('复制失败') - } -} - onMounted(() => { refreshData() }) diff --git a/frontend/src/views/user/components/UserModelDetailDrawer.vue b/frontend/src/views/user/components/UserModelDetailDrawer.vue index 3a3286a..1fa968f 100644 --- a/frontend/src/views/user/components/UserModelDetailDrawer.vue +++ b/frontend/src/views/user/components/UserModelDetailDrawer.vue @@ -352,6 +352,7 @@ import { } from 'lucide-vue-next' import { useEscapeKey } from '@/composables/useEscapeKey' import { useToast } from '@/composables/useToast' +import { useClipboard } from '@/composables/useClipboard' import Card from '@/components/ui/card.vue' import Badge from '@/components/ui/badge.vue' import Button from '@/components/ui/button.vue' @@ -375,6 +376,7 @@ const emit = defineEmits<{ }>() const { success: showSuccess, error: showError } = useToast() +const { copyToClipboard } = useClipboard() interface Props { model: PublicGlobalModel | null @@ -408,15 +410,6 @@ function handleClose() { emit('update:open', false) } -async function copyToClipboard(text: string) { - try { - await navigator.clipboard.writeText(text) - showSuccess('已复制') - } catch { - showError('复制失败') - } -} - function getFirstTierPrice( tieredPricing: TieredPricingConfig | undefined | null, priceKey: 'input_price_per_1m' | 'output_price_per_1m' | 'cache_creation_price_per_1m' | 'cache_read_price_per_1m'