Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14c76231d5 | ||
|
|
694f7629ee | ||
|
|
701c5a949d | ||
|
|
435a4172e4 |
@@ -24,13 +24,16 @@ import dayjs, { Dayjs } from "dayjs";
|
|||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { NETWORK_NAME, config } from "@config/config";
|
import { NETWORK_NAME, config } from "@config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { BurstDetectionResult } from "./types";
|
import { BurstDetectionResult } from "./types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onResult: (result: BurstDetectionResult) => void;
|
onResult: (result: BurstDetectionResult) => void;
|
||||||
|
state?: BurstDetectionAnalysisParametersState;
|
||||||
|
onStateChange?: (state: BurstDetectionAnalysisParametersState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SchemeItem {
|
export interface SchemeItem {
|
||||||
scheme_id: number;
|
scheme_id: number;
|
||||||
scheme_name: string;
|
scheme_name: string;
|
||||||
scheme_type: 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 { open } = useNotification();
|
||||||
const [schemeName, setSchemeName] = useState(`Burst_Detection_${Date.now()}`);
|
const [parametersState, setParametersState, setFormField] = useControllableObjectState(
|
||||||
const [dataSource, setDataSource] = useState<"monitoring" | "simulation">("monitoring");
|
state,
|
||||||
const [schemes, setSchemes] = useState<SchemeItem[]>([]);
|
onStateChange,
|
||||||
const [selectedSchemeId, setSelectedSchemeId] = useState<number | "">("");
|
createBurstDetectionAnalysisParametersState(),
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
schemeName,
|
||||||
|
dataSource,
|
||||||
|
schemes,
|
||||||
|
selectedSchemeId,
|
||||||
|
scadaStart,
|
||||||
|
scadaEnd,
|
||||||
|
mu,
|
||||||
|
pointsPerDay,
|
||||||
|
nEstimators,
|
||||||
|
contaminationInput,
|
||||||
|
advancedOpen,
|
||||||
|
} = parametersState;
|
||||||
const [schemeLoading, setSchemeLoading] = useState(false);
|
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 [running, setRunning] = useState(false);
|
||||||
const isSimulationMode = dataSource === "simulation";
|
const isSimulationMode = dataSource === "simulation";
|
||||||
|
|
||||||
@@ -63,9 +106,12 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
|
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
|
||||||
const end = start.add(durationSeconds, "second");
|
const end = start.add(durationSeconds, "second");
|
||||||
|
|
||||||
setScadaStart(start);
|
setParametersState((previous) => ({
|
||||||
setScadaEnd(end);
|
...previous,
|
||||||
}, []);
|
scadaStart: start,
|
||||||
|
scadaEnd: end,
|
||||||
|
}));
|
||||||
|
}, [setParametersState]);
|
||||||
|
|
||||||
const fetchSchemes = useCallback(
|
const fetchSchemes = useCallback(
|
||||||
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
|
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",
|
(scheme) => scheme.scheme_type === "burst_analysis",
|
||||||
);
|
);
|
||||||
|
|
||||||
setSchemes(burstSchemes);
|
setFormField("schemes", burstSchemes);
|
||||||
|
|
||||||
if (selectedSchemeId) {
|
if (selectedSchemeId) {
|
||||||
const matchedScheme = burstSchemes.find(
|
const matchedScheme = burstSchemes.find(
|
||||||
@@ -89,7 +135,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
if (matchedScheme) {
|
if (matchedScheme) {
|
||||||
applySchemeTimeRange(matchedScheme);
|
applySchemeTimeRange(matchedScheme);
|
||||||
} else {
|
} else {
|
||||||
setSelectedSchemeId("");
|
setFormField("selectedSchemeId", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,18 +157,18 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
setSchemeLoading(false);
|
setSchemeLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId],
|
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId, setFormField],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDataSourceChange = (value: "monitoring" | "simulation") => {
|
const handleDataSourceChange = (value: "monitoring" | "simulation") => {
|
||||||
setDataSource(value);
|
setFormField("dataSource", value);
|
||||||
if (value === "simulation") {
|
if (value === "simulation") {
|
||||||
void fetchSchemes();
|
void fetchSchemes();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSchemeSelect = (schemeId: number) => {
|
const handleSchemeSelect = (schemeId: number) => {
|
||||||
setSelectedSchemeId(schemeId);
|
setFormField("selectedSchemeId", schemeId);
|
||||||
const scheme = schemes.find((item) => item.scheme_id === schemeId);
|
const scheme = schemes.find((item) => item.scheme_id === schemeId);
|
||||||
if (scheme) {
|
if (scheme) {
|
||||||
applySchemeTimeRange(scheme);
|
applySchemeTimeRange(scheme);
|
||||||
@@ -239,7 +285,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
value={schemeName}
|
value={schemeName}
|
||||||
onChange={(event) => setSchemeName(event.target.value)}
|
onChange={(event) => setFormField("schemeName", event.target.value)}
|
||||||
placeholder="请输入方案名称"
|
placeholder="请输入方案名称"
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
@@ -318,7 +364,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={scadaStart}
|
value={scadaStart}
|
||||||
onChange={setScadaStart}
|
onChange={(value) => setFormField("scadaStart", value)}
|
||||||
maxDateTime={scadaEnd ? scadaEnd.subtract(2, "day") : undefined}
|
maxDateTime={scadaEnd ? scadaEnd.subtract(2, "day") : undefined}
|
||||||
disabled={isSimulationMode}
|
disabled={isSimulationMode}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
@@ -331,7 +377,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={scadaEnd}
|
value={scadaEnd}
|
||||||
onChange={setScadaEnd}
|
onChange={(value) => setFormField("scadaEnd", value)}
|
||||||
minDateTime={scadaStart ? scadaStart.add(2, "day") : undefined}
|
minDateTime={scadaStart ? scadaStart.add(2, "day") : undefined}
|
||||||
disabled={isSimulationMode}
|
disabled={isSimulationMode}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
@@ -356,10 +402,10 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
<Box
|
<Box
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setAdvancedOpen((prev) => !prev)}
|
onClick={() => setFormField("advancedOpen", !advancedOpen)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
setAdvancedOpen((prev) => !prev);
|
setFormField("advancedOpen", !advancedOpen);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -397,7 +443,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
type="number"
|
type="number"
|
||||||
label="频域截断系数"
|
label="频域截断系数"
|
||||||
value={mu}
|
value={mu}
|
||||||
onChange={(event) => setMu(Number(event.target.value))}
|
onChange={(event) => setFormField("mu", Number(event.target.value))}
|
||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
inputProps={{ min: 1 }}
|
inputProps={{ min: 1 }}
|
||||||
@@ -406,7 +452,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
type="number"
|
type="number"
|
||||||
label="每日采样点数"
|
label="每日采样点数"
|
||||||
value={pointsPerDay}
|
value={pointsPerDay}
|
||||||
onChange={(event) => setPointsPerDay(Number(event.target.value))}
|
onChange={(event) => setFormField("pointsPerDay", Number(event.target.value))}
|
||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
inputProps={{ min: 1 }}
|
inputProps={{ min: 1 }}
|
||||||
@@ -415,7 +461,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
type="number"
|
type="number"
|
||||||
label="孤立森林树数量"
|
label="孤立森林树数量"
|
||||||
value={nEstimators}
|
value={nEstimators}
|
||||||
onChange={(event) => setNEstimators(Number(event.target.value))}
|
onChange={(event) => setFormField("nEstimators", Number(event.target.value))}
|
||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
inputProps={{ min: 1 }}
|
inputProps={{ min: 1 }}
|
||||||
@@ -423,7 +469,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
<TextField
|
<TextField
|
||||||
label="异常比例"
|
label="异常比例"
|
||||||
value={contaminationInput}
|
value={contaminationInput}
|
||||||
onChange={(event) => setContaminationInput(event.target.value)}
|
onChange={(event) => setFormField("contaminationInput", event.target.value)}
|
||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
helperText="填写 auto 或 0~0.5 之间的小数。"
|
helperText="填写 auto 或 0~0.5 之间的小数。"
|
||||||
@@ -442,13 +488,16 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
disabled={running}
|
disabled={running}
|
||||||
sx={{ textTransform: "none", fontWeight: 500 }}
|
sx={{ textTransform: "none", fontWeight: 500 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSchemeName(`Burst_Detection_${Date.now()}`);
|
setParametersState((previous) => ({
|
||||||
setScadaStart(dayjs().subtract(3, "day"));
|
...previous,
|
||||||
setScadaEnd(dayjs());
|
schemeName: `Burst_Detection_${Date.now()}`,
|
||||||
setMu(100);
|
scadaStart: dayjs().subtract(3, "day"),
|
||||||
setPointsPerDay(96);
|
scadaEnd: dayjs(),
|
||||||
setNEstimators(50);
|
mu: 100,
|
||||||
setContaminationInput("auto");
|
pointsPerDay: 96,
|
||||||
|
nEstimators: 50,
|
||||||
|
contaminationInput: "auto",
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
|
|||||||
@@ -9,9 +9,18 @@ import {
|
|||||||
FormatListBulleted,
|
FormatListBulleted,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import AnalysisParameters from "./AnalysisParameters";
|
import AnalysisParameters, {
|
||||||
import DetectionResults from "./DetectionResults";
|
createBurstDetectionAnalysisParametersState,
|
||||||
import SchemeQuery from "./SchemeQuery";
|
type BurstDetectionAnalysisParametersState,
|
||||||
|
} from "./AnalysisParameters";
|
||||||
|
import DetectionResults, {
|
||||||
|
createBurstDetectionResultsState,
|
||||||
|
type BurstDetectionResultsState,
|
||||||
|
} from "./DetectionResults";
|
||||||
|
import SchemeQuery, {
|
||||||
|
createBurstDetectionSchemeQueryState,
|
||||||
|
type BurstDetectionSchemeQueryState,
|
||||||
|
} from "./SchemeQuery";
|
||||||
import { BurstDetectionResult, BurstDetectionSchemeRecord } from "./types";
|
import { BurstDetectionResult, BurstDetectionSchemeRecord } from "./types";
|
||||||
|
|
||||||
const TabPanel = ({
|
const TabPanel = ({
|
||||||
@@ -33,6 +42,18 @@ const BurstDetectionPanel: React.FC = () => {
|
|||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(0);
|
||||||
const [result, setResult] = useState<BurstDetectionResult | null>(null);
|
const [result, setResult] = useState<BurstDetectionResult | null>(null);
|
||||||
const [schemes, setSchemes] = useState<BurstDetectionSchemeRecord[]>([]);
|
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 drawerWidth = 450;
|
||||||
const panelTitle = "爆管侦测";
|
const panelTitle = "爆管侦测";
|
||||||
@@ -137,13 +158,27 @@ const BurstDetectionPanel: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TabPanel value={tab} index={0}>
|
<TabPanel value={tab} index={0}>
|
||||||
<AnalysisParameters onResult={handleResult} />
|
<AnalysisParameters
|
||||||
|
onResult={handleResult}
|
||||||
|
state={analysisState}
|
||||||
|
onStateChange={setAnalysisState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={tab} index={1}>
|
<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>
|
||||||
<TabPanel value={tab} index={2}>
|
<TabPanel value={tab} index={2}>
|
||||||
<DetectionResults result={result} />
|
<DetectionResults
|
||||||
|
result={result}
|
||||||
|
state={resultsState}
|
||||||
|
onStateChange={setResultsState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -24,8 +24,19 @@ import { Circle, Fill, Stroke, Style } from "ol/style";
|
|||||||
import { bbox, featureCollection } from "@turf/turf";
|
import { bbox, featureCollection } from "@turf/turf";
|
||||||
import { BurstDetectionResult, BurstDetectionRow } from "./types";
|
import { BurstDetectionResult, BurstDetectionRow } from "./types";
|
||||||
|
|
||||||
|
export interface BurstDetectionResultsState {
|
||||||
|
selectedDay: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createBurstDetectionResultsState =
|
||||||
|
(): BurstDetectionResultsState => ({
|
||||||
|
selectedDay: null,
|
||||||
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
result: BurstDetectionResult | null;
|
result: BurstDetectionResult | null;
|
||||||
|
state?: BurstDetectionResultsState;
|
||||||
|
onStateChange?: (state: BurstDetectionResultsState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MetricCardProps {
|
interface MetricCardProps {
|
||||||
@@ -106,11 +117,25 @@ const getScoreLevel = (score: number) => {
|
|||||||
|
|
||||||
const formatDateTime = (value?: string) => (value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-");
|
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 map = useMap();
|
||||||
const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import "dayjs/locale/zh-cn";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { NETWORK_NAME } from "@config/config";
|
import { NETWORK_NAME } from "@config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import {
|
import {
|
||||||
BurstDetectionResult,
|
BurstDetectionResult,
|
||||||
BurstDetectionSchemeDetail,
|
BurstDetectionSchemeDetail,
|
||||||
@@ -33,15 +34,39 @@ interface Props {
|
|||||||
onViewResult: (result: BurstDetectionResult) => void;
|
onViewResult: (result: BurstDetectionResult) => void;
|
||||||
schemes?: BurstDetectionSchemeRecord[];
|
schemes?: BurstDetectionSchemeRecord[];
|
||||||
onSchemesChange?: (schemes: BurstDetectionSchemeRecord[]) => void;
|
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 { open } = useNotification();
|
||||||
const [queryAll, setQueryAll] = useState(true);
|
const [queryState, , setQueryField] = useControllableObjectState(
|
||||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs());
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createBurstDetectionSchemeQueryState(),
|
||||||
|
);
|
||||||
|
const { queryAll, queryDate, expandedId } = queryState;
|
||||||
const [internalSchemes, setInternalSchemes] = useState<BurstDetectionSchemeRecord[]>([]);
|
const [internalSchemes, setInternalSchemes] = useState<BurstDetectionSchemeRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
||||||
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
||||||
const setSchemes = onSchemesChange || setInternalSchemes;
|
const setSchemes = onSchemesChange || setInternalSchemes;
|
||||||
|
|
||||||
@@ -159,7 +184,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
size="small"
|
size="small"
|
||||||
checked={queryAll}
|
checked={queryAll}
|
||||||
onChange={(event) => setQueryAll(event.target.checked)}
|
onChange={(event) => setQueryField("queryAll", event.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<Typography variant="body2">查询全部</Typography>}
|
label={<Typography variant="body2">查询全部</Typography>}
|
||||||
@@ -168,7 +193,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={queryDate}
|
value={queryDate}
|
||||||
onChange={setQueryDate}
|
onChange={(value) => setQueryField("queryDate", value)}
|
||||||
disabled={queryAll}
|
disabled={queryAll}
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
slotProps={{ textField: { size: "small", sx: { width: 180 } } }}
|
slotProps={{ textField: { size: "small", sx: { width: 180 } } }}
|
||||||
@@ -241,7 +266,10 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)
|
setQueryField(
|
||||||
|
"expandedId",
|
||||||
|
expandedId === scheme.scheme_id ? null : scheme.scheme_id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
color="primary"
|
color="primary"
|
||||||
className="p-1"
|
className="p-1"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useCallback, useMemo, useState } from "react";
|
|||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
@@ -24,14 +25,17 @@ import dayjs, { Dayjs } from "dayjs";
|
|||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { NETWORK_NAME, config } from "@config/config";
|
import { NETWORK_NAME, config } from "@config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
|
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
|
||||||
import { BurstLocationResult } from "./types";
|
import { BurstLocationResult } from "./types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onResult: (result: BurstLocationResult) => void;
|
onResult: (result: BurstLocationResult) => void;
|
||||||
|
state?: BurstLocationAnalysisParametersState;
|
||||||
|
onStateChange?: (state: BurstLocationAnalysisParametersState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SchemeItem {
|
export interface SchemeItem {
|
||||||
scheme_id: number;
|
scheme_id: number;
|
||||||
scheme_name: string;
|
scheme_name: string;
|
||||||
scheme_type: string;
|
scheme_type: string;
|
||||||
@@ -44,24 +48,60 @@ interface SchemeItem {
|
|||||||
|
|
||||||
type DataSource = "monitoring" | "simulation";
|
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 { open } = useNotification();
|
||||||
const [schemeName, setSchemeName] = useState(`Burst_Locate_${Date.now()}`);
|
const [parametersState, setParametersState, setFormField] = useControllableObjectState(
|
||||||
const [dataSource, setDataSource] = useState<DataSource>("monitoring");
|
state,
|
||||||
const [schemes, setSchemes] = useState<SchemeItem[]>([]);
|
onStateChange,
|
||||||
const [selectedSchemeId, setSelectedSchemeId] = useState<number | "">("");
|
createBurstLocationAnalysisParametersState(),
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
schemeName,
|
||||||
|
dataSource,
|
||||||
|
schemes,
|
||||||
|
selectedSchemeId,
|
||||||
|
burstLeakage,
|
||||||
|
enableFlow,
|
||||||
|
burstStartTime,
|
||||||
|
burstEndTime,
|
||||||
|
minDpressure,
|
||||||
|
basicPressure,
|
||||||
|
advancedOpen,
|
||||||
|
} = parametersState;
|
||||||
const [schemeLoading, setSchemeLoading] = useState(false);
|
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 [running, setRunning] = useState(false);
|
||||||
const isSimulationMode = dataSource === "simulation";
|
const isSimulationMode = dataSource === "simulation";
|
||||||
|
|
||||||
@@ -70,9 +110,12 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
|
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
|
||||||
const end = start.add(durationSeconds, "second");
|
const end = start.add(durationSeconds, "second");
|
||||||
|
|
||||||
setBurstStartTime(start);
|
setParametersState((previous) => ({
|
||||||
setBurstEndTime(end);
|
...previous,
|
||||||
}, []);
|
burstStartTime: start,
|
||||||
|
burstEndTime: end,
|
||||||
|
}));
|
||||||
|
}, [setParametersState]);
|
||||||
|
|
||||||
const fetchSchemes = useCallback(
|
const fetchSchemes = useCallback(
|
||||||
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
|
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
|
||||||
@@ -87,7 +130,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
(scheme) => scheme.scheme_type === "burst_analysis",
|
(scheme) => scheme.scheme_type === "burst_analysis",
|
||||||
);
|
);
|
||||||
|
|
||||||
setSchemes(burstSchemes);
|
setFormField("schemes", burstSchemes);
|
||||||
|
|
||||||
if (selectedSchemeId) {
|
if (selectedSchemeId) {
|
||||||
const matchedScheme = burstSchemes.find(
|
const matchedScheme = burstSchemes.find(
|
||||||
@@ -96,7 +139,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
if (matchedScheme) {
|
if (matchedScheme) {
|
||||||
applySchemeTimeRange(matchedScheme);
|
applySchemeTimeRange(matchedScheme);
|
||||||
} else {
|
} else {
|
||||||
setSelectedSchemeId("");
|
setFormField("selectedSchemeId", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,18 +161,18 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
setSchemeLoading(false);
|
setSchemeLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId],
|
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId, setFormField],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDataSourceChange = (value: DataSource) => {
|
const handleDataSourceChange = (value: DataSource) => {
|
||||||
setDataSource(value);
|
setFormField("dataSource", value);
|
||||||
if (value === "simulation") {
|
if (value === "simulation") {
|
||||||
void fetchSchemes();
|
void fetchSchemes();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSchemeSelect = (schemeId: number) => {
|
const handleSchemeSelect = (schemeId: number) => {
|
||||||
setSelectedSchemeId(schemeId);
|
setFormField("selectedSchemeId", schemeId);
|
||||||
const scheme = schemes.find((item) => item.scheme_id === schemeId);
|
const scheme = schemes.find((item) => item.scheme_id === schemeId);
|
||||||
if (scheme) {
|
if (scheme) {
|
||||||
applySchemeTimeRange(scheme);
|
applySchemeTimeRange(scheme);
|
||||||
@@ -219,7 +262,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
value={schemeName}
|
value={schemeName}
|
||||||
onChange={(e) => setSchemeName(e.target.value)}
|
onChange={(e) => setFormField("schemeName", e.target.value)}
|
||||||
placeholder="请输入方案名称"
|
placeholder="请输入方案名称"
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
@@ -239,6 +282,26 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
<MenuItem value="simulation">模拟方案</MenuItem>
|
<MenuItem value="simulation">模拟方案</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<Alert
|
||||||
|
severity="info"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
borderColor: "info.light",
|
||||||
|
backgroundColor: "#eff6ff",
|
||||||
|
"& .MuiAlert-message": { width: "100%" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" className="block font-semibold text-blue-900">
|
||||||
|
数据选择规则
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" className="block leading-5 text-blue-800">
|
||||||
|
{isSimulationMode
|
||||||
|
? "当前为模拟方案:爆管数据取所选方案模拟结果,正常数据取实时模拟结果,两者使用同一爆管时间窗。"
|
||||||
|
: "当前为监测数据:爆管数据取所选监测时间窗,正常数据默认取前一天同一时段的监测数据。"}
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{isSimulationMode && (
|
{isSimulationMode && (
|
||||||
@@ -300,7 +363,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={burstStartTime}
|
value={burstStartTime}
|
||||||
onChange={setBurstStartTime}
|
onChange={(value) => setFormField("burstStartTime", value)}
|
||||||
maxDateTime={burstEndTime ?? undefined}
|
maxDateTime={burstEndTime ?? undefined}
|
||||||
disabled={isSimulationMode}
|
disabled={isSimulationMode}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
@@ -313,7 +376,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={burstEndTime}
|
value={burstEndTime}
|
||||||
onChange={setBurstEndTime}
|
onChange={(value) => setFormField("burstEndTime", value)}
|
||||||
minDateTime={burstStartTime ?? undefined}
|
minDateTime={burstStartTime ?? undefined}
|
||||||
disabled={isSimulationMode}
|
disabled={isSimulationMode}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
@@ -333,7 +396,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
value={burstLeakage}
|
value={burstLeakage}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
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
|
fullWidth
|
||||||
inputProps={{ min: 0, step: 10 }}
|
inputProps={{ min: 0, step: 10 }}
|
||||||
@@ -349,9 +412,9 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
<Box
|
<Box
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setAdvancedOpen((prev) => !prev)}
|
onClick={() => setFormField("advancedOpen", !advancedOpen)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") setAdvancedOpen((prev) => !prev);
|
if (e.key === "Enter" || e.key === " ") setFormField("advancedOpen", !advancedOpen);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -391,7 +454,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
<FormControl fullWidth size="small">
|
<FormControl fullWidth size="small">
|
||||||
<Select
|
<Select
|
||||||
value={enableFlow ? "enabled" : "disabled"}
|
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="disabled">禁用</MenuItem>
|
||||||
<MenuItem value="enabled">启用(使用流量计)</MenuItem>
|
<MenuItem value="enabled">启用(使用流量计)</MenuItem>
|
||||||
@@ -404,14 +467,14 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
label="最小压降 (m)"
|
label="最小压降 (m)"
|
||||||
size="small"
|
size="small"
|
||||||
value={minDpressure}
|
value={minDpressure}
|
||||||
onChange={(e) => setMinDpressure(Number(e.target.value))}
|
onChange={(e) => setFormField("minDpressure", Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
label="基础压力 (m)"
|
label="基础压力 (m)"
|
||||||
size="small"
|
size="small"
|
||||||
value={basicPressure}
|
value={basicPressure}
|
||||||
onChange={(e) => setBasicPressure(Number(e.target.value))}
|
onChange={(e) => setFormField("basicPressure", Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -9,9 +9,15 @@ import {
|
|||||||
FormatListBulleted,
|
FormatListBulleted,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import AnalysisParameters from "./AnalysisParameters";
|
import AnalysisParameters, {
|
||||||
|
createBurstLocationAnalysisParametersState,
|
||||||
|
type BurstLocationAnalysisParametersState,
|
||||||
|
} from "./AnalysisParameters";
|
||||||
import LocationResults from "./LocationResults";
|
import LocationResults from "./LocationResults";
|
||||||
import SchemeQuery from "./SchemeQuery";
|
import SchemeQuery, {
|
||||||
|
createBurstLocationSchemeQueryState,
|
||||||
|
type BurstLocationSchemeQueryState,
|
||||||
|
} from "./SchemeQuery";
|
||||||
import { BurstLocationResult, BurstSchemeRecord } from "./types";
|
import { BurstLocationResult, BurstSchemeRecord } from "./types";
|
||||||
|
|
||||||
const TabPanel = ({
|
const TabPanel = ({
|
||||||
@@ -33,6 +39,14 @@ const BurstLocationPanel: React.FC = () => {
|
|||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(0);
|
||||||
const [result, setResult] = useState<BurstLocationResult | null>(null);
|
const [result, setResult] = useState<BurstLocationResult | null>(null);
|
||||||
const [schemes, setSchemes] = useState<BurstSchemeRecord[]>([]);
|
const [schemes, setSchemes] = useState<BurstSchemeRecord[]>([]);
|
||||||
|
const [analysisState, setAnalysisState] =
|
||||||
|
useState<BurstLocationAnalysisParametersState>(
|
||||||
|
createBurstLocationAnalysisParametersState,
|
||||||
|
);
|
||||||
|
const [queryState, setQueryState] =
|
||||||
|
useState<BurstLocationSchemeQueryState>(
|
||||||
|
createBurstLocationSchemeQueryState,
|
||||||
|
);
|
||||||
|
|
||||||
const drawerWidth = 450;
|
const drawerWidth = 450;
|
||||||
const panelTitle = "爆管定位";
|
const panelTitle = "爆管定位";
|
||||||
@@ -146,10 +160,20 @@ const BurstLocationPanel: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TabPanel value={tab} index={0}>
|
<TabPanel value={tab} index={0}>
|
||||||
<AnalysisParameters onResult={handleResult} />
|
<AnalysisParameters
|
||||||
|
onResult={handleResult}
|
||||||
|
state={analysisState}
|
||||||
|
onStateChange={setAnalysisState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={tab} index={1}>
|
<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>
|
||||||
<TabPanel value={tab} index={2}>
|
<TabPanel value={tab} index={2}>
|
||||||
<LocationResults result={result} />
|
<LocationResults result={result} />
|
||||||
|
|||||||
@@ -75,6 +75,33 @@ const toneStyles: Record<
|
|||||||
const formatDateTime = (value?: string) =>
|
const formatDateTime = (value?: string) =>
|
||||||
value ? dayjs(value).format("MM-DD HH:mm") : "-";
|
value ? dayjs(value).format("MM-DD HH:mm") : "-";
|
||||||
|
|
||||||
|
const formatDateTimeRange = (start?: string, end?: string) => {
|
||||||
|
if (!start || !end) return "-";
|
||||||
|
return `${formatDateTime(start)} 至 ${formatDateTime(end)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDataSourceLabel = (result: BurstLocationResult) =>
|
||||||
|
result.data_source === "simulation" ? "模拟方案" : "监测数据";
|
||||||
|
|
||||||
|
const getNormalDataDescription = (result: BurstLocationResult) => {
|
||||||
|
switch (result.observed_source) {
|
||||||
|
case "simulation_scheme_burst_realtime_normal_timerange":
|
||||||
|
return "正常数据读取实时模拟结果,与爆管数据使用同一时间窗。";
|
||||||
|
case "scada_burst_scada_normal_timerange":
|
||||||
|
return "正常数据读取监测数据;未单独指定正常时间窗时,默认使用爆管时段前一天同一时段。";
|
||||||
|
case "scada_burst_payload_normal_timerange":
|
||||||
|
return "爆管数据读取监测数据,正常数据来自请求载荷。";
|
||||||
|
case "simulation_scheme_timerange":
|
||||||
|
return "历史方案记录:爆管数据和正常数据均读取方案模拟结果。";
|
||||||
|
case "scada_timerange":
|
||||||
|
return "历史方案记录:爆管数据和正常数据使用同一监测时间窗。";
|
||||||
|
case "request_payload":
|
||||||
|
return "爆管数据和正常数据均来自请求载荷。";
|
||||||
|
default:
|
||||||
|
return "正常数据按后端返回的数据源规则选择。";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MetricCard = ({ label, value, hint, tone }: MetricCardProps) => {
|
const MetricCard = ({ label, value, hint, tone }: MetricCardProps) => {
|
||||||
const style = toneStyles[tone];
|
const style = toneStyles[tone];
|
||||||
return (
|
return (
|
||||||
@@ -214,6 +241,16 @@ const LocationResults: React.FC<Props> = ({ result }) => {
|
|||||||
const burstTime = result.scada_window?.burst_start
|
const burstTime = result.scada_window?.burst_start
|
||||||
? formatDateTime(result.scada_window.burst_start)
|
? formatDateTime(result.scada_window.burst_start)
|
||||||
: "-";
|
: "-";
|
||||||
|
const burstWindow = formatDateTimeRange(
|
||||||
|
result.scada_window?.burst_start,
|
||||||
|
result.scada_window?.burst_end,
|
||||||
|
);
|
||||||
|
const normalWindow = formatDateTimeRange(
|
||||||
|
result.scada_window?.normal_start,
|
||||||
|
result.scada_window?.normal_end,
|
||||||
|
);
|
||||||
|
const sourceLabel = getDataSourceLabel(result);
|
||||||
|
const normalDataDescription = getNormalDataDescription(result);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="h-full overflow-auto p-1">
|
<Box className="h-full overflow-auto p-1">
|
||||||
@@ -287,6 +324,31 @@ const LocationResults: React.FC<Props> = ({ result }) => {
|
|||||||
tone="green"
|
tone="green"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
|
||||||
|
<Box className="mb-1 flex flex-wrap items-center gap-1.5">
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={sourceLabel}
|
||||||
|
color={result.data_source === "simulation" ? "secondary" : "primary"}
|
||||||
|
sx={{ height: 22, fontSize: "0.72rem", fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" className="text-gray-600">
|
||||||
|
正常数据选择说明
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" className="block leading-5 text-gray-700">
|
||||||
|
{normalDataDescription}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" className="mt-1 block leading-5 text-gray-500">
|
||||||
|
爆管窗口: {burstWindow};正常窗口: {normalWindow}
|
||||||
|
</Typography>
|
||||||
|
{result.data_source === "simulation" && result.simulation_scheme?.name ? (
|
||||||
|
<Typography variant="caption" className="mt-1 block truncate text-purple-600">
|
||||||
|
爆管方案: {result.simulation_scheme.name}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Candidate List */}
|
{/* Candidate List */}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import dayjs, { Dayjs } from "dayjs";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { NETWORK_NAME, config } from "@config/config";
|
import { NETWORK_NAME, config } from "@config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import {
|
import {
|
||||||
BurstLocationResult,
|
BurstLocationResult,
|
||||||
BurstLocationSchemeDetail,
|
BurstLocationSchemeDetail,
|
||||||
@@ -34,15 +35,39 @@ interface Props {
|
|||||||
onViewResult: (result: BurstLocationResult) => void;
|
onViewResult: (result: BurstLocationResult) => void;
|
||||||
schemes?: BurstSchemeRecord[];
|
schemes?: BurstSchemeRecord[];
|
||||||
onSchemesChange?: (schemes: BurstSchemeRecord[]) => void;
|
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 { open } = useNotification();
|
||||||
const [queryAll, setQueryAll] = useState(true);
|
const [queryState, , setQueryField] = useControllableObjectState(
|
||||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs());
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createBurstLocationSchemeQueryState(),
|
||||||
|
);
|
||||||
|
const { queryAll, queryDate, expandedId } = queryState;
|
||||||
const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]);
|
const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
||||||
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
||||||
const setSchemes = onSchemesChange || setInternalSchemes;
|
const setSchemes = onSchemesChange || setInternalSchemes;
|
||||||
|
|
||||||
@@ -157,7 +182,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
size="small"
|
size="small"
|
||||||
checked={queryAll}
|
checked={queryAll}
|
||||||
onChange={(e) => setQueryAll(e.target.checked)}
|
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<Typography variant="body2">查询全部</Typography>}
|
label={<Typography variant="body2">查询全部</Typography>}
|
||||||
@@ -166,7 +191,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={queryDate}
|
value={queryDate}
|
||||||
onChange={setQueryDate}
|
onChange={(value) => setQueryField("queryDate", value)}
|
||||||
disabled={queryAll}
|
disabled={queryAll}
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
slotProps={{ textField: { size: "small", sx: { width: 200 } } }}
|
slotProps={{ textField: { size: "small", sx: { width: 200 } } }}
|
||||||
@@ -281,7 +306,10 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)
|
setQueryField(
|
||||||
|
"expandedId",
|
||||||
|
expandedId === scheme.scheme_id ? null : scheme.scheme_id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
color="primary"
|
color="primary"
|
||||||
className="p-1"
|
className="p-1"
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface BurstLocationResult {
|
|||||||
scada_window?: {
|
scada_window?: {
|
||||||
burst_start?: string;
|
burst_start?: string;
|
||||||
burst_end?: string;
|
burst_end?: string;
|
||||||
|
normal_start?: string;
|
||||||
|
normal_end?: string;
|
||||||
};
|
};
|
||||||
pressure_samples?: {
|
pressure_samples?: {
|
||||||
burst?: number;
|
burst?: number;
|
||||||
@@ -51,6 +53,8 @@ export interface BurstLocationSchemeDetail {
|
|||||||
scada_window?: {
|
scada_window?: {
|
||||||
burst_start?: string;
|
burst_start?: string;
|
||||||
burst_end?: string;
|
burst_end?: string;
|
||||||
|
normal_start?: string;
|
||||||
|
normal_end?: string;
|
||||||
};
|
};
|
||||||
result_summary?: {
|
result_summary?: {
|
||||||
located_pipe?: string;
|
located_pipe?: string;
|
||||||
|
|||||||
@@ -20,33 +20,60 @@ import { useMap } from "@components/olmap/core/MapComponent";
|
|||||||
import VectorLayer from "ol/layer/Vector";
|
import VectorLayer from "ol/layer/Vector";
|
||||||
import VectorSource from "ol/source/Vector";
|
import VectorSource from "ol/source/Vector";
|
||||||
import { Style, Stroke, Icon } from "ol/style";
|
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 Feature, { FeatureLike } from "ol/Feature";
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { config, NETWORK_NAME } from "@/config/config";
|
import { config, NETWORK_NAME } from "@/config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { along, lineString, length, toMercator } from "@turf/turf";
|
import { along, lineString, length, toMercator } from "@turf/turf";
|
||||||
import { Point } from "ol/geom";
|
import { Point } from "ol/geom";
|
||||||
import { toLonLat } from "ol/proj";
|
import { toLonLat } from "ol/proj";
|
||||||
|
|
||||||
interface PipePoint {
|
export interface PipePoint {
|
||||||
id: string;
|
id: string;
|
||||||
diameter: number;
|
diameter: number;
|
||||||
area: 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 map = useMap();
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
|
|
||||||
const [pipePoints, setPipePoints] = useState<PipePoint[]>([]);
|
const [parametersState, setParametersState, setParameterField] = useControllableObjectState(
|
||||||
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
|
state,
|
||||||
const [duration, setDuration] = useState<number>(3600);
|
onStateChange,
|
||||||
const [schemeName, setSchemeName] = useState<string>(
|
createBurstAnalysisParametersState(),
|
||||||
"FANGAN" + new Date().getTime(),
|
|
||||||
);
|
);
|
||||||
const [network, setNetwork] = useState<string>(NETWORK_NAME);
|
const { pipePoints, startTime, duration, schemeName, network } =
|
||||||
|
parametersState;
|
||||||
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
||||||
|
|
||||||
const [highlightLayer, setHighlightLayer] =
|
const [highlightLayer, setHighlightLayer] =
|
||||||
@@ -54,6 +81,17 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
||||||
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
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 =
|
const isFormValid =
|
||||||
pipePoints.length > 0 &&
|
pipePoints.length > 0 &&
|
||||||
@@ -190,6 +228,18 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [highlightFeatures, highlightLayer]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
setPipePoints((prevPipes) => {
|
setPipePoints((prevPipes) => {
|
||||||
@@ -211,12 +261,11 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
id: properties.id,
|
id: properties.id,
|
||||||
diameter: properties.diameter || 0,
|
diameter: properties.diameter || 0,
|
||||||
area: 15,
|
area: 15,
|
||||||
feature: feature,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return [...filtered, ...newPipes];
|
return [...filtered, ...newPipes];
|
||||||
});
|
});
|
||||||
}, [highlightFeatures]);
|
}, [highlightFeatures, setPipePoints]);
|
||||||
|
|
||||||
// 开始选择管道
|
// 开始选择管道
|
||||||
const handleStartSelection = () => {
|
const handleStartSelection = () => {
|
||||||
@@ -237,6 +286,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemovePipe = (id: string) => {
|
const handleRemovePipe = (id: string) => {
|
||||||
|
setPipePoints((prev) => prev.filter((pipe) => pipe.id !== id));
|
||||||
// 从高亮features中移除
|
// 从高亮features中移除
|
||||||
setHighlightFeatures((prev) =>
|
setHighlightFeatures((prev) =>
|
||||||
prev.filter((f) => f.getProperties().id !== id),
|
prev.filter((f) => f.getProperties().id !== id),
|
||||||
@@ -425,7 +475,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value && dayjs.isDayjs(value) && setStartTime(value)
|
value && dayjs.isDayjs(value) && setParameterField("startTime", value)
|
||||||
}
|
}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -452,7 +502,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={duration}
|
value={duration}
|
||||||
onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
|
onChange={(e) => setParameterField("duration", parseInt(e.target.value) || 0)}
|
||||||
placeholder="输入持续时长"
|
placeholder="输入持续时长"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -466,7 +516,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={schemeName}
|
value={schemeName}
|
||||||
onChange={(e) => setSchemeName(e.target.value)}
|
onChange={(e) => setParameterField("schemeName", e.target.value)}
|
||||||
placeholder="输入方案名称"
|
placeholder="输入方案名称"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -18,10 +18,19 @@ import {
|
|||||||
MyLocation as MyLocationIcon,
|
MyLocation as MyLocationIcon,
|
||||||
Handyman as HandymanIcon,
|
Handyman as HandymanIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import AnalysisParameters from "./AnalysisParameters";
|
import AnalysisParameters, {
|
||||||
import SchemeQuery from "./SchemeQuery";
|
createBurstAnalysisParametersState,
|
||||||
|
type BurstAnalysisParametersState,
|
||||||
|
} from "./AnalysisParameters";
|
||||||
|
import SchemeQuery, {
|
||||||
|
createBurstSchemeQueryState,
|
||||||
|
type BurstSchemeQueryState,
|
||||||
|
} from "./SchemeQuery";
|
||||||
import LocationResults from "./LocationResults";
|
import LocationResults from "./LocationResults";
|
||||||
import ValveIsolation from "./ValveIsolation";
|
import ValveIsolation, {
|
||||||
|
createValveIsolationState,
|
||||||
|
type ValveIsolationState,
|
||||||
|
} from "./ValveIsolation";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { config } from "@config/config";
|
import { config } from "@config/config";
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
@@ -58,6 +67,11 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [internalOpen, setInternalOpen] = useState(true);
|
const [internalOpen, setInternalOpen] = useState(true);
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
|
const [analysisState, setAnalysisState] =
|
||||||
|
useState<BurstAnalysisParametersState>(createBurstAnalysisParametersState);
|
||||||
|
const [queryState, setQueryState] = useState<BurstSchemeQueryState>(
|
||||||
|
createBurstSchemeQueryState,
|
||||||
|
);
|
||||||
|
|
||||||
// 持久化方案查询结果
|
// 持久化方案查询结果
|
||||||
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
|
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
|
||||||
@@ -66,6 +80,8 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
|
|||||||
// 关阀分析结果和加载状态
|
// 关阀分析结果和加载状态
|
||||||
const [valveAnalysisLoading, setValveAnalysisLoading] = useState(false);
|
const [valveAnalysisLoading, setValveAnalysisLoading] = useState(false);
|
||||||
const [valveAnalysisResult, setValveAnalysisResult] = useState<ValveIsolationResult | null>(null);
|
const [valveAnalysisResult, setValveAnalysisResult] = useState<ValveIsolationResult | null>(null);
|
||||||
|
const [valveIsolationState, setValveIsolationState] =
|
||||||
|
useState<ValveIsolationState>(createValveIsolationState);
|
||||||
|
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
|
|
||||||
@@ -224,7 +240,10 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
|
|||||||
|
|
||||||
{/* Tab 内容 */}
|
{/* Tab 内容 */}
|
||||||
<TabPanel value={currentTab} index={0}>
|
<TabPanel value={currentTab} index={0}>
|
||||||
<AnalysisParameters />
|
<AnalysisParameters
|
||||||
|
state={analysisState}
|
||||||
|
onStateChange={setAnalysisState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={currentTab} index={1}>
|
<TabPanel value={currentTab} index={1}>
|
||||||
@@ -232,6 +251,8 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
|
|||||||
schemes={schemes}
|
schemes={schemes}
|
||||||
onSchemesChange={setSchemes}
|
onSchemesChange={setSchemes}
|
||||||
onLocate={handleLocateScheme}
|
onLocate={handleLocateScheme}
|
||||||
|
state={queryState}
|
||||||
|
onStateChange={setQueryState}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
@@ -247,6 +268,8 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
|
|||||||
result={valveAnalysisResult}
|
result={valveAnalysisResult}
|
||||||
onLoadingChange={setValveAnalysisLoading}
|
onLoadingChange={setValveAnalysisLoading}
|
||||||
onResultChange={setValveAnalysisResult}
|
onResultChange={setValveAnalysisResult}
|
||||||
|
state={valveIsolationState}
|
||||||
|
onStateChange={setValveIsolationState}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { api } from "@/lib/api";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { config, NETWORK_NAME } from "@config/config";
|
import { config, NETWORK_NAME } from "@config/config";
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
|
|
||||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||||
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
||||||
@@ -56,31 +57,57 @@ interface SchemeQueryProps {
|
|||||||
onSchemesChange?: (schemes: SchemeRecord[]) => void;
|
onSchemesChange?: (schemes: SchemeRecord[]) => void;
|
||||||
onLocate?: (scheme: SchemeRecord) => void;
|
onLocate?: (scheme: SchemeRecord) => void;
|
||||||
network?: string;
|
network?: string;
|
||||||
|
state?: BurstSchemeQueryState;
|
||||||
|
onStateChange?: (state: BurstSchemeQueryState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEME_TYPE = "burst_analysis";
|
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> = ({
|
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||||
schemes: externalSchemes,
|
schemes: externalSchemes,
|
||||||
onSchemesChange,
|
onSchemesChange,
|
||||||
onLocate,
|
onLocate,
|
||||||
network = NETWORK_NAME,
|
network = NETWORK_NAME,
|
||||||
|
state,
|
||||||
|
onStateChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [queryAll, setQueryAll] = useState<boolean>(true);
|
const [queryState, setQueryState, setQueryField] = useControllableObjectState(
|
||||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createBurstSchemeQueryState(),
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
queryAll,
|
||||||
|
queryDate,
|
||||||
|
showTimeline,
|
||||||
|
selectedDate,
|
||||||
|
timeRange,
|
||||||
|
expandedId,
|
||||||
|
} = queryState;
|
||||||
const [highlightLayer, setHighlightLayer] =
|
const [highlightLayer, setHighlightLayer] =
|
||||||
useState<VectorLayer<VectorSource> | null>(null);
|
useState<VectorLayer<VectorSource> | null>(null);
|
||||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
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 [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
||||||
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素
|
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素
|
||||||
|
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
@@ -181,7 +208,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
const scheme = filteredSchemes.find((s) => s.id === id);
|
const scheme = filteredSchemes.find((s) => s.id === id);
|
||||||
if (!scheme) return;
|
if (!scheme) return;
|
||||||
|
|
||||||
setShowTimeline(true);
|
setQueryState((previous) => ({ ...previous, showTimeline: true }));
|
||||||
// 计算时间范围
|
// 计算时间范围
|
||||||
const schemeDate = scheme.startTime
|
const schemeDate = scheme.startTime
|
||||||
? new Date(scheme.startTime)
|
? new Date(scheme.startTime)
|
||||||
@@ -191,8 +218,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
const end = new Date(
|
const end = new Date(
|
||||||
start.getTime() + scheme.schemeDetail.modify_total_duration * 1000,
|
start.getTime() + scheme.schemeDetail.modify_total_duration * 1000,
|
||||||
);
|
);
|
||||||
setSelectedDate(schemeDate);
|
setQueryState((previous) => ({
|
||||||
setTimeRange({ start, end });
|
...previous,
|
||||||
|
showTimeline: true,
|
||||||
|
selectedDate: schemeDate,
|
||||||
|
timeRange: { start, end },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
setSchemeName?.(scheme.schemeName);
|
setSchemeName?.(scheme.schemeName);
|
||||||
handleLocatePipes(scheme.schemeDetail?.burst_ID || []);
|
handleLocatePipes(scheme.schemeDetail?.burst_ID || []);
|
||||||
@@ -324,7 +355,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={queryAll}
|
checked={queryAll}
|
||||||
onChange={(e) => setQueryAll(e.target.checked)}
|
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -338,7 +369,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
value={queryDate}
|
value={queryDate}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value && dayjs.isDayjs(value) && setQueryDate(value)
|
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||||
}
|
}
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
disabled={queryAll}
|
disabled={queryAll}
|
||||||
@@ -450,13 +481,14 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
expandedId === scheme.id ? "收起详情" : "查看详情"
|
expandedId === scheme.id ? "收起详情" : "查看详情"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setExpandedId(
|
setQueryField(
|
||||||
expandedId === scheme.id ? null : scheme.id,
|
"expandedId",
|
||||||
)
|
expandedId === scheme.id ? null : scheme.id,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
color="primary"
|
color="primary"
|
||||||
className="p-1"
|
className="p-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
} from "@turf/turf";
|
} from "@turf/turf";
|
||||||
import { Point } from "ol/geom";
|
import { Point } from "ol/geom";
|
||||||
import { toLonLat } from "ol/proj";
|
import { toLonLat } from "ol/proj";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
|
|
||||||
interface ValveIsolationProps {
|
interface ValveIsolationProps {
|
||||||
initialPipeIds?: string[];
|
initialPipeIds?: string[];
|
||||||
@@ -60,8 +61,24 @@ interface ValveIsolationProps {
|
|||||||
result?: ValveIsolationResult | null;
|
result?: ValveIsolationResult | null;
|
||||||
onLoadingChange?: (loading: boolean) => void;
|
onLoadingChange?: (loading: boolean) => void;
|
||||||
onResultChange?: (result: ValveIsolationResult | null) => 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> = ({
|
const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
||||||
initialPipeIds = [],
|
initialPipeIds = [],
|
||||||
shouldFetch = false,
|
shouldFetch = false,
|
||||||
@@ -70,6 +87,8 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
|||||||
result: externalResult,
|
result: externalResult,
|
||||||
onLoadingChange,
|
onLoadingChange,
|
||||||
onResultChange,
|
onResultChange,
|
||||||
|
state,
|
||||||
|
onStateChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [internalLoading, setInternalLoading] = useState(false);
|
const [internalLoading, setInternalLoading] = useState(false);
|
||||||
const [internalResult, setInternalResult] =
|
const [internalResult, setInternalResult] =
|
||||||
@@ -82,12 +101,41 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
|||||||
const setLoading = onLoadingChange || setInternalLoading;
|
const setLoading = onLoadingChange || setInternalLoading;
|
||||||
const setResult = onResultChange || setInternalResult;
|
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 [highlightFeature, setHighlightFeature] = useState<Feature | null>(null);
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
|
||||||
const [expandedResult, setExpandedResult] = useState(true);
|
const setSelectedPipeId = useCallback(
|
||||||
const [disabledValves, setDisabledValves] = useState<string[]>([]);
|
(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 { open } = useNotification();
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
@@ -119,7 +167,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isSelecting, map, open, setResult],
|
[isSelecting, map, open, setResult, setSelectedPipeId],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,6 +183,13 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
|||||||
};
|
};
|
||||||
}, [map, isSelecting, handleMapClick]);
|
}, [map, isSelecting, handleMapClick]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPipeId || highlightFeature) return;
|
||||||
|
queryFeaturesByIds([selectedPipeId], "geo_pipes_mat").then((features) => {
|
||||||
|
setHighlightFeature(features[0] ?? null);
|
||||||
|
});
|
||||||
|
}, [highlightFeature, selectedPipeId]);
|
||||||
|
|
||||||
const clearSelectedPipe = () => {
|
const clearSelectedPipe = () => {
|
||||||
setSelectedPipeId(null);
|
setSelectedPipeId(null);
|
||||||
setHighlightFeature(null);
|
setHighlightFeature(null);
|
||||||
@@ -297,7 +352,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[open, setLoading, setResult],
|
[open, setActiveStep, setDisabledValves, setLoading, setResult],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 监听外部传入的分析请求
|
// 监听外部传入的分析请求
|
||||||
@@ -319,7 +374,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
|||||||
onFetchComplete();
|
onFetchComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [shouldFetch, initialPipeIds, fetchAnalysis, onFetchComplete]);
|
}, [shouldFetch, initialPipeIds, fetchAnalysis, onFetchComplete, setSelectedPipeId]);
|
||||||
|
|
||||||
// 初始化高亮图层
|
// 初始化高亮图层
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -24,21 +24,57 @@ import VectorLayer from "ol/layer/Vector";
|
|||||||
import VectorSource from "ol/source/Vector";
|
import VectorSource from "ol/source/Vector";
|
||||||
import { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style";
|
import { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style";
|
||||||
import Feature from "ol/Feature";
|
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 map = useMap();
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
|
|
||||||
const network = NETWORK_NAME;
|
const network = NETWORK_NAME;
|
||||||
const [schemeName, setSchemeName] = useState<string>(
|
const [parametersState, , setFormField] = useControllableObjectState(
|
||||||
"WQ_" + new Date().getTime(),
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createContaminantAnalysisParametersState(),
|
||||||
);
|
);
|
||||||
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
|
const {
|
||||||
const [sourceNode, setSourceNode] = useState<string>("");
|
schemeName,
|
||||||
const [concentration, setConcentration] = useState<number>(100);
|
startTime,
|
||||||
const [duration, setDuration] = useState<number>(3600);
|
sourceNode,
|
||||||
const [pattern, setPattern] = useState<string>("");
|
concentration,
|
||||||
|
duration,
|
||||||
|
pattern,
|
||||||
|
} = parametersState;
|
||||||
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
||||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -77,12 +113,12 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
|
|
||||||
const id = feature.getProperties().id;
|
const id = feature.getProperties().id;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
setSourceNode(id);
|
setFormField("sourceNode", id);
|
||||||
setHighlightFeature(feature);
|
setHighlightFeature(feature);
|
||||||
setIsSelecting(false);
|
setIsSelecting(false);
|
||||||
map.un("click", handleMapClickSelectFeatures);
|
map.un("click", handleMapClickSelectFeatures);
|
||||||
},
|
},
|
||||||
[map, open],
|
[map, open, setFormField],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -144,6 +180,17 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [highlightFeature, highlightLayer]);
|
}, [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 = () => {
|
const handleStartSelection = () => {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
setIsSelecting(true);
|
setIsSelecting(true);
|
||||||
@@ -157,7 +204,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClearSource = () => {
|
const handleClearSource = () => {
|
||||||
setSourceNode("");
|
setFormField("sourceNode", "");
|
||||||
setHighlightFeature(null);
|
setHighlightFeature(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,7 +223,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
: "";
|
: "";
|
||||||
try {
|
try {
|
||||||
if (!pattern) {
|
if (!pattern) {
|
||||||
setPattern("CONSTANT");
|
setFormField("pattern", "CONSTANT");
|
||||||
console.log("默认设置 pattern 为 CONSTANT");
|
console.log("默认设置 pattern 为 CONSTANT");
|
||||||
}
|
}
|
||||||
const params = {
|
const params = {
|
||||||
@@ -310,7 +357,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value && dayjs.isDayjs(value) && setStartTime(value)
|
value && dayjs.isDayjs(value) && setFormField("startTime", value)
|
||||||
}
|
}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -335,7 +382,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={schemeName}
|
value={schemeName}
|
||||||
onChange={(e) => setSchemeName(e.target.value)}
|
onChange={(e) => setFormField("schemeName", e.target.value)}
|
||||||
placeholder="输入方案名称"
|
placeholder="输入方案名称"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -349,7 +396,9 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={concentration}
|
value={concentration}
|
||||||
onChange={(e) => setConcentration(parseFloat(e.target.value) || 0)}
|
onChange={(e) =>
|
||||||
|
setFormField("concentration", parseFloat(e.target.value) || 0)
|
||||||
|
}
|
||||||
placeholder="输入浓度"
|
placeholder="输入浓度"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -363,7 +412,9 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={duration}
|
value={duration}
|
||||||
onChange={(e) => setDuration(parseInt(e.target.value, 10) || 0)}
|
onChange={(e) =>
|
||||||
|
setFormField("duration", parseInt(e.target.value, 10) || 0)
|
||||||
|
}
|
||||||
placeholder="输入持续时长"
|
placeholder="输入持续时长"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -376,7 +427,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={pattern}
|
value={pattern}
|
||||||
onChange={(e) => setPattern(e.target.value)}
|
onChange={(e) => setFormField("pattern", e.target.value)}
|
||||||
placeholder="可选,输入 pattern 名称,默认为 CONSTANT"
|
placeholder="可选,输入 pattern 名称,默认为 CONSTANT"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import moment from "moment";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { config, NETWORK_NAME } from "@config/config";
|
import { config, NETWORK_NAME } from "@config/config";
|
||||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
||||||
import { GeoJSON } from "ol/format";
|
import { GeoJSON } from "ol/format";
|
||||||
import VectorLayer from "ol/layer/Vector";
|
import VectorLayer from "ol/layer/Vector";
|
||||||
@@ -45,32 +46,60 @@ interface SchemeQueryProps {
|
|||||||
onSchemesChange?: (schemes: ContaminantSchemeRecord[]) => void;
|
onSchemesChange?: (schemes: ContaminantSchemeRecord[]) => void;
|
||||||
onViewResults?: () => void;
|
onViewResults?: () => void;
|
||||||
network?: string;
|
network?: string;
|
||||||
|
state?: ContaminantSchemeQueryState;
|
||||||
|
onStateChange?: (state: ContaminantSchemeQueryState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEME_TYPE = "contaminant_analysis";
|
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> = ({
|
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||||
schemes: externalSchemes,
|
schemes: externalSchemes,
|
||||||
onSchemesChange,
|
onSchemesChange,
|
||||||
onViewResults,
|
onViewResults,
|
||||||
network = NETWORK_NAME,
|
network = NETWORK_NAME,
|
||||||
|
state,
|
||||||
|
onStateChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [queryAll, setQueryAll] = useState<boolean>(true);
|
const [queryState, setQueryState, setQueryField] = useControllableObjectState(
|
||||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createContaminantSchemeQueryState(),
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
queryAll,
|
||||||
|
queryDate,
|
||||||
|
showTimeline,
|
||||||
|
selectedDate,
|
||||||
|
timeRange,
|
||||||
|
expandedId,
|
||||||
|
} = queryState;
|
||||||
const [highlightLayer, setHighlightLayer] =
|
const [highlightLayer, setHighlightLayer] =
|
||||||
useState<VectorLayer<VectorSource> | null>(null);
|
useState<VectorLayer<VectorSource> | null>(null);
|
||||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
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<
|
const [internalSchemes, setInternalSchemes] = useState<
|
||||||
ContaminantSchemeRecord[]
|
ContaminantSchemeRecord[]
|
||||||
>([]);
|
>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
||||||
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
|
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
@@ -256,7 +285,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
const scheme = filteredSchemes.find((s) => s.id === id);
|
const scheme = filteredSchemes.find((s) => s.id === id);
|
||||||
if (!scheme) return;
|
if (!scheme) return;
|
||||||
|
|
||||||
setShowTimeline(true);
|
setQueryState((previous) => ({ ...previous, showTimeline: true }));
|
||||||
const schemeDate = scheme.startTime
|
const schemeDate = scheme.startTime
|
||||||
? new Date(scheme.startTime)
|
? new Date(scheme.startTime)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -265,8 +294,12 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
const end = new Date(
|
const end = new Date(
|
||||||
start.getTime() + scheme.schemeDetail.duration * 1000,
|
start.getTime() + scheme.schemeDetail.duration * 1000,
|
||||||
);
|
);
|
||||||
setSelectedDate(schemeDate);
|
setQueryState((previous) => ({
|
||||||
setTimeRange({ start, end });
|
...previous,
|
||||||
|
showTimeline: true,
|
||||||
|
selectedDate: schemeDate,
|
||||||
|
timeRange: { start, end },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
setSchemeName?.(scheme.schemeName);
|
setSchemeName?.(scheme.schemeName);
|
||||||
if (scheme.schemeDetail?.source) {
|
if (scheme.schemeDetail?.source) {
|
||||||
@@ -296,7 +329,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={queryAll}
|
checked={queryAll}
|
||||||
onChange={(e) => setQueryAll(e.target.checked)}
|
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -310,7 +343,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
value={queryDate}
|
value={queryDate}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value && dayjs.isDayjs(value) && setQueryDate(value)
|
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||||
}
|
}
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
disabled={queryAll}
|
disabled={queryAll}
|
||||||
@@ -422,7 +455,8 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setExpandedId(
|
setQueryField(
|
||||||
|
"expandedId",
|
||||||
expandedId === scheme.id ? null : scheme.id,
|
expandedId === scheme.id ? null : scheme.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ import {
|
|||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
MyLocation as MyLocationIcon,
|
MyLocation as MyLocationIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import ContaminantAnalysisParameters from "./AnalysisParameters";
|
import ContaminantAnalysisParameters, {
|
||||||
import ContaminantSchemeQuery from "./SchemeQuery";
|
createContaminantAnalysisParametersState,
|
||||||
|
type ContaminantAnalysisParametersState,
|
||||||
|
} from "./AnalysisParameters";
|
||||||
|
import ContaminantSchemeQuery, {
|
||||||
|
createContaminantSchemeQueryState,
|
||||||
|
type ContaminantSchemeQueryState,
|
||||||
|
} from "./SchemeQuery";
|
||||||
import { useData } from "@components/olmap/core/MapComponent";
|
import { useData } from "@components/olmap/core/MapComponent";
|
||||||
import { ContaminantSchemeRecord } from "./types";
|
import { ContaminantSchemeRecord } from "./types";
|
||||||
|
|
||||||
@@ -34,6 +40,14 @@ const WaterQualityPanel: React.FC<WaterQualityPanelProps> = ({
|
|||||||
const [internalOpen, setInternalOpen] = useState(true);
|
const [internalOpen, setInternalOpen] = useState(true);
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const [schemes, setSchemes] = useState<ContaminantSchemeRecord[]>([]);
|
const [schemes, setSchemes] = useState<ContaminantSchemeRecord[]>([]);
|
||||||
|
const [analysisState, setAnalysisState] =
|
||||||
|
useState<ContaminantAnalysisParametersState>(
|
||||||
|
createContaminantAnalysisParametersState,
|
||||||
|
);
|
||||||
|
const [queryState, setQueryState] =
|
||||||
|
useState<ContaminantSchemeQueryState>(
|
||||||
|
createContaminantSchemeQueryState,
|
||||||
|
);
|
||||||
|
|
||||||
const data = useData();
|
const data = useData();
|
||||||
|
|
||||||
@@ -170,7 +184,10 @@ const WaterQualityPanel: React.FC<WaterQualityPanelProps> = ({
|
|||||||
|
|
||||||
{/* Tab 内容 */}
|
{/* Tab 内容 */}
|
||||||
<TabPanel value={currentTab} index={0}>
|
<TabPanel value={currentTab} index={0}>
|
||||||
<ContaminantAnalysisParameters />
|
<ContaminantAnalysisParameters
|
||||||
|
state={analysisState}
|
||||||
|
onStateChange={setAnalysisState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={currentTab} index={1}>
|
<TabPanel value={currentTab} index={1}>
|
||||||
@@ -178,6 +195,8 @@ const WaterQualityPanel: React.FC<WaterQualityPanelProps> = ({
|
|||||||
schemes={schemes}
|
schemes={schemes}
|
||||||
onSchemesChange={setSchemes}
|
onSchemesChange={setSchemes}
|
||||||
onViewResults={() => setCurrentTab(2)}
|
onViewResults={() => setCurrentTab(2)}
|
||||||
|
state={queryState}
|
||||||
|
onStateChange={setQueryState}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -19,27 +19,63 @@ import "dayjs/locale/zh-cn";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { NETWORK_NAME, config } from "@config/config";
|
import { NETWORK_NAME, config } from "@config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { LeakageResultDetail } from "./types";
|
import { LeakageResultDetail } from "./types";
|
||||||
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
|
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onResult: (result: LeakageResultDetail) => void;
|
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 { open } = useNotification();
|
||||||
const [schemeName, setSchemeName] = useState(`DMA_Leak_${Date.now()}`);
|
const [parametersState, , setFormField] = useControllableObjectState(
|
||||||
const [dmaCount, setDmaCount] = useState<number>(5);
|
state,
|
||||||
const [startTime, setStartTime] = useState<Dayjs | null>(
|
onStateChange,
|
||||||
dayjs().subtract(2, "hour"),
|
createDMALeakAnalysisParametersState(),
|
||||||
);
|
);
|
||||||
const [endTime, setEndTime] = useState<Dayjs | null>(dayjs());
|
const {
|
||||||
const [popSize, setPopSize] = useState<number>(10);
|
schemeName,
|
||||||
const [maxGen, setMaxGen] = useState<number>(50);
|
dmaCount,
|
||||||
const [qSum, setQSum] = useState<number>(1440);
|
startTime,
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
endTime,
|
||||||
|
popSize,
|
||||||
|
maxGen,
|
||||||
|
qSum,
|
||||||
|
advancedOpen,
|
||||||
|
} = parametersState;
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
const isValid = useMemo(() => {
|
const isValid = useMemo(() => {
|
||||||
if (!schemeName.trim() || !startTime || !endTime) return false;
|
if (!schemeName.trim() || !startTime || !endTime) return false;
|
||||||
return startTime.isBefore(endTime) && qSum >= 360;
|
return startTime.isBefore(endTime) && qSum >= 360;
|
||||||
@@ -105,7 +141,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
value={schemeName}
|
value={schemeName}
|
||||||
onChange={(e) => setSchemeName(e.target.value)}
|
onChange={(e) => setFormField("schemeName", e.target.value)}
|
||||||
placeholder="请输入方案名称"
|
placeholder="请输入方案名称"
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
@@ -123,11 +159,11 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
const value = Number.parseInt(e.target.value, 10);
|
const value = Number.parseInt(e.target.value, 10);
|
||||||
// Limit between 3 and 10
|
// Limit between 3 and 10
|
||||||
if (Number.isNaN(value)) {
|
if (Number.isNaN(value)) {
|
||||||
setDmaCount(5);
|
setFormField("dmaCount", 5);
|
||||||
} else if (value > 10) {
|
} else if (value > 10) {
|
||||||
setDmaCount(10);
|
setFormField("dmaCount", 10);
|
||||||
} else {
|
} else {
|
||||||
setDmaCount(Math.max(3, value));
|
setFormField("dmaCount", Math.max(3, value));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -150,7 +186,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={setStartTime}
|
onChange={(value) => setFormField("startTime", value)}
|
||||||
maxDateTime={endTime ?? undefined}
|
maxDateTime={endTime ?? undefined}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||||
@@ -162,7 +198,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={endTime}
|
value={endTime}
|
||||||
onChange={setEndTime}
|
onChange={(value) => setFormField("endTime", value)}
|
||||||
minDateTime={startTime ?? undefined}
|
minDateTime={startTime ?? undefined}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||||
@@ -180,7 +216,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
value={qSum}
|
value={qSum}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value);
|
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 }}
|
inputProps={{ min: 360, step: 10 }}
|
||||||
/>
|
/>
|
||||||
@@ -195,9 +231,9 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
<Box
|
<Box
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setAdvancedOpen((prev) => !prev)}
|
onClick={() => setFormField("advancedOpen", !advancedOpen)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") setAdvancedOpen((prev) => !prev);
|
if (e.key === "Enter" || e.key === " ") setFormField("advancedOpen", !advancedOpen);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -235,14 +271,14 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
|
|||||||
label="种群规模"
|
label="种群规模"
|
||||||
size="small"
|
size="small"
|
||||||
value={popSize}
|
value={popSize}
|
||||||
onChange={(e) => setPopSize(Number(e.target.value))}
|
onChange={(e) => setFormField("popSize", Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
label="最大代数"
|
label="最大代数"
|
||||||
size="small"
|
size="small"
|
||||||
value={maxGen}
|
value={maxGen}
|
||||||
onChange={(e) => setMaxGen(Number(e.target.value))}
|
onChange={(e) => setFormField("maxGen", Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -19,8 +19,14 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { useMap } from "@components/olmap/core/MapComponent";
|
import { useMap } from "@components/olmap/core/MapComponent";
|
||||||
import StyleLegend from "@components/olmap/core/Controls/StyleLegend";
|
import StyleLegend from "@components/olmap/core/Controls/StyleLegend";
|
||||||
import AnalysisParameters from "./AnalysisParameters";
|
import AnalysisParameters, {
|
||||||
import SchemeQuery from "./SchemeQuery";
|
createDMALeakAnalysisParametersState,
|
||||||
|
type DMALeakAnalysisParametersState,
|
||||||
|
} from "./AnalysisParameters";
|
||||||
|
import SchemeQuery, {
|
||||||
|
createDMALeakSchemeQueryState,
|
||||||
|
type DMALeakSchemeQueryState,
|
||||||
|
} from "./SchemeQuery";
|
||||||
import RecognitionResults from "./RecognitionResults";
|
import RecognitionResults from "./RecognitionResults";
|
||||||
import { applyJunctionAreaRender } from "./applyJunctionAreaRender";
|
import { applyJunctionAreaRender } from "./applyJunctionAreaRender";
|
||||||
import { getAreaColor } from "./utils";
|
import { getAreaColor } from "./utils";
|
||||||
@@ -49,6 +55,13 @@ const DMALeakDetectionPanel: React.FC = () => {
|
|||||||
const [result, setResult] = useState<LeakageResultDetail | null>(null);
|
const [result, setResult] = useState<LeakageResultDetail | null>(null);
|
||||||
const [loadedResult, setLoadedResult] = useState<LeakageResultDetail | null>(null);
|
const [loadedResult, setLoadedResult] = useState<LeakageResultDetail | null>(null);
|
||||||
const [schemes, setSchemes] = useState<LeakageSchemeRecord[]>([]);
|
const [schemes, setSchemes] = useState<LeakageSchemeRecord[]>([]);
|
||||||
|
const [analysisState, setAnalysisState] =
|
||||||
|
useState<DMALeakAnalysisParametersState>(
|
||||||
|
createDMALeakAnalysisParametersState,
|
||||||
|
);
|
||||||
|
const [queryState, setQueryState] = useState<DMALeakSchemeQueryState>(
|
||||||
|
createDMALeakSchemeQueryState,
|
||||||
|
);
|
||||||
|
|
||||||
const drawerWidth = 450;
|
const drawerWidth = 450;
|
||||||
const panelTitle = "DMA 漏损识别";
|
const panelTitle = "DMA 漏损识别";
|
||||||
@@ -196,10 +209,20 @@ const DMALeakDetectionPanel: React.FC = () => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
<TabPanel value={tab} index={0}>
|
<TabPanel value={tab} index={0}>
|
||||||
<AnalysisParameters onResult={handleAnalysisResult} />
|
<AnalysisParameters
|
||||||
|
onResult={handleAnalysisResult}
|
||||||
|
state={analysisState}
|
||||||
|
onStateChange={setAnalysisState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={tab} index={1}>
|
<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>
|
||||||
<TabPanel value={tab} index={2}>
|
<TabPanel value={tab} index={2}>
|
||||||
<RecognitionResults result={result} />
|
<RecognitionResults result={result} />
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import dayjs, { Dayjs } from "dayjs";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { NETWORK_NAME, config } from "@config/config";
|
import { NETWORK_NAME, config } from "@config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { LeakageResultDetail, LeakageSchemeRecord } from "./types";
|
import { LeakageResultDetail, LeakageSchemeRecord } from "./types";
|
||||||
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
|
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
|
||||||
|
|
||||||
@@ -30,15 +31,38 @@ interface Props {
|
|||||||
onViewResult: (result: LeakageResultDetail) => void;
|
onViewResult: (result: LeakageResultDetail) => void;
|
||||||
schemes?: LeakageSchemeRecord[];
|
schemes?: LeakageSchemeRecord[];
|
||||||
onSchemesChange?: (schemes: LeakageSchemeRecord[]) => void;
|
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 { open } = useNotification();
|
||||||
const [queryAll, setQueryAll] = useState(true);
|
const [queryState, , setQueryField] = useControllableObjectState(
|
||||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs());
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createDMALeakSchemeQueryState(),
|
||||||
|
);
|
||||||
|
const { queryAll, queryDate, expandedId } = queryState;
|
||||||
const [internalSchemes, setInternalSchemes] = useState<LeakageSchemeRecord[]>([]);
|
const [internalSchemes, setInternalSchemes] = useState<LeakageSchemeRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
||||||
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
|
||||||
const setSchemes = onSchemesChange || setInternalSchemes;
|
const setSchemes = onSchemesChange || setInternalSchemes;
|
||||||
|
|
||||||
@@ -91,7 +115,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
size="small"
|
size="small"
|
||||||
checked={queryAll}
|
checked={queryAll}
|
||||||
onChange={(e) => setQueryAll(e.target.checked)}
|
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<Typography variant="body2">查询全部</Typography>}
|
label={<Typography variant="body2">查询全部</Typography>}
|
||||||
@@ -100,7 +124,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes,
|
|||||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={queryDate}
|
value={queryDate}
|
||||||
onChange={setQueryDate}
|
onChange={(value) => setQueryField("queryDate", value)}
|
||||||
disabled={queryAll}
|
disabled={queryAll}
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
slotProps={{ textField: { size: "small", sx: { width: 200 } } }}
|
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 ? "收起详情" : "查看详情"}>
|
<Tooltip title={expandedId === scheme.scheme_id ? "收起详情" : "查看详情"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
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"
|
color="primary"
|
||||||
className="p-1"
|
className="p-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,39 +22,82 @@ import { useMap } from "@components/olmap/core/MapComponent";
|
|||||||
import VectorLayer from "ol/layer/Vector";
|
import VectorLayer from "ol/layer/Vector";
|
||||||
import VectorSource from "ol/source/Vector";
|
import VectorSource from "ol/source/Vector";
|
||||||
import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style";
|
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 Feature, { FeatureLike } from "ol/Feature";
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { config, NETWORK_NAME } from "@/config/config";
|
import { config, NETWORK_NAME } from "@/config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
|
|
||||||
interface ValveItem {
|
export interface ValveItem {
|
||||||
id: string;
|
id: string;
|
||||||
k: number;
|
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 map = useMap();
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
|
|
||||||
// State
|
const [parametersState, setParametersState, setFormField] = useControllableObjectState(
|
||||||
const [schemeName, setSchemeName] = useState<string>(
|
state,
|
||||||
"Flushing_" + new Date().getTime(),
|
onStateChange,
|
||||||
|
createFlushingAnalysisParametersState(),
|
||||||
);
|
);
|
||||||
const [valves, setValves] = useState<ValveItem[]>([]);
|
const { schemeName, valves, drainageNode, startTime, flushFlow, duration } =
|
||||||
const [drainageNode, setDrainageNode] = useState<string | null>(null);
|
parametersState;
|
||||||
|
const [valveFeatures, setValveFeatures] = useState<Feature[]>([]);
|
||||||
const [drainageFeature, setDrainageFeature] = useState<Feature | null>(null);
|
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 [selectionMode, setSelectionMode] = useState<'none' | 'valve' | 'drainage'>('none');
|
||||||
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
||||||
|
|
||||||
const [highlightLayer, setHighlightLayer] = useState<VectorLayer<VectorSource> | null>(null);
|
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
|
// Map click handler
|
||||||
const handleMapClickSelectFeatures = useCallback(
|
const handleMapClickSelectFeatures = useCallback(
|
||||||
async (event: { coordinate: number[] }) => {
|
async (event: { coordinate: number[] }) => {
|
||||||
@@ -83,7 +126,8 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
});
|
});
|
||||||
return prev;
|
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') {
|
} else if (selectionMode === 'drainage') {
|
||||||
@@ -100,7 +144,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
map.un("click", handleMapClickSelectFeatures);
|
map.un("click", handleMapClickSelectFeatures);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[map, selectionMode, open]
|
[map, selectionMode, open, setDrainageNode, setValves]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize highlight layer
|
// Initialize highlight layer
|
||||||
@@ -162,9 +206,9 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
source.clear();
|
source.clear();
|
||||||
|
|
||||||
// Add valves
|
// Add valves
|
||||||
valves.forEach((v) => {
|
valveFeatures.forEach((feature) => {
|
||||||
if (v.feature) {
|
if (feature) {
|
||||||
const f = v.feature.clone(); // Clone to avoid modifying original
|
const f = feature.clone(); // Clone to avoid modifying original
|
||||||
f.set("type", "valve");
|
f.set("type", "valve");
|
||||||
// Ensure geometry is present (it should be for features from map)
|
// Ensure geometry is present (it should be for features from map)
|
||||||
if (f.getGeometry()) {
|
if (f.getGeometry()) {
|
||||||
@@ -180,7 +224,32 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
source.addFeature(f);
|
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
|
// Bind click event based on selection mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -205,6 +274,9 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
|
|
||||||
const handleRemoveValve = (id: string) => {
|
const handleRemoveValve = (id: string) => {
|
||||||
setValves((prev) => prev.filter((v) => v.id !== id));
|
setValves((prev) => prev.filter((v) => v.id !== id));
|
||||||
|
setValveFeatures((prev) =>
|
||||||
|
prev.filter((feature) => feature.getProperties().id !== id),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleValveKChange = (id: string, k: string) => {
|
const handleValveKChange = (id: string, k: string) => {
|
||||||
@@ -373,7 +445,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={(newValue) => setStartTime(newValue)}
|
onChange={(newValue) => setFormField("startTime", newValue)}
|
||||||
format="YYYY-MM-DD HH:mm"
|
format="YYYY-MM-DD HH:mm"
|
||||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||||
localeText={
|
localeText={
|
||||||
@@ -393,7 +465,7 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={schemeName}
|
value={schemeName}
|
||||||
onChange={(e) => setSchemeName(e.target.value)}
|
onChange={(e) => setFormField("schemeName", e.target.value)}
|
||||||
placeholder="请输入方案名称"
|
placeholder="请输入方案名称"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -408,7 +480,9 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={flushFlow}
|
value={flushFlow}
|
||||||
onChange={(e) => setFlushFlow(parseFloat(e.target.value) || 0)}
|
onChange={(e) =>
|
||||||
|
setFormField("flushFlow", parseFloat(e.target.value) || 0)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="flex-1">
|
<Box className="flex-1">
|
||||||
@@ -420,7 +494,9 @@ const AnalysisParameters: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={duration}
|
value={duration}
|
||||||
onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
|
onChange={(e) =>
|
||||||
|
setFormField("duration", parseInt(e.target.value) || 0)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ import {
|
|||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { MdCleaningServices } from "react-icons/md";
|
import { MdCleaningServices } from "react-icons/md";
|
||||||
import AnalysisParameters from "./AnalysisParameters";
|
import AnalysisParameters, {
|
||||||
import SchemeQuery from "./SchemeQuery";
|
createFlushingAnalysisParametersState,
|
||||||
|
type FlushingAnalysisParametersState,
|
||||||
|
} from "./AnalysisParameters";
|
||||||
|
import SchemeQuery, {
|
||||||
|
createFlushingSchemeQueryState,
|
||||||
|
type FlushingSchemeQueryState,
|
||||||
|
} from "./SchemeQuery";
|
||||||
import { SchemeRecord } from "./types";
|
import { SchemeRecord } from "./types";
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
@@ -53,6 +59,13 @@ const FlushingAnalysisPanel: React.FC<FlushingAnalysisPanelProps> = ({
|
|||||||
const [internalOpen, setInternalOpen] = useState(true);
|
const [internalOpen, setInternalOpen] = useState(true);
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
|
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
|
||||||
|
const [analysisState, setAnalysisState] =
|
||||||
|
useState<FlushingAnalysisParametersState>(
|
||||||
|
createFlushingAnalysisParametersState,
|
||||||
|
);
|
||||||
|
const [queryState, setQueryState] = useState<FlushingSchemeQueryState>(
|
||||||
|
createFlushingSchemeQueryState,
|
||||||
|
);
|
||||||
|
|
||||||
// Using controlled or uncontrolled state
|
// Using controlled or uncontrolled state
|
||||||
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
||||||
@@ -181,11 +194,19 @@ const FlushingAnalysisPanel: React.FC<FlushingAnalysisPanelProps> = ({
|
|||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<TabPanel value={currentTab} index={0}>
|
<TabPanel value={currentTab} index={0}>
|
||||||
<AnalysisParameters />
|
<AnalysisParameters
|
||||||
|
state={analysisState}
|
||||||
|
onStateChange={setAnalysisState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={currentTab} index={1}>
|
<TabPanel value={currentTab} index={1}>
|
||||||
<SchemeQuery schemes={schemes} onSchemesChange={setSchemes} />
|
<SchemeQuery
|
||||||
|
schemes={schemes}
|
||||||
|
onSchemesChange={setSchemes}
|
||||||
|
state={queryState}
|
||||||
|
onStateChange={setQueryState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { config, NETWORK_NAME } from "@config/config";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
||||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { GeoJSON } from "ol/format";
|
import { GeoJSON } from "ol/format";
|
||||||
import VectorLayer from "ol/layer/Vector";
|
import VectorLayer from "ol/layer/Vector";
|
||||||
import VectorSource from "ol/source/Vector";
|
import VectorSource from "ol/source/Vector";
|
||||||
@@ -46,29 +47,58 @@ interface SchemeQueryProps {
|
|||||||
schemes?: SchemeRecord[];
|
schemes?: SchemeRecord[];
|
||||||
onSchemesChange?: (schemes: SchemeRecord[]) => void;
|
onSchemesChange?: (schemes: SchemeRecord[]) => void;
|
||||||
network?: string;
|
network?: string;
|
||||||
|
state?: FlushingSchemeQueryState;
|
||||||
|
onStateChange?: (state: FlushingSchemeQueryState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEME_TYPE = "flushing_analysis";
|
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> = ({
|
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||||
schemes: externalSchemes,
|
schemes: externalSchemes,
|
||||||
onSchemesChange,
|
onSchemesChange,
|
||||||
network = NETWORK_NAME,
|
network = NETWORK_NAME,
|
||||||
|
state,
|
||||||
|
onStateChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [queryAll, setQueryAll] = useState<boolean>(true);
|
const [queryState, setQueryState, setQueryField] = useControllableObjectState(
|
||||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createFlushingSchemeQueryState(),
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
queryAll,
|
||||||
|
queryDate,
|
||||||
|
expandedId,
|
||||||
|
showTimeline,
|
||||||
|
selectedDate,
|
||||||
|
timeRange,
|
||||||
|
} = queryState;
|
||||||
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
|
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const [highlightLayer, setHighlightLayer] =
|
const [highlightLayer, setHighlightLayer] =
|
||||||
useState<VectorLayer<VectorSource> | null>(null);
|
useState<VectorLayer<VectorSource> | null>(null);
|
||||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
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 [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
@@ -269,15 +299,19 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleViewResults = (scheme: SchemeRecord) => {
|
const handleViewResults = (scheme: SchemeRecord) => {
|
||||||
setShowTimeline(true);
|
setQueryState((previous) => ({ ...previous, showTimeline: true }));
|
||||||
|
|
||||||
const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined;
|
const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined;
|
||||||
|
|
||||||
if (scheme.startTime && scheme.schemeDetail?.duration) {
|
if (scheme.startTime && scheme.schemeDetail?.duration) {
|
||||||
const start = new Date(scheme.startTime);
|
const start = new Date(scheme.startTime);
|
||||||
const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000);
|
const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000);
|
||||||
setSelectedDate(schemeDate);
|
setQueryState((previous) => ({
|
||||||
setTimeRange({ start, end });
|
...previous,
|
||||||
|
showTimeline: true,
|
||||||
|
selectedDate: schemeDate,
|
||||||
|
timeRange: { start, end },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
setSchemeName?.(scheme.schemeName);
|
setSchemeName?.(scheme.schemeName);
|
||||||
@@ -311,7 +345,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={queryAll}
|
checked={queryAll}
|
||||||
onChange={(e) => setQueryAll(e.target.checked)}
|
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -325,7 +359,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
value={queryDate}
|
value={queryDate}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value && dayjs.isDayjs(value) && setQueryDate(value)
|
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||||
}
|
}
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
disabled={queryAll}
|
disabled={queryAll}
|
||||||
@@ -443,13 +477,14 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
expandedId === scheme.id ? "收起详情" : "查看详情"
|
expandedId === scheme.id ? "收起详情" : "查看详情"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setExpandedId(
|
setQueryField(
|
||||||
expandedId === scheme.id ? null : scheme.id,
|
"expandedId",
|
||||||
)
|
expandedId === scheme.id ? null : scheme.id,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
color="primary"
|
color="primary"
|
||||||
className="p-1"
|
className="p-1"
|
||||||
>
|
>
|
||||||
|
|||||||
+19
-3
@@ -17,8 +17,14 @@ import {
|
|||||||
Analytics as AnalyticsIcon,
|
Analytics as AnalyticsIcon,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import OptimizationParameters from "./OptimizationParameters";
|
import OptimizationParameters, {
|
||||||
import SchemeQuery from "./SchemeQuery";
|
createOptimizationParametersState,
|
||||||
|
type OptimizationParametersState,
|
||||||
|
} from "./OptimizationParameters";
|
||||||
|
import SchemeQuery, {
|
||||||
|
createMonitoringSchemeQueryState,
|
||||||
|
type MonitoringSchemeQueryState,
|
||||||
|
} from "./SchemeQuery";
|
||||||
|
|
||||||
interface SchemeRecord {
|
interface SchemeRecord {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -63,6 +69,11 @@ const MonitoringPlaceOptimizationPanel: React.FC<
|
|||||||
|
|
||||||
// 持久化方案查询结果
|
// 持久化方案查询结果
|
||||||
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
|
const [schemes, setSchemes] = useState<SchemeRecord[]>([]);
|
||||||
|
const [optimizationState, setOptimizationState] =
|
||||||
|
useState<OptimizationParametersState>(createOptimizationParametersState);
|
||||||
|
const [queryState, setQueryState] = useState<MonitoringSchemeQueryState>(
|
||||||
|
createMonitoringSchemeQueryState,
|
||||||
|
);
|
||||||
|
|
||||||
// 使用受控或非受控状态
|
// 使用受控或非受控状态
|
||||||
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
||||||
@@ -192,13 +203,18 @@ const MonitoringPlaceOptimizationPanel: React.FC<
|
|||||||
|
|
||||||
{/* Tab 内容 */}
|
{/* Tab 内容 */}
|
||||||
<TabPanel value={currentTab} index={0}>
|
<TabPanel value={currentTab} index={0}>
|
||||||
<OptimizationParameters />
|
<OptimizationParameters
|
||||||
|
state={optimizationState}
|
||||||
|
onStateChange={setOptimizationState}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={currentTab} index={1}>
|
<TabPanel value={currentTab} index={1}>
|
||||||
<SchemeQuery
|
<SchemeQuery
|
||||||
schemes={schemes}
|
schemes={schemes}
|
||||||
onSchemesChange={setSchemes}
|
onSchemesChange={setSchemes}
|
||||||
|
state={queryState}
|
||||||
|
onStateChange={setQueryState}
|
||||||
onLocate={(id) => {
|
onLocate={(id) => {
|
||||||
console.log("定位方案:", id);
|
console.log("定位方案:", id);
|
||||||
// TODO: 在地图上定位
|
// TODO: 在地图上定位
|
||||||
|
|||||||
@@ -14,27 +14,53 @@ import { useNotification } from "@refinedev/core";
|
|||||||
import { useGetIdentity } from "@refinedev/core";
|
import { useGetIdentity } from "@refinedev/core";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { config, NETWORK_NAME } from "@/config/config";
|
import { config, NETWORK_NAME } from "@/config/config";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
|
|
||||||
type IUser = {
|
type IUser = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: 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 { open } = useNotification();
|
||||||
const { data: user } = useGetIdentity<IUser>();
|
const { data: user } = useGetIdentity<IUser>();
|
||||||
|
|
||||||
// 表单状态
|
const [parametersState, , setFormField] = useControllableObjectState(
|
||||||
const [sensorType, setSensorType] = useState<string>("pressure");
|
state,
|
||||||
const [method, setMethod] = useState<string>("kmeans");
|
onStateChange,
|
||||||
const [sensorCount, setSensorCount] = useState<number>(5);
|
createOptimizationParametersState(),
|
||||||
const [minDiameter, setMinDiameter] = useState<number>(5);
|
|
||||||
const [schemeName, setSchemeName] = useState<string>(
|
|
||||||
"Fangan" + new Date().getTime()
|
|
||||||
);
|
);
|
||||||
|
const { sensorType, method, sensorCount, minDiameter, schemeName } =
|
||||||
|
parametersState;
|
||||||
const [network] = useState<string>(NETWORK_NAME);
|
const [network] = useState<string>(NETWORK_NAME);
|
||||||
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
// 传感器类型选项
|
// 传感器类型选项
|
||||||
const sensorTypeOptions = [
|
const sensorTypeOptions = [
|
||||||
{ value: "pressure", label: "压力" },
|
{ value: "pressure", label: "压力" },
|
||||||
@@ -121,7 +147,7 @@ const OptimizationParameters: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 重置方案名称
|
// 重置方案名称
|
||||||
setSchemeName("Fangan" + new Date().getTime());
|
setFormField("schemeName", "Fangan" + new Date().getTime());
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data?.message || "创建失败");
|
throw new Error(response.data?.message || "创建失败");
|
||||||
}
|
}
|
||||||
@@ -153,7 +179,7 @@ const OptimizationParameters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={sensorType}
|
value={sensorType}
|
||||||
onChange={(e) => setSensorType(e.target.value)}
|
onChange={(e) => setFormField("sensorType", e.target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiOutlinedInput-root": {
|
"& .MuiOutlinedInput-root": {
|
||||||
"&:hover fieldset": {
|
"&:hover fieldset": {
|
||||||
@@ -186,7 +212,7 @@ const OptimizationParameters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={method}
|
value={method}
|
||||||
onChange={(e) => setMethod(e.target.value)}
|
onChange={(e) => setFormField("method", e.target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiOutlinedInput-root": {
|
"& .MuiOutlinedInput-root": {
|
||||||
"&:hover fieldset": {
|
"&:hover fieldset": {
|
||||||
@@ -219,7 +245,9 @@ const OptimizationParameters: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={sensorCount}
|
value={sensorCount}
|
||||||
onChange={(e) => setSensorCount(parseInt(e.target.value) || 0)}
|
onChange={(e) =>
|
||||||
|
setFormField("sensorCount", parseInt(e.target.value) || 0)
|
||||||
|
}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: { min: 1 },
|
htmlInput: { min: 1 },
|
||||||
}}
|
}}
|
||||||
@@ -249,7 +277,9 @@ const OptimizationParameters: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
value={minDiameter}
|
value={minDiameter}
|
||||||
onChange={(e) => setMinDiameter(parseInt(e.target.value) || 0)}
|
onChange={(e) =>
|
||||||
|
setFormField("minDiameter", parseInt(e.target.value) || 0)
|
||||||
|
}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: { min: 0 },
|
htmlInput: { min: 0 },
|
||||||
}}
|
}}
|
||||||
@@ -278,7 +308,7 @@ const OptimizationParameters: React.FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
value={schemeName}
|
value={schemeName}
|
||||||
onChange={(e) => setSchemeName(e.target.value)}
|
onChange={(e) => setFormField("schemeName", e.target.value)}
|
||||||
placeholder="请输入方案名称"
|
placeholder="请输入方案名称"
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiOutlinedInput-root": {
|
"& .MuiOutlinedInput-root": {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { config, NETWORK_NAME } from "@config/config";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { useMap } from "@components/olmap/core/MapComponent";
|
import { useMap } from "@components/olmap/core/MapComponent";
|
||||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||||
|
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
|
||||||
import { GeoJSON } from "ol/format";
|
import { GeoJSON } from "ol/format";
|
||||||
import VectorLayer from "ol/layer/Vector";
|
import VectorLayer from "ol/layer/Vector";
|
||||||
import VectorSource from "ol/source/Vector";
|
import VectorSource from "ol/source/Vector";
|
||||||
@@ -62,19 +63,39 @@ interface SchemeQueryProps {
|
|||||||
onSchemesChange?: (schemes: SchemeRecord[]) => void;
|
onSchemesChange?: (schemes: SchemeRecord[]) => void;
|
||||||
onLocate?: (id: number) => void;
|
onLocate?: (id: number) => void;
|
||||||
network?: string;
|
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> = ({
|
const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||||
schemes: externalSchemes,
|
schemes: externalSchemes,
|
||||||
onSchemesChange,
|
onSchemesChange,
|
||||||
onLocate,
|
onLocate,
|
||||||
network = NETWORK_NAME,
|
network = NETWORK_NAME,
|
||||||
|
state,
|
||||||
|
onStateChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [queryAll, setQueryAll] = useState<boolean>(true);
|
const [queryState, , setQueryField] = useControllableObjectState(
|
||||||
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
|
state,
|
||||||
|
onStateChange,
|
||||||
|
createMonitoringSchemeQueryState(),
|
||||||
|
);
|
||||||
|
const { queryAll, queryDate, expandedId } = queryState;
|
||||||
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
|
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
@@ -228,7 +249,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
|
|
||||||
// 查看详情(展开/收起)
|
// 查看详情(展开/收起)
|
||||||
const handleViewDetails = (id: number) => {
|
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={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={queryAll}
|
checked={queryAll}
|
||||||
onChange={(e) => setQueryAll(e.target.checked)}
|
onChange={(e) => setQueryField("queryAll", e.target.checked)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -264,7 +285,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
value={queryDate}
|
value={queryDate}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value && dayjs.isDayjs(value) && setQueryDate(value)
|
value && dayjs.isDayjs(value) && setQueryField("queryDate", value)
|
||||||
}
|
}
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
disabled={queryAll}
|
disabled={queryAll}
|
||||||
@@ -375,7 +396,8 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setExpandedId(
|
setQueryField(
|
||||||
|
"expandedId",
|
||||||
expandedId === scheme.id ? null : scheme.id,
|
expandedId === scheme.id ? null : scheme.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { act, renderHook } from "@testing-library/react";
|
||||||
|
|
||||||
|
import { useControllableObjectState } from "./useControllableState";
|
||||||
|
|
||||||
|
describe("useControllableObjectState", () => {
|
||||||
|
it("keeps controlled setters stable when the external value changes", () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
({ value }: { value: { count: number } }) =>
|
||||||
|
useControllableObjectState(value, onChange, { count: 0 }),
|
||||||
|
{
|
||||||
|
initialProps: { value: { count: 0 } },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSetValue = result.current[1];
|
||||||
|
const initialSetField = result.current[2];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[2]("count", 1);
|
||||||
|
});
|
||||||
|
expect(onChange).toHaveBeenCalledWith({ count: 1 });
|
||||||
|
|
||||||
|
rerender({ value: { count: 1 } });
|
||||||
|
|
||||||
|
expect(result.current[1]).toBe(initialSetValue);
|
||||||
|
expect(result.current[2]).toBe(initialSetField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not publish unchanged object fields", () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useControllableObjectState({ count: 1 }, onChange, { count: 0 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[2]("count", 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"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);
|
||||||
|
const isControlledRef = useRef(externalValue !== undefined);
|
||||||
|
const onExternalChangeRef = useRef(onExternalChange);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
valueRef.current = value;
|
||||||
|
isControlledRef.current = externalValue !== undefined;
|
||||||
|
onExternalChangeRef.current = onExternalChange;
|
||||||
|
}, [externalValue, onExternalChange, value]);
|
||||||
|
|
||||||
|
const setValue = useCallback(
|
||||||
|
(next: T | ((previous: T) => T)) => {
|
||||||
|
const nextValue =
|
||||||
|
typeof next === "function"
|
||||||
|
? (next as (previous: T) => T)(valueRef.current)
|
||||||
|
: next;
|
||||||
|
|
||||||
|
if (Object.is(valueRef.current, nextValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueRef.current = nextValue;
|
||||||
|
if (!isControlledRef.current) {
|
||||||
|
setInternalValue(nextValue);
|
||||||
|
}
|
||||||
|
onExternalChangeRef.current?.(nextValue);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
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) =>
|
||||||
|
Object.is(previous[key], nextValue)
|
||||||
|
? previous
|
||||||
|
: { ...previous, [key]: nextValue },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [value, setValue, setField] as const;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user