diff --git a/components/ui/FloatingGlassPanel.tsx b/components/ui/FloatingGlassPanel.tsx index fcf8496..0564a75 100644 --- a/components/ui/FloatingGlassPanel.tsx +++ b/components/ui/FloatingGlassPanel.tsx @@ -9,9 +9,10 @@ type FloatingGlassPanelProps = { children: ReactNode; width?: string; r_key?: string | number; + panel_style?: React.CSSProperties; }; -export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key }: FloatingGlassPanelProps) { +export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key, panel_style }: FloatingGlassPanelProps) { // 定义弹出动画 const bounceAnimation = { scale: [0.95, 1.02, 0.98, 1], @@ -48,7 +49,7 @@ export default function FloatingGlassPanel({ open, onClose, children, width = '3 }} >
{children} diff --git a/components/ui/HorizontalScroller.tsx b/components/ui/HorizontalScroller.tsx new file mode 100644 index 0000000..4bd65f8 --- /dev/null +++ b/components/ui/HorizontalScroller.tsx @@ -0,0 +1,172 @@ +import React, { + useRef, + useEffect, + useImperativeHandle, + useCallback, + forwardRef, + ReactNode, + Ref, + useState, +} from 'react' +import gsap from 'gsap' +import { Draggable } from 'gsap/Draggable' +import { motion } from 'framer-motion' + +gsap.registerPlugin(Draggable) + +export interface HorizontalScrollerProps { + children: ReactNode[] + itemWidth?: number | 'auto' + gap?: number + selectedIndex?: number + onItemClick?: (index: number) => void +} + +export interface HorizontalScrollerRef { + scrollToIndex: (index: number) => void +} + +const HorizontalScroller = forwardRef( + ( + { + children, + itemWidth = 300, + gap = 16, + selectedIndex, + onItemClick, + }: HorizontalScrollerProps, + ref: Ref + ) => { + const containerRef = useRef(null) + const wrapperRef = useRef(null) + const dragInstance = useRef(null) + const itemRefs = useRef<(HTMLDivElement | null)[]>([]) + const [currentIndex, setCurrentIndex] = useState(selectedIndex || 0) + + const scrollToIndex = useCallback( + (index: number) => { + if (!containerRef.current || !wrapperRef.current || index < 0 || index >= children.length) return + + const containerWidth = containerRef.current.offsetWidth + let targetX = 0 + + if (itemWidth === 'auto') { + const itemElement = itemRefs.current[index] + if (!itemElement) return + + const itemRect = itemElement.getBoundingClientRect() + const wrapperRect = wrapperRef.current.getBoundingClientRect() + targetX = -(itemElement.offsetLeft - (containerWidth - itemRect.width) / 2) + } else { + targetX = -(index * (itemWidth + gap) - (containerWidth - itemWidth) / 2) + } + + const maxScroll = wrapperRef.current.scrollWidth - containerWidth + targetX = Math.max(-maxScroll, Math.min(targetX, 0)) + + gsap.to(wrapperRef.current, { + x: targetX, + duration: 0.6, + ease: 'power3.out', + onUpdate: () => { + if (dragInstance.current?.update) { + dragInstance.current.update() + } + }, + }) + + setCurrentIndex(index) + }, + [itemWidth, gap, children.length] + ) + + useEffect(() => { + if (!wrapperRef.current || !containerRef.current) return + + const maxScroll = + wrapperRef.current.scrollWidth - containerRef.current.offsetWidth + + dragInstance.current = Draggable.create(wrapperRef.current, { + type: 'x', + edgeResistance: 0.85, + inertia: true, + bounds: { + minX: -maxScroll, + maxX: 0, + }, + onDrag: function () { + gsap.to(wrapperRef.current!, { + x: this.x, + duration: 0.3, + ease: 'power3.out', + }) + }, + })[0] + }, []) + + useEffect(() => { + if (typeof selectedIndex === 'number') { + scrollToIndex(selectedIndex) + } + }, [selectedIndex, scrollToIndex]) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft') { + e.preventDefault() + let newIndex = Math.max(0, currentIndex - 1) + if (newIndex === 0 && currentIndex - 1 < 0) { + newIndex = children.length - 1 + } + onItemClick?.(newIndex) + } else if (e.key === 'ArrowRight') { + e.preventDefault() + let newIndex = Math.min(children.length - 1, currentIndex + 1) + if (newIndex === children.length - 1 && currentIndex + 1 > children.length - 1) { + newIndex = 0 + } + onItemClick?.(newIndex) + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [currentIndex, children.length, scrollToIndex, onItemClick]) + + useImperativeHandle(ref, () => ({ + scrollToIndex, + })) + + return ( +
+
+ {React.Children.map(children, (child, index) => ( + { + onItemClick?.(index) + }} + ref={(el) => (itemRefs.current[index] = el)} + style={{ + minWidth: itemWidth === 'auto' ? 'auto' : itemWidth, + flexShrink: 0, + }} + className={`p-2 rounded-lg overflow-hidden cursor-pointer hover:scale-[1.03] transition-transform duration-300`} + > + {child} + + ))} +
+
+ ) + } +) + +export default HorizontalScroller diff --git a/components/ui/ImageBlurTransition.tsx b/components/ui/ImageBlurTransition.tsx index 361a199..84bb73a 100644 --- a/components/ui/ImageBlurTransition.tsx +++ b/components/ui/ImageBlurTransition.tsx @@ -10,9 +10,17 @@ type ImageBlurTransitionProps = { alt?: string; width?: number | string; height?: number | string; + enableAnimation?: boolean; }; -export default function ImageBlurTransition({ src, alt = '', width = 480, height = 300, className }: ImageBlurTransitionProps) { +export default function ImageBlurTransition({ + src, + alt = '', + width = 480, + height = 300, + className, + enableAnimation = true +}: ImageBlurTransitionProps) { const [current, setCurrent] = useState(src); const [isFlipping, setIsFlipping] = useState(false); @@ -33,36 +41,44 @@ export default function ImageBlurTransition({ src, alt = '', width = 480, height style={{ width, height, - perspective: 1000, // 关键:提供 3D 深度 + perspective: enableAnimation ? 1000 : 'none', // 只在启用动画时提供 3D 深度 }} > - - + + + ) : ( + {alt} - + )}
); } diff --git a/components/ui/ImageWave.tsx b/components/ui/ImageWave.tsx index 60feecf..2f4e1d8 100644 --- a/components/ui/ImageWave.tsx +++ b/components/ui/ImageWave.tsx @@ -1,5 +1,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import styled from 'styled-components'; +import { GlassIconButton } from '@/components/ui/glass-icon-button'; +import { Check } from 'lucide-react'; interface ImageWaveProps { // 图片列表数据 @@ -152,8 +154,6 @@ export const ImageWave: React.FC = ({ setCurrentExpandedItem(null); } else { setCurrentExpandedItem(index); - setCurrentSelectedIndex(index); - onClick?.(index); } }; @@ -188,6 +188,11 @@ export const ImageWave: React.FC = ({ }; }, [autoAnimate]); + const handleSelectImage = (index: number) => { + setCurrentSelectedIndex(index); + onClick?.(index); + }; + return ( @@ -196,11 +201,19 @@ export const ImageWave: React.FC = ({ key={index} width={itemWidth} height={itemHeight} - className={`item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`} + className={`group relative item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`} style={{ backgroundImage: `url(${image})` }} onClick={() => handleItemClick(index)} tabIndex={0} - /> + > + {/* 添加一个玻璃按钮 勾选当前图片 移入/选中改变图标颜色 */} + handleSelectImage(index)} + className="absolute top-1 right-1 z-[999] cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity duration-300 group-hover:bg-blue-500/50" + /> + ))} diff --git a/components/ui/character-library-selector.tsx b/components/ui/character-library-selector.tsx new file mode 100644 index 0000000..e8e01f9 --- /dev/null +++ b/components/ui/character-library-selector.tsx @@ -0,0 +1,57 @@ +import FloatingGlassPanel from './FloatingGlassPanel'; +import { ImageWave } from '@/components/ui/ImageWave'; + +const imageUrls = [ + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', +]; + +export function CharacterLibrarySelector({ + isReplaceLibraryOpen, + setIsReplaceLibraryOpen, + onSelect, +}: { + isReplaceLibraryOpen: boolean; + setIsReplaceLibraryOpen: (open: boolean) => void; + onSelect: (index: number) => void; +}) { + return ( + setIsReplaceLibraryOpen(false)} + > + {/* 内容 */} + { + onSelect(index); + }} + /> + + ); +} \ No newline at end of file diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index 6f6be3e..cf6ac7c 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -2,14 +2,12 @@ import React, { useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, X } from 'lucide-react'; import { cn } from '@/public/lib/utils'; -import { GlassIconButton } from './glass-icon-button'; -import { ReplaceCharacterModal } from './replace-character-modal'; -import { Slider } from './slider'; import CharacterEditor from './character-editor'; import ImageBlurTransition from './ImageBlurTransition'; import FloatingGlassPanel from './FloatingGlassPanel'; import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel'; -import { ImageWave } from '@/components/ui/ImageWave'; +import { CharacterLibrarySelector } from './character-library-selector'; +import HorizontalScroller from './HorizontalScroller'; interface Appearance { hairStyle: string; @@ -58,47 +56,25 @@ interface CharacterTabContentProps { roles: Role[]; } -const imageUrls = [ - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', -]; - export function CharacterTabContent({ taskSketch, currentRoleIndex, onSketchSelect, roles = [mockRole] }: CharacterTabContentProps) { - const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false); - const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload'); - const [newTag, setNewTag] = useState(''); const [localRole, setLocalRole] = useState(mockRole); const [currentRole, setCurrentRole] = useState(roles[currentRoleIndex]); const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); const [replacePanelKey, setReplacePanelKey] = useState(0); const [ignoreReplace, setIgnoreReplace] = useState(false); const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false); - const [replaceLibraryKey, setReplaceLibraryKey] = useState(0); + + const [selectRoleIndex, setSelectRoleIndex] = useState(0); + const [enableAnimation, setEnableAnimation] = useState(true); - const textareaRef = useRef(null); const handleReplaceCharacter = (url: string) => { + setEnableAnimation(true); setCurrentRole({ ...currentRole, url: url @@ -114,13 +90,14 @@ export function CharacterTabContent({ setIsReplacePanelOpen(false); }; + // 取消替换 const handleCloseReplacePanel = () => { setIsReplacePanelOpen(false); setIgnoreReplace(true); }; const handleChangeRole = (index: number) => { - if (currentRole.url !== roles[currentRoleIndex].url && !ignoreReplace) { + if (currentRole.url !== roles[selectRoleIndex].url && !ignoreReplace) { // 提示 角色已修改,弹出替换角色面板 if (isReplacePanelOpen) { setReplacePanelKey(replacePanelKey + 1); @@ -129,10 +106,21 @@ export function CharacterTabContent({ } return; } - onSketchSelect(index); + // 重置替换规则 + setEnableAnimation(false); + setIgnoreReplace(false); + + setSelectRoleIndex(index); setCurrentRole(roles[index]); }; + // 从角色库中选择角色 + const handleSelectCharacter = (index: number) => { + console.log('index', index); + setIsReplaceLibraryOpen(false); + handleReplaceCharacter('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg'); + }; + // 如果没有角色数据,显示占位内容 if (!roles || roles.length === 0) { return ( @@ -152,16 +140,20 @@ export function CharacterTabContent({ animate={{ opacity: 1, y: 0 }} >
-
+ handleChangeRole(i)} + > {roles.map((role, index) => ( handleChangeRole(index)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > @@ -175,13 +167,13 @@ export function CharacterTabContent({
))} -
+ {/* 下部分:角色详情 */} {/* 应用角色按钮 */}
@@ -260,7 +253,7 @@ export function CharacterTabContent({ {/* 重新生成按钮、替换形象按钮 */}
console.log('Replace')} + onClick={() => handleReplaceCharacter('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg')} className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20 text-pink-500 rounded-lg transition-colors" whileHover={{ scale: 1.02 }} @@ -287,7 +280,12 @@ export function CharacterTabContent({ - + handleCloseReplacePanel()} + > {/* 从角色库中选择角色 */} - - {/* 标题 从角色库中选择角色 */} -
Role Library
- {/* 内容 */} - { - console.log('index', index); - }} - /> - {/* 操作按钮 */} -
- - -
-
+
); } \ No newline at end of file diff --git a/components/ui/edit-modal.tsx b/components/ui/edit-modal.tsx index be4e2ec..c420bc5 100644 --- a/components/ui/edit-modal.tsx +++ b/components/ui/edit-modal.tsx @@ -235,7 +235,7 @@ export function EditModal({
{/* 底部操作栏 */} - {/*
+
-
*/} +
diff --git a/components/ui/person-detection.tsx b/components/ui/person-detection.tsx index 7c41fd5..8274d09 100644 --- a/components/ui/person-detection.tsx +++ b/components/ui/person-detection.tsx @@ -51,6 +51,7 @@ type Props = { scanTimeout?: number; isScanFailed?: boolean; // 外部传入的失败状态 onDetectionsChange?: (detections: PersonDetection[]) => void; + onPersonClick?: (person: PersonDetection) => void; }; export const PersonDetectionScene: React.FC = ({ @@ -63,7 +64,8 @@ export const PersonDetectionScene: React.FC = ({ onScanExit, scanTimeout = 10000, isScanFailed = false, - onDetectionsChange + onDetectionsChange, + onPersonClick }) => { const scanControls = useAnimation(); const videoRef = useRef(null); @@ -374,7 +376,9 @@ export const PersonDetectionScene: React.FC = ({ {detections.map((person, index) => { return ( - +
{ + onPersonClick?.(person); + }}> = ({ > {person.name} - +
); })} diff --git a/components/ui/replace-character-panel.tsx b/components/ui/replace-character-panel.tsx index 498a417..211ed01 100644 --- a/components/ui/replace-character-panel.tsx +++ b/components/ui/replace-character-panel.tsx @@ -4,6 +4,7 @@ import { Shot, Character } from '@/app/model/types'; interface ReplaceCharacterPanelProps { shots: Shot[]; character: Character; + showAddToLibrary?: boolean; onClose: () => void; onConfirm: (selectedShots: string[], addToLibrary: boolean) => void; } @@ -47,6 +48,7 @@ export const mockCharacter: Character = { export function ReplaceCharacterPanel({ shots = mockShots, character = mockCharacter, + showAddToLibrary = true, onClose, onConfirm, }: ReplaceCharacterPanelProps) { @@ -55,7 +57,7 @@ export function ReplaceCharacterPanel({ title="替换新形象" shots={shots} item={character} - showAddToLibrary={true} + showAddToLibrary={showAddToLibrary} addToLibraryText="新形象同步添加至角色库" onClose={onClose} onConfirm={onConfirm} diff --git a/components/ui/replace-panel.tsx b/components/ui/replace-panel.tsx index 08181a0..38d4fe9 100644 --- a/components/ui/replace-panel.tsx +++ b/components/ui/replace-panel.tsx @@ -100,14 +100,14 @@ export function ReplacePanel({ {/* 分镜展示区 */}
选择需要替换的分镜:
-
+
{shots.map((shot) => ( {/* 分镜缩略图行 */}
-
handleChangeScene(i)} > -
- {sketches.map((sketch, index) => ( - handleChangeScene(index)} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - {`Sketch -
- Scene {index + 1} -
- {/* 鼠标悬浮/移出 显示/隐藏 删除图标 */} -
- -
-
- ))} -
+ {sketches.map((sketch, index) => ( + + {`Sketch +
+ Scene {index + 1} +
+ {/* 鼠标悬浮/移出 显示/隐藏 删除图标 */} + {/*
+ +
*/} +
+ ))} {/* 新增占位符 */} {/* 添加场景
*/} -
+
{/* 脚本预览行 - 单行滚动 */}
-
handleChangeScene(i)} > {sketches.map((script, index) => { const isActive = currentSketchIndex === index; @@ -269,7 +271,6 @@ export function SceneTabContent({ 'flex-shrink-0 cursor-pointer transition-all duration-300', isActive ? 'text-white' : 'text-white/50 hover:text-white/80' )} - onClick={() => handleChangeScene(index)} initial={false} animate={{ scale: isActive ? 1.02 : 1, @@ -286,7 +287,7 @@ export function SceneTabContent({ ); })} -
+ {/* 渐变遮罩 */}
@@ -330,7 +331,7 @@ export function SceneTabContent({ {/* 重新生成按钮、替换形象按钮 */}
console.log('Replace')} + onClick={() => handleReplaceScene('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg')} className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20 text-pink-500 rounded-lg transition-colors" whileHover={{ scale: 1.02 }} diff --git a/components/ui/shot-editor/CharacterToken.tsx b/components/ui/shot-editor/CharacterToken.tsx index 4910397..7eaefff 100644 --- a/components/ui/shot-editor/CharacterToken.tsx +++ b/components/ui/shot-editor/CharacterToken.tsx @@ -11,13 +11,18 @@ interface CharacterAttributes { 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) - alert(`点击角色:${name}`) + const { editor } = props; + editor?.emit('character-clicked', props.node.attrs); } return ( @@ -38,7 +43,7 @@ export function CharacterToken(props: ReactNodeViewProps) { 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" + className="absolute top-full left-0 mt-2 w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-4 z-50" >
void), + // } + // }, + addNodeView() { return ReactNodeViewRenderer(CharacterToken); }, diff --git a/components/ui/shot-editor/ShotEditor.tsx b/components/ui/shot-editor/ShotEditor.tsx index 54a1319..e9ab0c0 100644 --- a/components/ui/shot-editor/ShotEditor.tsx +++ b/components/ui/shot-editor/ShotEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { EditorContent, useEditor } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { motion } from "framer-motion"; @@ -36,9 +36,10 @@ const initialContent = { interface ShotEditorProps { onAddSegment?: () => void; + onCharacterClick?: (attrs: any) => void; } -const ShotEditor = React.forwardRef<{ addSegment: () => void }, ShotEditorProps>(function ShotEditor({ onAddSegment }, ref) { +const ShotEditor = React.forwardRef<{ addSegment: () => void, onCharacterClick: (attrs: any) => void }, ShotEditorProps>(function ShotEditor({ onAddSegment, onCharacterClick }, ref) { const [segments, setSegments] = useState(initialContent.content); const editor = useEditor({ @@ -60,6 +61,20 @@ const ShotEditor = React.forwardRef<{ addSegment: () => void }, ShotEditorProps> }, }) + useEffect(() => { + const handleCharacterClick = (attrs: any) => { + console.log('SceneEditor 收到角色点击事件:', attrs) + // 你可以这里 setState 打开一个弹窗 / 面板等 + onCharacterClick?.(attrs); + }; + + editor?.on('character-clicked', handleCharacterClick as any); + + return () => { + editor?.off('character-clicked', handleCharacterClick as any); + }; + }, [editor]); + const addSegment = () => { if (!editor) return; diff --git a/components/ui/shot-tab-content.tsx b/components/ui/shot-tab-content.tsx index 6161876..a8d492f 100644 --- a/components/ui/shot-tab-content.tsx +++ b/components/ui/shot-tab-content.tsx @@ -10,6 +10,10 @@ import { MediaPropertiesModal } from './media-properties-modal'; import { DramaLineChart } from './drama-line-chart'; import { PersonDetection, PersonDetectionScene } from './person-detection'; import ShotEditor from './shot-editor/ShotEditor'; +import { CharacterLibrarySelector } from './character-library-selector'; +import FloatingGlassPanel from './FloatingGlassPanel'; +import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel'; +import HorizontalScroller from './HorizontalScroller'; interface ShotTabContentProps { taskSketch: any[]; @@ -24,21 +28,19 @@ export function ShotTabContent({ onSketchSelect, isPlaying: externalIsPlaying = true }: ShotTabContentProps) { - const thumbnailsRef = useRef(null); const editorRef = useRef(null); - const videosRef = useRef(null); const videoPlayerRef = useRef(null); const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying); - const [isMuted, setIsMuted] = React.useState(false); - const [progress, setProgress] = React.useState(0); - const [isReplaceModalOpen, setIsReplaceModalOpen] = React.useState(false); - const [activeReplaceMethod, setActiveReplaceMethod] = React.useState<'upload' | 'library' | 'generate'>('upload'); const [isMediaPropertiesModalOpen, setIsMediaPropertiesModalOpen] = React.useState(false); - const [triggerScan, setTriggerScan] = useState(false); const [detections, setDetections] = useState([]); const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected'>('idle'); + const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false); + const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); + + const [shots, setShots] = useState([]); + // 监听外部播放状态变化 useEffect(() => { @@ -48,31 +50,6 @@ export function ShotTabContent({ // 确保 taskSketch 是数组 const sketches = Array.isArray(taskSketch) ? taskSketch : []; - // 自动滚动到选中项 - useEffect(() => { - if (thumbnailsRef.current && videosRef.current) { - const thumbnailContainer = thumbnailsRef.current; - const videoContainer = videosRef.current; - - const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0; - const thumbnailGap = 16; // gap-4 = 16px - const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * currentSketchIndex; - - const videoElement = videoContainer.children[currentSketchIndex] as HTMLElement; - const videoScrollPosition = videoElement?.offsetLeft ?? 0; - - thumbnailContainer.scrollTo({ - left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2, - behavior: 'smooth' - }); - - videoContainer.scrollTo({ - left: videoScrollPosition - videoContainer.clientWidth / 2 + videoElement?.clientWidth / 2, - behavior: 'smooth' - }); - } - }, [currentSketchIndex]); - // 视频播放控制 useEffect(() => { if (videoPlayerRef.current) { @@ -87,13 +64,6 @@ export function ShotTabContent({ } }, [isPlaying, currentSketchIndex]); - // 更新进度条 - const handleTimeUpdate = () => { - if (videoPlayerRef.current) { - const progress = (videoPlayerRef.current.currentTime / videoPlayerRef.current.duration) * 100; - setProgress(progress); - } - }; // 处理扫描开始 const handleScan = () => { @@ -130,6 +100,33 @@ export function ShotTabContent({ } }; + // 处理人物点击 打开角色库 + const handlePersonClick = (person: PersonDetection) => { + console.log('person', person); + setIsReplaceLibraryOpen(true); + }; + + // 从角色库中选择角色 + const handleSelectCharacter = (index: number) => { + console.log('index', index); + setIsReplaceLibraryOpen(false); + // 模拟打开替换面板 + setTimeout(() => { + setIsReplacePanelOpen(true); + }, 1000); + }; + + // 确认替换角色 + const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => { + + }; + + // 切换选择分镜 + const handleSelectShot = (index: number) => { + // 切换前 判断数据是否发生变化 + onSketchSelect(index); + }; + // 如果没有数据,显示空状态 if (sketches.length === 0) { return ( @@ -148,9 +145,11 @@ export function ShotTabContent({ > {/* 分镜缩略图行 */}
-
handleSelectShot(i)} > {sketches.map((sketch, index) => ( onSketchSelect(index)} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} >
{/* 鼠标悬浮/移出 显示/隐藏 删除图标 */} -
+ {/*
-
+
*/} ))} -
+
{/* 视频描述行 - 单行滚动 */}
-
handleSelectShot(i)} > {sketches.map((video, index) => { const isActive = currentSketchIndex === index; @@ -204,7 +204,6 @@ export function ShotTabContent({ 'flex-shrink-0 cursor-pointer transition-all duration-300', isActive ? 'text-white' : 'text-white/50 hover:text-white/80' )} - onClick={() => onSketchSelect(index)} initial={false} animate={{ scale: isActive ? 1.02 : 1, @@ -221,7 +220,7 @@ export function ShotTabContent({ ); })} -
+ {/* 渐变遮罩 */}
@@ -251,6 +250,7 @@ export function ShotTabContent({ onScanTimeout={handleScanTimeout} onScanExit={handleScanTimeout} onDetectionsChange={handleDetectionsChange} + onPersonClick={handlePersonClick} /> {/*