forked from 77media/video-flow
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
"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 (
|
||
<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) ? (
|
||
<LazyLoad once>
|
||
<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)
|
||
}
|
||
/>
|
||
</LazyLoad>
|
||
) : (
|
||
<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>
|
||
);
|
||
};
|
||
|
||
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>
|
||
);
|
||
}
|