forked from 77media/video-flow
125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
import React, { useState } 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';
|
|
|
|
const initialContent = {
|
|
type: 'doc',
|
|
content: [
|
|
{
|
|
type: 'shotTitle',
|
|
attrs: { title: `分镜1` },
|
|
},
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{ type: 'characterToken', attrs: { name: '张三', gender: '男', age: '28', avatar: 'https://i.pravatar.cc/40?u=z3' }},
|
|
{ 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 {
|
|
onAddSegment?: () => void;
|
|
}
|
|
|
|
const ShotEditor = React.forwardRef<{ addSegment: () => void }, ShotEditorProps>(function ShotEditor({ onAddSegment }, ref) {
|
|
const [segments, setSegments] = useState(initialContent.content);
|
|
|
|
const editor = useEditor({
|
|
extensions: [
|
|
StarterKit,
|
|
CharacterTokenExtension,
|
|
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 === 'paragraph') {
|
|
shotCount++;
|
|
}
|
|
});
|
|
|
|
editor.chain().focus('end').insertContent([
|
|
{
|
|
type: 'shotTitle',
|
|
attrs: { title: `分镜${shotCount + 1}` },
|
|
},
|
|
{
|
|
type: 'paragraph',
|
|
content: [
|
|
{ type: 'text', text: '镜头描述' }
|
|
]
|
|
}
|
|
])
|
|
.focus('end') // 聚焦到文档末尾
|
|
.run();
|
|
|
|
// 调用外部传入的回调函数
|
|
onAddSegment?.();
|
|
};
|
|
|
|
// 暴露 addSegment 方法给父组件
|
|
React.useImperativeHandle(ref, () => ({
|
|
addSegment
|
|
}));
|
|
|
|
if (!editor) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="w-full max-w-3xl mx-auto relative p-[0.5rem] pb-[2.5rem] border border-white/10 rounded-[0.5rem]">
|
|
<EditorContent editor={editor} />
|
|
{/* <motion.button
|
|
onClick={addSegment}
|
|
className="group absolute bottom-[0.5rem] h-8 rounded-full bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 flex items-center justify-center overflow-hidden"
|
|
initial={{ width: "2rem" }}
|
|
whileHover={{
|
|
width: "8rem",
|
|
transition: { duration: 0.3, ease: "easeInOut" }
|
|
}}
|
|
>
|
|
<motion.div
|
|
className="flex items-center justify-center space-x-1 px-2 h-full">
|
|
<span className="text-lg">+</span>
|
|
<span className="text-sm group-hover:opacity-100 opacity-0 transition-all duration-500 w-0 group-hover:w-auto">新增分镜</span>
|
|
</motion.div>
|
|
</motion.button> */}
|
|
</div>
|
|
)
|
|
});
|
|
|
|
export default ShotEditor; |