forked from 77media/video-flow
修复编译问题
This commit is contained in:
parent
94acd18075
commit
7603a1a26e
@ -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 */}
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user