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

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 { useShotService } from "@/app/service/Interaction/ShotService";
import { useSearchParams } from 'next/navigation'; 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";
const mockRoleData = [{ const mockRoleData = [{
id: '1', id: '1',
@ -46,6 +47,18 @@ export const useEditData = (tabType: string) => {
regenerateRole regenerateRole
} = useRoleServiceHook(); } = useRoleServiceHook();
const {
shotSelectionList,
selectedRoleId,
isAllVideoSegmentSelected,
selectedVideoSegmentCount,
fetchRoleShots,
toggleSelectAllShots,
toggleShotSelection,
applyRoleToSelectedShots,
clearShotSelection
} = useRoleShotServiceHook(projectId);
useEffect(() => { useEffect(() => {
if (tabType === 'shot') { if (tabType === 'shot') {
getVideoSegmentList(projectId).then(() => { getVideoSegmentList(projectId).then(() => {
@ -56,7 +69,7 @@ export const useEditData = (tabType: string) => {
setLoading(false); setLoading(false);
}); });
} else if (tabType === 'role') { } else if (tabType === 'role') {
fetchUserRoleLibrary(); // fetchUserRoleLibrary();
fetchRoleList(projectId).then(() => { fetchRoleList(projectId).then(() => {
setLoading(false); setLoading(false);
}).catch((err) => { }).catch((err) => {
@ -73,8 +86,8 @@ export const useEditData = (tabType: string) => {
}, [videoSegments]); }, [videoSegments]);
useEffect(() => { useEffect(() => {
// setRoleData(roleList); setRoleData(roleList);
setRoleData(mockRoleData); // setRoleData(mockRoleData);
}, [roleList]); }, [roleList]);
return { return {
@ -91,6 +104,16 @@ export const useEditData = (tabType: string) => {
userRoleLibrary, userRoleLibrary,
optimizeRoleText, optimizeRoleText,
updateRoleText, updateRoleText,
regenerateRole regenerateRole,
// role shot
shotSelectionList,
selectedRoleId,
isAllVideoSegmentSelected,
selectedVideoSegmentCount,
fetchRoleShots,
toggleSelectAllShots,
toggleShotSelection,
applyRoleToSelectedShots,
clearShotSelection
} }
} }

View File

@ -11,35 +11,15 @@ interface CharacterEditorProps {
description: string; description: string;
highlight: TagValueObject[]; highlight: TagValueObject[];
onSmartPolish: (text: string) => void; 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>(({ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
className, className,
description, description,
highlight, highlight,
onSmartPolish onSmartPolish,
onUpdateText
}, ref) => { }, ref) => {
const [isOptimizing, setIsOptimizing] = useState(false); const [isOptimizing, setIsOptimizing] = useState(false);
const [content, setContent] = useState<any[]>([]); const [content, setContent] = useState<any[]>([]);
@ -53,6 +33,12 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
onSmartPolish(text); onSmartPolish(text);
}; };
const handleChangeContent = (content: any) => {
console.log('-==========handleChangeContent===========-', content);
onUpdateText(TextToShotAdapter.fromRoleToText(content));
setContent(content);
};
useEffect(() => { useEffect(() => {
setIsInit(true); setIsInit(true);
console.log('-==========description===========-', description); console.log('-==========description===========-', description);
@ -64,7 +50,7 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
setIsInit(false); setIsInit(false);
setIsOptimizing(false); setIsOptimizing(false);
}, 100); }, 100);
}, [description, highlight]); }, [highlight]);
// 暴露方法给父组件 // 暴露方法给父组件
React.useImperativeHandle(ref, () => ({ 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)}> <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 { CharacterEditor } from './character-editor';
import ImageBlurTransition from './ImageBlurTransition'; import ImageBlurTransition from './ImageBlurTransition';
import FloatingGlassPanel from './FloatingGlassPanel'; 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 { CharacterLibrarySelector } from './character-library-selector';
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';
@ -69,11 +69,12 @@ export function CharacterTabContent({
const [ignoreReplace, setIgnoreReplace] = useState(false); const [ignoreReplace, setIgnoreReplace] = useState(false);
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false); const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
const [isRemindReplacePanelOpen, setIsRemindReplacePanelOpen] = useState(false); const [isRemindReplacePanelOpen, setIsRemindReplacePanelOpen] = useState(false);
const [selectRoleIndex, setSelectRoleIndex] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [enableAnimation, setEnableAnimation] = useState(true); const [enableAnimation, setEnableAnimation] = useState(true);
const [showAddToLibrary, setShowAddToLibrary] = useState(true); const [showAddToLibrary, setShowAddToLibrary] = useState(true);
const characterEditorRef = useRef<any>(null); const characterEditorRef = useRef<any>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isRegenerate, setIsRegenerate] = useState(false);
const { const {
loading, loading,
@ -83,7 +84,17 @@ export function CharacterTabContent({
userRoleLibrary, userRoleLibrary,
optimizeRoleText, optimizeRoleText,
updateRoleText, updateRoleText,
regenerateRole regenerateRole,
// role shot
shotSelectionList,
selectedRoleId,
isAllVideoSegmentSelected,
selectedVideoSegmentCount,
fetchRoleShots,
toggleSelectAllShots,
toggleShotSelection,
applyRoleToSelectedShots,
clearShotSelection
} = useEditData('role'); } = useEditData('role');
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId'); const episodeId = searchParams.get('episodeId');
@ -91,19 +102,29 @@ export function CharacterTabContent({
useEffect(() => { useEffect(() => {
console.log('-==========roleData===========-', roleData); 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) => { const handleSmartPolish = (text: string) => {
// 首先更新
updateRoleText(text);
// 然后调用优化角色文本 // 然后调用优化角色文本
optimizeRoleText(text); optimizeRoleText(text);
}; };
const handleStartReplaceCharacter = () => {
// 获取当前角色对应的视频片段
fetchRoleShots(selectedRole?.id || '');
// 打开替换角色面板
setIsReplacePanelOpen(true);
};
const handleConfirmGotoReplace = () => { const handleConfirmGotoReplace = () => {
setIsRemindReplacePanelOpen(false); setIsRemindReplacePanelOpen(false);
setIsReplacePanelOpen(true); setIsReplacePanelOpen(true);
@ -134,7 +155,9 @@ export function CharacterTabContent({
}; };
const handleChangeRole = (index: number) => { 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); setIsRemindReplacePanelOpen(true);
return; return;
@ -142,8 +165,9 @@ export function CharacterTabContent({
// 重置替换规则 // 重置替换规则
setEnableAnimation(false); setEnableAnimation(false);
setIgnoreReplace(false); setIgnoreReplace(false);
setIsRegenerate(false);
setSelectRoleIndex(index); selectRole(roleData[index].id);
}; };
// 从角色库中选择角色 // 从角色库中选择角色
@ -166,14 +190,16 @@ export function CharacterTabContent({
setShowAddToLibrary(true); setShowAddToLibrary(true);
}; };
const handleRegenerate = () => { const handleRegenerate = async () => {
console.log('Regenerate'); console.log('Regenerate');
const text = characterEditorRef.current.getRoleText(); setIsRegenerate(true);
console.log('-==========text===========-', text); // const text = characterEditorRef.current.getRoleText();
// 重生前 更新 当前项 generateText // console.log('-==========text===========-', text);
updateRoleText(text); // // 重生前 更新 当前项 generateText
// updateRoleText(text);
// 然后调用重新生成角色 // 然后调用重新生成角色
regenerateRole(); await regenerateRole();
setIsRegenerate(false);
}; };
const handleUploadClick = () => { const handleUploadClick = () => {
@ -239,7 +265,7 @@ export function CharacterTabContent({
<HorizontalScroller <HorizontalScroller
itemWidth={96} itemWidth={96}
gap={0} gap={0}
selectedIndex={selectRoleIndex} selectedIndex={roleData.findIndex(role => role.id === selectedRole?.id)}
onItemClick={(i: number) => handleChangeRole(i)} onItemClick={(i: number) => handleChangeRole(i)}
> >
{roleData.map((role, index) => ( {roleData.map((role, index) => (
@ -248,7 +274,7 @@ export function CharacterTabContent({
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]',
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 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
@ -280,8 +306,8 @@ export function CharacterTabContent({
{/* 角色预览图 */} {/* 角色预览图 */}
<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={roleData[selectRoleIndex].imageUrl || ''} src={selectedRole?.imageUrl || ''}
alt={roleData[selectRoleIndex].name} alt={selectedRole?.name || ''}
width='100%' width='100%'
height='100%' height='100%'
enableAnimation={enableAnimation} enableAnimation={enableAnimation}
@ -315,14 +341,15 @@ export function CharacterTabContent({
<CharacterEditor <CharacterEditor
ref={characterEditorRef} ref={characterEditorRef}
className="min-h-[calc(100%-4rem)]" className="min-h-[calc(100%-4rem)]"
description={roleData[selectRoleIndex].generateText || ''} description={selectedRole?.generateText || ''}
highlight={roleData[selectRoleIndex].tags || []} highlight={selectedRole?.tags || []}
onSmartPolish={handleSmartPolish} onSmartPolish={handleSmartPolish}
onUpdateText={(text: string) => updateRoleText(text)}
/> />
{/* 重新生成按钮、替换形象按钮 */} {/* 重新生成按钮、替换形象按钮 */}
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<motion.button <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 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" text-pink-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
@ -334,12 +361,13 @@ export function CharacterTabContent({
<motion.button <motion.button
onClick={() => handleRegenerate()} onClick={() => handleRegenerate()}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20 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 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
disabled={isRegenerate}
> >
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
<span>Regenerate</span> <span>{isRegenerate ? 'Regenerating...' : 'Regenerate'}</span>
</motion.button> </motion.button>
</div> </div>
</div> </div>
@ -356,8 +384,8 @@ export function CharacterTabContent({
onClose={() => handleCloseReplacePanel()} onClose={() => handleCloseReplacePanel()}
> >
<ReplaceCharacterPanel <ReplaceCharacterPanel
shots={mockShots} shots={shotSelectionList}
character={mockCharacter} character={selectedRole}
showAddToLibrary={showAddToLibrary} showAddToLibrary={showAddToLibrary}
onClose={() => handleCloseReplacePanel()} onClose={() => handleCloseReplacePanel()}
onConfirm={handleConfirmReplace} onConfirm={handleConfirmReplace}

View File

@ -23,7 +23,7 @@ export function HighlightText(props: ReactNodeViewProps) {
<NodeViewWrapper <NodeViewWrapper
as="span" as="span"
data-alt="highlight-text" data-alt="highlight-text"
contentEditable={false} contentEditable={true}
className={`relative inline text-${color}-400 hover:text-${color}-300 transition-colors duration-200`} className={`relative inline text-${color}-400 hover:text-${color}-300 transition-colors duration-200`}
> >
{text} {text}

View File

@ -2,7 +2,7 @@ import { ReplacePanel } from './replace-panel';
import { Shot, Character } from '@/app/model/types'; import { Shot, Character } from '@/app/model/types';
interface ReplaceCharacterPanelProps { interface ReplaceCharacterPanelProps {
shots: Shot[]; shots: any[];
character: Character; character: Character;
showAddToLibrary?: boolean; showAddToLibrary?: boolean;
onClose: () => void; 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({ export function ReplaceCharacterPanel({
shots = mockShots, shots = [],
character = mockCharacter, character,
showAddToLibrary = true, showAddToLibrary = true,
onClose, onClose,
onConfirm, onConfirm,

View File

@ -3,25 +3,10 @@ import { motion } from 'framer-motion';
import { Check, X, CircleAlert, ArrowLeft, ArrowRight } from 'lucide-react'; import { Check, X, CircleAlert, ArrowLeft, ArrowRight } from 'lucide-react';
import { cn } from '@/public/lib/utils'; 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 { interface ReplacePanelProps {
title: string; title: string;
shots: Shot[]; shots: any[];
item: Item; item: any;
showAddToLibrary?: boolean; showAddToLibrary?: boolean;
addToLibraryText?: string; addToLibraryText?: string;
onClose: () => void; onClose: () => void;
@ -175,16 +160,16 @@ export function ReplacePanel({
/> />
)} )}
{!shot.videoUrl && ( {!shot.videoUrl && (
<img <>
src={shot.thumbnailUrl} <img
alt={`Shot ${shot.id}`} src={shot.sketchUrl}
className="w-full h-full object-cover" 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="absolute inset-0 bg-black/50 flex items-center justify-center"> <div className="text-white text-sm">...</div>
<div className="text-white text-sm">...</div> </div>
</div> </>
)} )}
{selectedShots.includes(shot.id) && ( {selectedShots.includes(shot.id) && (
<div className="absolute top-2 right-2"> <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"> <div className="flex items-center gap-4 bg-white/5 rounded-lg p-4">
<img <img
src={item.avatarUrl} src={item.imageUrl}
alt={item.name} alt={item.name}
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />