forked from 77media/video-flow
332 lines
12 KiB
TypeScript
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
|
|
});
|