From 3a7ddfdff1b7fa5e67726289d6dbdaa06d2f68c4 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 14 Feb 2026 03:25:33 +0800 Subject: [PATCH] fix(clipboard): add fallback helper and unify copy actions --- src/components/config/VisualConfigEditor.tsx | 31 ++++--------------- src/pages/AuthFilesPage.tsx | 3 +- src/pages/LogsPage.tsx | 25 +-------------- src/pages/OAuthPage.tsx | 12 +++---- .../authFiles => utils}/clipboard.ts | 27 ++++++++++++++-- 5 files changed, 38 insertions(+), 60 deletions(-) rename src/{features/authFiles => utils}/clipboard.ts (50%) diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx index 29b0bec..b47f9ad 100644 --- a/src/components/config/VisualConfigEditor.tsx +++ b/src/components/config/VisualConfigEditor.tsx @@ -8,6 +8,7 @@ import { IconChevronDown } from '@/components/ui/icons'; import { ConfigSection } from '@/components/config/ConfigSection'; import { useNotificationStore } from '@/stores'; import styles from './VisualConfigEditor.module.scss'; +import { copyToClipboard } from '@/utils/clipboard'; import type { PayloadFilterRule, PayloadModelEntry, @@ -268,31 +269,11 @@ function ApiKeysCardEditor({ }; const handleCopy = async (apiKey: string) => { - const copyByExecCommand = () => { - const textarea = document.createElement('textarea'); - textarea.value = apiKey; - textarea.setAttribute('readonly', ''); - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - textarea.style.pointerEvents = 'none'; - document.body.appendChild(textarea); - textarea.select(); - textarea.setSelectionRange(0, textarea.value.length); - const copied = document.execCommand('copy'); - document.body.removeChild(textarea); - if (!copied) throw new Error('copy_failed'); - }; - - try { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(apiKey); - } else { - copyByExecCommand(); - } - showNotification(t('notification.link_copied'), 'success'); - } catch { - showNotification(t('notification.copy_failed'), 'error'); - } + const copied = await copyToClipboard(apiKey); + showNotification( + t(copied ? 'notification.link_copied' : 'notification.copy_failed'), + copied ? 'success' : 'error' + ); }; return ( diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 7e0a329..3cd39e2 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -8,7 +8,7 @@ import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { EmptyState } from '@/components/ui/EmptyState'; -import { copyToClipboard } from '@/features/authFiles/clipboard'; +import { copyToClipboard } from '@/utils/clipboard'; import { MAX_CARD_PAGE_SIZE, MIN_CARD_PAGE_SIZE, @@ -523,4 +523,3 @@ export function AuthFilesPage() { ); } - diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index 61f6c29..090c924 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -20,6 +20,7 @@ import { import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import { logsApi } from '@/services/api/logs'; +import { copyToClipboard } from '@/utils/clipboard'; import { MANAGEMENT_API_PREFIX } from '@/utils/constants'; import { formatUnixTimestamp } from '@/utils/format'; import styles from './LogsPage.module.scss'; @@ -344,30 +345,6 @@ const getErrorMessage = (err: unknown): string => { return typeof message === 'string' ? message : ''; }; -const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - return true; - } catch { - try { - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - textarea.style.left = '-9999px'; - textarea.style.top = '0'; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - const ok = document.execCommand('copy'); - document.body.removeChild(textarea); - return ok; - } catch { - return false; - } - } -}; - type TabType = 'logs' | 'errors'; export function LogsPage() { diff --git a/src/pages/OAuthPage.tsx b/src/pages/OAuthPage.tsx index 0abb268..5cc2515 100644 --- a/src/pages/OAuthPage.tsx +++ b/src/pages/OAuthPage.tsx @@ -6,6 +6,7 @@ import { Input } from '@/components/ui/Input'; import { useNotificationStore, useThemeStore } from '@/stores'; import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth'; import { vertexApi, type VertexImportResponse } from '@/services/api/vertex'; +import { copyToClipboard } from '@/utils/clipboard'; import styles from './OAuthPage.module.scss'; import iconCodexLight from '@/assets/icons/codex_light.svg'; import iconCodexDark from '@/assets/icons/codex_drak.svg'; @@ -186,12 +187,11 @@ export function OAuthPage() { const copyLink = async (url?: string) => { if (!url) return; - try { - await navigator.clipboard.writeText(url); - showNotification(t('notification.link_copied'), 'success'); - } catch { - showNotification(t('notification.copy_failed'), 'error'); - } + const copied = await copyToClipboard(url); + showNotification( + t(copied ? 'notification.link_copied' : 'notification.copy_failed'), + copied ? 'success' : 'error' + ); }; const submitCallback = async (provider: OAuthProvider) => { diff --git a/src/features/authFiles/clipboard.ts b/src/utils/clipboard.ts similarity index 50% rename from src/features/authFiles/clipboard.ts rename to src/utils/clipboard.ts index 21435ce..ba274e7 100644 --- a/src/features/authFiles/clipboard.ts +++ b/src/utils/clipboard.ts @@ -1,4 +1,4 @@ -export const copyToClipboard = async (text: string): Promise => { +export async function copyToClipboard(text: string): Promise { try { if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); @@ -9,20 +9,41 @@ export const copyToClipboard = async (text: string): Promise => { } try { + if (typeof document === 'undefined') return false; + if (!document.body) return false; + + const activeElement = document.activeElement as HTMLElement | null; + const textarea = document.createElement('textarea'); textarea.value = text; + textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.opacity = '0'; + textarea.style.pointerEvents = 'none'; textarea.style.left = '-9999px'; textarea.style.top = '0'; + textarea.style.width = '1px'; + textarea.style.height = '1px'; + textarea.style.padding = '0'; + textarea.style.border = '0'; + document.body.appendChild(textarea); textarea.focus(); textarea.select(); + textarea.setSelectionRange(0, textarea.value.length); const copied = document.execCommand('copy'); document.body.removeChild(textarea); + + if (activeElement?.focus) { + try { + activeElement.focus(); + } catch { + // ignore + } + } + return copied; } catch { return false; } -}; - +}