video-flow-b/components/video-screen-layout.tsx
2025-07-11 16:02:45 +08:00

332 lines
12 KiB
TypeScript

'use client'; // Add this to ensure it's a client component
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { ChevronLeft, ChevronRight, Volume2, VolumeX, Play, Pause } from 'lucide-react';
import { Button } from '@/components/ui/button';
import dynamic from 'next/dynamic';
interface VideoScreenLayoutProps {
videos: {
id: string;
url: string;
title: string;
}[];
}
function VideoScreenLayoutComponent({ videos }: VideoScreenLayoutProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const [isMuted, setIsMuted] = useState(true); // 默认静音
const [volume, setVolume] = useState(0.8); // 添加音量状态
const [isPlaying, setIsPlaying] = useState(true); // 播放状态
const containerRef = useRef<HTMLDivElement>(null);
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
useEffect(() => {
setCurrentIndex(videos.length > 2 ? 1 : 0);
}, [videos.length]);
// 确保视频refs数组长度与videos数组一致
useEffect(() => {
videoRefs.current = videoRefs.current.slice(0, videos.length);
}, [videos.length]);
// 计算每个面板的样式
const getPanelStyle = (index: number) => {
const position = index - currentIndex;
const scale = Math.max(0.6, 1 - Math.abs(position) * 0.2);
const zIndex = 10 - Math.abs(position);
const opacity = Math.max(0.4, 1 - Math.abs(position) * 0.3);
let transform = `
perspective(1000px)
scale(${scale})
translateX(${position * 100}%)
`;
// 添加侧面板的 3D 旋转效果
if (position !== 0) {
const rotateY = position > 0 ? -15 : 15;
transform += ` rotateY(${rotateY}deg)`;
}
return {
transform,
zIndex,
opacity,
};
};
// 切换静音状态
const toggleMute = () => {
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
currentVideo.muted = !currentVideo.muted;
setIsMuted(currentVideo.muted);
}
};
// 音量控制函数
const handleVolumeChange = (newVolume: number) => {
setVolume(newVolume);
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
currentVideo.volume = newVolume;
}
};
// 应用音量设置到视频元素
const applyVolumeSettings = useCallback((videoElement: HTMLVideoElement) => {
if (videoElement) {
videoElement.volume = volume;
videoElement.muted = isMuted;
}
}, [volume, isMuted]);
// 处理切换
const handleSlide = useCallback((direction: 'prev' | 'next') => {
if (isAnimating) return;
setIsAnimating(true);
const newIndex = direction === 'next'
? (currentIndex + 1) % videos.length
: (currentIndex - 1 + videos.length) % videos.length;
setCurrentIndex(newIndex);
// 动画结束后重置状态并同步新视频的静音状态
setTimeout(() => {
setIsAnimating(false);
// 同步新视频的静音状态到UI并应用音量设置
const newVideo = videoRefs.current[newIndex];
if (newVideo) {
setIsMuted(newVideo.muted);
applyVolumeSettings(newVideo);
// 根据当前播放状态控制新视频
if (isPlaying) {
newVideo.play().catch(() => {
setIsPlaying(false);
});
} else {
newVideo.pause();
}
}
}, 500);
}, [isAnimating, currentIndex, videos.length, isPlaying, applyVolumeSettings]);
// 音量设置变化时应用到当前视频
useEffect(() => {
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
applyVolumeSettings(currentVideo);
}
}, [volume, isMuted, currentIndex, applyVolumeSettings]);
// 播放状态变化时应用到当前视频
useEffect(() => {
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
if (isPlaying) {
currentVideo.play().catch(() => {
// 处理自动播放策略限制
setIsPlaying(false);
});
} else {
currentVideo.pause();
}
}
}, [isPlaying, currentIndex]);
// 播放/暂停控制
const togglePlay = () => {
setIsPlaying(!isPlaying);
};
// 键盘事件监听器 - 添加左右箭头键控制
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// 检查是否正在动画中,如果是则不处理
if (isAnimating) return;
switch (event.key) {
case 'ArrowLeft':
event.preventDefault(); // 阻止默认行为
handleSlide('prev');
break;
case 'ArrowRight':
event.preventDefault(); // 阻止默认行为
handleSlide('next');
break;
}
};
// 添加键盘事件监听器
window.addEventListener('keydown', handleKeyDown);
// 清理函数 - 组件卸载时移除监听器
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isAnimating, handleSlide]); // 添加handleSlide到依赖项
return (
<div className="relative w-full h-[360px] overflow-hidden bg-[var(--background)]">
{/* 视频面板容器 */}
<div
ref={containerRef}
className="absolute inset-0 flex items-center justify-center"
style={{
perspective: '1000px',
transformStyle: 'preserve-3d',
}}
>
{videos.map((video, index) => (
<div
key={video.id}
className="absolute w-[640px] h-[360px] transition-all duration-500 ease-out"
style={getPanelStyle(index)}
>
<div className="relative w-full h-full overflow-hidden rounded-lg">
{/* 视频 - Add suppressHydrationWarning to prevent className mismatch warnings */}
<video
ref={(el) => (videoRefs.current[index] = el)}
src={video.url}
suppressHydrationWarning
className="w-full h-full object-cover"
autoPlay={index === currentIndex ? isPlaying : false}
loop
muted={index === currentIndex ? isMuted : true} // 只有当前视频受状态控制
playsInline
preload={`${index === currentIndex ? 'auto' : 'none'}`}
poster={`${video.url}?vframe/jpg/offset/1`}
onLoadedData={() => {
if (index === currentIndex && videoRefs.current[index]) {
applyVolumeSettings(videoRefs.current[index]!);
// 根据播放状态决定是否播放
if (isPlaying) {
videoRefs.current[index]!.play().catch(() => {
setIsPlaying(false);
});
}
}
}}
onPlay={() => {
if (index === currentIndex) {
setIsPlaying(true);
}
}}
onPause={() => {
if (index === currentIndex) {
setIsPlaying(false);
}
}}
/>
{/* 视频标题和控制 - 只在中间面板显示 */}
{index === currentIndex && (
<>
{/* 音量控制区域 */}
<div className="absolute top-4 right-4 flex items-center gap-2">
{/* 静音按钮 */}
<Button
variant="ghost"
size="icon"
className="w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={toggleMute}
title={isMuted ? "取消静音" : "静音"}
>
{isMuted ? (
<VolumeX className="w-5 h-5 text-white" />
) : (
<Volume2 className="w-5 h-5 text-white" />
)}
</Button>
{/* 音量滑块 */}
<div className="flex items-center gap-1 bg-black/40 rounded-full px-2 py-2 backdrop-blur-sm">
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
className="w-12 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:shadow-lg
[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:cursor-pointer
[&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:shadow-lg"
style={{
background: `linear-gradient(to right, white 0%, white ${volume * 100}%, rgba(255,255,255,0.2) ${volume * 100}%, rgba(255,255,255,0.2) 100%)`
}}
/>
<span className="text-xs text-white/70 w-6 text-center">
{Math.round(volume * 100)}%
</span>
</div>
</div>
{/* 底部控制区域 */}
<div className="absolute bottom-16 left-4">
{/* 播放/暂停按钮 */}
<Button
variant="ghost"
size="icon"
className="w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={togglePlay}
title={isPlaying ? "暂停" : "播放"}
>
{isPlaying ? (
<Pause className="w-5 h-5 text-white" />
) : (
<Play className="w-5 h-5 text-white" />
)}
</Button>
</div>
{/* 视频标题 */}
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 to-transparent">
<h3 className="text-white text-lg font-medium">{video.title}</h3>
</div>
</>
)}
{/* 玻璃态遮罩 - 侧面板半透明效果 */}
{index !== currentIndex && (
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm" />
)}
</div>
</div>
))}
</div>
{/* 切换按钮 */}
<Button
variant="ghost"
size="icon"
className="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all"
onClick={() => handleSlide('prev')}
disabled={isAnimating}
>
<ChevronLeft className="w-6 h-6 text-white" />
</Button>
<Button
variant="ghost"
size="icon"
className="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all"
onClick={() => handleSlide('next')}
disabled={isAnimating}
>
<ChevronRight className="w-6 h-6 text-white" />
</Button>
</div>
);
}
// Export as a client-only component to prevent hydration issues
export const VideoScreenLayout = dynamic(() => Promise.resolve(VideoScreenLayoutComponent), {
ssr: false
});