geo/frontend/components/ui/api-states.tsx

138 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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