75 lines
5.5 KiB
TypeScript
75 lines
5.5 KiB
TypeScript
import type { ScadaMonitoringCalibration, ScadaMonitoringConfig } from "../api/scada-replay-client";
|
||
|
||
export function ScadaMonitoringRules({ config, calibration, frozen }: {
|
||
config: ScadaMonitoringConfig;
|
||
calibration?: ScadaMonitoringCalibration | null;
|
||
frozen: boolean;
|
||
}) {
|
||
const weighted = config.decision_policy === "weighted";
|
||
const currentPolicy = calibration?.version === "scada-monitor-calibration@2"
|
||
&& calibration.diagnostic_policy === "normal-phase-envelope@1";
|
||
const usable = currentPolicy ? calibration : null;
|
||
const ready = usable?.status === "ready";
|
||
const indicators = [
|
||
{ key: "burst_detection", label: "爆管侦测", threshold: ready && usable.burst_detection?.threshold != null
|
||
? `得分 ≤ ${usable.burst_detection.threshold.toFixed(4)}` : "由前 15 天正常历史计算" },
|
||
{ key: "pressure", label: "压力下降", threshold: thresholdLabel(config.pressure_device_ids, usable?.pressure, "下降") },
|
||
{ key: "outlet_flow", label: "出厂流量", threshold: config.outlet_flow_device_ids.length
|
||
? thresholdLabel(config.outlet_flow_device_ids, usable?.outlet_flow, "增加") : "未配置,贡献为 0" }
|
||
] as const;
|
||
|
||
return (
|
||
<section aria-label="组合监测规则" className="space-y-3 text-xs leading-5">
|
||
<div className="space-y-1">
|
||
<h4 className="font-semibold text-foreground">SCADA 异常触发规则</h4>
|
||
<p className="text-muted-foreground">压力、流量越过历史同期正常范围,或水质越过已配置上下限,即报警并启动调查。</p>
|
||
<ol aria-label="诊断流程" className="flex flex-wrap gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
|
||
<li>数据诊断</li><li aria-hidden="true">→</li><li>指标异常</li><li aria-hidden="true">→</li><li>爆管诊断</li><li aria-hidden="true">→</li><li>报告</li>
|
||
</ol>
|
||
</div>
|
||
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
|
||
<h4 className="font-semibold text-foreground">{weighted ? "爆管诊断 · 三指标加权" : "爆管诊断 · 三指标联合判定"}</h4>
|
||
<span className="text-[11px] text-muted-foreground">{frozen ? "本轮已固定" : "下一轮使用项目配置"}</span>
|
||
</div>
|
||
{weighted ? (
|
||
<>
|
||
<dl className="divide-y divide-border">
|
||
{indicators.map(({ key, label, threshold }) => (
|
||
<div key={key} className="grid grid-cols-[1fr_auto] gap-x-3 py-2 first:pt-0">
|
||
<dt className="text-foreground">{label}</dt>
|
||
<dd className="font-medium text-foreground tabular-nums">权重 {(config.weights[key] * 100).toFixed(0)}%</dd>
|
||
<dd className="col-span-2 text-[11px] text-muted-foreground tabular-nums">{threshold}</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
<p className="font-medium text-foreground tabular-nums">综合分 ≥ {config.alert_threshold.toFixed(2)} 时标记疑似爆管</p>
|
||
<p className="text-[11px] text-muted-foreground">加权分用于评估爆管嫌疑,任一指标异常即启动调查。</p>
|
||
<p className="text-[11px] text-muted-foreground">压力下降与出厂流量增加需同时越过动态变化阈值和历史同期正常范围。正常日的日内波动由同期范围校验。</p>
|
||
<p className="text-[11px] text-muted-foreground">指标命中后计入对应权重;流量缺数按 0 计,不重新分配权重。压力或侦测数据不足时暂不作出组合判定。</p>
|
||
<div className="space-y-1 text-[11px] text-muted-foreground">
|
||
<p className="font-medium text-foreground">动态阈值 · {ready ? "本轮已冻结" : "等待历史基线"}</p>
|
||
{ready ? <p className="tabular-nums">历史区间 {dateTime(usable.history_start)} 至 {dateTime(usable.knowledge_cutoff)}(不含结束时刻)</p> : <p>{calibration && !currentPolicy ? "旧版校准已停用,请重新开始本轮。" : calibration ? "历史基线不足,动态阈值尚未就绪。" : "开始监测后,以演示开始前 15 天的正常历史逐测点计算阈值,并在本轮固定。"}</p>}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p className="text-muted-foreground">本轮使用三指标联合评估爆管嫌疑;任一经历史基线校验的指标异常即启动调查。</p>
|
||
)}
|
||
<p className="text-[11px] text-muted-foreground">同一异常只报警并建单一次,持续异常沿用原工单;连续两个正常周期且相关测点恢复后解除告警。</p>
|
||
<p className="text-[11px] text-muted-foreground tabular-nums">压力 {config.pressure_device_ids.length} 个测点 · 出厂流量 {config.outlet_flow_device_ids.length} 个测点 · {config.rule_version}</p>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function thresholdLabel(ids: string[], profiles: ScadaMonitoringCalibration["pressure"] | undefined, direction: string) {
|
||
const thresholds = ids.flatMap((id) => profiles?.[id] ? [profiles[id].threshold_percent] : []);
|
||
if (!thresholds.length) return "逐测点动态计算变化阈值";
|
||
const lower = Math.min(...thresholds).toFixed(2);
|
||
const upper = Math.max(...thresholds).toFixed(2);
|
||
const range = lower === upper ? `${direction}阈值 ${lower}%` : `${direction}阈值 ${lower}%~${upper}%(逐测点)`;
|
||
return thresholds.length < ids.length ? `${range} · 已就绪 ${thresholds.length}/${ids.length} 个测点` : range;
|
||
}
|
||
|
||
function dateTime(value: string) {
|
||
return new Date(value).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false });
|
||
}
|