forked from 77media/video-flow
148 lines
5.3 KiB
TypeScript
148 lines
5.3 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Plus } from 'lucide-react';
|
|
import { DndContext, closestCenter, DragEndEvent } from '@dnd-kit/core';
|
|
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
|
|
import { StoryboardCard as StoryboardCardType, mockStoryboards, mockSceneOptions } from '@/app/model/enums';
|
|
import FilterBar from './filter-bar';
|
|
import StoryboardCard from './storyboard-card';
|
|
|
|
const ScriptTabContent: React.FC = () => {
|
|
const [cards, setCards] = useState<StoryboardCardType[]>(mockStoryboards);
|
|
const [selectedScenes, setSelectedScenes] = useState<string[]>([]);
|
|
const [selectedCharacters, setSelectedCharacters] = useState<string[]>([]);
|
|
|
|
// 筛选卡片
|
|
const filteredCards = cards.filter(card => {
|
|
const matchesScene = selectedScenes.length === 0 ||
|
|
selectedScenes.includes(card.scene?.sceneId || '');
|
|
const matchesCharacter = selectedCharacters.length === 0 ||
|
|
card.characters.some(char => selectedCharacters.includes(char.characterId));
|
|
return matchesScene && matchesCharacter;
|
|
});
|
|
|
|
// 处理卡片拖拽
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
const { active, over } = event;
|
|
if (active.id !== over?.id) {
|
|
const oldIndex = cards.findIndex(c => c.id === active.id);
|
|
const newIndex = cards.findIndex(c => c.id === over?.id);
|
|
const newCards = [...cards];
|
|
const [removed] = newCards.splice(oldIndex, 1);
|
|
newCards.splice(newIndex, 0, removed);
|
|
setCards(newCards);
|
|
}
|
|
};
|
|
|
|
// 处理卡片更新
|
|
const handleCardUpdate = useCallback((cardId: string, updates: Partial<StoryboardCardType>) => {
|
|
setCards(cards => cards.map(card =>
|
|
card.id === cardId ? { ...card, ...updates } : card
|
|
));
|
|
}, []);
|
|
|
|
// 处理卡片删除
|
|
const handleCardDelete = useCallback((cardId: string) => {
|
|
setCards(cards => cards.filter(card => card.id !== cardId));
|
|
}, []);
|
|
|
|
// 处理卡片复制
|
|
const handleCardDuplicate = useCallback((cardId: string) => {
|
|
const card = cards.find(c => c.id === cardId);
|
|
if (card) {
|
|
const newCard: StoryboardCardType = {
|
|
...card,
|
|
id: `card-${Date.now()}`,
|
|
shotId: `SC-${cards.length + 1}`,
|
|
dialogues: card.dialogues.map(d => ({ ...d, id: `d${Date.now()}-${d.id}` })),
|
|
};
|
|
setCards(cards => [...cards, newCard]);
|
|
}
|
|
}, [cards]);
|
|
|
|
// 添加新卡片
|
|
const handleAddCard = () => {
|
|
const newCard: StoryboardCardType = {
|
|
id: `card-${Date.now()}`,
|
|
shotId: `SC-${cards.length + 1}`,
|
|
scene: undefined,
|
|
characters: [],
|
|
dialogues: [],
|
|
description: '',
|
|
shotType: '',
|
|
cameraMove: '',
|
|
};
|
|
setCards(cards => [...cards, newCard]);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* 筛选栏 - 固定在顶部 */}
|
|
<div className="flex-shrink-0 bg-black/20 backdrop-blur-sm z-10">
|
|
<FilterBar
|
|
selectedScenes={selectedScenes}
|
|
selectedCharacters={selectedCharacters}
|
|
onScenesChange={setSelectedScenes}
|
|
onCharactersChange={setSelectedCharacters}
|
|
onReset={() => {
|
|
setSelectedScenes([]);
|
|
setSelectedCharacters([]);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 卡片网格 - 可滚动区域 */}
|
|
<div className="flex-1 overflow-y-auto pt-4">
|
|
<DndContext
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<SortableContext
|
|
items={[...filteredCards.map(card => card.id), 'add-card']}
|
|
strategy={rectSortingStrategy}
|
|
>
|
|
<div
|
|
className="grid auto-rows-min gap-6 w-full"
|
|
style={{
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
|
|
justifyItems: 'center'
|
|
}}
|
|
>
|
|
<AnimatePresence>
|
|
{filteredCards.map((card) => (
|
|
<div key={card.id} className="w-full max-w-[480px]">
|
|
<StoryboardCard
|
|
card={card}
|
|
onUpdate={(updates) => handleCardUpdate(card.id, updates)}
|
|
onDelete={() => handleCardDelete(card.id)}
|
|
onDuplicate={() => handleCardDuplicate(card.id)}
|
|
/>
|
|
</div>
|
|
))}
|
|
{/* 添加卡片占位符 */}
|
|
<motion.div
|
|
key="add-card"
|
|
layout
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
onClick={handleAddCard}
|
|
className="w-full max-w-[480px] h-[480px] bg-black/20 backdrop-blur-sm rounded-xl
|
|
border border-dashed border-white/10 cursor-pointer
|
|
flex items-center justify-center
|
|
hover:bg-black/30 hover:border-white/20 transition-all duration-200
|
|
group"
|
|
>
|
|
<Plus className="w-8 h-8 text-white/40 group-hover:text-white/60 transition-colors" />
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ScriptTabContent;
|