fix(analysis): preserve tab panel state
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user