更新角色编辑和视频片段处理逻辑,新增角色选择和视频片段选择功能,优化角色数据的加载和状态管理。同时,调整角色编辑器以支持文本更新回调,确保角色描述的准确性和可视化效果。

This commit is contained in:
北枳 2025-08-12 16:50:54 +08:00
parent f6c2ebaacb
commit 944b465069
6 changed files with 111 additions and 95 deletions

View File

@ -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
}
}

View File

@ -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} />
}
{/* 智能润色按钮 */}

View File

@ -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}

View File

@ -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}

View File

@ -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,

View File

@ -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"
/>