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

273 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}