2025-08-09 10:34:37 +08:00

137 lines
3.6 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, { 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;