fix: use React Portal to render video modal outside nav element for proper centering

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name 2026-01-31 21:56:43 +08:00
parent 876386ecec
commit 51ba20be21

View File

@ -6,6 +6,7 @@ import { Settings, ExternalLink, Briefcase, Images, Sparkles, Trophy, Building2,
import { Button } from "@/components/ui/button";
import { useTheme } from "next-themes";
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import {
DropdownMenu,
DropdownMenuContent,
@ -152,7 +153,7 @@ export function TopNavbar({
setEditorImageUrl("");
};
return (
const navContent = (
<nav className="fixed top-0 left-0 right-0 z-[100] flex h-12 items-center justify-between bg-black/90 backdrop-blur-sm px-4 text-white border-b border-white/10 shadow-sm">
{/* 左侧:LOGO + 网站名称 */}
<a
@ -446,89 +447,93 @@ export function TopNavbar({
onCancel={handleImageEditorCancel}
/>
)}
</nav>
);
{/* 功能教程弹窗 - 全屏居中 */}
{showTutorialModal && currentTutorialVideo && (
<div
className="fixed inset-0 z-[99999] bg-black/95"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 0,
padding: '20px',
}}
// 使用 Portal 将视频弹窗渲染到 body,确保它在最顶层且不受 nav 的限制
const videoModal = showTutorialModal && currentTutorialVideo && typeof document !== 'undefined' ? createPortal(
<div
className="fixed inset-0 z-[99999] bg-black/95"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 0,
padding: '20px',
}}
onClick={() => {
setShowTutorialModal(false);
setCurrentTutorialVideo(null);
}}
>
<div
className="relative w-full max-w-[1200px]"
style={{ maxHeight: 'calc(100vh - 40px)' }}
onClick={(e) => e.stopPropagation()}
>
{/* 关闭按钮 */}
<button
onClick={() => {
setShowTutorialModal(false);
setCurrentTutorialVideo(null);
}}
className="absolute -top-10 right-0 text-white hover:text-red-400 transition-colors z-10"
title="关闭 (ESC)"
>
<div
className="relative w-full max-w-[1200px]"
style={{ maxHeight: 'calc(100vh - 40px)' }}
onClick={(e) => e.stopPropagation()}
>
{/* 关闭按钮 - 右上角固定 */}
<button
onClick={() => {
setShowTutorialModal(false);
setCurrentTutorialVideo(null);
}}
className="absolute -top-10 right-0 text-white hover:text-red-400 transition-colors z-10"
style={{ position: 'absolute', top: '-40px', right: '0' }}
title="关闭 (ESC)"
>
<X className="h-8 w-8" />
</button>
{/* 标题 */}
<h3 className="text-white text-lg font-medium mb-3 text-center truncate px-4">{currentTutorialVideo.title}</h3>
{/* 视频播放器 */}
{currentTutorialVideo.videoUrl.includes('youtube.com') || currentTutorialVideo.videoUrl.includes('youtu.be') ? (
// YouTube 嵌入 - 启用自动播放和性能优化
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
<iframe
src={`${currentTutorialVideo.videoUrl.replace('watch?v=', 'embed/').replace('youtu.be/', 'youtube.com/embed/')}?autoplay=1&rel=0&modestbranding=1`}
className="w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
loading="eager"
/>
</div>
) : currentTutorialVideo.videoUrl.includes('bilibili.com') ? (
// Bilibili 嵌入
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
<iframe
src={`${currentTutorialVideo.videoUrl.replace('www.bilibili.com/video/', 'player.bilibili.com/player.html?bvid=')}&autoplay=1&high_quality=1`}
className="w-full h-full"
allowFullScreen
loading="eager"
/>
</div>
) : (
// 自定义高性能视频播放器 - 针对慢速网络优化
<VideoPlayer
src={currentTutorialVideo.videoUrl}
poster={currentTutorialVideo.coverUrl || undefined}
title={currentTutorialVideo.title}
autoPlay={true}
className="shadow-2xl"
/>
)}
{/* 视频描述 */}
{currentTutorialVideo.description && (
<p className="text-gray-300 text-sm mt-3">{currentTutorialVideo.description}</p>
)}
<X className="h-8 w-8" />
</button>
{/* 标题 */}
<h3 className="text-white text-lg font-medium mb-3 text-center truncate px-4">{currentTutorialVideo.title}</h3>
{/* 视频播放器 */}
{currentTutorialVideo.videoUrl.includes('youtube.com') || currentTutorialVideo.videoUrl.includes('youtu.be') ? (
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
<iframe
src={`${currentTutorialVideo.videoUrl.replace('watch?v=', 'embed/').replace('youtu.be/', 'youtube.com/embed/')}?autoplay=1&rel=0&modestbranding=1`}
className="w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
loading="eager"
/>
</div>
</div>
)}
</nav>
) : currentTutorialVideo.videoUrl.includes('bilibili.com') ? (
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
<iframe
src={`${currentTutorialVideo.videoUrl.replace('www.bilibili.com/video/', 'player.bilibili.com/player.html?bvid=')}&autoplay=1&high_quality=1`}
className="w-full h-full"
allowFullScreen
loading="eager"
/>
</div>
) : (
<VideoPlayer
src={currentTutorialVideo.videoUrl}
poster={currentTutorialVideo.coverUrl || undefined}
title={currentTutorialVideo.title}
autoPlay={true}
className="shadow-2xl"
/>
)}
{/* 视频描述 */}
{currentTutorialVideo.description && (
<p className="text-gray-300 text-sm mt-3 text-center">{currentTutorialVideo.description}</p>
)}
</div>
</div>,
document.body
) : null;
return (
<>
{navContent}
{videoModal}
</>
);
}