fix(clipboard): add fallback helper and unify copy actions

This commit is contained in:
Supra4E8C
2026-02-14 03:25:33 +08:00
parent 431ec1e0f5
commit 3a7ddfdff1
5 changed files with 38 additions and 60 deletions

View File

@@ -8,6 +8,7 @@ import { IconChevronDown } from '@/components/ui/icons';
import { ConfigSection } from '@/components/config/ConfigSection'; import { ConfigSection } from '@/components/config/ConfigSection';
import { useNotificationStore } from '@/stores'; import { useNotificationStore } from '@/stores';
import styles from './VisualConfigEditor.module.scss'; import styles from './VisualConfigEditor.module.scss';
import { copyToClipboard } from '@/utils/clipboard';
import type { import type {
PayloadFilterRule, PayloadFilterRule,
PayloadModelEntry, PayloadModelEntry,
@@ -268,31 +269,11 @@ function ApiKeysCardEditor({
}; };
const handleCopy = async (apiKey: string) => { const handleCopy = async (apiKey: string) => {
const copyByExecCommand = () => { const copied = await copyToClipboard(apiKey);
const textarea = document.createElement('textarea'); showNotification(
textarea.value = apiKey; t(copied ? 'notification.link_copied' : 'notification.copy_failed'),
textarea.setAttribute('readonly', ''); copied ? 'success' : 'error'
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');
}
}; };
return ( return (

View File

@@ -8,7 +8,7 @@ import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { copyToClipboard } from '@/features/authFiles/clipboard'; import { copyToClipboard } from '@/utils/clipboard';
import { import {
MAX_CARD_PAGE_SIZE, MAX_CARD_PAGE_SIZE,
MIN_CARD_PAGE_SIZE, MIN_CARD_PAGE_SIZE,
@@ -523,4 +523,3 @@ export function AuthFilesPage() {
</div> </div>
); );
} }

View File

@@ -20,6 +20,7 @@ import {
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
import { logsApi } from '@/services/api/logs'; import { logsApi } from '@/services/api/logs';
import { copyToClipboard } from '@/utils/clipboard';
import { MANAGEMENT_API_PREFIX } from '@/utils/constants'; import { MANAGEMENT_API_PREFIX } from '@/utils/constants';
import { formatUnixTimestamp } from '@/utils/format'; import { formatUnixTimestamp } from '@/utils/format';
import styles from './LogsPage.module.scss'; import styles from './LogsPage.module.scss';
@@ -344,30 +345,6 @@ const getErrorMessage = (err: unknown): string => {
return typeof message === 'string' ? message : ''; 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'; type TabType = 'logs' | 'errors';
export function LogsPage() { export function LogsPage() {

View File

@@ -6,6 +6,7 @@ import { Input } from '@/components/ui/Input';
import { useNotificationStore, useThemeStore } from '@/stores'; import { useNotificationStore, useThemeStore } from '@/stores';
import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth'; import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth';
import { vertexApi, type VertexImportResponse } from '@/services/api/vertex'; import { vertexApi, type VertexImportResponse } from '@/services/api/vertex';
import { copyToClipboard } from '@/utils/clipboard';
import styles from './OAuthPage.module.scss'; import styles from './OAuthPage.module.scss';
import iconCodexLight from '@/assets/icons/codex_light.svg'; import iconCodexLight from '@/assets/icons/codex_light.svg';
import iconCodexDark from '@/assets/icons/codex_drak.svg'; import iconCodexDark from '@/assets/icons/codex_drak.svg';
@@ -186,12 +187,11 @@ export function OAuthPage() {
const copyLink = async (url?: string) => { const copyLink = async (url?: string) => {
if (!url) return; if (!url) return;
try { const copied = await copyToClipboard(url);
await navigator.clipboard.writeText(url); showNotification(
showNotification(t('notification.link_copied'), 'success'); t(copied ? 'notification.link_copied' : 'notification.copy_failed'),
} catch { copied ? 'success' : 'error'
showNotification(t('notification.copy_failed'), 'error'); );
}
}; };
const submitCallback = async (provider: OAuthProvider) => { const submitCallback = async (provider: OAuthProvider) => {

View File

@@ -1,4 +1,4 @@
export const copyToClipboard = async (text: string): Promise<boolean> => { export async function copyToClipboard(text: string): Promise<boolean> {
try { try {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
@@ -9,20 +9,41 @@ export const copyToClipboard = async (text: string): Promise<boolean> => {
} }
try { try {
if (typeof document === 'undefined') return false;
if (!document.body) return false;
const activeElement = document.activeElement as HTMLElement | null;
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.value = text; textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed'; textarea.style.position = 'fixed';
textarea.style.opacity = '0'; textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
textarea.style.left = '-9999px'; textarea.style.left = '-9999px';
textarea.style.top = '0'; textarea.style.top = '0';
textarea.style.width = '1px';
textarea.style.height = '1px';
textarea.style.padding = '0';
textarea.style.border = '0';
document.body.appendChild(textarea); document.body.appendChild(textarea);
textarea.focus(); textarea.focus();
textarea.select(); textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
const copied = document.execCommand('copy'); const copied = document.execCommand('copy');
document.body.removeChild(textarea); document.body.removeChild(textarea);
if (activeElement?.focus) {
try {
activeElement.focus();
} catch {
// ignore
}
}
return copied; return copied;
} catch { } catch {
return false; return false;
} }
}; }