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 { 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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} />
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* 智能润色按钮 */}
|
{/* 智能润色按钮 */}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
<img
|
||||||
src={shot.thumbnailUrl}
|
src={shot.sketchUrl}
|
||||||
alt={`Shot ${shot.id}`}
|
alt={`Shot ${shot.id}`}
|
||||||
className="w-full h-full object-cover"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user