video-flow-b/components/ui/script-tab-content.tsx
2025-07-29 10:57:25 +08:00

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;