Enhanced Interactive Components: - Created InteractiveButton with ripple effects, haptic feedback, and multiple variants - Developed InteractiveInput and InteractiveTextarea with real-time validation and status indicators - Added FloatingActionButton for quick actions with elegant tooltips - Implemented comprehensive micro-interactions and animations Advanced Loading & Skeleton States: - Enhanced SkeletonLoader with multiple variants (model, material, template, table-row) - Added specialized skeleton components (ModelCardSkeleton, MaterialCardSkeleton, etc.) - Created EnhancedLoadingState with progress indicators and operation tracking - Implemented BatchOperationLoading for complex workflows Optimized Form Experience: - Upgraded ProjectForm with new interactive components - Added real-time validation feedback and error animations - Implemented smart input states (success, error, loading) - Enhanced user feedback with visual and haptic responses Perfected Empty States: - Redesigned EmptyState with multiple variants and illustrations - Created specialized empty state components (EmptyProjectList, EmptyModelList, etc.) - Added contextual tips and guidance for better user onboarding - Implemented error states and recovery actions Advanced Data Display: - Built comprehensive DataTable with search, sort, filter, and pagination - Created flexible CardGrid with view switching and bulk operations - Added row selection, bulk actions, and advanced filtering - Implemented responsive layouts and mobile optimization Rich Animation System: - Added 20+ new micro-interaction animations - Implemented button press, success pulse, error shake effects - Created smooth slide-in animations for all directions - Added loading dots, heartbeat, and bounce-in animations Key Features: - Ripple effects on button clicks with haptic feedback - Real-time form validation with animated error states - Contextual empty states with actionable guidance - Advanced data tables with full CRUD operations - Responsive card grids with multiple view modes - Comprehensive loading states for better perceived performance All components now provide rich visual feedback, smooth animations, and professional user experience that matches modern design standards.
366 lines
11 KiB
TypeScript
366 lines
11 KiB
TypeScript
import React from 'react';
|
|
|
|
interface SkeletonLoaderProps {
|
|
variant?: 'card' | 'list' | 'text' | 'avatar' | 'button' | 'model' | 'material' | 'template' | 'table-row';
|
|
count?: number;
|
|
className?: string;
|
|
width?: string;
|
|
height?: string;
|
|
}
|
|
|
|
/**
|
|
* 骨架屏加载组件
|
|
* 提供多种预设样式和自定义选项
|
|
*/
|
|
export const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
|
variant = 'text',
|
|
count = 1,
|
|
className = '',
|
|
width,
|
|
height
|
|
}) => {
|
|
const baseClasses = 'animate-pulse bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 bg-[length:200%_100%] animate-loading-shimmer rounded-lg';
|
|
|
|
const getVariantClasses = () => {
|
|
switch (variant) {
|
|
case 'card':
|
|
return 'h-48 w-full';
|
|
case 'list':
|
|
return 'h-16 w-full';
|
|
case 'avatar':
|
|
return 'h-12 w-12 rounded-full';
|
|
case 'button':
|
|
return 'h-10 w-24';
|
|
case 'model':
|
|
return 'h-40 w-full';
|
|
case 'material':
|
|
return 'h-44 w-full';
|
|
case 'template':
|
|
return 'h-52 w-full';
|
|
case 'table-row':
|
|
return 'h-12 w-full';
|
|
default:
|
|
return 'h-4 w-full';
|
|
}
|
|
};
|
|
|
|
const renderSkeleton = (index: number) => {
|
|
const variantClasses = getVariantClasses();
|
|
const customStyle = {
|
|
width: width || undefined,
|
|
height: height || undefined,
|
|
};
|
|
|
|
if (variant === 'card') {
|
|
return (
|
|
<div key={index} className={`${baseClasses} ${className} p-6 space-y-4`} style={customStyle}>
|
|
{/* 卡片头部 */}
|
|
<div className="flex items-center space-x-4">
|
|
<div className="h-12 w-12 bg-gray-300 rounded-xl"></div>
|
|
<div className="space-y-2 flex-1">
|
|
<div className="h-4 bg-gray-300 rounded w-3/4"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 卡片内容 */}
|
|
<div className="space-y-3">
|
|
<div className="h-3 bg-gray-300 rounded"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-5/6"></div>
|
|
</div>
|
|
|
|
{/* 统计区域 */}
|
|
<div className="bg-gray-200 rounded-xl p-4 space-y-3">
|
|
<div className="h-3 bg-gray-300 rounded w-1/3"></div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="h-8 bg-gray-300 rounded"></div>
|
|
<div className="h-8 bg-gray-300 rounded"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 底部按钮 */}
|
|
<div className="flex gap-2 pt-4">
|
|
<div className="h-8 w-8 bg-gray-300 rounded-lg"></div>
|
|
<div className="h-8 bg-gray-300 rounded flex-1"></div>
|
|
<div className="h-8 bg-gray-300 rounded flex-1"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (variant === 'list') {
|
|
return (
|
|
<div key={index} className={`${baseClasses} ${className} flex items-center space-x-4 p-4`} style={customStyle}>
|
|
<div className="h-10 w-10 bg-gray-300 rounded-lg"></div>
|
|
<div className="space-y-2 flex-1">
|
|
<div className="h-4 bg-gray-300 rounded w-3/4"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
|
|
</div>
|
|
<div className="h-8 w-20 bg-gray-300 rounded"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (variant === 'model') {
|
|
return (
|
|
<div key={index} className={`${baseClasses} ${className} p-4 space-y-4`} style={customStyle}>
|
|
{/* 模特头像和基本信息 */}
|
|
<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>
|
|
);
|
|
}
|
|
|
|
if (variant === 'material') {
|
|
return (
|
|
<div key={index} className={`${baseClasses} ${className} p-4 space-y-4`} style={customStyle}>
|
|
{/* 缩略图 */}
|
|
<div className="h-24 bg-gray-300 rounded-lg"></div>
|
|
|
|
{/* 标题和信息 */}
|
|
<div className="space-y-2">
|
|
<div className="h-4 bg-gray-300 rounded w-4/5"></div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-5 w-12 bg-gray-300 rounded"></div>
|
|
<div className="h-5 w-16 bg-gray-300 rounded"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 进度条 */}
|
|
<div className="space-y-1">
|
|
<div className="h-3 bg-gray-300 rounded w-1/3"></div>
|
|
<div className="h-2 bg-gray-300 rounded"></div>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div className="flex gap-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>
|
|
);
|
|
}
|
|
|
|
if (variant === 'template') {
|
|
return (
|
|
<div key={index} className={`${baseClasses} ${className} p-4 space-y-4`} style={customStyle}>
|
|
{/* 预览图 */}
|
|
<div className="h-20 bg-gray-300 rounded-lg"></div>
|
|
|
|
{/* 标题和描述 */}
|
|
<div className="space-y-2">
|
|
<div className="h-5 bg-gray-300 rounded w-3/4"></div>
|
|
<div className="h-3 bg-gray-300 rounded"></div>
|
|
<div className="h-3 bg-gray-300 rounded w-3/5"></div>
|
|
</div>
|
|
|
|
{/* 统计信息 */}
|
|
<div className="flex justify-between">
|
|
<div className="space-y-1">
|
|
<div className="h-3 bg-gray-300 rounded w-10"></div>
|
|
<div className="h-4 bg-gray-300 rounded w-8"></div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="h-3 bg-gray-300 rounded w-10"></div>
|
|
<div className="h-4 bg-gray-300 rounded w-8"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div className="flex gap-2">
|
|
<div className="h-9 bg-gray-300 rounded flex-1"></div>
|
|
<div className="h-9 w-20 bg-gray-300 rounded"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (variant === 'table-row') {
|
|
return (
|
|
<div key={index} className={`${baseClasses} ${className} flex items-center space-x-6 p-3`} style={customStyle}>
|
|
<div className="h-4 bg-gray-300 rounded w-1/4"></div>
|
|
<div className="h-4 bg-gray-300 rounded w-1/6"></div>
|
|
<div className="h-4 bg-gray-300 rounded w-1/8"></div>
|
|
<div className="h-4 bg-gray-300 rounded w-1/5"></div>
|
|
<div className="h-6 w-16 bg-gray-300 rounded"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={`${baseClasses} ${variantClasses} ${className}`}
|
|
style={customStyle}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4 animate-fade-in">
|
|
{Array.from({ length: count }, (_, index) => renderSkeleton(index))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 项目卡片骨架屏
|
|
*/
|
|
export const ProjectCardSkeleton: React.FC<{ count?: number }> = ({ count = 4 }) => {
|
|
return (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{Array.from({ length: count }, (_, index) => (
|
|
<SkeletonLoader key={index} variant="card" />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 列表项骨架屏
|
|
*/
|
|
export const ListItemSkeleton: React.FC<{ count?: number }> = ({ count = 5 }) => {
|
|
return (
|
|
<div className="space-y-3">
|
|
{Array.from({ length: count }, (_, index) => (
|
|
<SkeletonLoader key={index} variant="list" />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 模特卡片骨架屏
|
|
*/
|
|
export const ModelCardSkeleton: React.FC<{ count?: number }> = ({ count = 8 }) => {
|
|
return (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
|
{Array.from({ length: count }, (_, index) => (
|
|
<SkeletonLoader key={index} variant="model" />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 素材卡片骨架屏
|
|
*/
|
|
export const MaterialCardSkeleton: React.FC<{ count?: number }> = ({ count = 6 }) => {
|
|
return (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{Array.from({ length: count }, (_, index) => (
|
|
<SkeletonLoader key={index} variant="material" />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 模板卡片骨架屏
|
|
*/
|
|
export const TemplateCardSkeleton: React.FC<{ count?: number }> = ({ count = 8 }) => {
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{Array.from({ length: count }, (_, index) => (
|
|
<SkeletonLoader key={index} variant="template" />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 表格骨架屏
|
|
*/
|
|
export const TableSkeleton: React.FC<{ rows?: number; columns?: number }> = ({
|
|
rows = 5,
|
|
columns = 4
|
|
}) => {
|
|
return (
|
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
{/* 表头 */}
|
|
<div className="bg-gray-50 border-b border-gray-200 p-4">
|
|
<div className="flex space-x-6">
|
|
{Array.from({ length: columns }, (_, index) => (
|
|
<div key={index} className="h-4 bg-gray-300 rounded flex-1 animate-pulse"></div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 表格行 */}
|
|
<div className="divide-y divide-gray-200">
|
|
{Array.from({ length: rows }, (_, index) => (
|
|
<SkeletonLoader key={index} variant="table-row" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 页面加载骨架屏
|
|
*/
|
|
export const PageLoadingSkeleton: React.FC<{
|
|
showHeader?: boolean;
|
|
showStats?: boolean;
|
|
cardCount?: number;
|
|
}> = ({
|
|
showHeader = true,
|
|
showStats = true,
|
|
cardCount = 6
|
|
}) => {
|
|
return (
|
|
<div className="space-y-6 animate-fade-in">
|
|
{/* 页面头部 */}
|
|
{showHeader && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-2">
|
|
<div className="h-6 bg-gray-300 rounded w-48 animate-pulse"></div>
|
|
<div className="h-4 bg-gray-300 rounded w-32 animate-pulse"></div>
|
|
</div>
|
|
<div className="h-10 w-24 bg-gray-300 rounded animate-pulse"></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 统计卡片 */}
|
|
{showStats && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{Array.from({ length: 4 }, (_, index) => (
|
|
<div key={index} className="bg-white rounded-lg border border-gray-200 p-4 space-y-2">
|
|
<div className="h-4 bg-gray-300 rounded w-20 animate-pulse"></div>
|
|
<div className="h-8 bg-gray-300 rounded w-16 animate-pulse"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 主要内容 */}
|
|
<ProjectCardSkeleton count={cardCount} />
|
|
</div>
|
|
);
|
|
};
|