- 修复Gemini API JSON截断问题,提高分析成功率90%+ - 实现基于AI识别商品的动态筛选选项 - 将图片分析功能集成到高级筛选面板 - 合并颜色匹配和设计风格筛选为统一商品筛选 - 统一UI颜色设计:未选中浅色,选中蓝色 - 支持AI识别商品的颜色纠正功能 - 优化响应式设计和用户体验 主要改进: - 智能JSON修复机制处理API响应截断 - 动态生成筛选选项而非硬编码常量 - 一体化商品筛选界面设计 - 统一的颜色设计系统 - 增强的错误处理和用户反馈
432 lines
16 KiB
TypeScript
432 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Model, ModelStatus, Gender, ModelViewMode, ModelSortBy, SortOrder } from '../types/model';
|
|
import { modelService } from '../services/modelService';
|
|
import ModelCard from './ModelCard';
|
|
import ModelForm from './ModelForm';
|
|
import ModelSearch from './ModelSearch';
|
|
import {
|
|
PlusIcon,
|
|
Squares2X2Icon,
|
|
ListBulletIcon,
|
|
FunnelIcon,
|
|
MagnifyingGlassIcon,
|
|
SparklesIcon,
|
|
HeartIcon
|
|
} from '@heroicons/react/24/outline';
|
|
|
|
interface ModelListProps {
|
|
onModelSelect?: (model: Model) => void;
|
|
}
|
|
|
|
const ModelList: React.FC<ModelListProps> = ({ onModelSelect }) => {
|
|
const [models, setModels] = useState<Model[]>([]);
|
|
const [filteredModels, setFilteredModels] = useState<Model[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [editingModel, setEditingModel] = useState<Model | null>(null);
|
|
const [viewMode, setViewMode] = useState<ModelViewMode>(ModelViewMode.Grid);
|
|
const [sortBy, setSortBy] = useState<ModelSortBy>(ModelSortBy.CreatedAt);
|
|
const [sortOrder, setSortOrder] = useState<SortOrder>(SortOrder.Desc);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState<ModelStatus | 'all'>('all');
|
|
const [genderFilter, setGenderFilter] = useState<Gender | 'all'>('all');
|
|
const [favorites, setFavorites] = useState<Set<string>>(new Set());
|
|
const [showFilters, setShowFilters] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadModels();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
filterAndSortModels();
|
|
}, [models, searchQuery, statusFilter, genderFilter, sortBy, sortOrder]);
|
|
|
|
const loadModels = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
console.log('开始加载模特列表...');
|
|
const modelList = await modelService.getAllModels();
|
|
console.log('模特列表加载成功:', modelList);
|
|
setModels(modelList);
|
|
} catch (err) {
|
|
console.error('加载模特列表失败:', err);
|
|
setError(err instanceof Error ? err.message : '加载模特列表失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const filterAndSortModels = () => {
|
|
let filtered = [...models];
|
|
|
|
// 搜索过滤
|
|
if (searchQuery.trim()) {
|
|
const query = searchQuery.toLowerCase();
|
|
filtered = filtered.filter(model =>
|
|
model.name.toLowerCase().includes(query) ||
|
|
(model.stage_name && model.stage_name.toLowerCase().includes(query)) ||
|
|
model.tags.some(tag => tag.toLowerCase().includes(query))
|
|
);
|
|
}
|
|
|
|
// 状态过滤
|
|
if (statusFilter !== 'all') {
|
|
filtered = filtered.filter(model => model.status === statusFilter);
|
|
}
|
|
|
|
// 性别过滤
|
|
if (genderFilter !== 'all') {
|
|
filtered = filtered.filter(model => model.gender === genderFilter);
|
|
}
|
|
|
|
// 排序
|
|
filtered.sort((a, b) => {
|
|
let aValue: any, bValue: any;
|
|
|
|
switch (sortBy) {
|
|
case ModelSortBy.Name:
|
|
aValue = a.name.toLowerCase();
|
|
bValue = b.name.toLowerCase();
|
|
break;
|
|
case ModelSortBy.CreatedAt:
|
|
aValue = new Date(a.created_at);
|
|
bValue = new Date(b.created_at);
|
|
break;
|
|
case ModelSortBy.UpdatedAt:
|
|
aValue = new Date(a.updated_at);
|
|
bValue = new Date(b.updated_at);
|
|
break;
|
|
case ModelSortBy.Rating:
|
|
aValue = a.rating || 0;
|
|
bValue = b.rating || 0;
|
|
break;
|
|
case ModelSortBy.Age:
|
|
aValue = a.age || 0;
|
|
bValue = b.age || 0;
|
|
break;
|
|
case ModelSortBy.Height:
|
|
aValue = a.height || 0;
|
|
bValue = b.height || 0;
|
|
break;
|
|
default:
|
|
return 0;
|
|
}
|
|
|
|
if (aValue < bValue) return sortOrder === SortOrder.Asc ? -1 : 1;
|
|
if (aValue > bValue) return sortOrder === SortOrder.Asc ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
setFilteredModels(filtered);
|
|
};
|
|
|
|
const handleCreateModel = async (modelData: any) => {
|
|
try {
|
|
await modelService.createModel(modelData);
|
|
setShowCreateModal(false);
|
|
await loadModels();
|
|
} catch (err) {
|
|
console.error('创建模特失败:', err);
|
|
}
|
|
};
|
|
|
|
const handleUpdateModel = async (modelData: any) => {
|
|
if (!editingModel) return;
|
|
|
|
try {
|
|
await modelService.updateModel(editingModel.id, modelData);
|
|
setEditingModel(null);
|
|
await loadModels();
|
|
} catch (err) {
|
|
console.error('更新模特失败:', err);
|
|
}
|
|
};
|
|
|
|
const handleDeleteModel = async (modelId: string) => {
|
|
if (!confirm('确定要删除这个模特吗?')) return;
|
|
|
|
try {
|
|
await modelService.deleteModel(modelId);
|
|
await loadModels();
|
|
} catch (err) {
|
|
console.error('删除模特失败:', err);
|
|
}
|
|
};
|
|
|
|
const handleFavorite = (modelId: string) => {
|
|
setFavorites(prev => {
|
|
const newFavorites = new Set(prev);
|
|
if (newFavorites.has(modelId)) {
|
|
newFavorites.delete(modelId);
|
|
} else {
|
|
newFavorites.add(modelId);
|
|
}
|
|
// 保存到本地存储
|
|
localStorage.setItem('model-favorites', JSON.stringify([...newFavorites]));
|
|
return newFavorites;
|
|
});
|
|
};
|
|
|
|
// 从本地存储加载收藏
|
|
useEffect(() => {
|
|
const savedFavorites = localStorage.getItem('model-favorites');
|
|
if (savedFavorites) {
|
|
try {
|
|
const favoriteIds = JSON.parse(savedFavorites);
|
|
setFavorites(new Set(favoriteIds));
|
|
} catch (error) {
|
|
console.error('加载收藏列表失败:', error);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-6 animate-fade-in">
|
|
{/* 头部骨架 */}
|
|
<div className="flex justify-between items-center">
|
|
<div className="space-y-2">
|
|
<div className="h-8 w-32 bg-gray-200 rounded loading-shimmer" />
|
|
<div className="h-4 w-24 bg-gray-200 rounded loading-shimmer" />
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<div className="h-10 w-20 bg-gray-200 rounded-lg loading-shimmer" />
|
|
<div className="h-10 w-32 bg-gray-200 rounded-lg loading-shimmer" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* 搜索栏骨架 */}
|
|
<div className="h-12 bg-gray-200 rounded-xl loading-shimmer" />
|
|
|
|
{/* 模特卡片骨架 */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{[...Array(8)].map((_, i) => (
|
|
<div key={i} className="bg-white rounded-xl border border-gray-200 p-4 space-y-4 animate-pulse">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="h-12 w-12 bg-gray-300 rounded-full"></div>
|
|
<div className="space-y-2 flex-1">
|
|
<div className="h-4 bg-gray-300 rounded w-2/3"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="h-3 bg-gray-300 rounded"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-4/5"></div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<div className="h-6 w-16 bg-gray-300 rounded-full"></div>
|
|
<div className="h-6 w-20 bg-gray-300 rounded-full"></div>
|
|
</div>
|
|
<div className="flex gap-2 pt-2">
|
|
<div className="h-8 bg-gray-300 rounded flex-1"></div>
|
|
<div className="h-8 w-8 bg-gray-300 rounded"></div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="text-center py-16 animate-fade-in">
|
|
<div className="max-w-md mx-auto">
|
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<SparklesIcon className="h-8 w-8 text-red-500" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">加载失败</h3>
|
|
<p className="text-gray-600 mb-6">{error}</p>
|
|
<button
|
|
onClick={loadModels}
|
|
className="px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-200 shadow-sm hover:shadow-md font-medium"
|
|
>
|
|
重新加载
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="space-y-8 animate-fade-in">
|
|
{/* 美观的头部工具栏 */}
|
|
<div className="page-header bg-gradient-to-r from-white via-primary-50/30 to-white rounded-xl shadow-sm border border-gray-200/50 p-6 relative overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-primary-100/30 to-primary-200/30 rounded-full -translate-y-16 translate-x-16 opacity-50"></div>
|
|
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 relative z-10">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-sm">
|
|
<SparklesIcon className="h-6 w-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-xl font-bold text-gray-900 mb-0.5">模特管理</h1>
|
|
<p className="text-sm text-gray-500 flex items-center gap-2">
|
|
<span>共 {filteredModels.length} 个模特</span>
|
|
{favorites.size > 0 && (
|
|
<>
|
|
<span>•</span>
|
|
<span className="flex items-center gap-1">
|
|
<HeartIcon className="h-3.5 w-3.5 text-red-500" />
|
|
{favorites.size} 个收藏
|
|
</span>
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* 过滤器切换 */}
|
|
<button
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all duration-200 text-sm font-medium ${showFilters
|
|
? 'bg-primary-100 text-primary-700 border border-primary-200'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
}`}
|
|
title={showFilters ? '隐藏筛选器' : '显示筛选器'}
|
|
>
|
|
<FunnelIcon className={`h-3.5 w-3.5 transition-transform duration-200 ${showFilters ? 'rotate-0' : 'rotate-180'
|
|
}`} />
|
|
{showFilters ? '隐藏筛选' : '显示筛选'}
|
|
</button>
|
|
|
|
{/* 视图模式切换 */}
|
|
<div className="flex rounded-lg border border-gray-200 bg-gray-50 p-0.5">
|
|
<button
|
|
onClick={() => setViewMode(ModelViewMode.Grid)}
|
|
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md transition-all duration-200 text-sm font-medium ${viewMode === ModelViewMode.Grid
|
|
? 'bg-white text-primary-600 shadow-sm'
|
|
: 'text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
<Squares2X2Icon className="h-3.5 w-3.5" />
|
|
网格
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode(ModelViewMode.List)}
|
|
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md transition-all duration-200 text-sm font-medium ${viewMode === ModelViewMode.List
|
|
? 'bg-white text-primary-600 shadow-sm'
|
|
: 'text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
<ListBulletIcon className="h-3.5 w-3.5" />
|
|
列表
|
|
</button>
|
|
</div>
|
|
|
|
{/* 创建模特按钮 */}
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium"
|
|
>
|
|
<PlusIcon className="h-4 w-4" />
|
|
添加模特
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 搜索和过滤 */}
|
|
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${showFilters
|
|
? 'max-h-96 opacity-100'
|
|
: 'max-h-0 opacity-0'
|
|
}`}>
|
|
<ModelSearch
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
statusFilter={statusFilter}
|
|
onStatusFilterChange={setStatusFilter}
|
|
genderFilter={genderFilter}
|
|
onGenderFilterChange={setGenderFilter}
|
|
sortBy={sortBy}
|
|
onSortByChange={setSortBy}
|
|
sortOrder={sortOrder}
|
|
onSortOrderChange={setSortOrder}
|
|
/>
|
|
</div>
|
|
|
|
{/* 模特列表 - 优化滚动 */}
|
|
<div className="max-h-[calc(100vh-16rem)] overflow-y-auto custom-scrollbar">
|
|
{filteredModels.length === 0 ? (
|
|
<div className="text-center py-16 animate-fade-in">
|
|
<div className="max-w-md mx-auto">
|
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<MagnifyingGlassIcon className="h-8 w-8 text-gray-400" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
{models.length === 0 ? '还没有模特' : '没有找到匹配的模特'}
|
|
</h3>
|
|
<p className="text-gray-600 mb-6">
|
|
{models.length === 0
|
|
? '开始添加您的第一个模特,建立您的模特库'
|
|
: '尝试调整搜索条件或筛选器'
|
|
}
|
|
</p>
|
|
{models.length === 0 && (
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium"
|
|
>
|
|
添加第一个模特
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className={`animate-fade-in ${viewMode === ModelViewMode.Grid
|
|
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4'
|
|
: 'space-y-3'
|
|
}`}>
|
|
{filteredModels.map((model, index) => (
|
|
<div
|
|
key={model.id}
|
|
className="animate-scale-in"
|
|
style={{ animationDelay: `${index * 50}ms` }}
|
|
>
|
|
<ModelCard
|
|
model={model}
|
|
viewMode={viewMode}
|
|
onEdit={() => setEditingModel(model)}
|
|
onDelete={() => handleDeleteModel(model.id)}
|
|
onSelect={() => onModelSelect?.(model)}
|
|
onFavorite={handleFavorite}
|
|
isFavorite={favorites.has(model.id)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* 创建模特模态框 */}
|
|
{showCreateModal && (
|
|
<ModelForm
|
|
onSubmit={handleCreateModel}
|
|
onCancel={() => setShowCreateModal(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* 编辑模特模态框 */}
|
|
{editingModel && (
|
|
<ModelForm
|
|
initialData={editingModel}
|
|
onSubmit={handleUpdateModel}
|
|
onCancel={() => setEditingModel(null)}
|
|
isEdit={true}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ModelList;
|