fix(analysis): preserve tab panel state
This commit is contained in:
@@ -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",
|
||||
}));
|
||||
}}
|
||||
>
|
||||
重置
|
||||
|
||||
@@ -9,9 +9,18 @@ import {
|
||||
FormatListBulleted,
|
||||
Search as SearchIcon,
|
||||
} from "@mui/icons-material";
|
||||
import AnalysisParameters from "./AnalysisParameters";
|
||||
import DetectionResults from "./DetectionResults";
|
||||
import SchemeQuery from "./SchemeQuery";
|
||||
import AnalysisParameters, {
|
||||
createBurstDetectionAnalysisParametersState,
|
||||
type BurstDetectionAnalysisParametersState,
|
||||
} from "./AnalysisParameters";
|
||||
import DetectionResults, {
|
||||
createBurstDetectionResultsState,
|
||||
type BurstDetectionResultsState,
|
||||
} from "./DetectionResults";
|
||||
import SchemeQuery, {
|
||||
createBurstDetectionSchemeQueryState,
|
||||
type BurstDetectionSchemeQueryState,
|
||||
} from "./SchemeQuery";
|
||||
import { BurstDetectionResult, BurstDetectionSchemeRecord } from "./types";
|
||||
|
||||
const TabPanel = ({
|
||||
@@ -33,6 +42,18 @@ const BurstDetectionPanel: React.FC = () => {
|
||||
const [tab, setTab] = useState(0);
|
||||
const [result, setResult] = useState<BurstDetectionResult | null>(null);
|
||||
const [schemes, setSchemes] = useState<BurstDetectionSchemeRecord[]>([]);
|
||||
const [analysisState, setAnalysisState] =
|
||||
useState<BurstDetectionAnalysisParametersState>(
|
||||
createBurstDetectionAnalysisParametersState,
|
||||
);
|
||||
const [queryState, setQueryState] =
|
||||
useState<BurstDetectionSchemeQueryState>(
|
||||
createBurstDetectionSchemeQueryState,
|
||||
);
|
||||
const [resultsState, setResultsState] =
|
||||
useState<BurstDetectionResultsState>(
|
||||
createBurstDetectionResultsState,
|
||||
);
|
||||
|
||||
const drawerWidth = 450;
|
||||
const panelTitle = "爆管侦测";
|
||||
@@ -137,13 +158,27 @@ const BurstDetectionPanel: React.FC = () => {
|
||||
</Box>
|
||||
|
||||
<TabPanel value={tab} index={0}>
|
||||
<AnalysisParameters onResult={handleResult} />
|
||||
<AnalysisParameters
|
||||
onResult={handleResult}
|
||||
state={analysisState}
|
||||
onStateChange={setAnalysisState}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={tab} index={1}>
|
||||
<SchemeQuery onViewResult={handleResult} schemes={schemes} onSchemesChange={setSchemes} />
|
||||
<SchemeQuery
|
||||
onViewResult={handleResult}
|
||||
schemes={schemes}
|
||||
onSchemesChange={setSchemes}
|
||||
state={queryState}
|
||||
onStateChange={setQueryState}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={tab} index={2}>
|
||||
<DetectionResults result={result} />
|
||||
<DetectionResults
|
||||
result={result}
|
||||
state={resultsState}
|
||||
onStateChange={setResultsState}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Drawer>
|
||||
|
||||
@@ -24,8 +24,19 @@ import { Circle, Fill, Stroke, Style } from "ol/style";
|
||||
import { bbox, featureCollection } from "@turf/turf";
|
||||
import { BurstDetectionResult, BurstDetectionRow } from "./types";
|
||||
|
||||
export interface BurstDetectionResultsState {
|
||||
selectedDay: number | null;
|
||||
}
|
||||
|
||||
export const createBurstDetectionResultsState =
|
||||
(): BurstDetectionResultsState => ({
|
||||
selectedDay: null,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
result: BurstDetectionResult | null;
|
||||
state?: BurstDetectionResultsState;
|
||||
onStateChange?: (state: BurstDetectionResultsState) => void;
|
||||
}
|
||||
|
||||
interface MetricCardProps {
|
||||
@@ -106,11 +117,25 @@ const getScoreLevel = (score: number) => {
|
||||
|
||||
const formatDateTime = (value?: string) => (value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-");
|
||||
|
||||
const DetectionResults: React.FC<Props> = ({ result }) => {
|
||||
const DetectionResults: React.FC<Props> = ({
|
||||
result,
|
||||
state,
|
||||
onStateChange,
|
||||
}) => {
|
||||
const map = useMap();
|
||||
const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
||||
const [selectedDay, setSelectedDay] = useState<number | null>(null);
|
||||
const [internalResultsState, setInternalResultsState] =
|
||||
useState<BurstDetectionResultsState>(createBurstDetectionResultsState);
|
||||
const resultsState = state ?? internalResultsState;
|
||||
const selectedDay = resultsState.selectedDay;
|
||||
const setSelectedDay = (value: number | null) => {
|
||||
const nextState = { ...resultsState, selectedDay: value };
|
||||
if (state === undefined) {
|
||||
setInternalResultsState(nextState);
|
||||
}
|
||||
onStateChange?.(nextState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
@@ -23,6 +23,7 @@ import "dayjs/locale/zh-cn";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { api } from "@/lib/api";
|
||||
import { NETWORK_NAME } from "@config/config";
|
||||
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||
import {
|
||||
BurstDetectionResult,
|
||||
BurstDetectionSchemeDetail,
|
||||
@@ -33,15 +34,39 @@ interface Props {
|
||||
onViewResult: (result: BurstDetectionResult) => void;
|
||||
schemes?: BurstDetectionSchemeRecord[];
|
||||
onSchemesChange?: (schemes: BurstDetectionSchemeRecord[]) => void;
|
||||
state?: BurstDetectionSchemeQueryState;
|
||||
onStateChange?: (state: BurstDetectionSchemeQueryState) => void;
|
||||
}
|
||||
|
||||
const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes, onSchemesChange }) => {
|
||||
export interface BurstDetectionSchemeQueryState {
|
||||
queryAll: boolean;
|
||||
queryDate: Dayjs | null;
|
||||
expandedId: number | null;
|
||||
}
|
||||
|
||||
export const createBurstDetectionSchemeQueryState =
|
||||
(): BurstDetectionSchemeQueryState => ({
|
||||
queryAll: true,
|
||||
queryDate: dayjs(),
|
||||
expandedId: null,
|
||||
});
|
||||
|
||||
const SchemeQuery: React.FC<Props> = ({
|
||||
onViewResult,
|
||||
schemes: externalSchemes,
|
||||
onSchemesChange,
|
||||
state,
|
||||
onStateChange,
|
||||
}) => {
|
||||
const { open } = useNotification();
|
||||
const [queryAll, setQueryAll] = useState(true);
|
||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs());
|
||||
const [queryState, , setQueryField] = useControllableObjectState(
|
||||
state,
|
||||
onStateChange,
|
||||
createBurstDetectionSchemeQueryState(),
|
||||
);
|
||||
const { queryAll, queryDate, expandedId } = queryState;
|
||||
const [internalSchemes, setInternalSchemes] = useState<BurstDetectionSchemeRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
||||
const setSchemes = onSchemesChange || setInternalSchemes;
|
||||
|
||||
@@ -159,7 +184,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={queryAll}
|
||||
onChange={(event) => setQueryAll(event.target.checked)}
|
||||
onChange={(event) => setQueryField("queryAll", event.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">查询全部</Typography>}
|
||||
@@ -168,7 +193,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||
<DatePicker
|
||||
value={queryDate}
|
||||
onChange={setQueryDate}
|
||||
onChange={(value) => setQueryField("queryDate", value)}
|
||||
disabled={queryAll}
|
||||
format="YYYY-MM-DD"
|
||||
slotProps={{ textField: { size: "small", sx: { width: 180 } } }}
|
||||
@@ -241,7 +266,10 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)
|
||||
setQueryField(
|
||||
"expandedId",
|
||||
expandedId === scheme.scheme_id ? null : scheme.scheme_id,
|
||||
)
|
||||
}
|
||||
color="primary"
|
||||
className="p-1"
|
||||
|
||||
Reference in New Issue
Block a user