fix(api): align frontend with REST contracts
This commit is contained in:
@@ -322,16 +322,15 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
|
||||
: "";
|
||||
const modify_total_duration = duration;
|
||||
const params = {
|
||||
network: network,
|
||||
modify_pattern_start_time: modify_pattern_start_time,
|
||||
burst_ID: burst_ID,
|
||||
burst_id: burst_ID,
|
||||
burst_size: burst_size,
|
||||
modify_total_duration: modify_total_duration,
|
||||
scheme_name: schemeName,
|
||||
};
|
||||
|
||||
try {
|
||||
await api.get(`${config.BACKEND_URL}/api/v1/burst-analysis`, {
|
||||
await api.post(`${config.BACKEND_URL}/api/v1/burst-analyses`, undefined, {
|
||||
params,
|
||||
paramsSerializer: {
|
||||
indexes: null, // 移除数组索引,即由 burst_ID[] 变为 burst_ID
|
||||
|
||||
@@ -29,9 +29,12 @@ import {
|
||||
PrintOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { NETWORK_NAME } from "@config/config";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import {
|
||||
clearPipeDiameterCache,
|
||||
getCachedPipeDiameters,
|
||||
getPipeDiameterDisplay,
|
||||
getPipeDiameterQueryKey,
|
||||
loadPipeDiameters,
|
||||
type PipeDiameterMap,
|
||||
} from "./schemePipeDiameters";
|
||||
import { SchemeRecord, ValveIsolationResult } from "./types";
|
||||
@@ -54,66 +57,7 @@ const REPORT_BLUE = "#0b4f87";
|
||||
const REPORT_INK = "#172435";
|
||||
const REPORT_MUTED = "#5f6f80";
|
||||
const REPORT_LINE = "#d8e0e8";
|
||||
const diameterCache = new Map<string, PipeDiameterMap>();
|
||||
const diameterRequestCache = new Map<string, Promise<PipeDiameterMap>>();
|
||||
|
||||
export const clearAnalysisReportDiameterCache = () => {
|
||||
diameterCache.clear();
|
||||
diameterRequestCache.clear();
|
||||
};
|
||||
|
||||
const loadPipeDiameters = async (
|
||||
pipeIds: string[],
|
||||
queryKey: string,
|
||||
): Promise<PipeDiameterMap> => {
|
||||
const cachedDiameters = diameterCache.get(queryKey);
|
||||
if (cachedDiameters) {
|
||||
return cachedDiameters;
|
||||
}
|
||||
|
||||
const cachedRequest = diameterRequestCache.get(queryKey);
|
||||
if (cachedRequest) {
|
||||
return cachedRequest;
|
||||
}
|
||||
|
||||
const request = (async () => {
|
||||
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
|
||||
const foundIds = new Set(
|
||||
features.map((feature) => String(feature.getProperties().id)),
|
||||
);
|
||||
const missingIds = pipeIds.filter((pipeId) => !foundIds.has(pipeId));
|
||||
if (missingIds.length) {
|
||||
features = [
|
||||
...features,
|
||||
...(await queryFeaturesByIds(missingIds, "geo_pipes")),
|
||||
];
|
||||
}
|
||||
|
||||
const diameters: PipeDiameterMap = Object.fromEntries(
|
||||
pipeIds.map((pipeId) => [pipeId, null]),
|
||||
);
|
||||
features.forEach((feature) => {
|
||||
const properties = feature.getProperties();
|
||||
const pipeId = String(properties.id);
|
||||
const diameter = Number(properties.diameter);
|
||||
if (pipeIds.includes(pipeId)) {
|
||||
diameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
|
||||
}
|
||||
});
|
||||
|
||||
diameterCache.set(queryKey, diameters);
|
||||
return diameters;
|
||||
})();
|
||||
|
||||
diameterRequestCache.set(queryKey, request);
|
||||
try {
|
||||
return await request;
|
||||
} finally {
|
||||
if (diameterRequestCache.get(queryKey) === request) {
|
||||
diameterRequestCache.delete(queryKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
export const clearAnalysisReportDiameterCache = clearPipeDiameterCache;
|
||||
|
||||
const formatDateTime = (value: Date | string | null | undefined) => {
|
||||
if (!value) return "未记录";
|
||||
@@ -411,8 +355,8 @@ const AnalysisReport: React.FC<AnalysisReportProps> = ({
|
||||
() => scheme?.schemeDetail?.burst_ID ?? [],
|
||||
[scheme],
|
||||
);
|
||||
const diameterQueryKey = pipeIds.join("\u0000");
|
||||
const cachedDiameters = diameterCache.get(diameterQueryKey);
|
||||
const diameterQueryKey = getPipeDiameterQueryKey(pipeIds);
|
||||
const cachedDiameters = getCachedPipeDiameters(pipeIds);
|
||||
const [diameterState, setDiameterState] = useState<{
|
||||
queryKey: string | null;
|
||||
values: PipeDiameterMap;
|
||||
@@ -435,13 +379,13 @@ const AnalysisReport: React.FC<AnalysisReportProps> = ({
|
||||
if (!pipeIds.length) {
|
||||
return;
|
||||
}
|
||||
if (diameterCache.has(diameterQueryKey)) {
|
||||
if (getCachedPipeDiameters(pipeIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
loadPipeDiameters(pipeIds, diameterQueryKey)
|
||||
loadPipeDiameters(pipeIds)
|
||||
.then((nextDiameters) => {
|
||||
if (!cancelled) {
|
||||
setDiameterState({
|
||||
|
||||
@@ -53,7 +53,9 @@ import { toLonLat } from "ol/proj";
|
||||
import Timeline from "@components/olmap/core/Controls/Timeline";
|
||||
import { SchemaItem, SchemeRecord } from "./types";
|
||||
import {
|
||||
getCachedPipeDiameters,
|
||||
getPipeDiameterDisplay,
|
||||
loadPipeDiameters,
|
||||
type PipeDiameterMap,
|
||||
} from "./schemePipeDiameters";
|
||||
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
|
||||
@@ -160,7 +162,6 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
network,
|
||||
scheme_type: SCHEME_TYPE,
|
||||
};
|
||||
if (!queryAll && queryDate) {
|
||||
@@ -251,40 +252,23 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedDiameters = getCachedPipeDiameters(pipeIds);
|
||||
if (cachedDiameters) {
|
||||
setPipeDiametersByScheme((previous) => ({
|
||||
...previous,
|
||||
[expandedId]: cachedDiameters,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoadingDiameterByScheme((previous) => ({
|
||||
...previous,
|
||||
[expandedId]: true,
|
||||
}));
|
||||
|
||||
const loadPipeDiameters = async () => {
|
||||
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
|
||||
const foundPipeIds = new Set(
|
||||
features.map((feature) => String(feature.getProperties().id)),
|
||||
);
|
||||
const missingPipeIds = pipeIds.filter(
|
||||
(pipeId) => !foundPipeIds.has(pipeId),
|
||||
);
|
||||
|
||||
if (missingPipeIds.length > 0) {
|
||||
const fallbackFeatures = await queryFeaturesByIds(
|
||||
missingPipeIds,
|
||||
"geo_pipes",
|
||||
);
|
||||
features = [...features, ...fallbackFeatures];
|
||||
}
|
||||
|
||||
const nextDiameters: PipeDiameterMap = Object.fromEntries(
|
||||
pipeIds.map((pipeId) => [pipeId, null]),
|
||||
);
|
||||
|
||||
features.forEach((feature) => {
|
||||
const properties = feature.getProperties();
|
||||
const pipeId = String(properties.id);
|
||||
const diameter = Number(properties.diameter);
|
||||
nextDiameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
|
||||
});
|
||||
|
||||
const applyPipeDiameters = async () => {
|
||||
const nextDiameters = await loadPipeDiameters(pipeIds);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
@@ -299,7 +283,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
|
||||
}));
|
||||
};
|
||||
|
||||
loadPipeDiameters().catch((error) => {
|
||||
applyPipeDiameters().catch((error) => {
|
||||
console.error("查询管径失败:", error);
|
||||
if (!cancelled) {
|
||||
setLoadingDiameterByScheme((previous) => ({
|
||||
|
||||
@@ -361,14 +361,14 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
|
||||
}
|
||||
try {
|
||||
const params: any = {
|
||||
network: NETWORK_NAME,
|
||||
accident_element: ids,
|
||||
};
|
||||
if (disabled.length > 0) {
|
||||
params.disabled_valves = disabled;
|
||||
}
|
||||
const response = await api.get<ValveIsolationResult>(
|
||||
`${config.BACKEND_URL}/api/v1/valve-isolation-analysis`,
|
||||
const response = await api.post<ValveIsolationResult>(
|
||||
`${config.BACKEND_URL}/api/v1/valve-isolation-analyses`,
|
||||
undefined,
|
||||
{
|
||||
params,
|
||||
paramsSerializer: {
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
import { getPipeDiameterDisplay } from "./schemePipeDiameters";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import {
|
||||
clearPipeDiameterCache,
|
||||
getPipeDiameterDisplay,
|
||||
loadPipeDiameters,
|
||||
} from "./schemePipeDiameters";
|
||||
|
||||
jest.mock("@/utils/mapQueryService", () => ({
|
||||
queryFeaturesByIds: jest.fn(),
|
||||
}));
|
||||
|
||||
const feature = (id: string, diameter: number) => ({
|
||||
getProperties: () => ({ id, diameter }),
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearPipeDiameterCache();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("loadPipeDiameters", () => {
|
||||
it("shares an in-flight WFS request and reuses its resolved result", async () => {
|
||||
let resolveFeatures: ((features: ReturnType<typeof feature>[]) => void) | undefined;
|
||||
const pendingFeatures = new Promise<ReturnType<typeof feature>[]>((resolve) => {
|
||||
resolveFeatures = resolve;
|
||||
});
|
||||
(queryFeaturesByIds as jest.Mock).mockImplementation(
|
||||
async () => pendingFeatures,
|
||||
);
|
||||
|
||||
const firstRequest = loadPipeDiameters(["P-1"]);
|
||||
const duplicateRequest = loadPipeDiameters(["P-1"]);
|
||||
|
||||
expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveFeatures?.([feature("P-1", 315)]);
|
||||
await expect(firstRequest).resolves.toEqual({ "P-1": 315 });
|
||||
await expect(duplicateRequest).resolves.toEqual({ "P-1": 315 });
|
||||
|
||||
await expect(loadPipeDiameters(["P-1"])).resolves.toEqual({
|
||||
"P-1": 315,
|
||||
});
|
||||
expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPipeDiameterDisplay", () => {
|
||||
it("shows the actual diameter for one burst pipe", () => {
|
||||
|
||||
@@ -1,5 +1,94 @@
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import config from "@config/config";
|
||||
|
||||
export type PipeDiameterMap = Record<string, number | null | undefined>;
|
||||
|
||||
const diameterCache = new Map<string, PipeDiameterMap>();
|
||||
const diameterRequestCache = new Map<string, Promise<PipeDiameterMap>>();
|
||||
|
||||
const normalizePipeIds = (pipeIds: string[]) =>
|
||||
[...new Set(pipeIds.map((pipeId) => String(pipeId).trim()).filter(Boolean))];
|
||||
|
||||
export const getPipeDiameterQueryKey = (pipeIds: string[]) =>
|
||||
[
|
||||
config.MAP_URL,
|
||||
config.MAP_WORKSPACE,
|
||||
...normalizePipeIds(pipeIds).sort(),
|
||||
].join("\u0000");
|
||||
|
||||
export const getCachedPipeDiameters = (pipeIds: string[]) =>
|
||||
diameterCache.get(getPipeDiameterQueryKey(pipeIds));
|
||||
|
||||
export const clearPipeDiameterCache = () => {
|
||||
diameterCache.clear();
|
||||
diameterRequestCache.clear();
|
||||
};
|
||||
|
||||
export const loadPipeDiameters = async (
|
||||
pipeIds: string[],
|
||||
): Promise<PipeDiameterMap> => {
|
||||
const normalizedPipeIds = normalizePipeIds(pipeIds);
|
||||
if (normalizedPipeIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const queryKey = getPipeDiameterQueryKey(normalizedPipeIds);
|
||||
const cachedDiameters = diameterCache.get(queryKey);
|
||||
if (cachedDiameters) {
|
||||
return cachedDiameters;
|
||||
}
|
||||
|
||||
const cachedRequest = diameterRequestCache.get(queryKey);
|
||||
if (cachedRequest) {
|
||||
return cachedRequest;
|
||||
}
|
||||
|
||||
const request = (async () => {
|
||||
let features = await queryFeaturesByIds(
|
||||
normalizedPipeIds,
|
||||
"geo_pipes_mat",
|
||||
);
|
||||
const foundPipeIds = new Set(
|
||||
features.map((feature) => String(feature.getProperties().id)),
|
||||
);
|
||||
const missingPipeIds = normalizedPipeIds.filter(
|
||||
(pipeId) => !foundPipeIds.has(pipeId),
|
||||
);
|
||||
|
||||
if (missingPipeIds.length > 0) {
|
||||
const fallbackFeatures = await queryFeaturesByIds(
|
||||
missingPipeIds,
|
||||
"geo_pipes",
|
||||
);
|
||||
features = [...features, ...fallbackFeatures];
|
||||
}
|
||||
|
||||
const diameters: PipeDiameterMap = Object.fromEntries(
|
||||
normalizedPipeIds.map((pipeId) => [pipeId, null]),
|
||||
);
|
||||
features.forEach((feature) => {
|
||||
const properties = feature.getProperties();
|
||||
const pipeId = String(properties.id);
|
||||
const diameter = Number(properties.diameter);
|
||||
if (pipeId in diameters) {
|
||||
diameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
|
||||
}
|
||||
});
|
||||
|
||||
diameterCache.set(queryKey, diameters);
|
||||
return diameters;
|
||||
})();
|
||||
|
||||
diameterRequestCache.set(queryKey, request);
|
||||
try {
|
||||
return await request;
|
||||
} finally {
|
||||
if (diameterRequestCache.get(queryKey) === request) {
|
||||
diameterRequestCache.delete(queryKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getPipeDiameterDisplay = (
|
||||
pipeIds: string[] | undefined,
|
||||
diameters: PipeDiameterMap | undefined,
|
||||
|
||||
Reference in New Issue
Block a user