forked from 77media/video-flow
298 lines
9.2 KiB
TypeScript
298 lines
9.2 KiB
TypeScript
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
|
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 mockShotsData = [
|
|
{
|
|
name: 'shot1',
|
|
shotDescContent: [{
|
|
type: 'paragraph',
|
|
content: [
|
|
{ type: 'text', text: '镜头聚焦在' },
|
|
{ type: 'characterToken', attrs: { name: '"沙利"·沙利文中士', gender: '男', age: '28', avatar: 'https://i.pravatar.cc/40?u=z3' }},
|
|
{ type: 'text', text: ' 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。' },
|
|
]
|
|
}],
|
|
shotDialogsContent: [
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{ type: 'characterToken', attrs: { name: '李四', gender: '女', age: '26', avatar: 'https://i.pravatar.cc/40?u=l4' }},
|
|
{ type: 'text', text: ' 微微低头,没有说话。' }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
const createEmptyShot = (): Shot => ({
|
|
name: `shot${Date.now()}`,
|
|
shotDescContent: [{
|
|
type: 'paragraph',
|
|
content: [
|
|
{ type: 'text', text: '在这里添加分镜描述...' }
|
|
]
|
|
}],
|
|
shotDialogsContent: [{
|
|
type: 'paragraph',
|
|
content: [
|
|
{ type: 'text', text: '在这里添加分镜对话...' }
|
|
]
|
|
}]
|
|
});
|
|
|
|
interface ShotsEditorProps {
|
|
roles: any[];
|
|
shotInfo: any[];
|
|
style?: React.CSSProperties;
|
|
}
|
|
|
|
export const ShotsEditor = forwardRef<any, ShotsEditorProps>(({ roles, shotInfo, style }, ref) => {
|
|
const [currentShotIndex, setCurrentShotIndex] = useState(0);
|
|
const [shots, setShots] = useState<Shot[]>([]);
|
|
const descEditorRef = useRef<any>(null);
|
|
const dialogEditorRef = useRef<any>(null);
|
|
|
|
useEffect(() => {
|
|
console.log('-==========shotInfo===========-', shotInfo);
|
|
if (shotInfo) {
|
|
const shots = shotInfo.map((shot) => {
|
|
return TextToShotAdapter.fromLensType(shot, roles);
|
|
});
|
|
console.log('-==========shots===========-', shots);
|
|
setShots(shots as Shot[]);
|
|
}
|
|
}, [shotInfo]);
|
|
|
|
const getShotInfo = () => {
|
|
console.log('-==========shots===========-', shots);
|
|
const shotInfo = shots.map((shot) => {
|
|
return TextToShotAdapter.toLensType(shot);
|
|
});
|
|
return shotInfo;
|
|
}
|
|
|
|
const addShot = () => {
|
|
if (shots.length > 3) {
|
|
toast.error('不能超过4个分镜', {
|
|
duration: 3000,
|
|
position: 'top-center',
|
|
richColors: true,
|
|
});
|
|
return;
|
|
}
|
|
const newShot = createEmptyShot();
|
|
setShots([...shots, newShot]);
|
|
// onShotsChange([...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: '新角色',
|
|
gender: '男',
|
|
age: '25',
|
|
avatar: 'https://i.pravatar.cc/40'
|
|
}
|
|
};
|
|
|
|
// 在当前位置插入角色Token
|
|
descEditorRef.current.insertCharacter(defaultCharacter);
|
|
};
|
|
|
|
const handleAddCharacterToDialog = () => {
|
|
if (!dialogEditorRef.current) return;
|
|
|
|
// 创建一个默认角色Token
|
|
const defaultCharacter: CharacterToken = {
|
|
type: 'characterToken',
|
|
attrs: {
|
|
name: '新角色',
|
|
gender: '男',
|
|
age: '25',
|
|
avatar: 'https://i.pravatar.cc/40'
|
|
}
|
|
};
|
|
|
|
// 在当前位置插入角色Token
|
|
dialogEditorRef.current.insertCharacter(defaultCharacter);
|
|
};
|
|
|
|
const handleAddNewDialog = () => {
|
|
if (!dialogEditorRef.current) return;
|
|
|
|
// 创建一个新的对话行
|
|
const newDialog = {
|
|
type: 'paragraph',
|
|
content: [
|
|
{
|
|
type: 'characterToken',
|
|
attrs: {
|
|
name: '新角色',
|
|
gender: '男',
|
|
age: '25',
|
|
avatar: 'https://i.pravatar.cc/40'
|
|
}
|
|
},
|
|
{ type: 'text', 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={() => setCurrentShotIndex(index)}
|
|
>
|
|
<span className="text-sm font-medium">镜头{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 text-red-400 hidden group-hover:block transition-all" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* <button
|
|
data-alt="add-shot-button"
|
|
onClick={handleAddShot}
|
|
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" />
|
|
新增镜头
|
|
</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">分镜描述</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}
|
|
/>
|
|
</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">分镜对话</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}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}); |