修复编译问题

This commit is contained in:
北枳 2025-07-04 17:06:26 +08:00
parent 94acd18075
commit 7603a1a26e
7 changed files with 478 additions and 356 deletions

View File

@ -2,7 +2,21 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { DragDropContext, DropResult } from 'react-beautiful-dnd'; import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import type { DragEndEvent } from '@dnd-kit/core';
import { ScriptMetaInfo } from '../script-overview/script-meta-info'; import { ScriptMetaInfo } from '../script-overview/script-meta-info';
import { SceneFilmstrip } from '../script-overview/scene-filmstrip'; import { SceneFilmstrip } from '../script-overview/scene-filmstrip';
import { SceneCardList } from '../script-overview/scene-card-list'; import { SceneCardList } from '../script-overview/scene-card-list';
@ -30,6 +44,14 @@ export interface ScriptMeta {
} }
export default function ScriptOverview() { export default function ScriptOverview() {
// Configure drag sensors
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Example Data // Example Data
const [scriptMeta] = useState<ScriptMeta>({ const [scriptMeta] = useState<ScriptMeta>({
title: "Lost Stars", title: "Lost Stars",
@ -164,14 +186,21 @@ export default function ScriptOverview() {
const [selectedSceneId, setSelectedSceneId] = useState<string>(); const [selectedSceneId, setSelectedSceneId] = useState<string>();
// Handle scene drag and drop sorting // Handle scene drag and drop sorting
const handleDragEnd = (result: DropResult) => { const handleDragEnd = (event: DragEndEvent) => {
if (!result.destination) return; const { active, over } = event;
const items = Array.from(scenes); if (active.id === over?.id) {
const [reorderedItem] = items.splice(result.source.index, 1); return;
items.splice(result.destination.index, 0, reorderedItem); }
setScenes(items); const oldIndex = scenes.findIndex(scene => scene.id === active.id);
const newIndex = scenes.findIndex(scene => scene.id === over?.id);
if (oldIndex === -1 || newIndex === -1) {
return;
}
setScenes(arrayMove(scenes, oldIndex, newIndex));
}; };
// Handle scene updates // Handle scene updates
@ -215,15 +244,24 @@ export default function ScriptOverview() {
<div className="flex-grow min-w-0 h-full overflow-hidden flex flex-col"> <div className="flex-grow min-w-0 h-full overflow-hidden flex flex-col">
{/* Scene Card List */} {/* Scene Card List */}
<div className="flex-grow overflow-y-auto px-8"> <div className="flex-grow overflow-y-auto px-8">
<DragDropContext onDragEnd={handleDragEnd}> <DndContext
<SceneCardList sensors={sensors}
scenes={scenes} collisionDetection={closestCenter}
selectedSceneId={selectedSceneId} onDragEnd={handleDragEnd}
onSceneUpdate={handleSceneUpdate} >
onSceneDelete={handleSceneDelete} <SortableContext
onSceneDuplicate={handleSceneDuplicate} items={scenes.map(scene => scene.id)}
/> strategy={verticalListSortingStrategy}
</DragDropContext> >
<SceneCardList
scenes={scenes}
selectedSceneId={selectedSceneId}
onSceneUpdate={handleSceneUpdate}
onSceneDelete={handleSceneDelete}
onSceneDuplicate={handleSceneDuplicate}
/>
</SortableContext>
</DndContext>
</div> </div>
{/* Filmstrip Preview */} {/* Filmstrip Preview */}

View File

@ -2,7 +2,21 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { DragDropContext, DropResult } from 'react-beautiful-dnd'; import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import type { DragEndEvent } from '@dnd-kit/core';
import { ScriptMetaInfo } from '../script-overview/script-meta-info'; import { ScriptMetaInfo } from '../script-overview/script-meta-info';
import { SceneFilmstrip } from '../script-overview/scene-filmstrip'; import { SceneFilmstrip } from '../script-overview/scene-filmstrip';
import { StoryboardCardList } from '../storyboard/storyboard-card-list'; import { StoryboardCardList } from '../storyboard/storyboard-card-list';
@ -145,14 +159,21 @@ export default function StoryboardView() {
const [selectedSceneId, setSelectedSceneId] = useState<string>(); const [selectedSceneId, setSelectedSceneId] = useState<string>();
// 处理场景拖拽排序 // 处理场景拖拽排序
const handleDragEnd = (result: DropResult) => { const handleDragEnd = (event: DragEndEvent) => {
if (!result.destination) return; const { active, over } = event;
const items = Array.from(scenes); if (active.id === over?.id) {
const [reorderedItem] = items.splice(result.source.index, 1); return;
items.splice(result.destination.index, 0, reorderedItem); }
setScenes(items); const oldIndex = scenes.findIndex(scene => scene.id === active.id);
const newIndex = scenes.findIndex(scene => scene.id === over?.id);
if (oldIndex === -1 || newIndex === -1) {
return;
}
setScenes(arrayMove(scenes, oldIndex, newIndex));
}; };
// 处理场景更新 // 处理场景更新
@ -209,15 +230,29 @@ export default function StoryboardView() {
<div className="flex-grow min-w-0 h-full overflow-hidden flex flex-col"> <div className="flex-grow min-w-0 h-full overflow-hidden flex flex-col">
{/* Scene Card List */} {/* Scene Card List */}
<div className="flex-grow overflow-y-auto"> <div className="flex-grow overflow-y-auto">
<DragDropContext onDragEnd={handleDragEnd}> <DndContext
<StoryboardCardList sensors={useSensors(
scenes={scenes} useSensor(PointerSensor),
selectedSceneId={selectedSceneId} useSensor(KeyboardSensor, {
onSceneUpdate={handleSceneUpdate} coordinateGetter: sortableKeyboardCoordinates,
onSceneDelete={handleSceneDelete} })
onSceneDuplicate={handleSceneDuplicate} )}
/> collisionDetection={closestCenter}
</DragDropContext> onDragEnd={handleDragEnd}
>
<SortableContext
items={scenes.map(scene => scene.id)}
strategy={verticalListSortingStrategy}
>
<StoryboardCardList
scenes={scenes}
selectedSceneId={selectedSceneId}
onSceneUpdate={handleSceneUpdate}
onSceneDelete={handleSceneDelete}
onSceneDuplicate={handleSceneDuplicate}
/>
</SortableContext>
</DndContext>
</div> </div>
{/* Filmstrip Preview */} {/* Filmstrip Preview */}

View File

@ -1,4 +1,3 @@
import { Droppable } from 'react-beautiful-dnd';
import { Scene } from '../pages/script-overview'; import { Scene } from '../pages/script-overview';
import { SceneCard } from './scene-card'; import { SceneCard } from './scene-card';
@ -18,31 +17,18 @@ export function SceneCardList({
onSceneDuplicate onSceneDuplicate
}: SceneCardListProps) { }: SceneCardListProps) {
return ( return (
<Droppable droppableId="scenes" direction="horizontal"> <div className="flex gap-6 overflow-x-auto hide-scrollbar h-full overflow-y-hidden transition-colors duration-300 rounded-xl p-2">
{(provided, snapshot) => ( {scenes.map((scene, index) => (
<div <SceneCard
ref={provided.innerRef} key={scene.id}
{...provided.droppableProps} scene={scene}
className={` index={index}
flex gap-6 overflow-x-auto hide-scrollbar h-full overflow-y-hidden isSelected={scene.id === selectedSceneId}
${snapshot.isDraggingOver ? 'bg-white/5' : ''} onUpdate={(updates) => onSceneUpdate(scene.id, updates)}
transition-colors duration-300 rounded-xl p-2 onDelete={() => onSceneDelete(scene.id)}
`} onDuplicate={() => onSceneDuplicate(scene.id)}
> />
{scenes.map((scene, index) => ( ))}
<SceneCard </div>
key={scene.id}
scene={scene}
index={index}
isSelected={scene.id === selectedSceneId}
onUpdate={(updates) => onSceneUpdate(scene.id, updates)}
onDelete={() => onSceneDelete(scene.id)}
onDuplicate={() => onSceneDuplicate(scene.id)}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
); );
} }

View File

@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Draggable } from 'react-beautiful-dnd'; import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Trash2, Copy, RefreshCw, GripVertical } from 'lucide-react'; import { Trash2, Copy, RefreshCw, GripVertical } from 'lucide-react';
import { Scene } from '../pages/script-overview'; import { Scene } from '../pages/script-overview';
import Image from 'next/image'; import Image from 'next/image';
@ -29,6 +30,20 @@ export function SceneCard({
const cardRef = useRef<HTMLDivElement>(null); const cardRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<NodeJS.Timeout>(); const timeoutRef = useRef<NodeJS.Timeout>();
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: scene.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
// 处理自动保存 // 处理自动保存
const handleContentChange = ( const handleContentChange = (
field: keyof Scene, field: keyof Scene,
@ -67,42 +82,48 @@ export function SceneCard({
}, [isSelected]); }, [isSelected]);
return ( return (
<Draggable draggableId={scene.id} index={index}> <div
{(provided, snapshot) => ( ref={setNodeRef}
style={style}
{...attributes}
>
<motion.div
id={`scene-card-${scene.id}`}
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: 1,
x: isDragging ? 5 : 0,
y: isDragging ? 5 : 0,
rotate: isDragging ? 2 : 0
}}
transition={{
type: "spring",
stiffness: 200,
damping: 20
}}
className={`
relative flex-shrink-0 w-[400px] bg-white/5 backdrop-blur-sm rounded-xl overflow-hidden h-full
flex flex-col group cursor-grab active:cursor-grabbing
${isDragging ? 'ring-2 ring-blue-500/50 shadow-lg z-50' : ''}
${isEditing ? 'ring-2 ring-yellow-500/50' : ''}
${isSelected ? 'ring-2 ring-purple-500/50' : ''}
transition-all duration-300
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setIsHovered(false);
setShowDeleteConfirm(false);
}}
>
{/* Drag Handle */}
<div <div
ref={provided.innerRef} {...listeners}
{...provided.draggableProps} className="absolute top-2 right-2 z-10 p-2 rounded-lg bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing"
{...provided.dragHandleProps}
> >
<motion.div <GripVertical className="w-4 h-4 text-white/60" />
id={`scene-card-${scene.id}`} </div>
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: 1,
x: snapshot.isDragging ? 5 : 0,
y: snapshot.isDragging ? 5 : 0,
rotate: snapshot.isDragging ? 2 : 0
}}
transition={{
type: "spring",
stiffness: 200,
damping: 20
}}
className={`
relative flex-shrink-0 w-[400px] bg-white/5 backdrop-blur-sm rounded-xl overflow-hidden h-full
flex flex-col group cursor-grab active:cursor-grabbing
${snapshot.isDragging ? 'ring-2 ring-blue-500/50 shadow-lg z-50' : ''}
${isEditing ? 'ring-2 ring-yellow-500/50' : ''}
${isSelected ? 'ring-2 ring-purple-500/50' : ''}
transition-all duration-300
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setIsHovered(false);
setShowDeleteConfirm(false);
}}
>
{/* Scene Image */} {/* Scene Image */}
<div className="relative w-full h-[200px] flex-shrink-0"> <div className="relative w-full h-[200px] flex-shrink-0">
<Image <Image
@ -304,7 +325,5 @@ export function SceneCard({
</motion.div> </motion.div>
</motion.div> </motion.div>
</div> </div>
)}
</Draggable>
); );
} }

View File

@ -1,4 +1,3 @@
import { Droppable } from 'react-beautiful-dnd';
import { StoryboardScene } from '../pages/storyboard-view'; import { StoryboardScene } from '../pages/storyboard-view';
import { StoryboardCard } from './storyboard-card'; import { StoryboardCard } from './storyboard-card';
@ -18,27 +17,18 @@ export function StoryboardCardList({
onSceneDuplicate onSceneDuplicate
}: StoryboardCardListProps) { }: StoryboardCardListProps) {
return ( return (
<Droppable droppableId="storyboard-scenes"> <div className="flex gap-6 overflow-x-auto hide-scrollbar h-full overflow-y-hidden transition-colors duration-300 rounded-xl p-2">
{(provided) => ( {scenes.map((scene, index) => (
<div <StoryboardCard
ref={provided.innerRef} key={scene.id}
{...provided.droppableProps} scene={scene}
className="flex gap-6 overflow-x-auto hide-scrollbar h-full overflow-y-hidden p-8" index={index}
> isSelected={scene.id === selectedSceneId}
{scenes.map((scene, index) => ( onUpdate={(updates) => onSceneUpdate(scene.id, updates)}
<StoryboardCard onDelete={() => onSceneDelete(scene.id)}
key={scene.id} onDuplicate={() => onSceneDuplicate(scene.id)}
scene={scene} />
index={index} ))}
isSelected={selectedSceneId === scene.id} </div>
onUpdate={(updates) => onSceneUpdate(scene.id, updates)}
onDelete={() => onSceneDelete(scene.id)}
onDuplicate={() => onSceneDuplicate(scene.id)}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
); );
} }

View File

@ -1,7 +1,8 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Draggable } from 'react-beautiful-dnd'; import { useSortable } from '@dnd-kit/sortable';
import { Trash2, Copy, RefreshCw } from 'lucide-react'; import { CSS } from '@dnd-kit/utilities';
import { Trash2, Copy, RefreshCw, GripVertical } from 'lucide-react';
import { StoryboardScene } from '../pages/storyboard-view'; import { StoryboardScene } from '../pages/storyboard-view';
import Image from 'next/image'; import Image from 'next/image';
@ -29,6 +30,20 @@ export function StoryboardCard({
const cardRef = useRef<HTMLDivElement>(null); const cardRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<NodeJS.Timeout>(); const timeoutRef = useRef<NodeJS.Timeout>();
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: scene.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
// 处理自动保存 // 处理自动保存
const handleContentChange = ( const handleContentChange = (
field: keyof StoryboardScene, field: keyof StoryboardScene,
@ -67,244 +82,261 @@ export function StoryboardCard({
}, [isSelected]); }, [isSelected]);
return ( return (
<Draggable draggableId={scene.id} index={index}> <div
{(provided, snapshot) => ( ref={setNodeRef}
style={style}
className={`
relative flex-shrink-0 w-[400px] bg-white/5 backdrop-blur-sm rounded-xl overflow-hidden h-full
flex flex-col group cursor-grab active:cursor-grabbing
${isDragging ? 'ring-2 ring-blue-500/50 shadow-lg z-50' : ''}
${isEditing ? 'ring-2 ring-yellow-500/50' : ''}
${isSelected ? 'ring-2 ring-purple-500/50' : ''}
transition-all duration-300
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setIsHovered(false);
setShowDeleteConfirm(false);
}}
>
<motion.div
id={`scene-card-${scene.id}`}
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: 1,
x: isDragging ? 5 : 0,
y: isDragging ? 5 : 0,
rotate: isDragging ? 2 : 0
}}
transition={{
type: "spring",
stiffness: 200,
damping: 20
}}
className={`
relative flex-shrink-0 w-[400px] bg-white/5 backdrop-blur-sm rounded-xl overflow-hidden h-full
flex flex-col group cursor-grab active:cursor-grabbing
${isDragging ? 'ring-2 ring-blue-500/50 shadow-lg z-50' : ''}
${isEditing ? 'ring-2 ring-yellow-500/50' : ''}
${isSelected ? 'ring-2 ring-purple-500/50' : ''}
transition-all duration-300
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setIsHovered(false);
setShowDeleteConfirm(false);
}}
>
{/* Drag Handle */}
<div <div
ref={provided.innerRef} {...listeners}
{...provided.draggableProps} {...attributes}
{...provided.dragHandleProps} className="absolute top-2 right-2 z-10 p-2 rounded-lg bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing"
> >
<motion.div <GripVertical className="w-4 h-4 text-white/60" />
id={`scene-card-${scene.id}`} </div>
initial={{ opacity: 0, scale: 0.8 }}
animate={{ {/* Scene Image */}
opacity: 1, <div className="relative w-full h-[200px] flex-shrink-0">
scale: 1, <Image
x: snapshot.isDragging ? 5 : 0, src={scene.imageUrl}
y: snapshot.isDragging ? 5 : 0, alt={scene.name}
rotate: snapshot.isDragging ? 2 : 0 fill
}} className="object-cover"
transition={{ />
type: "spring", <div className="absolute inset-0 bg-gradient-to-t from-[#0C0E11] via-transparent to-transparent" />
stiffness: 200, </div>
damping: 20
}} {/* Content Area */}
className={` <div className="flex flex-col h-full">
relative flex-shrink-0 w-[400px] bg-white/5 backdrop-blur-sm rounded-xl overflow-hidden h-full {/* Scrollable Content */}
flex flex-col group cursor-grab active:cursor-grabbing <div className="flex-grow overflow-y-auto custom-scrollbar p-6 -mt-12">
${snapshot.isDragging ? 'ring-2 ring-blue-500/50 shadow-lg z-50' : ''} {/* Scene Name */}
${isEditing ? 'ring-2 ring-yellow-500/50' : ''} <motion.div
${isSelected ? 'ring-2 ring-purple-500/50' : ''} animate={{ scale: 0.8 }}
transition-all duration-300 transition={{ duration: 0.3 }}
`} className="text-xl font-semibold mb-3"
onMouseEnter={() => setIsHovered(true)} >
onMouseLeave={() => { <div
setIsHovered(false); contentEditable
setShowDeleteConfirm(false); suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('name', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors"
dangerouslySetInnerHTML={{ __html: scene.name }}
/>
</motion.div>
{/* Scene Description */}
<motion.div
animate={{ opacity: 1 }}
className="text-sm text-white/60 mb-4"
>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('description', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors"
dangerouslySetInnerHTML={{ __html: scene.description }}
/>
</motion.div>
{/* Shot Description */}
<div className="space-y-2 mb-4">
<label className="text-sm text-white/60">Shot Description</label>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('shot', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors text-white/80"
dangerouslySetInnerHTML={{ __html: scene.shot }}
/>
</div>
{/* Frame Description */}
<div className="space-y-2 mb-4">
<label className="text-sm text-white/60">Frame Description</label>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('frame', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors text-white/80"
dangerouslySetInnerHTML={{ __html: scene.frame }}
/>
</div>
{/* Atmosphere */}
<div className="space-y-2">
<label className="text-sm text-white/60">Atmosphere</label>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('atmosphere', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors text-white/80"
dangerouslySetInnerHTML={{ __html: scene.atmosphere }}
/>
</div>
</div>
</div>
{/* Floating Action Bar */}
<motion.div
initial={false}
animate={{
y: isHovered ? 0 : 100,
opacity: isHovered ? 1 : 0
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30
}}
className="absolute bottom-0 left-0 right-0 p-4 bg-black/30 backdrop-blur-sm
border-t border-white/10 flex items-center justify-end gap-2"
>
{/* Delete Button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="p-2 rounded-lg hover:bg-red-500/20 text-red-500
transition-colors relative group"
onClick={(e) => {
e.stopPropagation();
setShowDeleteConfirm(true);
}} }}
> >
{/* Scene Image */} <Trash2 className="w-5 h-5" />
<div className="relative w-full h-[200px] flex-shrink-0"> {/* Delete Confirmation */}
<Image {showDeleteConfirm && (
src={scene.imageUrl} <div
alt={scene.name} className="absolute bottom-full right-0 mb-2 p-2
fill bg-red-500 rounded-lg whitespace-nowrap flex items-center gap-2"
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#0C0E11] via-transparent to-transparent" />
</div>
{/* Content Area */}
<div className="flex flex-col h-full">
{/* Scrollable Content */}
<div className="flex-grow overflow-y-auto custom-scrollbar p-6 -mt-12">
{/* Scene Name */}
<motion.div
animate={{ scale: 0.8 }}
transition={{ duration: 0.3 }}
className="text-xl font-semibold mb-3"
>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('name', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors"
dangerouslySetInnerHTML={{ __html: scene.name }}
/>
</motion.div>
{/* Scene Description */}
<motion.div
animate={{ opacity: 1 }}
className="text-sm text-white/60 mb-4"
>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('description', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors"
dangerouslySetInnerHTML={{ __html: scene.description }}
/>
</motion.div>
{/* Shot Description */}
<div className="space-y-2 mb-4">
<label className="text-sm text-white/60">Shot Description</label>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('shot', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors text-white/80"
dangerouslySetInnerHTML={{ __html: scene.shot }}
/>
</div>
{/* Frame Description */}
<div className="space-y-2 mb-4">
<label className="text-sm text-white/60">Frame Description</label>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('frame', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors text-white/80"
dangerouslySetInnerHTML={{ __html: scene.frame }}
/>
</div>
{/* Atmosphere */}
<div className="space-y-2">
<label className="text-sm text-white/60">Atmosphere</label>
<div
contentEditable
suppressContentEditableWarning
onFocus={() => setIsEditing(true)}
onBlur={(e) => {
setIsEditing(false);
handleContentChange('atmosphere', e.currentTarget.textContent || '', e.currentTarget);
}}
className="outline-none focus:bg-white/5 rounded px-2 py-1
transition-colors text-white/80"
dangerouslySetInnerHTML={{ __html: scene.atmosphere }}
/>
</div>
</div>
</div>
{/* Floating Action Bar */}
<motion.div
initial={false}
animate={{
y: isHovered ? 0 : 100,
opacity: isHovered ? 1 : 0
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30
}}
className="absolute bottom-0 left-0 right-0 p-4 bg-black/30 backdrop-blur-sm
border-t border-white/10 flex items-center justify-end gap-2"
>
{/* Delete Button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="p-2 rounded-lg hover:bg-red-500/20 text-red-500
transition-colors relative group"
onClick={(e) => {
e.stopPropagation();
setShowDeleteConfirm(true);
}}
>
<Trash2 className="w-5 h-5" />
{/* Delete Confirmation */}
{showDeleteConfirm && (
<div
className="absolute bottom-full right-0 mb-2 p-2
bg-red-500 rounded-lg whitespace-nowrap flex items-center gap-2"
onClick={e => e.stopPropagation()}
>
<span>Confirm delete?</span>
<button
className="px-2 py-1 rounded bg-red-600 hover:bg-red-700
transition-colors text-white text-sm"
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
>
Confirm
</button>
<button
className="px-2 py-1 rounded bg-white/20 hover:bg-white/30
transition-colors text-white text-sm"
onClick={(e) => {
e.stopPropagation();
setShowDeleteConfirm(false);
}}
>
Cancel
</button>
</div>
)}
</motion.button>
{/* Duplicate Button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={(e) => {
e.stopPropagation();
onDuplicate();
}}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
>
<Copy className="w-5 h-5" />
</motion.button>
{/* Regenerate Button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
> >
<RefreshCw className="w-5 h-5" /> <span>Confirm delete?</span>
</motion.button> <button
</motion.div> className="px-2 py-1 rounded bg-red-600 hover:bg-red-700
transition-colors text-white text-sm"
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
>
Confirm
</button>
<button
className="px-2 py-1 rounded bg-white/20 hover:bg-white/30
transition-colors text-white text-sm"
onClick={(e) => {
e.stopPropagation();
setShowDeleteConfirm(false);
}}
>
Cancel
</button>
</div>
)}
</motion.button>
{/* Save Indicator */} {/* Duplicate Button */}
<motion.div <motion.button
initial={{ opacity: 0, y: 20 }} whileHover={{ scale: 1.05 }}
animate={{ opacity: showSaveIndicator ? 1 : 0, y: showSaveIndicator ? 0 : 20 }} whileTap={{ scale: 0.95 }}
className="absolute bottom-20 right-4 px-3 py-1.5 rounded-full onClick={(e) => {
bg-green-500/20 text-green-400 text-sm" e.stopPropagation();
> onDuplicate();
Saved }}
</motion.div> className="p-2 rounded-lg hover:bg-white/10 transition-colors"
</motion.div> >
</div> <Copy className="w-5 h-5" />
)} </motion.button>
</Draggable>
{/* Regenerate Button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={e => e.stopPropagation()}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
>
<RefreshCw className="w-5 h-5" />
</motion.button>
</motion.div>
{/* Save Indicator */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: showSaveIndicator ? 1 : 0, y: showSaveIndicator ? 0 : 20 }}
className="absolute bottom-20 right-4 px-3 py-1.5 rounded-full
bg-green-500/20 text-green-400 text-sm"
>
Saved
</motion.div>
</motion.div>
</div>
); );
} }

View File

@ -6,12 +6,28 @@ import {
} from './constants'; } from './constants';
// 当前选择的mock数据 // 当前选择的mock数据
let selectedMockData = getRandomMockData(); let selectedMockData: any = null;
// 加载mock数据的辅助函数
const loadMockData = async () => {
if (!selectedMockData) {
try {
selectedMockData = await getRandomMockData();
} catch (error) {
// 如果API失败使用本地fallback数据
const { MOCK_DATA } = await import('./constants');
const randomIndex = Math.floor(Math.random() * MOCK_DATA.length);
selectedMockData = MOCK_DATA[randomIndex];
console.log('使用本地fallback数据:', selectedMockData);
}
}
return selectedMockData;
};
// 模拟接口请求 获取任务详情 // 模拟接口请求 获取任务详情
export const getTaskDetail = async (taskId: string): Promise<TaskObject> => { export const getTaskDetail = async (taskId: string): Promise<TaskObject> => {
// 每次获取任务详情时重新随机选择数据 // 确保已经加载了数据
selectedMockData = getRandomMockData(); await loadMockData();
const data: TaskObject = { const data: TaskObject = {
projectId: selectedMockData.detail.projectId, projectId: selectedMockData.detail.projectId,
@ -32,6 +48,9 @@ export const getTaskSketch = async (
taskId: string, taskId: string,
onProgress: (sketch: SketchItem, index: number) => void onProgress: (sketch: SketchItem, index: number) => void
): Promise<void> => { ): Promise<void> => {
// 确保已经加载了数据
await loadMockData();
const sketchData = selectedMockData.sketch; const sketchData = selectedMockData.sketch;
const totalSketches = sketchData.length; const totalSketches = sketchData.length;
@ -71,6 +90,9 @@ export const getTaskVideo = async (
sketchCount: number, sketchCount: number,
onProgress: (video: VideoItem, index: number) => void onProgress: (video: VideoItem, index: number) => void
): Promise<void> => { ): Promise<void> => {
// 确保已经加载了数据
await loadMockData();
const videoData = selectedMockData.video; const videoData = selectedMockData.video;
const totalVideos = videoData.length; const totalVideos = videoData.length;