2025-09-06 16:58:21 +08:00

293 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { forwardRef, useRef, useState } from "react";
import { useDeepCompareEffect } from "@/hooks/useDeepCompareEffect";
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
import ShotEditor from "./ShotEditor";
import { TextToShotAdapter } from "@/app/service/adapter/textToShot";
interface Shot {
name: string;
shotDescContent: any[];
shotDialogsContent: any[];
}
interface CharacterToken {
type: 'characterToken';
attrs: {
name: string;
gender: string;
age: string;
avatar: string;
};
}
const createEmptyShot = (): Shot => ({
name: `shot${Date.now()}`,
shotDescContent: [{
type: 'paragraph',
content: []
}],
shotDialogsContent: [{
type: 'paragraph',
content: []
}]
});
interface ShotsEditorProps {
roles: any[];
shotInfo: any[];
style?: React.CSSProperties;
}
export const ShotsEditor = forwardRef<any, ShotsEditorProps>(function ShotsEditor({ roles, shotInfo, style }, ref) {
const [currentShotIndex, setCurrentShotIndex] = useState(0);
const [shots, setShots] = useState<Shot[]>([]);
const descEditorRef = useRef<any>(null);
const dialogEditorRef = useRef<any>(null);
useDeepCompareEffect(() => {
if (shotInfo) {
console.log('-==========shotInfo===========-', shotInfo);
const shots = shotInfo.map((shot) => {
return TextToShotAdapter.fromLensType(shot, roles);
});
console.log('-==========shots===========-', shots);
setShots(shots as Shot[]);
}
}, [shotInfo, roles]);
const handleDescContentChange = (content: any) => {
const shot = shots[currentShotIndex];
shot.shotDescContent = content;
setShots(prevShots =>
prevShots.map((item, index) =>
index === currentShotIndex ? shot : item
)
);
}
const handleDialogContentChange = (content: any) => {
const shot = shots[currentShotIndex];
shot.shotDialogsContent = content;
setShots(prevShots =>
prevShots.map((item, index) =>
index === currentShotIndex ? shot : item
)
);
}
const handleShotTabClick = (index: number) => {
setCurrentShotIndex(index);
}
const getShotInfo = () => {
console.log('-==========getShotInfo shots===========-', shots);
const shotInfo = shots.map((shot) => {
return TextToShotAdapter.toLensType(shot);
});
return shotInfo;
}
const addShot = () => {
if (shots.length > 3) {
window.msg.error('max 4 shots');
return;
}
const newShot = createEmptyShot();
setShots([...shots, newShot]);
// 自动切换到新创建的分镜
setCurrentShotIndex(shots.length);
};
const handleDeleteShot = (index: number) => {
if (shots.length <= 1) return; // 保留最后一个分镜
const newShots = shots.filter((_, i) => i !== index);
setShots(newShots);
// onShotsChange(newShots);
// 如果删除的是当前选中的分镜,或者删除的是最后一个分镜
if (currentShotIndex === index || currentShotIndex >= newShots.length) {
setCurrentShotIndex(Math.max(0, newShots.length - 1));
} else if (currentShotIndex > index) {
// 如果删除的分镜在当前选中分镜之前,需要更新索引
setCurrentShotIndex(currentShotIndex - 1);
}
};
const handleAddCharacterToDesc = () => {
if (!descEditorRef.current) return;
// 创建一个默认角色Token
const defaultCharacter: CharacterToken = {
type: 'characterToken',
attrs: {
name: 'Select Role',
gender: '',
age: '',
avatar: ''
}
};
// 在当前位置插入角色Token
descEditorRef.current.insertCharacter(defaultCharacter);
};
const handleAddCharacterToDialog = () => {
if (!dialogEditorRef.current) return;
// 创建一个默认角色Token
const defaultCharacter: CharacterToken = {
type: 'characterToken',
attrs: {
name: 'Select Role',
gender: '',
age: '',
avatar: ''
}
};
// 在当前位置插入角色Token
dialogEditorRef.current.insertCharacter(defaultCharacter);
};
const handleAddNewDialog = () => {
if (!dialogEditorRef.current) return;
// 创建一个新的对话行
const newDialog = {
type: 'paragraph',
content: [
{
type: 'characterToken',
attrs: {
name: 'Select Role',
gender: '',
age: '',
avatar: ''
}
},
{ type: 'readonlyText', attrs: { text: '' } }
]
};
// 在编辑器末尾添加新对话
dialogEditorRef.current.editor?.commands.focus('end');
dialogEditorRef.current.insertContent([
{ type: 'text', text: '\n' },
newDialog
]);
};
// 暴露方法给父组件
React.useImperativeHandle(ref, () => ({
addShot,
getShotInfo
}));
return (
<div className="flex flex-col gap-2" style={style}>
{/* 分镜标签(可删除)、新增分镜标签 */}
<div data-alt="shots-tabs" className="flex items-center gap-2">
<div className="flex gap-2 flex-wrap">
{shots.map((shot, index) => (
<div
key={shot.name}
data-alt="shot-tab"
className={`group flex items-center gap-2 px-3 py-1.5 rounded-md cursor-pointer transition-all
${currentShotIndex === index
? 'bg-blue-500/10 text-blue-500'
: 'text-white/60 bg-white/5'
}`}
onClick={() => handleShotTabClick(index)}
>
<span className="text-sm font-medium">Shot {index + 1}</span>
{shots.length > 1 && (
<button
className="p-0.5 rounded hover:bg-white/10 transition-colors"
onClick={(e) => {
e.stopPropagation();
handleDeleteShot(index);
}}
>
<X className="w-3.5 h-3.5 hover:text-red-400" />
</button>
)}
</div>
))}
</div>
<button
data-alt="add-shot-button"
onClick={addShot}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white/60 hover:text-white hover:bg-white/5 rounded-md transition-colors"
>
<Plus className="w-4 h-4" />
Add
</button>
</div>
{/* 分镜内容 */}
{shots[currentShotIndex] && (
<div className="flex flex-col gap-3 border border-white/10 p-2 rounded-[0.5rem]" key={currentShotIndex} style={{minHeight: 'calc(100% - 2rem)'}}>
{/* 分镜描述 添加角色 */}
<div data-alt="shot-description-section" className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<ClipboardType className="w-5 h-5 text-white/60" />
<span className="text-lg font-medium text-white/60">Shot Description</span>
<button
data-alt="add-character-desc"
className="p-1 rounded-md"
onClick={() => handleAddCharacterToDesc()}
>
<UserRoundPlus className="w-5 h-5 text-blue-600" />
</button>
</div>
{/* 分镜描述内容 可视化编辑 */}
<ShotEditor
ref={descEditorRef}
content={shots[currentShotIndex].shotDescContent}
onCharacterClick={() => {}}
roles={roles}
placeholder="Add shot description here..."
onChangeContent={(content) => {handleDescContentChange(content)}}
/>
</div>
{/* 分镜对话 添加角色 添加对话 */}
<div data-alt="shot-dialog-section" className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<MessageCircleMore className="w-5 h-5 text-white/60" />
<span className="text-lg font-medium text-white/60">Shot Dialogue</span>
<button
data-alt="add-character-dialog"
className="p-1 rounded-md"
onClick={() => handleAddCharacterToDialog()}
>
<UserRoundPlus className="w-5 h-5 text-blue-600" />
</button>
<button
data-alt="add-new-dialog"
className="p-1 rounded-md"
onClick={() => handleAddNewDialog()}
>
<MessageCirclePlus className="w-5 h-5 text-blue-600" />
</button>
</div>
{/* 分镜对话内容 可视化编辑 */}
<ShotEditor
ref={dialogEditorRef}
content={shots[currentShotIndex].shotDialogsContent}
onCharacterClick={() => {}}
roles={roles}
placeholder="Add shot dialogue here..."
onChangeContent={(content) => {handleDialogContentChange(content)}}
/>
</div>
</div>
)}
</div>
);
});