import { invokeNative } from '@jolibox/native-bridge'; import { context } from '@/common/context'; import { hostEmitter } from '@jolibox/common'; interface IframeModalOptions { url: string; height?: string; // default '50%' showCloseButton?: boolean; // default true enableDragClose?: boolean; // default true useAnimation?: boolean; // default true onClose?: () => void; onLoad?: () => void; } const DEFAULT_OPTIONS: Partial = { height: '50%', showCloseButton: true, enableDragClose: true, useAnimation: true }; let modalOverlay: HTMLElement | null = null; let modalContainer: HTMLElement | null = null; let iframe: HTMLIFrameElement | null = null; /** * create a half-screen iframe modal */ export function createIframeModal(options: IframeModalOptions): { close: () => void } { const mergedOptions = { ...DEFAULT_OPTIONS, ...options }; const { url, height, showCloseButton, enableDragClose, useAnimation, onClose, onLoad } = mergedOptions; let startY = 0; let currentY = 0; let isModalVisible = false; const getAnimationStyles = () => { if (useAnimation) { return { overlayTransition: 'opacity 0.3s ease', containerTransition: 'transform 0.3s ease', iframeTransition: 'opacity 0.3s ease' }; } else { return { overlayTransition: 'none', containerTransition: 'none', iframeTransition: 'none' }; } }; // 获取动画延迟时间 const getAnimationDelay = () => (useAnimation ? 300 : 0); // create modal dom function createModalDOM() { const { overlayTransition, containerTransition, iframeTransition } = getAnimationStyles(); modalOverlay = document.createElement('div'); modalOverlay.id = 'jolibox-modal-overlay'; modalOverlay.style.cssText = `position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; display:flex; justify-content:center; align-items:flex-end; transition:${overlayTransition}; opacity:0; visibility:hidden;`; modalContainer = document.createElement('div'); modalContainer.id = 'jolibox-modal-container'; modalContainer.style.cssText = `width:100%; height:${height}; background:white; border-radius:12px 12px 0 0; overflow:hidden; box-shadow:0 -2px 10px rgba(0,0,0,0.2); transform:translateY(100%); transition:${containerTransition};`; const loadingIndicator = document.createElement('div'); loadingIndicator.id = 'jolibox-modal-loading'; loadingIndicator.style.cssText = 'position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); width:40px; height:40px; border:4px solid #f3f3f3; border-top:4px solid #3498db; border-radius:50%; animation:spin 1s linear infinite;'; const styleElement = document.createElement('style'); styleElement.textContent = '@keyframes spin { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } }'; document.head.appendChild(styleElement); // safe area adapter const safeAreaDiv = document.createElement('div'); safeAreaDiv.style.cssText = 'width:100%; height:100%; display:flex; flex-direction:column; padding-bottom:env(safe-area-inset-bottom, 0); position:relative;'; // create iframe iframe = document.createElement('iframe'); iframe.style.cssText = `width:100%; flex:1; border:none; opacity:0; transition:${iframeTransition};`; iframe.src = url; // iframe loaded event iframe.onload = () => { if (loadingIndicator.parentNode) { loadingIndicator.parentNode.removeChild(loadingIndicator); } // 显示iframe内容 iframe!.style.opacity = '1'; // 显示modal showModal(); // 触发加载完成回调 onLoad?.(); // 发送全局事件 hostEmitter.emit('onModalIframeLoaded', { url }); }; // 组装DOM safeAreaDiv.appendChild(iframe); safeAreaDiv.appendChild(loadingIndicator); modalContainer.appendChild(safeAreaDiv); // 添加关闭按钮 if (showCloseButton) { const closeBtn = document.createElement('div'); closeBtn.style.cssText = 'position:absolute; top:10px; right:10px; width:30px; height:30px; background:rgba(0,0,0,0.1); border-radius:15px; display:flex; justify-content:center; align-items:center; cursor:pointer; z-index:10;'; closeBtn.innerHTML = '✕'; closeBtn.onclick = closeModal; modalContainer.appendChild(closeBtn); } modalOverlay.appendChild(modalContainer); document.body.appendChild(modalOverlay); // 点击遮罩关闭 modalOverlay.addEventListener('click', function (e) { if (e.target === modalOverlay) closeModal(); }); if (enableDragClose) { addDragSupport(); } } // show modal function showModal() { if (!modalOverlay || !modalContainer || isModalVisible) return; modalOverlay.style.visibility = 'visible'; if (useAnimation) { requestAnimationFrame(() => { if (modalOverlay) modalOverlay.style.opacity = '1'; if (modalContainer) modalContainer.style.transform = 'translateY(0)'; isModalVisible = true; }); } else { if (modalOverlay) modalOverlay.style.opacity = '1'; if (modalContainer) modalContainer.style.transform = 'translateY(0)'; isModalVisible = true; } invokeNative('updateContainerConfigSync', { displayCapsuleButton: false, webviewId: context.webviewId }); } function addDragSupport() { if (!modalContainer) return; const handleTouchStart = (e: TouchEvent) => { startY = e.touches[0].clientY; }; const handleTouchMove = (e: TouchEvent) => { if (!isModalVisible) return; currentY = e.touches[0].clientY; const deltaY = currentY - startY; if (deltaY > 0) { if (modalContainer) { // 拖动时始终不使用过渡效果 modalContainer.style.transition = 'none'; modalContainer.style.transform = `translateY(${deltaY}px)`; } e.preventDefault(); } }; const handleTouchEnd = () => { if (!isModalVisible || !modalContainer) return; modalContainer.style.transition = useAnimation ? 'transform 0.3s ease' : 'none'; if (currentY - startY > 80) { closeModal(); } else { modalContainer.style.transform = 'translateY(0)'; } }; modalContainer.addEventListener('touchstart', handleTouchStart); modalContainer.addEventListener('touchmove', handleTouchMove); modalContainer.addEventListener('touchend', handleTouchEnd); } // close function closeModal() { if (!modalOverlay || !modalContainer || !isModalVisible) return; if (useAnimation) { modalContainer.style.transform = 'translateY(100%)'; modalOverlay.style.opacity = '0'; } else { modalOverlay.style.visibility = 'hidden'; } isModalVisible = false; // 恢复原生UI invokeNative('updateContainerConfigSync', { displayCapsuleButton: true, webviewId: context.webviewId }); setTimeout(() => { if (modalOverlay) { modalOverlay.remove(); modalOverlay = null; modalContainer = null; iframe = null; // 触发关闭回调 onClose?.(); // 发送全局事件 hostEmitter.emit('onModalIframeClosed', { url }); } }, getAnimationDelay()); } createModalDOM(); return { close: closeModal }; } declare global { interface Window { joliboxUI?: { createIframeModal: (options: IframeModalOptions) => { close: () => void }; }; } } export function registerIframeModalToGlobal() { window.joliboxUI = { ...window.joliboxUI, createIframeModal }; return () => { window.joliboxUI = undefined; if (modalOverlay && modalOverlay.parentNode) { modalOverlay.parentNode.removeChild(modalOverlay); } if (modalContainer && modalContainer.parentNode) { modalContainer.parentNode.removeChild(modalContainer); } if (iframe && iframe.parentNode) { iframe.parentNode.removeChild(iframe); } // 清空引用 modalOverlay = null; modalContainer = null; iframe = null; }; }