实现DMA漏损识别面板整体设计

This commit is contained in:
JIANG
2026-03-06 09:59:06 +08:00
parent b73481d604
commit 377fc32f4c
10 changed files with 1096 additions and 68 deletions
@@ -0,0 +1,260 @@
"use client";
import React, { useMemo, useState } from "react";
import {
Alert,
Box,
Button,
Collapse,
TextField,
Typography,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config";
import { LeakageResultDetail } from "./types";
interface Props {
onResult: (result: LeakageResultDetail) => void;
}
const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
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 [endTime, setEndTime] = useState<Dayjs | null>(dayjs());
const [popSize, setPopSize] = useState<number>(50);
const [maxGen, setMaxGen] = useState<number>(100);
const [qSum, setQSum] = useState<number>(0.4);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [running, setRunning] = useState(false);
const isValid = useMemo(() => {
if (!schemeName.trim() || !startTime || !endTime) return false;
return startTime.isBefore(endTime) && qSum >= 0.1;
}, [schemeName, startTime, endTime, qSum]);
const handleRun = async () => {
if (!isValid || !startTime || !endTime) {
open?.({ type: "error", message: "请完善参数并确认时间范围合法" });
return;
}
setRunning(true);
open?.({
key: "dma-leak-analysis",
type: "progress",
message: "方案提交分析中",
undoableTimeout: 3,
});
try {
const response = await api.post(
`${config.BACKEND_URL}/api/v1/leakage/identify/`,
{
network: NETWORK_NAME,
scheme_name: schemeName.trim(),
dma_count: dmaCount,
scada_start: startTime.toISOString(),
scada_end: endTime.toISOString(),
pop_size: popSize,
max_gen: maxGen,
q_sum: qSum,
q_sum_unit: "m3/s",
output_flow_unit: "m3/s",
},
);
onResult(response.data as LeakageResultDetail);
open?.({
key: "dma-leak-analysis",
type: "success",
message: "方案分析成功",
description: "DMA漏损识别完成,请在方案查询中查看结果。",
});
} catch (error: any) {
open?.({
key: "dma-leak-analysis",
type: "error",
message: "提交分析失败",
description: error?.response?.data?.detail ?? "请求失败",
});
} finally {
setRunning(false);
}
};
return (
<Box className="flex flex-col flex-1 min-h-0">
<Box className="flex flex-col gap-3">
<Alert severity="info">
DMA DMA
</Alert>
<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">
DMA
</Typography>
<TextField
type="number"
value={dmaCount}
onChange={(e) => {
const value = Number.parseInt(e.target.value, 10);
setDmaCount(Number.isNaN(value) ? 5 : Math.max(3, value));
}}
fullWidth
size="small"
inputProps={{ min: 3, step: 1 }}
/>
</Box>
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale="zh-cn"
localeText={
pickerZhCN.components.MuiLocalizationProvider.defaultProps.localeText
}
>
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
SCADA
</Typography>
<DateTimePicker
value={startTime}
onChange={setStartTime}
maxDateTime={endTime ?? undefined}
format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }}
/>
</Box>
<Box>
<Typography variant="subtitle2" className="mb-1 font-medium">
SCADA
</Typography>
<DateTimePicker
value={endTime}
onChange={setEndTime}
minDateTime={startTime ?? undefined}
format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }}
/>
</Box>
</LocalizationProvider>
<Box className="flex flex-col gap-2">
<Typography variant="subtitle2" className="mb-1 font-medium">
(m3/s)
</Typography>
<TextField
type="number"
size="small"
value={qSum}
onChange={(e) => {
const value = Number(e.target.value);
setQSum(Number.isNaN(value) ? 0.4 : Math.max(0.1, value));
}}
inputProps={{ min: 0.1, step: 0.1 }}
/>
<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="grid grid-cols-2 gap-2">
<TextField
type="number"
label="种群规模"
size="small"
value={popSize}
onChange={(e) => setPopSize(Number(e.target.value))}
/>
<TextField
type="number"
label="最大代数"
size="small"
value={maxGen}
onChange={(e) => setMaxGen(Number(e.target.value))}
/>
</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;