138 lines
4.1 KiB
TypeScript
138 lines
4.1 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* 统一 API 状态组件
|
||
* - LoadingState: 骨架屏加载状态
|
||
* - ErrorState: 错误展示 + 重试按钮
|
||
* - EmptyState: 空数据状态
|
||
*/
|
||
|
||
import { AlertCircle, RefreshCw, Inbox } from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
|
||
// ── LoadingState ─────────────────────────────────────────────────────────────
|
||
|
||
export interface LoadingStateProps {
|
||
/** 骨架行数(默认 3) */
|
||
rows?: number;
|
||
/** 是否展示卡片网格布局(默认 false,展示列表) */
|
||
grid?: boolean;
|
||
/** 网格列数(grid=true 时生效,默认 3) */
|
||
cols?: 2 | 3 | 4;
|
||
/** 自定义高度类名(如 "h-24") */
|
||
rowHeight?: string;
|
||
}
|
||
|
||
export function LoadingState({
|
||
rows = 3,
|
||
grid = false,
|
||
cols = 3,
|
||
rowHeight = "h-24",
|
||
}: LoadingStateProps) {
|
||
const colClass = {
|
||
2: "md:grid-cols-2",
|
||
3: "md:grid-cols-2 lg:grid-cols-3",
|
||
4: "md:grid-cols-2 lg:grid-cols-4",
|
||
}[cols];
|
||
|
||
if (grid) {
|
||
return (
|
||
<div className={`grid gap-4 ${colClass}`}>
|
||
{Array.from({ length: rows }).map((_, i) => (
|
||
<Skeleton key={i} className={`${rowHeight} w-full rounded-xl`} />
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
{Array.from({ length: rows }).map((_, i) => (
|
||
<Skeleton key={i} className={`${rowHeight} w-full rounded-xl`} />
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── ErrorState ───────────────────────────────────────────────────────────────
|
||
|
||
export interface ErrorStateProps {
|
||
/** 错误对象或错误消息字符串 */
|
||
error: Error | string | null | undefined;
|
||
/** 点击重试的回调 */
|
||
onRetry?: () => void;
|
||
/** 重试按钮文字(默认"重试") */
|
||
retryLabel?: string;
|
||
/** 标题(默认"数据加载失败") */
|
||
title?: string;
|
||
}
|
||
|
||
export function ErrorState({
|
||
error,
|
||
onRetry,
|
||
retryLabel = "重试",
|
||
title = "数据加载失败",
|
||
}: ErrorStateProps) {
|
||
const message =
|
||
error instanceof Error
|
||
? error.message
|
||
: typeof error === "string"
|
||
? error
|
||
: "发生未知错误,请稍后重试";
|
||
|
||
return (
|
||
<div className="flex flex-col items-center justify-center rounded-xl border border-red-200 bg-red-50 py-16 text-center">
|
||
<AlertCircle className="h-10 w-10 text-red-400 mb-3" />
|
||
<p className="text-sm font-medium text-red-600">{title}</p>
|
||
<p className="text-xs text-red-500 mt-1 max-w-sm">{message}</p>
|
||
{onRetry && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="mt-4"
|
||
onClick={onRetry}
|
||
>
|
||
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
|
||
{retryLabel}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── EmptyState ───────────────────────────────────────────────────────────────
|
||
|
||
export interface EmptyStateProps {
|
||
/** 主提示文字 */
|
||
message?: string;
|
||
/** 次级说明文字 */
|
||
description?: string;
|
||
/** 操作按钮(可选) */
|
||
action?: React.ReactNode;
|
||
/** 自定义图标 */
|
||
icon?: React.ReactNode;
|
||
}
|
||
|
||
export function EmptyState({
|
||
message = "暂无数据",
|
||
description,
|
||
action,
|
||
icon,
|
||
}: EmptyStateProps) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center rounded-xl border border-gray-200 bg-white py-16 text-center">
|
||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
|
||
{icon ?? <Inbox className="h-6 w-6 text-gray-400" />}
|
||
</div>
|
||
<p className="text-base font-semibold text-gray-900">{message}</p>
|
||
{description && (
|
||
<p className="mt-2 mb-4 max-w-sm text-sm text-gray-500">
|
||
{description}
|
||
</p>
|
||
)}
|
||
{action && <div className="mt-4">{action}</div>}
|
||
</div>
|
||
);
|
||
}
|