From 12e9aeb88e650c07e9077aecec6aa514881590cb 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: Tue, 29 Jul 2025 21:22:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E8=A7=92=E8=89=B2=E3=80=81?= =?UTF-8?q?=E5=9C=BA=E6=99=AFtab=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/model/types.ts | 20 ++ components/ui/FloatingGlassPanel.tsx | 60 ++++++ components/ui/ImageBlurTransition.tsx | 68 ++++++ components/ui/ImageWave.tsx | 12 +- components/ui/character-editor.tsx | 84 +++----- components/ui/character-tab-content.tsx | 243 +++++++++++++--------- components/ui/replace-character-panel.tsx | 64 ++++++ components/ui/replace-panel.tsx | 195 +++++++++++++++++ components/ui/replace-scene-panel.tsx | 76 +++++++ components/ui/scene-editor.tsx | 84 +++----- components/ui/scene-tab-content.tsx | 115 ++++++---- 11 files changed, 764 insertions(+), 257 deletions(-) create mode 100644 app/model/types.ts create mode 100644 components/ui/FloatingGlassPanel.tsx create mode 100644 components/ui/ImageBlurTransition.tsx create mode 100644 components/ui/replace-character-panel.tsx create mode 100644 components/ui/replace-panel.tsx create mode 100644 components/ui/replace-scene-panel.tsx diff --git a/app/model/types.ts b/app/model/types.ts new file mode 100644 index 0000000..076bc3f --- /dev/null +++ b/app/model/types.ts @@ -0,0 +1,20 @@ +// 基础类型 +export interface Shot { + id: string; + videoUrl?: string; + thumbnailUrl: string; + isGenerating: boolean; + isSelected: boolean; +} + +export interface Character { + id: string; + name: string; + avatarUrl: string; +} + +export interface Scene { + id: string; + name: string; + avatarUrl: string; +} \ No newline at end of file diff --git a/components/ui/FloatingGlassPanel.tsx b/components/ui/FloatingGlassPanel.tsx new file mode 100644 index 0000000..324c1fc --- /dev/null +++ b/components/ui/FloatingGlassPanel.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { ReactNode } from 'react'; + +type FloatingGlassPanelProps = { + open: boolean; + onClose?: () => void; + children: ReactNode; + width?: string; + r_key?: string | number; +}; + +export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key }: FloatingGlassPanelProps) { + // 定义弹出动画 + const bounceAnimation = { + scale: [0.95, 1.02, 0.98, 1], + rotate: [0, -1, 1, -1, 0], + }; + + return ( + + {open && ( + +
+ {children} +
+
+ )} +
+ ); +} diff --git a/components/ui/ImageBlurTransition.tsx b/components/ui/ImageBlurTransition.tsx new file mode 100644 index 0000000..c1c395b --- /dev/null +++ b/components/ui/ImageBlurTransition.tsx @@ -0,0 +1,68 @@ +// Image3DFlipper.tsx +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { useEffect, useState } from 'react'; + +type ImageBlurTransitionProps = { + className?: string; + src: string; + alt?: string; + width?: number | string; + height?: number | string; +}; + +export default function ImageBlurTransition({ src, alt = '', width = 480, height = 300, className }: ImageBlurTransitionProps) { + const [current, setCurrent] = useState(src); + const [isFlipping, setIsFlipping] = useState(false); + + useEffect(() => { + if (src !== current) { + setIsFlipping(true); + const timeout = setTimeout(() => { + setCurrent(src); + setIsFlipping(false); + }, 150); // 时长 = exit 动画时长 + return () => clearTimeout(timeout); + } + }, [src, current]); + + return ( +
+ + + +
+ ); +} diff --git a/components/ui/ImageWave.tsx b/components/ui/ImageWave.tsx index 016e589..60feecf 100644 --- a/components/ui/ImageWave.tsx +++ b/components/ui/ImageWave.tsx @@ -18,6 +18,8 @@ interface ImageWaveProps { autoAnimate?: boolean; // 自动动画间隔时间(ms) autoAnimateInterval?: number; + // 是否开启点击事件 + onClick?: (index: number) => void; } const Wrapper = styled.div<{ width?: string; height?: string }>` @@ -122,6 +124,10 @@ const Item = styled.div<{ width?: string; height?: string }>` transform: translateZ(calc(var(--index) * 7.8)); margin: 0.45vw; } + + &.selected { + filter: inherit; + } `; export const ImageWave: React.FC = ({ @@ -133,8 +139,10 @@ export const ImageWave: React.FC = ({ gap, autoAnimate = false, autoAnimateInterval = 2000, + onClick, }) => { const [currentExpandedItem, setCurrentExpandedItem] = useState(null); + const [currentSelectedIndex, setCurrentSelectedIndex] = useState(null); const itemsRef = useRef(null); const autoAnimateRef = useRef(null); const currentIndexRef = useRef(0); @@ -144,6 +152,8 @@ export const ImageWave: React.FC = ({ setCurrentExpandedItem(null); } else { setCurrentExpandedItem(index); + setCurrentSelectedIndex(index); + onClick?.(index); } }; @@ -186,7 +196,7 @@ export const ImageWave: React.FC = ({ key={index} width={itemWidth} height={itemHeight} - className={`item ${currentExpandedItem === index ? 'expanded' : ''}`} + className={`item ${currentExpandedItem === index ? 'expanded' : ''} ${currentSelectedIndex === index ? 'selected' : ''}`} style={{ backgroundImage: `url(${image})` }} onClick={() => handleItemClick(index)} tabIndex={0} diff --git a/components/ui/character-editor.tsx b/components/ui/character-editor.tsx index da9ae81..0a2b3f8 100644 --- a/components/ui/character-editor.tsx +++ b/components/ui/character-editor.tsx @@ -1,7 +1,7 @@ import { useState, useRef } from "react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; -import { Sparkles, X, Plus } from 'lucide-react'; +import { Sparkles, X, Plus, RefreshCw } from 'lucide-react'; import { cn } from "@/public/lib/utils"; import ContentEditable from 'react-contenteditable'; @@ -17,6 +17,7 @@ interface CharacterEditorProps { initialDescription?: string; onDescriptionChange?: (description: string) => void; onAttributesChange?: (attributes: CharacterAttribute[]) => void; + onReplaceCharacter?: (url: string) => void; } const mockParse = (text: string): CharacterAttribute[] => { @@ -37,9 +38,11 @@ export default function CharacterEditor({ initialDescription = "一个银白短发的精灵女性,大约20岁,肤色白皙,身材高挑,身着白色连衣裙", onDescriptionChange, onAttributesChange, + onReplaceCharacter, }: CharacterEditorProps) { const [inputText, setInputText] = useState(initialDescription); const [isOptimizing, setIsOptimizing] = useState(false); + const [isRegenerating, setIsRegenerating] = useState(false); const [customTags, setCustomTags] = useState([]); const [newTag, setNewTag] = useState(""); const attributesRef = useRef(mockParse(initialDescription)); @@ -96,8 +99,16 @@ export default function CharacterEditor({ onAttributesChange?.(attributesRef.current); }; + const handleRegenerate = () => { + setIsRegenerating(true); + setTimeout(() => { + onReplaceCharacter?.("https://c.huiying.video/images/0411ac7b-ab7e-4a17-ab4f-6880a28f8915.jpg"); + setIsRegenerating(false); + }, 3000); + }; + return ( -
+
{/* 自由输入区域 */}
@@ -154,60 +165,19 @@ export default function CharacterEditor({ ))}
- {/* 自定义标签区域 */} -
-
- {customTags.map((tag) => ( - - {tag} - - - ))} -
-
- setNewTag(e.target.value)} - onKeyPress={(e) => { - if (e.key === 'Enter' && newTag.trim()) { - setCustomTags(tags => [...tags, newTag.trim()]); - setInputText((text: string) => text + (text.endsWith("。") ? "" : ",") + newTag.trim()); - setNewTag(""); - } - }} - placeholder="添加自定义标签..." - className="flex-1 px-3 py-2 bg-white/5 border-none rounded-lg text-sm - focus:outline-none focus:ring-2 focus:ring-blue-500 placeholder:text-white/30" - /> - -
-
+ {/* 重新生成按钮 */} + + + {isRegenerating ? "生成中..." : "重新生成"} +
); } diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index a9aac41..5c14a54 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -1,11 +1,15 @@ import React, { useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, Sparkles, Plus, X } from 'lucide-react'; +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'; interface Appearance { hairStyle: string; @@ -54,6 +58,27 @@ 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, @@ -64,67 +89,48 @@ export function CharacterTabContent({ 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 handleAddTag = () => { - if (newTag.trim() && !localRole.tags.includes(newTag.trim())) { - const newTagText = newTag.trim(); - // 更新标签数组 - const updatedTags = [...localRole.tags, newTagText]; - // 更新角色描述文本 - const updatedDescription = localRole.roleDescription + (localRole.roleDescription ? ',' : '') + newTagText; - - setLocalRole({ - ...localRole, - tags: updatedTags, - roleDescription: updatedDescription - }); - setNewTag(''); + const handleReplaceCharacter = (url: string) => { + setCurrentRole({ + ...currentRole, + url: url + }); - // 自动调整文本框高度 - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; + setIsReplacePanelOpen(true); + }; + + const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => { + // 处理替换确认逻辑 + console.log('Selected shots:', selectedShots); + console.log('Add to library:', addToLibrary); + setIsReplacePanelOpen(false); + }; + + const handleCloseReplacePanel = () => { + setIsReplacePanelOpen(false); + setIgnoreReplace(true); + }; + + const handleChangeRole = (index: number) => { + if (currentRole.url !== roles[currentRoleIndex].url && !ignoreReplace) { + // 提示 角色已修改,弹出替换角色面板 + if (isReplacePanelOpen) { + setReplacePanelKey(replacePanelKey + 1); + } else { + setIsReplacePanelOpen(true); } + return; } - }; - - // 处理标签删除 - const handleRemoveTag = (tagToRemove: string) => { - setLocalRole({ - ...localRole, - tags: localRole.tags.filter(tag => tag !== tagToRemove) - }); - }; - - // 处理年龄滑块变化 - const handleAgeChange = (value: number[]) => { - setLocalRole({ - ...localRole, - age: value[0] - }); - }; - - // 处理描述更新 - const handleDescriptionChange = (e: React.ChangeEvent) => { - setLocalRole({ - ...localRole, - roleDescription: e.target.value - }); - - // 自动调整文本框高度 - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; - } - }; - - // 新增智能优化处理函数 - const handleSmartOptimize = () => { - console.log('Optimizing character description...'); - // TODO: 调用 AI 优化接口 + onSketchSelect(index); + setCurrentRole(roles[index]); }; // 如果没有角色数据,显示占位内容 @@ -137,9 +143,6 @@ export function CharacterTabContent({ ); } - // 获取当前选中的角色 - const currentRole = roles[currentRoleIndex]; - return (
{/* 上部分:角色缩略图 */} @@ -158,7 +161,7 @@ export function CharacterTabContent({ 'aspect-[9/16]', currentRoleIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50' )} - onClick={() => onSketchSelect(index)} + onClick={() => handleChangeRole(index)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > @@ -183,48 +186,7 @@ export function CharacterTabContent({ animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} > - {/* 左列:角色预览 */} -
- {/* 角色预览图 */} -
- {currentRole.name} -
-
- console.log('regenerate character')} - /> -
-
-
- - {/* 操作按钮 */} -
- - -
-
- - {/* 右列:角色信息 */} + {/* 左列:角色信息 */}
{ + handleReplaceCharacter(url); + }} />
+ + {/* 右列:角色预览 */} +
+ {/* 角色预览图 */} +
+ + {/* 应用角色按钮 */} +
+ setIsReplaceLibraryOpen(true)} + > + + +
+
+ +
+ + + + + handleCloseReplacePanel()} + onConfirm={handleConfirmReplace} + /> + + + {/* 从角色库中选择角色 */} + + {/* 标题 从角色库中选择角色 */} +
Role Library
+ {/* 内容 */} + { + console.log('index', index); + }} + /> + {/* 操作按钮 */} +
+ + +
+
); } \ No newline at end of file diff --git a/components/ui/replace-character-panel.tsx b/components/ui/replace-character-panel.tsx new file mode 100644 index 0000000..498a417 --- /dev/null +++ b/components/ui/replace-character-panel.tsx @@ -0,0 +1,64 @@ +import { ReplacePanel } from './replace-panel'; +import { Shot, Character } from '@/app/model/types'; + +interface ReplaceCharacterPanelProps { + shots: Shot[]; + character: Character; + onClose: () => void; + onConfirm: (selectedShots: string[], addToLibrary: boolean) => void; +} + +// Mock数据 +export const mockShots: Shot[] = [ + { + id: '1', + thumbnailUrl: '/assets/3dr_chihiro.png', + videoUrl: 'https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ1-0-20250725023719.mp4', + isGenerating: false, + isSelected: true, + }, + { + id: '2', + thumbnailUrl: '/assets/3dr_mono.png', + isGenerating: true, + isSelected: true, + }, + { + id: '3', + thumbnailUrl: '/assets/3dr_howlcastle.png', + videoUrl: 'https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ3-0-20250725023725.mp4', + isGenerating: false, + isSelected: true, + }, + { + id: '4', + thumbnailUrl: '/assets/3dr_spirited.jpg', + isGenerating: true, + isSelected: true, + }, +]; + +export const mockCharacter: Character = { + id: '1', + name: '千寻', + avatarUrl: '/assets/3dr_chihiro.png', +}; + +export function ReplaceCharacterPanel({ + shots = mockShots, + character = mockCharacter, + onClose, + onConfirm, +}: ReplaceCharacterPanelProps) { + return ( + + ); +} \ No newline at end of file diff --git a/components/ui/replace-panel.tsx b/components/ui/replace-panel.tsx new file mode 100644 index 0000000..420ae48 --- /dev/null +++ b/components/ui/replace-panel.tsx @@ -0,0 +1,195 @@ +import React, { useState, useRef } from 'react'; +import { motion } from 'framer-motion'; +import { Check, X, CircleAlert } from 'lucide-react'; +import { cn } from '@/public/lib/utils'; + +// 定义类型 +interface Shot { + id: string; + videoUrl?: string; + thumbnailUrl: string; + isGenerating: boolean; + isSelected: boolean; +} + +interface Item { + id: string; + name: string; + avatarUrl: string; +} + +interface ReplacePanelProps { + title: string; + shots: Shot[]; + item: Item; + showAddToLibrary?: boolean; + addToLibraryText?: string; + onClose: () => void; + onConfirm: (selectedShots: string[], addToLibrary: boolean) => void; +} + +export function ReplacePanel({ + title, + shots, + item, + showAddToLibrary = false, + addToLibraryText = "同步添加至库", + onClose, + onConfirm, +}: ReplacePanelProps) { + const [selectedShots, setSelectedShots] = useState( + shots.filter(shot => shot.isSelected).map(shot => shot.id) + ); + const [addToLibrary, setAddToLibrary] = useState(true); + const [hoveredVideoId, setHoveredVideoId] = useState(null); + const videoRefs = useRef<{ [key: string]: HTMLVideoElement }>({}); + + const handleShotToggle = (shotId: string) => { + setSelectedShots(prev => + prev.includes(shotId) + ? prev.filter(id => id !== shotId) + : [...prev, shotId] + ); + }; + + const handleSelectAllShots = (checked: boolean) => { + setSelectedShots(checked ? shots.map(shot => shot.id) : []); + }; + + const handleMouseEnter = (shotId: string) => { + setHoveredVideoId(shotId); + if (videoRefs.current[shotId]) { + videoRefs.current[shotId].play(); + } + }; + + const handleMouseLeave = (shotId: string) => { + setHoveredVideoId(null); + if (videoRefs.current[shotId]) { + videoRefs.current[shotId].pause(); + videoRefs.current[shotId].currentTime = 0; + } + }; + + const handleConfirm = () => { + onConfirm(selectedShots, addToLibrary); + }; + + return ( +
+ {/* 标题 */} +
{title}
+ + {/* 提示信息 */} +
+
+ + 该内容出现在 {shots.length} 个分镜中,替换后将影响如下分镜 +
+
+ handleSelectAllShots(e.target.checked)} + className="w-4 h-4 rounded border-white/20" + /> + +
+
+ + {/* 分镜展示区 */} +
+
选择需要替换的分镜:
+
+ {shots.map((shot) => ( + handleShotToggle(shot.id)} + onMouseEnter={() => handleMouseEnter(shot.id)} + onMouseLeave={() => handleMouseLeave(shot.id)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {shot.videoUrl && ( + + ))} +
+
+ + {/* 预览信息 */} +
+ {item.name} +
{item.name}
+
+ + {/* 同步到库选项 */} + {showAddToLibrary && ( +
+ setAddToLibrary(e.target.checked)} + className="w-4 h-4 rounded border-white/20" + /> + +
+ )} + + {/* 操作按钮 */} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/components/ui/replace-scene-panel.tsx b/components/ui/replace-scene-panel.tsx new file mode 100644 index 0000000..266ca36 --- /dev/null +++ b/components/ui/replace-scene-panel.tsx @@ -0,0 +1,76 @@ +import { ReplacePanel } from './replace-panel'; + +interface Shot { + id: string; + videoUrl?: string; + thumbnailUrl: string; + isGenerating: boolean; + isSelected: boolean; +} + +interface Scene { + id: string; + name: string; + avatarUrl: string; +} + +interface ReplaceScenePanelProps { + shots: Shot[]; + scene: Scene; + onClose: () => void; + onConfirm: (selectedShots: string[], addToLibrary: boolean) => void; +} + +// Mock数据 +export const mockShots: Shot[] = [ + { + id: '1', + thumbnailUrl: '/assets/3dr_chihiro.png', + videoUrl: 'https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ1-0-20250725023719.mp4', + isGenerating: false, + isSelected: true, + }, + { + id: '2', + thumbnailUrl: '/assets/3dr_mono.png', + isGenerating: true, + isSelected: true, + }, + { + id: '3', + thumbnailUrl: '/assets/3dr_howlcastle.png', + videoUrl: 'https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ3-0-20250725023725.mp4', + isGenerating: false, + isSelected: true, + }, + { + id: '4', + thumbnailUrl: '/assets/3dr_spirited.jpg', + isGenerating: true, + isSelected: true, + }, +]; + +export const mockScene: Scene = { + id: '1', + name: '场景 1', + avatarUrl: '/assets/3dr_howlbg.jpg', +}; + +export function ReplaceScenePanel({ + shots = mockShots, + scene = mockScene, + onClose, + onConfirm, +}: ReplaceScenePanelProps) { + return ( + + ); +} \ No newline at end of file diff --git a/components/ui/scene-editor.tsx b/components/ui/scene-editor.tsx index d849fe0..d168890 100644 --- a/components/ui/scene-editor.tsx +++ b/components/ui/scene-editor.tsx @@ -1,7 +1,7 @@ import { useState, useRef } from "react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; -import { Sparkles, X, Plus, Clock, MapPin, Sun, Moon, Cloud, CloudRain, CloudSnow, CloudLightning, Palette } from 'lucide-react'; +import { Sparkles, X, Plus, Clock, MapPin, Sun, Moon, Cloud, CloudRain, CloudSnow, CloudLightning, Palette, RefreshCw } from 'lucide-react'; import { cn } from "@/public/lib/utils"; import ContentEditable from 'react-contenteditable'; @@ -38,6 +38,7 @@ interface SceneEditorProps { onDescriptionChange?: (description: string) => void; onAttributesChange?: (attributes: SceneAttribute[]) => void; onEnvironmentChange?: (environment: SceneEnvironment) => void; + onReplaceScene?: (url: string) => void; className?: string; } @@ -117,10 +118,12 @@ export default function SceneEditor({ onDescriptionChange, onAttributesChange, onEnvironmentChange, + onReplaceScene, className }: SceneEditorProps) { const [inputText, setInputText] = useState(initialDescription); const [isOptimizing, setIsOptimizing] = useState(false); + const [isRegenerating, setIsRegenerating] = useState(false); const [customTags, setCustomTags] = useState([]); const [newTag, setNewTag] = useState(""); const parseResult = useRef(mockParse(initialDescription)); @@ -190,8 +193,16 @@ export default function SceneEditor({ onEnvironmentChange?.(newParseResult.environment); }; + const handleRegenerate = () => { + setIsRegenerating(true); + setTimeout(() => { + onReplaceScene?.("https://c.huiying.video/images/0411ac7b-ab7e-4a17-ab4f-6880a28f8915.jpg"); + setIsRegenerating(false); + }, 3000); + }; + return ( -
+
{/* 自由输入区域 */}
@@ -249,60 +260,19 @@ export default function SceneEditor({ ))}
- {/* 自定义标签区域 */} -
-
- {customTags.map((tag) => ( - - {tag} - - - ))} -
-
- setNewTag(e.target.value)} - onKeyPress={(e) => { - if (e.key === 'Enter' && newTag.trim()) { - setCustomTags(tags => [...tags, newTag.trim()]); - setInputText((text: string) => text + (text.endsWith("。") ? "" : ",") + newTag.trim()); - setNewTag(""); - } - }} - placeholder="添加自定义标签..." - className="flex-1 px-3 py-2 bg-white/5 border-none rounded-lg text-sm - focus:outline-none focus:ring-2 focus:ring-blue-500 placeholder:text-white/30" - /> - -
-
+ {/* 重新生成按钮 */} + + + {isRegenerating ? "生成中..." : "重新生成"} +
); } \ No newline at end of file diff --git a/components/ui/scene-tab-content.tsx b/components/ui/scene-tab-content.tsx index 110c6ff..1e443b5 100644 --- a/components/ui/scene-tab-content.tsx +++ b/components/ui/scene-tab-content.tsx @@ -2,9 +2,11 @@ import React, { useRef, useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Trash2, RefreshCw, Sun, Moon, Cloud, CloudRain, CloudSnow, CloudLightning, Sparkles, Clock, MapPin, Palette, Check, Plus } from 'lucide-react'; +import { Trash2, RefreshCw, Sun, Moon, Cloud, CloudRain, CloudSnow, CloudLightning, Sparkles, Clock, MapPin, Palette, Check, Plus, ReplaceAll } from 'lucide-react'; import { cn } from '@/public/lib/utils'; import SceneEditor from './scene-editor'; +import FloatingGlassPanel from './FloatingGlassPanel'; +import { ReplaceScenePanel, mockShots } from './replace-scene-panel'; interface SceneEnvironment { time: { @@ -74,6 +76,10 @@ export function SceneTabContent({ const [localSketch, setLocalSketch] = useState(mockSketch); const textareaRef = useRef(null); + const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false); + const [replacePanelKey, setReplacePanelKey] = useState(0); + const [ignoreReplace, setIgnoreReplace] = useState(false); + const [currentScene, setCurrentScene] = useState(taskSketch[currentSketchIndex]); // 天气图标映射 const weatherIcons = { @@ -136,6 +142,41 @@ export function SceneTabContent({ } }, [currentSketchIndex]); + const handleReplaceScene = (url: string) => { + setCurrentScene({ + ...currentScene, + url: url + }); + + setIsReplacePanelOpen(true); + }; + + const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => { + // 处理替换确认逻辑 + console.log('Selected shots:', selectedShots); + console.log('Add to library:', addToLibrary); + setIsReplacePanelOpen(false); + }; + + const handleCloseReplacePanel = () => { + setIsReplacePanelOpen(false); + setIgnoreReplace(true); + }; + + const handleChangeScene = (index: number) => { + if (currentScene?.url !== taskSketch[currentSketchIndex]?.url && !ignoreReplace) { + // 提示 场景已修改,弹出替换场景面板 + if (isReplacePanelOpen) { + setReplacePanelKey(replacePanelKey + 1); + } else { + setIsReplacePanelOpen(true); + } + return; + } + onSketchSelect(index); + setCurrentScene(taskSketch[index]); + }; + // 如果没有数据,显示空状态 if (sketches.length === 0) { return ( @@ -166,7 +207,7 @@ export function SceneTabContent({ 'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group', currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50' )} - onClick={() => onSketchSelect(index)} + onClick={() => handleChangeScene(index)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > @@ -194,7 +235,7 @@ export function SceneTabContent({ ))}
{/* 新增占位符 */} - 添加场景
- + */} @@ -228,7 +269,7 @@ export function SceneTabContent({ 'flex-shrink-0 cursor-pointer transition-all duration-300', isActive ? 'text-white' : 'text-white/50 hover:text-white/80' )} - onClick={() => onSketchSelect(index)} + onClick={() => handleChangeScene(index)} initial={false} animate={{ scale: isActive ? 1.02 : 1, @@ -262,41 +303,6 @@ export function SceneTabContent({ > {/* 左列:脚本编辑器 */}
- {/* 选中的分镜预览 */} - - {`Sketch - - {/* 操作按钮 */} -
- - -
-
- - {/* 右列:环境设置 */} -
- {/* 使用新的场景编辑器组件 */} { @@ -305,10 +311,39 @@ export function SceneTabContent({ script: description }); }} + onReplaceScene={handleReplaceScene} className="min-h-[200px]" />
+ + {/* 右列:场景预览 */} +
+ + {`Scene + +
+ + {/* 替换场景面板 */} + + + ); } \ No newline at end of file