feat:init
This commit is contained in:
1 parent
53d0eeac6b
commit
ae0c050010
494 files changed
+17701
-2318
No files matched your search
@@ -0,0 +1,155 @@
|
||||
import { Outlet, useMatches, useLocation } from "react-router-dom";
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { getPublicPath } from "@/lib/utils";
|
||||
import type { VideoRouteHandle } from "@/router/types";
|
||||
import Video, { type VideoRef } from "@/components/video";
|
||||
import { VideoProvider } from "@/contexts/VideoContext";
|
||||
|
||||
/**
|
||||
* 故居布局组件
|
||||
* 用于所有故居页面的统一布局容器
|
||||
* 支持视频前置播放和平滑过渡动画
|
||||
* 渲染子路由内容
|
||||
*/
|
||||
const ResidenceLayout = () => {
|
||||
const matches = useMatches();
|
||||
const location = useLocation();
|
||||
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
||||
const [isContentVisible, setIsContentVisible] = useState(false);
|
||||
const [isVideoFadingOut, setIsVideoFadingOut] = useState(false);
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
const previousPathRef = useRef<string>("");
|
||||
|
||||
// 获取当前路由的视频路径
|
||||
const videoSrc = useMemo(() => {
|
||||
const currentMatch = matches[matches.length - 1];
|
||||
return (currentMatch?.handle as VideoRouteHandle | undefined)?.videoSrc;
|
||||
}, [matches]);
|
||||
|
||||
// 判断是否是前进导航(而不是后退)
|
||||
const isForwardNavigation = useMemo(() => {
|
||||
const currentPath = location.pathname;
|
||||
const previousPath = previousPathRef.current || "";
|
||||
|
||||
// 提取场景编号(例如:/module/chenyun/place5 -> 5)
|
||||
const getCurrentPlaceNumber = (path: string) => {
|
||||
const match = path.match(/place(\d+)/);
|
||||
return match && match[1] ? parseInt(match[1], 10) : 0;
|
||||
};
|
||||
|
||||
const currentPlace = getCurrentPlaceNumber(currentPath);
|
||||
const previousPlace = getCurrentPlaceNumber(previousPath);
|
||||
|
||||
// 如果当前场景编号大于前一个场景编号,说明是前进
|
||||
// 如果是首次加载(previousPath为空),也视为前进
|
||||
return !previousPath || currentPlace > previousPlace;
|
||||
}, [location.pathname]);
|
||||
|
||||
// 处理视频播放完成(自然结束)
|
||||
const handleVideoEnded = () => {
|
||||
// 先让内容层开始淡入
|
||||
setIsContentVisible(true);
|
||||
// 开始视频层淡出动画
|
||||
setIsVideoFadingOut(true);
|
||||
// 然后延迟移除视频层,让视频层淡出动画完成
|
||||
setTimeout(() => {
|
||||
setIsVideoPlaying(false);
|
||||
setIsVideoFadingOut(false);
|
||||
}, 500); // 与 CSS transition 时间一致
|
||||
};
|
||||
|
||||
// 处理跳过操作
|
||||
const handleSkip = () => {
|
||||
// 立即暂停视频
|
||||
videoRef.current?.pause();
|
||||
// 立即显示内容层
|
||||
setIsContentVisible(true);
|
||||
// 立即移除视频层,不等待淡出动画
|
||||
setIsVideoPlaying(false);
|
||||
setIsVideoFadingOut(false);
|
||||
};
|
||||
|
||||
// 处理视频加载错误
|
||||
const handleVideoError = () => {
|
||||
// 视频加载失败,直接显示内容
|
||||
setIsVideoPlaying(false);
|
||||
setIsContentVisible(true);
|
||||
setIsVideoFadingOut(false);
|
||||
};
|
||||
|
||||
// 路由变化时重置状态
|
||||
useEffect(() => {
|
||||
// 只有在前进导航且有视频时才播放视频
|
||||
if (videoSrc && isForwardNavigation) {
|
||||
// 有视频且是前进,重置状态准备播放
|
||||
setIsVideoPlaying(true);
|
||||
setIsContentVisible(false);
|
||||
setIsVideoFadingOut(false);
|
||||
} else {
|
||||
// 没有视频或是后退,直接显示内容
|
||||
setIsVideoPlaying(false);
|
||||
setIsContentVisible(true);
|
||||
setIsVideoFadingOut(false);
|
||||
}
|
||||
|
||||
// 更新前一个路径
|
||||
previousPathRef.current = location.pathname;
|
||||
}, [location.pathname, videoSrc, isForwardNavigation]);
|
||||
|
||||
// 当视频元素渲染后,重置并确保播放
|
||||
useEffect(() => {
|
||||
if (isVideoPlaying && videoRef.current?.video && videoSrc) {
|
||||
const video = videoRef.current.video;
|
||||
video.currentTime = 0;
|
||||
video.play().catch(() => {
|
||||
// 自动播放失败,直接显示内容
|
||||
handleVideoError();
|
||||
});
|
||||
}
|
||||
}, [isVideoPlaying, videoSrc]);
|
||||
|
||||
return (
|
||||
<VideoProvider
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
isContentVisible={isContentVisible}
|
||||
>
|
||||
<div className="relative w-full h-full">
|
||||
{/* 内容层 - 始终渲染,根据状态控制显示 */}
|
||||
<div
|
||||
className={`absolute inset-0 ${
|
||||
isContentVisible
|
||||
? "z-50 opacity-100" // 显示时在上层
|
||||
: "z-10 opacity-0 pointer-events-none" // 隐藏时在下层且不可交互
|
||||
} transition-opacity duration-300 ease-in`}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* 视频层 - 只在播放时渲染,z-index 动态调整 */}
|
||||
{isVideoPlaying && (
|
||||
<div
|
||||
className={`absolute inset-0 ${
|
||||
isVideoFadingOut
|
||||
? "z-40 opacity-0 pointer-events-none" // 淡出时移到下层
|
||||
: "z-50 opacity-100" // 播放时在上层
|
||||
} bg-black transition-opacity duration-500 ease-out`}
|
||||
>
|
||||
<Video
|
||||
ref={videoRef}
|
||||
src={videoSrc ? getPublicPath(videoSrc) : undefined}
|
||||
autoPlay
|
||||
playsInline
|
||||
onEnded={handleVideoEnded}
|
||||
onError={handleVideoError}
|
||||
showSkipButton={true}
|
||||
skipText="跳过"
|
||||
onSkip={handleSkip}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</VideoProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResidenceLayout;
|
||||
Reference in new issue
Block a user