video-flow-b/components/pages/create-to-video2.tsx
2025-08-23 01:30:24 +08:00

273 lines
11 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';
// 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 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="container mx-auto h-[calc(100vh-6rem)] flex flex-col absolute top-[4rem] left-0 right-0 px-[1rem]">
{/* 优化后的主要内容区域 */}
<div className="flex-1 min-h-0">
<div
ref={scrollContainerRef}
className="h-full overflow-y-auto 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 xl:grid-cols-4 2xl:grid-cols-5 gap-6 pb-6">
{[...Array(10)].map((_, index) => (
<div
key={index}
className="bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-2xl overflow-hidden animate-pulse"
>
<div className="h-[200px] bg-gradient-to-br from-white/[0.08] to-white/[0.04]">
<div className="h-full bg-white/[0.06] animate-pulse"></div>
</div>
<div className="p-5">
<div className="h-4 bg-white/[0.08] rounded-lg mb-3 animate-pulse"></div>
<div className="h-3 bg-white/[0.06] rounded-lg mb-4 w-3/4 animate-pulse"></div>
<div className="flex justify-between">
<div className="h-3 bg-white/[0.06] rounded-lg w-20 animate-pulse"></div>
<div className="h-3 bg-white/[0.06] rounded-lg w-16 animate-pulse"></div>
</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 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
{episodeList.map(renderEpisodeCard)}
</div>
{/* 加载更多指示器 */}
{isLoadingMore && (
<div className="flex justify-center py-12">
<div className="flex items-center gap-3 px-6 py-3 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl">
<Loader2 className="w-5 h-5 animate-spin text-blue-400" />
<span className="text-white/70 font-medium">Loading more episodes...</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-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl flex items-center justify-center mx-auto mb-3">
<Check className="w-6 h-6 text-green-400" />
</div>
<p className="text-white/50 text-sm">All episodes loaded</p>
</div>
</div>
)}
</div>
) : (
<></>
)}
</div>
</div>
</div>
{/* 视频工具组件 - 使用独立组件 */}
<ChatInputBox />
{episodeList.length === 0 && !isLoading && (
<div className='h-full flex flex-col items-center fixed top-[4rem] left-0 right-0 bottom-0'>
<EmptyStateAnimation className='' />
</div>
)}
</>
);
}