video-flow-b/components/pages/create-to-video2.tsx
2025-09-19 16:28:13 +08:00

316 lines
11 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 { useState, useEffect, useRef, useCallback } from 'react';
import { Loader2, Download } from 'lucide-react';
import { useRouter } from 'next/navigation';
import './style/create-to-video2.css';
import { getScriptEpisodeListNew } from "@/api/script_episode";
import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
import cover_image1 from '@/public/assets/cover_image3.jpg';
import { motion } from 'framer-motion';
import { Tooltip, Button } from 'antd';
import { downloadVideo, getFirstFrame } from '@/utils/tools';
import LazyLoad from "react-lazyload";
// ideaText已迁移到ChatInputBox组件中
export default function CreateToVideo2() {
const router = useRouter();
const [episodeList, setEpisodeList] = useState<any[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [perPage] = 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);
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
// 添加一个 ref 来跟踪当前正在加载的页码
const loadingPageRef = useRef<number | null>(null);
// 在客户端挂载后读取localStorage
// 监听滚动事件,实现无限加载
// 修改滚动处理函数,添加节流
const handleScroll = useCallback(() => {
if (!scrollContainerRef.current || !hasMore || isLoadingMore || isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
if (scrollHeight - scrollTop - clientHeight < 100) {
// 直接使用 currentPage不再使用 setCurrentPage 的回调
const nextPage = currentPage + 1;
if (nextPage <= totalPages) {
getEpisodeList(userId, nextPage, true);
}
}
}, [hasMore, isLoadingMore, isLoading, totalPages, userId, currentPage]);
useEffect(() => {
if (typeof window !== 'undefined') {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
console.log('currentUser', currentUser);
setUserId(currentUser.id);
getEpisodeList(currentUser.id, 1, false);
}
}, []);
// 添加滚动监听
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
return () => scrollContainer.removeEventListener('scroll', handleScroll);
}
}, [handleScroll]);
// 修改获取剧集列表函数
const getEpisodeList = async (userId: number, page: number = 1, loadMore: boolean = false) => {
// 检查是否正在加载该页
if (loadingPageRef.current === page) return;
if (isLoading || (isLoadingMore && !loadMore)) return;
// 设置当前正在加载的页码
loadingPageRef.current = page;
if (loadMore) {
setIsLoadingMore(true);
} else {
setIsLoading(true);
}
try {
const params = {
user_id: String(userId),
page,
per_page: perPage
};
const episodeListResponse = await getScriptEpisodeListNew(params);
if (episodeListResponse.code === 0) {
const { movie_projects, total_pages } = episodeListResponse.data;
// 确保数据不重复
if (loadMore) {
setEpisodeList(prev => {
const newProjects = movie_projects.filter(
project => !prev.some(p => p.project_id === project.project_id)
);
return [...prev, ...newProjects];
});
} else {
setEpisodeList(movie_projects);
}
setTotalPages(total_pages);
setHasMore(page < total_pages);
setCurrentPage(page);
}
} catch (error) {
console.error('Failed to fetch episode list:', error);
} finally {
setIsLoading(false);
setIsLoadingMore(false);
// 清除当前加载页码
loadingPageRef.current = null;
}
};
// 视频上传和创建功能已迁移到ChatInputBox组件中
// 所有视频工具相关的函数和配置已迁移到ChatInputBox组件中
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"
>
{/* 进行中 脉冲小圆点 */}
{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)]"></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 (
<LazyLoad once>
<div
key={project.project_id}
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300"
onMouseEnter={() => handleMouseEnter(project.project_id)}
onMouseLeave={() => handleMouseLeave(project.project_id)}
data-alt="project-card"
>
{/* 视频/图片区域 */}
<div className="relative aspect-video" onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}>
{(project.final_video_url || project.final_simple_video_url || project.video_urls) ? (
<video
ref={(el) => setVideoRef(project.project_id, el)}
src={project.final_video_url || project.final_simple_video_url || project.video_urls}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
muted
loop
playsInline
preload="none"
poster={
getFirstFrame(project.final_video_url || project.final_simple_video_url || project.video_urls)
}
/>
) : (
<div
className="w-full h-full bg-cover bg-center group-hover:scale-105 transition-transform duration-500"
style={{
backgroundImage: `url(${cover_image1.src})`,
}}
/>
)}
{/* 下载按钮 右上角 */}
{(project.final_video_url || project.final_simple_video_url) && (
<div className="absolute top-1 right-1">
<Tooltip placement="top" title="Download">
<Button size="small" type="text" disabled={isLoadingDownloadBtn} className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" onClick={async (e) => {
e.stopPropagation(); // 阻止事件冒泡
setIsLoadingDownloadBtn(true);
await downloadVideo(project.final_video_url || project.final_simple_video_url);
setIsLoadingDownloadBtn(false);
}}>
{isLoadingDownloadBtn ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : <Download className="w-4 h-4 text-white" />}
</Button>
</Tooltip>
</div>
)}
{/* 状态标签 - 左上角 */}
<div className="absolute top-3 left-3">
{StatusBadge((project.status === 'COMPLETED' || project.final_simple_video_url) ? 'completed' : project.status === 'FAILED' ? 'failed' : 'pending')}
</div>
</div>
{/* 底部信息 */}
<div className="p-4 group">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-sm font-medium text-white line-clamp-1">
{project.name || "Unnamed"}
</h2>
{/* TODO 编辑标题 */}
{/* <Tooltip title="Edit Title">
<Button size="small" type="text" className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" /></Button>
</Tooltip> */}
</div>
{/* TODO 删除 */}
{/* <Tooltip title="Delete">
<Button size="small" type="text" className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" /></Button>
</Tooltip> */}
</div>
</div>
</div>
</LazyLoad>
);
};
return (
<div className="flex flex-col w-full h-full relative px-2">
<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'
}}
>
{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">
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
</div>
</div>
)}
</div>
)}
{episodeList.length === 0 && isLoading && (
<div className="flex justify-center py-12 h-full">
<div className="flex items-center">
<Loader2 className="w-8 h-8 animate-spin text-purple-400" />
</div>
</div>
)}
</div>
{/* 视频工具组件 - 使用独立组件 */}
{!isLoading &&
<ChatInputBox noData={episodeList.length === 0} />
}
</div>
);
}