From 14a61b9dec60f46374294ed81f509b9e823c9891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Fri, 1 Aug 2025 13:30:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=9B=BF=E6=8D=A2=E8=81=94?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/FloatingGlassPanel.tsx | 5 +- components/ui/ImageWave.tsx | 21 ++++- components/ui/character-library-selector.tsx | 57 ++++++++++++ components/ui/character-tab-content.tsx | 87 +++++------------- components/ui/person-detection.tsx | 10 ++- components/ui/replace-character-panel.tsx | 4 +- components/ui/replace-panel.tsx | 4 +- components/ui/scene-tab-content.tsx | 2 +- components/ui/shot-editor/CharacterToken.tsx | 13 ++- components/ui/shot-editor/ShotEditor.tsx | 19 +++- components/ui/shot-tab-content.tsx | 92 +++++++++++++------- 11 files changed, 201 insertions(+), 113 deletions(-) create mode 100644 components/ui/character-library-selector.tsx 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/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..42d7204 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -2,14 +2,11 @@ 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'; interface Appearance { hairStyle: string; @@ -58,45 +55,19 @@ 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 textareaRef = useRef(null); const handleReplaceCharacter = (url: string) => { setCurrentRole({ @@ -133,6 +104,13 @@ export function CharacterTabContent({ 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 ( @@ -260,7 +238,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 +265,12 @@ export function CharacterTabContent({ - + handleCloseReplacePanel()} + > {/* 从角色库中选择角色 */} - - {/* 标题 从角色库中选择角色 */} -
Role Library
- {/* 内容 */} - { - console.log('index', index); - }} - /> - {/* 操作按钮 */} -
- - -
-
+
); } \ No newline at end of file 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) => ( 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..313fbff 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 ( @@ -81,6 +86,12 @@ export const CharacterTokenExtension = Node.create({ return ['character-token', mergeAttributes(HTMLAttributes)]; }, + // addStorage() { + // return { + // onClickCharacter: null as null | ((character: CharacterAttributes) => 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..9c9b7cf 100644 --- a/components/ui/shot-tab-content.tsx +++ b/components/ui/shot-tab-content.tsx @@ -10,6 +10,9 @@ 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'; interface ShotTabContentProps { taskSketch: any[]; @@ -29,16 +32,16 @@ export function ShotTabContent({ 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(() => { @@ -87,13 +90,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 +126,27 @@ 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) => { + + }; + // 如果没有数据,显示空状态 if (sketches.length === 0) { return ( @@ -176,14 +193,14 @@ export function ShotTabContent({ Shot {index + 1}
{/* 鼠标悬浮/移出 显示/隐藏 删除图标 */} -
+ {/*
-
+
*/} ))}
@@ -251,6 +268,7 @@ export function ShotTabContent({ onScanTimeout={handleScanTimeout} onScanExit={handleScanTimeout} onDetectionsChange={handleDetectionsChange} + onPersonClick={handlePersonClick} /> {/*