Files
TJWaterFrontend_Refine/src/components/olmap/BurstLocation/AnalysisParameters.tsx
T
2026-03-12 11:40:37 +08:00

440 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useCallback, useMemo, useState } from "react";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
Box,
Button,
CircularProgress,
Collapse,
FormControl,
MenuItem,
Select,
TextField,
Typography,
IconButton,
} from "@mui/material";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales";
import { useNotification } from "@refinedev/core";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn";
import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config";
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
import { BurstLocationResult } from "./types";
interface Props {
onResult: (result: BurstLocationResult) => void;
}
interface SchemeItem {
scheme_id: number;
scheme_name: string;
scheme_type: string;
create_time: string;
scheme_start_time: string;
scheme_detail?: {
modify_total_duration: number;
};
}
type DataSource = "monitoring" | "simulation";
const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
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 [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";
const applySchemeTimeRange = useCallback((scheme: SchemeItem) => {
const start = dayjs(scheme.scheme_start_time);
const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600;
const end = start.add(durationSeconds, "second");
setBurstStartTime(start);
setBurstEndTime(end);
}, []);
const fetchSchemes = useCallback(
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
if (schemeLoading || (!force && schemes.length > 0)) return;
setSchemeLoading(true);
try {
const response = await api.get(`${config.BACKEND_URL}/api/v1/getallschemes/`, {
params: { network: NETWORK_NAME },
});
const burstSchemes = (response.data as SchemeItem[]).filter(
(scheme) => scheme.scheme_type === "burst_analysis",
);
setSchemes(burstSchemes);
if (selectedSchemeId) {
const matchedScheme = burstSchemes.find(
(scheme) => scheme.scheme_id === selectedSchemeId,
);
if (matchedScheme) {
applySchemeTimeRange(matchedScheme);
} else {
setSelectedSchemeId("");
}
}
if (notify) {
open?.({
type: "success",
message: "方案列表已刷新",
description: `当前可选爆管分析方案 ${burstSchemes.length}`,
});
}
} catch (error: any) {
open?.({
type: "error",
message: "刷新方案失败",
description:
error?.response?.data?.detail ?? error?.message ?? "无法获取爆管分析方案列表",
});
} finally {
setSchemeLoading(false);
}
},
[applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId],
);
const handleDataSourceChange = (value: DataSource) => {
setDataSource(value);
if (value === "simulation") {
void fetchSchemes();
}
};
const handleSchemeSelect = (schemeId: number) => {
setSelectedSchemeId(schemeId);
const scheme = schemes.find((item) => item.scheme_id === schemeId);
if (scheme) {
applySchemeTimeRange(scheme);
}
};
const isValid = useMemo(() => {
if (!Number.isFinite(burstLeakage) || burstLeakage <= 0) return false;
if (!burstStartTime || !burstEndTime) {
return false;
}
if (dataSource === "simulation" && !selectedSchemeId) {
return false;
}
return burstStartTime.isBefore(burstEndTime);
}, [
burstLeakage,
burstStartTime,
burstEndTime,
dataSource,
selectedSchemeId,
]);
const handleRun = async () => {
if (!isValid || !burstStartTime || !burstEndTime) {
open?.({ type: "error", message: "请完善参数并确认时间范围合法" });
return;
}
setRunning(true);
open?.({
key: "burst-location-analysis-progress",
type: "progress",
message: "方案提交分析中",
undoableTimeout: 3,
});
try {
const selectedScheme =
dataSource === "simulation"
? schemes.find((item) => item.scheme_id === selectedSchemeId)
: undefined;
const response = await api.post(
`${config.BACKEND_URL}/api/v1/burst-location/locate/`,
{
network: NETWORK_NAME,
data_source: dataSource,
scheme_name: schemeName.trim() || undefined,
burst_leakage: toM3s(burstLeakage, FLOW_DISPLAY_UNIT),
min_dpressure: minDpressure,
basic_pressure: basicPressure,
scada_burst_start: burstStartTime.toISOString(),
scada_burst_end: burstEndTime.toISOString(),
use_scada_flow: enableFlow || undefined,
simulation_scheme_name: selectedScheme?.scheme_name,
simulation_scheme_type: selectedScheme?.scheme_type,
},
);
onResult(response.data as BurstLocationResult);
open?.({
key: "burst-location-analysis-success",
type: "success",
message: "爆管定位成功",
description: `定位到管段: ${(response.data as BurstLocationResult).located_pipe}`,
});
} catch (error: any) {
open?.({
key: "burst-location-analysis-error",
type: "error",
message: "提交分析失败",
description: error?.response?.data?.detail ?? error?.message ?? "请求失败",
});
} finally {
setRunning(false);
}
};
return (
<Box className="flex flex-col flex-1 min-h-0">
<Box className="flex flex-col gap-3">
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
</Typography>
<TextField
value={schemeName}
onChange={(e) => setSchemeName(e.target.value)}
placeholder="请输入方案名称"
fullWidth
size="small"
/>
</Box>
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
SCADA
</Typography>
<FormControl fullWidth size="small">
<Select
value={dataSource}
onChange={(e) => handleDataSourceChange(e.target.value as DataSource)}
>
<MenuItem value="monitoring"></MenuItem>
<MenuItem value="simulation"></MenuItem>
</Select>
</FormControl>
</Box>
{isSimulationMode && (
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<FormControl fullWidth size="small">
<Select
value={selectedSchemeId}
onChange={(e) => handleSchemeSelect(Number(e.target.value))}
disabled={schemeLoading}
displayEmpty
>
<MenuItem value="" disabled>
</MenuItem>
{schemes.map((scheme) => (
<MenuItem key={scheme.scheme_id} value={scheme.scheme_id}>
{scheme.scheme_name}
</MenuItem>
))}
</Select>
</FormControl>
<IconButton
size="small"
color="primary"
onClick={() => void fetchSchemes({ force: true, notify: true })}
disabled={schemeLoading}
aria-label="刷新爆管分析方案"
sx={{
border: "1px solid",
borderColor: "divider",
borderRadius: 1,
}}
>
{schemeLoading ? (
<CircularProgress size={18} color="inherit" />
) : (
<RefreshIcon fontSize="small" />
)}
</IconButton>
</Box>
</Box>
)}
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale="zh-cn"
localeText={
pickerZhCN.components.MuiLocalizationProvider.defaultProps.localeText
}
>
<Box className="grid grid-cols-2 gap-2">
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
</Typography>
<DateTimePicker
value={burstStartTime}
onChange={setBurstStartTime}
maxDateTime={burstEndTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }}
/>
</Box>
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
</Typography>
<DateTimePicker
value={burstEndTime}
onChange={setBurstEndTime}
minDateTime={burstStartTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }}
/>
</Box>
</Box>
</LocalizationProvider>
<Box className="flex flex-col gap-2">
<Typography variant="subtitle2" className="mb-1 font-medium">
({FLOW_DISPLAY_UNIT})
</Typography>
<TextField
type="number"
size="small"
value={burstLeakage}
onChange={(e) => {
const value = Number(e.target.value);
setBurstLeakage(Number.isNaN(value) ? 1440 : Math.max(0, value));
}}
fullWidth
inputProps={{ min: 0, step: 10 }}
/>
<Box
sx={{
border: "1px solid",
borderColor: "grey.200",
borderRadius: 1,
overflow: "hidden",
}}
>
<Box
role="button"
tabIndex={0}
onClick={() => setAdvancedOpen((prev) => !prev)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setAdvancedOpen((prev) => !prev);
}}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
px: 1.25,
py: 0.75,
cursor: "pointer",
backgroundColor: "transparent",
"&:hover": { backgroundColor: "action.hover" },
}}
>
<Typography variant="body2" color="text.secondary">
</Typography>
<ExpandMoreIcon
sx={{
transform: advancedOpen ? "rotate(180deg)" : "rotate(0deg)",
transition: "transform 0.2s ease",
}}
/>
</Box>
<Collapse in={advancedOpen} timeout="auto" unmountOnExit>
<Box
sx={{
px: 1.25,
pt: 1.25,
pb: 1.25,
backgroundColor: "transparent",
}}
>
<Box className="flex flex-col gap-3">
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
</Typography>
<FormControl fullWidth size="small">
<Select
value={enableFlow ? "enabled" : "disabled"}
onChange={(e) => setEnableFlow(e.target.value === "enabled")}
>
<MenuItem value="disabled"></MenuItem>
<MenuItem value="enabled">使</MenuItem>
</Select>
</FormControl>
</Box>
<Box className="grid grid-cols-2 gap-2">
<TextField
type="number"
label="最小压降 (m)"
size="small"
value={minDpressure}
onChange={(e) => setMinDpressure(Number(e.target.value))}
/>
<TextField
type="number"
label="基础压力 (m)"
size="small"
value={basicPressure}
onChange={(e) => setBasicPressure(Number(e.target.value))}
/>
</Box>
</Box>
</Box>
</Collapse>
</Box>
</Box>
</Box>
<Box className="mt-auto pt-3">
<Button
fullWidth
variant="contained"
onClick={handleRun}
disabled={!isValid || running}
className="bg-blue-600 hover:bg-blue-700"
>
{running ? "定位中..." : "开始定位"}
</Button>
</Box>
</Box>
);
};
export default AnalysisParameters;