video-flow-b/components/pages/work-flow.tsx
2025-06-26 20:12:55 +08:00

332 lines
13 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"
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>
)
}