feat:init

This commit is contained in:
zhongyidian committed 2025-12-29 19:19:10 +08:00
1 parent 53d0eeac6b
commit ae0c050010
494 files changed
+17701 -2318

No files matched your search

+155
View File
@@ -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;