forked from 77media/video-flow
413 lines
16 KiB
TypeScript
413 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { ArrowLeft, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Calendar, Clock, Eye, Heart, Share2, Video } from 'lucide-react';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import './style/create-to-video2.css';
|
|
|
|
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
|
|
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
|
|
import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
|
|
import cover_image1 from '@/public/assets/cover_image1.jpg';
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
|
// ideaText已迁移到ChatInputBox组件中
|
|
|
|
export default function CreateToVideo2() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const projectId = searchParams.get('projectId') ? parseInt(searchParams.get('projectId')!) : 0;
|
|
const [isClient, setIsClient] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [runTour, setRunTour] = useState(true);
|
|
const [episodeId, setEpisodeId] = useState<number>(0);
|
|
const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]);
|
|
const [projectName, setProjectName] = useState('默认名称');
|
|
const [episodeList, setEpisodeList] = useState<any[]>([]);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [totalPage, setTotalPage] = useState(1);
|
|
const [limit, setLimit] = useState(12);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [hasMore, setHasMore] = useState(true);
|
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
const [userId, setUserId] = useState<number>(0);
|
|
|
|
// 在客户端挂载后读取localStorage
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
console.log('currentUser', currentUser);
|
|
setUserId(currentUser.id);
|
|
const savedProjectName = localStorage.getItem('projectName');
|
|
if (savedProjectName) {
|
|
setProjectName(savedProjectName);
|
|
}
|
|
getEpisodeList(currentUser.id);
|
|
}
|
|
}, []);
|
|
|
|
// 获取剧集列表
|
|
const getEpisodeList = async (userId: number) => {
|
|
if (isLoading || isLoadingMore) return;
|
|
console.log('getEpisodeList', userId);
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const params = {
|
|
user_id: String(userId),
|
|
};
|
|
|
|
const episodeListResponse = await getScriptEpisodeListNew(params);
|
|
console.log('episodeListResponse', episodeListResponse);
|
|
|
|
if (episodeListResponse.code === 0) {
|
|
setEpisodeList(episodeListResponse.data.movie_projects);
|
|
// 每一项 有
|
|
// final_video_url: "", // 生成的视频地址
|
|
// last_message: "",
|
|
// name: "After the Flood", // 剧集名称
|
|
// project_id: "9c34fcc4-c8d8-44fc-879e-9bd56f608c76", // 剧集ID
|
|
// status: "INIT", // 剧集状态 INIT 初始化
|
|
// step: "INIT" // 剧集步骤 INIT 初始化
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch episode list:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
setIsLoadingMore(false);
|
|
}
|
|
};
|
|
|
|
// 视频上传和创建功能已迁移到ChatInputBox组件中
|
|
|
|
// 所有视频工具相关的函数和配置已迁移到ChatInputBox组件中
|
|
|
|
// 检查是否需要显示引导
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
const hasCompletedTour = localStorage.getItem('hasCompletedTour');
|
|
if (hasCompletedTour) {
|
|
setRunTour(false);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setIsClient(true);
|
|
}, []);
|
|
|
|
const StatusBadge = (status: string) => {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
className="flex items-center gap-2 rounded-full
|
|
bg-white/10 border border-white/20
|
|
px-3 py-1 backdrop-blur-md shadow-[0_0_8px_rgba(255,255,255,0.3)]"
|
|
>
|
|
{/* 进行中 脉冲小圆点 */}
|
|
{status === 'pending' && (
|
|
<>
|
|
<motion.span
|
|
className="w-2 h-2 rounded-full bg-yellow-400 shadow-[0_0_8px_rgba(255,220,100,0.9)]"
|
|
animate={{ scale: [1, 1.4, 1] }}
|
|
transition={{ repeat: Infinity, duration: 1.5 }}
|
|
/>
|
|
{/* 状态文字 */}
|
|
<span className="text-xs tracking-widest text-yellow-300 font-medium drop-shadow-[0_0_6px_rgba(255,220,100,0.6)]">
|
|
PROCESSING
|
|
</span>
|
|
</>
|
|
)}
|
|
{/* 已完成 */}
|
|
{status === 'completed' && (
|
|
<>
|
|
<motion.span
|
|
className="w-2 h-2 rounded-full bg-green-400 shadow-[0_0_8px_rgba(0,255,120,0.9)]"
|
|
/>
|
|
<span className="text-xs tracking-widest text-green-300 font-medium drop-shadow-[0_0_6px_rgba(0,255,120,0.6)]">
|
|
COMPLETED
|
|
</span>
|
|
</>
|
|
)}
|
|
{/* 失败 */}
|
|
{status === 'failed' && (
|
|
<>
|
|
<motion.span
|
|
className="w-2 h-2 rounded-full bg-red-500 shadow-[0_0_8px_rgba(255,0,80,0.9)]"
|
|
/>
|
|
<span className="text-xs tracking-widest text-red-400 font-medium drop-shadow-[0_0_6px_rgba(255,0,80,0.6)]">
|
|
FAILED
|
|
</span>
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// 创建一个视频引用Map
|
|
const videoRefs = useRef<Map<string, HTMLVideoElement>>(new Map());
|
|
|
|
const handleMouseEnter = (projectId: string) => {
|
|
const videoElement = videoRefs.current.get(projectId);
|
|
if (videoElement) {
|
|
videoElement.play().catch(() => {
|
|
console.log('Video autoplay prevented');
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleMouseLeave = (projectId: string) => {
|
|
const videoElement = videoRefs.current.get(projectId);
|
|
if (videoElement) {
|
|
videoElement.pause();
|
|
videoElement.currentTime = 0;
|
|
}
|
|
};
|
|
|
|
const setVideoRef = (projectId: string, element: HTMLVideoElement | null) => {
|
|
if (element) {
|
|
videoRefs.current.set(projectId, element);
|
|
} else {
|
|
videoRefs.current.delete(projectId);
|
|
}
|
|
};
|
|
|
|
const renderProjectCard = (project: any) => {
|
|
|
|
return (
|
|
<div
|
|
key={project.project_id}
|
|
className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden cursor-pointer"
|
|
onClick={() => router.push(`/create/work-flow?episodeId=${project.project_id}`)}
|
|
onMouseEnter={() => handleMouseEnter(project.project_id)}
|
|
onMouseLeave={() => handleMouseLeave(project.project_id)}
|
|
data-alt="project-card"
|
|
>
|
|
{/* 视频/图片区域 */}
|
|
{project.final_video_url ? (
|
|
<video
|
|
ref={(el) => setVideoRef(project.project_id, el)}
|
|
src={project.final_video_url}
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
|
muted
|
|
loop
|
|
playsInline
|
|
preload="none"
|
|
poster={`${project.final_video_url}?vframe/jpg/offset/1`}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="w-full h-full bg-cover bg-center group-hover:scale-105 transition-transform duration-500"
|
|
style={{
|
|
backgroundImage: `url(${cover_image1.src})`,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* 渐变遮罩 */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
|
|
|
{/* 状态标签 - 左上角 */}
|
|
<div className="absolute top-3 left-3">
|
|
{StatusBadge(project.status === 'COMPLETED' ? 'completed' : project.status === 'FAILED' ? 'failed' : 'pending')}
|
|
</div>
|
|
|
|
{/* 底部信息 */}
|
|
<div className="absolute bottom-0 left-0 right-0 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-medium text-white line-clamp-1">
|
|
{project.name || "Unnamed"}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 渲染剧集卡片
|
|
const renderEpisodeCard = (episode: any) => {
|
|
return (
|
|
<div
|
|
key={episode.project_id}
|
|
className="group relative bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] rounded-[12px] overflow-hidden hover:bg-white/[0.12] transition-all duration-300 hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)] cursor-pointer"
|
|
onClick={() => router.push(`/create/work-flow?episodeId=${episode.project_id}`)}
|
|
>
|
|
{/* 视频缩略图 */}
|
|
<div className="relative h-[180px] bg-gradient-to-br from-purple-500/20 to-blue-500/20 overflow-hidden">
|
|
{episode.final_video_url ? (
|
|
<video
|
|
src={episode.final_video_url}
|
|
className="w-full h-full object-cover"
|
|
muted
|
|
loop
|
|
preload="none"
|
|
poster={`${episode.final_video_url}?vframe/jpg/offset/1`}
|
|
onMouseEnter={(e) => (e.target as HTMLVideoElement).play()}
|
|
onMouseLeave={(e) => (e.target as HTMLVideoElement).pause()}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full">
|
|
<Video className="w-12 h-12 text-white/30" />
|
|
</div>
|
|
)}
|
|
|
|
{/* 播放按钮覆盖 */}
|
|
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
|
|
<Play className="w-6 h-6 text-white ml-1" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* 状态标签 */}
|
|
<div className="absolute top-3 left-3">
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
episode.status === 'COMPLETED' ? 'bg-green-500/80 text-white' :
|
|
episode.status !== 'COMPLETED' ? 'bg-yellow-500/80 text-white' :
|
|
'bg-gray-500/80 text-white'
|
|
}`}>
|
|
{episode.status === 'COMPLETED' ? 'finished' : 'processing'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 时长标签 */}
|
|
{episode.duration && (
|
|
<div className="absolute bottom-3 right-3">
|
|
<span className="px-2 py-1 bg-black/60 backdrop-blur-sm rounded text-xs text-white">
|
|
{episode.duration}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 内容区域 */}
|
|
<div className="p-4">
|
|
<h3 className="text-white font-medium text-sm mb-2 line-clamp-2 group-hover:text-blue-300 transition-colors">
|
|
{episode.name || episode.title || 'Unnamed episode'}
|
|
</h3>
|
|
|
|
{/* 元数据 */}
|
|
<div className="flex items-center justify-between text-xs text-white/40">
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="w-3 h-3" />
|
|
<span>{new Date(episode.created_at).toLocaleDateString()}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
<span>{new Date(episode.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
<div className="flex gap-2">
|
|
<button className="w-8 h-8 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-colors">
|
|
<Share2 className="w-4 h-4 text-white" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col absolute top-[5rem] left-0 right-0 bottom-[1rem] px-6">
|
|
{/* 优化后的主要内容区域 */}
|
|
<div className="flex-1 min-h-0">
|
|
<div
|
|
ref={scrollContainerRef}
|
|
className="h-full overflow-y-auto overflow-x-hidden custom-scrollbar"
|
|
style={{
|
|
scrollbarWidth: 'thin',
|
|
scrollbarColor: 'rgba(255,255,255,0.1) transparent'
|
|
}}
|
|
>
|
|
{isLoading && episodeList.length === 0 ? (
|
|
/* 优化的加载状态 */
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pb-6">
|
|
{[...Array(6)].map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden animate-pulse"
|
|
>
|
|
{/* 背景占位 */}
|
|
<div className="w-full h-full bg-gradient-to-br from-white/[0.04] to-white/[0.02]" />
|
|
|
|
{/* 渐变遮罩 */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
|
|
|
{/* 状态标签占位 */}
|
|
<div className="absolute top-3 left-3">
|
|
<div className="flex items-center gap-2 rounded-full bg-white/10 px-3 py-1">
|
|
<div className="w-2 h-2 rounded-full bg-white/20"></div>
|
|
<div className="w-16 h-3 bg-white/20 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 项目ID占位 */}
|
|
<div className="absolute top-3 right-3">
|
|
<div className="w-20 h-3 bg-white/10 rounded-full"></div>
|
|
</div>
|
|
|
|
{/* 底部信息占位 */}
|
|
<div className="absolute bottom-0 left-0 right-0 p-4">
|
|
<div className="w-2/3 h-5 bg-white/10 rounded-lg"></div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : episodeList.length > 0 ? (
|
|
/* 优化的剧集网格 */
|
|
<div className="pb-8">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{episodeList.map(renderProjectCard)}
|
|
</div>
|
|
|
|
{/* 加载更多指示器 */}
|
|
{isLoadingMore && (
|
|
<div className="flex justify-center py-12">
|
|
<div className="flex items-center gap-3 px-6 py-3 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full">
|
|
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
|
|
<span className="text-white/90 font-medium">正在加载更多项目...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 到底提示 */}
|
|
{!hasMore && episodeList.length > 0 && (
|
|
<div className="flex justify-center py-12">
|
|
<div className="text-center">
|
|
<div className="w-12 h-12 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
<Check className="w-6 h-6 text-purple-400" />
|
|
</div>
|
|
<p className="text-white/70 text-sm">已加载全部项目</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<></>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 视频工具组件 - 使用独立组件 */}
|
|
{!isLoading &&
|
|
<ChatInputBox noData={episodeList.length === 0} />
|
|
}
|
|
</>
|
|
);
|
|
}
|