270 lines
8.6 KiB
TypeScript
270 lines
8.6 KiB
TypeScript
"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";
|
||
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
|
||
|
||
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>(10);
|
||
const [maxGen, setMaxGen] = useState<number>(50);
|
||
const [qSum, setQSum] = useState<number>(1440);
|
||
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 >= 360;
|
||
}, [schemeName, startTime, endTime, qSum]);
|
||
|
||
const handleRun = async () => {
|
||
if (!isValid || !startTime || !endTime) {
|
||
open?.({ type: "error", message: "请完善参数并确认时间范围合法" });
|
||
return;
|
||
}
|
||
setRunning(true);
|
||
open?.({
|
||
key: "dma-leak-analysis-progress",
|
||
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: toM3s(qSum, FLOW_DISPLAY_UNIT),
|
||
q_sum_unit: "m3/s",
|
||
output_flow_unit: FLOW_DISPLAY_UNIT,
|
||
},
|
||
);
|
||
onResult(response.data as LeakageResultDetail);
|
||
open?.({
|
||
key: "dma-leak-analysis-success",
|
||
type: "success",
|
||
message: "方案分析成功",
|
||
description: "DMA 漏损识别完成,请在方案查询中查看结果。",
|
||
});
|
||
} catch (error: any) {
|
||
open?.({
|
||
key: "dma-leak-analysis-error",
|
||
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);
|
||
// Limit between 3 and 10
|
||
if (Number.isNaN(value)) {
|
||
setDmaCount(5);
|
||
} else if (value > 10) {
|
||
setDmaCount(10);
|
||
} else {
|
||
setDmaCount(Math.max(3, value));
|
||
}
|
||
}}
|
||
fullWidth
|
||
size="small"
|
||
inputProps={{ min: 3, max: 10, step: 1 }}
|
||
helperText="DMA 数量限制为 3-10 个"
|
||
/>
|
||
</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">
|
||
总漏损流量 ({FLOW_DISPLAY_UNIT})
|
||
</Typography>
|
||
<TextField
|
||
type="number"
|
||
size="small"
|
||
value={qSum}
|
||
onChange={(e) => {
|
||
const value = Number(e.target.value);
|
||
setQSum(Number.isNaN(value) ? 1440 : Math.max(360, value));
|
||
}}
|
||
inputProps={{ min: 360, 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="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;
|