forked from 77media/video-flow
273 lines
8.7 KiB
TypeScript
273 lines
8.7 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, Sparkles, Plus, X } from 'lucide-react';
|
||
import { cn } from '@/public/lib/utils';
|
||
import { GlassIconButton } from './glass-icon-button';
|
||
import { ReplaceCharacterModal } from './replace-character-modal';
|
||
import { Slider } from './slider';
|
||
import CharacterEditor from './character-editor';
|
||
|
||
interface Appearance {
|
||
hairStyle: string;
|
||
skinTone: string;
|
||
facialFeatures: string;
|
||
bodyType: string;
|
||
}
|
||
|
||
interface Role {
|
||
name: string;
|
||
url: string;
|
||
sound: string;
|
||
soundDescription: string;
|
||
roleDescription: string;
|
||
age: number;
|
||
gender: 'male' | 'female' | 'other';
|
||
ethnicity: string;
|
||
appearance: Appearance;
|
||
// 新增标签数组
|
||
tags: string[];
|
||
}
|
||
|
||
// Mock 数据
|
||
const mockRole: Role = {
|
||
name: "青春女学生",
|
||
url: "/assets/3dr_chihiro.png",
|
||
sound: "",
|
||
soundDescription: "",
|
||
roleDescription: "一位充满活力和梦想的高中女生,蓝色长发随风飘扬,眼神中透露着对未来的憧憬。她身着整洁的校服,举止优雅而不失活力。",
|
||
age: 16,
|
||
gender: 'female',
|
||
ethnicity: '亚洲人',
|
||
appearance: {
|
||
hairStyle: "鲜艳蓝色长发",
|
||
skinTone: "白皙",
|
||
facialFeatures: "大眼睛,清秀五官",
|
||
bodyType: "苗条"
|
||
},
|
||
tags: ['高中生', '校服', '蓝色长发', '大眼睛', '清秀五官', '苗条']
|
||
};
|
||
|
||
interface CharacterTabContentProps {
|
||
taskSketch: any[];
|
||
currentRoleIndex: number;
|
||
onSketchSelect: (index: number) => void;
|
||
roles: Role[];
|
||
}
|
||
|
||
export function CharacterTabContent({
|
||
taskSketch,
|
||
currentRoleIndex,
|
||
onSketchSelect,
|
||
roles = [mockRole]
|
||
}: CharacterTabContentProps) {
|
||
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
|
||
const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload');
|
||
const [newTag, setNewTag] = useState('');
|
||
const [localRole, setLocalRole] = useState(mockRole);
|
||
|
||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||
|
||
// 处理标签添加
|
||
const handleAddTag = () => {
|
||
if (newTag.trim() && !localRole.tags.includes(newTag.trim())) {
|
||
const newTagText = newTag.trim();
|
||
// 更新标签数组
|
||
const updatedTags = [...localRole.tags, newTagText];
|
||
// 更新角色描述文本
|
||
const updatedDescription = localRole.roleDescription + (localRole.roleDescription ? ',' : '') + newTagText;
|
||
|
||
setLocalRole({
|
||
...localRole,
|
||
tags: updatedTags,
|
||
roleDescription: updatedDescription
|
||
});
|
||
setNewTag('');
|
||
|
||
// 自动调整文本框高度
|
||
if (textareaRef.current) {
|
||
textareaRef.current.style.height = 'auto';
|
||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理标签删除
|
||
const handleRemoveTag = (tagToRemove: string) => {
|
||
setLocalRole({
|
||
...localRole,
|
||
tags: localRole.tags.filter(tag => tag !== tagToRemove)
|
||
});
|
||
};
|
||
|
||
// 处理年龄滑块变化
|
||
const handleAgeChange = (value: number[]) => {
|
||
setLocalRole({
|
||
...localRole,
|
||
age: value[0]
|
||
});
|
||
};
|
||
|
||
// 处理描述更新
|
||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||
setLocalRole({
|
||
...localRole,
|
||
roleDescription: e.target.value
|
||
});
|
||
|
||
// 自动调整文本框高度
|
||
if (textareaRef.current) {
|
||
textareaRef.current.style.height = 'auto';
|
||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
|
||
}
|
||
};
|
||
|
||
// 新增智能优化处理函数
|
||
const handleSmartOptimize = () => {
|
||
console.log('Optimizing character description...');
|
||
// TODO: 调用 AI 优化接口
|
||
};
|
||
|
||
// 如果没有角色数据,显示占位内容
|
||
if (!roles || roles.length === 0) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||
<Users className="w-16 h-16 mb-4" />
|
||
<p>No character data</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 获取当前选中的角色
|
||
const currentRole = roles[currentRoleIndex];
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
{/* 上部分:角色缩略图 */}
|
||
<motion.div
|
||
className="space-y-6"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
>
|
||
<div className="relative">
|
||
<div className="flex gap-4 overflow-x-auto p-2 hide-scrollbar">
|
||
{roles.map((role, index) => (
|
||
<motion.div
|
||
key={`role-${index}`}
|
||
className={cn(
|
||
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
|
||
'aspect-[9/16]',
|
||
currentRoleIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||
)}
|
||
onClick={() => onSketchSelect(index)}
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<img
|
||
src={role.url}
|
||
alt={role.name}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90 line-clamp-1">{role.name}</span>
|
||
</div>
|
||
</motion.div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* 下部分:角色详情 */}
|
||
<motion.div
|
||
className="grid grid-cols-2 gap-6 p-4"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.2 }}
|
||
>
|
||
{/* 左列:角色预览 */}
|
||
<div className="space-y-4">
|
||
{/* 角色预览图 */}
|
||
<div className="w-full mx-auto rounded-lg overflow-hidden relative group">
|
||
<img
|
||
src={currentRole.url}
|
||
alt={currentRole.name}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent
|
||
opacity-0 transition-opacity group-hover:opacity-100">
|
||
<div className="absolute bottom-4 left-4">
|
||
<GlassIconButton
|
||
icon={Wand2}
|
||
onClick={() => console.log('regenerate character')}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<button
|
||
onClick={() => console.log('Apply changes')}
|
||
className="flex items-center justify-center gap-2 px-4 py-3 bg-green-500/10 hover:bg-green-500/20
|
||
text-green-500 rounded-lg transition-colors"
|
||
>
|
||
<Check className="w-4 h-4" />
|
||
<span>应用</span>
|
||
</button>
|
||
<button
|
||
onClick={() => console.log('Regenerate')}
|
||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||
text-blue-500 rounded-lg transition-colors"
|
||
>
|
||
<RefreshCw className="w-4 h-4" />
|
||
<span>重新生成</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 右列:角色信息 */}
|
||
<div className="space-y-4">
|
||
<CharacterEditor
|
||
initialDescription={localRole.roleDescription}
|
||
onDescriptionChange={(description) => {
|
||
setLocalRole({
|
||
...localRole,
|
||
roleDescription: description
|
||
});
|
||
}}
|
||
onAttributesChange={(attributes) => {
|
||
const newRole = { ...localRole };
|
||
|
||
attributes.forEach(attr => {
|
||
switch (attr.key) {
|
||
case 'age':
|
||
newRole.age = parseInt(attr.value);
|
||
break;
|
||
case 'gender':
|
||
if (attr.value === '男性') {
|
||
newRole.gender = 'male';
|
||
} else if (attr.value === '女性') {
|
||
newRole.gender = 'female';
|
||
} else {
|
||
newRole.gender = 'other';
|
||
}
|
||
break;
|
||
case 'hair':
|
||
newRole.appearance.hairStyle = attr.value;
|
||
break;
|
||
case 'skin':
|
||
newRole.appearance.skinTone = attr.value;
|
||
break;
|
||
case 'build':
|
||
newRole.appearance.bodyType = attr.value;
|
||
break;
|
||
}
|
||
});
|
||
|
||
setLocalRole(newRole);
|
||
}}
|
||
/>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
);
|
||
}
|