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