import { useRef, useEffect, useState, useMemo, useCallback } from 'react'; import { Sidebar } from './Sidebar'; import { InputBar } from './InputBar'; import { GenerationCard } from './GenerationCard'; import { VideoDetailModal } from './VideoDetailModal'; import { AnnouncementBanner } from './AnnouncementBanner'; import { useGenerationStore } from '../store/generation'; import { useAuthStore } from '../store/auth'; import type { GenerationTask } from '../types'; import styles from './VideoGenerationPage.module.css'; export function VideoGenerationPage() { const tasks = useGenerationStore((s) => s.tasks); const loadTasks = useGenerationStore((s) => s.loadTasks); const loadMore = useGenerationStore((s) => s.loadMore); const isLoadingMore = useGenerationStore((s) => s.isLoadingMore); const teamDisabled = useAuthStore((s) => s.teamDisabled); const reEdit = useGenerationStore((s) => s.reEdit); const regenerate = useGenerationStore((s) => s.regenerate); const removeTask = useGenerationStore((s) => s.removeTask); const scrollRef = useRef(null); const prevCountRef = useRef(tasks.length); const initialLoadRef = useRef(true); const savedScrollTop = useGenerationStore((s) => s.savedScrollTop); const saveScrollPosition = useGenerationStore((s) => s.saveScrollPosition); const [detailTask, setDetailTask] = useState(null); // Load tasks from backend on mount (persist across page refresh) useEffect(() => { loadTasks(); }, [loadTasks]); // Restore scroll position after initial load, or scroll to bottom for new tasks useEffect(() => { if (tasks.length === 0) return; if (initialLoadRef.current) { initialLoadRef.current = false; // Use requestAnimationFrame to ensure DOM has rendered requestAnimationFrame(() => { if (savedScrollTop !== null && scrollRef.current) { scrollRef.current.scrollTop = savedScrollTop; } else if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }); prevCountRef.current = tasks.length; return; } if (tasks.length > prevCountRef.current && scrollRef.current) { scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }); } prevCountRef.current = tasks.length; }, [tasks.length, savedScrollTop]); // Save scroll position + auto-load older tasks when scrolled near top const handleScroll = useCallback(() => { if (!scrollRef.current) return; saveScrollPosition(scrollRef.current.scrollTop); // Trigger loadMore when scrolled within 100px of the top if (scrollRef.current.scrollTop < 100) { const el = scrollRef.current; const prevHeight = el.scrollHeight; loadMore().then(() => { // After older tasks are prepended, restore visual position so user doesn't jump requestAnimationFrame(() => { const diff = el.scrollHeight - prevHeight; if (diff > 0) el.scrollTop += diff; }); }); } }, [saveScrollPosition, loadMore]); const handleReEdit = (id: string) => { reEdit(id); setDetailTask(null); }; const handleRegenerate = (id: string) => { regenerate(id); setDetailTask(null); }; const handleDelete = (id: string) => { removeTask(id); setDetailTask(null); }; const completedTasks = useMemo( () => tasks.filter((t) => t.status === 'completed' && t.resultUrl), [tasks], ); const detailIdx = detailTask ? completedTasks.findIndex((t) => t.id === detailTask.id) : -1; if (teamDisabled) { return (

您的团队已被停用

请联系管理员

); } return (
{tasks.length === 0 ? (

在下方输入提示词,开始创作 AI 视频

) : (
{isLoadingMore && (
加载中…
)} {tasks.map((task) => ( ))}
)}
setDetailTask(null)} onReEdit={handleReEdit} onRegenerate={handleRegenerate} onDelete={handleDelete} hasPrev={detailIdx > 0} hasNext={detailIdx >= 0 && detailIdx < completedTasks.length - 1} onPrev={() => detailIdx > 0 && setDetailTask(completedTasks[detailIdx - 1])} onNext={() => detailIdx < completedTasks.length - 1 && setDetailTask(completedTasks[detailIdx + 1])} />
); }