video-flow-b/components/video-grid-layout.tsx
2025-07-03 06:42:58 +08:00

153 lines
5.3 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, Volume2, VolumeX } 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 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 = false;
video.play();
setIsPlaying(prev => ({ ...prev, [id]: true }));
setIsMuted(prev => ({ ...prev, [id]: false }));
} 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 }));
}
};
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
/>
{/* 播放按钮遮罩 */}
<div
className={`absolute inset-0 flex items-center justify-center bg-black/40 transition-opacity duration-300
${hoveredId === video.id ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => togglePlay(video.id)}
>
<Play className={`w-12 h-12 text-white/90 transition-transform duration-300
${isPlaying[video.id] ? 'scale-90 opacity-0' : 'scale-100 opacity-100'}`}
/>
</div>
{/* 操作按钮 */}
<div
className={`absolute top-4 right-4 flex gap-2 transition-all duration-300 transform
${hoveredId === video.id ? 'translate-y-0 opacity-100' : 'translate-y-[-10px] opacity-0'}
`}
>
<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={() => onEdit?.(video.id)}
>
<Edit2 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={() => onDelete?.(video.id)}
>
<Trash2 className="w-4 h-4 text-white" />
</Button>
</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
});