video-flow-b/components/ui/shot-editor/ShotEditor copy.tsx
2025-08-09 10:34:37 +08:00

182 lines
5.2 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 {
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;