import { Radio, X } from 'lucide-react'; import Image from 'next/image'; import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; interface ActionItem { id: string; label: string; icon: React.ReactNode; onClick: (e?: React.MouseEvent) => void | Promise; color?: 'default' | 'danger' | 'primary'; disabled?: boolean; } interface MobileActionSheetProps { isOpen: boolean; onClose: () => void; title: string; actions: ActionItem[]; poster?: string; sources?: string[]; // 播放源信息 isAggregate?: boolean; // 是否为聚合内容 sourceName?: string; // 播放源名称 currentEpisode?: number; // 当前集数 totalEpisodes?: number; // 总集数 origin?: 'vod' | 'live'; } const MobileActionSheet: React.FC = ({ isOpen, onClose, title, actions, poster, sources, isAggregate, sourceName, currentEpisode, totalEpisodes, origin = 'vod', }) => { const [isVisible, setIsVisible] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const [mounted, setMounted] = useState(false); // 确保组件在客户端挂载后才渲染 Portal useEffect(() => { setMounted(true); }, []); // 控制动画状态 useEffect(() => { let animationId: number; let timer: NodeJS.Timeout; if (isOpen) { setIsVisible(true); // 使用双重 requestAnimationFrame 确保DOM完全渲染 animationId = requestAnimationFrame(() => { animationId = requestAnimationFrame(() => { setIsAnimating(true); }); }); } else { setIsAnimating(false); // 等待动画完成后隐藏组件 timer = setTimeout(() => { setIsVisible(false); }, 200); } return () => { if (animationId) { cancelAnimationFrame(animationId); } if (timer) { clearTimeout(timer); } }; }, [isOpen]); // 阻止背景滚动 useEffect(() => { if (isVisible) { // 保存当前滚动位置 const scrollY = window.scrollY; const scrollX = window.scrollX; const body = document.body; const html = document.documentElement; // 获取滚动条宽度 const scrollBarWidth = window.innerWidth - html.clientWidth; // 保存原始样式 const originalBodyStyle = { position: body.style.position, top: body.style.top, left: body.style.left, right: body.style.right, width: body.style.width, paddingRight: body.style.paddingRight, overflow: body.style.overflow, }; // 设置body样式来阻止滚动,但保持原位置 body.style.position = 'fixed'; body.style.top = `-${scrollY}px`; body.style.left = `-${scrollX}px`; body.style.right = '0'; body.style.width = '100%'; body.style.overflow = 'hidden'; body.style.paddingRight = `${scrollBarWidth}px`; return () => { // 恢复所有原始样式 body.style.position = originalBodyStyle.position; body.style.top = originalBodyStyle.top; body.style.left = originalBodyStyle.left; body.style.right = originalBodyStyle.right; body.style.width = originalBodyStyle.width; body.style.paddingRight = originalBodyStyle.paddingRight; body.style.overflow = originalBodyStyle.overflow; // 使用 requestAnimationFrame 确保样式恢复后再滚动 requestAnimationFrame(() => { window.scrollTo(scrollX, scrollY); }); }; } }, [isVisible]); // ESC键关闭 useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); } }; if (isVisible) { document.addEventListener('keydown', handleEsc); return () => document.removeEventListener('keydown', handleEsc); } }, [isVisible, onClose]); if (!isVisible || !mounted) return null; const getActionColor = (color: ActionItem['color']) => { switch (color) { case 'danger': return 'text-red-600 dark:text-red-400'; case 'primary': return 'text-green-600 dark:text-green-400'; default: return 'text-gray-700 dark:text-gray-300'; } }; const getActionHoverColor = (color: ActionItem['color']) => { switch (color) { case 'danger': return 'hover:bg-red-50/50 dark:hover:bg-red-900/10'; case 'primary': return 'hover:bg-green-50/50 dark:hover:bg-green-900/10'; default: return 'hover:bg-gray-50/50 dark:hover:bg-gray-800/20'; } }; const content = (
{ // 阻止最外层容器的触摸移动,防止背景滚动 e.preventDefault(); e.stopPropagation(); }} style={{ touchAction: 'none', // 禁用所有触摸操作 }} > {/* 背景遮罩 */}
{ // 只阻止滚动,允许其他触摸事件(包括点击) e.preventDefault(); }} onWheel={(e) => { // 阻止滚轮滚动 e.preventDefault(); }} style={{ backdropFilter: 'blur(4px)', willChange: 'opacity', touchAction: 'none', // 禁用所有触摸操作 }} /> {/* 操作表单 */}
{ // 允许操作表单内部滚动,阻止事件冒泡到外层 e.stopPropagation(); }} style={{ marginBottom: 'calc(1rem + env(safe-area-inset-bottom))', willChange: 'transform, opacity', backfaceVisibility: 'hidden', // 避免闪烁 transform: isAnimating ? 'translateY(0) translateZ(0)' : 'translateY(100%) translateZ(0)', // 组合变换保持滑入效果和硬件加速 opacity: isAnimating ? 1 : 0, touchAction: 'auto', // 允许操作表单内的正常触摸操作 }} > {/* 头部 */}
{poster && (
{title}
)}

{title}

{sourceName && ( {origin === 'live' && ( )} {sourceName} )}

选择操作

{/* 操作列表 */}
{actions.map((action, index) => (
{/* 分割线 - 最后一项不显示 */} {index < actions.length - 1 && (
)}
))}
{/* 播放源信息展示区域 */} {isAggregate && sources && sources.length > 0 && (
{/* 标题区域 */}

可用播放源

共 {sources.length} 个播放源

{/* 播放源列表 */}
{sources.map((source, index) => (
{source}
))}
)}
); // 使用 Portal 将组件渲染到 document.body return createPortal(content, document.body); }; export default MobileActionSheet;