mixvideo-v2/apps/desktop/src/components/ModelCard.tsx

455 lines
16 KiB
TypeScript

import React, { useState } from 'react';
import { Model, ModelStatus, Gender, ModelViewMode } from '../types/model';
import {
PencilIcon,
TrashIcon,
StarIcon,
UserIcon,
TagIcon,
CalendarIcon,
EyeIcon,
HeartIcon,
ShareIcon,
PhotoIcon
} from '@heroicons/react/24/outline';
import {
HeartIcon as HeartIconSolid
} from '@heroicons/react/24/solid';
interface ModelCardProps {
model: Model;
viewMode: ModelViewMode;
onEdit: () => void;
onDelete: () => void;
onSelect?: () => void;
onFavorite?: (id: string) => void;
isFavorite?: boolean;
isLoading?: boolean;
}
const ModelCard: React.FC<ModelCardProps> = ({
model,
viewMode,
onEdit,
onDelete,
onSelect,
onFavorite,
isFavorite = false,
isLoading = false
}) => {
const [isHovered, setIsHovered] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const getStatusColor = (status: ModelStatus) => {
switch (status) {
case ModelStatus.Active:
return 'bg-green-100 text-green-800 border border-green-200';
case ModelStatus.Inactive:
return 'bg-gray-100 text-gray-800 border border-gray-200';
case ModelStatus.Retired:
return 'bg-red-100 text-red-800 border border-red-200';
case ModelStatus.Suspended:
return 'bg-yellow-100 text-yellow-800 border border-yellow-200';
default:
return 'bg-gray-100 text-gray-800 border border-gray-200';
}
};
const getStatusText = (status: ModelStatus) => {
switch (status) {
case ModelStatus.Active:
return '活跃';
case ModelStatus.Inactive:
return '不活跃';
case ModelStatus.Retired:
return '退役';
case ModelStatus.Suspended:
return '暂停';
default:
return '未知';
}
};
const getGenderText = (gender: Gender) => {
switch (gender) {
case Gender.Male:
return '男';
case Gender.Female:
return '女';
case Gender.Other:
return '其他';
default:
return '未知';
}
};
const avatarUrl = model.avatar_path || (model.photos.length > 0 ? model.photos[0].file_path : null);
const renderRating = (rating: number) => {
return (
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<StarIcon
key={i}
className={`h-4 w-4 transition-colors ${i < rating
? 'text-yellow-400 fill-current'
: 'text-gray-200'
}`}
/>
))}
<span className="text-sm font-medium text-gray-600 ml-1">
{rating.toFixed(1)}
</span>
</div>
);
};
if (isLoading) {
return (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="aspect-[3/4] bg-gray-200 loading-shimmer" />
<div className="p-5 space-y-3">
<div className="h-6 bg-gray-200 rounded loading-shimmer" />
<div className="h-4 bg-gray-200 rounded w-3/4 loading-shimmer" />
<div className="h-4 bg-gray-200 rounded w-1/2 loading-shimmer" />
</div>
</div>
);
}
if (viewMode === ModelViewMode.List) {
// 列表视图
return (
<div className="group bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md hover:border-primary-200 transition-all duration-300 animate-fade-in">
<div className="flex">
{/* 头像 */}
<div className="relative w-24 min-h-full flex-shrink-0">
{avatarUrl && !imageError ? (
<img
src={avatarUrl}
alt={model.name}
className={`w-full h-full object-cover transition-all duration-300 ${imageLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={() => setImageLoaded(true)}
onError={() => setImageError(true)}
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-primary-50 to-primary-100 flex items-center justify-center">
<UserIcon className="h-8 w-8 text-primary-300" />
</div>
)}
{/* 状态指示器 */}
<div className="absolute top-2 right-2">
<div className={`w-3 h-3 rounded-full ${model.status === ModelStatus.Active ? 'bg-green-400' : 'bg-gray-300'
}`} />
</div>
</div>
{/* 内容 */}
<div className="flex-1 p-4 min-w-0">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-gray-900 truncate group-hover:text-primary-600 transition-colors">
{model.name}
</h3>
{model.stage_name && (
<p className="text-sm text-gray-500 truncate">: {model.stage_name}</p>
)}
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<span className="flex items-center gap-1">
<UserIcon className="h-4 w-4" />
{getGenderText(model.gender)}
</span>
{model.age && <span>{model.age}</span>}
{model.height && <span>{model.height}cm</span>}
<span className="flex items-center gap-1">
<PhotoIcon className="h-4 w-4" />
{model.photos.length}
</span>
</div>
{/* 标签 */}
{model.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{model.tags.slice(0, 3).map((tag, index) => (
<span
key={index}
className="px-2 py-1 bg-primary-50 text-primary-600 text-xs font-medium rounded-full"
>
{tag}
</span>
))}
{model.tags.length > 3 && (
<span className="px-2 py-1 bg-gray-100 text-gray-500 text-xs font-medium rounded-full">
+{model.tags.length - 3}
</span>
)}
</div>
)}
</div>
{/* 评分和操作 */}
<div className="flex flex-col items-end gap-2 ml-4">
{model.rating && renderRating(model.rating)}
<div className="flex gap-1">
<button
onClick={(e) => {
e.stopPropagation();
onFavorite?.(model.id);
}}
className={`p-2 rounded-lg transition-all duration-200 ${isFavorite
? 'bg-red-100 text-red-600'
: 'bg-gray-100 text-gray-600 hover:bg-red-50 hover:text-red-600'
}`}
>
{isFavorite ? (
<HeartIconSolid className="h-4 w-4" />
) : (
<HeartIcon className="h-4 w-4" />
)}
</button>
<button
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
className="p-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-blue-50 hover:text-blue-600 transition-all duration-200"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="p-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-red-50 hover:text-red-600 transition-all duration-200"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 网格视图 - 增强的现代化设计
return (
<div
className="group relative bg-gradient-to-br from-white to-gray-50/50 hover:from-white hover:to-primary-50/30 rounded-2xl shadow-sm border border-gray-100/50 overflow-hidden hover:shadow-xl hover:border-primary-200 transition-all duration-500 hover:-translate-y-2 hover:scale-[1.02] animate-scale-in"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onSelect}
>
{/* 装饰性背景 */}
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-br from-primary-100/30 to-primary-200/30 rounded-full -translate-y-10 translate-x-10 opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
{/* 封面图片容器 */}
<div className="relative aspect-[4/5] bg-gradient-to-br from-gray-50 to-gray-100 overflow-hidden">
{/* 图片 */}
{avatarUrl && !imageError ? (
<img
src={avatarUrl}
alt={model.name}
className={`w-full h-full object-cover transition-all duration-500 group-hover:scale-105 ${imageLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={() => setImageLoaded(true)}
onError={() => setImageError(true)}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100">
<div className="text-center">
<PhotoIcon className="h-16 w-16 text-primary-300 mx-auto mb-2" />
<p className="text-sm text-primary-400 font-medium"></p>
</div>
</div>
)}
{/* 加载状态 */}
{!imageLoaded && !imageError && avatarUrl && (
<div className="absolute inset-0 bg-gray-200 loading-shimmer" />
)}
{/* 悬停遮罩 */}
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300" />
{/* 状态标签 */}
<div className="absolute top-3 right-3">
<span className={`px-3 py-1 rounded-full text-xs font-semibold backdrop-blur-sm ${getStatusColor(model.status)}`}>
{getStatusText(model.status)}
</span>
</div>
{/* 收藏按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
onFavorite?.(model.id);
}}
className={`absolute top-3 left-3 p-2 rounded-full backdrop-blur-sm transition-all duration-200 focus-ring ${isFavorite
? 'bg-red-500 text-white shadow-lg'
: 'bg-white bg-opacity-80 text-gray-600 hover:bg-red-500 hover:text-white'
}`}
>
{isFavorite ? (
<HeartIconSolid className="h-4 w-4" />
) : (
<HeartIcon className="h-4 w-4" />
)}
</button>
{/* 照片数量指示器 */}
{model.photos.length > 0 && (
<div className="absolute bottom-3 left-3 flex items-center gap-1 px-2 py-1 bg-black bg-opacity-50 rounded-full text-white text-xs backdrop-blur-sm">
<PhotoIcon className="h-3 w-3" />
<span>{model.photos.length}</span>
</div>
)}
{/* 快速操作按钮 */}
<div className={`absolute bottom-3 right-3 flex gap-2 transition-all duration-300 ${isHovered ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'
}`}>
<button
onClick={(e) => {
e.stopPropagation();
onSelect?.();
}}
className="p-2 bg-white bg-opacity-90 hover:bg-primary-500 hover:text-white rounded-full shadow-lg transition-all duration-200 focus-ring"
title="查看详情"
>
<EyeIcon className="h-4 w-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
className="p-2 bg-white bg-opacity-90 hover:bg-blue-500 hover:text-white rounded-full shadow-lg transition-all duration-200 focus-ring"
title="编辑"
>
<PencilIcon className="h-4 w-4" />
</button>
</div>
</div>
{/* 模特信息 */}
<div className="p-4">
{/* 标题和评分 */}
<div className="flex items-start justify-between mb-2.5">
<div className="flex-1 min-w-0">
<h3 className="text-base font-bold text-gray-900 truncate mb-0.5 group-hover:text-primary-600 transition-colors">
{model.name}
</h3>
{model.stage_name && (
<p className="text-xs text-gray-500 truncate">: {model.stage_name}</p>
)}
</div>
{/* 评分 */}
{model.rating && (
<div className="flex items-center gap-1 ml-3">
<div className="flex">
{[...Array(5)].map((_, i) => (
<StarIcon
key={i}
className={`h-4 w-4 transition-colors ${i < model.rating!
? 'text-yellow-400 fill-current'
: 'text-gray-200'
}`}
/>
))}
</div>
<span className="text-sm font-medium text-gray-600 ml-1">
{model.rating.toFixed(1)}
</span>
</div>
)}
</div>
{/* 基本信息 */}
<div className="flex items-center gap-3 text-xs text-gray-500 mb-3">
<span className="flex items-center gap-1">
<UserIcon className="h-3 w-3" />
{getGenderText(model.gender)}
</span>
{model.age && (
<span className="flex items-center gap-1">
<CalendarIcon className="h-3 w-3" />
{model.age}
</span>
)}
{model.height && (
<span className="text-xs bg-gray-100 px-1.5 py-0.5 rounded-full">
{model.height}cm
</span>
)}
</div>
{/* 标签 */}
{model.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
{model.tags.slice(0, 2).map((tag, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-2 py-0.5 bg-primary-50 text-primary-600 text-xs font-medium rounded-md"
>
<TagIcon className="h-2.5 w-2.5" />
{tag}
</span>
))}
{model.tags.length > 2 && (
<span className="inline-flex items-center px-2 py-0.5 bg-gray-100 text-gray-500 text-xs font-medium rounded-md">
+{model.tags.length - 2}
</span>
)}
</div>
)}
{/* 操作按钮 */}
<div className="flex gap-1.5 pt-2 border-t border-gray-100">
<button
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-lg hover:from-blue-600 hover:to-blue-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium focus-ring"
>
<PencilIcon className="h-3.5 w-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="flex items-center justify-center px-3 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-red-50 hover:text-red-600 transition-all duration-200 text-sm font-medium focus-ring"
title="删除"
>
<TrashIcon className="h-3.5 w-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
// 分享功能
}}
className="flex items-center justify-center px-3 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-green-50 hover:text-green-600 transition-all duration-200 text-sm font-medium focus-ring"
title="分享"
>
<ShareIcon className="h-3.5 w-3.5" />
</button>
</div>
</div>
</div>
);
};
export default ModelCard;