mixvideo-v2/apps/desktop/src/components/SkeletonLoader.tsx

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