Files
2026-07-20 16:42:33 +08:00

114 lines
3.2 KiB
TypeScript

import { AlertTriangle, CheckCircle2, Copy, Loader2, MoonStar, Sparkles, XCircle } from "lucide-react";
import type { ReactNode } from "react";
import { cx } from "../utils/classes";
import { safeDiagnosticText } from "../utils/safeDiagnosticText";
interface EmptyStateProps {
title: string;
description: string;
actionLabel?: string;
onAction?: () => void;
icon?: ReactNode;
}
export function EmptyState({ title, description, actionLabel, onAction, icon }: EmptyStateProps) {
return (
<div className="state-view state-empty" role="status">
<span className="state-icon" aria-hidden="true">
{icon ?? <MoonStar size={26} />}
</span>
<strong>{title}</strong>
<p>{description}</p>
{actionLabel && onAction && (
<button type="button" className="state-action" onClick={onAction}>
<Sparkles size={14} />
<span>{actionLabel}</span>
</button>
)}
</div>
);
}
interface LoadingStateProps {
label: string;
compact?: boolean;
}
export function LoadingState({ label, compact }: LoadingStateProps) {
return (
<div className={cx("state-view state-loading", compact && "state-compact")} role="status" aria-live="polite">
<Loader2 size={compact ? 16 : 22} className="state-spinner" aria-hidden="true" />
<span>{label}</span>
</div>
);
}
interface ErrorStateProps {
title: string;
reason?: string;
diagnosticId?: string;
onRetry?: () => void;
compact?: boolean;
}
export function ErrorState({ title, reason, diagnosticId, onRetry, compact }: ErrorStateProps) {
return (
<div className={cx("state-view state-error", compact && "state-compact")} role="alert">
<span className="state-icon" aria-hidden="true">
<AlertTriangle size={compact ? 16 : 24} />
</span>
<strong>{title}</strong>
{reason && <p>{safeDiagnosticText(reason)}</p>}
{diagnosticId && <DiagnosticSummary diagnosticId={diagnosticId} />}
{onRetry && (
<button type="button" className="state-action" onClick={onRetry}>
<Sparkles size={14} />
<span>重试</span>
</button>
)}
</div>
);
}
interface ResultBadgeProps {
status: "pending" | "succeeded" | "failed";
label: string;
}
export function ResultBadge({ status, label }: ResultBadgeProps) {
const icon =
status === "pending" ? <Loader2 size={13} className="state-spinner" /> : status === "succeeded" ? <CheckCircle2 size={13} /> : <XCircle size={13} />;
return (
<span className={cx("result-badge", `result-badge-${status}`)}>
{icon}
<span>{status === "failed" ? safeDiagnosticText(label) : label}</span>
</span>
);
}
interface DiagnosticSummaryProps {
diagnosticId: string;
detail?: string;
}
export function DiagnosticSummary({ diagnosticId, detail }: DiagnosticSummaryProps) {
const summary = detail ? `${diagnosticId} ${detail}` : diagnosticId;
return (
<span className="diagnostic-summary">
<code>{diagnosticId}</code>
<button
type="button"
className="diagnostic-copy"
title="复制诊断信息"
onClick={() => {
void navigator.clipboard?.writeText(summary);
}}
>
<Copy size={12} />
<span>复制</span>
</button>
</span>
);
}