删除文件

This commit is contained in:
北枳 2025-08-29 03:56:39 +08:00
parent 8decd2893a
commit 2708f14378
2 changed files with 0 additions and 451 deletions

View File

@ -1,167 +0,0 @@
import React, { useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { motion } from 'framer-motion';
import { GripVertical, Trash2, Edit2, ChevronDown } from 'lucide-react';
import { DialogueItem as DialogueItemType, CharacterOption } from '@/app/model/enums';
import KeywordText from './keyword-text';
import * as Popover from '@radix-ui/react-popover';
import { mockCharacterOptions } from '@/app/model/enums';
import './style/dialogue-item.css';
interface DialogueItemProps {
dialogue: DialogueItemType;
onUpdate: (updates: Partial<DialogueItemType>) => void;
onDelete: () => void;
onCharacterChange?: (character: CharacterOption) => void;
}
const DialogueItem: React.FC<DialogueItemProps> = ({
dialogue,
onUpdate,
onDelete,
onCharacterChange,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(dialogue.text);
const [isCharacterSelectOpen, setIsCharacterSelectOpen] = useState(false);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: dialogue.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const handleSave = () => {
onUpdate({ text: editText });
setIsEditing(false);
};
const handleCharacterSelect = (character: CharacterOption) => {
onUpdate({ speaker: character.name });
onCharacterChange?.(character);
setIsCharacterSelectOpen(false);
};
return (
<motion.div
ref={setNodeRef}
style={style}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`
group relative flex items-start gap-3 p-3 rounded-lg
${isDragging ? 'bg-white/10' : 'hover:bg-white/5'}
transition-colors duration-200 dialogue-item
`}
>
{/* Drag Handle */}
<div
{...attributes}
{...listeners}
className="mt-1 cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-50 hover:opacity-100 transition-opacity"
>
<GripVertical className="w-4 h-4" />
</div>
{/* Content */}
<div className="flex-grow">
{/* Character Selector */}
<Popover.Root open={isCharacterSelectOpen} onOpenChange={setIsCharacterSelectOpen}>
<Popover.Trigger asChild>
<button
className="mb-1 px-2 py-0.5 text-sm font-medium text-gray-400 rounded
hover:bg-white/5 flex items-center gap-1 transition-colors group/select"
>
{dialogue.speaker}
<ChevronDown className="w-3 h-3 opacity-50 group-hover/select:opacity-100 transition-opacity" />
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="w-[200px] bg-black/90 backdrop-blur-xl rounded-lg border border-white/10
shadow-xl animate-in fade-in-80 z-50"
sideOffset={5}
>
<div className="p-1">
{mockCharacterOptions.map((char) => (
<button
key={char.characterId}
onClick={() => handleCharacterSelect(char)}
className="w-full px-3 py-2 text-sm text-left text-gray-200 rounded-md
hover:bg-white/5 transition-colors flex items-center gap-2"
>
<img
src={char.image}
alt={char.name}
className="w-6 h-6 rounded-full object-cover"
/>
{char.name}
</button>
))}
<div className="h-[1px] bg-white/10 my-1" />
<button
onClick={() => {
onUpdate({ speaker: '旁白' });
setIsCharacterSelectOpen(false);
}}
className="w-full px-3 py-2 text-sm text-left text-gray-400 rounded-md
hover:bg-white/5 transition-colors"
>
</button>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
{isEditing ? (
<div className="flex flex-col gap-2">
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
className="w-full bg-black/20 rounded-lg p-2 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-blue-500/50"
rows={3}
onBlur={() => {
handleSave();
setIsEditing(false);
}}
autoFocus
/>
</div>
) : (
<div className="text-sm text-gray-200" onClick={() => setIsEditing(true)}>
<KeywordText text={dialogue.text} />
</div>
)}
</div>
{/* Actions */}
<div className="flex gap-2 opacity-0 transition-opacity opt-btn-group">
<button
onClick={() => setIsEditing(true)}
className="p-1 rounded-md hover:bg-white/10 transition-colors"
>
<Edit2 className="w-4 h-4 text-gray-400" />
</button>
<button
onClick={onDelete}
className="p-1 rounded-md hover:bg-white/10 transition-colors"
>
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</motion.div>
);
};
export default DialogueItem;

View File

@ -1,284 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { motion, AnimatePresence } from 'framer-motion';
import { GripVertical, Trash2, Copy, Plus } from 'lucide-react';
import { StoryboardCard as StoryboardCardType, DialogueItem as DialogueItemType, CharacterOption, mockCharacterOptions } from '@/app/model/enums';
import DialogueItem from './dialogue-item';
import KeywordText from './keyword-text';
import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
interface StoryboardCardProps {
card: StoryboardCardType;
onUpdate: (updates: Partial<StoryboardCardType>) => void;
onDelete: () => void;
onDuplicate: () => void;
}
const EditableField: React.FC<{
label: string;
value: string;
onChange: (value: string) => void;
className?: string;
}> = ({ label, value, onChange, className = '' }) => {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleSave = () => {
onChange(editValue);
setIsEditing(false);
};
// 自动调整文本框高度
useEffect(() => {
if (isEditing && textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
}, [isEditing, editValue]);
return (
<div className={className}>
<div className="text-sm text-gray-400 mb-1">{label}</div>
{isEditing ? (
<textarea
ref={textareaRef}
value={editValue}
onChange={(e) => {
setEditValue(e.target.value);
// 调整高度
e.target.style.height = 'auto';
e.target.style.height = `${e.target.scrollHeight}px`;
}}
onBlur={handleSave}
autoFocus
className="w-full bg-black/20 rounded-lg p-2 text-sm
focus:outline-none focus:ring-1 focus:ring-blue-500/50
resize-none overflow-hidden min-h-[2.5rem]"
style={{
maxHeight: '20rem', // 防止内容过多时超出视口
}}
/>
) : (
<div
onClick={() => {
setIsEditing(true);
setEditValue(value); // 重置编辑值
}}
className="text-sm text-gray-200 cursor-text
hover:bg-white/5 rounded-lg p-2 transition-colors
line-clamp-3"
>
{value || `点击添加${label}...`}
</div>
)}
</div>
);
};
const StoryboardCard: React.FC<StoryboardCardProps> = ({
card,
onUpdate,
onDelete,
onDuplicate,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editNotes, setEditNotes] = useState(card.notes || '');
console.log('card', card);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: card.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const handleDialogueReorder = (event: any) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = card.dialogues.findIndex(d => d.id === active.id);
const newIndex = card.dialogues.findIndex(d => d.id === over?.id);
const newDialogues = [...card.dialogues];
const [removed] = newDialogues.splice(oldIndex, 1);
newDialogues.splice(newIndex, 0, removed);
onUpdate({ dialogues: newDialogues });
}
};
const handleDialogueUpdate = (dialogueId: string, updates: Partial<DialogueItemType>) => {
const newDialogues = card.dialogues.map(d =>
d.id === dialogueId ? { ...d, ...updates } : d
);
onUpdate({ dialogues: newDialogues });
};
const handleCharacterChange = (dialogueId: string, character: CharacterOption) => {
// 更新对话中的角色
handleDialogueUpdate(dialogueId, { speaker: character.name });
// 更新卡片的角色列表
const allCharacters = new Map(card.characters.map(char => [char.characterId, char]));
allCharacters.set(character.characterId, character);
// 获取所有对话中出现的角色
card.dialogues.forEach(dialogue => {
const char = mockCharacterOptions.find(c => c.name === dialogue.speaker);
if (char) {
allCharacters.set(char.characterId, char);
}
});
onUpdate({ characters: Array.from(allCharacters.values()) });
};
const handleAddDialogue = () => {
const newDialogue: DialogueItemType = {
id: `d${Date.now()}`,
speaker: '新角色',
text: '在此输入对话内容',
};
onUpdate({ dialogues: [...card.dialogues, newDialogue] });
};
const handleSaveNotes = () => {
onUpdate({ notes: editNotes });
setIsEditing(false);
};
return (
<motion.div
ref={setNodeRef}
style={style}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: 1,
scale: 1,
x: isDragging ? 5 : 0,
y: isDragging ? 5 : 0,
rotate: isDragging ? 2 : 0
}}
exit={{ opacity: 0, scale: 0.8 }}
className={`
group relative w-full bg-black/30 backdrop-blur-lg rounded-xl overflow-hidden
border border-white/10 shadow-xl h-[480px] flex flex-col
${isDragging ? 'z-50 shadow-2xl ring-2 ring-blue-500/50' : 'hover:ring-1 hover:ring-white/20'}
transition-all duration-200
`}
>
{/* Drag Handle */}
<div
{...attributes}
{...listeners}
className="absolute top-3 right-3 p-2 rounded-lg bg-black/20 opacity-0 group-hover:opacity-100
transition-opacity cursor-grab active:cursor-grabbing"
>
<GripVertical className="w-4 h-4 text-white/60" />
</div>
{/* Card Header */}
<div className="p-4 border-b border-white/10">
<div className="text-lg font-medium text-white mb-2">
{card.shotId}
</div>
<div className="flex flex-wrap gap-2 items-center">
{card.scene && <KeywordText text={`[${card.scene.name}]`} id={card.scene.sceneId} />}
{card.characters.map((char) => (
<span key={char.characterId} className="text-sm">
<KeywordText text={`#${char.name}#`} id={char.characterId} />
</span>
))}
</div>
</div>
{/* Dialogues */}
<div className="flex-1 overflow-hidden flex flex-col bg-black/10">
<div className="p-4 pb-2 flex-shrink-0 flex justify-between items-center">
<div className="text-sm text-gray-400"></div>
<button
onClick={handleAddDialogue}
className="px-2 py-1 rounded-md border border-dashed border-white/20
text-xs text-gray-400 hover:bg-white/5 transition-colors
flex items-center gap-1"
>
<Plus className="w-3 h-3" />
</button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar px-4 pb-4">
<DndContext
collisionDetection={closestCenter}
onDragEnd={handleDialogueReorder}
>
<SortableContext
items={card.dialogues.map(d => d.id)}
strategy={verticalListSortingStrategy}
>
<AnimatePresence>
{card.dialogues.map((dialogue) => (
<DialogueItem
key={dialogue.id}
dialogue={dialogue}
onUpdate={(updates) => handleDialogueUpdate(dialogue.id, updates)}
onDelete={() => {
const newDialogues = card.dialogues.filter(d => d.id !== dialogue.id);
onUpdate({ dialogues: newDialogues });
const remainingCharacters = new Map<string, CharacterOption>();
newDialogues.forEach(d => {
const char = mockCharacterOptions.find(c => c.name === d.speaker);
if (char) {
remainingCharacters.set(char.characterId, char);
}
});
onUpdate({ characters: Array.from(remainingCharacters.values()) });
}}
onCharacterChange={(character) => handleCharacterChange(dialogue.id, character)}
/>
))}
</AnimatePresence>
</SortableContext>
</DndContext>
</div>
</div>
{/* Shot Description */}
<div className="p-4 border-t border-white/10 bg-black/5">
<EditableField
label="分镜描述"
value={card.description}
onChange={(value) => onUpdate({ description: value })}
/>
</div>
{/* Card Actions */}
<div className="absolute top-3 right-[4rem] flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={onDuplicate}
className="p-2 rounded-lg bg-black/20 hover:bg-black/30 transition-colors"
title="复制"
>
<Copy className="w-4 h-4 text-blue-400" />
</button>
<button
onClick={onDelete}
className="p-2 rounded-lg bg-black/20 hover:bg-black/30 transition-colors"
title="删除"
>
<Trash2 className="w-4 h-4 text-red-400" />
</button>
</div>
</motion.div>
);
};
export default StoryboardCard;