视频首帧、下载loading等

This commit is contained in:
北枳 2025-09-04 22:20:13 +08:00
parent eee3026699
commit 08ca156174
7 changed files with 51 additions and 41 deletions

View File

@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useRef, useCallback } from 'react';
import { Loader2, Check, Pencil, Trash, Download } from 'lucide-react';
import { Loader2, Download } from 'lucide-react';
import { useRouter } from 'next/navigation';
import './style/create-to-video2.css';
@ -28,6 +28,7 @@ export default function CreateToVideo2() {
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);
@ -193,23 +194,24 @@ export default function CreateToVideo2() {
<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"
onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}
onMouseEnter={() => handleMouseEnter(project.project_id)}
onMouseLeave={() => handleMouseLeave(project.project_id)}
data-alt="project-card"
>
{/* 视频/图片区域 */}
<div className="relative aspect-video">
{(project.final_video_url || project.final_simple_video_url) ? (
<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}
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={`${project.final_video_url || project.final_simple_video_url}?vframe/jpg/offset/1`}
poster={
project.final_video_url || project.final_simple_video_url ? `${project.final_video_url || project.final_simple_video_url}?vframe/jpg/offset/1` : `${project.video_urls}?x-oss-process=video/snapshot,t_1000,f_jpg`
}
/>
) : (
<div
@ -224,9 +226,14 @@ export default function CreateToVideo2() {
{(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" 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={() => {
downloadVideo(project.final_video_url || project.final_simple_video_url);
}}><Download className="w-4 h-4 text-white" /></Button>
<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>
)}

View File

@ -50,18 +50,8 @@ const WorkFlow = React.memo(function WorkFlow() {
aiEditingButtonRef.current?.handleAIEditing();
editingNotificationKey.current = `editing-${Date.now()}`;
showEditingNotification({
title: '视频编辑中', // 可选
description: 'AI正在为您编辑视频...', // 可选
key: editingNotificationKey.current,
onComplete: () => {
console.log('编辑完成');
// 3秒后关闭通知
setTimeout(() => {
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, 3000);
},
onFail: () => {
console.log('编辑失败');
// 清缓存 生成计划 视频重新分析
@ -115,7 +105,16 @@ const WorkFlow = React.memo(function WorkFlow() {
// 更新通知状态为完成
showEditingNotification({
isCompleted: true,
key: editingNotificationKey.current
key: editingNotificationKey.current,
onComplete: () => {
console.log('编辑完成');
// 3秒后关闭通知
setTimeout(() => {
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, 3000);
},
});
}
}, [taskObject.final, isHandleEdit]);

View File

@ -489,6 +489,7 @@ export const AIEditingIframeButton = React.forwardRef<
// 暴露 handleAIEditing 方法
React.useImperativeHandle(ref, () => ({
handleAIEditing: async () => {
console.log('handleAIEditing', iframeRef.current);
iframeRef.current?.startAIEditing();
}
}));

View File

@ -34,8 +34,6 @@ const descriptionStyle = {
interface EditingNotificationProps {
/** 编辑是否完成 */
isCompleted?: boolean;
/** 自定义标题 */
title?: string;
/** 自定义描述 */
description?: string;
/** key */
@ -53,7 +51,6 @@ interface EditingNotificationProps {
export const showEditingNotification = (props: EditingNotificationProps) => {
const {
isCompleted = false,
title = 'AI Video Editing',
description = 'Your video is being edited by AI...',
key,
onComplete,
@ -125,15 +122,14 @@ export const showEditingNotification = (props: EditingNotificationProps) => {
return (
<div data-alt="editing-notification" style={{ minWidth: '300px' }}>
<h3 style={messageStyle}>
{scissorsIcon}
{title}
</h3>
<p style={{
<h3 style={{
...descriptionStyle,
color: status === 'exception' ? '#ff4d4f' :
'rgba(255, 255, 255, 0.65)',
}}>{currentDescription}</p>
}}>
{scissorsIcon}
{currentDescription}
</h3>
<Progress
percent={Math.round(progress)}
status={status}

View File

@ -64,6 +64,7 @@ export const MediaViewer = React.memo(function MediaViewer({
const [finalVideoReady, setFinalVideoReady] = useState(false);
const [userHasInteracted, setUserHasInteracted] = useState(false);
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
useEffect(() => {
if (isSmartChatBoxOpen) {
@ -365,8 +366,10 @@ export const MediaViewer = React.memo(function MediaViewer({
</Tooltip>
{/* 下载按钮 */}
<Tooltip placement="top" title="Download video">
<GlassIconButton icon={Download} size='sm' onClick={() => {
downloadVideo(taskObject.final.url);
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
setIsLoadingDownloadBtn(true);
await downloadVideo(taskObject.final.url);
setIsLoadingDownloadBtn(false);
}} />
</Tooltip>
{showGotoCutButton && (
@ -492,10 +495,12 @@ export const MediaViewer = React.memo(function MediaViewer({
</Tooltip>
{/* 下载按钮 */}
<Tooltip placement="top" title="Download video">
<GlassIconButton icon={Download} size='sm' onClick={() => {
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
const currentVideo = taskObject.videos.data[currentSketchIndex];
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0) {
downloadVideo(currentVideo.urls[0]);
setIsLoadingDownloadBtn(true);
await downloadVideo(currentVideo.urls[0]);
setIsLoadingDownloadBtn(false);
}
}} />
</Tooltip>

View File

@ -648,7 +648,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps =
fallbackToStep,
originalText: state.originalText,
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
showGotoCutButton: canGoToCut && isGenerateEditPlan ? true : false,
showGotoCutButton: canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') ? true : false,
generateEditPlan: openEditPlan,
handleRetryVideo
};

View File

@ -2,7 +2,7 @@
import React, { forwardRef } from 'react';
import { motion } from 'framer-motion';
import { LucideIcon } from 'lucide-react';
import { LucideIcon, Loader2 } from 'lucide-react';
import { cn } from '@/public/lib/utils';
// Define props without the ref
@ -14,6 +14,7 @@ interface GlassIconButtonProps {
className?: string;
[key: string]: any; // To allow spreading other props
text?: string;
loading?: boolean;
}
const variantStyles = {
@ -38,12 +39,12 @@ const iconSizes = {
const MotionButton = motion.button;
export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProps>(
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, text, ...props }, ref) => {
({ icon: Icon, tooltip, variant = 'secondary', size = 'md', className, text, loading = false, ...props }, ref) => {
return (
<MotionButton
ref={ref}
className={cn(
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border flex items-center gap-2',
'relative rounded-full backdrop-blur-md transition-colors shadow-lg border flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed',
variantStyles[variant],
sizeStyles[size],
className
@ -59,12 +60,13 @@ export const GlassIconButton = forwardRef<HTMLButtonElement, GlassIconButtonProp
perspective: '1000px'
}}
{...props}
disabled={loading}
>
<Icon className={cn('text-white', iconSizes[size])} />
{text && (
{loading ? <Loader2 className={cn('text-white', iconSizes[size])} /> : <Icon className={cn('text-white', iconSizes[size])} />}
{text && !loading && (
<span className="text-white text-xs">{text}</span>
)}
{tooltip && (
{tooltip && !loading && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs
bg-black/80 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity
pointer-events-none whitespace-nowrap">