feat: 实现模特管理功能
- 新增模特数据模型和数据库表结构 - 实现模特的完整CRUD操作 - 添加模特照片管理功能 - 实现素材与模特关联功能 - 创建模特管理前端界面 - 集成到主应用导航和路由 - 修复数据库连接死锁问题 功能特性: - 模特基本信息管理(姓名、艺名、性别、年龄等) - 照片管理和封面设置 - 标签系统 - 状态管理(活跃、不活跃、退役、暂停) - 评分系统 - 搜索和过滤功能 - 素材关联功能
This commit is contained in:
284
apps/desktop/src/components/ModelList.tsx
Normal file
284
apps/desktop/src/components/ModelList.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
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 } 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');
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-red-600 mb-4">{error}</div>
|
||||
<button
|
||||
onClick={loadModels}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 头部工具栏 */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">模特管理</h1>
|
||||
<p className="text-gray-600">共 {filteredModels.length} 个模特</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 视图模式切换 */}
|
||||
<div className="flex rounded-lg border border-gray-300">
|
||||
<button
|
||||
onClick={() => setViewMode(ModelViewMode.Grid)}
|
||||
className={`p-2 ${viewMode === ModelViewMode.Grid
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Squares2X2Icon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode(ModelViewMode.List)}
|
||||
className={`p-2 ${viewMode === ModelViewMode.List
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<ListBulletIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 创建模特按钮 */}
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
添加模特
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和过滤 */}
|
||||
<ModelSearch
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
genderFilter={genderFilter}
|
||||
onGenderFilterChange={setGenderFilter}
|
||||
sortBy={sortBy}
|
||||
onSortByChange={setSortBy}
|
||||
sortOrder={sortOrder}
|
||||
onSortOrderChange={setSortOrder}
|
||||
/>
|
||||
|
||||
{/* 模特列表 */}
|
||||
{filteredModels.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500 mb-4">
|
||||
{models.length === 0 ? '还没有添加任何模特' : '没有找到匹配的模特'}
|
||||
</div>
|
||||
{models.length === 0 && (
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
添加第一个模特
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={
|
||||
viewMode === ModelViewMode.Grid
|
||||
? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6'
|
||||
: 'space-y-4'
|
||||
}>
|
||||
{filteredModels.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
viewMode={viewMode}
|
||||
onEdit={() => setEditingModel(model)}
|
||||
onDelete={() => handleDeleteModel(model.id)}
|
||||
onSelect={() => onModelSelect?.(model)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 创建模特模态框 */}
|
||||
{showCreateModal && (
|
||||
<ModelForm
|
||||
onSubmit={handleCreateModel}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 编辑模特模态框 */}
|
||||
{editingModel && (
|
||||
<ModelForm
|
||||
initialData={editingModel}
|
||||
onSubmit={handleUpdateModel}
|
||||
onCancel={() => setEditingModel(null)}
|
||||
isEdit={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelList;
|
||||
Reference in New Issue
Block a user