feat: 优化模特管理界面UI/UX设计

界面精致化优化:
- 优化模特卡片尺寸,宽高比调整为4:5,更加紧凑
- 精致化按钮设计,统一尺寸规范和间距
- 优化搜索框和筛选器,提升视觉精致度
- 修复下拉选择组件图标间距问题

 组件优化:
- ModelCard: 减少内边距,优化文字和图标尺寸
- ModelList: 精致化头部工具栏和按钮设计
- ModelSearch: 重新设计搜索框和筛选器布局
- 创建CustomSelect组件,解决原生select样式问题

 用户体验提升:
- 修复筛选按钮功能,添加展开/收起动画
- 优化网格布局,支持更多列显示
- 提高信息密度,在相同空间显示更多内容
- 统一设计语言,建立完整的设计系统

 响应式优化:
- 优化移动端显示效果
- 在大屏幕上支持5列布局
- 改进触摸目标尺寸和间距

技术改进:
- 建立统一的设计令牌系统
- 优化CSS动画和过渡效果
- 改进组件可维护性和复用性
- 遵循前端开发规范和视觉设计标准
This commit is contained in:
imeepos
2025-07-14 10:45:08 +08:00
parent 7a2d045be3
commit 6b21a389c0
9 changed files with 2014 additions and 378 deletions

View File

@@ -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>
)}