/** * 图片沉浸式全屏灯箱(Image Lightbox / Quick Look) * 模拟 Apple Notes 与 macOS Quick Look 的流畅看图体验: * - 毛玻璃暗色背景与平滑淡入缩放 * - 支持 ESC 键与点击背景退出 * - 单击图片切换「适应屏幕」与「100% 原始尺寸」 */ interface LightboxState { container: HTMLElement; img: HTMLImageElement; isZoomedOriginal: boolean; keydownListener: (e: KeyboardEvent) => void; } let activeLightbox: LightboxState | null = null; export const closeImageLightbox = (): void => { if (!activeLightbox) return; const { container, keydownListener } = activeLightbox; window.removeEventListener('keydown', keydownListener); container.style.opacity = '0'; container.style.pointerEvents = 'none'; setTimeout(() => { if (container.parentElement) { container.parentElement.removeChild(container); } }, 200); activeLightbox = null; }; export const showImageLightbox = (src: string, alt = ''): HTMLElement => { if (activeLightbox) { closeImageLightbox(); } const overlay = document.createElement('div'); overlay.className = 'cm-ext-image-lightbox-overlay'; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-label', alt || 'Image Preview'); // 样式:固定全屏、深色毛玻璃、居中对齐 Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh', backgroundColor: 'rgba(0, 0, 0, 0.82)', backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', zIndex: '99999', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', opacity: '0', transition: 'opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1)', cursor: 'zoom-out', padding: '24px', boxSizing: 'border-box', }); // 顶部操作栏 const headerBar = document.createElement('div'); headerBar.className = 'cm-lightbox-header'; Object.assign(headerBar.style, { position: 'absolute', top: '16px', right: '20px', display: 'flex', alignItems: 'center', gap: '12px', zIndex: '100001', }); // 关闭按钮 const closeBtn = document.createElement('button'); closeBtn.className = 'cm-lightbox-close-btn'; closeBtn.title = '关闭 (ESC)'; closeBtn.innerHTML = '✕'; Object.assign(closeBtn.style, { background: 'rgba(255, 255, 255, 0.15)', border: 'none', color: '#ffffff', width: '32px', height: '32px', borderRadius: '50%', fontSize: '16px', fontWeight: 'bold', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background 0.15s ease', }); closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.3)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeImageLightbox(); }); headerBar.appendChild(closeBtn); overlay.appendChild(headerBar); // 图片容器与图片元素 const imgWrapper = document.createElement('div'); imgWrapper.className = 'cm-lightbox-img-wrapper'; Object.assign(imgWrapper.style, { display: 'flex', alignItems: 'center', justifyContent: 'center', maxWidth: '100%', maxHeight: '100%', overflow: 'auto', }); const img = document.createElement('img'); img.className = 'cm-lightbox-img'; img.src = src; img.alt = alt; const applyFitStyle = () => { Object.assign(img.style, { maxWidth: '92vw', maxHeight: '88vh', width: 'auto', height: 'auto', objectFit: 'contain', borderRadius: '8px', boxShadow: '0 20px 48px rgba(0, 0, 0, 0.6)', cursor: 'zoom-in', transition: 'transform 0.2s cubic-bezier(0.16, 1, 0.3, 1)', }); }; const applyOriginalStyle = () => { Object.assign(img.style, { maxWidth: 'none', maxHeight: 'none', width: 'auto', height: 'auto', objectFit: 'none', cursor: 'zoom-out', transition: 'none', }); }; applyFitStyle(); // 单击图片切换适应/原图 let isZoomedOriginal = false; img.addEventListener('click', (e) => { e.stopPropagation(); isZoomedOriginal = !isZoomedOriginal; if (isZoomedOriginal) { applyOriginalStyle(); } else { applyFitStyle(); } if (activeLightbox) { activeLightbox.isZoomedOriginal = isZoomedOriginal; } }); imgWrapper.appendChild(img); overlay.appendChild(imgWrapper); // 点击背景遮罩关闭 overlay.addEventListener('click', (e) => { if (e.target === overlay || e.target === imgWrapper) { closeImageLightbox(); } }); // ESC 键盘监听 const keydownListener = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeImageLightbox(); } }; window.addEventListener('keydown', keydownListener); document.body.appendChild(overlay); // 渐显进入 requestAnimationFrame(() => { overlay.style.opacity = '1'; }); activeLightbox = { container: overlay, img, isZoomedOriginal: false, keydownListener, }; return overlay; }; export const getActiveLightbox = (): LightboxState | null => activeLightbox;