From e66de732acf78b2fe64404a8ffcb489dd82e23a1 Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 11 Feb 2026 14:44:56 +0800 Subject: [PATCH] fix: copy button do not work --- src/components/copy-button/index.tsx | 147 ++++++++++++++---- .../components/add-apikey-modal/index.tsx | 9 +- src/pages/backends/forms/version-info.tsx | 10 +- 3 files changed, 124 insertions(+), 42 deletions(-) diff --git a/src/components/copy-button/index.tsx b/src/components/copy-button/index.tsx index 4c7bd88e..2b4454b2 100644 --- a/src/components/copy-button/index.tsx +++ b/src/components/copy-button/index.tsx @@ -1,58 +1,137 @@ import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Typography } from 'antd'; -import { TextProps } from 'antd/es/typography/Text'; -import React, { useMemo } from 'react'; +import { Button, message, Tooltip } from 'antd'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; +import AutoTooltip from '../auto-tooltip'; type CopyButtonProps = { children?: React.ReactNode; text: string; fontSize?: string; + type?: 'text' | 'primary' | 'dashed' | 'link' | 'default'; + size?: 'small' | 'middle' | 'large'; + shape?: 'circle' | 'round' | 'default'; + tips?: string; + placement?: + | 'top' + | 'left' + | 'right' + | 'bottom' + | 'topLeft' + | 'topRight' + | 'bottomLeft' + | 'bottomRight'; btnStyle?: React.CSSProperties; style?: React.CSSProperties; - format?: 'text/plain' | 'text/html'; }; -const CopyButton: React.FC = ({ +const CopyButton: React.FC = ({ children, + tips, text, + type = 'text', + shape = 'default', fontSize = '14px', style, btnStyle, - format = 'text/plain', - ...rest + placement, + size = 'small' }) => { const intl = useIntl(); + const [copied, setCopied] = useState(false); + const timerRef = useRef(); - const tooltips = useMemo(() => { - return [ - intl.formatMessage({ id: 'common.button.copy' }), - intl.formatMessage({ id: 'common.button.copied' }) - ]; - }, [intl]); + const resetCopied = () => { + window.clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { + setCopied(false); + }, 3000); + }; + + /** + * fallback:execCommand(old Safari / NON-HTTPS) + */ + const legacyCopy = (value: string) => { + const textarea = document.createElement('textarea'); + textarea.value = value; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + return true; + } catch { + return false; + } finally { + document.body.removeChild(textarea); + } + }; + + const handleCopy = useCallback(async () => { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + } else { + const success = legacyCopy(text); + if (!success) throw new Error('legacy copy failed'); + } + + setCopied(true); + } catch { + message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string); + } + }, [text, intl]); + + const tipTitle = useMemo(() => { + if (copied) { + return intl.formatMessage({ id: 'common.button.copied' }); + } + return tips ?? intl.formatMessage({ id: 'common.button.copy' }); + }, [copied, tips, intl]); + + useEffect(() => { + resetCopied(); + }, [copied]); return ( - , - - ] - }} - > - {children} - +
+ {children && ( + + {children} + + )} + + + + + +
); }; diff --git a/src/pages/api-keys/components/add-apikey-modal/index.tsx b/src/pages/api-keys/components/add-apikey-modal/index.tsx index 55882d11..a89a385d 100644 --- a/src/pages/api-keys/components/add-apikey-modal/index.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/index.tsx @@ -285,7 +285,14 @@ const AddModal: React.FC = ({ } + addAfter={ + + } > )} diff --git a/src/pages/backends/forms/version-info.tsx b/src/pages/backends/forms/version-info.tsx index 812777ae..6c5ca477 100644 --- a/src/pages/backends/forms/version-info.tsx +++ b/src/pages/backends/forms/version-info.tsx @@ -53,7 +53,8 @@ const RowWrapper = styled.div` const InfoItem = styled.div` display: grid; - grid-template-columns: max-content 1fr; + align-items: center; + grid-template-columns: max-content minmax(0, 1fr); gap: 8px; .label { color: var(--ant-color-text-tertiary); @@ -136,12 +137,7 @@ export const VersionItem: React.FC = ({ data }) => { {intl.formatMessage({ id: 'backend.imageName' })}: - + {data.is_built_in ? intl.formatMessage({ id: 'backend.versionInfo.autoImage' }) : data.image_name}