- routes.ts registers all workspaces under /console (+ detail routes, engine, audit, login) - api.ts: cookie session first (credentials include, CSRF header), Bearer only with stored key, 401 -> /login - sse.ts: shared EventSource with ?after=seq catch-up, backoff reconnect, per-type subscribe - shared components: data-table, filter-bar, detail-sheet, timeline, json-view, confirm-dialog, form-field, kv-list, stat-tile, error/loading states; ui tabs and textarea Co-Authored-By: Claude Opus 5.5 <[email protected]>
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
import type { LucideIcon } from "lucide-react";
|
|
import { AlertTriangle, Inbox, RotateCw } from "lucide-react";
|
|
import type { ReactNode } from "react";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { cn } from "~/lib/utils.ts";
|
|
|
|
/** 空态:图标 + 标题 + 说明 + 可选操作。用于"尚无数据"与"功能建设中"占位 */
|
|
export function EmptyState({
|
|
icon: Icon = Inbox,
|
|
title,
|
|
description,
|
|
action,
|
|
plain = false,
|
|
className,
|
|
}: {
|
|
icon?: LucideIcon;
|
|
title: string;
|
|
description?: ReactNode;
|
|
action?: ReactNode;
|
|
/** 放在已有卡片内时用 plain,避免玻璃叠玻璃 */
|
|
plain?: boolean;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col items-center justify-center rounded-xl px-6 py-14 text-center",
|
|
!plain && "pg-slab",
|
|
className,
|
|
)}
|
|
>
|
|
<span className="mb-4 grid h-11 w-11 place-items-center rounded-full bg-foreground/[0.05] text-muted-foreground">
|
|
<Icon className="h-5 w-5" />
|
|
</span>
|
|
<p className="text-lead font-medium text-foreground">{title}</p>
|
|
{description ? <p className="mt-1.5 max-w-md text-caption text-muted-foreground">{description}</p> : null}
|
|
{action ? <div className="mt-5">{action}</div> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** 错误态:图标 + 标题 + 错误消息 + 可选重试。放在卡片里时用 plain */
|
|
export function ErrorState({
|
|
title = "加载失败",
|
|
error,
|
|
onRetry,
|
|
plain = false,
|
|
className,
|
|
}: {
|
|
title?: string;
|
|
error?: unknown;
|
|
onRetry?: () => void;
|
|
plain?: boolean;
|
|
className?: string;
|
|
}) {
|
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
return (
|
|
<div
|
|
role="alert"
|
|
className={cn(
|
|
"flex flex-col items-center justify-center rounded-xl px-6 py-12 text-center",
|
|
!plain && "pg-slab",
|
|
className,
|
|
)}
|
|
>
|
|
<span className="mb-4 grid h-11 w-11 place-items-center rounded-full bg-destructive/10 text-destructive">
|
|
<AlertTriangle className="h-5 w-5" />
|
|
</span>
|
|
<p className="text-lead font-medium text-foreground">{title}</p>
|
|
{message ? <p className="mt-1.5 max-w-md text-caption break-words text-muted-foreground">{message}</p> : null}
|
|
{onRetry ? (
|
|
<Button variant="outline" size="sm" className="mt-5" onClick={onRetry}>
|
|
<RotateCw />
|
|
重试
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** 加载态:若干行骨架条 */
|
|
export function LoadingState({ rows = 3, className }: { rows?: number; className?: string }) {
|
|
return (
|
|
<div role="status" aria-busy="true" aria-label="加载中" className={cn("space-y-2.5 py-2", className)}>
|
|
{Array.from({ length: rows }, (_, i) => (
|
|
<div key={i} className="h-9 animate-pulse rounded-tight bg-foreground/[0.06]" />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|