fix(analysis): 统一方案名与接口错误提示
Generic Container CI/CD / test-build-publish (push) Successful in 1m2s
Frontend CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m2s

This commit is contained in:
2026-08-17 18:54:29 +08:00
parent 9e79d52dc8
commit 45def5bba3
12 changed files with 720 additions and 55 deletions
@@ -21,7 +21,15 @@ import { api } from "@/lib/api";
import { NETWORK_NAME } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft";
import { getApiErrorMessage } from "@/lib/apiError";
import { BurstDetectionResult } from "./types";
import {
createSchemeName,
isSchemeNameValid,
normalizeSchemeName,
SCHEME_NAME_MAX_LENGTH,
SCHEME_NAME_PREFIXES,
} from "@utils/schemeName";
interface Props {
onResult: (result: BurstDetectionResult) => void;
@@ -49,7 +57,7 @@ const currentQuarterHour = () => {
export const createBurstDetectionAnalysisParametersState =
(): BurstDetectionAnalysisParametersState => ({
schemeName: `Burst_Detection_${Date.now()}`,
schemeName: createSchemeName(SCHEME_NAME_PREFIXES.burstDetection),
detectionMode: "latest",
targetTime: currentQuarterHour(),
samplingIntervalMinutes: 15,
@@ -94,7 +102,7 @@ export const resolvePressureSamplingInterval = (items: ScadaInfoItem[]) => {
export const buildBurstDetectionRequest = (
parameters: BurstDetectionAnalysisParametersState,
) => ({
scheme_name: parameters.schemeName.trim(),
scheme_name: normalizeSchemeName(parameters.schemeName),
sampling_interval_minutes: parameters.samplingIntervalMinutes,
...(parameters.detectionMode === "historical" && parameters.targetTime
? { target_time: parameters.targetTime.toISOString() }
@@ -164,7 +172,7 @@ const AnalysisParameters: React.FC<Props> = ({
const isValid = useMemo(
() =>
schemeName.trim().length > 0 &&
isSchemeNameValid(schemeName) &&
samplingIntervalValid &&
(detectionMode === "latest" || Boolean(targetTime?.isValid())),
[detectionMode, samplingIntervalValid, schemeName, targetTime],
@@ -203,12 +211,12 @@ const AnalysisParameters: React.FC<Props> = ({
? "目标时刻存在异常信号,请优先复核相关测点。"
: "目标时刻未发现爆管异常。",
});
} catch (error: any) {
} catch (error) {
open?.({
key: "burst-detection-analysis-error",
type: "error",
message: "侦测失败",
description: error?.response?.data?.detail ?? error?.message ?? "请求失败",
description: getApiErrorMessage(error, "爆管侦测请求失败"),
});
} finally {
setRunning(false);
@@ -226,6 +234,13 @@ const AnalysisParameters: React.FC<Props> = ({
value={schemeName}
onChange={(event) => setFormField("schemeName", event.target.value)}
placeholder="请输入方案名称"
error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH}
helperText={
schemeName.trim().length > SCHEME_NAME_MAX_LENGTH
? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符`
: undefined
}
inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }}
fullWidth
size="small"
/>
@@ -27,9 +27,17 @@ import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft";
import { getApiErrorMessage } from "@/lib/apiError";
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
import { BurstLocationResult } from "./types";
import { getBurstLocationErrorNotice } from "./burstLocationError";
import {
createSchemeName,
isSchemeNameValid,
normalizeSchemeName,
SCHEME_NAME_MAX_LENGTH,
SCHEME_NAME_PREFIXES,
} from "@utils/schemeName";
interface Props {
onResult: (result: BurstLocationResult) => void;
@@ -67,7 +75,7 @@ export interface BurstLocationAnalysisParametersState {
export const createBurstLocationAnalysisParametersState =
(): BurstLocationAnalysisParametersState => ({
schemeName: `Burst_Locate_${Date.now()}`,
schemeName: createSchemeName(SCHEME_NAME_PREFIXES.burstLocation),
dataSource: "monitoring",
schemes: [],
selectedSchemeId: "",
@@ -163,12 +171,14 @@ const AnalysisParameters: React.FC<Props> = ({
description: `当前可选爆管分析方案 ${burstSchemes.length}`,
});
}
} catch (error: any) {
} catch (error) {
open?.({
type: "error",
message: "刷新方案失败",
description:
error?.response?.data?.detail ?? error?.message ?? "无法获取爆管分析方案列表",
description: getApiErrorMessage(
error,
"无法获取爆管分析方案列表",
),
});
} finally {
setSchemeLoading(false);
@@ -193,6 +203,7 @@ const AnalysisParameters: React.FC<Props> = ({
};
const isValid = useMemo(() => {
if (!isSchemeNameValid(schemeName)) return false;
if (!Number.isFinite(burstLeakage) || burstLeakage <= 0) return false;
if (!burstStartTime || !burstEndTime) {
return false;
@@ -204,6 +215,7 @@ const AnalysisParameters: React.FC<Props> = ({
return burstStartTime.isBefore(burstEndTime);
}, [
burstLeakage,
schemeName,
burstStartTime,
burstEndTime,
dataSource,
@@ -234,7 +246,7 @@ const AnalysisParameters: React.FC<Props> = ({
`${config.BACKEND_URL}/api/v1/burst-locations`,
{
data_source: dataSource,
scheme_name: schemeName.trim() || undefined,
scheme_name: normalizeSchemeName(schemeName),
burst_leakage: toM3s(burstLeakage, FLOW_DISPLAY_UNIT),
min_dpressure: minDpressure,
basic_pressure: basicPressure,
@@ -270,7 +282,7 @@ const AnalysisParameters: React.FC<Props> = ({
message: "爆管定位成功",
description: `定位到管段: ${(response.data as BurstLocationResult).located_pipe}`,
});
} catch (error: any) {
} catch (error) {
const notice = getBurstLocationErrorNotice(error);
open?.({
key: "burst-location-analysis-error",
@@ -294,6 +306,13 @@ const AnalysisParameters: React.FC<Props> = ({
value={schemeName}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="请输入方案名称"
error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH}
helperText={
schemeName.trim().length > SCHEME_NAME_MAX_LENGTH
? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符`
: undefined
}
inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }}
fullWidth
size="small"
/>
@@ -1,3 +1,5 @@
import { getApiErrorMessage } from "@/lib/apiError";
export interface BurstLocationErrorNotice {
message: string;
description: string;
@@ -6,26 +8,10 @@ export interface BurstLocationErrorNotice {
const DATA_GAP_PATTERN =
/^(爆管压力数据|正常压力数据|爆管流量数据|正常流量数据) 在时间窗内无有效模拟数据: (.+)$/;
const extractErrorDetail = (error: unknown): string => {
const candidate = error as {
message?: string;
response?: { data?: { detail?: unknown } };
};
const detail = candidate?.response?.data?.detail;
if (typeof detail === "string" && detail.trim()) {
return detail.trim();
}
if (typeof candidate?.message === "string" && candidate.message.trim()) {
return candidate.message.trim();
}
return "请求失败";
};
export const getBurstLocationErrorNotice = (
error: unknown,
): BurstLocationErrorNotice => {
const detail = extractErrorDetail(error);
const detail = getApiErrorMessage(error);
const match = detail.match(DATA_GAP_PATTERN);
if (!match) {
@@ -30,9 +30,17 @@ import { api } from "@/lib/api";
import { config, NETWORK_NAME } from "@/config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft";
import { getApiErrorMessage } from "@/lib/apiError";
import { along, lineString, length, toMercator } from "@turf/turf";
import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
import {
createSchemeName,
isSchemeNameValid,
normalizeSchemeName,
SCHEME_NAME_MAX_LENGTH,
SCHEME_NAME_PREFIXES,
} from "@utils/schemeName";
export interface PipePoint {
id: string;
@@ -52,7 +60,7 @@ export const createBurstAnalysisParametersState = (): BurstAnalysisParametersSta
pipePoints: [],
startTime: dayjs(new Date()),
duration: 3600,
schemeName: "FANGAN" + new Date().getTime(),
schemeName: createSchemeName(SCHEME_NAME_PREFIXES.burstAnalysis),
network: NETWORK_NAME,
});
@@ -104,7 +112,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
pipePoints.length > 0 &&
startTime !== null &&
duration > 0 &&
schemeName.trim() !== "";
isSchemeNameValid(schemeName);
// 地图点击选择要素事件处理函数
const handleMapClickSelectFeatures = useCallback(
@@ -333,7 +341,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
burst_id: burst_ID,
burst_size: burst_size,
modify_total_duration: modify_total_duration,
scheme_name: schemeName,
scheme_name: normalizeSchemeName(schemeName),
};
try {
@@ -358,8 +366,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
key: "burst-analysis",
type: "error",
message: "提交分析失败",
description:
error instanceof Error ? error.message : "请检查网络连接或稍后重试",
description: getApiErrorMessage(error, "爆管模拟请求失败"),
});
} finally {
setAnalyzing(false);
@@ -525,6 +532,13 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
value={schemeName}
onChange={(e) => setParameterField("schemeName", e.target.value)}
placeholder="输入方案名称"
error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH}
helperText={
schemeName.trim().length > SCHEME_NAME_MAX_LENGTH
? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符`
: undefined
}
inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }}
/>
</Box>
@@ -30,6 +30,14 @@ import {
} from "@/utils/mapQueryService";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft";
import { getApiErrorMessage } from "@/lib/apiError";
import {
createSchemeName,
isSchemeNameValid,
normalizeSchemeName,
SCHEME_NAME_MAX_LENGTH,
SCHEME_NAME_PREFIXES,
} from "@utils/schemeName";
export interface ContaminantAnalysisParametersState {
schemeName: string;
@@ -42,7 +50,7 @@ export interface ContaminantAnalysisParametersState {
export const createContaminantAnalysisParametersState =
(): ContaminantAnalysisParametersState => ({
schemeName: "WQ_" + new Date().getTime(),
schemeName: createSchemeName(SCHEME_NAME_PREFIXES.contaminantAnalysis),
startTime: dayjs(new Date()),
sourceNode: "",
concentration: 100,
@@ -98,7 +106,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
Boolean(sourceNode) &&
concentration > 0 &&
duration > 0 &&
schemeName.trim() !== ""
isSchemeNameValid(schemeName)
);
}, [network, startTime, sourceNode, concentration, duration, schemeName]);
@@ -240,7 +248,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
concentration,
duration,
pattern: pattern || undefined,
scheme_name: schemeName,
scheme_name: normalizeSchemeName(schemeName),
};
await api.post(`${config.BACKEND_URL}/api/v1/contaminant-simulations`, undefined, {
@@ -259,8 +267,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
key: "contaminant-analysis",
type: "error",
message: "提交分析失败",
description:
error instanceof Error ? error.message : "请检查网络连接或稍后重试",
description: getApiErrorMessage(error, "污染物模拟请求失败"),
});
} finally {
setSubmitting(false);
@@ -391,6 +398,13 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
value={schemeName}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="输入方案名称"
error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH}
helperText={
schemeName.trim().length > SCHEME_NAME_MAX_LENGTH
? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符`
: undefined
}
inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }}
/>
</Box>
@@ -21,8 +21,16 @@ import { api } from "@/lib/api";
import { config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft";
import { getApiErrorMessage } from "@/lib/apiError";
import { LeakageResultDetail } from "./types";
import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units";
import {
createSchemeName,
isSchemeNameValid,
normalizeSchemeName,
SCHEME_NAME_MAX_LENGTH,
SCHEME_NAME_PREFIXES,
} from "@utils/schemeName";
interface Props {
onResult: (result: LeakageResultDetail) => void;
@@ -43,7 +51,7 @@ export interface DMALeakAnalysisParametersState {
export const createDMALeakAnalysisParametersState =
(): DMALeakAnalysisParametersState => ({
schemeName: `DMA_Leak_${Date.now()}`,
schemeName: createSchemeName(SCHEME_NAME_PREFIXES.dmaLeakIdentification),
dmaCount: 5,
startTime: dayjs().subtract(2, "hour"),
endTime: dayjs(),
@@ -93,7 +101,7 @@ const AnalysisParameters: React.FC<Props> = ({
qSumInput.trim() !== "" && Number.isFinite(parsedQSum) && parsedQSum >= 0;
const isValid = useMemo(() => {
if (!schemeName.trim() || !startTime || !endTime) return false;
if (!isSchemeNameValid(schemeName) || !startTime || !endTime) return false;
return startTime.isBefore(endTime) && qSumIsValid;
}, [schemeName, startTime, endTime, qSumIsValid]);
@@ -118,7 +126,7 @@ const AnalysisParameters: React.FC<Props> = ({
const response = await api.post(
`${config.BACKEND_URL}/api/v1/leakage-identifications`,
{
scheme_name: schemeName.trim(),
scheme_name: normalizeSchemeName(schemeName),
dma_count: dmaCount,
scada_start: startTime.toISOString(),
scada_end: endTime.toISOString(),
@@ -136,12 +144,12 @@ const AnalysisParameters: React.FC<Props> = ({
message: "方案分析成功",
description: "DMA 漏损识别完成,请在方案查询中查看结果。",
});
} catch (error: any) {
} catch (error) {
open?.({
key: "dma-leak-analysis-error",
type: "error",
message: "提交分析失败",
description: error?.response?.data?.detail ?? "请求失败",
description: getApiErrorMessage(error, "DMA 漏损识别请求失败"),
});
} finally {
setRunning(false);
@@ -163,6 +171,13 @@ const AnalysisParameters: React.FC<Props> = ({
value={schemeName}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="请输入方案名称"
error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH}
helperText={
schemeName.trim().length > SCHEME_NAME_MAX_LENGTH
? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符`
: undefined
}
inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }}
fullWidth
size="small"
/>
@@ -36,7 +36,15 @@ import { api } from "@/lib/api";
import { config } from "@/config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft";
import { getApiErrorMessage } from "@/lib/apiError";
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
import {
createSchemeName,
isSchemeNameValid,
normalizeSchemeName,
SCHEME_NAME_MAX_LENGTH,
SCHEME_NAME_PREFIXES,
} from "@utils/schemeName";
import {
type LinkStatus,
getValveSettingHelperText,
@@ -65,7 +73,7 @@ export interface FlushingAnalysisParametersState {
export const createFlushingAnalysisParametersState =
(): FlushingAnalysisParametersState => ({
schemeName: "Flushing_" + new Date().getTime(),
schemeName: createSchemeName(SCHEME_NAME_PREFIXES.flushingAnalysis),
valves: [],
drainageNode: null,
startTime: dayjs(new Date()),
@@ -334,6 +342,22 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
open?.({
type: "error",
message: `阀门 ${valve.id} 的部分属性读取失败`,
description: [
propertiesResult.status === "rejected"
? `阀门属性:${getApiErrorMessage(
propertiesResult.reason,
"读取失败",
)}`
: null,
statusResult.status === "rejected"
? `开关状态:${getApiErrorMessage(
statusResult.reason,
"读取失败",
)}`
: null,
]
.filter(Boolean)
.join(""),
});
}
})
@@ -400,7 +424,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
};
const handleAnalyze = async () => {
if (!startTime || !drainageNode || !schemeName.trim()) {
if (!startTime || !drainageNode || !isSchemeNameValid(schemeName)) {
open?.({
type: "error",
message: "请填写完整参数",
@@ -442,7 +466,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
const formattedTime = startTime.format("YYYY-MM-DDTHH:mm:00Z"); // ISO format with seconds set to 00
const params = {
scheme_name: schemeName,
scheme_name: normalizeSchemeName(schemeName),
start_time: formattedTime,
...(valves.length > 0 && {
valves: valves.map(v => v.id),
@@ -480,7 +504,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
open?.({
type: "error",
message: "提交分析失败",
description: error instanceof Error ? error.message : "未知错误",
description: getApiErrorMessage(error, "管道冲洗分析请求失败"),
});
} finally {
setAnalyzing(false);
@@ -668,6 +692,13 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
value={schemeName}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="请输入方案名称"
error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH}
helperText={
schemeName.trim().length > SCHEME_NAME_MAX_LENGTH
? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符`
: undefined
}
inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }}
/>
</Box>
@@ -710,7 +741,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
onClick={handleAnalyze}
disabled={
analyzing ||
!schemeName.trim() ||
!isSchemeNameValid(schemeName) ||
!drainageNode ||
!startTime ||
// !flushFlow ||
@@ -12,8 +12,16 @@ import { PlayArrow as PlayArrowIcon } from "@mui/icons-material";
import { useNotification } from "@refinedev/core";
import { NETWORK_NAME } from "@/config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { getApiErrorMessage } from "@/lib/apiError";
import { optimizeSensorPlacement } from "./schemeApi";
import type { SensorPlacementScheme } from "./types";
import {
createSchemeName,
isSchemeNameValid,
normalizeSchemeName,
SCHEME_NAME_MAX_LENGTH,
SCHEME_NAME_PREFIXES,
} from "@utils/schemeName";
export interface OptimizationParametersState {
method: string;
@@ -27,7 +35,7 @@ export const createOptimizationParametersState =
method: "kmeans",
sensorCount: 5,
minDiameter: 5,
schemeName: "Fangan" + new Date().getTime(),
schemeName: createSchemeName(SCHEME_NAME_PREFIXES.sensorPlacement),
});
interface OptimizationParametersProps {
@@ -62,7 +70,7 @@ const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
// 创建方案
const handleCreateScheme = async () => {
// 验证输入
if (!schemeName.trim()) {
if (!isSchemeNameValid(schemeName)) {
open?.({
type: "error",
message: "请输入方案名称",
@@ -90,7 +98,7 @@ const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
try {
const created = await optimizeSensorPlacement({
scheme_name: schemeName,
scheme_name: normalizeSchemeName(schemeName),
sensor_type: "pressure",
method: method as "sensitivity" | "kmeans",
sensor_count: sensorCount,
@@ -102,14 +110,16 @@ const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
description: `方案 "${schemeName}" 已完成优化分析`,
});
onSchemeCreated?.(created);
setFormField("schemeName", "Fangan" + new Date().getTime());
} catch (error: any) {
setFormField(
"schemeName",
createSchemeName(SCHEME_NAME_PREFIXES.sensorPlacement),
);
} catch (error) {
console.error("创建方案失败:", error);
open?.({
type: "error",
message: "创建方案失败",
description:
error.response?.data?.message || error.message || "未知错误",
description: getApiErrorMessage(error, "监测点优化请求失败"),
});
} finally {
setAnalyzing(false);
@@ -255,6 +265,13 @@ const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
value={schemeName}
onChange={(e) => setFormField("schemeName", e.target.value)}
placeholder="请输入方案名称"
error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH}
helperText={
schemeName.trim().length > SCHEME_NAME_MAX_LENGTH
? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符`
: undefined
}
inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }}
sx={{
"& .MuiOutlinedInput-root": {
"&:hover fieldset": {