diff --git a/components/ui/CharacterToken.tsx b/components/ui/CharacterToken.tsx new file mode 100644 index 0000000..3ddd8c8 --- /dev/null +++ b/components/ui/CharacterToken.tsx @@ -0,0 +1,81 @@ +import { Node, mergeAttributes } from '@tiptap/core' +import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' +import { motion, AnimatePresence } from 'framer-motion' +import { useState } from 'react' + +export const CharacterToken = Node.create({ + name: 'characterToken', + group: 'inline', + inline: true, + atom: true, + selectable: true, + + addAttributes() { + return { + id: { default: null }, + name: { default: '角色名' }, + avatar: { default: '' }, + gender: { default: '未知' }, + age: { default: '-' }, + } + }, + + parseHTML() { + return [{ tag: 'span[data-character]' }] + }, + + renderHTML({ HTMLAttributes }) { + return ['span', mergeAttributes({ 'data-character': '' }, HTMLAttributes), HTMLAttributes.name] + }, + + addNodeView() { + return ReactNodeViewRenderer(CharacterView) + }, +}) + +function CharacterView({ node }) { + const [showCard, setShowCard] = useState(false) + const { name, avatar, gender, age } = node.attrs + + const handleClick = () => { + console.log('点击角色:', name) + alert(`点击角色:${name}`) + } + + return ( + setShowCard(true)} + onMouseLeave={() => setShowCard(false)} + onClick={handleClick} + > + {name} + + + {showCard && ( + +
+ {name} +
+
{name}
+
{gender} / {age}
+
+
+
+ )} +
+
+ ) +} diff --git a/components/ui/ImageBlurTransition.tsx b/components/ui/ImageBlurTransition.tsx index c1c395b..361a199 100644 --- a/components/ui/ImageBlurTransition.tsx +++ b/components/ui/ImageBlurTransition.tsx @@ -29,7 +29,7 @@ export default function ImageBlurTransition({ src, alt = '', width = 480, height return (
{/* 应用角色按钮 */}
diff --git a/components/ui/edit-modal.tsx b/components/ui/edit-modal.tsx index 42f84c0..03a13fc 100644 --- a/components/ui/edit-modal.tsx +++ b/components/ui/edit-modal.tsx @@ -6,7 +6,7 @@ import { X, Image, Users, Video, Music, Settings, FileText, Maximize, Minimize } import { cn } from '@/public/lib/utils'; import ScriptTabContent from './script-tab-content'; import { SceneTabContent } from './scene-tab-content'; -import { VideoTabContent } from './video-tab-content'; +import { ShotTabContent } from './shot-tab-content'; import { SettingsTabContent } from './settings-tab-content'; import { CharacterTabContent } from './character-tab-content'; import { MusicTabContent } from './music-tab-content'; @@ -110,7 +110,7 @@ export function EditModal({ ); case '3': return ( - (({ person }, ref) => { + return ( + + + ); +}); + +PersonBox.displayName = 'PersonBox'; + +export type PersonDetection = { + id: string; + name: string; + position: { + top: number; + left: number; + width: number; + height: number; + }; +}; + +type Props = { + backgroundImage?: string; + videoSrc?: string; + detections: PersonDetection[]; + triggerScan: boolean; + onScanStart?: (currentTime?: number) => void; + onScanTimeout?: () => void; + onScanExit?: () => void; // 扫描退出回调 + scanTimeout?: number; + isScanFailed?: boolean; // 外部传入的失败状态 + onDetectionsChange?: (detections: PersonDetection[]) => void; +}; + +export const PersonDetectionScene: React.FC = ({ + backgroundImage, + videoSrc, + detections, + triggerScan, + onScanStart, + onScanTimeout, + onScanExit, + scanTimeout = 10000, + isScanFailed = false, + onDetectionsChange +}) => { + const scanControls = useAnimation(); + const videoRef = useRef(null); + const [scanStatus, setScanStatus] = useState<'idle' | 'scanning' | 'timeout' | 'failed'>('idle'); + const timeoutRef = useRef(); + const exitTimeoutRef = useRef(); + + + // 处理扫描状态 + useEffect(() => { + if (triggerScan && detections.length === 0) { + setScanStatus('scanning'); + // 设置超时定时器 + timeoutRef.current = setTimeout(() => { + setScanStatus('timeout'); + onScanTimeout?.(); + }, scanTimeout); + } else { + // 清除所有定时器 + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + if (exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current); + } + setScanStatus('idle'); + } + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + if (exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current); + } + }; + }, [triggerScan, detections.length, scanTimeout, onScanTimeout]); + + // 处理外部失败状态 + useEffect(() => { + if (isScanFailed && scanStatus === 'scanning') { + setScanStatus('failed'); + } + }, [isScanFailed, scanStatus]); + + // 处理失败/超时自动退出 + useEffect(() => { + if (scanStatus === 'timeout' || scanStatus === 'failed') { + exitTimeoutRef.current = setTimeout(() => { + setScanStatus('idle'); + onScanExit?.(); + }, 3000); + } + return () => { + if (exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current); + } + }; + }, [scanStatus, onScanExit]); + + // 处理视频/图片源变化 + useEffect(() => { + setScanStatus('idle'); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + if (exitTimeoutRef.current) { + clearTimeout(exitTimeoutRef.current); + } + }, [videoSrc, backgroundImage]); + + useEffect(() => { + if (scanStatus !== 'idle') { + if (videoRef.current) { + videoRef.current.pause(); + onScanStart?.(videoRef.current.currentTime); + } else { + onScanStart?.(); + } + + scanControls.start({ + opacity: [0, 1, 1, 0], + scale: [0.98, 1, 1, 0.98], + y: ["-120%", "120%"], + transition: { + duration: 3, + repeat: Infinity, + repeatType: "loop", + times: [0, 0.2, 0.8, 1], + ease: "easeInOut" + } + }); + } else { + scanControls.stop(); + } + }, [triggerScan, scanStatus, scanControls, onScanStart]); + + // 监听检测结果变化 + useEffect(() => { + onDetectionsChange?.(detections); + }, [detections, onDetectionsChange]); + + const isShowError = scanStatus === 'timeout' || scanStatus === 'failed'; + const isShowScan = scanStatus !== 'idle'; + + return ( +
+ {/* 背景层 */} + {backgroundImage && ( +
+ )} + {videoSrc && ( +