forked from 77media/video-flow
更新角色编辑和视频片段处理逻辑,新增角色选择和视频片段选择功能,优化角色数据的加载和状态管理。同时,调整角色编辑器以支持文本更新回调,确保角色描述的准确性和可视化效果。
This commit is contained in:
parent
f6c2ebaacb
commit
944b465069
@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import { useShotService } from "@/app/service/Interaction/ShotService";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRoleServiceHook } from "@/app/service/Interaction/RoleService";
|
||||
import { useRoleShotServiceHook } from "@/app/service/Interaction/RoleShotService";
|
||||
|
||||
const mockRoleData = [{
|
||||
id: '1',
|
||||
@ -46,6 +47,18 @@ export const useEditData = (tabType: string) => {
|
||||
regenerateRole
|
||||
} = useRoleServiceHook();
|
||||
|
||||
const {
|
||||
shotSelectionList,
|
||||
selectedRoleId,
|
||||
isAllVideoSegmentSelected,
|
||||
selectedVideoSegmentCount,
|
||||
fetchRoleShots,
|
||||
toggleSelectAllShots,
|
||||
toggleShotSelection,
|
||||
applyRoleToSelectedShots,
|
||||
clearShotSelection
|
||||
} = useRoleShotServiceHook(projectId);
|
||||
|
||||
useEffect(() => {
|
||||
if (tabType === 'shot') {
|
||||
getVideoSegmentList(projectId).then(() => {
|
||||
@ -56,7 +69,7 @@ export const useEditData = (tabType: string) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else if (tabType === 'role') {
|
||||
fetchUserRoleLibrary();
|
||||
// fetchUserRoleLibrary();
|
||||
fetchRoleList(projectId).then(() => {
|
||||
setLoading(false);
|
||||
}).catch((err) => {
|
||||
@ -73,8 +86,8 @@ export const useEditData = (tabType: string) => {
|
||||
}, [videoSegments]);
|
||||
|
||||
useEffect(() => {
|
||||
// setRoleData(roleList);
|
||||
setRoleData(mockRoleData);
|
||||
setRoleData(roleList);
|
||||
// setRoleData(mockRoleData);
|
||||
}, [roleList]);
|
||||
|
||||
return {
|
||||
@ -91,6 +104,16 @@ export const useEditData = (tabType: string) => {
|
||||
userRoleLibrary,
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
regenerateRole,
|
||||
// role shot
|
||||
shotSelectionList,
|
||||
selectedRoleId,
|
||||
isAllVideoSegmentSelected,
|
||||
selectedVideoSegmentCount,
|
||||
fetchRoleShots,
|
||||
toggleSelectAllShots,
|
||||
toggleShotSelection,
|
||||
applyRoleToSelectedShots,
|
||||
clearShotSelection
|
||||
}
|
||||
}
|
||||
@ -11,35 +11,15 @@ interface CharacterEditorProps {
|
||||
description: string;
|
||||
highlight: TagValueObject[];
|
||||
onSmartPolish: (text: string) => void;
|
||||
onUpdateText: (text: string) => void;
|
||||
}
|
||||
|
||||
const mockContent = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{ type: 'highlightText', attrs: { text: 'Hello, world!', color: 'blue' } },
|
||||
{ type: 'text', text: 'Hello, world!' },
|
||||
{ type: 'highlightText', attrs: { text: 'Hello, world!', color: 'red' } },
|
||||
{ type: 'text', text: 'Hello, world!' },
|
||||
{ type: 'highlightText', attrs: { text: 'Hello, world!', color: 'green' } },
|
||||
{ type: 'text', text: 'Hello, world!' },
|
||||
{ type: 'highlightText', attrs: { text: 'Hello, world!', color: 'yellow' } },
|
||||
{ type: 'text', text: 'Hello, world!' },
|
||||
{ type: 'highlightText', attrs: { text: 'Hello, world!', color: 'purple' } },
|
||||
{ type: 'text', text: 'Hello, world!' },
|
||||
{ type: 'highlightText', attrs: { text: 'Hello, world!', color: 'orange' } },
|
||||
{ type: 'text', text: 'Hello, world!' },
|
||||
{ type: 'highlightText', attrs: { text: 'Hello, world!', color: 'pink' } },
|
||||
{ type: 'text', text: 'Hello, world!' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
className,
|
||||
description,
|
||||
highlight,
|
||||
onSmartPolish
|
||||
onSmartPolish,
|
||||
onUpdateText
|
||||
}, ref) => {
|
||||
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||
const [content, setContent] = useState<any[]>([]);
|
||||
@ -53,6 +33,12 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
onSmartPolish(text);
|
||||
};
|
||||
|
||||
const handleChangeContent = (content: any) => {
|
||||
console.log('-==========handleChangeContent===========-', content);
|
||||
onUpdateText(TextToShotAdapter.fromRoleToText(content));
|
||||
setContent(content);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsInit(true);
|
||||
console.log('-==========description===========-', description);
|
||||
@ -64,7 +50,7 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
setIsInit(false);
|
||||
setIsOptimizing(false);
|
||||
}, 100);
|
||||
}, [description, highlight]);
|
||||
}, [highlight]);
|
||||
|
||||
// 暴露方法给父组件
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
@ -77,7 +63,7 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
<div className={cn("space-y-2 border border-white/10 relative p-2 rounded-[0.5rem] pb-12", className)}>
|
||||
{/* 自由输入区域 */}
|
||||
{
|
||||
!isInit && <MainEditor content={content} onChangeContent={setContent} />
|
||||
!isInit && <MainEditor content={content} onChangeContent={handleChangeContent} />
|
||||
}
|
||||
|
||||
{/* 智能润色按钮 */}
|
||||
|
||||
@ -5,7 +5,7 @@ import { cn } from '@/public/lib/utils';
|
||||
import { CharacterEditor } from './character-editor';
|
||||
import ImageBlurTransition from './ImageBlurTransition';
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||
import { ReplaceCharacterPanel } from './replace-character-panel';
|
||||
import { CharacterLibrarySelector } from './character-library-selector';
|
||||
import HorizontalScroller from './HorizontalScroller';
|
||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||
@ -69,11 +69,12 @@ export function CharacterTabContent({
|
||||
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
||||
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
||||
const [isRemindReplacePanelOpen, setIsRemindReplacePanelOpen] = useState(false);
|
||||
const [selectRoleIndex, setSelectRoleIndex] = useState(0);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [enableAnimation, setEnableAnimation] = useState(true);
|
||||
const [showAddToLibrary, setShowAddToLibrary] = useState(true);
|
||||
const characterEditorRef = useRef<any>(null);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isRegenerate, setIsRegenerate] = useState(false);
|
||||
|
||||
const {
|
||||
loading,
|
||||
@ -83,7 +84,17 @@ export function CharacterTabContent({
|
||||
userRoleLibrary,
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
regenerateRole,
|
||||
// role shot
|
||||
shotSelectionList,
|
||||
selectedRoleId,
|
||||
isAllVideoSegmentSelected,
|
||||
selectedVideoSegmentCount,
|
||||
fetchRoleShots,
|
||||
toggleSelectAllShots,
|
||||
toggleShotSelection,
|
||||
applyRoleToSelectedShots,
|
||||
clearShotSelection
|
||||
} = useEditData('role');
|
||||
const searchParams = useSearchParams();
|
||||
const episodeId = searchParams.get('episodeId');
|
||||
@ -91,19 +102,29 @@ export function CharacterTabContent({
|
||||
|
||||
useEffect(() => {
|
||||
console.log('-==========roleData===========-', roleData);
|
||||
if (roleData.length > 0) {
|
||||
selectRole(roleData[selectRoleIndex].id);
|
||||
|
||||
// 只在初始化且有角色数据时执行
|
||||
if (!isInitialized && roleData.length > 0) {
|
||||
selectRole(roleData[0].id);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [selectRoleIndex, roleData]);
|
||||
}, [roleData, isInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('获取选中项数据', selectedRole);
|
||||
}, [selectedRole]);
|
||||
|
||||
const handleSmartPolish = (text: string) => {
|
||||
// 首先更新
|
||||
updateRoleText(text);
|
||||
// 然后调用优化角色文本
|
||||
optimizeRoleText(text);
|
||||
};
|
||||
|
||||
const handleStartReplaceCharacter = () => {
|
||||
// 获取当前角色对应的视频片段
|
||||
fetchRoleShots(selectedRole?.id || '');
|
||||
// 打开替换角色面板
|
||||
setIsReplacePanelOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmGotoReplace = () => {
|
||||
setIsRemindReplacePanelOpen(false);
|
||||
setIsReplacePanelOpen(true);
|
||||
@ -134,7 +155,9 @@ export function CharacterTabContent({
|
||||
};
|
||||
|
||||
const handleChangeRole = (index: number) => {
|
||||
if (selectedRole?.imageUrl !== roleData[selectRoleIndex].imageUrl && !ignoreReplace) {
|
||||
const oldRole = roleData.find(role => role.id === selectedRole?.id);
|
||||
console.log('切换角色前对比', selectedRole?.imageUrl, oldRole?.imageUrl);
|
||||
if (selectedRole?.imageUrl !== oldRole?.imageUrl && !ignoreReplace) {
|
||||
// 提示 角色已修改,弹出替换角色面板
|
||||
setIsRemindReplacePanelOpen(true);
|
||||
return;
|
||||
@ -142,8 +165,9 @@ export function CharacterTabContent({
|
||||
// 重置替换规则
|
||||
setEnableAnimation(false);
|
||||
setIgnoreReplace(false);
|
||||
setIsRegenerate(false);
|
||||
|
||||
setSelectRoleIndex(index);
|
||||
selectRole(roleData[index].id);
|
||||
};
|
||||
|
||||
// 从角色库中选择角色
|
||||
@ -166,14 +190,16 @@ export function CharacterTabContent({
|
||||
setShowAddToLibrary(true);
|
||||
};
|
||||
|
||||
const handleRegenerate = () => {
|
||||
const handleRegenerate = async () => {
|
||||
console.log('Regenerate');
|
||||
const text = characterEditorRef.current.getRoleText();
|
||||
console.log('-==========text===========-', text);
|
||||
// 重生前 更新 当前项 generateText
|
||||
updateRoleText(text);
|
||||
setIsRegenerate(true);
|
||||
// const text = characterEditorRef.current.getRoleText();
|
||||
// console.log('-==========text===========-', text);
|
||||
// // 重生前 更新 当前项 generateText
|
||||
// updateRoleText(text);
|
||||
// 然后调用重新生成角色
|
||||
regenerateRole();
|
||||
await regenerateRole();
|
||||
setIsRegenerate(false);
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
@ -239,7 +265,7 @@ export function CharacterTabContent({
|
||||
<HorizontalScroller
|
||||
itemWidth={96}
|
||||
gap={0}
|
||||
selectedIndex={selectRoleIndex}
|
||||
selectedIndex={roleData.findIndex(role => role.id === selectedRole?.id)}
|
||||
onItemClick={(i: number) => handleChangeRole(i)}
|
||||
>
|
||||
{roleData.map((role, index) => (
|
||||
@ -248,7 +274,7 @@ export function CharacterTabContent({
|
||||
className={cn(
|
||||
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
||||
'aspect-[9/16]',
|
||||
selectRoleIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||
role.id === selectedRole?.id ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||
)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
@ -280,8 +306,8 @@ export function CharacterTabContent({
|
||||
{/* 角色预览图 */}
|
||||
<div className="w-full h-full mx-auto rounded-lg relative group">
|
||||
<ImageBlurTransition
|
||||
src={roleData[selectRoleIndex].imageUrl || ''}
|
||||
alt={roleData[selectRoleIndex].name}
|
||||
src={selectedRole?.imageUrl || ''}
|
||||
alt={selectedRole?.name || ''}
|
||||
width='100%'
|
||||
height='100%'
|
||||
enableAnimation={enableAnimation}
|
||||
@ -315,14 +341,15 @@ export function CharacterTabContent({
|
||||
<CharacterEditor
|
||||
ref={characterEditorRef}
|
||||
className="min-h-[calc(100%-4rem)]"
|
||||
description={roleData[selectRoleIndex].generateText || ''}
|
||||
highlight={roleData[selectRoleIndex].tags || []}
|
||||
description={selectedRole?.generateText || ''}
|
||||
highlight={selectedRole?.tags || []}
|
||||
onSmartPolish={handleSmartPolish}
|
||||
onUpdateText={(text: string) => updateRoleText(text)}
|
||||
/>
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<motion.button
|
||||
onClick={() => handleReplaceCharacter('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg')}
|
||||
onClick={() => handleStartReplaceCharacter()}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
|
||||
text-pink-500 rounded-lg transition-colors"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
@ -334,12 +361,13 @@ export function CharacterTabContent({
|
||||
<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"
|
||||
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>Regenerate</span>
|
||||
<span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
@ -356,8 +384,8 @@ export function CharacterTabContent({
|
||||
onClose={() => handleCloseReplacePanel()}
|
||||
>
|
||||
<ReplaceCharacterPanel
|
||||
shots={mockShots}
|
||||
character={mockCharacter}
|
||||
shots={shotSelectionList}
|
||||
character={selectedRole}
|
||||
showAddToLibrary={showAddToLibrary}
|
||||
onClose={() => handleCloseReplacePanel()}
|
||||
onConfirm={handleConfirmReplace}
|
||||
|
||||
@ -23,7 +23,7 @@ export function HighlightText(props: ReactNodeViewProps) {
|
||||
<NodeViewWrapper
|
||||
as="span"
|
||||
data-alt="highlight-text"
|
||||
contentEditable={false}
|
||||
contentEditable={true}
|
||||
className={`relative inline text-${color}-400 hover:text-${color}-300 transition-colors duration-200`}
|
||||
>
|
||||
{text}
|
||||
|
||||
@ -2,7 +2,7 @@ import { ReplacePanel } from './replace-panel';
|
||||
import { Shot, Character } from '@/app/model/types';
|
||||
|
||||
interface ReplaceCharacterPanelProps {
|
||||
shots: Shot[];
|
||||
shots: any[];
|
||||
character: Character;
|
||||
showAddToLibrary?: boolean;
|
||||
onClose: () => void;
|
||||
@ -39,15 +39,9 @@ export const mockShots: Shot[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCharacter: Character = {
|
||||
id: '1',
|
||||
name: '千寻',
|
||||
avatarUrl: '/assets/3dr_chihiro.png',
|
||||
};
|
||||
|
||||
export function ReplaceCharacterPanel({
|
||||
shots = mockShots,
|
||||
character = mockCharacter,
|
||||
shots = [],
|
||||
character,
|
||||
showAddToLibrary = true,
|
||||
onClose,
|
||||
onConfirm,
|
||||
|
||||
@ -3,25 +3,10 @@ import { motion } from 'framer-motion';
|
||||
import { Check, X, CircleAlert, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { cn } from '@/public/lib/utils';
|
||||
|
||||
// 定义类型
|
||||
interface Shot {
|
||||
id: string;
|
||||
videoUrl?: string;
|
||||
thumbnailUrl: string;
|
||||
isGenerating: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
interface ReplacePanelProps {
|
||||
title: string;
|
||||
shots: Shot[];
|
||||
item: Item;
|
||||
shots: any[];
|
||||
item: any;
|
||||
showAddToLibrary?: boolean;
|
||||
addToLibraryText?: string;
|
||||
onClose: () => void;
|
||||
@ -175,16 +160,16 @@ export function ReplacePanel({
|
||||
/>
|
||||
)}
|
||||
{!shot.videoUrl && (
|
||||
<img
|
||||
src={shot.thumbnailUrl}
|
||||
alt={`Shot ${shot.id}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{shot.isGenerating && (
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
||||
<div className="text-white text-sm">生成中...</div>
|
||||
</div>
|
||||
<>
|
||||
<img
|
||||
src={shot.sketchUrl}
|
||||
alt={`Shot ${shot.id}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
||||
<div className="text-white text-sm">生成中...</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{selectedShots.includes(shot.id) && (
|
||||
<div className="absolute top-2 right-2">
|
||||
@ -219,7 +204,7 @@ export function ReplacePanel({
|
||||
{/* 预览信息 */}
|
||||
<div className="flex items-center gap-4 bg-white/5 rounded-lg p-4">
|
||||
<img
|
||||
src={item.avatarUrl}
|
||||
src={item.imageUrl}
|
||||
alt={item.name}
|
||||
className="w-12 h-12 rounded-full object-cover"
|
||||
/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user