forked from 77media/video-flow
重新设计镜头页面
This commit is contained in:
parent
46bda04605
commit
250fa8441e
182
components/ui/shot-editor/ShotEditor copy.tsx
Normal file
182
components/ui/shot-editor/ShotEditor copy.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { EditorContent, useEditor } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { CharacterTokenExtension } from './CharacterToken';
|
||||||
|
import { ShotTitle } from './ShotTitle';
|
||||||
|
import { ReadonlyText } from './ReadonlyText';
|
||||||
|
import { Sparkles } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
const initialContent = {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'shotTitle',
|
||||||
|
attrs: { title: `分镜1` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: '镜头聚焦在' },
|
||||||
|
{ type: 'characterToken', attrs: { name: '"沙利"·沙利文中士', gender: '男', age: '28', avatar: 'https://i.pravatar.cc/40?u=z3' }},
|
||||||
|
{ type: 'text', text: ' 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'shotTitle', attrs: { title: `对话` } },
|
||||||
|
{ type: 'characterToken', attrs: { name: '"沙利"·沙利文中士', gender: '男', age: '28', avatar: 'https://i.pravatar.cc/40?u=z3' }},
|
||||||
|
{ type: 'text', text: ':' },
|
||||||
|
{ type: 'text', text: '掩护!趴下!' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shotTitle',
|
||||||
|
attrs: { title: `分镜2` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'characterToken', attrs: { name: '李四', gender: '女', age: '26', avatar: 'https://i.pravatar.cc/40?u=l4' }},
|
||||||
|
{ type: 'text', text: ' 微微低头,没有说话。' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ShotEditorProps {
|
||||||
|
roles?: any[];
|
||||||
|
onAddSegment?: () => void;
|
||||||
|
onCharacterClick?: (attrs: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
characterToken: {
|
||||||
|
setCharacterToken: (attrs: any) => ReturnType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShotEditor = React.forwardRef<{ addSegment: () => void, onCharacterClick: (attrs: any) => void }, ShotEditorProps>(
|
||||||
|
function ShotEditor({ onAddSegment, onCharacterClick, roles }, ref) {
|
||||||
|
const [segments, setSegments] = useState(initialContent.content);
|
||||||
|
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||||
|
|
||||||
|
const handleSmartPolish = () => {
|
||||||
|
setIsOptimizing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOptimizing(false);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
CharacterTokenExtension.configure({
|
||||||
|
roles
|
||||||
|
}),
|
||||||
|
ShotTitle,
|
||||||
|
ReadonlyText,
|
||||||
|
],
|
||||||
|
content: { type: 'doc', content: segments },
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: 'prose prose-invert max-w-none min-h-[150px] focus:outline-none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediatelyRender: false,
|
||||||
|
onCreate: ({ editor }) => {
|
||||||
|
editor.setOptions({ editable: true })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const addSegment = () => {
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
// 自动编号(获取已有 shotTitle 节点数量)
|
||||||
|
const doc = editor.state.doc;
|
||||||
|
let shotCount = 0;
|
||||||
|
doc.descendants((node) => {
|
||||||
|
if (node.type.name === 'shotTitle' && node.attrs.title.includes('分镜')) {
|
||||||
|
shotCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 不能超过4个分镜
|
||||||
|
if (shotCount >= 4) {
|
||||||
|
toast.error('不能超过4个分镜', {
|
||||||
|
duration: 3000,
|
||||||
|
position: 'top-center',
|
||||||
|
richColors: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.chain().focus('end').insertContent([
|
||||||
|
{
|
||||||
|
type: 'shotTitle',
|
||||||
|
attrs: { title: `分镜${shotCount + 1}` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: '镜头描述' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shotTitle',
|
||||||
|
attrs: { title: `对话` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'characterToken', attrs: { name: '讲话人', gender: '女', age: '26', avatar: 'https://i.pravatar.cc/40?u=l4' }},
|
||||||
|
{ type: 'text', text: ':' },
|
||||||
|
{ type: 'text', text: '讲话内容' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.focus('end') // 聚焦到文档末尾
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// 调用外部传入的回调函数
|
||||||
|
onAddSegment?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
addSegment,
|
||||||
|
onCharacterClick: (attrs: any) => {
|
||||||
|
onCharacterClick?.(attrs);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full relative p-[0.5rem] pb-[2.5rem] border border-white/10 rounded-[0.5rem]">
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
{/* 智能润色按钮 */}
|
||||||
|
<motion.button
|
||||||
|
onClick={handleSmartPolish}
|
||||||
|
disabled={isOptimizing}
|
||||||
|
className="absolute bottom-3 right-3 flex items-center gap-1.5 px-3 py-1.5
|
||||||
|
bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 rounded-full
|
||||||
|
transition-colors text-xs disabled:opacity-50"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-3.5 h-3.5" />
|
||||||
|
<span>{isOptimizing ? "优化中..." : "智能优化"}</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ShotEditor;
|
||||||
@ -47,8 +47,8 @@ const initialContent = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ShotEditorProps {
|
interface ShotEditorProps {
|
||||||
|
content: any[];
|
||||||
roles?: any[];
|
roles?: any[];
|
||||||
onAddSegment?: () => void;
|
|
||||||
onCharacterClick?: (attrs: any) => void;
|
onCharacterClick?: (attrs: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,9 +60,25 @@ declare module '@tiptap/core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShotEditor = React.forwardRef<{ addSegment: () => void, onCharacterClick: (attrs: any) => void }, ShotEditorProps>(
|
interface CharacterToken {
|
||||||
function ShotEditor({ onAddSegment, onCharacterClick, roles }, ref) {
|
type: 'characterToken';
|
||||||
const [segments, setSegments] = useState(initialContent.content);
|
attrs: {
|
||||||
|
name: string;
|
||||||
|
gender: string;
|
||||||
|
age: string;
|
||||||
|
avatar: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditorRef {
|
||||||
|
editor: any;
|
||||||
|
insertCharacter: (character: CharacterToken) => void;
|
||||||
|
insertContent: (content: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShotEditor = React.forwardRef<EditorRef, ShotEditorProps>(
|
||||||
|
function ShotEditor({ content, onCharacterClick, roles }, ref) {
|
||||||
|
const [segments, setSegments] = useState(content);
|
||||||
const [isOptimizing, setIsOptimizing] = useState(false);
|
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||||
|
|
||||||
const handleSmartPolish = () => {
|
const handleSmartPolish = () => {
|
||||||
@ -84,7 +100,7 @@ const ShotEditor = React.forwardRef<{ addSegment: () => void, onCharacterClick:
|
|||||||
content: { type: 'doc', content: segments },
|
content: { type: 'doc', content: segments },
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: 'prose prose-invert max-w-none min-h-[150px] focus:outline-none'
|
class: 'prose prose-invert max-w-none focus:outline-none'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
@ -93,64 +109,18 @@ const ShotEditor = React.forwardRef<{ addSegment: () => void, onCharacterClick:
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const addSegment = () => {
|
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
// 自动编号(获取已有 shotTitle 节点数量)
|
|
||||||
const doc = editor.state.doc;
|
|
||||||
let shotCount = 0;
|
|
||||||
doc.descendants((node) => {
|
|
||||||
if (node.type.name === 'shotTitle' && node.attrs.title.includes('分镜')) {
|
|
||||||
shotCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 不能超过4个分镜
|
|
||||||
if (shotCount >= 4) {
|
|
||||||
toast.error('不能超过4个分镜', {
|
|
||||||
duration: 3000,
|
|
||||||
position: 'top-center',
|
|
||||||
richColors: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.chain().focus('end').insertContent([
|
|
||||||
{
|
|
||||||
type: 'shotTitle',
|
|
||||||
attrs: { title: `分镜${shotCount + 1}` },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
content: [
|
|
||||||
{ type: 'text', text: '镜头描述' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'shotTitle',
|
|
||||||
attrs: { title: `对话` },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
content: [
|
|
||||||
{ type: 'characterToken', attrs: { name: '讲话人', gender: '女', age: '26', avatar: 'https://i.pravatar.cc/40?u=l4' }},
|
|
||||||
{ type: 'text', text: ':' },
|
|
||||||
{ type: 'text', text: '讲话内容' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.focus('end') // 聚焦到文档末尾
|
|
||||||
.run();
|
|
||||||
|
|
||||||
// 调用外部传入的回调函数
|
|
||||||
onAddSegment?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
addSegment,
|
editor,
|
||||||
onCharacterClick: (attrs: any) => {
|
insertCharacter: (character: CharacterToken) => {
|
||||||
onCharacterClick?.(attrs);
|
editor?.commands.insertContent([
|
||||||
|
{ type: 'text', text: ' ' },
|
||||||
|
character,
|
||||||
|
{ type: 'text', text: ' ' }
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
insertContent: (content: any) => {
|
||||||
|
editor?.commands.insertContent(content);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -159,22 +129,7 @@ const ShotEditor = React.forwardRef<{ addSegment: () => void, onCharacterClick:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full relative p-[0.5rem] pb-[2.5rem] border border-white/10 rounded-[0.5rem]">
|
<EditorContent editor={editor} />
|
||||||
<EditorContent editor={editor} />
|
|
||||||
{/* 智能润色按钮 */}
|
|
||||||
<motion.button
|
|
||||||
onClick={handleSmartPolish}
|
|
||||||
disabled={isOptimizing}
|
|
||||||
className="absolute bottom-3 right-3 flex items-center gap-1.5 px-3 py-1.5
|
|
||||||
bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 rounded-full
|
|
||||||
transition-colors text-xs disabled:opacity-50"
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
|
||||||
<Sparkles className="w-3.5 h-3.5" />
|
|
||||||
<span>{isOptimizing ? "优化中..." : "智能优化"}</span>
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
271
components/ui/shot-editor/ShotsEditor.tsx
Normal file
271
components/ui/shot-editor/ShotsEditor.tsx
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import React, { forwardRef, useRef, useState } from "react";
|
||||||
|
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
||||||
|
import ShotEditor from "./ShotEditor";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Shot {
|
||||||
|
id: string;
|
||||||
|
shotDescContent: any[];
|
||||||
|
shotDialogsContent: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CharacterToken {
|
||||||
|
type: 'characterToken';
|
||||||
|
attrs: {
|
||||||
|
name: string;
|
||||||
|
gender: string;
|
||||||
|
age: string;
|
||||||
|
avatar: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockShotsData = [
|
||||||
|
{
|
||||||
|
id: '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 => ({
|
||||||
|
id: `shot${Date.now()}`,
|
||||||
|
shotDescContent: [{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: '在这里添加分镜描述...' }
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
shotDialogsContent: [{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: '在这里添加分镜对话...' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ShotsEditorProps {
|
||||||
|
roles: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShotsEditor = forwardRef<any, ShotsEditorProps>(({ roles }, ref) => {
|
||||||
|
const [currentShotIndex, setCurrentShotIndex] = useState(0);
|
||||||
|
const [shots, setShots] = useState<Shot[]>(mockShotsData);
|
||||||
|
const descEditorRef = useRef<any>(null);
|
||||||
|
const dialogEditorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* 分镜标签(可删除)、新增分镜标签 */}
|
||||||
|
<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.id}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* 分镜内容 */}
|
||||||
|
<div className="flex flex-col gap-3 border border-white/10 p-2 rounded-[0.5rem]" key={currentShotIndex}>
|
||||||
|
{/* 分镜描述 添加角色 */}
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -9,7 +9,7 @@ import { ReplaceVideoModal } from './replace-video-modal';
|
|||||||
import { MediaPropertiesModal } from './media-properties-modal';
|
import { MediaPropertiesModal } from './media-properties-modal';
|
||||||
import { DramaLineChart } from './drama-line-chart';
|
import { DramaLineChart } from './drama-line-chart';
|
||||||
import { PersonDetection, PersonDetectionScene } from './person-detection';
|
import { PersonDetection, PersonDetectionScene } from './person-detection';
|
||||||
import ShotEditor from './shot-editor/ShotEditor';
|
import { ShotsEditor } from './shot-editor/ShotsEditor';
|
||||||
import { CharacterLibrarySelector } from './character-library-selector';
|
import { CharacterLibrarySelector } from './character-library-selector';
|
||||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||||
@ -43,6 +43,8 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
const [shots, setShots] = useState<any[]>([]);
|
const [shots, setShots] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const shotsEditorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
|
||||||
// 监听外部播放状态变化
|
// 监听外部播放状态变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -123,6 +125,12 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 新增分镜
|
||||||
|
const handleAddShot = () => {
|
||||||
|
console.log('add shot');
|
||||||
|
shotsEditorRef.current.addShot();
|
||||||
|
};
|
||||||
|
|
||||||
// 切换选择分镜
|
// 切换选择分镜
|
||||||
const handleSelectShot = (index: number) => {
|
const handleSelectShot = (index: number) => {
|
||||||
// 切换前 判断数据是否发生变化
|
// 切换前 判断数据是否发生变化
|
||||||
@ -326,23 +334,15 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
{/* 基础配置 */}
|
{/* 基础配置 */}
|
||||||
<div className='space-y-4 col-span-1'>
|
<div className='space-y-4 col-span-1'>
|
||||||
<ShotEditor
|
<ShotsEditor
|
||||||
ref={editorRef}
|
ref={shotsEditorRef}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
onAddSegment={() => {
|
|
||||||
// 可以在这里添加其他逻辑
|
|
||||||
console.log('分镜添加成功');
|
|
||||||
}}
|
|
||||||
onCharacterClick={(attrs) => {
|
|
||||||
console.log('attrs', attrs);
|
|
||||||
setIsReplaceLibraryOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 重新生成按钮、新增分镜按钮 */}
|
{/* 重新生成按钮、新增分镜按钮 */}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => editorRef.current?.addSegment()}
|
onClick={() => handleAddShot()}
|
||||||
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 }}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user