fix(analysis): preserve tab panel state

This commit is contained in:
2026-07-09 13:59:01 +08:00
parent 701c5a949d
commit 694f7629ee
24 changed files with 1119 additions and 281 deletions
@@ -24,13 +24,16 @@ import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn";
import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { BurstDetectionResult } from "./types";
interface Props {
onResult: (result: BurstDetectionResult) => void;
state?: BurstDetectionAnalysisParametersState;
onStateChange?: (state: BurstDetectionAnalysisParametersState) => void;
}
interface SchemeItem {
export interface SchemeItem {
scheme_id: number;
scheme_name: string;
scheme_type: string;
@@ -41,20 +44,60 @@ interface SchemeItem {
};
}
const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
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;
}
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,
});
const AnalysisParameters: React.FC<Props> = ({
onResult,
state,
onStateChange,
}) => {
const { open } = useNotification();
const [schemeName, setSchemeName] = useState(`Burst_Detection_${Date.now()}`);
const [dataSource, setDataSource] = useState<"monitoring" | "simulation">("monitoring");
const [schemes, setSchemes] = useState<SchemeItem[]>([]);
const [selectedSchemeId, setSelectedSchemeId] = useState<number | "">("");
const [parametersState, setParametersState, setFormField] = useControllableObjectState(
state,
onStateChange,
createBurstDetectionAnalysisParametersState(),
);
const {
schemeName,
dataSource,
schemes,
selectedSchemeId,
scadaStart,
scadaEnd,
mu,
pointsPerDay,
nEstimators,
contaminationInput,
advancedOpen,
} = parametersState;
const [schemeLoading, setSchemeLoading] = useState(false);
const [scadaStart, setScadaStart] = useState<Dayjs | null>(dayjs().subtract(3, "day"));
const [scadaEnd, setScadaEnd] = useState<Dayjs | null>(dayjs());
const [mu, setMu] = useState<number>(100);
const [pointsPerDay, setPointsPerDay] = useState<number>(96);
const [nEstimators, setNEstimators] = useState<number>(50);
const [contaminationInput, setContaminationInput] = useState<string>("auto");
const [advancedOpen, setAdvancedOpen] = useState(false);
const [running, setRunning] = useState(false);
const isSimulationMode = dataSource === "simulation";
@@ -63,9 +106,12 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
const end = start.add(durationSeconds, "second");
setScadaStart(start);
setScadaEnd(end);
}, []);
setParametersState((previous) => ({
...previous,
scadaStart: start,
scadaEnd: end,
}));
}, [setParametersState]);
const fetchSchemes = useCallback(
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
@@ -80,7 +126,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
(scheme) => scheme.scheme_type === "burst_analysis",
);
setSchemes(burstSchemes);
setFormField("schemes", burstSchemes);
if (selectedSchemeId) {
const matchedScheme = burstSchemes.find(
@@ -89,7 +135,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
if (matchedScheme) {
applySchemeTimeRange(matchedScheme);
} else {
setSelectedSchemeId("");
setFormField("selectedSchemeId", "");
}
}
@@ -111,18 +157,18 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
setSchemeLoading(false);
}
},
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId],
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId, setFormField],
);
const handleDataSourceChange = (value: "monitoring" | "simulation") => {
setDataSource(value);
setFormField("dataSource", value);
if (value === "simulation") {
void fetchSchemes();
}
};
const handleSchemeSelect = (schemeId: number) => {
setSelectedSchemeId(schemeId);
setFormField("selectedSchemeId", schemeId);
const scheme = schemes.find((item) => item.scheme_id === schemeId);
if (scheme) {
applySchemeTimeRange(scheme);
@@ -239,7 +285,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<TextField
value={schemeName}
onChange={(event) => setSchemeName(event.target.value)}
onChange={(event) => setFormField("schemeName", event.target.value)}
placeholder="请输入方案名称"
fullWidth
size="small"
@@ -318,7 +364,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<DateTimePicker
value={scadaStart}
onChange={setScadaStart}
onChange={(value) => setFormField("scadaStart", value)}
maxDateTime={scadaEnd ? scadaEnd.subtract(2, "day") : undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm"
@@ -331,7 +377,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<DateTimePicker
value={scadaEnd}
onChange={setScadaEnd}
onChange={(value) => setFormField("scadaEnd", value)}
minDateTime={scadaStart ? scadaStart.add(2, "day") : undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm"
@@ -356,10 +402,10 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
<Box
role="button"
tabIndex={0}
onClick={() => setAdvancedOpen((prev) => !prev)}
onClick={() => setFormField("advancedOpen", !advancedOpen)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
setAdvancedOpen((prev) => !prev);
setFormField("advancedOpen", !advancedOpen);
}
}}
sx={{
@@ -397,7 +443,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
type="number"
label="频域截断系数"
value={mu}
onChange={(event) => setMu(Number(event.target.value))}
onChange={(event) => setFormField("mu", Number(event.target.value))}
size="small"
fullWidth
inputProps={{ min: 1 }}
@@ -406,7 +452,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
type="number"
label="每日采样点数"
value={pointsPerDay}
onChange={(event) => setPointsPerDay(Number(event.target.value))}
onChange={(event) => setFormField("pointsPerDay", Number(event.target.value))}
size="small"
fullWidth
inputProps={{ min: 1 }}
@@ -415,7 +461,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
type="number"
label="孤立森林树数量"
value={nEstimators}
onChange={(event) => setNEstimators(Number(event.target.value))}
onChange={(event) => setFormField("nEstimators", Number(event.target.value))}
size="small"
fullWidth
inputProps={{ min: 1 }}
@@ -423,7 +469,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
<TextField
label="异常比例"
value={contaminationInput}
onChange={(event) => setContaminationInput(event.target.value)}
onChange={(event) => setFormField("contaminationInput", event.target.value)}
size="small"
fullWidth
helperText="填写 auto 或 0~0.5 之间的小数。"
@@ -442,13 +488,16 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
disabled={running}
sx={{ textTransform: "none", fontWeight: 500 }}
onClick={() => {
setSchemeName(`Burst_Detection_${Date.now()}`);
setScadaStart(dayjs().subtract(3, "day"));
setScadaEnd(dayjs());
setMu(100);
setPointsPerDay(96);
setNEstimators(50);
setContaminationInput("auto");
setParametersState((previous) => ({
...previous,
schemeName: `Burst_Detection_${Date.now()}`,
scadaStart: dayjs().subtract(3, "day"),
scadaEnd: dayjs(),
mu: 100,
pointsPerDay: 96,
nEstimators: 50,
contaminationInput: "auto",
}));
}}
>