video-flow-b/components/ui/shot-editor/CharacterToken.tsx
2025-08-01 13:30:50 +08:00

99 lines
2.7 KiB
TypeScript

import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer, NodeViewWrapper, ReactNodeViewProps } from '@tiptap/react'
import { motion, AnimatePresence } from 'framer-motion'
import { useState } from 'react'
interface CharacterAttributes {
id: string | null;
name: string;
avatar: string;
gender: string;
age: string;
}
// interface CharacterTokenProps extends ReactNodeViewProps {
// onClick?: (attrs: CharacterAttributes) => void
// }
export function CharacterToken(props: ReactNodeViewProps) {
const [showCard, setShowCard] = useState(false)
const { name, avatar, gender, age } = props.node.attrs as CharacterAttributes
const handleClick = () => {
console.log('点击角色:', name)
const { editor } = props;
editor?.emit('character-clicked', props.node.attrs);
}
return (
<NodeViewWrapper
as="span"
contentEditable={false}
className="relative inline text-blue-400 cursor-pointer hover:text-blue-300 transition-colors duration-200"
onMouseEnter={() => setShowCard(true)}
onMouseLeave={() => setShowCard(false)}
onClick={handleClick}
>
{name}
<AnimatePresence>
{showCard && (
<motion.div
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.2 }}
className="absolute top-full left-0 mt-2 w-64 rounded-lg bg-gray-900 border border-gray-800 shadow-2xl p-4 z-50"
>
<div className="flex items-center gap-3">
<img
src={avatar || 'https://placekitten.com/64/64'}
alt={name}
className="w-12 h-12 rounded-full border"
/>
<div>
<div className="font-medium text-base text-gray-200">{name}</div>
<div className="text-sm text-gray-400">{gender} / {age}</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</NodeViewWrapper>
)
}
export const CharacterTokenExtension = Node.create({
name: 'characterToken',
group: 'inline',
inline: true,
atom: true,
addAttributes() {
return {
name: {},
gender: {},
age: {},
avatar: {},
};
},
parseHTML() {
return [{ tag: 'character-token' }];
},
renderHTML({ HTMLAttributes }) {
return ['character-token', mergeAttributes(HTMLAttributes)];
},
// addStorage() {
// return {
// onClickCharacter: null as null | ((character: CharacterAttributes) => void),
// }
// },
addNodeView() {
return ReactNodeViewRenderer(CharacterToken);
},
});