video-flow-b/components/video-grid-layout.tsx

230 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'; // Add this to ensure it's a client component
import React, { useState, useRef } from 'react';
import { Edit2, Trash2, Play, Pause, Volume2, VolumeX, Maximize, Minimize } from 'lucide-react';
import { Button } from '@/components/ui/button';
import dynamic from 'next/dynamic';
interface VideoGridLayoutProps {
videos: {
id: string;
url: string;
title: string;
date?: string;
}[];
onEdit?: (id: string) => void;
onDelete?: (id: string) => void;
}
function VideoGridLayoutComponent({ videos, onEdit, onDelete }: VideoGridLayoutProps) {
const [hoveredId, setHoveredId] = useState<string | null>(null);
const [isPlaying, setIsPlaying] = useState<{ [key: string]: boolean }>({});
const [isMuted, setIsMuted] = useState<{ [key: string]: boolean }>({});
const [volume, setVolume] = useState<{ [key: string]: number }>({});
const [isFullscreen, setIsFullscreen] = useState<{ [key: string]: boolean }>({});
const videoRefs = useRef<{ [key: string]: HTMLVideoElement | null }>({});
const handleMouseEnter = (id: string) => {
setHoveredId(id);
};
const handleMouseLeave = (id: string) => {
setHoveredId(null);
// 暂停视频并重新静音以便下次预览
const video = videoRefs.current[id];
if (video) {
video.pause();
video.muted = true;
setIsPlaying(prev => ({ ...prev, [id]: false }));
setIsMuted(prev => ({ ...prev, [id]: true }));
}
};
const togglePlay = (id: string) => {
const video = videoRefs.current[id];
if (video) {
if (video.paused) {
// 在用户主动播放时取消静音
video.muted = isMuted[id] !== false;
video.play();
setIsPlaying(prev => ({ ...prev, [id]: true }));
} else {
video.pause();
setIsPlaying(prev => ({ ...prev, [id]: false }));
}
}
};
const toggleMute = (id: string, event: React.MouseEvent) => {
event.stopPropagation(); // 防止触发父元素的点击事件
const video = videoRefs.current[id];
if (video) {
video.muted = !video.muted;
setIsMuted(prev => ({ ...prev, [id]: video.muted }));
}
};
const handleVolumeChange = (id: string, newVolume: number) => {
const video = videoRefs.current[id];
if (video) {
video.volume = newVolume;
setVolume(prev => ({ ...prev, [id]: newVolume }));
}
};
const toggleFullscreen = (id: string, event: React.MouseEvent) => {
event.stopPropagation();
const video = videoRefs.current[id];
if (video) {
if (!document.fullscreenElement) {
// 进入全屏
video.requestFullscreen?.() ||
(video as any).webkitRequestFullscreen?.() ||
(video as any).msRequestFullscreen?.();
setIsFullscreen(prev => ({ ...prev, [id]: true }));
} else {
// 退出全屏
document.exitFullscreen?.() ||
(document as any).webkitExitFullscreen?.() ||
(document as any).msExitFullscreen?.();
setIsFullscreen(prev => ({ ...prev, [id]: false }));
}
}
};
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-6">
{videos.map((video) => (
<div
key={video.id}
className="group relative bg-white/5 rounded-lg overflow-hidden transition-all duration-300 hover:bg-white/10"
onMouseEnter={() => handleMouseEnter(video.id)}
onMouseLeave={() => handleMouseLeave(video.id)}
>
{/* 视频容器 */}
<div className="relative aspect-video">
<video
ref={(el) => (videoRefs.current[video.id] = el)}
src={video.url}
suppressHydrationWarning
className="w-full h-full object-cover"
loop
muted={isMuted[video.id] !== false} // 默认静音除非明确设置为false
playsInline
preload="none"
poster={`${video.url}?vframe/jpg/offset/1`}
onLoadedData={() => {
// 设置默认音量
if (videoRefs.current[video.id] && !volume[video.id]) {
videoRefs.current[video.id]!.volume = 0.8;
setVolume(prev => ({ ...prev, [video.id]: 0.8 }));
}
}}
/>
{/* 播放按钮遮罩 */}
<div
className={`absolute inset-0 flex items-center justify-center bg-black/40 transition-opacity duration-300
${hoveredId === video.id && !isPlaying[video.id] ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => togglePlay(video.id)}
>
<Play className="w-12 h-12 text-white/90" />
</div>
{/* 底部控制区域 */}
<div
className={`absolute bottom-4 left-4 right-4 transition-all duration-300 transform
${hoveredId === video.id ? 'translate-y-0 opacity-100' : 'translate-y-[10px] opacity-0'}
`}
>
{/* 音量控制滑块 */}
{isPlaying[video.id] && (
<div className="flex items-center gap-2 bg-black/60 rounded-full px-3 py-2 backdrop-blur-sm mb-3">
<Volume2 className="w-4 h-4 text-white" />
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume[video.id] || 0.8}
onChange={(e) => handleVolumeChange(video.id, parseFloat(e.target.value))}
className="flex-1 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[video.id] || 0.8) * 100}%, rgba(255,255,255,0.2) ${(volume[video.id] || 0.8) * 100}%, rgba(255,255,255,0.2) 100%)`
}}
/>
<span className="text-xs text-white/70 w-8 text-center">
{Math.round((volume[video.id] || 0.8) * 100)}%
</span>
</div>
)}
{/* 控制按钮组 */}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="w-8 h-8 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={() => togglePlay(video.id)}
title={isPlaying[video.id] ? "暂停" : "播放"}
>
{isPlaying[video.id] ? (
<Pause className="w-4 h-4 text-white" />
) : (
<Play className="w-4 h-4 text-white" />
)}
</Button>
<Button
variant="ghost"
size="icon"
className="w-8 h-8 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={(e) => toggleMute(video.id, e)}
title={isMuted[video.id] !== false ? "取消静音" : "静音"}
>
{isMuted[video.id] !== false ? (
<VolumeX className="w-4 h-4 text-white" />
) : (
<Volume2 className="w-4 h-4 text-white" />
)}
</Button>
<Button
variant="ghost"
size="icon"
className="w-8 h-8 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={(e) => toggleFullscreen(video.id, e)}
title={isFullscreen[video.id] ? "退出全屏" : "全屏"}
>
{isFullscreen[video.id] ? (
<Minimize className="w-4 h-4 text-white" />
) : (
<Maximize className="w-4 h-4 text-white" />
)}
</Button>
</div>
</div>
</div>
{/* 视频信息 */}
<div className="p-4">
<h3 className="text-white text-lg font-medium mb-1">{video.title}</h3>
{video.date && (
<p className="text-white/60 text-sm">{video.date}</p>
)}
</div>
</div>
))}
</div>
);
}
// Export as a client-only component to prevent hydration issues
export const VideoGridLayout = dynamic(() => Promise.resolve(VideoGridLayoutComponent), {
ssr: false
});