455 lines
16 KiB
TypeScript
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;
|