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": {