first commit
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { AlertTriangle, CheckCircle2, Copy, Loader2, MoonStar, Sparkles, XCircle } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { cx } from "../utils/classes";
|
||||
|
||||
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>{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>{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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user