同步主界面数据到编辑界面

This commit is contained in:
北枳 2025-08-21 20:55:15 +08:00
parent 2c88b8201b
commit 0ef11c4e89
8 changed files with 367 additions and 210 deletions

View File

@ -621,7 +621,7 @@ export interface RoleResponse {
} }
interface Role { export interface Role {
name: string; name: string;
url: string; url: string;
status: number; status: number;
@ -636,7 +636,7 @@ interface ShotSketch {
script: string; script: string;
status: number; status: number;
} }
interface Video { export interface ShotVideo {
video_id: string; video_id: string;
urls: string[]; urls: string[];
video_status: number; video_status: number;
@ -663,7 +663,7 @@ export interface TaskObject {
total_count: number; total_count: number;
}; // 分镜草图 }; // 分镜草图
videos: { videos: {
data: Video[]; data: ShotVideo[];
total_count: number; total_count: number;
}; // 视频 }; // 视频
final: { final: {

View File

@ -179,6 +179,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
return role.fromDraft return role.fromDraft
}); });
console.log('newDraftRoleList', newDraftRoleList) console.log('newDraftRoleList', newDraftRoleList)
console.log('应用角色到分镜', shotSelectionList)
// 循环调用接口,为每个选中的分镜单独调用 // 循环调用接口,为每个选中的分镜单独调用
const res = await Promise.all( shotSelectionList.map(async (shot) => { const res = await Promise.all( shotSelectionList.map(async (shot) => {
// 调用应用角色到分镜接口(不等待完成) // 调用应用角色到分镜接口(不等待完成)
@ -191,6 +192,8 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
}) })
})) }))
console.log('应用角色到分镜', res);
SaveEditUseCase.setVideoTasks([ SaveEditUseCase.setVideoTasks([
...SaveEditUseCase.videoTasks, ...SaveEditUseCase.videoTasks,
...res.map(item=>{ ...res.map(item=>{

View File

@ -228,6 +228,7 @@ const WorkFlow = React.memo(function WorkFlow() {
SaveEditUseCase.clearData(); SaveEditUseCase.clearData();
setIsEditModalOpen(false) setIsEditModalOpen(false)
}} }}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex} currentSketchIndex={currentSketchIndex}
roles={taskObject.roles.data} roles={taskObject.roles.data}
setIsPauseWorkFlow={setIsPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}

View File

@ -5,13 +5,14 @@ import { useSearchParams } from 'next/navigation';
import { useRoleServiceHook } from "@/app/service/Interaction/RoleService"; import { useRoleServiceHook } from "@/app/service/Interaction/RoleService";
import { useRoleShotServiceHook } from "@/app/service/Interaction/RoleShotService"; import { useRoleShotServiceHook } from "@/app/service/Interaction/RoleShotService";
import { useScriptService } from "@/app/service/Interaction/ScriptService"; import { useScriptService } from "@/app/service/Interaction/ScriptService";
import { VideoSegmentEntity } from "@/app/service/domain/Entities";
export const useEditData = (tabType: string, originalText?: string) => { export const useEditData = (tabType: string, originalText?: string) => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const projectId = searchParams.get('episodeId') || ''; const projectId = searchParams.get('episodeId') || '';
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [scriptData, setScriptData] = useState<any[]>([]); const [scriptData, setScriptData] = useState<any[]>([]);
const [shotData, setShotData] = useState<any[]>([]); const [shotData, setShotData] = useState<VideoSegmentEntity[]>([]);
const [roleData, setRoleData] = useState<any[]>([]); const [roleData, setRoleData] = useState<any[]>([]);
@ -24,6 +25,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
const { const {
videoSegments, videoSegments,
selectedSegment,
scriptRoles, scriptRoles,
getVideoSegmentList, getVideoSegmentList,
setSelectedSegment, setSelectedSegment,
@ -123,6 +125,7 @@ export const useEditData = (tabType: string, originalText?: string) => {
applyScript, applyScript,
// shot // shot
shotData, shotData,
selectedSegment,
scriptRoles, scriptRoles,
setSelectedSegment, setSelectedSegment,
regenerateVideoSegment, regenerateVideoSegment,

View File

@ -11,30 +11,10 @@ import HorizontalScroller from './HorizontalScroller';
import { useEditData } from '@/components/pages/work-flow/use-edit-data'; import { useEditData } from '@/components/pages/work-flow/use-edit-data';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { RoleEntity } from '@/app/service/domain/Entities'; import { RoleEntity } from '@/app/service/domain/Entities';
import { Role } from '@/api/DTO/movieEdit';
interface Appearance {
hairStyle: string;
skinTone: string;
facialFeatures: string;
bodyType: string;
}
interface Role {
name: string;
url: string;
sound: string;
soundDescription: string;
roleDescription: string;
age: number;
gender: 'male' | 'female' | 'other';
ethnicity: string;
appearance: Appearance;
// 新增标签数组
tags: string[];
}
interface CharacterTabContentProps { interface CharacterTabContentProps {
originalRoles: Role[];
onClose: () => void; onClose: () => void;
onApply: () => void; onApply: () => void;
setActiveTab: (tabId: string) => void; setActiveTab: (tabId: string) => void;
@ -42,10 +22,10 @@ interface CharacterTabContentProps {
export const CharacterTabContent = forwardRef< export const CharacterTabContent = forwardRef<
{ switchBefore: (tabId: string) => boolean, saveBefore: () => void }, { switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void },
CharacterTabContentProps CharacterTabContentProps
>((props, ref) => { >((props, ref) => {
const { onClose, onApply, setActiveTab } = props; const { onClose, onApply, setActiveTab, originalRoles } = props;
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
const [replacePanelKey, setReplacePanelKey] = useState(0); const [replacePanelKey, setReplacePanelKey] = useState(0);
const [ignoreReplace, setIgnoreReplace] = useState(false); const [ignoreReplace, setIgnoreReplace] = useState(false);
@ -76,7 +56,6 @@ CharacterTabContentProps
regenerateRole, regenerateRole,
fetchUserRoleLibrary, fetchUserRoleLibrary,
uploadImageAndUpdateRole, uploadImageAndUpdateRole,
changeTabCallback,
// role shot // role shot
shotSelectionList, shotSelectionList,
fetchRoleShots, fetchRoleShots,
@ -91,7 +70,8 @@ CharacterTabContentProps
switchBefore: (tabId: string) => { switchBefore: (tabId: string) => {
setNextToTabId(tabId); setNextToTabId(tabId);
// 判断 角色是否修改 // 判断 角色是否修改
const isChange = selectedRole!.isChangeRole const currentIndex = getCurrentIndex();
const isChange = currentIndex !== -1 && isRoleChange(originalRoles[currentIndex]);
console.log('switchBefore', isChange); console.log('switchBefore', isChange);
if (isChange) { if (isChange) {
setTriggerType('tab'); setTriggerType('tab');
@ -99,15 +79,16 @@ CharacterTabContentProps
} }
return isChange; return isChange;
}, },
saveBefore: () => { saveOrCloseBefore: () => {
console.log('saveBefore'); console.log('saveOrCloseBefore');
// 判断 角色是否修改 // 判断 角色是否修改
changeTabCallback((isChange: Boolean) => { const currentIndex = getCurrentIndex();
if (isChange) { if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) {
setTriggerType('apply'); setTriggerType('apply');
handleStartReplaceCharacter(); handleStartReplaceCharacter();
} } else {
}); onClose();
}
} }
})); }));
@ -179,24 +160,33 @@ CharacterTabContentProps
setIsReplacePanelOpen(false); setIsReplacePanelOpen(false);
}; };
// 对比角色是否修改
const isRoleChange = (role: Role) => {
console.log('对比角色是否修改', role, selectedRole);
return role.name !== selectedRole?.name || role.url !== selectedRole?.imageUrl;
};
// 获取当前选中下标
const getCurrentIndex = () => {
return originalRoles.findIndex(role => role.name === selectedRole?.name);
};
const handleChangeRole = (index: number) => { const handleChangeRole = (index: number) => {
const oldRole = roleData.find(role => role.id === selectedRole?.id);
console.log('切换角色前对比'); console.log('切换角色前对比');
changeTabCallback((isChange: Boolean) => { const currentIndex = getCurrentIndex();
if (isChange) { if (currentIndex === index) return;
setTriggerType('user'); if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) {
setIsRemindReplacePanelOpen(true); setTriggerType('user');
setNextToUserIndex(index); setIsRemindReplacePanelOpen(true);
return; setNextToUserIndex(index);
} return;
}
// 重置替换规则 // 重置替换规则
setEnableAnimation(false); setEnableAnimation(false);
setIgnoreReplace(false); setIgnoreReplace(false);
setIsRegenerate(false); setIsRegenerate(false);
selectRole(roleData[index]); selectRole(roleData[index]);
});
}; };
// 从角色库中选择角色 // 从角色库中选择角色
@ -266,18 +256,8 @@ CharacterTabContentProps
}); });
}; };
// 如果loading 显示loading状态
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
<p>Loading...</p>
</div>
);
}
// 如果没有角色数据,显示占位内容 // 如果没有角色数据,显示占位内容
if (roleData.length === 0) { if (originalRoles.length === 0) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50"> <div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<Users className="w-16 h-16 mb-4" /> <Users className="w-16 h-16 mb-4" />
@ -306,22 +286,22 @@ CharacterTabContentProps
<HorizontalScroller <HorizontalScroller
itemWidth={96} itemWidth={96}
gap={0} gap={0}
selectedIndex={roleData.findIndex(role => role.id === selectedRole?.id)} selectedIndex={originalRoles?.findIndex(role => role.name === selectedRole?.name)}
onItemClick={(i: number) => handleChangeRole(i)} onItemClick={(i: number) => handleChangeRole(i)}
> >
{roleData.map((role, index) => ( {originalRoles.map((role, index) => (
<motion.div <motion.div
key={`role-${index}`} key={`role-${index}`}
className={cn( className={cn(
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer', 'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
'aspect-[9/16]', 'aspect-[9/16]',
role.id === selectedRole?.id ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50' role.name === selectedRole?.name ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)} )}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
<img <img
src={role.imageUrl} src={role.url}
alt={role.name} alt={role.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@ -335,78 +315,83 @@ CharacterTabContentProps
</motion.div> </motion.div>
{/* 下部分:角色详情 */} {/* 下部分:角色详情 */}
<motion.div { loading ? (
className="grid grid-cols-2 gap-6" <div className="flex flex-col items-center justify-center min-h-[300px] text-white/50">
initial={{ opacity: 0, y: 20 }} <Loader2 className="w-12 h-12 mb-4 animate-spin" />
animate={{ opacity: 1, y: 0 }} <p>Loading...</p>
transition={{ delay: 0.2 }} </div>
> ) : (
<motion.div
className="grid grid-cols-2 gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{/* 左列:角色预览 */} {/* 左列:角色预览 */}
<div className="space-y-4"> <div className="space-y-4">
{/* 角色预览图 */} {/* 角色预览图 */}
<div className="w-full h-full mx-auto rounded-lg relative group"> <div className="w-full h-full mx-auto rounded-lg relative group">
<ImageBlurTransition <ImageBlurTransition
src={selectedRole?.imageUrl || ''} src={selectedRole?.imageUrl || ''}
alt={selectedRole?.name || ''} alt={selectedRole?.name || ''}
width='100%' width='100%'
height='100%' height='100%'
enableAnimation={enableAnimation} enableAnimation={enableAnimation}
/>
{/* 应用角色按钮 */}
<div className='absolute top-3 right-3 flex gap-2'>
<motion.button
className="p-2 bg-black/50 hover:bg-black/70
text-white rounded-full backdrop-blur-sm transition-colors z-10"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleUploadClick}
disabled={isUploading}
>
{isUploading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageUp className="w-4 h-4" />}
</motion.button>
<motion.button
className="p-2 bg-black/50 hover:bg-black/70
text-white rounded-full backdrop-blur-sm transition-colors z-10"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleOpenReplaceLibrary()}
>
<Library className="w-4 h-4" />
</motion.button>
</div>
</div>
</div>
{/* 右列:角色信息 */}
<div className="space-y-4">
<CharacterEditor
ref={characterEditorRef}
className="min-h-[calc(100%-4rem)]"
description={selectedRole?.generateText || ''}
highlight={selectedRole?.tags || []}
onSmartPolish={handleSmartPolish}
onUpdateText={(text: string) => updateRoleText(text)}
/> />
{/* 应用角色按钮 */} {/* 重新生成按钮、替换形象按钮 */}
<div className='absolute top-3 right-3 flex gap-2'> <div className="grid grid-cols-1 gap-2">
<motion.button <motion.button
className="p-2 bg-black/50 hover:bg-black/70 onClick={() => handleRegenerate()}
text-white rounded-full backdrop-blur-sm transition-colors z-10" className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
whileHover={{ scale: 1.05 }} text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
whileTap={{ scale: 0.95 }} whileHover={{ scale: 1.02 }}
onClick={handleUploadClick} whileTap={{ scale: 0.98 }}
disabled={isUploading} disabled={isRegenerate}
> >
{isUploading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageUp className="w-4 h-4" />} <RefreshCw className="w-4 h-4" />
</motion.button> <span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
<motion.button
className="p-2 bg-black/50 hover:bg-black/70
text-white rounded-full backdrop-blur-sm transition-colors z-10"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleOpenReplaceLibrary()}
>
<Library className="w-4 h-4" />
</motion.button> </motion.button>
</div> </div>
</div> </div>
</motion.div>
</div> )}
{/* 右列:角色信息 */}
<div className="space-y-4">
<CharacterEditor
ref={characterEditorRef}
className="min-h-[calc(100%-4rem)]"
description={selectedRole?.generateText || ''}
highlight={selectedRole?.tags || []}
onSmartPolish={handleSmartPolish}
onUpdateText={(text: string) => updateRoleText(text)}
/>
{/* 重新生成按钮、替换形象按钮 */}
<div className="grid grid-cols-1 gap-2">
<motion.button
onClick={() => handleRegenerate()}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
disabled={isRegenerate}
>
<RefreshCw className="w-4 h-4" />
<span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
</motion.button>
</div>
</div>
</motion.div>
<FloatingGlassPanel <FloatingGlassPanel
open={isReplacePanelOpen} open={isReplacePanelOpen}

View File

@ -12,6 +12,7 @@ import { CharacterTabContent } from './character-tab-content';
import { MusicTabContent } from './music-tab-content'; import { MusicTabContent } from './music-tab-content';
import FloatingGlassPanel from './FloatingGlassPanel'; import FloatingGlassPanel from './FloatingGlassPanel';
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase'; import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
import { TaskObject } from '@/api/DTO/movieEdit';
interface EditModalProps { interface EditModalProps {
isOpen: boolean; isOpen: boolean;
@ -23,6 +24,7 @@ interface EditModalProps {
isPauseWorkFlow: boolean; isPauseWorkFlow: boolean;
fallbackToStep: any; fallbackToStep: any;
originalText?: string; originalText?: string;
taskObject?: TaskObject;
} }
const tabs = [ const tabs = [
@ -44,7 +46,8 @@ export function EditModal({
setIsPauseWorkFlow, setIsPauseWorkFlow,
isPauseWorkFlow, isPauseWorkFlow,
fallbackToStep, fallbackToStep,
originalText originalText,
taskObject
}: EditModalProps) { }: EditModalProps) {
const [activeTab, setActiveTab] = useState(activeEditTab); const [activeTab, setActiveTab] = useState(activeEditTab);
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex); const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
@ -55,6 +58,7 @@ export function EditModal({
const [remindFallbackText, setRemindFallbackText] = useState('The task will be regenerated and edited. Do you want to continue?'); const [remindFallbackText, setRemindFallbackText] = useState('The task will be regenerated and edited. Do you want to continue?');
const scriptTabContentRef = useRef<any>(null); const scriptTabContentRef = useRef<any>(null);
const characterTabContentRef = useRef<any>(null); const characterTabContentRef = useRef<any>(null);
const shotTabContentRef = useRef<any>(null);
// 添加一个状态来标记是否是从切换tab触发的提醒 // 添加一个状态来标记是否是从切换tab触发的提醒
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null); const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
const [disabledBtn, setDisabledBtn] = useState(false); const [disabledBtn, setDisabledBtn] = useState(false);
@ -72,9 +76,9 @@ export function EditModal({
const isTabDisabled = (tabId: string) => { const isTabDisabled = (tabId: string) => {
if (tabId === 'settings') return false; if (tabId === 'settings') return false;
// 换成 如果对应标签下 数据存在 就不禁用 // 换成 如果对应标签下 数据存在 就不禁用
// if (tabId === '1') return roles.length === 0; if (tabId === '1') return taskObject?.roles.data.length === 0;
// if (tabId === '2') return taskScenes.length === 0; // if (tabId === '2') return taskScenes.length === 0;
// if (tabId === '3') return sketchVideo.length === 0; if (tabId === '3') return taskObject?.videos.data.length === 0;
if (tabId === '4') return false; if (tabId === '4') return false;
return false; return false;
}; };
@ -98,6 +102,11 @@ export function EditModal({
if (characterTabContent) { if (characterTabContent) {
return characterTabContent.switchBefore(tabId); return characterTabContent.switchBefore(tabId);
} }
} else if (activeTab === '3') {
const shotTabContent = shotTabContentRef.current;
if (shotTabContent) {
return shotTabContent.switchBefore(tabId);
}
} }
return false; return false;
} }
@ -117,11 +126,11 @@ export function EditModal({
console.log('handleSave'); console.log('handleSave');
// setIsRemindFallbackOpen(true); // setIsRemindFallbackOpen(true);
if (activeTab === '0') { if (activeTab === '0') {
scriptTabContentRef.current.saveBefore(); scriptTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '1') { } else if (activeTab === '1') {
characterTabContentRef.current.saveBefore(); characterTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '3') { } else if (activeTab === '3') {
handleConfirmGotoFallback(); shotTabContentRef.current.saveOrCloseBefore('apply');
} }
} }
@ -170,7 +179,13 @@ export function EditModal({
const handleClickClose = () => { const handleClickClose = () => {
// TODO 关闭前 检查 当前tab 下是否有更新 如果有更新 则提醒用户 是否确认应用 // TODO 关闭前 检查 当前tab 下是否有更新 如果有更新 则提醒用户 是否确认应用
// 暂时 默认弹出提醒 // 暂时 默认弹出提醒
setIsRemindCloseOpen(true); if (activeTab === '0') {
scriptTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '1') {
characterTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '3') {
shotTabContentRef.current.saveOrCloseBefore('close');
}
} }
const handleConfirmApply = () => { const handleConfirmApply = () => {
@ -195,6 +210,7 @@ export function EditModal({
originalText={originalText} originalText={originalText}
onApply={handleApply} onApply={handleApply}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
onClose={onClose}
/> />
); );
case '1': case '1':
@ -204,6 +220,7 @@ export function EditModal({
onClose={onClose} onClose={onClose}
onApply={handleApply} onApply={handleApply}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
originalRoles={taskObject?.roles.data || []}
/> />
); );
case '2': case '2':
@ -215,9 +232,12 @@ export function EditModal({
case '3': case '3':
return ( return (
<ShotTabContent <ShotTabContent
ref={shotTabContentRef}
originalVideos={taskObject?.videos.data || []}
currentSketchIndex={currentIndex} currentSketchIndex={currentIndex}
roles={roles}
onApply={handleApply} onApply={handleApply}
onClose={onClose}
setActiveTab={setActiveTab}
/> />
); );
case '4': case '4':

View File

@ -12,13 +12,14 @@ interface ScriptTabContentProps {
originalText?: string; originalText?: string;
onApply: () => void; onApply: () => void;
setActiveTab: (tabId: string) => void; setActiveTab: (tabId: string) => void;
onClose: () => void;
} }
export const ScriptTabContent = forwardRef< export const ScriptTabContent = forwardRef<
{ switchBefore: (tabId: string) => boolean, saveBefore: () => void }, { switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void },
ScriptTabContentProps ScriptTabContentProps
>((props, ref) => { >((props, ref) => {
const { setIsPauseWorkFlow, isPauseWorkFlow, originalText, onApply, setActiveTab } = props; const { setIsPauseWorkFlow, isPauseWorkFlow, originalText, onApply, setActiveTab, onClose } = props;
const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText); const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText);
const [isUpdate, setIsUpdate] = useState(false); const [isUpdate, setIsUpdate] = useState(false);
@ -39,10 +40,12 @@ export const ScriptTabContent = forwardRef<
} }
return isUpdate; return isUpdate;
}, },
saveBefore: () => { saveOrCloseBefore: () => {
console.log('saveBefore'); console.log('saveOrCloseBefore');
if (isUpdate) { if (isUpdate) {
onApply(); onApply();
} else {
onClose();
} }
} }
})); }));

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useRef, useEffect, useState } from 'react'; import React, { useRef, useEffect, useState, forwardRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { RefreshCw, User, Loader2, X, Plus, Video, CircleX } from 'lucide-react'; import { RefreshCw, User, Loader2, X, Plus, Video, CircleX } from 'lucide-react';
import { cn } from '@/public/lib/utils'; import { cn } from '@/public/lib/utils';
@ -11,19 +11,26 @@ import FloatingGlassPanel from './FloatingGlassPanel';
import { ReplaceCharacterPanel } from './replace-character-panel'; import { ReplaceCharacterPanel } from './replace-character-panel';
import HorizontalScroller from './HorizontalScroller'; import HorizontalScroller from './HorizontalScroller';
import { useEditData } from '@/components/pages/work-flow/use-edit-data'; import { useEditData } from '@/components/pages/work-flow/use-edit-data';
import { RoleEntity } from '@/app/service/domain/Entities'; import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities';
import { ShotVideo } from '@/api/DTO/movieEdit';
interface ShotTabContentProps { interface ShotTabContentProps {
currentSketchIndex: number; currentSketchIndex: number;
roles?: any[]; originalVideos: ShotVideo[];
onApply: () => void; onApply: () => void;
onClose: () => void;
setActiveTab: (tabId: string) => void;
} }
export const ShotTabContent = (props: ShotTabContentProps) => { export const ShotTabContent = forwardRef<
const { currentSketchIndex = 0, roles = [], onApply } = props; { switchBefore: (tabId: string) => boolean, saveOrCloseBefore: (type: 'apply' | 'close') => void },
ShotTabContentProps
>((props, ref) => {
const { currentSketchIndex = 0, onApply, onClose, originalVideos, setActiveTab } = props;
const { const {
loading, loading,
shotData, shotData,
selectedSegment,
scriptRoles, scriptRoles,
setSelectedSegment, setSelectedSegment,
regenerateVideoSegment, regenerateVideoSegment,
@ -36,8 +43,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
calculateRecognitionBoxes, calculateRecognitionBoxes,
setSelectedRole setSelectedRole
} = useEditData('shot'); } = useEditData('shot');
const [selectedIndex, setSelectedIndex] = useState(currentSketchIndex);
const [detections, setDetections] = useState<PersonDetection[]>([]); const [detections, setDetections] = useState<PersonDetection[]>([]);
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected' | 'failed' | 'timeout'>('idle'); const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected' | 'failed' | 'timeout'>('idle');
const [isScanFailed, setIsScanFailed] = useState(false); const [isScanFailed, setIsScanFailed] = useState(false);
@ -51,13 +56,54 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
const [isRegenerate, setIsRegenerate] = useState(false); const [isRegenerate, setIsRegenerate] = useState(false);
const [pendingRegeneration, setPendingRegeneration] = useState(false); const [pendingRegeneration, setPendingRegeneration] = useState(false);
const [isInitialized, setIsInitialized] = useState(true);
const [triggerType, setTriggerType] = useState<'tab' | 'apply' | 'close'>('tab');
const [nextToTabId, setNextToTabId] = useState<string>('');
const [isRemindApplyUpdate, setIsRemindApplyUpdate] = useState(false);
const [updateData, setUpdateData] = useState<VideoSegmentEntity[]>([]);
useEffect(() => { useEffect(() => {
console.log('shotTabContent-----shotData', shotData); console.log('shotTabContent-----shotData', shotData);
}, [shotData]); }, [shotData]);
useEffect(() => {
console.log('-==========shotData===========-', shotData);
// 只在初始化且有角色数据时执行
if (isInitialized && shotData.length > 0) {
setIsInitialized(false);
setSelectedSegment(shotData[0]);
}
}, [shotData, isInitialized]);
// 暴露方法给父组件
React.useImperativeHandle(ref, () => ({
switchBefore: (tabId: string) => {
setNextToTabId(tabId);
// 判断 是否修改数据
const isChange = handleGetUpdateData().length > 0;
console.log('switchBefore', isChange);
if (isChange) {
setTriggerType('tab');
setIsRemindApplyUpdate(true);
}
return isChange;
},
saveOrCloseBefore: (type: 'apply' | 'close') => {
console.log('saveOrCloseBefore');
// 判断 是否修改数据
const isChange = handleGetUpdateData().length > 0;
if (isChange) {
setTriggerType(type);
setIsRemindApplyUpdate(true);
} else {
onClose();
}
}
}));
useEffect(() => { useEffect(() => {
if (pendingRegeneration) { if (pendingRegeneration) {
console.log('pendingRegeneration', pendingRegeneration, shotData[selectedIndex]?.lens); console.log('pendingRegeneration', pendingRegeneration, selectedSegment?.lens);
regenerateVideoSegment().then(() => { regenerateVideoSegment().then(() => {
setPendingRegeneration(false); setPendingRegeneration(false);
setIsRegenerate(false); setIsRegenerate(false);
@ -65,16 +111,35 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
} }
}, [pendingRegeneration]); }, [pendingRegeneration]);
// 监听当前选中index变化 // 监听当前选中segment变化
useEffect(() => { useEffect(() => {
console.log('shotTabContent-----shotData', shotData); console.log('shotTabContent-----shotData', shotData);
if (shotData.length > 0) { if (shotData.length > 0 && !selectedSegment) {
// 清空检测状态 和 检测结果 // 清空检测状态 和 检测结果
setScanState('idle'); setScanState('idle');
setDetections([]); setDetections([]);
setSelectedSegment(shotData[selectedIndex]); setSelectedSegment(shotData[0]);
} }
}, [selectedIndex, shotData]); }, [shotData, selectedSegment]);
// 获取修改的数据
const handleGetUpdateData = () => {
console.log('handleGetUpdateData', shotData, originalVideos);
const updateData: VideoSegmentEntity[] = [];
shotData.forEach((shot, index) => {
const a = shot.videoUrl.map((url) => url.video_url).join(',');
const b = originalVideos[index].urls.join(',');
if (a !== b) {
updateData.push({
...shot,
name: 'Segment ' + (index + 1)
});
}
});
console.log('updateData', updateData);
setUpdateData(updateData);
return updateData;
}
// 处理扫描开始 // 处理扫描开始
const handleScan = async () => { const handleScan = async () => {
@ -174,16 +239,34 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
onApply(); onApply();
}; };
// 应用修改
const handleApplyUpdate = () => {
console.log('apply update');
onApply();
}
// 忽略修改
const handleIgnoreUpdate = () => {
console.log('ignore update');
if (triggerType === 'apply') {
onClose();
} else if (triggerType === 'tab') {
setActiveTab(nextToTabId);
}
}
// 点击按钮重新生成 // 点击按钮重新生成
const handleRegenerate = async () => { const handleRegenerate = async () => {
console.log('regenerate'); console.log('regenerate');
setIsRegenerate(true); setIsRegenerate(true);
const shotInfo = shotsEditorRef.current.getShotInfo(); const shotInfo = shotsEditorRef.current.getShotInfo();
console.log('shotInfo', shotInfo); console.log('shotInfo', shotInfo);
setSelectedSegment({ if (selectedSegment) {
...shotData[selectedIndex], setSelectedSegment({
lens: shotInfo ...selectedSegment,
}); lens: shotInfo
});
}
setTimeout(() => { setTimeout(() => {
setPendingRegeneration(true); setPendingRegeneration(true);
}, 1000); }, 1000);
@ -197,22 +280,20 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
// 切换选择分镜 // 切换选择分镜
const handleSelectShot = (index: number) => { const handleSelectShot = (index: number) => {
// 切换前 判断数据是否发生变化 // 通过 video_id 找到对应的分镜
setSelectedIndex(index); const selectedVideo = originalVideos[index];
const targetSegment = shotData.find(shot =>
shot.videoUrl.some(url => url.video_id === selectedVideo.video_id)
);
if (targetSegment) {
setSelectedSegment(targetSegment);
}
}; };
// 如果loading 显示loading状态
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
<p>Loading...</p>
</div>
);
}
// 如果没有数据,显示空状态 // 如果没有数据,显示空状态
if (shotData.length === 0) { if (originalVideos.length === 0) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50"> <div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<Video className="w-16 h-16 mb-4" /> <Video className="w-16 h-16 mb-4" />
@ -233,28 +314,23 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
<HorizontalScroller <HorizontalScroller
itemWidth={128} itemWidth={128}
gap={0} gap={0}
selectedIndex={selectedIndex} selectedIndex={originalVideos.findIndex(shot => selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id))}
onItemClick={(i: number) => handleSelectShot(i)} onItemClick={(i: number) => handleSelectShot(i)}
> >
{shotData.map((shot, index) => ( {originalVideos.map((shot, index) => (
<motion.div <motion.div
key={shot.id || index} key={shot.video_id || index}
className={cn( className={cn(
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group', 'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
selectedIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50' selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id) ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)} )}
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
{(shot.status === 0 || shot.videoUrl.length === 0) && ( {shot.urls.length > 0 && (
<div className="w-full h-full flex items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
</div>
)}
{shot.status === 1 && shot.videoUrl[0] && (
<video <video
src={shot.videoUrl[0].video_url} src={shot.urls[0]}
key={shot.videoUrl[0].video_url} key={shot.urls[0]}
className="w-full h-full object-cover" className="w-full h-full object-cover"
muted muted
loop loop
@ -263,12 +339,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
onMouseLeave={(e) => e.currentTarget.pause()} onMouseLeave={(e) => e.currentTarget.pause()}
/> />
)} )}
{/* 任务失败 */}
{shot.status === 2 && (
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
<CircleX className="w-4 h-4 text-red-500" />
</div>
)}
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent"> <div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90">{index + 1}</span> <span className="text-xs text-white/90">{index + 1}</span>
</div> </div>
@ -291,14 +361,14 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
<HorizontalScroller <HorizontalScroller
itemWidth={'auto'} itemWidth={'auto'}
gap={0} gap={0}
selectedIndex={selectedIndex} selectedIndex={originalVideos.findIndex(shot => selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id))}
onItemClick={(i: number) => handleSelectShot(i)} onItemClick={(i: number) => handleSelectShot(i)}
> >
{shotData.map((shot, index) => { {originalVideos.map((shot, index) => {
const isActive = selectedIndex === index; const isActive = selectedSegment?.videoUrl.some(url => url.video_id === shot.video_id);
return ( return (
<motion.div <motion.div
key={shot.id || index} key={shot.video_id || index}
className={cn( className={cn(
'flex-shrink-0 cursor-pointer transition-all duration-300', 'flex-shrink-0 cursor-pointer transition-all duration-300',
isActive ? 'text-white' : 'text-white/50 hover:text-white/80' isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
@ -311,14 +381,8 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm whitespace-nowrap flex items-center gap-1"> <span className="text-sm whitespace-nowrap flex items-center gap-1">
<span>Segment {index + 1}</span> <span>Segment {index + 1}</span>
{shot.status === 0 && (
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
)}
{shot.status === 2 && (
<CircleX className="w-4 h-4 text-red-500" />
)}
</span> </span>
{index < shotData.length - 1 && ( {index < originalVideos.length - 1 && (
<span className="text-white/20">|</span> <span className="text-white/20">|</span>
)} )}
</div> </div>
@ -335,7 +399,12 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
{/* 下部分 */} {/* 下部分 */}
{shotData[selectedIndex] && ( {loading ? (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
<p>Loading...</p>
</div>
) : selectedSegment && (
<motion.div <motion.div
className="grid grid-cols-2 gap-4 w-full" className="grid grid-cols-2 gap-4 w-full"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@ -346,23 +415,23 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
<div className="space-y-4 col-span-1"> <div className="space-y-4 col-span-1">
{/* 选中的视频预览 */} {/* 选中的视频预览 */}
<> <>
{(shotData[selectedIndex]?.status === 0) && ( {(selectedSegment?.status === 0) && (
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30"> <div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" /> <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
<span className="text-white/50">Loading...</span> <span className="text-white/50">Loading...</span>
</div> </div>
)} )}
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{shotData[selectedIndex]?.status === 1 && shotData[selectedIndex]?.videoUrl.length && ( {selectedSegment?.status === 1 && selectedSegment?.videoUrl.length && (
<motion.div <motion.div
className="aspect-video rounded-lg overflow-hidden relative group" className="aspect-video rounded-lg overflow-hidden relative group"
key={`video-preview-${selectedIndex}`} key={`video-preview-${selectedSegment?.id}`}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
> >
<PersonDetectionScene <PersonDetectionScene
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url} videoSrc={selectedSegment?.videoUrl[0].video_url}
detections={detections} detections={detections}
scanState={scanState} scanState={scanState}
triggerScan={scanState === 'scanning'} triggerScan={scanState === 'scanning'}
@ -396,7 +465,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{(shotData[selectedIndex]?.status === 2) && ( {(selectedSegment?.status === 2) && (
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10"> <div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
<CircleX className="w-4 h-4 text-red-500" /> <CircleX className="w-4 h-4 text-red-500" />
<span className="text-white/50">Failed, click to regenerate</span> <span className="text-white/50">Failed, click to regenerate</span>
@ -406,11 +475,11 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
</div> </div>
{/* 基础配置 */} {/* 基础配置 */}
<div className='space-y-4 col-span-1' key={selectedIndex}> <div className='space-y-4 col-span-1' key={selectedSegment?.id}>
<ShotsEditor <ShotsEditor
ref={shotsEditorRef} ref={shotsEditorRef}
roles={scriptRoles} roles={scriptRoles}
shotInfo={shotData[selectedIndex].lens} shotInfo={selectedSegment.lens}
style={{height: 'calc(100% - 4rem)'}} style={{height: 'calc(100% - 4rem)'}}
/> />
@ -466,6 +535,79 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
setIsReplaceLibraryOpen={setIsReplaceLibraryOpen} setIsReplaceLibraryOpen={setIsReplaceLibraryOpen}
onSelect={handleSelectCharacter} onSelect={handleSelectCharacter}
/> />
<FloatingGlassPanel
open={isRemindApplyUpdate}
width='66vw'
onClose={() => setIsRemindApplyUpdate(false)}
>
<div className='h-full w-full flex flex-col'>
{/* 提示文字 有分镜被修改 是否需要应用 */}
<div className='flex-1 flex items-center justify-center'>
<span className='text-white/50'>Three are some segments have been modified, do you need to apply?</span>
</div>
{/* 已修改分镜列表 */}
<HorizontalScroller
itemWidth={128}
gap={0}
>
{updateData.map((shot, index) => (
<motion.div
key={shot.id || index}
className={cn(
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group'
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{(shot.status === 0 || shot.videoUrl.length === 0) && (
<div className="w-full h-full flex items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
</div>
)}
{shot.status === 1 && shot.videoUrl[0] && (
<video
src={shot.videoUrl[0].video_url}
key={shot.videoUrl[0].video_url}
className="w-full h-full object-cover"
muted
loop
playsInline
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
/>
)}
{/* 任务失败 */}
{shot.status === 2 && (
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
<CircleX className="w-4 h-4 text-red-500" />
</div>
)}
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90">{shot.name}</span>
</div>
</motion.div>
))}
</HorizontalScroller>
{/* 按钮 应用 忽略 */}
<div className='flex items-center justify-end gap-2'>
<motion.button
onClick={() => handleApplyUpdate()}
className='px-4 py-2 bg-blue-500/10 hover:bg-blue-500/20
text-blue-500 rounded-lg transition-colors'
>
<span>Apply</span>
</motion.button>
<motion.button
onClick={() => handleIgnoreUpdate()}
className='px-4 py-2 bg-gray-500/10 hover:bg-gray-500/20
text-gray-500 rounded-lg transition-colors'
>
<span>Ignore</span>
</motion.button>
</div>
</div>
</FloatingGlassPanel>
</div> </div>
); );
} });