fix: copy failed in non-localhost and non-https

This commit is contained in:
jialin
2026-03-23 12:31:45 +08:00
parent 73f3cfceb1
commit aa7247baaf
+46 -28
View File
@@ -1,13 +1,7 @@
import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons'; import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, message, Tooltip } from 'antd'; import { Button, message, Tooltip } from 'antd';
import React, { import React, { useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import AutoTooltip from '../auto-tooltip'; import AutoTooltip from '../auto-tooltip';
type CopyButtonProps = { type CopyButtonProps = {
@@ -55,39 +49,63 @@ const CopyButton: React.FC<CopyButtonProps> = ({
}; };
/** /**
* fallbackexecCommandold Safari / NON-HTTPS * Modern clipboard API (works in secure contexts: HTTPS or localhost)
*/ */
const legacyCopy = (value: string) => { const asyncCopy = async (value: string): Promise<boolean> => {
const textarea = document.createElement('textarea');
textarea.value = value;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try { try {
document.execCommand('copy'); await navigator.clipboard.writeText(value);
return true; return true;
} catch { } catch (error) {
return false; return false;
} finally {
document.body.removeChild(textarea);
} }
}; };
const handleCopy = useCallback(async () => { /**
* Fallback: execCommand with copy event listener
* More reliable than textarea selection method
*/
const execCopy = (value: string): boolean => {
let copySuccess = false;
const onCopy = (event: ClipboardEvent) => {
event.stopPropagation();
event.preventDefault();
event.clipboardData?.clearData();
event.clipboardData?.setData('text/plain', value);
copySuccess = true;
};
try { try {
if (navigator.clipboard?.writeText) { document.addEventListener('copy', onCopy, { capture: true });
await navigator.clipboard.writeText(text); document.execCommand('copy');
} else { return copySuccess;
const success = legacyCopy(text); } catch (error) {
if (!success) throw new Error('legacy copy failed'); return false;
} finally {
document.removeEventListener('copy', onCopy, { capture: true });
}
};
const handleCopy = async () => {
try {
// Try modern clipboard API first
if (await asyncCopy(text)) {
setCopied(true);
return;
} }
setCopied(true); // Fallback to execCommand method
} catch { if (execCopy(text)) {
setCopied(true);
return;
}
// Both methods failed
throw new Error('Copy failed');
} catch (error) {
message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string); message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string);
} }
}, [text, intl]); };
const tipTitle = useMemo(() => { const tipTitle = useMemo(() => {
if (copied) { if (copied) {