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