From 525b152a7601f5a2c8c7b9e98abeb44b53227929 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 7 Feb 2026 12:22:16 +0800 Subject: [PATCH] fix(config): preserve mobile scroll after API key modal close and add one-click key copy --- src/components/config/VisualConfigEditor.tsx | 33 +++++++++ src/components/ui/Modal.tsx | 73 ++++++++++++++++++-- src/i18n/locales/en.json | 1 + src/i18n/locales/ru.json | 1 + src/i18n/locales/zh-CN.json | 1 + 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx index 7691045..7f41220 100644 --- a/src/components/config/VisualConfigEditor.tsx +++ b/src/components/config/VisualConfigEditor.tsx @@ -6,6 +6,7 @@ import { Modal } from '@/components/ui/Modal'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { IconChevronDown } from '@/components/ui/icons'; import { ConfigSection } from '@/components/config/ConfigSection'; +import { useNotificationStore } from '@/stores'; import styles from './VisualConfigEditor.module.scss'; import type { PayloadFilterRule, @@ -201,6 +202,7 @@ function ApiKeysCardEditor({ onChange: (nextValue: string) => void; }) { const { t } = useTranslation(); + const { showNotification } = useNotificationStore(); const apiKeys = useMemo( () => value @@ -263,6 +265,34 @@ function ApiKeysCardEditor({ closeModal(); }; + 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'); + } + }; + return (
@@ -294,6 +324,9 @@ function ApiKeysCardEditor({
{maskApiKey(String(key || ''))}
+ diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 45eff25..03790b5 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -16,11 +16,53 @@ const CLOSE_ANIMATION_DURATION = 350; const MODAL_LOCK_CLASS = 'modal-open'; let activeModalCount = 0; +const scrollLockSnapshot = { + scrollY: 0, + contentScrollTop: 0, + contentEl: null as HTMLElement | null, + bodyPosition: '', + bodyTop: '', + bodyLeft: '', + bodyRight: '', + bodyWidth: '', + bodyOverflow: '', + htmlOverflow: '', +}; + +const resolveContentScrollContainer = () => { + if (typeof document === 'undefined') return null; + const contentEl = document.querySelector('.content'); + return contentEl instanceof HTMLElement ? contentEl : null; +}; + const lockScroll = () => { if (typeof document === 'undefined') return; if (activeModalCount === 0) { - document.body?.classList.add(MODAL_LOCK_CLASS); - document.documentElement?.classList.add(MODAL_LOCK_CLASS); + const body = document.body; + const html = document.documentElement; + const contentEl = resolveContentScrollContainer(); + + scrollLockSnapshot.scrollY = window.scrollY || window.pageYOffset || html.scrollTop || 0; + scrollLockSnapshot.contentEl = contentEl; + scrollLockSnapshot.contentScrollTop = contentEl?.scrollTop ?? 0; + scrollLockSnapshot.bodyPosition = body.style.position; + scrollLockSnapshot.bodyTop = body.style.top; + scrollLockSnapshot.bodyLeft = body.style.left; + scrollLockSnapshot.bodyRight = body.style.right; + scrollLockSnapshot.bodyWidth = body.style.width; + scrollLockSnapshot.bodyOverflow = body.style.overflow; + scrollLockSnapshot.htmlOverflow = html.style.overflow; + + body.classList.add(MODAL_LOCK_CLASS); + html.classList.add(MODAL_LOCK_CLASS); + + body.style.position = 'fixed'; + body.style.top = `-${scrollLockSnapshot.scrollY}px`; + body.style.left = '0'; + body.style.right = '0'; + body.style.width = '100%'; + body.style.overflow = 'hidden'; + html.style.overflow = 'hidden'; } activeModalCount += 1; }; @@ -29,8 +71,31 @@ const unlockScroll = () => { if (typeof document === 'undefined') return; activeModalCount = Math.max(0, activeModalCount - 1); if (activeModalCount === 0) { - document.body?.classList.remove(MODAL_LOCK_CLASS); - document.documentElement?.classList.remove(MODAL_LOCK_CLASS); + const body = document.body; + const html = document.documentElement; + const scrollY = scrollLockSnapshot.scrollY; + const contentScrollTop = scrollLockSnapshot.contentScrollTop; + const contentEl = scrollLockSnapshot.contentEl; + + body.classList.remove(MODAL_LOCK_CLASS); + html.classList.remove(MODAL_LOCK_CLASS); + + body.style.position = scrollLockSnapshot.bodyPosition; + body.style.top = scrollLockSnapshot.bodyTop; + body.style.left = scrollLockSnapshot.bodyLeft; + body.style.right = scrollLockSnapshot.bodyRight; + body.style.width = scrollLockSnapshot.bodyWidth; + body.style.overflow = scrollLockSnapshot.bodyOverflow; + html.style.overflow = scrollLockSnapshot.htmlOverflow; + + if (contentEl) { + contentEl.scrollTo({ top: contentScrollTop, left: 0, behavior: 'auto' }); + } + window.scrollTo({ top: scrollY, left: 0, behavior: 'auto' }); + + scrollLockSnapshot.scrollY = 0; + scrollLockSnapshot.contentScrollTop = 0; + scrollLockSnapshot.contentEl = null; } }; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e6a2276..e5d4001 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1094,6 +1094,7 @@ "gemini_api_key": "Gemini API key", "codex_api_key": "Codex API key", "claude_api_key": "Claude API key", + "copy_failed": "Copy failed", "link_copied": "Link copied to clipboard" }, "language": { diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index d30a9eb..80c0b9f 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1099,6 +1099,7 @@ "gemini_api_key": "API-ключ Gemini", "codex_api_key": "API-ключ Codex", "claude_api_key": "API-ключ Claude", + "copy_failed": "Не удалось скопировать", "link_copied": "Ссылка скопирована в буфер обмена" }, "language": { diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 8e00018..014f25d 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1094,6 +1094,7 @@ "gemini_api_key": "Gemini API密钥", "codex_api_key": "Codex API密钥", "claude_api_key": "Claude API密钥", + "copy_failed": "复制失败", "link_copied": "已复制" }, "language": {