Files
next-tjwater-frontend/src/features/workbench/peripheral-sample/scada-monitoring-notice.tsx
T

89 lines
5.7 KiB
TypeScript

import { useEffect, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
import { ArrowUpRight, CheckCircle2, CircleHelp, Clock3, LoaderCircle, TriangleAlert } from "lucide-react";
import { cn } from "@/shared/ui/cn";
import { StatusIcon, type StatusTone } from "@/shared/ui/status";
import { Button } from "@/shared/ui/button";
import type { ScadaMonitoringResult } from "../api/scada-replay-client";
export function ScadaMonitoringNotice({ result, animationId, processing, onOpenInvestigation }: {
result: ScadaMonitoringResult | null | undefined;
animationId: string | null;
processing: boolean;
onOpenInvestigation?: (taskId: string) => void;
}) {
const reduced = useReducedMotion();
const [animating, setAnimating] = useState(false);
useEffect(() => {
if (!animationId || reduced) return;
setAnimating(true);
const timer = window.setTimeout(() => setAnimating(false), 2600);
return () => window.clearTimeout(timer);
}, [animationId, reduced]);
const recovered = result?.incident?.action === "resolved";
const ongoing = result?.incident?.action === "ongoing";
const abnormal = Boolean(result && !recovered && (result.task_id || ["attention", "suspected_burst"].includes(result.status)));
const suspectedBurst = abnormal && result?.status === "suspected_burst";
const normal = result?.status === "normal" || recovered;
const tone: StatusTone = suspectedBurst ? "danger" : abnormal ? "warning" : normal ? "success" : processing ? "info" : "neutral";
const Icon = !result ? processing ? LoaderCircle : Clock3 : abnormal ? TriangleAlert : normal ? CheckCircle2 : CircleHelp;
const title = !result ? processing ? "正在监测" : "监测待机"
: recovered ? "异常已恢复" : ongoing ? "异常持续跟踪" : suspectedBurst ? "发现疑似爆管" : abnormal ? "发现指标异常"
: normal ? "监测正常" : "监测数据不足";
const note = !result ? processing ? "正在核对本周期数据" : "等待播放或单步监测"
: result.task_id ? recovered ? "告警已解除" : ongoing ? "沿用原调查" : "已启动调查"
: normal ? "本周期未发现异常" : abnormal ? "请留意后续监测结果" : "暂无法形成监测结论";
const hint = processing ? result ? "正在监测下一周期" : "处理完成后更新结果"
: abnormal ? "诊断详情见调查报告" : recovered ? "可回看本次调查"
: result && !normal ? "待后续周期继续监测" : "异常时自动启动调查";
const emphasis = animating && abnormal && !reduced;
return (
<section aria-label="最新监测结果" role="status" aria-busy={processing}
aria-atomic="true"
data-animation-state={emphasis ? "running" : "idle"} data-announcement-id={animationId ?? undefined}
data-status-tone={tone}
className={cn(
"relative isolate grid h-40 shrink-0 grid-rows-[1.25rem_1fr_2.25rem] gap-2 overflow-hidden rounded-lg bg-[var(--status-soft)] p-3.5 text-foreground ring-1 ring-inset ring-[var(--status-border)]",
`status-tone-${tone}`
)}>
{emphasis ? <motion.div key={`${animationId}-surface`} aria-hidden="true"
data-slot="scada-alert-emphasis" className="pointer-events-none absolute inset-0 rounded-lg border border-[var(--status-mark)]"
initial={{ opacity: 0 }} animate={{ opacity: [0, 0.55, 0] }}
transition={{ duration: 1.2, repeat: 1, repeatDelay: 0.2, ease: "easeOut" }} /> : null}
<div data-slot="scada-notice-header" className="flex min-w-0 items-center justify-between gap-2 text-[11px] leading-5 text-foreground/75">
<span>调查监测</span>
{result ? <time dateTime={result.period_end} className="whitespace-nowrap tabular-nums">
<span className="sr-only">最新监测 </span>
{new Date(result.period_end).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false })}
</time> : <span>尚无监测结果</span>}
</div>
<div data-slot="scada-notice-content" className="flex min-w-0 items-center gap-3">
<span className="relative shrink-0" aria-hidden="true">
{emphasis ? <motion.span key={animationId} data-slot="scada-alert-pulse"
className="pointer-events-none absolute inset-0 rounded-lg border border-[var(--status-mark)]"
initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: [0, 0.65, 0], scale: [0.8, 1.35, 1.65] }}
transition={{ duration: 1.2, repeat: 1, repeatDelay: 0.2, ease: "easeOut" }} /> : null}
<StatusIcon tone={tone} size="md" className="size-9 bg-background/70"
activity={!result && processing ? "loading" : "static"}>
<motion.span key={`${animationId}-icon`} animate={emphasis ? { rotate: [0, -12, 12, -6, 0] } : { rotate: 0 }}
transition={{ duration: 0.5 }}><Icon size={18} /></motion.span>
</StatusIcon>
</span>
<div data-slot="scada-notice-summary" className="min-w-0 flex-1 space-y-0.5 text-xs leading-5">
<p className="font-semibold">{title}</p>
<p className="text-foreground/75">{note}</p>
</div>
</div>
<div data-slot="scada-notice-footer" className="flex min-w-0 items-center justify-between gap-2 border-t border-[var(--status-border)] pt-1.5">
<p className="text-[11px] leading-4 text-foreground/75">{hint}</p>
<div data-slot="scada-notice-action" className="flex h-8 w-[5.5rem] shrink-0 justify-end">
{result?.task_id && onOpenInvestigation ? <Button size="sm" variant="ghost" className="h-8 gap-1 px-2 text-xs"
onClick={() => onOpenInvestigation(result.task_id!)}>
查看调查<ArrowUpRight size={12} aria-hidden="true" />
</Button> : null}
</div>
</div>
</section>
);
}