forked from 77media/video-flow
437 lines
14 KiB
TypeScript
437 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useRef, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Edit3, Play, Pause } from 'lucide-react';
|
|
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
|
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
|
|
|
interface MediaViewerProps {
|
|
currentStep: string;
|
|
currentSketchIndex: number;
|
|
taskSketch: any[];
|
|
taskVideos: any[];
|
|
isVideoPlaying: boolean;
|
|
isPlaying: boolean;
|
|
showControls: boolean;
|
|
onControlsChange: (show: boolean) => void;
|
|
onEditModalOpen: (tab: string) => void;
|
|
onToggleVideoPlay: () => void;
|
|
onTogglePlay: () => void;
|
|
}
|
|
|
|
const MOCK_FINAL_VIDEO = {
|
|
id: 'final-video',
|
|
url: 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
|
|
thumbnail: 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
|
};
|
|
|
|
export function MediaViewer({
|
|
currentStep,
|
|
currentSketchIndex,
|
|
taskSketch,
|
|
taskVideos,
|
|
isVideoPlaying,
|
|
isPlaying,
|
|
showControls,
|
|
onControlsChange,
|
|
onEditModalOpen,
|
|
onToggleVideoPlay,
|
|
onTogglePlay
|
|
}: MediaViewerProps) {
|
|
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
// 视频播放控制
|
|
useEffect(() => {
|
|
if (mainVideoRef.current) {
|
|
if (isVideoPlaying) {
|
|
mainVideoRef.current.play().catch(error => {
|
|
console.log('视频播放失败:', error);
|
|
});
|
|
} else {
|
|
mainVideoRef.current.pause();
|
|
}
|
|
}
|
|
}, [isVideoPlaying]);
|
|
|
|
// 当切换视频时重置视频播放
|
|
useEffect(() => {
|
|
if (mainVideoRef.current) {
|
|
mainVideoRef.current.currentTime = 0;
|
|
if (isVideoPlaying) {
|
|
mainVideoRef.current.play().catch(error => {
|
|
console.log('视频播放失败:', error);
|
|
});
|
|
}
|
|
}
|
|
}, [currentSketchIndex]);
|
|
|
|
// 渲染最终成片
|
|
const renderFinalVideo = () => (
|
|
<div
|
|
className="relative w-full h-full rounded-lg overflow-hidden"
|
|
onMouseEnter={() => onControlsChange(true)}
|
|
onMouseLeave={() => onControlsChange(false)}
|
|
>
|
|
<div className="relative w-full h-full">
|
|
{/* 背景模糊的视频 */}
|
|
<motion.div
|
|
className="absolute inset-0 overflow-hidden"
|
|
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
|
|
animate={{ filter: "blur(20px)", scale: 1.1, opacity: 0.5 }}
|
|
transition={{ duration: 0.8, ease: "easeInOut" }}
|
|
>
|
|
<video
|
|
className="w-full h-full rounded-lg object-cover object-center"
|
|
src={taskVideos[currentSketchIndex]?.url}
|
|
autoPlay
|
|
loop
|
|
muted
|
|
playsInline
|
|
/>
|
|
</motion.div>
|
|
|
|
{/* 最终成片视频 */}
|
|
<motion.div
|
|
initial={{ clipPath: "inset(0 50% 0 50%)", filter: "blur(10px)" }}
|
|
animate={{ clipPath: "inset(0 0% 0 0%)", filter: "blur(0px)" }}
|
|
transition={{
|
|
clipPath: { duration: 1.2, ease: [0.43, 0.13, 0.23, 0.96] },
|
|
filter: { duration: 0.6, delay: 0.3 }
|
|
}}
|
|
className="relative z-10"
|
|
>
|
|
<video
|
|
className="w-full h-full object-cover rounded-lg"
|
|
src={MOCK_FINAL_VIDEO.url}
|
|
poster={MOCK_FINAL_VIDEO.thumbnail}
|
|
autoPlay
|
|
loop
|
|
muted
|
|
playsInline
|
|
/>
|
|
</motion.div>
|
|
|
|
{/* 操作按钮组 */}
|
|
<AnimatePresence>
|
|
{showControls && (
|
|
<motion.div
|
|
className="absolute top-4 right-4 z-10 flex gap-2"
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<GlassIconButton
|
|
icon={Edit3}
|
|
tooltip="编辑分镜"
|
|
onClick={() => onEditModalOpen('4')}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 视频信息浮层 */}
|
|
<motion.div
|
|
className="absolute bottom-0 left-0 right-0 z-10 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 1, duration: 0.6 }}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<motion.div
|
|
className="w-2 h-2 rounded-full bg-emerald-500"
|
|
animate={{
|
|
scale: [1, 1.2, 1],
|
|
opacity: [1, 0.6, 1]
|
|
}}
|
|
transition={{
|
|
duration: 2,
|
|
repeat: Infinity,
|
|
ease: "easeInOut"
|
|
}}
|
|
/>
|
|
<span className="text-sm font-medium text-white/90">最终成片</span>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* 完成标记 */}
|
|
<motion.div
|
|
className="absolute top-4 right-4 px-3 py-1.5 rounded-full bg-emerald-500/20 backdrop-blur-sm
|
|
border border-emerald-500/30 text-emerald-400 text-sm font-medium"
|
|
initial={{ opacity: 0, scale: 0.8, x: 20 }}
|
|
animate={{ opacity: 1, scale: 1, x: 0 }}
|
|
transition={{ delay: 1.2, duration: 0.6 }}
|
|
>
|
|
制作完成
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// 渲染视频内容
|
|
const renderVideoContent = () => (
|
|
<div
|
|
className="relative w-full h-full rounded-lg"
|
|
onMouseEnter={() => onControlsChange(true)}
|
|
onMouseLeave={() => onControlsChange(false)}
|
|
>
|
|
{taskVideos[currentSketchIndex] ? (
|
|
<ProgressiveReveal
|
|
className="w-full h-full rounded-lg"
|
|
customVariants={{
|
|
hidden: {
|
|
opacity: 0,
|
|
filter: "blur(20px)",
|
|
clipPath: "inset(0 100% 0 0)"
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
filter: "blur(0px)",
|
|
clipPath: "inset(0 0% 0 0)",
|
|
transition: {
|
|
duration: 1,
|
|
ease: [0.43, 0.13, 0.23, 0.96],
|
|
opacity: { duration: 0.8, ease: "easeOut" },
|
|
filter: { duration: 0.6, ease: "easeOut" },
|
|
clipPath: { duration: 0.8, ease: "easeInOut" }
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<div className="relative w-full h-full">
|
|
{/* 背景模糊的图片 */}
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<img
|
|
className="w-full h-full object-cover filter blur-lg scale-110 opacity-50"
|
|
src={taskSketch[currentSketchIndex]?.url}
|
|
alt="background"
|
|
/>
|
|
</div>
|
|
|
|
{/* 视频 */}
|
|
<motion.div
|
|
initial={{ clipPath: "inset(0 100% 0 0)" }}
|
|
animate={{ clipPath: "inset(0 0% 0 0)" }}
|
|
transition={{ duration: 0.8, ease: [0.43, 0.13, 0.23, 0.96] }}
|
|
className="relative z-10"
|
|
>
|
|
<video
|
|
ref={mainVideoRef}
|
|
key={taskVideos[currentSketchIndex].url}
|
|
className="w-full h-full rounded-lg object-cover object-center"
|
|
src={taskVideos[currentSketchIndex].url}
|
|
autoPlay={isVideoPlaying}
|
|
muted
|
|
loop={false}
|
|
playsInline
|
|
poster={taskSketch[currentSketchIndex]?.url}
|
|
onEnded={() => {
|
|
if (isVideoPlaying) {
|
|
// 自动切换到下一个视频的逻辑在父组件处理
|
|
}
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
</ProgressiveReveal>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
|
<img
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
src={taskSketch[currentSketchIndex]?.url}
|
|
alt={`分镜草图 ${currentSketchIndex + 1}`}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 操作按钮组 */}
|
|
<AnimatePresence>
|
|
{showControls && (
|
|
<motion.div
|
|
className="absolute top-4 right-4 flex gap-2"
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<GlassIconButton
|
|
icon={Edit3}
|
|
tooltip="编辑分镜"
|
|
onClick={() => onEditModalOpen('3')}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 底部播放按钮 */}
|
|
<AnimatePresence>
|
|
<motion.div
|
|
className="absolute bottom-4 left-4"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<motion.div
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
>
|
|
<GlassIconButton
|
|
icon={isVideoPlaying ? Pause : Play}
|
|
tooltip={isVideoPlaying ? "暂停播放" : "自动播放"}
|
|
onClick={onToggleVideoPlay}
|
|
size="sm"
|
|
/>
|
|
</motion.div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
|
|
// 渲染分镜草图
|
|
const renderSketchContent = () => (
|
|
<div
|
|
className="relative w-full h-full rounded-lg"
|
|
onMouseEnter={() => onControlsChange(true)}
|
|
onMouseLeave={() => onControlsChange(false)}
|
|
>
|
|
{taskSketch[currentSketchIndex] ? (
|
|
<ProgressiveReveal
|
|
className="w-full h-full rounded-lg"
|
|
{...presets.main}
|
|
>
|
|
<img
|
|
key={currentSketchIndex}
|
|
src={taskSketch[currentSketchIndex].url}
|
|
alt={`分镜草图 ${currentSketchIndex + 1}`}
|
|
className="w-full h-full rounded-lg object-cover"
|
|
/>
|
|
</ProgressiveReveal>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
|
{/* 动态渐变背景 */}
|
|
<motion.div
|
|
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
|
|
animate={{
|
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
|
}}
|
|
transition={{
|
|
duration: 5,
|
|
repeat: Infinity,
|
|
ease: "linear"
|
|
}}
|
|
style={{
|
|
backgroundSize: "200% 200%",
|
|
}}
|
|
/>
|
|
{/* 动态光效 */}
|
|
<motion.div
|
|
className="absolute inset-0 opacity-50"
|
|
style={{
|
|
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
|
}}
|
|
animate={{
|
|
scale: [1, 1.2, 1],
|
|
}}
|
|
transition={{
|
|
duration: 2,
|
|
repeat: Infinity,
|
|
ease: "easeInOut"
|
|
}}
|
|
/>
|
|
<motion.div
|
|
className="flex flex-col items-center gap-4 relative z-10"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<div className="relative">
|
|
<motion.div
|
|
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
|
animate={{
|
|
scale: [1, 1.2, 1],
|
|
rotate: [0, 180, 360],
|
|
}}
|
|
transition={{
|
|
duration: 4,
|
|
repeat: Infinity,
|
|
ease: "linear"
|
|
}}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 操作按钮组 */}
|
|
<AnimatePresence>
|
|
{showControls && (
|
|
<motion.div
|
|
className="absolute top-4 right-4 flex gap-2"
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<GlassIconButton
|
|
icon={Edit3}
|
|
tooltip="编辑分镜"
|
|
onClick={() => onEditModalOpen('1')}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 底部播放按钮 */}
|
|
<AnimatePresence>
|
|
<motion.div
|
|
className="absolute bottom-4 left-4"
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<motion.div
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
>
|
|
<GlassIconButton
|
|
icon={isPlaying ? Pause : Play}
|
|
tooltip={isPlaying ? "暂停播放" : "自动播放"}
|
|
onClick={onTogglePlay}
|
|
size="sm"
|
|
/>
|
|
</motion.div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
|
|
{/* 播放进度指示器 */}
|
|
<AnimatePresence>
|
|
{isPlaying && (
|
|
<motion.div
|
|
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500/20"
|
|
initial={{ scaleX: 0 }}
|
|
animate={{ scaleX: 1 }}
|
|
exit={{ scaleX: 0 }}
|
|
transition={{ duration: 2, repeat: Infinity }}
|
|
style={{ transformOrigin: "left" }}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
|
|
// 根据当前步骤渲染对应内容
|
|
if (Number(currentStep) === 6) {
|
|
return renderFinalVideo();
|
|
}
|
|
|
|
if (Number(currentStep) > 2 && Number(currentStep) < 6) {
|
|
return renderVideoContent();
|
|
}
|
|
|
|
return renderSketchContent();
|
|
}
|