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
@@ -19,27 +19,63 @@ import "dayjs/locale/zh-cn";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { LeakageResultDetail } from "./types";
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
interface Props {
onResult: (result: LeakageResultDetail) => void;
state?: DMALeakAnalysisParametersState;
onStateChange?: (state: DMALeakAnalysisParametersState) => void;
}
const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
export interface DMALeakAnalysisParametersState {
schemeName: string;
dmaCount: number;
startTime: Dayjs | null;
endTime: Dayjs | null;
popSize: number;
maxGen: number;
qSum: number;
advancedOpen: boolean;
}
export const createDMALeakAnalysisParametersState =
(): DMALeakAnalysisParametersState => ({
schemeName: `DMA_Leak_${Date.now()}`,
dmaCount: 5,
startTime: dayjs().subtract(2, "hour"),
endTime: dayjs(),
popSize: 10,
maxGen: 50,
qSum: 1440,
advancedOpen: false,
});
const AnalysisParameters: React.FC<Props> = ({
onResult,
state,
onStateChange,
}) => {
const { open } = useNotification();
const [schemeName, setSchemeName] = useState(`DMA_Leak_${Date.now()}`);
const [dmaCount, setDmaCount] = useState<number>(5);
const [startTime, setStartTime] = useState<Dayjs | null>(
dayjs().subtract(2, "hour"),
const [parametersState, , setFormField] = useControllableObjectState(
state,
onStateChange,
createDMALeakAnalysisParametersState(),
);
const [endTime, setEndTime] = useState<Dayjs | null>(dayjs());
const [popSize, setPopSize] = useState<number>(10);
const [maxGen, setMaxGen] = useState<number>(50);
const [qSum, setQSum] = useState<number>(1440);
const [advancedOpen, setAdvancedOpen] = useState(false);
const {
schemeName,
dmaCount,
startTime,
endTime,
popSize,
maxGen,
qSum,
advancedOpen,
} = parametersState;
const [running, setRunning] = useState(false);
const isValid = useMemo(() => {
if (!schemeName.trim() || !startTime || !endTime) return false;
return startTime.isBefore(endTime) && qSum >= 360;
@@ -105,7 +141,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<TextField
value={schemeName}
onChange={(e) => setSchemeName(e.target.value)}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="请输入方案名称"
fullWidth
size="small"
@@ -123,11 +159,11 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
const value = Number.parseInt(e.target.value, 10);
// Limit between 3 and 10
if (Number.isNaN(value)) {
setDmaCount(5);
setFormField("dmaCount", 5);
} else if (value > 10) {
setDmaCount(10);
setFormField("dmaCount", 10);
} else {
setDmaCount(Math.max(3, value));
setFormField("dmaCount", Math.max(3, value));
}
}}
fullWidth
@@ -150,7 +186,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<DateTimePicker
value={startTime}
onChange={setStartTime}
onChange={(value) => setFormField("startTime", value)}
maxDateTime={endTime ?? undefined}
format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }}
@@ -162,7 +198,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<DateTimePicker
value={endTime}
onChange={setEndTime}
onChange={(value) => setFormField("endTime", value)}
minDateTime={startTime ?? undefined}
format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }}
@@ -180,7 +216,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
value={qSum}
onChange={(e) => {
const value = Number(e.target.value);
setQSum(Number.isNaN(value) ? 1440 : Math.max(360, value));
setFormField("qSum", Number.isNaN(value) ? 1440 : Math.max(360, value));
}}
inputProps={{ min: 360, step: 10 }}
/>
@@ -195,9 +231,9 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
<Box
role="button"
tabIndex={0}
onClick={() => setAdvancedOpen((prev) => !prev)}
onClick={() => setFormField("advancedOpen", !advancedOpen)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setAdvancedOpen((prev) => !prev);
if (e.key === "Enter" || e.key === " ") setFormField("advancedOpen", !advancedOpen);
}}
sx={{
display: "flex",
@@ -235,14 +271,14 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
label="种群规模"
size="small"
value={popSize}
onChange={(e) => setPopSize(Number(e.target.value))}
onChange={(e) => setFormField("popSize", Number(e.target.value))}
/>
<TextField
type="number"
label="最大代数"
size="small"
value={maxGen}
onChange={(e) => setMaxGen(Number(e.target.value))}
onChange={(e) => setFormField("maxGen", Number(e.target.value))}
/>
</Box>
</Box>
@@ -19,8 +19,14 @@ import {
} from "@mui/icons-material";
import { useMap } from "@components/olmap/core/MapComponent";
import StyleLegend from "@components/olmap/core/Controls/StyleLegend";
import AnalysisParameters from "./AnalysisParameters";
import SchemeQuery from "./SchemeQuery";
import AnalysisParameters, {
createDMALeakAnalysisParametersState,
type DMALeakAnalysisParametersState,
} from "./AnalysisParameters";
import SchemeQuery, {
createDMALeakSchemeQueryState,
type DMALeakSchemeQueryState,
} from "./SchemeQuery";
import RecognitionResults from "./RecognitionResults";
import { applyJunctionAreaRender } from "./applyJunctionAreaRender";
import { getAreaColor } from "./utils";
@@ -49,6 +55,13 @@ const DMALeakDetectionPanel: React.FC = () => {
const [result, setResult] = useState<LeakageResultDetail | null>(null);
const [loadedResult, setLoadedResult] = useState<LeakageResultDetail | null>(null);
const [schemes, setSchemes] = useState<LeakageSchemeRecord[]>([]);
const [analysisState, setAnalysisState] =
useState<DMALeakAnalysisParametersState>(
createDMALeakAnalysisParametersState,
);
const [queryState, setQueryState] = useState<DMALeakSchemeQueryState>(
createDMALeakSchemeQueryState,
);
const drawerWidth = 450;
const panelTitle = "DMA 漏损识别";
@@ -196,10 +209,20 @@ const DMALeakDetectionPanel: React.FC = () => {
</Tabs>
</Box>
<TabPanel value={tab} index={0}>
<AnalysisParameters onResult={handleAnalysisResult} />
<AnalysisParameters
onResult={handleAnalysisResult}
state={analysisState}
onStateChange={setAnalysisState}
/>
</TabPanel>
<TabPanel value={tab} index={1}>
<SchemeQuery onViewResult={handleViewResult} schemes={schemes} onSchemesChange={setSchemes} />
<SchemeQuery
onViewResult={handleViewResult}
schemes={schemes}
onSchemesChange={setSchemes}
state={queryState}
onStateChange={setQueryState}
/>
</TabPanel>
<TabPanel value={tab} index={2}>
<RecognitionResults result={result} />
@@ -23,6 +23,7 @@ import dayjs, { Dayjs } from "dayjs";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { LeakageResultDetail, LeakageSchemeRecord } from "./types";
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
@@ -30,15 +31,38 @@ interface Props {
onViewResult: (result: LeakageResultDetail) => void;
schemes?: LeakageSchemeRecord[];
onSchemesChange?: (schemes: LeakageSchemeRecord[]) => void;
state?: DMALeakSchemeQueryState;
onStateChange?: (state: DMALeakSchemeQueryState) => void;
}
const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes, onSchemesChange }) => {
export interface DMALeakSchemeQueryState {
queryAll: boolean;
queryDate: Dayjs | null;
expandedId: number | null;
}
export const createDMALeakSchemeQueryState = (): DMALeakSchemeQueryState => ({
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,
createDMALeakSchemeQueryState(),
);
const { queryAll, queryDate, expandedId } = queryState;
const [internalSchemes, setInternalSchemes] = useState<LeakageSchemeRecord[]>([]);
const [loading, setLoading] = useState(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
const setSchemes = onSchemesChange || setInternalSchemes;
@@ -91,7 +115,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
<Checkbox
size="small"
checked={queryAll}
onChange={(e) => setQueryAll(e.target.checked)}
onChange={(e) => setQueryField("queryAll", e.target.checked)}
/>
}
label={<Typography variant="body2"></Typography>}
@@ -100,7 +124,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: 200 } } }}
@@ -178,7 +202,12 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
<Tooltip title={expandedId === scheme.scheme_id ? "收起详情" : "查看详情"}>
<IconButton
size="small"
onClick={() => setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)}
onClick={() =>
setQueryField(
"expandedId",
expandedId === scheme.scheme_id ? null : scheme.scheme_id,
)
}
color="primary"
className="p-1"
>