Share project JSON transport, interaction replies and report selectors. Settle tool progress consistently for direct streams, reconnects and history, preserving failure reasons and stopped state.
90 lines
5.7 KiB
TypeScript
90 lines
5.7 KiB
TypeScript
import { formatLocalMonthDayTime } from "@/shared/utils/date-time";
|
|
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>
|
|
{formatLocalMonthDayTime(result.period_end)}
|
|
</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>
|
|
);
|
|
}
|