geo/frontend/components/business/alert-card.tsx

203 lines
6.4 KiB
TypeScript

"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Card } from "@/components/ui/card";
export type AlertSeverity = "critical" | "warning" | "info" | "success";
export interface AlertCardItem {
id: string;
title: string;
description?: string;
severity: AlertSeverity;
timestamp?: string;
icon?: React.ReactNode;
actions?: React.ReactNode;
}
export interface AlertCardProps extends React.HTMLAttributes<HTMLDivElement> {
alerts: AlertCardItem[];
title?: string;
onDismiss?: (id: string) => void;
maxVisible?: number;
emptyText?: string;
}
const alertSeverityConfig: Record<
AlertSeverity,
{ iconBg: string; iconColor: string; dot: string; label: string }
> = {
critical: {
iconBg: "bg-destructive/10",
iconColor: "text-destructive",
dot: "bg-destructive",
label: "Critical",
},
warning: {
iconBg: "bg-accent/10",
iconColor: "text-accent",
dot: "bg-accent",
label: "Warning",
},
info: {
iconBg: "bg-blue-50",
iconColor: "text-blue-500",
dot: "bg-blue-400",
label: "Info",
},
success: {
iconBg: "bg-primary/10",
iconColor: "text-primary",
dot: "bg-primary",
label: "OK",
},
};
const DefaultAlertIcon = ({ severity }: { severity: AlertSeverity }) => {
if (severity === "critical" || severity === "warning") {
return (
<svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
<path
d="M8 2L14.5 13H1.5L8 2Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M8 7V9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="8" cy="11.5" r="0.75" fill="currentColor" />
</svg>
);
}
if (severity === "info") {
return (
<svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
<path d="M8 7V11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="8" cy="5" r="0.75" fill="currentColor" />
</svg>
);
}
return (
<svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
<path d="M5 8L7 10L11 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
const AlertCard = React.forwardRef<HTMLDivElement, AlertCardProps>(
(
{
className,
alerts,
title = "Smart Alert",
onDismiss,
maxVisible,
emptyText = "暂无告警",
...props
},
ref
) => {
const visible = maxVisible ? alerts.slice(0, maxVisible) : alerts;
return (
<Card ref={ref} className={cn("overflow-hidden", className)} {...props}>
{/* Header */}
<div className="flex items-center justify-between px-5 py-3.5 border-b border-border">
<div className="flex items-center gap-2">
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
{title}
</p>
{alerts.length > 0 && (
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive/10 px-1.5 text-[10px] font-bold text-destructive">
{alerts.length}
</span>
)}
</div>
</div>
{/* Alert list */}
<div className="divide-y divide-border/50">
{visible.length === 0 ? (
<div className="px-5 py-8 text-center text-sm text-muted-foreground">
{emptyText}
</div>
) : (
visible.map((alert) => {
const config = alertSeverityConfig[alert.severity];
return (
<div
key={alert.id}
className="group flex items-start gap-3 px-5 py-3.5 transition-colors duration-150 hover:bg-muted/30"
>
{/* Icon */}
<div
className={cn(
"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg",
config.iconBg,
config.iconColor
)}
>
{alert.icon ?? <DefaultAlertIcon severity={alert.severity} />}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-semibold text-foreground leading-snug truncate">
{alert.title}
</p>
{alert.timestamp && (
<span className="shrink-0 text-xs text-muted-foreground">{alert.timestamp}</span>
)}
</div>
{alert.description && (
<p className="mt-0.5 text-xs text-muted-foreground leading-snug">
{alert.description}
</p>
)}
{/* Actions */}
<div className="mt-2 flex items-center gap-2">
{alert.actions}
{onDismiss && (
<button
type="button"
onClick={() => onDismiss(alert.id)}
className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground transition-colors duration-150"
>
Dismiss
</button>
)}
</div>
</div>
{/* severity dot */}
<div
className={cn(
"mt-2 h-1.5 w-1.5 shrink-0 rounded-full",
config.dot,
alert.severity === "critical" && "animate-pulse"
)}
/>
</div>
);
})
)}
</div>
{/* overflow hint */}
{maxVisible && alerts.length > maxVisible && (
<div className="px-5 py-2.5 border-t border-border text-xs text-muted-foreground text-center">
{alerts.length - maxVisible}
</div>
)}
</Card>
);
}
);
AlertCard.displayName = "AlertCard";
export { AlertCard };