2025-08-18 21:42:26 +08:00

298 lines
9.1 KiB
TypeScript

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 { toast } from "sonner";
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) {
toast.error('No more than 4 shots', {
duration: 3000,
position: 'top-center',
richColors: true,
});
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: 'text', text: ' says:' }
]
};
// 在编辑器末尾添加新对话
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>
);
});