forked from 77media/video-flow
332 lines
13 KiB
TypeScript
332 lines
13 KiB
TypeScript
"use client"
|
||
import { useEffect, useState, useRef } from "react";
|
||
import { Play, ChevronUp, Loader2 } from "lucide-react";
|
||
import "./style/work-flow.css";
|
||
import LiquidGlass from '@/plugins/liquid-glass';
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import { AISuggestionBar } from "@/components/ai-suggestion-bar";
|
||
import { motion, AnimatePresence } from "framer-motion";
|
||
import { debounce } from "lodash";
|
||
|
||
const MOCK_SKETCH_URLS = [
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||
];
|
||
const MOCK_SKETCH_SCRIPT = [
|
||
'script-123',
|
||
'script-123',
|
||
'script-123',
|
||
'script-123',
|
||
];
|
||
const MOCK_SKETCH_COUNT = 8;
|
||
|
||
export default function WorkFlow() {
|
||
const [taskObject, setTaskObject] = useState<any>(null);
|
||
const [projectObject, setProjectObject] = useState<any>(null);
|
||
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
||
const [sketchCount, setSketchCount] = useState(0);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [isAIBarVisible, setIsAIBarVisible] = useState(true);
|
||
const [currentStep, setCurrentStep] = useState('0');
|
||
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
||
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [startX, setStartX] = useState(0);
|
||
const [scrollLeft, setScrollLeft] = useState(0);
|
||
|
||
// 模拟 AI 建议
|
||
const mockSuggestions = [
|
||
"优化场景转场效果",
|
||
"调整画面构图",
|
||
"改进角色动作设计",
|
||
"增加环境氛围",
|
||
"调整镜头语言"
|
||
];
|
||
|
||
useEffect(() => {
|
||
const taskId = localStorage.getItem("taskId") || "taskId-123";
|
||
getTaskDetail(taskId).then((data) => {
|
||
setTaskObject(data);
|
||
setIsLoading(false);
|
||
setCurrentStep('1');
|
||
});
|
||
// 轮询获取分镜草图 防抖 1000ms
|
||
const debouncedGetTaskSketch = debounce(() => {
|
||
getTaskSketch(taskId);
|
||
}, 1000);
|
||
debouncedGetTaskSketch();
|
||
}, []);
|
||
|
||
// 监听当前选中索引变化,自动滚动到对应位置
|
||
useEffect(() => {
|
||
if (thumbnailsRef.current && taskSketch.length > 0) {
|
||
const container = thumbnailsRef.current;
|
||
const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距)
|
||
const scrollPosition = currentSketchIndex * thumbnailWidth;
|
||
|
||
container.scrollTo({
|
||
left: scrollPosition,
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
}, [currentSketchIndex, taskSketch.length]);
|
||
|
||
// 处理鼠标/触摸拖动事件
|
||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||
setIsDragging(true);
|
||
setStartX(e.pageX - thumbnailsRef.current!.offsetLeft);
|
||
setScrollLeft(thumbnailsRef.current!.scrollLeft);
|
||
};
|
||
|
||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||
if (!isDragging) return;
|
||
e.preventDefault();
|
||
const x = e.pageX - thumbnailsRef.current!.offsetLeft;
|
||
const walk = (x - startX) * 2;
|
||
thumbnailsRef.current!.scrollLeft = scrollLeft - walk;
|
||
};
|
||
|
||
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
||
setIsDragging(false);
|
||
if (!isDragging) return;
|
||
|
||
const container = thumbnailsRef.current!;
|
||
const thumbnailWidth = container.offsetWidth / 4;
|
||
const currentScroll = container.scrollLeft;
|
||
const nearestIndex = Math.round(currentScroll / thumbnailWidth);
|
||
|
||
// 只有在拖动距离较小时才触发选中
|
||
const x = e.pageX - container.offsetLeft;
|
||
const walk = Math.abs(x - startX);
|
||
if (walk < 10) {
|
||
return; // 如果拖动距离太小,保持原有的点击选中逻辑
|
||
}
|
||
|
||
setCurrentSketchIndex(Math.min(Math.max(0, nearestIndex), taskSketch.length - 1));
|
||
};
|
||
|
||
// 模拟接口请求 获取任务详情
|
||
const getTaskDetail = async (taskId: string) => {
|
||
// const response = await fetch(`/api/task/${taskId}`);
|
||
// const data = await response.json();
|
||
// mock data
|
||
const data = {
|
||
projectId: 'projectId-123',
|
||
projectName: "Project 1",
|
||
taskId: taskId,
|
||
taskName: "Task 1",
|
||
taskDescription: "Task 1 Description",
|
||
taskStatus: "1", // '1' 绘制分镜、'2' 绘制角色、'3' 生成分镜视频、'4' 视频后期制作、'5' 最终成品
|
||
taskProgress: 0,
|
||
taskCreatedAt: new Date().toISOString(),
|
||
taskUpdatedAt: new Date().toISOString(),
|
||
};
|
||
return data;
|
||
}
|
||
|
||
// 模拟接口请求 每次获取一个分镜草图 轮询获取
|
||
const getTaskSketch = async (taskId: string) => {
|
||
setIsGeneratingSketch(true);
|
||
setTaskSketch([]);
|
||
|
||
// 模拟分批获取分镜草图
|
||
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
|
||
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||
|
||
const newSketch = {
|
||
id: `sketch-${i}`,
|
||
url: MOCK_SKETCH_URLS[i % MOCK_SKETCH_URLS.length],
|
||
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
|
||
status: 'done'
|
||
};
|
||
|
||
setTaskSketch(prev => [...prev, newSketch]);
|
||
setCurrentSketchIndex(i);
|
||
setSketchCount(i + 1);
|
||
}
|
||
|
||
setIsGeneratingSketch(false);
|
||
}
|
||
|
||
const handleSuggestionClick = (suggestion: string) => {
|
||
console.log('Selected suggestion:', suggestion);
|
||
};
|
||
|
||
const handleSubmit = (text: string) => {
|
||
console.log('Submitted text:', text);
|
||
};
|
||
|
||
// 渲染分镜草图或加载动画
|
||
const renderSketchContent = () => {
|
||
if (!taskSketch[currentSketchIndex]) {
|
||
return (
|
||
<div className="w-full h-full flex items-center justify-center bg-black/20 backdrop-blur-sm rounded-lg">
|
||
<div className="flex flex-col items-center gap-4">
|
||
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
|
||
<p className="text-sm text-white/70">正在生成分镜草图 {sketchCount + 1}/{MOCK_SKETCH_COUNT}</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<motion.img
|
||
key={currentSketchIndex}
|
||
src={taskSketch[currentSketchIndex].url}
|
||
alt={`分镜草图 ${currentSketchIndex + 1}`}
|
||
className="w-full h-full object-contain rounded-lg"
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ duration: 0.3 }}
|
||
/>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="w-full h-full overflow-hidden">
|
||
<div className="flex h-full flex-col p-6 justify-center items-center">
|
||
<div className="container-H2sRZG">
|
||
<div className="splashContainer-otuV_A">
|
||
<div className="content-vPGYx8">
|
||
<div className="info-UUGkPJ">
|
||
{isLoading ? (
|
||
<>
|
||
<Skeleton className="h-8 w-64 mb-2" />
|
||
<Skeleton className="h-4 w-96" />
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="title-JtMejk">{taskObject?.projectName}:{taskObject?.taskName}</div>
|
||
<p className="normalS400 subtitle-had8uE">{taskObject?.taskDescription}</p>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="media-Ocdu1O">
|
||
<div className="videoContainer-qteKNi" ref={containerRef}>
|
||
{isLoading ? (
|
||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||
) : (
|
||
<>
|
||
{
|
||
currentStep === '1' ? (
|
||
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}>
|
||
{renderSketchContent()}
|
||
</div>
|
||
) : (
|
||
<video
|
||
className="heroVideo-FIzuK1"
|
||
src="https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome.mp4"
|
||
style={{
|
||
aspectRatio: "16 / 9"
|
||
}}
|
||
autoPlay
|
||
muted
|
||
loop
|
||
></video>
|
||
)
|
||
}
|
||
<button className="container-kIPoeH secondary-_HxO1W large-_aHMgD videoPlaybackButton-uFNO1b">
|
||
<Play className="w-6 h-6 icon" />
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="imageGrid-ymZV9z">
|
||
{isLoading ? (
|
||
<>
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
</>
|
||
) : (
|
||
<div
|
||
ref={thumbnailsRef}
|
||
className="w-full grid grid-flow-col auto-cols-[25%] gap-4 overflow-x-auto hidden-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing"
|
||
onMouseDown={handleMouseDown}
|
||
onMouseMove={handleMouseMove}
|
||
onMouseUp={handleMouseUp}
|
||
onMouseLeave={() => setIsDragging(false)}
|
||
>
|
||
{currentStep === '1' ? (
|
||
<>
|
||
{taskSketch.map((sketch, index) => (
|
||
<motion.div
|
||
key={sketch.id}
|
||
className={`relative aspect-video rounded-lg overflow-hidden
|
||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||
onClick={() => !isDragging && setCurrentSketchIndex(index)}
|
||
initial={false}
|
||
animate={{
|
||
scale: currentSketchIndex === index ? 1.05 : 1,
|
||
rotateY: currentSketchIndex === index ? 5 : 0,
|
||
rotateX: currentSketchIndex === index ? -5 : 0,
|
||
translateZ: currentSketchIndex === index ? '20px' : '0px',
|
||
transition: {
|
||
type: "spring",
|
||
stiffness: 300,
|
||
damping: 20
|
||
}
|
||
}}
|
||
style={{
|
||
transformStyle: 'preserve-3d',
|
||
perspective: '1000px'
|
||
}}
|
||
>
|
||
<motion.img
|
||
className="w-full h-full object-cover"
|
||
src={sketch.url}
|
||
alt={`缩略图 ${index + 1}`}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ duration: 0.3 }}
|
||
/>
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90">场景 {index + 1}</span>
|
||
</div>
|
||
</motion.div>
|
||
))}
|
||
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
|
||
<div className="relative aspect-video rounded-lg bg-black/20 backdrop-blur-sm
|
||
flex items-center justify-center">
|
||
<Loader2 className="w-6 h-6 text-blue-500 animate-spin" />
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
<>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* AI 建议栏 */}
|
||
<AnimatePresence>
|
||
<motion.div
|
||
initial={{ y: 100, opacity: 0 }}
|
||
animate={{ y: 0, opacity: 1 }}
|
||
exit={{ y: 100, opacity: 0 }}
|
||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||
className="mb-16"
|
||
>
|
||
<AISuggestionBar
|
||
suggestions={mockSuggestions}
|
||
onSuggestionClick={handleSuggestionClick}
|
||
onSubmit={handleSubmit}
|
||
placeholder="请输入你的想法,或点击预设词条获取 AI 建议..."
|
||
/>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
)
|
||
} |