forked from 77media/video-flow
88 lines
2.3 KiB
TypeScript
88 lines
2.3 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;
|
|
}
|
|
|
|
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)
|
|
alert(`点击角色:${name}`)
|
|
}
|
|
|
|
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)];
|
|
},
|
|
|
|
addNodeView() {
|
|
return ReactNodeViewRenderer(CharacterToken);
|
|
},
|
|
});
|