forked from 77media/video-flow
137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
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 {
|
||
content: any[];
|
||
roles?: any[];
|
||
onCharacterClick?: (attrs: any) => void;
|
||
}
|
||
|
||
declare module '@tiptap/core' {
|
||
interface Commands<ReturnType> {
|
||
characterToken: {
|
||
setCharacterToken: (attrs: any) => ReturnType;
|
||
}
|
||
}
|
||
}
|
||
|
||
interface CharacterToken {
|
||
type: 'characterToken';
|
||
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 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 focus:outline-none'
|
||
}
|
||
},
|
||
immediatelyRender: false,
|
||
onCreate: ({ editor }) => {
|
||
editor.setOptions({ editable: true })
|
||
},
|
||
})
|
||
|
||
// 暴露方法给父组件
|
||
React.useImperativeHandle(ref, () => ({
|
||
editor,
|
||
insertCharacter: (character: CharacterToken) => {
|
||
editor?.commands.insertContent([
|
||
{ type: 'text', text: ' ' },
|
||
character,
|
||
{ type: 'text', text: ' ' }
|
||
]);
|
||
},
|
||
insertContent: (content: any) => {
|
||
editor?.commands.insertContent(content);
|
||
}
|
||
}));
|
||
|
||
if (!editor) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<EditorContent editor={editor} />
|
||
)
|
||
}
|
||
);
|
||
|
||
export default ShotEditor; |