feat: 优化模特管理界面UI/UX设计
界面精致化优化: - 优化模特卡片尺寸,宽高比调整为4:5,更加紧凑 - 精致化按钮设计,统一尺寸规范和间距 - 优化搜索框和筛选器,提升视觉精致度 - 修复下拉选择组件图标间距问题 组件优化: - ModelCard: 减少内边距,优化文字和图标尺寸 - ModelList: 精致化头部工具栏和按钮设计 - ModelSearch: 重新设计搜索框和筛选器布局 - 创建CustomSelect组件,解决原生select样式问题 用户体验提升: - 修复筛选按钮功能,添加展开/收起动画 - 优化网格布局,支持更多列显示 - 提高信息密度,在相同空间显示更多内容 - 统一设计语言,建立完整的设计系统 响应式优化: - 优化移动端显示效果 - 在大屏幕上支持5列布局 - 改进触摸目标尺寸和间距 技术改进: - 建立统一的设计令牌系统 - 优化CSS动画和过渡效果 - 改进组件可维护性和复用性 - 遵循前端开发规范和视觉设计标准
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } 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';
|
||||
import {
|
||||
PlusIcon,
|
||||
Squares2X2Icon,
|
||||
ListBulletIcon,
|
||||
FunnelIcon,
|
||||
MagnifyingGlassIcon,
|
||||
SparklesIcon,
|
||||
HeartIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import '../styles/design-system.css';
|
||||
|
||||
interface ModelListProps {
|
||||
onModelSelect?: (model: Model) => void;
|
||||
@@ -23,6 +32,8 @@ const ModelList: React.FC<ModelListProps> = ({ onModelSelect }) => {
|
||||
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();
|
||||
@@ -145,117 +156,248 @@ const ModelList: React.FC<ModelListProps> = ({ onModelSelect }) => {
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
// 骨架屏组件
|
||||
const ModelCardSkeleton = () => (
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden animate-pulse">
|
||||
<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 className="flex gap-2 pt-2">
|
||||
<div className="flex-1 h-10 bg-gray-200 rounded-xl loading-shimmer" />
|
||||
<div className="w-10 h-10 bg-gray-200 rounded-xl loading-shimmer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 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) => (
|
||||
<ModelCardSkeleton key={i} />
|
||||
))}
|
||||
</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 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-6">
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{/* 头部工具栏 */}
|
||||
<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 className="bg-white rounded-xl shadow-sm border border-gray-100 p-4">
|
||||
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-600 rounded-lg flex items-center justify-center">
|
||||
<SparklesIcon className="h-5 w-5 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>
|
||||
|
||||
{/* 创建模特按钮 */}
|
||||
<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 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>
|
||||
|
||||
{/* 搜索和过滤 */}
|
||||
<ModelSearch
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
genderFilter={genderFilter}
|
||||
onGenderFilterChange={setGenderFilter}
|
||||
sortBy={sortBy}
|
||||
onSortByChange={setSortBy}
|
||||
sortOrder={sortOrder}
|
||||
onSortOrderChange={setSortOrder}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* 模特列表 */}
|
||||
{filteredModels.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500 mb-4">
|
||||
{models.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>
|
||||
{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={
|
||||
<div className={`animate-fade-in ${
|
||||
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
|
||||
? '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}
|
||||
model={model}
|
||||
viewMode={viewMode}
|
||||
onEdit={() => setEditingModel(model)}
|
||||
onDelete={() => handleDeleteModel(model.id)}
|
||||
onSelect={() => onModelSelect?.(model)}
|
||||
/>
|
||||
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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user