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",
}));
}}
>
@@ -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"
@@ -25,14 +25,17 @@ 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 { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
import { BurstLocationResult } from "./types";
interface Props {
onResult: (result: BurstLocationResult) => void;
state?: BurstLocationAnalysisParametersState;
onStateChange?: (state: BurstLocationAnalysisParametersState) => void;
}
interface SchemeItem {
export interface SchemeItem {
scheme_id: number;
scheme_name: string;
scheme_type: string;
@@ -45,24 +48,60 @@ interface SchemeItem {
type DataSource = "monitoring" | "simulation";
const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
export interface BurstLocationAnalysisParametersState {
schemeName: string;
dataSource: DataSource;
schemes: SchemeItem[];
selectedSchemeId: number | "";
burstLeakage: number;
enableFlow: boolean;
burstStartTime: Dayjs | null;
burstEndTime: Dayjs | null;
minDpressure: number;
basicPressure: number;
advancedOpen: boolean;
}
export const createBurstLocationAnalysisParametersState =
(): BurstLocationAnalysisParametersState => ({
schemeName: `Burst_Locate_${Date.now()}`,
dataSource: "monitoring",
schemes: [],
selectedSchemeId: "",
burstLeakage: 1440,
enableFlow: false,
burstStartTime: dayjs().subtract(20, "minute"),
burstEndTime: dayjs().subtract(5, "minute"),
minDpressure: 2,
basicPressure: 10,
advancedOpen: false,
});
const AnalysisParameters: React.FC<Props> = ({
onResult,
state,
onStateChange,
}) => {
const { open } = useNotification();
const [schemeName, setSchemeName] = useState(`Burst_Locate_${Date.now()}`);
const [dataSource, setDataSource] = useState<DataSource>("monitoring");
const [schemes, setSchemes] = useState<SchemeItem[]>([]);
const [selectedSchemeId, setSelectedSchemeId] = useState<number | "">("");
const [parametersState, setParametersState, setFormField] = useControllableObjectState(
state,
onStateChange,
createBurstLocationAnalysisParametersState(),
);
const {
schemeName,
dataSource,
schemes,
selectedSchemeId,
burstLeakage,
enableFlow,
burstStartTime,
burstEndTime,
minDpressure,
basicPressure,
advancedOpen,
} = parametersState;
const [schemeLoading, setSchemeLoading] = useState(false);
const [burstLeakage, setBurstLeakage] = useState<number>(1440);
const [enableFlow, setEnableFlow] = useState(false);
const [burstStartTime, setBurstStartTime] = useState<Dayjs | null>(
dayjs().subtract(20, "minute"),
);
const [burstEndTime, setBurstEndTime] = useState<Dayjs | null>(
dayjs().subtract(5, "minute"),
);
const [minDpressure, setMinDpressure] = useState<number>(2);
const [basicPressure, setBasicPressure] = useState<number>(10);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [running, setRunning] = useState(false);
const isSimulationMode = dataSource === "simulation";
@@ -71,9 +110,12 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
const end = start.add(durationSeconds, "second");
setBurstStartTime(start);
setBurstEndTime(end);
}, []);
setParametersState((previous) => ({
...previous,
burstStartTime: start,
burstEndTime: end,
}));
}, [setParametersState]);
const fetchSchemes = useCallback(
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
@@ -88,7 +130,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
(scheme) => scheme.scheme_type === "burst_analysis",
);
setSchemes(burstSchemes);
setFormField("schemes", burstSchemes);
if (selectedSchemeId) {
const matchedScheme = burstSchemes.find(
@@ -97,7 +139,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
if (matchedScheme) {
applySchemeTimeRange(matchedScheme);
} else {
setSelectedSchemeId("");
setFormField("selectedSchemeId", "");
}
}
@@ -119,18 +161,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: DataSource) => {
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);
@@ -220,7 +262,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"
@@ -321,7 +363,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<DateTimePicker
value={burstStartTime}
onChange={setBurstStartTime}
onChange={(value) => setFormField("burstStartTime", value)}
maxDateTime={burstEndTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm"
@@ -334,7 +376,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</Typography>
<DateTimePicker
value={burstEndTime}
onChange={setBurstEndTime}
onChange={(value) => setFormField("burstEndTime", value)}
minDateTime={burstStartTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm"
@@ -354,7 +396,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
value={burstLeakage}
onChange={(e) => {
const value = Number(e.target.value);
setBurstLeakage(Number.isNaN(value) ? 1440 : Math.max(0, value));
setFormField("burstLeakage", Number.isNaN(value) ? 1440 : Math.max(0, value));
}}
fullWidth
inputProps={{ min: 0, step: 10 }}
@@ -370,9 +412,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",
@@ -412,7 +454,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
<FormControl fullWidth size="small">
<Select
value={enableFlow ? "enabled" : "disabled"}
onChange={(e) => setEnableFlow(e.target.value === "enabled")}
onChange={(e) => setFormField("enableFlow", e.target.value === "enabled")}
>
<MenuItem value="disabled"></MenuItem>
<MenuItem value="enabled">使</MenuItem>
@@ -425,14 +467,14 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
label="最小压降 (m)"
size="small"
value={minDpressure}
onChange={(e) => setMinDpressure(Number(e.target.value))}
onChange={(e) => setFormField("minDpressure", Number(e.target.value))}
/>
<TextField
type="number"
label="基础压力 (m)"
size="small"
value={basicPressure}
onChange={(e) => setBasicPressure(Number(e.target.value))}
onChange={(e) => setFormField("basicPressure", Number(e.target.value))}
/>
</Box>
</Box>
@@ -9,9 +9,15 @@ import {
FormatListBulleted,
Search as SearchIcon,
} from "@mui/icons-material";
import AnalysisParameters from "./AnalysisParameters";
import AnalysisParameters, {
createBurstLocationAnalysisParametersState,
type BurstLocationAnalysisParametersState,
} from "./AnalysisParameters";
import LocationResults from "./LocationResults";
import SchemeQuery from "./SchemeQuery";
import SchemeQuery, {
createBurstLocationSchemeQueryState,
type BurstLocationSchemeQueryState,
} from "./SchemeQuery";
import { BurstLocationResult, BurstSchemeRecord } from "./types";
const TabPanel = ({
@@ -33,6 +39,14 @@ const BurstLocationPanel: React.FC = () => {
const [tab, setTab] = useState(0);
const [result, setResult] = useState<BurstLocationResult | null>(null);
const [schemes, setSchemes] = useState<BurstSchemeRecord[]>([]);
const [analysisState, setAnalysisState] =
useState<BurstLocationAnalysisParametersState>(
createBurstLocationAnalysisParametersState,
);
const [queryState, setQueryState] =
useState<BurstLocationSchemeQueryState>(
createBurstLocationSchemeQueryState,
);
const drawerWidth = 450;
const panelTitle = "爆管定位";
@@ -146,10 +160,20 @@ const BurstLocationPanel: 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={handleViewResult} schemes={schemes} onSchemesChange={setSchemes} />
<SchemeQuery
onViewResult={handleViewResult}
schemes={schemes}
onSchemesChange={setSchemes}
state={queryState}
onStateChange={setQueryState}
/>
</TabPanel>
<TabPanel value={tab} index={2}>
<LocationResults 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 {
BurstLocationResult,
BurstLocationSchemeDetail,
@@ -34,15 +35,39 @@ interface Props {
onViewResult: (result: BurstLocationResult) => void;
schemes?: BurstSchemeRecord[];
onSchemesChange?: (schemes: BurstSchemeRecord[]) => void;
state?: BurstLocationSchemeQueryState;
onStateChange?: (state: BurstLocationSchemeQueryState) => void;
}
const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes, onSchemesChange }) => {
export interface BurstLocationSchemeQueryState {
queryAll: boolean;
queryDate: Dayjs | null;
expandedId: number | null;
}
export const createBurstLocationSchemeQueryState =
(): BurstLocationSchemeQueryState => ({
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,
createBurstLocationSchemeQueryState(),
);
const { queryAll, queryDate, expandedId } = queryState;
const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]);
const [loading, setLoading] = useState(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
const setSchemes = onSchemesChange || setInternalSchemes;
@@ -157,7 +182,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>}
@@ -166,7 +191,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 } } }}
@@ -281,7 +306,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"
@@ -20,33 +20,60 @@ import { useMap } from "@components/olmap/core/MapComponent";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { Style, Stroke, Icon } from "ol/style";
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import {
handleMapClickSelectFeatures as mapClickSelectFeatures,
queryFeaturesByIds,
} from "@/utils/mapQueryService";
import Feature, { FeatureLike } from "ol/Feature";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { config, NETWORK_NAME } from "@/config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { along, lineString, length, toMercator } from "@turf/turf";
import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
interface PipePoint {
export interface PipePoint {
id: string;
diameter: number;
area: number;
feature?: any; // 存储管道要素用于高亮
}
const AnalysisParameters: React.FC = () => {
export interface BurstAnalysisParametersState {
pipePoints: PipePoint[];
startTime: Dayjs | null;
duration: number;
schemeName: string;
network: string;
}
export const createBurstAnalysisParametersState = (): BurstAnalysisParametersState => ({
pipePoints: [],
startTime: dayjs(new Date()),
duration: 3600,
schemeName: "FANGAN" + new Date().getTime(),
network: NETWORK_NAME,
});
interface AnalysisParametersProps {
state?: BurstAnalysisParametersState;
onStateChange?: (state: BurstAnalysisParametersState) => void;
}
const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
state,
onStateChange,
}) => {
const map = useMap();
const { open } = useNotification();
const [pipePoints, setPipePoints] = useState<PipePoint[]>([]);
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
const [duration, setDuration] = useState<number>(3600);
const [schemeName, setSchemeName] = useState<string>(
"FANGAN" + new Date().getTime(),
const [parametersState, setParametersState, setParameterField] = useControllableObjectState(
state,
onStateChange,
createBurstAnalysisParametersState(),
);
const [network, setNetwork] = useState<string>(NETWORK_NAME);
const { pipePoints, startTime, duration, schemeName, network } =
parametersState;
const [isSelecting, setIsSelecting] = useState<boolean>(false);
const [highlightLayer, setHighlightLayer] =
@@ -54,6 +81,17 @@ const AnalysisParameters: React.FC = () => {
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [analyzing, setAnalyzing] = useState<boolean>(false);
const setPipePoints = useCallback(
(next: PipePoint[] | ((previous: PipePoint[]) => PipePoint[])) => {
setParametersState((previous) => ({
...previous,
pipePoints:
typeof next === "function" ? next(previous.pipePoints) : next,
}));
},
[setParametersState],
);
// 检查是否所有必要参数都已填写
const isFormValid =
pipePoints.length > 0 &&
@@ -190,6 +228,18 @@ const AnalysisParameters: React.FC = () => {
});
}, [highlightFeatures, highlightLayer]);
useEffect(() => {
if (highlightFeatures.length > 0 || pipePoints.length === 0) return;
queryFeaturesByIds(
pipePoints.map((pipe) => pipe.id),
"geo_pipes_mat",
).then((features) => {
if (features.length > 0) {
setHighlightFeatures(features);
}
});
}, [highlightFeatures.length, pipePoints]);
// 同步高亮要素和爆管点信息
useEffect(() => {
setPipePoints((prevPipes) => {
@@ -211,12 +261,11 @@ const AnalysisParameters: React.FC = () => {
id: properties.id,
diameter: properties.diameter || 0,
area: 15,
feature: feature,
};
});
return [...filtered, ...newPipes];
});
}, [highlightFeatures]);
}, [highlightFeatures, setPipePoints]);
// 开始选择管道
const handleStartSelection = () => {
@@ -237,6 +286,7 @@ const AnalysisParameters: React.FC = () => {
};
const handleRemovePipe = (id: string) => {
setPipePoints((prev) => prev.filter((pipe) => pipe.id !== id));
// 从高亮features中移除
setHighlightFeatures((prev) =>
prev.filter((f) => f.getProperties().id !== id),
@@ -425,7 +475,7 @@ const AnalysisParameters: React.FC = () => {
<DateTimePicker
value={startTime}
onChange={(value) =>
value && dayjs.isDayjs(value) && setStartTime(value)
value && dayjs.isDayjs(value) && setParameterField("startTime", value)
}
format="YYYY-MM-DD HH:mm"
slotProps={{
@@ -452,7 +502,7 @@ const AnalysisParameters: React.FC = () => {
size="small"
type="number"
value={duration}
onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
onChange={(e) => setParameterField("duration", parseInt(e.target.value) || 0)}
placeholder="输入持续时长"
/>
</Box>
@@ -466,7 +516,7 @@ const AnalysisParameters: React.FC = () => {
fullWidth
size="small"
value={schemeName}
onChange={(e) => setSchemeName(e.target.value)}
onChange={(e) => setParameterField("schemeName", e.target.value)}
placeholder="输入方案名称"
/>
</Box>
@@ -18,10 +18,19 @@ import {
MyLocation as MyLocationIcon,
Handyman as HandymanIcon,
} from "@mui/icons-material";
import AnalysisParameters from "./AnalysisParameters";
import SchemeQuery from "./SchemeQuery";
import AnalysisParameters, {
createBurstAnalysisParametersState,
type BurstAnalysisParametersState,
} from "./AnalysisParameters";
import SchemeQuery, {
createBurstSchemeQueryState,
type BurstSchemeQueryState,
} from "./SchemeQuery";
import LocationResults from "./LocationResults";
import ValveIsolation from "./ValveIsolation";
import ValveIsolation, {
createValveIsolationState,
type ValveIsolationState,
} from "./ValveIsolation";
import { api } from "@/lib/api";
import { config } from "@config/config";
import { useNotification } from "@refinedev/core";
@@ -58,6 +67,11 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
}) => {
const [internalOpen, setInternalOpen] = useState(true);
const [currentTab, setCurrentTab] = useState(0);
const [analysisState, setAnalysisState] =
useState<BurstAnalysisParametersState>(createBurstAnalysisParametersState);
const [queryState, setQueryState] = useState<BurstSchemeQueryState>(
createBurstSchemeQueryState,
);
// 持久化方案查询结果
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
@@ -66,6 +80,8 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
// 关阀分析结果和加载状态
const [valveAnalysisLoading, setValveAnalysisLoading] = useState(false);
const [valveAnalysisResult, setValveAnalysisResult] = useState<ValveIsolationResult | null>(null);
const [valveIsolationState, setValveIsolationState] =
useState<ValveIsolationState>(createValveIsolationState);
const { open } = useNotification();
@@ -224,7 +240,10 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
{/* Tab 内容 */}
<TabPanel value={currentTab} index={0}>
<AnalysisParameters />
<AnalysisParameters
state={analysisState}
onStateChange={setAnalysisState}
/>
</TabPanel>
<TabPanel value={currentTab} index={1}>
@@ -232,6 +251,8 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
schemes={schemes}
onSchemesChange={setSchemes}
onLocate={handleLocateScheme}
state={queryState}
onStateChange={setQueryState}
/>
</TabPanel>
@@ -247,6 +268,8 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
result={valveAnalysisResult}
onLoadingChange={setValveAnalysisLoading}
onResultChange={setValveAnalysisResult}
state={valveIsolationState}
onStateChange={setValveIsolationState}
/>
</TabPanel>
</Box>
@@ -30,6 +30,7 @@ import { api } from "@/lib/api";
import moment from "moment";
import { config, NETWORK_NAME } from "@config/config";
import { useNotification } from "@refinedev/core";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { useData, useMap } from "@components/olmap/core/MapComponent";
@@ -56,31 +57,57 @@ interface SchemeQueryProps {
onSchemesChange?: (schemes: SchemeRecord[]) => void;
onLocate?: (scheme: SchemeRecord) => void;
network?: string;
state?: BurstSchemeQueryState;
onStateChange?: (state: BurstSchemeQueryState) => void;
}
const SCHEME_TYPE = "burst_analysis";
export interface BurstSchemeQueryState {
queryAll: boolean;
queryDate: Dayjs | null;
showTimeline: boolean;
selectedDate: Date | undefined;
timeRange: { start: Date; end: Date } | undefined;
expandedId: number | null;
}
export const createBurstSchemeQueryState = (): BurstSchemeQueryState => ({
queryAll: true,
queryDate: dayjs(new Date()),
showTimeline: false,
selectedDate: undefined,
timeRange: undefined,
expandedId: null,
});
const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemes: externalSchemes,
onSchemesChange,
onLocate,
network = NETWORK_NAME,
state,
onStateChange,
}) => {
const [queryAll, setQueryAll] = useState<boolean>(true);
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
const [queryState, setQueryState, setQueryField] = useControllableObjectState(
state,
onStateChange,
createBurstSchemeQueryState(),
);
const {
queryAll,
queryDate,
showTimeline,
selectedDate,
timeRange,
expandedId,
} = queryState;
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
// 时间轴相关状态
const [showTimeline, setShowTimeline] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [timeRange, setTimeRange] = useState<
{ start: Date; end: Date } | undefined
>();
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素
const { open } = useNotification();
@@ -181,7 +208,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const scheme = filteredSchemes.find((s) => s.id === id);
if (!scheme) return;
setShowTimeline(true);
setQueryState((previous) => ({ ...previous, showTimeline: true }));
// 计算时间范围
const schemeDate = scheme.startTime
? new Date(scheme.startTime)
@@ -191,8 +218,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const end = new Date(
start.getTime() + scheme.schemeDetail.modify_total_duration * 1000,
);
setSelectedDate(schemeDate);
setTimeRange({ start, end });
setQueryState((previous) => ({
...previous,
showTimeline: true,
selectedDate: schemeDate,
timeRange: { start, end },
}));
}
setSchemeName?.(scheme.schemeName);
handleLocatePipes(scheme.schemeDetail?.burst_ID || []);
@@ -324,7 +355,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
control={
<Checkbox
checked={queryAll}
onChange={(e) => setQueryAll(e.target.checked)}
onChange={(e) => setQueryField("queryAll", e.target.checked)}
size="small"
/>
}
@@ -338,7 +369,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
<DatePicker
value={queryDate}
onChange={(value) =>
value && dayjs.isDayjs(value) && setQueryDate(value)
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
}
format="YYYY-MM-DD"
disabled={queryAll}
@@ -450,13 +481,14 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
expandedId === scheme.id ? "收起详情" : "查看详情"
}
>
<IconButton
size="small"
onClick={() =>
setExpandedId(
expandedId === scheme.id ? null : scheme.id,
)
}
<IconButton
size="small"
onClick={() =>
setQueryField(
"expandedId",
expandedId === scheme.id ? null : scheme.id,
)
}
color="primary"
className="p-1"
>
@@ -51,6 +51,7 @@ import {
} from "@turf/turf";
import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
interface ValveIsolationProps {
initialPipeIds?: string[];
@@ -60,8 +61,24 @@ interface ValveIsolationProps {
result?: ValveIsolationResult | null;
onLoadingChange?: (loading: boolean) => void;
onResultChange?: (result: ValveIsolationResult | null) => void;
state?: ValveIsolationState;
onStateChange?: (state: ValveIsolationState) => void;
}
export interface ValveIsolationState {
selectedPipeId: string | null;
activeStep: number;
expandedResult: boolean;
disabledValves: string[];
}
export const createValveIsolationState = (): ValveIsolationState => ({
selectedPipeId: null,
activeStep: 0,
expandedResult: true,
disabledValves: [],
});
const ValveIsolation: React.FC<ValveIsolationProps> = ({
initialPipeIds = [],
shouldFetch = false,
@@ -70,6 +87,8 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
result: externalResult,
onLoadingChange,
onResultChange,
state,
onStateChange,
}) => {
const [internalLoading, setInternalLoading] = useState(false);
const [internalResult, setInternalResult] =
@@ -82,12 +101,41 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
const setLoading = onLoadingChange || setInternalLoading;
const setResult = onResultChange || setInternalResult;
const [selectedPipeId, setSelectedPipeId] = useState<string | null>(null);
const [flowState, setFlowState, setFlowField] = useControllableObjectState(
state,
onStateChange,
createValveIsolationState(),
);
const { selectedPipeId, activeStep, expandedResult, disabledValves } =
flowState;
const [highlightFeature, setHighlightFeature] = useState<Feature | null>(null);
const [isSelecting, setIsSelecting] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [expandedResult, setExpandedResult] = useState(true);
const [disabledValves, setDisabledValves] = useState<string[]>([]);
const setSelectedPipeId = useCallback(
(value: string | null) => setFlowField("selectedPipeId", value),
[setFlowField],
);
const setActiveStep = useCallback(
(value: number) => setFlowField("activeStep", value),
[setFlowField],
);
const setExpandedResult = useCallback(
(value: boolean) => setFlowField("expandedResult", value),
[setFlowField],
);
const setDisabledValves = useCallback(
(next: string[] | ((previous: string[]) => string[])) => {
setFlowState((previous) => ({
...previous,
disabledValves:
typeof next === "function" ? next(previous.disabledValves) : next,
}));
},
[setFlowState],
);
const { open } = useNotification();
const map = useMap();
@@ -119,7 +167,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
}
}
},
[isSelecting, map, open, setResult],
[isSelecting, map, open, setResult, setSelectedPipeId],
);
useEffect(() => {
@@ -135,6 +183,13 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
};
}, [map, isSelecting, handleMapClick]);
useEffect(() => {
if (!selectedPipeId || highlightFeature) return;
queryFeaturesByIds([selectedPipeId], "geo_pipes_mat").then((features) => {
setHighlightFeature(features[0] ?? null);
});
}, [highlightFeature, selectedPipeId]);
const clearSelectedPipe = () => {
setSelectedPipeId(null);
setHighlightFeature(null);
@@ -297,7 +352,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
setLoading(false);
}
},
[open, setLoading, setResult],
[open, setActiveStep, setDisabledValves, setLoading, setResult],
);
// 监听外部传入的分析请求
@@ -319,7 +374,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
onFetchComplete();
}
}
}, [shouldFetch, initialPipeIds, fetchAnalysis, onFetchComplete]);
}, [shouldFetch, initialPipeIds, fetchAnalysis, onFetchComplete, setSelectedPipeId]);
// 初始化高亮图层
useEffect(() => {
@@ -24,21 +24,57 @@ import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style";
import Feature from "ol/Feature";
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import {
handleMapClickSelectFeatures as mapClickSelectFeatures,
queryFeaturesByIds,
} from "@/utils/mapQueryService";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
const AnalysisParameters: React.FC = () => {
export interface ContaminantAnalysisParametersState {
schemeName: string;
startTime: Dayjs | null;
sourceNode: string;
concentration: number;
duration: number;
pattern: string;
}
export const createContaminantAnalysisParametersState =
(): ContaminantAnalysisParametersState => ({
schemeName: "WQ_" + new Date().getTime(),
startTime: dayjs(new Date()),
sourceNode: "",
concentration: 100,
duration: 3600,
pattern: "",
});
interface AnalysisParametersProps {
state?: ContaminantAnalysisParametersState;
onStateChange?: (state: ContaminantAnalysisParametersState) => void;
}
const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
state,
onStateChange,
}) => {
const map = useMap();
const { open } = useNotification();
const network = NETWORK_NAME;
const [schemeName, setSchemeName] = useState<string>(
"WQ_" + new Date().getTime(),
const [parametersState, , setFormField] = useControllableObjectState(
state,
onStateChange,
createContaminantAnalysisParametersState(),
);
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
const [sourceNode, setSourceNode] = useState<string>("");
const [concentration, setConcentration] = useState<number>(100);
const [duration, setDuration] = useState<number>(3600);
const [pattern, setPattern] = useState<string>("");
const {
schemeName,
startTime,
sourceNode,
concentration,
duration,
pattern,
} = parametersState;
const [isSelecting, setIsSelecting] = useState<boolean>(false);
const [submitting, setSubmitting] = useState<boolean>(false);
@@ -77,12 +113,12 @@ const AnalysisParameters: React.FC = () => {
const id = feature.getProperties().id;
if (!id) return;
setSourceNode(id);
setFormField("sourceNode", id);
setHighlightFeature(feature);
setIsSelecting(false);
map.un("click", handleMapClickSelectFeatures);
},
[map, open],
[map, open, setFormField],
);
useEffect(() => {
@@ -144,6 +180,17 @@ const AnalysisParameters: React.FC = () => {
}
}, [highlightFeature, highlightLayer]);
useEffect(() => {
if (!sourceNode) {
setHighlightFeature(null);
return;
}
if (highlightFeature) return;
queryFeaturesByIds([sourceNode], "geo_junctions_mat").then((features) => {
setHighlightFeature(features[0] ?? null);
});
}, [highlightFeature, sourceNode]);
const handleStartSelection = () => {
if (!map) return;
setIsSelecting(true);
@@ -157,7 +204,7 @@ const AnalysisParameters: React.FC = () => {
};
const handleClearSource = () => {
setSourceNode("");
setFormField("sourceNode", "");
setHighlightFeature(null);
};
@@ -176,7 +223,7 @@ const AnalysisParameters: React.FC = () => {
: "";
try {
if (!pattern) {
setPattern("CONSTANT");
setFormField("pattern", "CONSTANT");
console.log("默认设置 pattern 为 CONSTANT");
}
const params = {
@@ -310,7 +357,7 @@ const AnalysisParameters: React.FC = () => {
<DateTimePicker
value={startTime}
onChange={(value) =>
value && dayjs.isDayjs(value) && setStartTime(value)
value && dayjs.isDayjs(value) && setFormField("startTime", value)
}
format="YYYY-MM-DD HH:mm"
slotProps={{
@@ -335,7 +382,7 @@ const AnalysisParameters: React.FC = () => {
fullWidth
size="small"
value={schemeName}
onChange={(e) => setSchemeName(e.target.value)}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="输入方案名称"
/>
</Box>
@@ -349,7 +396,9 @@ const AnalysisParameters: React.FC = () => {
size="small"
type="number"
value={concentration}
onChange={(e) => setConcentration(parseFloat(e.target.value) || 0)}
onChange={(e) =>
setFormField("concentration", parseFloat(e.target.value) || 0)
}
placeholder="输入浓度"
/>
</Box>
@@ -363,7 +412,9 @@ const AnalysisParameters: React.FC = () => {
size="small"
type="number"
value={duration}
onChange={(e) => setDuration(parseInt(e.target.value, 10) || 0)}
onChange={(e) =>
setFormField("duration", parseInt(e.target.value, 10) || 0)
}
placeholder="输入持续时长"
/>
</Box>
@@ -376,7 +427,7 @@ const AnalysisParameters: React.FC = () => {
fullWidth
size="small"
value={pattern}
onChange={(e) => setPattern(e.target.value)}
onChange={(e) => setFormField("pattern", e.target.value)}
placeholder="可选,输入 pattern 名称,默认为 CONSTANT"
/>
</Box>
@@ -30,6 +30,7 @@ import moment from "moment";
import { useNotification } from "@refinedev/core";
import { config, NETWORK_NAME } from "@config/config";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useData, useMap } from "@components/olmap/core/MapComponent";
import { GeoJSON } from "ol/format";
import VectorLayer from "ol/layer/Vector";
@@ -45,32 +46,60 @@ interface SchemeQueryProps {
onSchemesChange?: (schemes: ContaminantSchemeRecord[]) => void;
onViewResults?: () => void;
network?: string;
state?: ContaminantSchemeQueryState;
onStateChange?: (state: ContaminantSchemeQueryState) => void;
}
const SCHEME_TYPE = "contaminant_analysis";
export interface ContaminantSchemeQueryState {
queryAll: boolean;
queryDate: Dayjs | null;
showTimeline: boolean;
selectedDate: Date | undefined;
timeRange: { start: Date; end: Date } | undefined;
expandedId: number | null;
}
export const createContaminantSchemeQueryState =
(): ContaminantSchemeQueryState => ({
queryAll: true,
queryDate: dayjs(new Date()),
showTimeline: false,
selectedDate: undefined,
timeRange: undefined,
expandedId: null,
});
const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemes: externalSchemes,
onSchemesChange,
onViewResults,
network = NETWORK_NAME,
state,
onStateChange,
}) => {
const [queryAll, setQueryAll] = useState<boolean>(true);
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
const [queryState, setQueryState, setQueryField] = useControllableObjectState(
state,
onStateChange,
createContaminantSchemeQueryState(),
);
const {
queryAll,
queryDate,
showTimeline,
selectedDate,
timeRange,
expandedId,
} = queryState;
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [showTimeline, setShowTimeline] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [timeRange, setTimeRange] = useState<
{ start: Date; end: Date } | undefined
>();
const [internalSchemes, setInternalSchemes] = useState<
ContaminantSchemeRecord[]
>([]);
const [loading, setLoading] = useState<boolean>(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
const { open } = useNotification();
@@ -256,7 +285,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const scheme = filteredSchemes.find((s) => s.id === id);
if (!scheme) return;
setShowTimeline(true);
setQueryState((previous) => ({ ...previous, showTimeline: true }));
const schemeDate = scheme.startTime
? new Date(scheme.startTime)
: undefined;
@@ -265,8 +294,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const end = new Date(
start.getTime() + scheme.schemeDetail.duration * 1000,
);
setSelectedDate(schemeDate);
setTimeRange({ start, end });
setQueryState((previous) => ({
...previous,
showTimeline: true,
selectedDate: schemeDate,
timeRange: { start, end },
}));
}
setSchemeName?.(scheme.schemeName);
if (scheme.schemeDetail?.source) {
@@ -296,7 +329,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
control={
<Checkbox
checked={queryAll}
onChange={(e) => setQueryAll(e.target.checked)}
onChange={(e) => setQueryField("queryAll", e.target.checked)}
size="small"
/>
}
@@ -310,7 +343,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
<DatePicker
value={queryDate}
onChange={(value) =>
value && dayjs.isDayjs(value) && setQueryDate(value)
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
}
format="YYYY-MM-DD"
disabled={queryAll}
@@ -422,7 +455,8 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
<IconButton
size="small"
onClick={() =>
setExpandedId(
setQueryField(
"expandedId",
expandedId === scheme.id ? null : scheme.id,
)
}
@@ -17,8 +17,14 @@ import {
Search as SearchIcon,
MyLocation as MyLocationIcon,
} from "@mui/icons-material";
import ContaminantAnalysisParameters from "./AnalysisParameters";
import ContaminantSchemeQuery from "./SchemeQuery";
import ContaminantAnalysisParameters, {
createContaminantAnalysisParametersState,
type ContaminantAnalysisParametersState,
} from "./AnalysisParameters";
import ContaminantSchemeQuery, {
createContaminantSchemeQueryState,
type ContaminantSchemeQueryState,
} from "./SchemeQuery";
import { useData } from "@components/olmap/core/MapComponent";
import { ContaminantSchemeRecord } from "./types";
@@ -34,6 +40,14 @@ const WaterQualityPanel: React.FC<WaterQualityPanelProps> = ({
const [internalOpen, setInternalOpen] = useState(true);
const [currentTab, setCurrentTab] = useState(0);
const [schemes, setSchemes] = useState<ContaminantSchemeRecord[]>([]);
const [analysisState, setAnalysisState] =
useState<ContaminantAnalysisParametersState>(
createContaminantAnalysisParametersState,
);
const [queryState, setQueryState] =
useState<ContaminantSchemeQueryState>(
createContaminantSchemeQueryState,
);
const data = useData();
@@ -170,7 +184,10 @@ const WaterQualityPanel: React.FC<WaterQualityPanelProps> = ({
{/* Tab 内容 */}
<TabPanel value={currentTab} index={0}>
<ContaminantAnalysisParameters />
<ContaminantAnalysisParameters
state={analysisState}
onStateChange={setAnalysisState}
/>
</TabPanel>
<TabPanel value={currentTab} index={1}>
@@ -178,6 +195,8 @@ const WaterQualityPanel: React.FC<WaterQualityPanelProps> = ({
schemes={schemes}
onSchemesChange={setSchemes}
onViewResults={() => setCurrentTab(2)}
state={queryState}
onStateChange={setQueryState}
/>
</TabPanel>
</Box>
@@ -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"
>
@@ -22,39 +22,82 @@ import { useMap } from "@components/olmap/core/MapComponent";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style";
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import {
handleMapClickSelectFeatures as mapClickSelectFeatures,
queryFeaturesByIds,
} from "@/utils/mapQueryService";
import Feature, { FeatureLike } from "ol/Feature";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { config, NETWORK_NAME } from "@/config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
interface ValveItem {
export interface ValveItem {
id: string;
k: number;
feature?: any;
}
const AnalysisParameters: React.FC = () => {
export interface FlushingAnalysisParametersState {
schemeName: string;
valves: ValveItem[];
drainageNode: string | null;
startTime: Dayjs | null;
flushFlow: number;
duration: number;
}
export const createFlushingAnalysisParametersState =
(): FlushingAnalysisParametersState => ({
schemeName: "Flushing_" + new Date().getTime(),
valves: [],
drainageNode: null,
startTime: dayjs(new Date()),
flushFlow: 200,
duration: 3600,
});
interface AnalysisParametersProps {
state?: FlushingAnalysisParametersState;
onStateChange?: (state: FlushingAnalysisParametersState) => void;
}
const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
state,
onStateChange,
}) => {
const map = useMap();
const { open } = useNotification();
// State
const [schemeName, setSchemeName] = useState<string>(
"Flushing_" + new Date().getTime(),
const [parametersState, setParametersState, setFormField] = useControllableObjectState(
state,
onStateChange,
createFlushingAnalysisParametersState(),
);
const [valves, setValves] = useState<ValveItem[]>([]);
const [drainageNode, setDrainageNode] = useState<string | null>(null);
const { schemeName, valves, drainageNode, startTime, flushFlow, duration } =
parametersState;
const [valveFeatures, setValveFeatures] = useState<Feature[]>([]);
const [drainageFeature, setDrainageFeature] = useState<Feature | null>(null);
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
const [flushFlow, setFlushFlow] = useState<number>(200);
const [duration, setDuration] = useState<number>(3600);
const [selectionMode, setSelectionMode] = useState<'none' | 'valve' | 'drainage'>('none');
const [analyzing, setAnalyzing] = useState<boolean>(false);
const [highlightLayer, setHighlightLayer] = useState<VectorLayer<VectorSource> | null>(null);
const setValves = useCallback(
(next: ValveItem[] | ((previous: ValveItem[]) => ValveItem[])) => {
setParametersState((previous) => ({
...previous,
valves: typeof next === "function" ? next(previous.valves) : next,
}));
},
[setParametersState],
);
const setDrainageNode = useCallback(
(value: string | null) => setFormField("drainageNode", value),
[setFormField],
);
// Map click handler
const handleMapClickSelectFeatures = useCallback(
async (event: { coordinate: number[] }) => {
@@ -83,7 +126,8 @@ const AnalysisParameters: React.FC = () => {
});
return prev;
}
return [...prev, { id: featureId, k: 1.0, feature }];
setValveFeatures((features) => [...features, feature]);
return [...prev, { id: featureId, k: 1.0 }];
});
} else if (selectionMode === 'drainage') {
@@ -100,7 +144,7 @@ const AnalysisParameters: React.FC = () => {
map.un("click", handleMapClickSelectFeatures);
}
},
[map, selectionMode, open]
[map, selectionMode, open, setDrainageNode, setValves]
);
// Initialize highlight layer
@@ -162,9 +206,9 @@ const AnalysisParameters: React.FC = () => {
source.clear();
// Add valves
valves.forEach((v) => {
if (v.feature) {
const f = v.feature.clone(); // Clone to avoid modifying original
valveFeatures.forEach((feature) => {
if (feature) {
const f = feature.clone(); // Clone to avoid modifying original
f.set("type", "valve");
// Ensure geometry is present (it should be for features from map)
if (f.getGeometry()) {
@@ -180,7 +224,32 @@ const AnalysisParameters: React.FC = () => {
source.addFeature(f);
}
}, [highlightLayer, valves, drainageFeature]);
}, [highlightLayer, valveFeatures, drainageFeature]);
useEffect(() => {
if (valves.length === 0) {
setValveFeatures([]);
return;
}
if (valveFeatures.length > 0) return;
queryFeaturesByIds(
valves.map((valve) => valve.id),
"geo_valves",
).then((features) => {
setValveFeatures(features);
});
}, [valveFeatures.length, valves]);
useEffect(() => {
if (!drainageNode) {
setDrainageFeature(null);
return;
}
if (drainageFeature) return;
queryFeaturesByIds([drainageNode], "geo_junctions").then((features) => {
setDrainageFeature(features[0] ?? null);
});
}, [drainageFeature, drainageNode]);
// Bind click event based on selection mode
useEffect(() => {
@@ -205,6 +274,9 @@ const AnalysisParameters: React.FC = () => {
const handleRemoveValve = (id: string) => {
setValves((prev) => prev.filter((v) => v.id !== id));
setValveFeatures((prev) =>
prev.filter((feature) => feature.getProperties().id !== id),
);
};
const handleValveKChange = (id: string, k: string) => {
@@ -373,7 +445,7 @@ const AnalysisParameters: React.FC = () => {
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
<DateTimePicker
value={startTime}
onChange={(newValue) => setStartTime(newValue)}
onChange={(newValue) => setFormField("startTime", newValue)}
format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }}
localeText={
@@ -393,7 +465,7 @@ const AnalysisParameters: React.FC = () => {
fullWidth
size="small"
value={schemeName}
onChange={(e) => setSchemeName(e.target.value)}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="请输入方案名称"
/>
</Box>
@@ -408,7 +480,9 @@ const AnalysisParameters: React.FC = () => {
size="small"
type="number"
value={flushFlow}
onChange={(e) => setFlushFlow(parseFloat(e.target.value) || 0)}
onChange={(e) =>
setFormField("flushFlow", parseFloat(e.target.value) || 0)
}
/>
</Box>
<Box className="flex-1">
@@ -420,7 +494,9 @@ const AnalysisParameters: React.FC = () => {
size="small"
type="number"
value={duration}
onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
onChange={(e) =>
setFormField("duration", parseInt(e.target.value) || 0)
}
/>
</Box>
</Box>
@@ -17,8 +17,14 @@ import {
Search as SearchIcon,
} from "@mui/icons-material";
import { MdCleaningServices } from "react-icons/md";
import AnalysisParameters from "./AnalysisParameters";
import SchemeQuery from "./SchemeQuery";
import AnalysisParameters, {
createFlushingAnalysisParametersState,
type FlushingAnalysisParametersState,
} from "./AnalysisParameters";
import SchemeQuery, {
createFlushingSchemeQueryState,
type FlushingSchemeQueryState,
} from "./SchemeQuery";
import { SchemeRecord } from "./types";
interface TabPanelProps {
@@ -53,6 +59,13 @@ const FlushingAnalysisPanel: React.FC<FlushingAnalysisPanelProps> = ({
const [internalOpen, setInternalOpen] = useState(true);
const [currentTab, setCurrentTab] = useState(0);
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
const [analysisState, setAnalysisState] =
useState<FlushingAnalysisParametersState>(
createFlushingAnalysisParametersState,
);
const [queryState, setQueryState] = useState<FlushingSchemeQueryState>(
createFlushingSchemeQueryState,
);
// Using controlled or uncontrolled state
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
@@ -181,11 +194,19 @@ const FlushingAnalysisPanel: React.FC<FlushingAnalysisPanelProps> = ({
{/* Tab Content */}
<TabPanel value={currentTab} index={0}>
<AnalysisParameters />
<AnalysisParameters
state={analysisState}
onStateChange={setAnalysisState}
/>
</TabPanel>
<TabPanel value={currentTab} index={1}>
<SchemeQuery schemes={schemes} onSchemesChange={setSchemes} />
<SchemeQuery
schemes={schemes}
onSchemesChange={setSchemes}
state={queryState}
onStateChange={setQueryState}
/>
</TabPanel>
</Box>
</Drawer>
@@ -32,6 +32,7 @@ import { config, NETWORK_NAME } from "@config/config";
import { useNotification } from "@refinedev/core";
import { useData, useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { GeoJSON } from "ol/format";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
@@ -46,29 +47,58 @@ interface SchemeQueryProps {
schemes?: SchemeRecord[];
onSchemesChange?: (schemes: SchemeRecord[]) => void;
network?: string;
state?: FlushingSchemeQueryState;
onStateChange?: (state: FlushingSchemeQueryState) => void;
}
const SCHEME_TYPE = "flushing_analysis";
export interface FlushingSchemeQueryState {
queryAll: boolean;
queryDate: Dayjs | null;
expandedId: number | null;
showTimeline: boolean;
selectedDate: Date | undefined;
timeRange: { start: Date; end: Date } | undefined;
}
export const createFlushingSchemeQueryState =
(): FlushingSchemeQueryState => ({
queryAll: true,
queryDate: dayjs(new Date()),
expandedId: null,
showTimeline: false,
selectedDate: undefined,
timeRange: undefined,
});
const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemes: externalSchemes,
onSchemesChange,
network = NETWORK_NAME,
state,
onStateChange,
}) => {
const [queryAll, setQueryAll] = useState<boolean>(true);
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
const [queryState, setQueryState, setQueryField] = useControllableObjectState(
state,
onStateChange,
createFlushingSchemeQueryState(),
);
const {
queryAll,
queryDate,
expandedId,
showTimeline,
selectedDate,
timeRange,
} = queryState;
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
// Timeline related state
const [showTimeline, setShowTimeline] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [timeRange, setTimeRange] = useState<{ start: Date; end: Date } | undefined>();
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
const { open } = useNotification();
@@ -269,15 +299,19 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
};
const handleViewResults = (scheme: SchemeRecord) => {
setShowTimeline(true);
setQueryState((previous) => ({ ...previous, showTimeline: true }));
const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined;
if (scheme.startTime && scheme.schemeDetail?.duration) {
const start = new Date(scheme.startTime);
const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000);
setSelectedDate(schemeDate);
setTimeRange({ start, end });
setQueryState((previous) => ({
...previous,
showTimeline: true,
selectedDate: schemeDate,
timeRange: { start, end },
}));
}
setSchemeName?.(scheme.schemeName);
@@ -311,7 +345,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
control={
<Checkbox
checked={queryAll}
onChange={(e) => setQueryAll(e.target.checked)}
onChange={(e) => setQueryField("queryAll", e.target.checked)}
size="small"
/>
}
@@ -325,7 +359,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
<DatePicker
value={queryDate}
onChange={(value) =>
value && dayjs.isDayjs(value) && setQueryDate(value)
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
}
format="YYYY-MM-DD"
disabled={queryAll}
@@ -443,13 +477,14 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
expandedId === scheme.id ? "收起详情" : "查看详情"
}
>
<IconButton
size="small"
onClick={() =>
setExpandedId(
expandedId === scheme.id ? null : scheme.id,
)
}
<IconButton
size="small"
onClick={() =>
setQueryField(
"expandedId",
expandedId === scheme.id ? null : scheme.id,
)
}
color="primary"
className="p-1"
>
@@ -17,8 +17,14 @@ import {
Analytics as AnalyticsIcon,
Search as SearchIcon,
} from "@mui/icons-material";
import OptimizationParameters from "./OptimizationParameters";
import SchemeQuery from "./SchemeQuery";
import OptimizationParameters, {
createOptimizationParametersState,
type OptimizationParametersState,
} from "./OptimizationParameters";
import SchemeQuery, {
createMonitoringSchemeQueryState,
type MonitoringSchemeQueryState,
} from "./SchemeQuery";
interface SchemeRecord {
id: number;
@@ -63,6 +69,11 @@ const MonitoringPlaceOptimizationPanel: React.FC<
// 持久化方案查询结果
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
const [optimizationState, setOptimizationState] =
useState<OptimizationParametersState>(createOptimizationParametersState);
const [queryState, setQueryState] = useState<MonitoringSchemeQueryState>(
createMonitoringSchemeQueryState,
);
// 使用受控或非受控状态
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
@@ -192,13 +203,18 @@ const MonitoringPlaceOptimizationPanel: React.FC<
{/* Tab 内容 */}
<TabPanel value={currentTab} index={0}>
<OptimizationParameters />
<OptimizationParameters
state={optimizationState}
onStateChange={setOptimizationState}
/>
</TabPanel>
<TabPanel value={currentTab} index={1}>
<SchemeQuery
schemes={schemes}
onSchemesChange={setSchemes}
state={queryState}
onStateChange={setQueryState}
onLocate={(id) => {
console.log("定位方案:", id);
// TODO: 在地图上定位
@@ -14,27 +14,53 @@ import { useNotification } from "@refinedev/core";
import { useGetIdentity } from "@refinedev/core";
import { api } from "@/lib/api";
import { config, NETWORK_NAME } from "@/config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
type IUser = {
id: string;
name?: string;
};
const OptimizationParameters: React.FC = () => {
export interface OptimizationParametersState {
sensorType: string;
method: string;
sensorCount: number;
minDiameter: number;
schemeName: string;
}
export const createOptimizationParametersState =
(): OptimizationParametersState => ({
sensorType: "pressure",
method: "kmeans",
sensorCount: 5,
minDiameter: 5,
schemeName: "Fangan" + new Date().getTime(),
});
interface OptimizationParametersProps {
state?: OptimizationParametersState;
onStateChange?: (state: OptimizationParametersState) => void;
}
const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
state,
onStateChange,
}) => {
const { open } = useNotification();
const { data: user } = useGetIdentity<IUser>();
// 表单状态
const [sensorType, setSensorType] = useState<string>("pressure");
const [method, setMethod] = useState<string>("kmeans");
const [sensorCount, setSensorCount] = useState<number>(5);
const [minDiameter, setMinDiameter] = useState<number>(5);
const [schemeName, setSchemeName] = useState<string>(
"Fangan" + new Date().getTime()
const [parametersState, , setFormField] = useControllableObjectState(
state,
onStateChange,
createOptimizationParametersState(),
);
const { sensorType, method, sensorCount, minDiameter, schemeName } =
parametersState;
const [network] = useState<string>(NETWORK_NAME);
const [analyzing, setAnalyzing] = useState<boolean>(false);
// 传感器类型选项
const sensorTypeOptions = [
{ value: "pressure", label: "压力" },
@@ -121,7 +147,7 @@ const OptimizationParameters: React.FC = () => {
});
// 重置方案名称
setSchemeName("Fangan" + new Date().getTime());
setFormField("schemeName", "Fangan" + new Date().getTime());
} else {
throw new Error(response.data?.message || "创建失败");
}
@@ -153,7 +179,7 @@ const OptimizationParameters: React.FC = () => {
fullWidth
size="small"
value={sensorType}
onChange={(e) => setSensorType(e.target.value)}
onChange={(e) => setFormField("sensorType", e.target.value)}
sx={{
"& .MuiOutlinedInput-root": {
"&:hover fieldset": {
@@ -186,7 +212,7 @@ const OptimizationParameters: React.FC = () => {
fullWidth
size="small"
value={method}
onChange={(e) => setMethod(e.target.value)}
onChange={(e) => setFormField("method", e.target.value)}
sx={{
"& .MuiOutlinedInput-root": {
"&:hover fieldset": {
@@ -219,7 +245,9 @@ const OptimizationParameters: React.FC = () => {
size="small"
type="number"
value={sensorCount}
onChange={(e) => setSensorCount(parseInt(e.target.value) || 0)}
onChange={(e) =>
setFormField("sensorCount", parseInt(e.target.value) || 0)
}
slotProps={{
htmlInput: { min: 1 },
}}
@@ -249,7 +277,9 @@ const OptimizationParameters: React.FC = () => {
size="small"
type="number"
value={minDiameter}
onChange={(e) => setMinDiameter(parseInt(e.target.value) || 0)}
onChange={(e) =>
setFormField("minDiameter", parseInt(e.target.value) || 0)
}
slotProps={{
htmlInput: { min: 0 },
}}
@@ -278,7 +308,7 @@ const OptimizationParameters: React.FC = () => {
fullWidth
size="small"
value={schemeName}
onChange={(e) => setSchemeName(e.target.value)}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="请输入方案名称"
sx={{
"& .MuiOutlinedInput-root": {
@@ -30,6 +30,7 @@ import { config, NETWORK_NAME } from "@config/config";
import { useNotification } from "@refinedev/core";
import { useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { GeoJSON } from "ol/format";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
@@ -62,19 +63,39 @@ interface SchemeQueryProps {
onSchemesChange?: (schemes: SchemeRecord[]) => void;
onLocate?: (id: number) => void;
network?: string;
state?: MonitoringSchemeQueryState;
onStateChange?: (state: MonitoringSchemeQueryState) => void;
}
export interface MonitoringSchemeQueryState {
queryAll: boolean;
queryDate: Dayjs | null;
expandedId: number | null;
}
export const createMonitoringSchemeQueryState =
(): MonitoringSchemeQueryState => ({
queryAll: true,
queryDate: dayjs(new Date()),
expandedId: null,
});
const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemes: externalSchemes,
onSchemesChange,
onLocate,
network = NETWORK_NAME,
state,
onStateChange,
}) => {
const [queryAll, setQueryAll] = useState<boolean>(true);
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
const [queryState, , setQueryField] = useControllableObjectState(
state,
onStateChange,
createMonitoringSchemeQueryState(),
);
const { queryAll, queryDate, expandedId } = queryState;
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const { open } = useNotification();
const map = useMap();
@@ -228,7 +249,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
// 查看详情(展开/收起)
const handleViewDetails = (id: number) => {
setExpandedId(expandedId === id ? null : id);
setQueryField("expandedId", expandedId === id ? null : id);
};
// 保存方案(示例功能)
@@ -250,7 +271,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
control={
<Checkbox
checked={queryAll}
onChange={(e) => setQueryAll(e.target.checked)}
onChange={(e) => setQueryField("queryAll", e.target.checked)}
size="small"
/>
}
@@ -264,7 +285,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
<DatePicker
value={queryDate}
onChange={(value) =>
value && dayjs.isDayjs(value) && setQueryDate(value)
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
}
format="YYYY-MM-DD"
disabled={queryAll}
@@ -375,7 +396,8 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
<IconButton
size="small"
onClick={() =>
setExpandedId(
setQueryField(
"expandedId",
expandedId === scheme.id ? null : scheme.id,
)
}
@@ -0,0 +1,55 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
export const useControllableState = <T,>(
externalValue: T | undefined,
onExternalChange: ((value: T) => void) | undefined,
defaultValue: T,
) => {
const [internalValue, setInternalValue] = useState<T>(defaultValue);
const value = externalValue !== undefined ? externalValue : internalValue;
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value;
}, [value]);
const setValue = useCallback(
(next: T | ((previous: T) => T)) => {
const nextValue =
typeof next === "function"
? (next as (previous: T) => T)(valueRef.current)
: next;
valueRef.current = nextValue;
if (externalValue === undefined) {
setInternalValue(nextValue);
}
onExternalChange?.(nextValue);
},
[externalValue, onExternalChange],
);
return [value, setValue] as const;
};
export const useControllableObjectState = <T extends object>(
externalValue: T | undefined,
onExternalChange: ((value: T) => void) | undefined,
defaultValue: T,
) => {
const [value, setValue] = useControllableState(
externalValue,
onExternalChange,
defaultValue,
);
const setField = useCallback(
<K extends keyof T>(key: K, nextValue: T[K]) => {
setValue((previous) => ({ ...previous, [key]: nextValue }));
},
[setValue],
);
return [value, setValue, setField] as const;
};