From 0ef11c4e89578bd0a1eea815960873c85f6104ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Thu, 21 Aug 2025 20:55:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=B8=BB=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=B0=E7=BC=96=E8=BE=91=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/DTO/movieEdit.ts | 6 +- app/service/Interaction/RoleShotService.ts | 3 + components/pages/work-flow.tsx | 1 + components/pages/work-flow/use-edit-data.tsx | 5 +- components/ui/character-tab-content.tsx | 241 ++++++++-------- components/ui/edit-modal.tsx | 36 ++- components/ui/script-tab-content.tsx | 11 +- components/ui/shot-tab-content.tsx | 274 ++++++++++++++----- 8 files changed, 367 insertions(+), 210 deletions(-) diff --git a/api/DTO/movieEdit.ts b/api/DTO/movieEdit.ts index 62b67ea..989013a 100644 --- a/api/DTO/movieEdit.ts +++ b/api/DTO/movieEdit.ts @@ -621,7 +621,7 @@ export interface RoleResponse { } -interface Role { +export interface Role { name: string; url: string; status: number; @@ -636,7 +636,7 @@ interface ShotSketch { script: string; status: number; } -interface Video { +export interface ShotVideo { video_id: string; urls: string[]; video_status: number; @@ -663,7 +663,7 @@ export interface TaskObject { total_count: number; }; // 分镜草图 videos: { - data: Video[]; + data: ShotVideo[]; total_count: number; }; // 视频 final: { diff --git a/app/service/Interaction/RoleShotService.ts b/app/service/Interaction/RoleShotService.ts index 4c75ed1..de33edf 100644 --- a/app/service/Interaction/RoleShotService.ts +++ b/app/service/Interaction/RoleShotService.ts @@ -179,6 +179,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity, return role.fromDraft }); console.log('newDraftRoleList', newDraftRoleList) + console.log('应用角色到分镜', shotSelectionList) // 循环调用接口,为每个选中的分镜单独调用 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.videoTasks, ...res.map(item=>{ diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index 991adbf..e29b966 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -228,6 +228,7 @@ const WorkFlow = React.memo(function WorkFlow() { SaveEditUseCase.clearData(); setIsEditModalOpen(false) }} + taskObject={taskObject} currentSketchIndex={currentSketchIndex} roles={taskObject.roles.data} setIsPauseWorkFlow={setIsPauseWorkFlow} diff --git a/components/pages/work-flow/use-edit-data.tsx b/components/pages/work-flow/use-edit-data.tsx index 5217cf5..33a81c4 100644 --- a/components/pages/work-flow/use-edit-data.tsx +++ b/components/pages/work-flow/use-edit-data.tsx @@ -5,13 +5,14 @@ import { useSearchParams } from 'next/navigation'; import { useRoleServiceHook } from "@/app/service/Interaction/RoleService"; import { useRoleShotServiceHook } from "@/app/service/Interaction/RoleShotService"; import { useScriptService } from "@/app/service/Interaction/ScriptService"; +import { VideoSegmentEntity } from "@/app/service/domain/Entities"; export const useEditData = (tabType: string, originalText?: string) => { const searchParams = useSearchParams(); const projectId = searchParams.get('episodeId') || ''; const [loading, setLoading] = useState(true); const [scriptData, setScriptData] = useState([]); - const [shotData, setShotData] = useState([]); + const [shotData, setShotData] = useState([]); const [roleData, setRoleData] = useState([]); @@ -24,6 +25,7 @@ export const useEditData = (tabType: string, originalText?: string) => { const { videoSegments, + selectedSegment, scriptRoles, getVideoSegmentList, setSelectedSegment, @@ -123,6 +125,7 @@ export const useEditData = (tabType: string, originalText?: string) => { applyScript, // shot shotData, + selectedSegment, scriptRoles, setSelectedSegment, regenerateVideoSegment, diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index db18732..38aa4c0 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -11,30 +11,10 @@ import HorizontalScroller from './HorizontalScroller'; import { useEditData } from '@/components/pages/work-flow/use-edit-data'; import { useSearchParams } from 'next/navigation'; import { RoleEntity } from '@/app/service/domain/Entities'; - -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[]; -} - +import { Role } from '@/api/DTO/movieEdit'; interface CharacterTabContentProps { + originalRoles: Role[]; onClose: () => void; onApply: () => void; setActiveTab: (tabId: string) => void; @@ -42,10 +22,10 @@ interface CharacterTabContentProps { export const CharacterTabContent = forwardRef< - { switchBefore: (tabId: string) => boolean, saveBefore: () => void }, + { switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void }, CharacterTabContentProps >((props, ref) => { - const { onClose, onApply, setActiveTab } = props; + const { onClose, onApply, setActiveTab, originalRoles } = props; const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); const [replacePanelKey, setReplacePanelKey] = useState(0); const [ignoreReplace, setIgnoreReplace] = useState(false); @@ -76,7 +56,6 @@ CharacterTabContentProps regenerateRole, fetchUserRoleLibrary, uploadImageAndUpdateRole, - changeTabCallback, // role shot shotSelectionList, fetchRoleShots, @@ -91,7 +70,8 @@ CharacterTabContentProps switchBefore: (tabId: string) => { setNextToTabId(tabId); // 判断 角色是否修改 - const isChange = selectedRole!.isChangeRole + const currentIndex = getCurrentIndex(); + const isChange = currentIndex !== -1 && isRoleChange(originalRoles[currentIndex]); console.log('switchBefore', isChange); if (isChange) { setTriggerType('tab'); @@ -99,15 +79,16 @@ CharacterTabContentProps } return isChange; }, - saveBefore: () => { - console.log('saveBefore'); + saveOrCloseBefore: () => { + console.log('saveOrCloseBefore'); // 判断 角色是否修改 - changeTabCallback((isChange: Boolean) => { - if (isChange) { - setTriggerType('apply'); - handleStartReplaceCharacter(); - } - }); + const currentIndex = getCurrentIndex(); + if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) { + setTriggerType('apply'); + handleStartReplaceCharacter(); + } else { + onClose(); + } } })); @@ -179,24 +160,33 @@ CharacterTabContentProps 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 oldRole = roleData.find(role => role.id === selectedRole?.id); console.log('切换角色前对比'); - changeTabCallback((isChange: Boolean) => { - if (isChange) { - setTriggerType('user'); - setIsRemindReplacePanelOpen(true); - setNextToUserIndex(index); - return; - } + const currentIndex = getCurrentIndex(); + if (currentIndex === index) return; + if (currentIndex !== -1 && isRoleChange(originalRoles[currentIndex])) { + setTriggerType('user'); + setIsRemindReplacePanelOpen(true); + setNextToUserIndex(index); + return; + } - // 重置替换规则 - setEnableAnimation(false); - setIgnoreReplace(false); - setIsRegenerate(false); + // 重置替换规则 + setEnableAnimation(false); + setIgnoreReplace(false); + setIsRegenerate(false); - selectRole(roleData[index]); - }); + selectRole(roleData[index]); }; // 从角色库中选择角色 @@ -266,18 +256,8 @@ CharacterTabContentProps }); }; - // 如果loading 显示loading状态 - if (loading) { - return ( -
-
-

Loading...

-
- ); - } - // 如果没有角色数据,显示占位内容 - if (roleData.length === 0) { + if (originalRoles.length === 0) { return (
@@ -306,22 +286,22 @@ CharacterTabContentProps role.id === selectedRole?.id)} + selectedIndex={originalRoles?.findIndex(role => role.name === selectedRole?.name)} onItemClick={(i: number) => handleChangeRole(i)} > - {roleData.map((role, index) => ( + {originalRoles.map((role, index) => ( {role.name} @@ -335,78 +315,83 @@ CharacterTabContentProps {/* 下部分:角色详情 */} - + { loading ? ( +
+ +

Loading...

+
+ ) : ( + - {/* 左列:角色预览 */} -
- {/* 角色预览图 */} -
- + {/* 角色预览图 */} +
+ + {/* 应用角色按钮 */} +
+ + {isUploading ? : } + + handleOpenReplaceLibrary()} + > + + +
+
+ +
+ {/* 右列:角色信息 */} +
+ updateRoleText(text)} /> - {/* 应用角色按钮 */} -
+ {/* 重新生成按钮、替换形象按钮 */} +
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} > - {isUploading ? : } - - handleOpenReplaceLibrary()} - > - + + {isRegenerate ? 'Regenerating...' : 'Regenerate'}
- -
- {/* 右列:角色信息 */} -
- updateRoleText(text)} - /> - {/* 重新生成按钮、替换形象按钮 */} -
- 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} - > - - {isRegenerate ? 'Regenerating...' : 'Regenerate'} - -
-
- - - - + + )} + (null); const characterTabContentRef = useRef(null); + const shotTabContentRef = useRef(null); // 添加一个状态来标记是否是从切换tab触发的提醒 const [pendingSwitchTabId, setPendingSwitchTabId] = useState(null); const [disabledBtn, setDisabledBtn] = useState(false); @@ -72,9 +76,9 @@ export function EditModal({ const isTabDisabled = (tabId: string) => { 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 === '3') return sketchVideo.length === 0; + if (tabId === '3') return taskObject?.videos.data.length === 0; if (tabId === '4') return false; return false; }; @@ -98,6 +102,11 @@ export function EditModal({ if (characterTabContent) { return characterTabContent.switchBefore(tabId); } + } else if (activeTab === '3') { + const shotTabContent = shotTabContentRef.current; + if (shotTabContent) { + return shotTabContent.switchBefore(tabId); + } } return false; } @@ -117,11 +126,11 @@ export function EditModal({ console.log('handleSave'); // setIsRemindFallbackOpen(true); if (activeTab === '0') { - scriptTabContentRef.current.saveBefore(); + scriptTabContentRef.current.saveOrCloseBefore(); } else if (activeTab === '1') { - characterTabContentRef.current.saveBefore(); + characterTabContentRef.current.saveOrCloseBefore(); } else if (activeTab === '3') { - handleConfirmGotoFallback(); + shotTabContentRef.current.saveOrCloseBefore('apply'); } } @@ -170,7 +179,13 @@ export function EditModal({ const handleClickClose = () => { // 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 = () => { @@ -195,6 +210,7 @@ export function EditModal({ originalText={originalText} onApply={handleApply} setActiveTab={setActiveTab} + onClose={onClose} /> ); case '1': @@ -204,6 +220,7 @@ export function EditModal({ onClose={onClose} onApply={handleApply} setActiveTab={setActiveTab} + originalRoles={taskObject?.roles.data || []} /> ); case '2': @@ -215,9 +232,12 @@ export function EditModal({ case '3': return ( ); case '4': diff --git a/components/ui/script-tab-content.tsx b/components/ui/script-tab-content.tsx index c347006..6efd486 100644 --- a/components/ui/script-tab-content.tsx +++ b/components/ui/script-tab-content.tsx @@ -12,13 +12,14 @@ interface ScriptTabContentProps { originalText?: string; onApply: () => void; setActiveTab: (tabId: string) => void; + onClose: () => void; } export const ScriptTabContent = forwardRef< - { switchBefore: (tabId: string) => boolean, saveBefore: () => void }, + { switchBefore: (tabId: string) => boolean, saveOrCloseBefore: () => void }, ScriptTabContentProps >((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 [isUpdate, setIsUpdate] = useState(false); @@ -39,10 +40,12 @@ export const ScriptTabContent = forwardRef< } return isUpdate; }, - saveBefore: () => { - console.log('saveBefore'); + saveOrCloseBefore: () => { + console.log('saveOrCloseBefore'); if (isUpdate) { onApply(); + } else { + onClose(); } } })); diff --git a/components/ui/shot-tab-content.tsx b/components/ui/shot-tab-content.tsx index e15c2d2..b97da86 100644 --- a/components/ui/shot-tab-content.tsx +++ b/components/ui/shot-tab-content.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useRef, useEffect, useState } from 'react'; +import React, { useRef, useEffect, useState, forwardRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { RefreshCw, User, Loader2, X, Plus, Video, CircleX } from 'lucide-react'; import { cn } from '@/public/lib/utils'; @@ -11,19 +11,26 @@ import FloatingGlassPanel from './FloatingGlassPanel'; import { ReplaceCharacterPanel } from './replace-character-panel'; import HorizontalScroller from './HorizontalScroller'; 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 { currentSketchIndex: number; - roles?: any[]; + originalVideos: ShotVideo[]; onApply: () => void; + onClose: () => void; + setActiveTab: (tabId: string) => void; } -export const ShotTabContent = (props: ShotTabContentProps) => { - const { currentSketchIndex = 0, roles = [], onApply } = props; +export const ShotTabContent = forwardRef< + { switchBefore: (tabId: string) => boolean, saveOrCloseBefore: (type: 'apply' | 'close') => void }, + ShotTabContentProps +>((props, ref) => { + const { currentSketchIndex = 0, onApply, onClose, originalVideos, setActiveTab } = props; const { loading, shotData, + selectedSegment, scriptRoles, setSelectedSegment, regenerateVideoSegment, @@ -36,8 +43,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => { calculateRecognitionBoxes, setSelectedRole } = useEditData('shot'); - const [selectedIndex, setSelectedIndex] = useState(currentSketchIndex); - const [detections, setDetections] = useState([]); const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected' | 'failed' | 'timeout'>('idle'); const [isScanFailed, setIsScanFailed] = useState(false); @@ -51,13 +56,54 @@ export const ShotTabContent = (props: ShotTabContentProps) => { const [isRegenerate, setIsRegenerate] = useState(false); const [pendingRegeneration, setPendingRegeneration] = useState(false); + const [isInitialized, setIsInitialized] = useState(true); + const [triggerType, setTriggerType] = useState<'tab' | 'apply' | 'close'>('tab'); + const [nextToTabId, setNextToTabId] = useState(''); + const [isRemindApplyUpdate, setIsRemindApplyUpdate] = useState(false); + const [updateData, setUpdateData] = useState([]); + useEffect(() => { console.log('shotTabContent-----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(() => { if (pendingRegeneration) { - console.log('pendingRegeneration', pendingRegeneration, shotData[selectedIndex]?.lens); + console.log('pendingRegeneration', pendingRegeneration, selectedSegment?.lens); regenerateVideoSegment().then(() => { setPendingRegeneration(false); setIsRegenerate(false); @@ -65,16 +111,35 @@ export const ShotTabContent = (props: ShotTabContentProps) => { } }, [pendingRegeneration]); - // 监听当前选中index变化 + // 监听当前选中segment变化 useEffect(() => { console.log('shotTabContent-----shotData', shotData); - if (shotData.length > 0) { + if (shotData.length > 0 && !selectedSegment) { // 清空检测状态 和 检测结果 setScanState('idle'); 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 () => { @@ -174,16 +239,34 @@ export const ShotTabContent = (props: ShotTabContentProps) => { 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 () => { console.log('regenerate'); setIsRegenerate(true); const shotInfo = shotsEditorRef.current.getShotInfo(); console.log('shotInfo', shotInfo); - setSelectedSegment({ - ...shotData[selectedIndex], - lens: shotInfo - }); + if (selectedSegment) { + setSelectedSegment({ + ...selectedSegment, + lens: shotInfo + }); + } setTimeout(() => { setPendingRegeneration(true); }, 1000); @@ -197,22 +280,20 @@ export const ShotTabContent = (props: ShotTabContentProps) => { // 切换选择分镜 const handleSelectShot = (index: number) => { - // 切换前 判断数据是否发生变化 - setSelectedIndex(index); + // 通过 video_id 找到对应的分镜 + 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 ( -
-
-

Loading...

-
- ); - } + // 如果没有数据,显示空状态 - if (shotData.length === 0) { + if (originalVideos.length === 0) { return (