feat(burst-detection): update analysis parameters
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState, useCallback } from "react";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Collapse,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
||||
@@ -23,7 +18,7 @@ import { useNotification } from "@refinedev/core";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { api } from "@/lib/api";
|
||||
import { NETWORK_NAME, config } from "@config/config";
|
||||
import { NETWORK_NAME } from "@config/config";
|
||||
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||
import { BurstDetectionResult } from "./types";
|
||||
|
||||
@@ -33,186 +28,149 @@ interface Props {
|
||||
onStateChange?: (state: BurstDetectionAnalysisParametersState) => void;
|
||||
}
|
||||
|
||||
export interface SchemeItem {
|
||||
scheme_id: number;
|
||||
scheme_name: string;
|
||||
scheme_type: string;
|
||||
create_time: string;
|
||||
scheme_start_time: string;
|
||||
scheme_detail?: {
|
||||
modify_total_duration: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BurstDetectionAnalysisParametersState {
|
||||
schemeName: string;
|
||||
dataSource: "monitoring" | "simulation";
|
||||
schemes: SchemeItem[];
|
||||
selectedSchemeId: number | "";
|
||||
scadaStart: Dayjs | null;
|
||||
scadaEnd: Dayjs | null;
|
||||
mu: number;
|
||||
pointsPerDay: number;
|
||||
nEstimators: number;
|
||||
contaminationInput: string;
|
||||
advancedOpen: boolean;
|
||||
detectionMode: "latest" | "historical";
|
||||
targetTime: Dayjs | null;
|
||||
samplingIntervalMinutes: number;
|
||||
samplingIntervalSource: "metadata" | "manual";
|
||||
}
|
||||
|
||||
interface ScadaInfoItem {
|
||||
type?: string;
|
||||
transmission_frequency?: string | number | null;
|
||||
}
|
||||
|
||||
const currentQuarterHour = () => {
|
||||
const now = dayjs().second(0).millisecond(0);
|
||||
return now.minute(Math.floor(now.minute() / 15) * 15);
|
||||
};
|
||||
|
||||
export const createBurstDetectionAnalysisParametersState =
|
||||
(): BurstDetectionAnalysisParametersState => ({
|
||||
schemeName: `Burst_Detection_${Date.now()}`,
|
||||
dataSource: "monitoring",
|
||||
schemes: [],
|
||||
selectedSchemeId: "",
|
||||
scadaStart: dayjs().subtract(3, "day"),
|
||||
scadaEnd: dayjs(),
|
||||
mu: 100,
|
||||
pointsPerDay: 96,
|
||||
nEstimators: 50,
|
||||
contaminationInput: "auto",
|
||||
advancedOpen: false,
|
||||
detectionMode: "latest",
|
||||
targetTime: currentQuarterHour(),
|
||||
samplingIntervalMinutes: 15,
|
||||
samplingIntervalSource: "metadata",
|
||||
});
|
||||
|
||||
export const parseScadaFrequencyMinutes = (
|
||||
value: string | number | null | undefined,
|
||||
): number | null => {
|
||||
if (typeof value === "number") {
|
||||
return Number.isInteger(value) && value > 0 ? value : null;
|
||||
}
|
||||
if (!value) return null;
|
||||
const normalized = value.trim();
|
||||
const dayMatch = normalized.match(/^(\d+)\s+days?,\s*(.+)$/i);
|
||||
const days = dayMatch ? Number(dayMatch[1]) : 0;
|
||||
const timePart = dayMatch ? dayMatch[2] : normalized;
|
||||
const parts = timePart.split(":").map(Number);
|
||||
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
|
||||
return null;
|
||||
}
|
||||
const minutes = days * 1440 + parts[0] * 60 + parts[1] + parts[2] / 60;
|
||||
return Number.isInteger(minutes) && minutes > 0 ? minutes : null;
|
||||
};
|
||||
|
||||
export const resolvePressureSamplingInterval = (items: ScadaInfoItem[]) => {
|
||||
const counts = new Map<number, number>();
|
||||
items
|
||||
.filter((item) => item.type?.toLowerCase() === "pressure")
|
||||
.forEach((item) => {
|
||||
const minutes = parseScadaFrequencyMinutes(item.transmission_frequency);
|
||||
if (minutes && 1440 % minutes === 0) {
|
||||
counts.set(minutes, (counts.get(minutes) ?? 0) + 1);
|
||||
}
|
||||
});
|
||||
return [...counts.entries()].sort(
|
||||
([minutesA, countA], [minutesB, countB]) =>
|
||||
countB - countA || minutesA - minutesB,
|
||||
)[0]?.[0] ?? 15;
|
||||
};
|
||||
|
||||
export const buildBurstDetectionRequest = (
|
||||
parameters: BurstDetectionAnalysisParametersState,
|
||||
network: string,
|
||||
) => ({
|
||||
network,
|
||||
scheme_name: parameters.schemeName.trim(),
|
||||
sampling_interval_minutes: parameters.samplingIntervalMinutes,
|
||||
...(parameters.detectionMode === "historical" && parameters.targetTime
|
||||
? { target_time: parameters.targetTime.toISOString() }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const AnalysisParameters: React.FC<Props> = ({
|
||||
onResult,
|
||||
state,
|
||||
onStateChange,
|
||||
}) => {
|
||||
const { open } = useNotification();
|
||||
const [parametersState, setParametersState, setFormField] = useControllableObjectState(
|
||||
state,
|
||||
onStateChange,
|
||||
createBurstDetectionAnalysisParametersState(),
|
||||
);
|
||||
const [parametersState, setParametersState, setFormField] =
|
||||
useControllableObjectState(
|
||||
state,
|
||||
onStateChange,
|
||||
createBurstDetectionAnalysisParametersState(),
|
||||
);
|
||||
const {
|
||||
schemeName,
|
||||
dataSource,
|
||||
schemes,
|
||||
selectedSchemeId,
|
||||
scadaStart,
|
||||
scadaEnd,
|
||||
mu,
|
||||
pointsPerDay,
|
||||
nEstimators,
|
||||
contaminationInput,
|
||||
advancedOpen,
|
||||
detectionMode,
|
||||
targetTime,
|
||||
samplingIntervalMinutes,
|
||||
samplingIntervalSource,
|
||||
} = parametersState;
|
||||
const [schemeLoading, setSchemeLoading] = useState(false);
|
||||
const [running, setRunning] = useState(false);
|
||||
const isSimulationMode = dataSource === "simulation";
|
||||
const [frequencyLoading, setFrequencyLoading] = useState(false);
|
||||
|
||||
const applySchemeTimeRange = useCallback((scheme: SchemeItem) => {
|
||||
const start = dayjs(scheme.scheme_start_time);
|
||||
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
|
||||
const end = start.add(durationSeconds, "second");
|
||||
|
||||
setParametersState((previous) => ({
|
||||
...previous,
|
||||
scadaStart: start,
|
||||
scadaEnd: end,
|
||||
}));
|
||||
}, [setParametersState]);
|
||||
|
||||
const fetchSchemes = useCallback(
|
||||
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
|
||||
if (schemeLoading || (!force && schemes.length > 0)) return;
|
||||
|
||||
setSchemeLoading(true);
|
||||
try {
|
||||
const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
|
||||
params: { network: NETWORK_NAME },
|
||||
});
|
||||
const burstSchemes = (response.data as SchemeItem[]).filter(
|
||||
(scheme) => scheme.scheme_type === "burst_analysis",
|
||||
).sort(
|
||||
(a, b) =>
|
||||
dayjs(b.create_time).valueOf() - dayjs(a.create_time).valueOf(),
|
||||
useEffect(() => {
|
||||
if (samplingIntervalSource !== "metadata") return;
|
||||
let active = true;
|
||||
setFrequencyLoading(true);
|
||||
api
|
||||
.get("/api/v1/getallscadainfo/", { params: { network: NETWORK_NAME } })
|
||||
.then((response) => {
|
||||
if (!active) return;
|
||||
const interval = resolvePressureSamplingInterval(
|
||||
response.data as ScadaInfoItem[],
|
||||
);
|
||||
setParametersState((previous) =>
|
||||
previous.samplingIntervalSource === "metadata"
|
||||
? { ...previous, samplingIntervalMinutes: interval }
|
||||
: previous,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep the 15-minute fallback when SCADA metadata is unavailable.
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setFrequencyLoading(false);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [samplingIntervalSource, setParametersState]);
|
||||
|
||||
setFormField("schemes", burstSchemes);
|
||||
const samplingIntervalValid =
|
||||
Number.isInteger(samplingIntervalMinutes) &&
|
||||
samplingIntervalMinutes > 0 &&
|
||||
1440 % samplingIntervalMinutes === 0;
|
||||
|
||||
if (selectedSchemeId) {
|
||||
const matchedScheme = burstSchemes.find(
|
||||
(scheme) => scheme.scheme_id === selectedSchemeId,
|
||||
);
|
||||
if (matchedScheme) {
|
||||
applySchemeTimeRange(matchedScheme);
|
||||
} else {
|
||||
setFormField("selectedSchemeId", "");
|
||||
}
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
open?.({
|
||||
type: "success",
|
||||
message: "方案列表已刷新",
|
||||
description: `当前可选爆管分析方案 ${burstSchemes.length} 个`,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "刷新方案失败",
|
||||
description:
|
||||
error?.response?.data?.detail ?? error?.message ?? "无法获取爆管分析方案列表",
|
||||
});
|
||||
} finally {
|
||||
setSchemeLoading(false);
|
||||
}
|
||||
},
|
||||
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId, setFormField],
|
||||
const isValid = useMemo(
|
||||
() =>
|
||||
schemeName.trim().length > 0 &&
|
||||
samplingIntervalValid &&
|
||||
(detectionMode === "latest" || Boolean(targetTime?.isValid())),
|
||||
[detectionMode, samplingIntervalValid, schemeName, targetTime],
|
||||
);
|
||||
|
||||
const handleDataSourceChange = (value: "monitoring" | "simulation") => {
|
||||
setFormField("dataSource", value);
|
||||
if (value === "simulation") {
|
||||
void fetchSchemes();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSchemeSelect = (schemeId: number) => {
|
||||
setFormField("selectedSchemeId", schemeId);
|
||||
const scheme = schemes.find((item) => item.scheme_id === schemeId);
|
||||
if (scheme) {
|
||||
applySchemeTimeRange(scheme);
|
||||
}
|
||||
};
|
||||
|
||||
const timeWindowValid = useMemo(() => {
|
||||
if (!scadaStart || !scadaEnd) return false;
|
||||
return scadaEnd.diff(scadaStart, "day", true) >= 2;
|
||||
}, [scadaEnd, scadaStart]);
|
||||
|
||||
const contaminationValue = useMemo(() => {
|
||||
const normalized = contaminationInput.trim().toLowerCase();
|
||||
if (!normalized || normalized === "auto") {
|
||||
return "auto" as const;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= 0.5) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}, [contaminationInput]);
|
||||
|
||||
const isValid =
|
||||
Boolean(scadaStart && scadaEnd) &&
|
||||
timeWindowValid &&
|
||||
Number.isFinite(mu) &&
|
||||
mu > 0 &&
|
||||
Number.isFinite(pointsPerDay) &&
|
||||
pointsPerDay > 0 &&
|
||||
Number.isFinite(nEstimators) &&
|
||||
nEstimators > 0 &&
|
||||
contaminationValue !== null &&
|
||||
(dataSource !== "simulation" || Boolean(selectedSchemeId));
|
||||
|
||||
const handleRun = async () => {
|
||||
if (!isValid || !scadaStart || !scadaEnd || contaminationValue === null) {
|
||||
if (!isValid) {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "参数不完整",
|
||||
description: "请检查时间范围(至少2天)和高级参数是否填写正确。",
|
||||
description: "请输入方案名称,并检查历史目标时间。",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -222,50 +180,23 @@ const AnalysisParameters: React.FC<Props> = ({
|
||||
key: "burst-detection-analysis-progress",
|
||||
type: "progress",
|
||||
message: "正在执行爆管侦测",
|
||||
description: "正在读取数据并计算异常分数。",
|
||||
description: "正在读取目标时刻及前 14 天同刻基线。",
|
||||
undoableTimeout: 3,
|
||||
});
|
||||
|
||||
try {
|
||||
const selectedScheme =
|
||||
dataSource === "simulation"
|
||||
? schemes.find((item) => item.scheme_id === selectedSchemeId)
|
||||
: undefined;
|
||||
|
||||
const response = await api.post("/api/v1/burst-detection/detect/", {
|
||||
network: NETWORK_NAME,
|
||||
data_source: dataSource,
|
||||
scheme_name: schemeName.trim() || undefined,
|
||||
scada_start: scadaStart.toISOString(),
|
||||
scada_end: scadaEnd.toISOString(),
|
||||
mu,
|
||||
points_per_day: pointsPerDay,
|
||||
iforest_params: {
|
||||
n_estimators: nEstimators,
|
||||
contamination: contaminationValue,
|
||||
},
|
||||
simulation_scheme_name: selectedScheme?.scheme_name,
|
||||
simulation_scheme_type: selectedScheme?.scheme_type,
|
||||
});
|
||||
|
||||
onResult({
|
||||
...(response.data as BurstDetectionResult),
|
||||
scheme_name: schemeName.trim() || (response.data as BurstDetectionResult).scheme_name,
|
||||
algorithm_params: {
|
||||
mu,
|
||||
points_per_day: pointsPerDay,
|
||||
iforest_params: {
|
||||
n_estimators: nEstimators,
|
||||
contamination: contaminationValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await api.post(
|
||||
"/api/v1/burst-detection/detect/",
|
||||
buildBurstDetectionRequest(parametersState, NETWORK_NAME),
|
||||
);
|
||||
onResult(response.data as BurstDetectionResult);
|
||||
open?.({
|
||||
key: "burst-detection-analysis-success",
|
||||
type: "success",
|
||||
message: "爆管侦测完成",
|
||||
description: `共识别 ${response.data.summary?.anomaly_day_count ?? 0} 个异常日。`,
|
||||
description: response.data.summary?.burst_detected
|
||||
? "目标时刻存在异常信号,请优先复核相关测点。"
|
||||
: "目标时刻未发现爆管异常。",
|
||||
});
|
||||
} catch (error: any) {
|
||||
open?.({
|
||||
@@ -280,7 +211,7 @@ const AnalysisParameters: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col flex-1 min-h-0">
|
||||
<Box className="flex min-h-0 flex-1 flex-col">
|
||||
<Box className="flex flex-col gap-3">
|
||||
<Box>
|
||||
<Typography variant="subtitle2" className="mb-1 font-medium">
|
||||
@@ -297,211 +228,89 @@ const AnalysisParameters: React.FC<Props> = ({
|
||||
|
||||
<Box>
|
||||
<Typography variant="subtitle2" className="mb-1 font-medium">
|
||||
数据来源
|
||||
侦测方式
|
||||
</Typography>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
value={dataSource}
|
||||
onChange={(e) => handleDataSourceChange(e.target.value as "monitoring" | "simulation")}
|
||||
value={detectionMode}
|
||||
onChange={(event) =>
|
||||
setFormField(
|
||||
"detectionMode",
|
||||
event.target.value as "latest" | "historical",
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuItem value="monitoring">监测数据</MenuItem>
|
||||
<MenuItem value="simulation">模拟方案</MenuItem>
|
||||
<MenuItem value="latest">检测最新数据</MenuItem>
|
||||
<MenuItem value="historical">历史时刻回放</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{isSimulationMode && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" className="mb-1 font-medium">
|
||||
选择爆管分析方案
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
value={selectedSchemeId}
|
||||
onChange={(e) => handleSchemeSelect(Number(e.target.value))}
|
||||
disabled={schemeLoading}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="" disabled>
|
||||
请选择方案
|
||||
</MenuItem>
|
||||
{schemes.map((scheme) => (
|
||||
<MenuItem key={scheme.scheme_id} value={scheme.scheme_id}>
|
||||
{scheme.scheme_name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() => void fetchSchemes({ force: true, notify: true })}
|
||||
disabled={schemeLoading}
|
||||
aria-label="刷新爆管分析方案"
|
||||
sx={{
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
{schemeLoading ? (
|
||||
<CircularProgress size={18} color="inherit" />
|
||||
) : (
|
||||
<RefreshIcon fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDayjs}
|
||||
adapterLocale="zh-cn"
|
||||
localeText={pickerZhCN.components.MuiLocalizationProvider.defaultProps.localeText}
|
||||
>
|
||||
<Box className="grid grid-cols-2 gap-2">
|
||||
<Box>
|
||||
<Typography variant="subtitle2" className="mb-1 font-medium">
|
||||
侦测开始时间
|
||||
</Typography>
|
||||
<DateTimePicker
|
||||
value={scadaStart}
|
||||
onChange={(value) => setFormField("scadaStart", value)}
|
||||
maxDateTime={scadaEnd ? scadaEnd.subtract(2, "day") : undefined}
|
||||
disabled={isSimulationMode}
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" className="mb-1 font-medium">
|
||||
侦测结束时间
|
||||
</Typography>
|
||||
<DateTimePicker
|
||||
value={scadaEnd}
|
||||
onChange={(value) => setFormField("scadaEnd", value)}
|
||||
minDateTime={scadaStart ? scadaStart.add(2, "day") : undefined}
|
||||
disabled={isSimulationMode}
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</LocalizationProvider>
|
||||
|
||||
<Box className="rounded-lg border border-blue-100 bg-blue-50 px-3 py-2 text-sm text-blue-900">
|
||||
当前页面为展示版:手动触发一次侦测,展示异常日、最新测点排名和结果表格,不做定时轮询。
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
border: "1px solid",
|
||||
borderColor: "grey.200",
|
||||
borderRadius: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setFormField("advancedOpen", !advancedOpen)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
setFormField("advancedOpen", !advancedOpen);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent",
|
||||
"&:hover": { backgroundColor: "action.hover" },
|
||||
}}
|
||||
{detectionMode === "historical" ? (
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDayjs}
|
||||
adapterLocale="zh-cn"
|
||||
localeText={pickerZhCN.components.MuiLocalizationProvider.defaultProps.localeText}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
高级参数
|
||||
</Typography>
|
||||
<ExpandMoreIcon
|
||||
sx={{
|
||||
transform: advancedOpen ? "rotate(180deg)" : "rotate(0deg)",
|
||||
transition: "transform 0.2s ease",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Collapse in={advancedOpen} timeout="auto" unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
px: 1.25,
|
||||
pt: 1.25,
|
||||
pb: 1.25,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
>
|
||||
<Box className="flex flex-col gap-3">
|
||||
<TextField
|
||||
type="number"
|
||||
label="频域截断系数"
|
||||
value={mu}
|
||||
onChange={(event) => setFormField("mu", Number(event.target.value))}
|
||||
size="small"
|
||||
fullWidth
|
||||
inputProps={{ min: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="每日采样点数"
|
||||
value={pointsPerDay}
|
||||
onChange={(event) => setFormField("pointsPerDay", Number(event.target.value))}
|
||||
size="small"
|
||||
fullWidth
|
||||
inputProps={{ min: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="孤立森林树数量"
|
||||
value={nEstimators}
|
||||
onChange={(event) => setFormField("nEstimators", Number(event.target.value))}
|
||||
size="small"
|
||||
fullWidth
|
||||
inputProps={{ min: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="异常比例"
|
||||
value={contaminationInput}
|
||||
onChange={(event) => setFormField("contaminationInput", event.target.value)}
|
||||
size="small"
|
||||
fullWidth
|
||||
helperText="填写 auto 或 0~0.5 之间的小数。"
|
||||
error={contaminationValue === null}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" className="mb-1 font-medium">
|
||||
目标时刻
|
||||
</Typography>
|
||||
<DateTimePicker
|
||||
value={targetTime}
|
||||
onChange={(value) => setFormField("targetTime", value)}
|
||||
maxDateTime={dayjs()}
|
||||
minutesStep={15}
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</LocalizationProvider>
|
||||
) : null}
|
||||
|
||||
<TextField
|
||||
type="number"
|
||||
label="采样间隔(分钟)"
|
||||
value={samplingIntervalMinutes}
|
||||
onChange={(event) => {
|
||||
setParametersState((previous) => ({
|
||||
...previous,
|
||||
samplingIntervalMinutes: Number(event.target.value),
|
||||
samplingIntervalSource: "manual",
|
||||
}));
|
||||
}}
|
||||
size="small"
|
||||
fullWidth
|
||||
error={!samplingIntervalValid}
|
||||
inputProps={{ min: 1, max: 1440, step: 1 }}
|
||||
helperText={
|
||||
samplingIntervalValid
|
||||
? `${frequencyLoading ? "正在读取 SCADA 频率" : samplingIntervalSource === "metadata" ? "默认取自压力 SCADA 频率" : "已手动设置"},每天 ${1440 / samplingIntervalMinutes} 个采样点`
|
||||
: "请输入能整除 1440 分钟的正整数,例如 1、5、10、15、30 或 60。"
|
||||
}
|
||||
/>
|
||||
|
||||
{detectionMode === "latest" ? (
|
||||
<Box className="rounded-lg border border-blue-100 bg-blue-50 px-3 py-2 text-sm text-blue-900">
|
||||
系统自动读取目标时刻及前 14 天同一时刻数据。每天使用截至该时刻的
|
||||
24 小时压力序列提取扰动特征,仅判定目标时刻是否异常。
|
||||
</Box>
|
||||
) : null}
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
当前口径:{samplingIntervalMinutes || "-"} 分钟采样、
|
||||
{samplingIntervalValid ? 1440 / samplingIntervalMinutes : "-"} 点/天、14
|
||||
个参考日;缺失数据的测点不会插值,将从本次分析中排除。
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box className="mt-auto pt-3 flex gap-2">
|
||||
<Box className="mt-auto flex gap-2 pt-3">
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
disabled={running}
|
||||
sx={{ textTransform: "none", fontWeight: 500 }}
|
||||
onClick={() => {
|
||||
setParametersState((previous) => ({
|
||||
...previous,
|
||||
schemeName: `Burst_Detection_${Date.now()}`,
|
||||
scadaStart: dayjs().subtract(3, "day"),
|
||||
scadaEnd: dayjs(),
|
||||
mu: 100,
|
||||
pointsPerDay: 96,
|
||||
nEstimators: 50,
|
||||
contaminationInput: "auto",
|
||||
}));
|
||||
}}
|
||||
onClick={() =>
|
||||
setParametersState(createBurstDetectionAnalysisParametersState())
|
||||
}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
@@ -509,11 +318,13 @@ const AnalysisParameters: React.FC<Props> = ({
|
||||
variant="contained"
|
||||
fullWidth
|
||||
disabled={!isValid || running}
|
||||
onClick={handleRun}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
sx={{ textTransform: "none", fontWeight: 500 }}
|
||||
onClick={() => void handleRun()}
|
||||
>
|
||||
{running ? <CircularProgress size={20} color="inherit" /> : "开始侦测"}
|
||||
{running
|
||||
? "侦测中..."
|
||||
: detectionMode === "latest"
|
||||
? "侦测最新数据"
|
||||
: "回放目标时刻"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user