mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
fix(config): preserve mobile scroll after API key modal close and add one-click key copy
This commit is contained in:
@@ -6,6 +6,7 @@ import { Modal } from '@/components/ui/Modal';
|
|||||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||||
import { IconChevronDown } from '@/components/ui/icons';
|
import { IconChevronDown } from '@/components/ui/icons';
|
||||||
import { ConfigSection } from '@/components/config/ConfigSection';
|
import { ConfigSection } from '@/components/config/ConfigSection';
|
||||||
|
import { useNotificationStore } from '@/stores';
|
||||||
import styles from './VisualConfigEditor.module.scss';
|
import styles from './VisualConfigEditor.module.scss';
|
||||||
import type {
|
import type {
|
||||||
PayloadFilterRule,
|
PayloadFilterRule,
|
||||||
@@ -201,6 +202,7 @@ function ApiKeysCardEditor({
|
|||||||
onChange: (nextValue: string) => void;
|
onChange: (nextValue: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { showNotification } = useNotificationStore();
|
||||||
const apiKeys = useMemo(
|
const apiKeys = useMemo(
|
||||||
() =>
|
() =>
|
||||||
value
|
value
|
||||||
@@ -263,6 +265,34 @@ function ApiKeysCardEditor({
|
|||||||
closeModal();
|
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 (
|
return (
|
||||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||||||
@@ -294,6 +324,9 @@ function ApiKeysCardEditor({
|
|||||||
<div className="item-subtitle">{maskApiKey(String(key || ''))}</div>
|
<div className="item-subtitle">{maskApiKey(String(key || ''))}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="item-actions">
|
<div className="item-actions">
|
||||||
|
<Button variant="secondary" size="sm" onClick={() => handleCopy(key)} disabled={disabled}>
|
||||||
|
{t('common.copy')}
|
||||||
|
</Button>
|
||||||
<Button variant="secondary" size="sm" onClick={() => openEditModal(index)} disabled={disabled}>
|
<Button variant="secondary" size="sm" onClick={() => openEditModal(index)} disabled={disabled}>
|
||||||
{t('config_management.visual.common.edit')}
|
{t('config_management.visual.common.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -16,11 +16,53 @@ const CLOSE_ANIMATION_DURATION = 350;
|
|||||||
const MODAL_LOCK_CLASS = 'modal-open';
|
const MODAL_LOCK_CLASS = 'modal-open';
|
||||||
let activeModalCount = 0;
|
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 = () => {
|
const lockScroll = () => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
if (activeModalCount === 0) {
|
if (activeModalCount === 0) {
|
||||||
document.body?.classList.add(MODAL_LOCK_CLASS);
|
const body = document.body;
|
||||||
document.documentElement?.classList.add(MODAL_LOCK_CLASS);
|
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;
|
activeModalCount += 1;
|
||||||
};
|
};
|
||||||
@@ -29,8 +71,31 @@ const unlockScroll = () => {
|
|||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
activeModalCount = Math.max(0, activeModalCount - 1);
|
activeModalCount = Math.max(0, activeModalCount - 1);
|
||||||
if (activeModalCount === 0) {
|
if (activeModalCount === 0) {
|
||||||
document.body?.classList.remove(MODAL_LOCK_CLASS);
|
const body = document.body;
|
||||||
document.documentElement?.classList.remove(MODAL_LOCK_CLASS);
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1094,6 +1094,7 @@
|
|||||||
"gemini_api_key": "Gemini API key",
|
"gemini_api_key": "Gemini API key",
|
||||||
"codex_api_key": "Codex API key",
|
"codex_api_key": "Codex API key",
|
||||||
"claude_api_key": "Claude API key",
|
"claude_api_key": "Claude API key",
|
||||||
|
"copy_failed": "Copy failed",
|
||||||
"link_copied": "Link copied to clipboard"
|
"link_copied": "Link copied to clipboard"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
|
|||||||
@@ -1099,6 +1099,7 @@
|
|||||||
"gemini_api_key": "API-ключ Gemini",
|
"gemini_api_key": "API-ключ Gemini",
|
||||||
"codex_api_key": "API-ключ Codex",
|
"codex_api_key": "API-ключ Codex",
|
||||||
"claude_api_key": "API-ключ Claude",
|
"claude_api_key": "API-ключ Claude",
|
||||||
|
"copy_failed": "Не удалось скопировать",
|
||||||
"link_copied": "Ссылка скопирована в буфер обмена"
|
"link_copied": "Ссылка скопирована в буфер обмена"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
|
|||||||
@@ -1094,6 +1094,7 @@
|
|||||||
"gemini_api_key": "Gemini API密钥",
|
"gemini_api_key": "Gemini API密钥",
|
||||||
"codex_api_key": "Codex API密钥",
|
"codex_api_key": "Codex API密钥",
|
||||||
"claude_api_key": "Claude API密钥",
|
"claude_api_key": "Claude API密钥",
|
||||||
|
"copy_failed": "复制失败",
|
||||||
"link_copied": "已复制"
|
"link_copied": "已复制"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
|
|||||||
Reference in New Issue
Block a user