refactor(frontend): align TJWater Next integrations

This commit is contained in:
2026-08-26 12:26:29 +08:00
parent 6623f4f7fb
commit fee4fc6ce1
51 changed files with 3074 additions and 22280 deletions
+3 -3
View File
@@ -7,8 +7,8 @@ NEXTAUTH_URL="https://frontend.example.com/"
BACKEND_URL="https://server.example.com" BACKEND_URL="https://server.example.com"
AGENT_URL="https://agent.example.com" AGENT_URL="https://agent.example.com"
MAP_URL="https://geoserver.example.com/geoserver" MAP_URL="https://geoserver.example.com/geoserver"
MAP_WORKSPACE="tjwater" MAP_WORKSPACE="tjwater_next"
MAP_EXTENT="13490131,3630016,13525879,3666968.25" MAP_EXTENT="13508801.93,3608163.35,13555650.64,3633685.14"
NETWORK_NAME="tjwater" NETWORK_NAME="tjwater_next"
MAPBOX_TOKEN="replace-with-public-mapbox-token" MAPBOX_TOKEN="replace-with-public-mapbox-token"
TIANDITU_TOKEN="replace-with-public-tianditu-token" TIANDITU_TOKEN="replace-with-public-tianditu-token"
+1 -1
View File
@@ -7,7 +7,7 @@
}, },
"server": { "server": {
"file": "server-v1.openapi.json", "file": "server-v1.openapi.json",
"sha256": "ac9b6fac185dfd999f1791cba51eb482df17a427b361963250aafa5fb1a276b4" "sha256": "404a196c0177faed2aa5b46ee86430a034dfe990e0a77a43428a727748a882b6"
} }
} }
} }
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -9,22 +9,22 @@ loadEnvConfig(projectDir, process.env.NODE_ENV !== "production");
const parseExtent = (value) => { const parseExtent = (value) => {
if (!value) { if (!value) {
return [13508849, 3608036, 13555781, 3633813]; return [13508801.93, 3608163.35, 13555650.64, 3633685.14];
} }
const extent = value.split(",").map(Number); const extent = value.split(",").map(Number);
return extent.length === 4 && extent.every(Number.isFinite) return extent.length === 4 && extent.every(Number.isFinite)
? extent ? extent
: [13508849, 3608036, 13555781, 3633813]; : [13508801.93, 3608163.35, 13555650.64, 3633685.14];
}; };
const config = { const config = {
BACKEND_URL: process.env.BACKEND_URL || "http://127.0.0.1:8000", BACKEND_URL: process.env.BACKEND_URL || "http://127.0.0.1:8000",
AGENT_URL: process.env.AGENT_URL || "http://127.0.0.1:8788", AGENT_URL: process.env.AGENT_URL || "http://127.0.0.1:8788",
MAP_URL: process.env.MAP_URL || "http://127.0.0.1:8080/geoserver", MAP_URL: process.env.MAP_URL || "http://127.0.0.1:8080/geoserver",
MAP_WORKSPACE: process.env.MAP_WORKSPACE || "tjwater", MAP_WORKSPACE: process.env.MAP_WORKSPACE || "tjwater_next",
MAP_EXTENT: parseExtent(process.env.MAP_EXTENT), MAP_EXTENT: parseExtent(process.env.MAP_EXTENT),
NETWORK_NAME: process.env.NETWORK_NAME || "tjwater", NETWORK_NAME: process.env.NETWORK_NAME || "tjwater_next",
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN || "", MAPBOX_TOKEN: process.env.MAPBOX_TOKEN || "",
TIANDITU_TOKEN: process.env.TIANDITU_TOKEN || "", TIANDITU_TOKEN: process.env.TIANDITU_TOKEN || "",
}; };
+12 -12
View File
@@ -45,12 +45,12 @@ type ToolMeta = {
const LOCATE_TOOL_TO_LAYER: Record<string, string> = { const LOCATE_TOOL_TO_LAYER: Record<string, string> = {
locate_features: "", locate_features: "",
locate_junctions: "geo_junctions_mat", locate_junctions: "junctions",
locate_pipes: "geo_pipes_mat", locate_pipes: "pipes",
locate_valves: "geo_valves", locate_valves: "valves",
locate_reservoirs: "geo_reservoirs", locate_reservoirs: "reservoirs",
locate_pumps: "geo_pumps", locate_pumps: "pumps",
locate_tanks: "geo_tanks", locate_tanks: "tanks",
}; };
const LOCATE_LINE_TOOLS = new Set<string>(["locate_pipes"]); const LOCATE_LINE_TOOLS = new Set<string>(["locate_pipes"]);
@@ -672,22 +672,22 @@ export const ChatToolCallBlock: React.FC<ChatToolCallBlockProps> = ({
switch (featureType) { switch (featureType) {
case "junction": case "junction":
case "junctions": case "junctions":
return { layer: "geo_junctions_mat", geometryKind: "point" }; return { layer: "junctions", geometryKind: "point" };
case "pipe": case "pipe":
case "pipes": case "pipes":
return { layer: "geo_pipes_mat", geometryKind: "line" }; return { layer: "pipes", geometryKind: "line" };
case "valve": case "valve":
case "valves": case "valves":
return { layer: "geo_valves", geometryKind: "point" }; return { layer: "valves", geometryKind: "point" };
case "reservoir": case "reservoir":
case "reservoirs": case "reservoirs":
return { layer: "geo_reservoirs", geometryKind: "point" }; return { layer: "reservoirs", geometryKind: "point" };
case "pump": case "pump":
case "pumps": case "pumps":
return { layer: "geo_pumps", geometryKind: "point" }; return { layer: "pumps", geometryKind: "point" };
case "tank": case "tank":
case "tanks": case "tanks":
return { layer: "geo_tanks", geometryKind: "point" }; return { layer: "tanks", geometryKind: "point" };
default: default:
return null; return null;
} }
@@ -22,30 +22,30 @@ const FEATURE_TYPE_MAP: Record<
string, string,
{ layer: string; geometryKind: "point" | "line"; label: string } { layer: string; geometryKind: "point" | "line"; label: string }
> = { > = {
junction: { layer: "geo_junctions_mat", geometryKind: "point", label: "节点" }, junction: { layer: "junctions", geometryKind: "point", label: "节点" },
junctions: { layer: "geo_junctions_mat", geometryKind: "point", label: "节点" }, junctions: { layer: "junctions", geometryKind: "point", label: "节点" },
pipe: { layer: "geo_pipes_mat", geometryKind: "line", label: "管道" }, pipe: { layer: "pipes", geometryKind: "line", label: "管道" },
pipes: { layer: "geo_pipes_mat", geometryKind: "line", label: "管道" }, pipes: { layer: "pipes", geometryKind: "line", label: "管道" },
valve: { layer: "geo_valves", geometryKind: "point", label: "阀门" }, valve: { layer: "valves", geometryKind: "point", label: "阀门" },
valves: { layer: "geo_valves", geometryKind: "point", label: "阀门" }, valves: { layer: "valves", geometryKind: "point", label: "阀门" },
reservoir: { layer: "geo_reservoirs", geometryKind: "point", label: "水源" }, reservoir: { layer: "reservoirs", geometryKind: "point", label: "水源" },
reservoirs: { layer: "geo_reservoirs", geometryKind: "point", label: "水源" }, reservoirs: { layer: "reservoirs", geometryKind: "point", label: "水源" },
pump: { layer: "geo_pumps", geometryKind: "point", label: "泵站" }, pump: { layer: "pumps", geometryKind: "point", label: "泵站" },
pumps: { layer: "geo_pumps", geometryKind: "point", label: "泵站" }, pumps: { layer: "pumps", geometryKind: "point", label: "泵站" },
tank: { layer: "geo_tanks", geometryKind: "point", label: "水池" }, tank: { layer: "tanks", geometryKind: "point", label: "水池" },
tanks: { layer: "geo_tanks", geometryKind: "point", label: "水池" }, tanks: { layer: "tanks", geometryKind: "point", label: "水池" },
}; };
const LOCATE_TOOL_CONFIG: Record< const LOCATE_TOOL_CONFIG: Record<
string, string,
{ layer: string; geometryKind: "point" | "line"; label: string } { layer: string; geometryKind: "point" | "line"; label: string }
> = { > = {
locate_pipes: { layer: "geo_pipes_mat", geometryKind: "line", label: "管道" }, locate_pipes: { layer: "pipes", geometryKind: "line", label: "管道" },
locate_junctions: { layer: "geo_junctions_mat", geometryKind: "point", label: "节点" }, locate_junctions: { layer: "junctions", geometryKind: "point", label: "节点" },
locate_valves: { layer: "geo_valves", geometryKind: "point", label: "阀门" }, locate_valves: { layer: "valves", geometryKind: "point", label: "阀门" },
locate_reservoirs: { layer: "geo_reservoirs", geometryKind: "point", label: "水源" }, locate_reservoirs: { layer: "reservoirs", geometryKind: "point", label: "水源" },
locate_pumps: { layer: "geo_pumps", geometryKind: "point", label: "泵站" }, locate_pumps: { layer: "pumps", geometryKind: "point", label: "泵站" },
locate_tanks: { layer: "geo_tanks", geometryKind: "point", label: "水池" }, locate_tanks: { layer: "tanks", geometryKind: "point", label: "水池" },
}; };
const LOCATE_ID_PARAM_KEYS = [ const LOCATE_ID_PARAM_KEYS = [
@@ -179,10 +179,7 @@ const DetectionResults: React.FC<Props> = ({ result, state, onStateChange }) =>
const locateSensors = async (sensorIds: string[]) => { const locateSensors = async (sensorIds: string[]) => {
if (!map || sensorIds.length === 0) return; if (!map || sensorIds.length === 0) return;
let features = await queryFeaturesByIds(sensorIds, "geo_junctions_mat"); const features = await queryFeaturesByIds(sensorIds, "junctions");
if (features.length === 0) {
features = await queryFeaturesByIds(sensorIds, "geo_junctions");
}
if (features.length === 0) return; if (features.length === 0) return;
setHighlightFeatures(features); setHighlightFeatures(features);
const format = new GeoJSON(); const format = new GeoJSON();
@@ -21,7 +21,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api"; import { getAnalysisScheme, listAnalysisSchemes } from "@/lib/analysisRuns";
import { NETWORK_NAME } from "@config/config"; import { NETWORK_NAME } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState"; import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
@@ -42,7 +42,7 @@ interface Props {
export interface BurstDetectionSchemeQueryState { export interface BurstDetectionSchemeQueryState {
queryAll: boolean; queryAll: boolean;
queryDate: Dayjs | null; queryDate: Dayjs | null;
expandedId: number | null; expandedId: string | null;
hasQueried: boolean; hasQueried: boolean;
} }
@@ -130,15 +130,12 @@ const SchemeQuery: React.FC<Props> = ({
const handleQuery = async () => { const handleQuery = async () => {
setLoading(true); setLoading(true);
try { try {
const params: Record<string, string> = { const nextSchemes = (await listAnalysisSchemes({
scheme_type: "burst_detection", runType: "burst_detection",
}; queryDate: !queryAll && queryDate
if (!queryAll && queryDate) { ? queryDate.format("YYYY-MM-DD")
params.query_date = queryDate.startOf("day").toISOString(); : undefined,
} })) as BurstDetectionSchemeRecord[];
const response = await api.get("/api/v1/schemes", { params });
const nextSchemes = response.data as BurstDetectionSchemeRecord[];
setSchemes(nextSchemes); setSchemes(nextSchemes);
setQueryField("hasQueried", true); setQueryField("hasQueried", true);
open?.({ open?.({
@@ -157,13 +154,9 @@ const SchemeQuery: React.FC<Props> = ({
} }
}; };
const handleViewSchemeResult = async (schemeName: string) => { const handleViewSchemeResult = async (runId: string) => {
try { try {
const response = await api.get( const schemeRecord = (await getAnalysisScheme(runId)) as BurstDetectionSchemeRecord & {
`/api/v1/schemes/${encodeURIComponent(schemeName)}`,
{ params: { scheme_type: "burst_detection" } },
);
const schemeRecord = response.data as BurstDetectionSchemeRecord & {
result_payload?: BurstDetectionResult; result_payload?: BurstDetectionResult;
}; };
const normalizedResult = const normalizedResult =
@@ -185,7 +178,7 @@ const SchemeQuery: React.FC<Props> = ({
open?.({ open?.({
type: "success", type: "success",
message: "方案加载成功", message: "方案加载成功",
description: `已加载方案:${schemeName}`, description: `已加载方案:${schemeRecord.scheme_name}`,
}); });
} catch (error: any) { } catch (error: any) {
open?.({ open?.({
@@ -398,7 +391,7 @@ const SchemeQuery: React.FC<Props> = ({
size="small" size="small"
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
sx={{ textTransform: "none", fontWeight: 500 }} sx={{ textTransform: "none", fontWeight: 500 }}
onClick={() => handleViewSchemeResult(scheme.scheme_name)} onClick={() => handleViewSchemeResult(scheme.scheme_id)}
> >
</Button> </Button>
+1 -1
View File
@@ -97,7 +97,7 @@ export interface BurstDetectionSchemeDetail {
} }
export interface BurstDetectionSchemeRecord { export interface BurstDetectionSchemeRecord {
scheme_id: number; scheme_id: string;
scheme_name: string; scheme_name: string;
scheme_type?: string; scheme_type?: string;
create_time: string; create_time: string;
@@ -24,6 +24,7 @@ import { useNotification } from "@refinedev/core";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { listAnalysisSchemes } from "@/lib/analysisRuns";
import { NETWORK_NAME, config } from "@config/config"; import { NETWORK_NAME, config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft"; import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft";
@@ -46,7 +47,7 @@ interface Props {
} }
export interface SchemeItem { export interface SchemeItem {
scheme_id: number; scheme_id: string;
scheme_name: string; scheme_name: string;
scheme_type: string; scheme_type: string;
create_time: string; create_time: string;
@@ -63,7 +64,7 @@ export interface BurstLocationAnalysisParametersState {
schemeName: string; schemeName: string;
dataSource: DataSource; dataSource: DataSource;
schemes: SchemeItem[]; schemes: SchemeItem[];
selectedSchemeId: number | ""; selectedSchemeId: string;
burstLeakage: number; burstLeakage: number;
enableFlow: boolean; enableFlow: boolean;
burstStartTime: Dayjs | null; burstStartTime: Dayjs | null;
@@ -141,12 +142,9 @@ const AnalysisParameters: React.FC<Props> = ({
setSchemeLoading(true); setSchemeLoading(true);
try { try {
const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, { const burstSchemes = (await listAnalysisSchemes({
params: { scheme_type: "burst_analysis" }, runType: "burst_analysis",
}); }) as unknown as SchemeItem[]).sort(
const burstSchemes = (response.data as SchemeItem[]).filter(
(scheme) => scheme.scheme_type === "burst_analysis",
).sort(
(a, b) => (a, b) =>
dayjs(b.create_time).valueOf() - dayjs(a.create_time).valueOf(), dayjs(b.create_time).valueOf() - dayjs(a.create_time).valueOf(),
); );
@@ -194,7 +192,7 @@ const AnalysisParameters: React.FC<Props> = ({
} }
}; };
const handleSchemeSelect = (schemeId: number) => { const handleSchemeSelect = (schemeId: string) => {
setFormField("selectedSchemeId", schemeId); setFormField("selectedSchemeId", schemeId);
const scheme = schemes.find((item) => item.scheme_id === schemeId); const scheme = schemes.find((item) => item.scheme_id === schemeId);
if (scheme) { if (scheme) {
@@ -253,8 +251,7 @@ const AnalysisParameters: React.FC<Props> = ({
scada_burst_start: burstStartTime.toISOString(), scada_burst_start: burstStartTime.toISOString(),
scada_burst_end: burstEndTime.toISOString(), scada_burst_end: burstEndTime.toISOString(),
use_scada_flow: enableFlow || undefined, use_scada_flow: enableFlow || undefined,
simulation_scheme_name: selectedScheme?.scheme_name, simulation_run_id: selectedScheme?.scheme_id,
simulation_scheme_type: selectedScheme?.scheme_type,
}, },
); );
@@ -370,7 +367,7 @@ const AnalysisParameters: React.FC<Props> = ({
<FormControl fullWidth size="small"> <FormControl fullWidth size="small">
<Select <Select
value={selectedSchemeId} value={selectedSchemeId}
onChange={(e) => handleSchemeSelect(Number(e.target.value))} onChange={(e) => handleSchemeSelect(String(e.target.value))}
disabled={schemeLoading} disabled={schemeLoading}
displayEmpty displayEmpty
> >
@@ -202,10 +202,7 @@ const LocationResults: React.FC<Props> = ({ result }) => {
if (!pipeIds.length || !map) return; if (!pipeIds.length || !map) return;
try { try {
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat"); const features = await queryFeaturesByIds(pipeIds, "pipes");
if (features.length === 0) {
features = await queryFeaturesByIds(pipeIds, "geo_pipes");
}
if (features.length === 0) return; if (features.length === 0) return;
setHighlightFeatures(features); setHighlightFeatures(features);
@@ -25,8 +25,8 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api"; import { getAnalysisScheme, listAnalysisSchemes } from "@/lib/analysisRuns";
import { NETWORK_NAME, config } from "@config/config"; import { NETWORK_NAME } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useMap } from "@components/olmap/core/MapComponent"; import { useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { queryFeaturesByIds } from "@/utils/mapQueryService";
@@ -56,7 +56,7 @@ interface Props {
export interface BurstLocationSchemeQueryState { export interface BurstLocationSchemeQueryState {
queryAll: boolean; queryAll: boolean;
queryDate: Dayjs | null; queryDate: Dayjs | null;
expandedId: number | null; expandedId: string | null;
simulationBurstIdsByName: Record<string, string[]>; simulationBurstIdsByName: Record<string, string[]>;
hasQueried: boolean; hasQueried: boolean;
} }
@@ -147,10 +147,7 @@ const SchemeQuery: React.FC<Props> = ({
if (!uniquePipeIds.length || !map) return; if (!uniquePipeIds.length || !map) return;
try { try {
let features = await queryFeaturesByIds(uniquePipeIds, "geo_pipes_mat"); const features = await queryFeaturesByIds(uniquePipeIds, "pipes");
if (features.length === 0) {
features = await queryFeaturesByIds(uniquePipeIds, "geo_pipes");
}
if (features.length === 0) return; if (features.length === 0) return;
setHighlightFeatures(features); setHighlightFeatures(features);
@@ -227,23 +224,17 @@ const SchemeQuery: React.FC<Props> = ({
const handleQuery = async () => { const handleQuery = async () => {
setLoading(true); setLoading(true);
try { try {
const params: Record<string, string> = { const [nextSchemes, simulationSchemes] = await Promise.all([
scheme_type: "burst_location", listAnalysisSchemes({
}; runType: "burst_location",
if (!queryAll && queryDate) { queryDate: !queryAll && queryDate
params.query_date = queryDate.startOf("day").toISOString(); ? queryDate.format("YYYY-MM-DD")
} : undefined,
const [response, simulationResponse] = await Promise.all([
api.get(`${config.BACKEND_URL}/api/v1/schemes`, { params }),
api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
params: { scheme_type: "burst_analysis" },
}), }),
listAnalysisSchemes({ runType: "burst_analysis" }),
]); ]);
const nextSchemes = response.data as BurstSchemeRecord[];
const nextSimulationBurstIdsByName = Object.fromEntries( const nextSimulationBurstIdsByName = Object.fromEntries(
(simulationResponse.data as BurstSimulationSchemeItem[]) (simulationSchemes as BurstSimulationSchemeItem[])
.filter((scheme) => scheme.scheme_type === "burst_analysis")
.map((scheme) => [ .map((scheme) => [
scheme.scheme_name, scheme.scheme_name,
normalizeBurstIds(scheme.scheme_detail?.burst_ID), normalizeBurstIds(scheme.scheme_detail?.burst_ID),
@@ -253,7 +244,7 @@ const SchemeQuery: React.FC<Props> = ({
setSchemes( setSchemes(
nextSchemes.map((scheme) => nextSchemes.map((scheme) =>
enrichSchemeWithSimulationBurstIds( enrichSchemeWithSimulationBurstIds(
scheme, scheme as BurstSchemeRecord,
nextSimulationBurstIdsByName, nextSimulationBurstIdsByName,
), ),
), ),
@@ -276,13 +267,9 @@ const SchemeQuery: React.FC<Props> = ({
} }
}; };
const handleViewSchemeResult = async (schemeName: string) => { const handleViewSchemeResult = async (runId: string) => {
try { try {
const response = await api.get( const schemeRecord = (await getAnalysisScheme(runId)) as BurstSchemeRecord & {
`${config.BACKEND_URL}/api/v1/schemes/${encodeURIComponent(schemeName)}`,
{ params: { scheme_type: "burst_location" } },
);
const schemeRecord = response.data as BurstSchemeRecord & {
result_payload?: BurstLocationResult; result_payload?: BurstLocationResult;
}; };
const normalizedResult = const normalizedResult =
@@ -302,7 +289,7 @@ const SchemeQuery: React.FC<Props> = ({
open?.({ open?.({
type: "success", type: "success",
message: "方案加载成功", message: "方案加载成功",
description: `已加载方案: ${schemeName}`, description: `已加载方案: ${schemeRecord.scheme_name}`,
}); });
} catch (error: any) { } catch (error: any) {
open?.({ open?.({
@@ -516,7 +503,7 @@ const SchemeQuery: React.FC<Props> = ({
size="small" size="small"
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
sx={{ textTransform: "none", fontWeight: 500 }} sx={{ textTransform: "none", fontWeight: 500 }}
onClick={() => handleViewSchemeResult(scheme.scheme_name)} onClick={() => handleViewSchemeResult(scheme.scheme_id)}
> >
</Button> </Button>
+1 -1
View File
@@ -66,7 +66,7 @@ export interface BurstLocationSchemeDetail {
} }
export interface BurstSchemeRecord { export interface BurstSchemeRecord {
scheme_id: number; scheme_id: string;
scheme_name: string; scheme_name: string;
scheme_type?: string; scheme_type?: string;
create_time: string; create_time: string;
@@ -124,7 +124,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
if (!feature) return; if (!feature) return;
if ( if (
feature.getGeometry()?.getType() === "Point" || feature.getGeometry()?.getType() === "Point" ||
(layer !== "geo_pipes_mat" && layer !== "geo_pipes") layer !== "pipes"
) { ) {
open?.({ open?.({
type: "error", type: "error",
@@ -248,7 +248,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
if (highlightFeatures.length > 0 || pipePoints.length === 0) return; if (highlightFeatures.length > 0 || pipePoints.length === 0) return;
queryFeaturesByIds( queryFeaturesByIds(
pipePoints.map((pipe) => pipe.id), pipePoints.map((pipe) => pipe.id),
"geo_pipes_mat", "pipes",
).then((features) => { ).then((features) => {
if (features.length > 0) { if (features.length > 0) {
setHighlightFeatures(features); setHighlightFeatures(features);
@@ -21,7 +21,7 @@ jest.mock("@/utils/mapQueryService", () => ({
})); }));
const scheme: SchemeRecord = { const scheme: SchemeRecord = {
id: 17, id: "17",
schemeName: "burst-report-demo", schemeName: "burst-report-demo",
type: "burst_analysis", type: "burst_analysis",
username: "operator", username: "operator",
@@ -52,10 +52,12 @@ const feature = (id: string, diameter: number) => ({
describe("AnalysisReport", () => { describe("AnalysisReport", () => {
beforeEach(() => { beforeEach(() => {
(queryFeaturesByIds as jest.Mock).mockImplementation( (queryFeaturesByIds as jest.Mock).mockImplementation(
async (_ids: string[], layerName: string) => async (ids: string[], layerName: string) =>
layerName === "geo_pipes_mat" layerName === "pipes"
? [feature("P-1", 315)] ? ids.map((id) =>
: [feature("P-2", 800)], id === "P-1" ? feature(id, 315) : feature(id, 800),
)
: [],
); );
}); });
@@ -109,8 +111,8 @@ describe("AnalysisReport", () => {
expect(preview.getByText("800 mm")).toBeInTheDocument(); expect(preview.getByText("800 mm")).toBeInTheDocument();
}); });
expect(queryFeaturesByIds).toHaveBeenCalledWith( expect(queryFeaturesByIds).toHaveBeenCalledWith(
["P-2"], ["P-1", "P-2"],
"geo_pipes", "pipes",
); );
}); });
@@ -263,7 +265,7 @@ describe("AnalysisReport", () => {
), ),
).toBeInTheDocument(), ).toBeInTheDocument(),
); );
expect(queryFeaturesByIds).toHaveBeenCalledTimes(2); expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
firstRender.unmount(); firstRender.unmount();
render(<AnalysisReport {...props} />); render(<AnalysisReport {...props} />);
@@ -273,6 +275,6 @@ describe("AnalysisReport", () => {
"800 mm", "800 mm",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(queryFeaturesByIds).toHaveBeenCalledTimes(2); expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
}); });
}); });
@@ -27,9 +27,9 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/zh-cn"; // 引入中文包 import "dayjs/locale/zh-cn"; // 引入中文包
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { api } from "@/lib/api"; import { listAnalysisSchemes } from "@/lib/analysisRuns";
import moment from "moment"; import moment from "moment";
import { config, NETWORK_NAME } from "@config/config"; import { NETWORK_NAME } from "@config/config";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useControllableObjectState } from "@components/olmap/core/useControllableState";
@@ -51,7 +51,7 @@ import {
import { Point } from "ol/geom"; import { Point } from "ol/geom";
import { toLonLat } from "ol/proj"; import { toLonLat } from "ol/proj";
import Timeline from "@components/olmap/core/Controls/Timeline"; import Timeline from "@components/olmap/core/Controls/Timeline";
import { SchemaItem, SchemeRecord } from "./types"; import { SchemeRecord } from "./types";
import { import {
getCachedPipeDiameters, getCachedPipeDiameters,
getPipeDiameterDisplay, getPipeDiameterDisplay,
@@ -78,7 +78,7 @@ export interface BurstSchemeQueryState {
showTimeline: boolean; showTimeline: boolean;
selectedDate: Date | undefined; selectedDate: Date | undefined;
timeRange: { start: Date; end: Date } | undefined; timeRange: { start: Date; end: Date } | undefined;
expandedId: number | null; expandedId: string | null;
hasQueried: boolean; hasQueried: boolean;
} }
@@ -123,17 +123,17 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const creatorName = useSchemeCreatorName(); const creatorName = useSchemeCreatorName();
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素 const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素
const [pipeDiametersByScheme, setPipeDiametersByScheme] = useState< const [pipeDiametersByScheme, setPipeDiametersByScheme] = useState<
Record<number, PipeDiameterMap> Record<string, PipeDiameterMap>
>({}); >({});
const [loadingDiameterByScheme, setLoadingDiameterByScheme] = useState< const [loadingDiameterByScheme, setLoadingDiameterByScheme] = useState<
Record<number, boolean> Record<string, boolean>
>({}); >({});
const { open } = useNotification(); const { open } = useNotification();
const map = useMap(); const map = useMap();
const data = useData(); const data = useData();
const { schemeName, setSchemeName } = data || {}; const { schemeName, setSchemeName, schemeRunId, setSchemeRunId } = data || {};
// 使用外部提供的 schemes 或内部状态 // 使用外部提供的 schemes 或内部状态
const schemes = const schemes =
@@ -161,38 +161,16 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true); setLoading(true);
try { try {
const params: Record<string, string> = { const nextSchemes = (await listAnalysisSchemes({
scheme_type: SCHEME_TYPE, runType: SCHEME_TYPE,
}; queryDate: !queryAll && queryDate
if (!queryAll && queryDate) { ? queryDate.format("YYYY-MM-DD")
params.query_date = queryDate.startOf("day").toISOString(); : undefined,
} })) as unknown as SchemeRecord[];
const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
params,
});
let filteredResults = response.data;
if (!queryAll) {
const formattedDate = queryDate!.format("YYYY-MM-DD");
filteredResults = response.data.filter((item: SchemaItem) => {
const itemDate = moment(item.create_time).format("YYYY-MM-DD");
return itemDate === formattedDate;
});
}
const nextSchemes = filteredResults.map((item: SchemaItem) => ({
id: item.scheme_id,
schemeName: item.scheme_name,
type: item.scheme_type,
username: item.username,
create_time: item.create_time,
startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail,
}));
setSchemes(nextSchemes); setSchemes(nextSchemes);
setQueryField("hasQueried", true); setQueryField("hasQueried", true);
if (filteredResults.length === 0) { if (nextSchemes.length === 0) {
open?.({ open?.({
type: "success", type: "success",
message: "查询结果", message: "查询结果",
@@ -204,7 +182,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
open?.({ open?.({
type: "success", type: "success",
message: "查询成功", message: "查询成功",
description: `共找到 ${filteredResults.length} 条方案记录`, description: `共找到 ${nextSchemes.length} 条方案记录`,
}); });
} }
} catch (error) { } catch (error) {
@@ -221,7 +199,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const handleLocatePipes = (pipeIds: string[]) => { const handleLocatePipes = (pipeIds: string[]) => {
if (pipeIds.length > 0) { if (pipeIds.length > 0) {
queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => { queryFeaturesByIds(pipeIds, "pipes").then((features) => {
if (features.length > 0) { if (features.length > 0) {
// 设置高亮要素 // 设置高亮要素
setHighlightFeatures(features); setHighlightFeatures(features);
@@ -299,7 +277,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}, [expandedId, filteredSchemes, pipeDiametersByScheme]); }, [expandedId, filteredSchemes, pipeDiametersByScheme]);
// 内部的方案查询函数 // 内部的方案查询函数
const handleViewDetails = (id: number) => { const handleViewDetails = (id: string) => {
const scheme = filteredSchemes.find((s) => s.id === id); const scheme = filteredSchemes.find((s) => s.id === id);
if (!scheme) return; if (!scheme) return;
@@ -321,6 +299,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
})); }));
} }
setSchemeName?.(scheme.schemeName); setSchemeName?.(scheme.schemeName);
setSchemeRunId?.(scheme.id);
handleLocatePipes(scheme.schemeDetail?.burst_ID || []); handleLocatePipes(scheme.schemeDetail?.burst_ID || []);
}; };
@@ -438,6 +417,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
timeRange={timeRange} timeRange={timeRange}
disableDateSelection={!!timeRange} disableDateSelection={!!timeRange}
schemeName={schemeName} schemeName={schemeName}
schemeRunId={schemeRunId}
schemeType={SCHEME_TYPE} schemeType={SCHEME_TYPE}
/>, />,
mapContainer, // 渲染到地图容器中,而不是 body mapContainer, // 渲染到地图容器中,而不是 body
@@ -210,7 +210,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
useEffect(() => { useEffect(() => {
if (!selectedPipeId || highlightFeature) return; if (!selectedPipeId || highlightFeature) return;
queryFeaturesByIds([selectedPipeId], "geo_pipes_mat").then((features) => { queryFeaturesByIds([selectedPipeId], "pipes").then((features) => {
setHighlightFeature(features[0] ?? null); setHighlightFeature(features[0] ?? null);
}); });
}, [highlightFeature, selectedPipeId]); }, [highlightFeature, selectedPipeId]);
@@ -245,7 +245,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
const handleLocatePipes = (pipeIds: string[], highlight: boolean = true) => { const handleLocatePipes = (pipeIds: string[], highlight: boolean = true) => {
if (pipeIds.length > 0) { if (pipeIds.length > 0) {
queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => { queryFeaturesByIds(pipeIds, "pipes").then((features) => {
if (features.length > 0) { if (features.length > 0) {
if (highlight) { if (highlight) {
// 设置高亮类型为管段 // 设置高亮类型为管段
@@ -270,7 +270,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
const handleLocateNodes = (nodeIds: string[]) => { const handleLocateNodes = (nodeIds: string[]) => {
if (nodeIds.length > 0) { if (nodeIds.length > 0) {
queryFeaturesByIds(nodeIds, "geo_junctions").then((features) => { queryFeaturesByIds(nodeIds, "junctions").then((features) => {
if (features.length > 0) { if (features.length > 0) {
// 设置高亮类型为受影响节点 // 设置高亮类型为受影响节点
setHighlightType("affected_node"); setHighlightType("affected_node");
@@ -294,7 +294,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
const handleLocateMustCloseValves = (valveIds: string[]) => { const handleLocateMustCloseValves = (valveIds: string[]) => {
if (valveIds.length > 0) { if (valveIds.length > 0) {
queryFeaturesByIds(valveIds, "geo_valves").then((features) => { queryFeaturesByIds(valveIds, "valves").then((features) => {
if (features.length > 0) { if (features.length > 0) {
// 设置高亮类型为必关阀门 // 设置高亮类型为必关阀门
setHighlightType("must_close"); setHighlightType("must_close");
@@ -318,7 +318,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
const handleLocateOptionalValves = (valveIds: string[]) => { const handleLocateOptionalValves = (valveIds: string[]) => {
if (valveIds.length > 0) { if (valveIds.length > 0) {
queryFeaturesByIds(valveIds, "geo_valves").then((features) => { queryFeaturesByIds(valveIds, "valves").then((features) => {
if (features.length > 0) { if (features.length > 0) {
// 设置高亮类型为可选阀门 // 设置高亮类型为可选阀门
setHighlightType("optional"); setHighlightType("optional");
@@ -411,7 +411,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
setSelectedPipeId(initialPipeIds[0]); setSelectedPipeId(initialPipeIds[0]);
// 尝试获取Feature以高亮 (可选) // 尝试获取Feature以高亮 (可选)
queryFeaturesByIds(initialPipeIds, "geo_pipes_mat").then((features) => { queryFeaturesByIds(initialPipeIds, "pipes").then((features) => {
if (features && features.length > 0) { if (features && features.length > 0) {
setHighlightFeature(features[0]); setHighlightFeature(features[0]);
} }
@@ -44,24 +44,10 @@ export const loadPipeDiameters = async (
} }
const request = (async () => { const request = (async () => {
let features = await queryFeaturesByIds( const features = await queryFeaturesByIds(
normalizedPipeIds, normalizedPipeIds,
"geo_pipes_mat", "pipes",
); );
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( const diameters: PipeDiameterMap = Object.fromEntries(
normalizedPipeIds.map((pipeId) => [pipeId, null]), normalizedPipeIds.map((pipeId) => [pipeId, null]),
+1 -11
View File
@@ -8,7 +8,7 @@ export interface SchemeDetail {
} }
export interface SchemeRecord { export interface SchemeRecord {
id: number; id: string;
schemeName: string; schemeName: string;
type: string; type: string;
username: string; username: string;
@@ -18,16 +18,6 @@ export interface SchemeRecord {
schemeDetail?: SchemeDetail; schemeDetail?: SchemeDetail;
} }
export interface SchemaItem {
scheme_id: number;
scheme_name: string;
scheme_type: string;
username: string;
create_time: string;
scheme_start_time: string;
scheme_detail?: SchemeDetail;
}
export interface ValveIsolationResult { export interface ValveIsolationResult {
accident_elements: string[]; accident_elements: string[];
affected_nodes: string[]; affected_nodes: string[];
@@ -202,7 +202,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
return; return;
} }
if (highlightFeature) return; if (highlightFeature) return;
queryFeaturesByIds([sourceNode], "geo_junctions_mat").then((features) => { queryFeaturesByIds([sourceNode], "junctions").then((features) => {
setHighlightFeature(features[0] ?? null); setHighlightFeature(features[0] ?? null);
}); });
}, [highlightFeature, sourceNode]); }, [highlightFeature, sourceNode]);
@@ -25,10 +25,10 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { api } from "@/lib/api"; import { listAnalysisSchemes } from "@/lib/analysisRuns";
import moment from "moment"; import moment from "moment";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { config, NETWORK_NAME } from "@config/config"; import { NETWORK_NAME } from "@config/config";
import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { useData, useMap } from "@components/olmap/core/MapComponent"; import { useData, useMap } from "@components/olmap/core/MapComponent";
@@ -39,7 +39,7 @@ import { Style, Icon, Circle, Fill, Stroke } from "ol/style";
import Feature from "ol/Feature"; import Feature from "ol/Feature";
import { bbox, featureCollection } from "@turf/turf"; import { bbox, featureCollection } from "@turf/turf";
import Timeline from "@components/olmap/core/Controls/Timeline"; import Timeline from "@components/olmap/core/Controls/Timeline";
import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types"; import { ContaminantSchemeRecord } from "./types";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState"; import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
@@ -60,7 +60,7 @@ export interface ContaminantSchemeQueryState {
showTimeline: boolean; showTimeline: boolean;
selectedDate: Date | undefined; selectedDate: Date | undefined;
timeRange: { start: Date; end: Date } | undefined; timeRange: { start: Date; end: Date } | undefined;
expandedId: number | null; expandedId: string | null;
hasQueried: boolean; hasQueried: boolean;
} }
@@ -111,7 +111,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const { open } = useNotification(); const { open } = useNotification();
const map = useMap(); const map = useMap();
const data = useData(); const data = useData();
const { schemeName, setSchemeName } = data || {}; const { schemeName, setSchemeName, schemeRunId, setSchemeRunId } = data || {};
const schemes = const schemes =
externalSchemes !== undefined ? externalSchemes : internalSchemes; externalSchemes !== undefined ? externalSchemes : internalSchemes;
@@ -222,40 +222,16 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
if (!queryAll && !queryDate) return; if (!queryAll && !queryDate) return;
setLoading(true); setLoading(true);
try { try {
const params: Record<string, string> = { const nextSchemes = (await listAnalysisSchemes({
scheme_type: SCHEME_TYPE, runType: SCHEME_TYPE,
}; queryDate: !queryAll && queryDate
if (!queryAll && queryDate) { ? queryDate.format("YYYY-MM-DD")
params.query_date = queryDate.startOf("day").toISOString(); : undefined,
} })) as unknown as ContaminantSchemeRecord[];
const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
params,
});
let filteredResults = response.data;
if (!queryAll) {
const formattedDate = queryDate!.format("YYYY-MM-DD");
filteredResults = response.data.filter(
(item: ContaminantSchemaItem) => {
const itemDate = moment(item.create_time).format("YYYY-MM-DD");
return itemDate === formattedDate;
},
);
}
const nextSchemes = filteredResults.map((item: ContaminantSchemaItem) => ({
id: item.scheme_id,
schemeName: item.scheme_name,
type: item.scheme_type,
username: item.username,
create_time: item.create_time,
startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail,
}));
setSchemes(nextSchemes); setSchemes(nextSchemes);
setQueryField("hasQueried", true); setQueryField("hasQueried", true);
if (filteredResults.length === 0) { if (nextSchemes.length === 0) {
open?.({ open?.({
type: "success", type: "success",
message: "查询结果", message: "查询结果",
@@ -267,7 +243,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
open?.({ open?.({
type: "success", type: "success",
message: "查询成功", message: "查询成功",
description: `共找到 ${filteredResults.length} 条方案记录`, description: `共找到 ${nextSchemes.length} 条方案记录`,
}); });
} }
} catch (error) { } catch (error) {
@@ -284,7 +260,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const handleLocateSource = (sourceIds: string[]) => { const handleLocateSource = (sourceIds: string[]) => {
if (sourceIds.length > 0) { if (sourceIds.length > 0) {
queryFeaturesByIds(sourceIds, "geo_junctions_mat").then((features) => { queryFeaturesByIds(sourceIds, "junctions").then((features) => {
if (features.length > 0) { if (features.length > 0) {
// 设置高亮要素 // 设置高亮要素
setHighlightFeatures(features); setHighlightFeatures(features);
@@ -307,7 +283,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
} }
}; };
const handleViewDetails = (id: number) => { const handleViewDetails = (id: string) => {
const scheme = filteredSchemes.find((s) => s.id === id); const scheme = filteredSchemes.find((s) => s.id === id);
if (!scheme) return; if (!scheme) return;
@@ -328,6 +304,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
})); }));
} }
setSchemeName?.(scheme.schemeName); setSchemeName?.(scheme.schemeName);
setSchemeRunId?.(scheme.id);
if (scheme.schemeDetail?.source) { if (scheme.schemeDetail?.source) {
handleLocateSource([scheme.schemeDetail.source]); handleLocateSource([scheme.schemeDetail.source]);
} }
@@ -343,6 +320,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
timeRange={timeRange} timeRange={timeRange}
disableDateSelection={!!timeRange} disableDateSelection={!!timeRange}
schemeName={schemeName} schemeName={schemeName}
schemeRunId={schemeRunId}
schemeType={SCHEME_TYPE} schemeType={SCHEME_TYPE}
/>, />,
mapContainer, mapContainer,
@@ -6,7 +6,7 @@ export interface ContaminantSchemeDetail {
} }
export interface ContaminantSchemeRecord { export interface ContaminantSchemeRecord {
id: number; id: string;
schemeName: string; schemeName: string;
type: string; type: string;
username: string; username: string;
@@ -14,13 +14,3 @@ export interface ContaminantSchemeRecord {
startTime: string; startTime: string;
schemeDetail?: ContaminantSchemeDetail; schemeDetail?: ContaminantSchemeDetail;
} }
export interface ContaminantSchemaItem {
scheme_id: number;
scheme_name: string;
scheme_type: string;
username: string;
create_time: string;
scheme_start_time: string;
scheme_detail?: ContaminantSchemeDetail;
}
@@ -21,8 +21,11 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api"; import {
import { NETWORK_NAME, config } from "@config/config"; getAnalysisResults,
getAnalysisScheme,
listAnalysisSchemes,
} from "@/lib/analysisRuns";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { LeakageResultDetail, LeakageSchemeRecord } from "./types"; import { LeakageResultDetail, LeakageSchemeRecord } from "./types";
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
@@ -40,7 +43,7 @@ interface Props {
export interface DMALeakSchemeQueryState { export interface DMALeakSchemeQueryState {
queryAll: boolean; queryAll: boolean;
queryDate: Dayjs | null; queryDate: Dayjs | null;
expandedId: number | null; expandedId: string | null;
hasQueried: boolean; hasQueried: boolean;
} }
@@ -84,16 +87,12 @@ const SchemeQuery: React.FC<Props> = ({
const handleQuery = async () => { const handleQuery = async () => {
setLoading(true); setLoading(true);
try { try {
const params: Record<string, string> = { const nextSchemes = (await listAnalysisSchemes({
scheme_type: "dma_leak_identification", runType: "dma_leak_identification",
}; queryDate: !queryAll && queryDate
if (!queryAll && queryDate) { ? queryDate.format("YYYY-MM-DD")
params.query_date = queryDate.startOf("day").toISOString(); : undefined,
} })) as LeakageSchemeRecord[];
const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
params,
});
const nextSchemes = response.data as LeakageSchemeRecord[];
setSchemes(nextSchemes); setSchemes(nextSchemes);
setQueryField("hasQueried", true); setQueryField("hasQueried", true);
if (nextSchemes.length === 0) { if (nextSchemes.length === 0) {
@@ -122,22 +121,30 @@ const SchemeQuery: React.FC<Props> = ({
} }
}; };
const handleViewSchemeResult = async (schemeName: string) => { const handleViewSchemeResult = async (runId: string) => {
try { try {
const response = await api.get( const [scheme, results] = await Promise.all([
`${config.BACKEND_URL}/api/v1/schemes/${encodeURIComponent(schemeName)}`, getAnalysisScheme(runId),
{ getAnalysisResults(runId, "leakage_identification"),
params: { ]);
scheme_type: "dma_leak_identification", const result = results[0]?.payload;
}, if (!result) {
}, throw new Error("方案详情缺少漏损识别结果");
); }
onViewResult(response.data as LeakageResultDetail); onViewResult({
...result,
scheme_name: scheme.scheme_name,
scheme_detail: scheme.scheme_detail,
scheme_start_time: scheme.scheme_start_time,
create_time: scheme.create_time,
username: scheme.username,
} as LeakageResultDetail);
} catch (error: any) { } catch (error: any) {
open?.({ open?.({
type: "error", type: "error",
message: "查看详情失败", message: "查看详情失败",
description: error?.response?.data?.detail ?? "无法获取方案详情", description:
error?.response?.data?.detail ?? error?.message ?? "无法获取方案详情",
}); });
} }
}; };
@@ -293,7 +300,7 @@ const SchemeQuery: React.FC<Props> = ({
size="small" size="small"
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
sx={{ textTransform: "none", fontWeight: 500 }} sx={{ textTransform: "none", fontWeight: 500 }}
onClick={() => handleViewSchemeResult(scheme.scheme_name)} onClick={() => handleViewSchemeResult(scheme.scheme_id)}
> >
</Button> </Button>
@@ -6,7 +6,7 @@ export interface LeakageRow {
} }
export interface LeakageSchemeRecord { export interface LeakageSchemeRecord {
scheme_id: number; scheme_id: string;
scheme_name: string; scheme_name: string;
scheme_type: string; scheme_type: string;
username: string; username: string;
@@ -143,7 +143,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
const featureId = String(feature.getProperties().id); const featureId = String(feature.getProperties().id);
if (selectionMode === 'valve') { if (selectionMode === 'valve') {
if (layer !== 'geo_valves') { if (layer !== 'valves') {
open?.({ open?.({
type: "error", type: "error",
message: "请选择阀门要素", message: "请选择阀门要素",
@@ -173,7 +173,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
}); });
} else if (selectionMode === 'drainage') { } else if (selectionMode === 'drainage') {
if (layer !== 'geo_junctions') { if (layer !== 'junctions') {
open?.({ open?.({
type: "error", type: "error",
message: "请选择节点要素作为排水点", message: "请选择节点要素作为排水点",
@@ -277,7 +277,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
if (valveFeatures.length > 0) return; if (valveFeatures.length > 0) return;
queryFeaturesByIds( queryFeaturesByIds(
valves.map((valve) => valve.id), valves.map((valve) => valve.id),
"geo_valves", "valves",
).then((features) => { ).then((features) => {
setValveFeatures(features); setValveFeatures(features);
}); });
@@ -374,7 +374,7 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
return; return;
} }
if (drainageFeature) return; if (drainageFeature) return;
queryFeaturesByIds([drainageNode], "geo_junctions").then((features) => { queryFeaturesByIds([drainageNode], "junctions").then((features) => {
setDrainageFeature(features[0] ?? null); setDrainageFeature(features[0] ?? null);
}); });
}, [drainageFeature, drainageNode]); }, [drainageFeature, drainageNode]);
@@ -26,9 +26,9 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { api } from "@/lib/api"; import { listAnalysisSchemes } from "@/lib/analysisRuns";
import moment from "moment"; import moment from "moment";
import { config, NETWORK_NAME } from "@config/config"; import { NETWORK_NAME } from "@config/config";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { useData, useMap } from "@components/olmap/core/MapComponent"; import { useData, useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { queryFeaturesByIds } from "@/utils/mapQueryService";
@@ -40,7 +40,7 @@ import { Style, Icon, Circle, Fill, Stroke } from "ol/style";
import Feature, { FeatureLike } from "ol/Feature"; import Feature, { FeatureLike } from "ol/Feature";
import { bbox, featureCollection } from "@turf/turf"; import { bbox, featureCollection } from "@turf/turf";
import Timeline from "@components/olmap/core/Controls/Timeline"; import Timeline from "@components/olmap/core/Controls/Timeline";
import { SchemeRecord, SchemaItem } from "./types"; import { SchemeRecord } from "./types";
import { FLOW_DISPLAY_UNIT } from "@utils/units"; import { FLOW_DISPLAY_UNIT } from "@utils/units";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState"; import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
@@ -59,7 +59,7 @@ const SCHEME_TYPE = "flushing_analysis";
export interface FlushingSchemeQueryState { export interface FlushingSchemeQueryState {
queryAll: boolean; queryAll: boolean;
queryDate: Dayjs | null; queryDate: Dayjs | null;
expandedId: number | null; expandedId: string | null;
showTimeline: boolean; showTimeline: boolean;
selectedDate: Date | undefined; selectedDate: Date | undefined;
timeRange: { start: Date; end: Date } | undefined; timeRange: { start: Date; end: Date } | undefined;
@@ -111,7 +111,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const { open } = useNotification(); const { open } = useNotification();
const map = useMap(); const map = useMap();
const data = useData(); const data = useData();
const { schemeName, setSchemeName } = data || {}; const { schemeName, setSchemeName, schemeRunId, setSchemeRunId } = data || {};
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
const setSchemes = onSchemesChange || setInternalSchemes; const setSchemes = onSchemesChange || setInternalSchemes;
@@ -214,7 +214,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const handleLocateDrainageNode = (nodeId: string) => { const handleLocateDrainageNode = (nodeId: string) => {
if (!nodeId) return; if (!nodeId) return;
queryFeaturesByIds([nodeId], "geo_junctions_mat").then((features) => { queryFeaturesByIds([nodeId], "junctions").then((features) => {
if (features.length > 0) { if (features.length > 0) {
// Add type property to distinguish styling // Add type property to distinguish styling
features.forEach(f => f.set("type", "drainage")); features.forEach(f => f.set("type", "drainage"));
@@ -231,7 +231,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const handleLocateValves = (valveIds: string[]) => { const handleLocateValves = (valveIds: string[]) => {
if (!valveIds || valveIds.length === 0) return; if (!valveIds || valveIds.length === 0) return;
queryFeaturesByIds(valveIds, "geo_valves").then((features) => { queryFeaturesByIds(valveIds, "valves").then((features) => {
if (features.length > 0) { if (features.length > 0) {
features.forEach(f => f.set("type", "valve")); features.forEach(f => f.set("type", "valve"));
setHighlightFeatures(features); setHighlightFeatures(features);
@@ -269,42 +269,16 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true); setLoading(true);
try { try {
const params: Record<string, string> = { const nextSchemes = (await listAnalysisSchemes({
scheme_type: SCHEME_TYPE, runType: SCHEME_TYPE,
}; queryDate: !queryAll && queryDate
if (!queryAll && queryDate) { ? queryDate.format("YYYY-MM-DD")
params.query_date = queryDate.startOf("day").toISOString(); : undefined,
} })) as unknown as SchemeRecord[];
const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, {
params,
});
let filteredResults = response.data;
// Filter by type
filteredResults = filteredResults.filter((item: SchemaItem) => item.scheme_type === SCHEME_TYPE);
if (!queryAll && queryDate) {
const formattedDate = queryDate.format("YYYY-MM-DD");
filteredResults = filteredResults.filter((item: SchemaItem) => {
const itemDate = moment(item.create_time).format("YYYY-MM-DD");
return itemDate === formattedDate;
});
}
const nextSchemes = filteredResults.map((item: SchemaItem) => ({
id: item.scheme_id,
schemeName: item.scheme_name,
type: item.scheme_type,
username: item.username,
create_time: item.create_time,
startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail,
}));
setSchemes(nextSchemes); setSchemes(nextSchemes);
setQueryField("hasQueried", true); setQueryField("hasQueried", true);
if (filteredResults.length === 0) { if (nextSchemes.length === 0) {
open?.({ open?.({
type: "success", type: "success",
message: "未找到相关方案", message: "未找到相关方案",
@@ -314,7 +288,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
open?.({ open?.({
type: "success", type: "success",
message: "查询成功", message: "查询成功",
description: `共找到 ${filteredResults.length} 条方案记录`, description: `共找到 ${nextSchemes.length} 条方案记录`,
}); });
} }
} catch (error) { } catch (error) {
@@ -346,6 +320,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
} }
setSchemeName?.(scheme.schemeName); setSchemeName?.(scheme.schemeName);
setSchemeRunId?.(scheme.id);
// Locate drainage node by default if available // Locate drainage node by default if available
if (scheme.schemeDetail?.drainage_node_ID) { if (scheme.schemeDetail?.drainage_node_ID) {
@@ -363,6 +338,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
timeRange={timeRange} timeRange={timeRange}
disableDateSelection={!!timeRange} disableDateSelection={!!timeRange}
schemeName={schemeName} schemeName={schemeName}
schemeRunId={schemeRunId}
schemeType={SCHEME_TYPE} schemeType={SCHEME_TYPE}
/>, />,
mapContainer, mapContainer,
+1 -11
View File
@@ -14,7 +14,7 @@ export interface SchemeDetail {
} }
export interface SchemeRecord { export interface SchemeRecord {
id: number; id: string;
schemeName: string; schemeName: string;
type: string; type: string;
username: string; username: string;
@@ -23,13 +23,3 @@ export interface SchemeRecord {
// 详情信息 // 详情信息
schemeDetail?: SchemeDetail; schemeDetail?: SchemeDetail;
} }
export interface SchemaItem {
scheme_id: number;
scheme_name: string;
scheme_type: string;
username: string;
create_time: string;
scheme_start_time: string;
scheme_detail?: SchemeDetail;
}
@@ -26,11 +26,11 @@ jest.mock("./SchemeQuery", () => ({
onEdit, onEdit,
active, active,
}: { }: {
onEdit: (schemeId: number) => void; onEdit: (schemeId: string) => void;
active?: boolean; active?: boolean;
}) => { }) => {
mockSchemeQueryRender(active); mockSchemeQueryRender(active);
return <button onClick={() => onEdit(7)}></button>; return <button onClick={() => onEdit("run-7")}></button>;
}, },
createMonitoringSchemeQueryState: () => ({}), createMonitoringSchemeQueryState: () => ({}),
})); }));
@@ -61,7 +61,7 @@ jest.mock("@components/olmap/common/PanelEmptyState", () => ({
const mockGetSensorPlacementScheme = jest.mocked(getSensorPlacementScheme); const mockGetSensorPlacementScheme = jest.mocked(getSensorPlacementScheme);
const scheme: SensorPlacementScheme = { const scheme: SensorPlacementScheme = {
id: 7, id: "run-7",
scheme_name: "测试方案", scheme_name: "测试方案",
sensor_number: 1, sensor_number: 1,
min_diameter: 100, min_diameter: 100,
@@ -103,7 +103,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
const [activeScheme, setActiveScheme] = const [activeScheme, setActiveScheme] =
useState<SensorPlacementScheme | null>(null); useState<SensorPlacementScheme | null>(null);
const [loadingScheme, setLoadingScheme] = useState(false); const [loadingScheme, setLoadingScheme] = useState(false);
const [pendingOpenSchemeId, setPendingOpenSchemeId] = useState<number | null>( const [pendingOpenSchemeId, setPendingOpenSchemeId] = useState<string | null>(
null, null,
); );
const { open: notify } = useNotification(); const { open: notify } = useNotification();
@@ -138,7 +138,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
setPendingOpenSchemeId(null); setPendingOpenSchemeId(null);
}, []); }, []);
const handleOpenScheme = async (schemeId: number) => { const handleOpenScheme = async (schemeId: string) => {
setLoadingScheme(true); setLoadingScheme(true);
try { try {
const scheme = await getSensorPlacementScheme(schemeId); const scheme = await getSensorPlacementScheme(schemeId);
@@ -98,7 +98,7 @@ const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
try { try {
const created = await optimizeSensorPlacement({ const created = await optimizeSensorPlacement({
scheme_name: normalizeSchemeName(schemeName), run_name: normalizeSchemeName(schemeName),
sensor_type: "pressure", sensor_type: "pressure",
method: method as "sensitivity" | "kmeans", method: method as "sensitivity" | "kmeans",
sensor_count: sensorCount, sensor_count: sensorCount,
@@ -146,7 +146,7 @@ jest.mock("./schemeApi", () => ({
})); }));
const scheme: SensorPlacementScheme = { const scheme: SensorPlacementScheme = {
id: 1, id: "run-1",
scheme_name: "测试方案", scheme_name: "测试方案",
sensor_number: 1, sensor_number: 1,
min_diameter: 300, min_diameter: 300,
@@ -24,9 +24,8 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/zh-cn"; // 引入中文包 import "dayjs/locale/zh-cn"; // 引入中文包
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { api } from "@/lib/api";
import moment from "moment"; import moment from "moment";
import { config, NETWORK_NAME } from "@config/config"; import { NETWORK_NAME } from "@config/config";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { useMap } from "@components/olmap/core/MapComponent"; import { useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { queryFeaturesByIds } from "@/utils/mapQueryService";
@@ -40,22 +39,13 @@ import { bbox, featureCollection } from "@turf/turf";
import type { SchemeRecord } from "./types"; import type { SchemeRecord } from "./types";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState"; import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
import { listSensorPlacementSchemes } from "./schemeApi";
interface SchemaItem {
id: number;
scheme_name: string;
sensor_number: number;
min_diameter: number;
username: string;
create_time: string;
sensor_location?: string[];
}
interface SchemeQueryProps { interface SchemeQueryProps {
schemes?: SchemeRecord[]; schemes?: SchemeRecord[];
active?: boolean; active?: boolean;
onSchemesChange?: (schemes: SchemeRecord[]) => void; onSchemesChange?: (schemes: SchemeRecord[]) => void;
onEdit?: (id: number) => void; onEdit?: (id: string) => void;
network?: string; network?: string;
state?: MonitoringSchemeQueryState; state?: MonitoringSchemeQueryState;
onStateChange?: (state: MonitoringSchemeQueryState) => void; onStateChange?: (state: MonitoringSchemeQueryState) => void;
@@ -64,7 +54,7 @@ interface SchemeQueryProps {
export interface MonitoringSchemeQueryState { export interface MonitoringSchemeQueryState {
queryAll: boolean; queryAll: boolean;
queryDate: Dayjs | null; queryDate: Dayjs | null;
expandedId: number | null; expandedId: string | null;
hasQueried: boolean; hasQueried: boolean;
} }
@@ -185,22 +175,18 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true); setLoading(true);
try { try {
const response = await api.get( let filteredResults = await listSensorPlacementSchemes();
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes`,
);
let filteredResults = response.data;
// 按日期过滤 // 按日期过滤
if (!queryAll && queryDate) { if (!queryAll && queryDate) {
const formattedDate = queryDate.format("YYYY-MM-DD"); const formattedDate = queryDate.format("YYYY-MM-DD");
filteredResults = filteredResults.filter((item: SchemaItem) => { filteredResults = filteredResults.filter((item) => {
const itemDate = moment(item.create_time).format("YYYY-MM-DD"); const itemDate = moment(item.create_time).format("YYYY-MM-DD");
return itemDate === formattedDate; return itemDate === formattedDate;
}); });
} }
const nextSchemes = filteredResults.map((item: SchemaItem) => ({ const nextSchemes = filteredResults.map((item) => ({
id: item.id, id: item.id,
schemeName: item.scheme_name, schemeName: item.scheme_name,
sensorNumber: item.sensor_number, sensorNumber: item.sensor_number,
@@ -250,7 +236,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
} }
if (sensorIds.length > 0) { if (sensorIds.length > 0) {
queryFeaturesByIds(sensorIds, "geo_junctions_mat").then((features) => { queryFeaturesByIds(sensorIds, "junctions").then((features) => {
if (features.length > 0) { if (features.length > 0) {
// 设置高亮要素 // 设置高亮要素
setHighlightFeatures(features); setHighlightFeatures(features);
@@ -271,7 +257,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}; };
// 查看详情(展开/收起) // 查看详情(展开/收起)
const handleViewDetails = (id: number) => { const handleViewDetails = (id: string) => {
setQueryField("expandedId", expandedId === id ? null : id); setQueryField("expandedId", expandedId === id ? null : id);
}; };
@@ -122,7 +122,7 @@ const point = (
const rows = [point("A", 1, 400, 200), point("B", 2, 500, 300)]; const rows = [point("A", 1, 400, 200), point("B", 2, 500, 300)];
const scheme: SensorPlacementScheme = { const scheme: SensorPlacementScheme = {
id: 1, id: "run-1",
scheme_name: "测试方案", scheme_name: "测试方案",
sensor_number: rows.length, sensor_number: rows.length,
min_diameter: 300, min_diameter: 300,
@@ -0,0 +1,98 @@
import { api } from "@/lib/api";
import {
exportSensorPlacementExcel,
getSensorPlacementScheme,
listSensorPlacementSchemes,
optimizeSensorPlacement,
overwriteSensorPlacementScheme,
} from "./schemeApi";
jest.mock("@/lib/api", () => ({
api: {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
},
}));
const run = {
run_id: "9fb546cb-5f72-4ddd-8a46-d13e9aee2c6b",
name: "sensor-plan-a",
sensor_count: 2,
min_diameter: 300,
created_by: "operator",
created_at: "2026-08-25T08:00:00Z",
sensor_locations: ["J-1", "J-2"],
sensor_points: [],
can_edit: true,
status: "completed",
};
describe("sensor placement runs API adapter", () => {
beforeEach(() => jest.clearAllMocks());
it("lists paged runs and maps run fields to the existing screen model", async () => {
jest.mocked(api.get).mockResolvedValue({
data: { items: [run], total: 1, limit: 1000, offset: 0 },
});
await expect(listSensorPlacementSchemes()).resolves.toEqual([
expect.objectContaining({
id: run.run_id,
scheme_name: run.name,
sensor_number: 2,
sensor_location: ["J-1", "J-2"],
}),
]);
expect(api.get).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/sensor-placement-runs"),
{ params: { limit: 1000, offset: 0 } },
);
});
it("uses run endpoints for creation and detail lookup", async () => {
jest.mocked(api.post).mockResolvedValue({ data: run });
jest.mocked(api.get).mockResolvedValue({ data: run });
await optimizeSensorPlacement({
run_name: run.name,
sensor_type: "pressure",
method: "sensitivity",
sensor_count: 2,
min_diameter: 300,
});
await getSensorPlacementScheme(run.run_id);
expect(api.post).toHaveBeenCalledWith(
expect.stringMatching(/\/api\/v1\/sensor-placement-runs$/),
expect.objectContaining({ run_name: run.name }),
);
expect(api.get).toHaveBeenLastCalledWith(
expect.stringContaining(`/api/v1/sensor-placement-runs/${run.run_id}`),
);
});
it("uses the plural result fields required by update and export", async () => {
jest.mocked(api.put).mockResolvedValue({ data: run });
jest.mocked(api.post).mockResolvedValue({ data: new Blob() });
await overwriteSensorPlacementScheme(run.run_id, ["J-1"], ["J-2"]);
await exportSensorPlacementExcel(run.run_id, ["J-2"], { "J-2": "added" });
expect(api.put).toHaveBeenCalledWith(
expect.stringContaining(`/sensor-placement-runs/${run.run_id}`),
{
expected_sensor_locations: ["J-1"],
sensor_locations: ["J-2"],
},
);
expect(api.post).toHaveBeenCalledWith(
expect.stringContaining(`/sensor-placement-runs/${run.run_id}/exports/excel`),
{
sensor_locations: ["J-2"],
adjustment_status: { "J-2": "added" },
},
{ responseType: "blob" },
);
});
});
@@ -7,30 +7,67 @@ import type {
} from "./types"; } from "./types";
export interface OptimizeSchemeInput { export interface OptimizeSchemeInput {
scheme_name: string; run_name: string;
sensor_type: "pressure"; sensor_type: "pressure";
method: "sensitivity" | "kmeans"; method: "sensitivity" | "kmeans";
sensor_count: number; sensor_count: number;
min_diameter: number; min_diameter: number;
} }
type SensorPlacementRunResponse = {
run_id: string;
name: string;
sensor_count: number;
min_diameter: number;
created_by: string;
created_at: string;
sensor_locations: string[];
sensor_points: SensorPoint[];
can_edit: boolean;
};
const toSensorPlacementScheme = (
run: SensorPlacementRunResponse,
): SensorPlacementScheme => ({
id: run.run_id,
scheme_name: run.name,
sensor_number: run.sensor_count,
min_diameter: run.min_diameter,
username: run.created_by,
create_time: run.created_at,
sensor_location: run.sensor_locations,
sensor_points: run.sensor_points,
can_edit: run.can_edit,
});
export const optimizeSensorPlacement = async ( export const optimizeSensorPlacement = async (
input: OptimizeSchemeInput, input: OptimizeSchemeInput,
): Promise<SensorPlacementScheme> => { ): Promise<SensorPlacementScheme> => {
const response = await api.post<SensorPlacementScheme>( const response = await api.post<SensorPlacementRunResponse>(
`${config.BACKEND_URL}/api/v1/sensor-placement-optimization-runs`, `${config.BACKEND_URL}/api/v1/sensor-placement-runs`,
input, input,
); );
return response.data; return toSensorPlacementScheme(response.data);
};
export const listSensorPlacementSchemes = async (): Promise<
SensorPlacementScheme[]
> => {
const response = await api.get<{
items: SensorPlacementRunResponse[];
}>(`${config.BACKEND_URL}/api/v1/sensor-placement-runs`, {
params: { limit: 1000, offset: 0 },
});
return response.data.items.map(toSensorPlacementScheme);
}; };
export const getSensorPlacementScheme = async ( export const getSensorPlacementScheme = async (
schemeId: number, schemeId: string,
): Promise<SensorPlacementScheme> => { ): Promise<SensorPlacementScheme> => {
const response = await api.get<SensorPlacementScheme>( const response = await api.get<SensorPlacementRunResponse>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/${schemeId}`, `${config.BACKEND_URL}/api/v1/sensor-placement-runs/${encodeURIComponent(schemeId)}`,
); );
return response.data; return toSensorPlacementScheme(response.data);
}; };
export const getSensorPlacementCandidate = async ( export const getSensorPlacementCandidate = async (
@@ -43,29 +80,29 @@ export const getSensorPlacementCandidate = async (
}; };
export const overwriteSensorPlacementScheme = async ( export const overwriteSensorPlacementScheme = async (
schemeId: number, schemeId: string,
expectedSensorLocation: string[], expectedSensorLocation: string[],
sensorLocation: string[], sensorLocation: string[],
): Promise<SensorPlacementScheme> => { ): Promise<SensorPlacementScheme> => {
const response = await api.put<SensorPlacementScheme>( const response = await api.put<SensorPlacementRunResponse>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/${schemeId}`, `${config.BACKEND_URL}/api/v1/sensor-placement-runs/${encodeURIComponent(schemeId)}`,
{ {
expected_sensor_location: expectedSensorLocation, expected_sensor_locations: expectedSensorLocation,
sensor_location: sensorLocation, sensor_locations: sensorLocation,
}, },
); );
return response.data; return toSensorPlacementScheme(response.data);
}; };
export const exportSensorPlacementExcel = async ( export const exportSensorPlacementExcel = async (
schemeId: number, schemeId: string,
sensorLocation: string[], sensorLocation: string[],
adjustmentStatus: Record<string, AdjustmentStatus>, adjustmentStatus: Record<string, AdjustmentStatus>,
) => { ) => {
const response = await api.post<Blob>( const response = await api.post<Blob>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/${schemeId}/exports/excel`, `${config.BACKEND_URL}/api/v1/sensor-placement-runs/${encodeURIComponent(schemeId)}/exports/excel`,
{ {
sensor_location: sensorLocation, sensor_locations: sensorLocation,
adjustment_status: adjustmentStatus, adjustment_status: adjustmentStatus,
}, },
{ {
@@ -24,7 +24,7 @@ const point = (node_id: string): SensorPoint => ({
}); });
const scheme: SensorPlacementScheme = { const scheme: SensorPlacementScheme = {
id: 1, id: "run-1",
scheme_name: "测试方案", scheme_name: "测试方案",
sensor_number: 2, sensor_number: 2,
min_diameter: 300, min_diameter: 300,
@@ -13,7 +13,7 @@ export interface SensorPoint {
} }
export interface SensorPlacementScheme { export interface SensorPlacementScheme {
id: number; id: string;
scheme_name: string; scheme_name: string;
sensor_number: number; sensor_number: number;
min_diameter: number; min_diameter: number;
@@ -25,7 +25,7 @@ export interface SensorPlacementScheme {
} }
export interface SchemeRecord { export interface SchemeRecord {
id: number; id: string;
schemeName: string; schemeName: string;
sensorNumber: number; sensorNumber: number;
minDiameter: number; minDiameter: number;
@@ -192,7 +192,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
setLoading(true); setLoading(true);
try { try {
const activeWorkspace = workspace || config.MAP_WORKSPACE; const activeWorkspace = workspace || config.MAP_WORKSPACE;
const url = `${config.MAP_URL}/${activeWorkspace}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${activeWorkspace}:geo_scada&outputFormat=application/json`; const url = `${config.MAP_URL}/${activeWorkspace}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${activeWorkspace}:scada_devices&outputFormat=application/json`;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch SCADA devices"); if (!response.ok) throw new Error("Failed to fetch SCADA devices");
const json = await response.json(); const json = await response.json();
@@ -202,7 +202,8 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
name: feature.get("id") || feature.getId(), name: feature.get("id") || feature.getId(),
transmission_frequency: feature.get("transmission_frequency"), transmission_frequency: feature.get("transmission_frequency"),
reliability: feature.get("reliability"), reliability: feature.get("reliability"),
type: feature.get("type") === "pipe_flow" ? "流量" : "压力", type:
feature.get("device_type") === "pipe_flow" ? "流量" : "压力",
status: STATUS_OPTIONS[Math.floor(Math.random() * 4)], status: STATUS_OPTIONS[Math.floor(Math.random() * 4)],
coordinates: (feature.getGeometry() as Point)?.getCoordinates() as [ coordinates: (feature.getGeometry() as Point)?.getCoordinates() as [
number, number,
@@ -488,7 +489,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
const layer = feature?.getId()?.toString().split(".")[0]; const layer = feature?.getId()?.toString().split(".")[0];
if (!feature) return; if (!feature) return;
if (layer !== "geo_scada_mat" && layer !== "geo_scada") { if (layer !== "scada_devices") {
open?.({ open?.({
type: "error", type: "error",
message: "请选择 SCADA 设备。", message: "请选择 SCADA 设备。",
+37 -12
View File
@@ -45,6 +45,7 @@ interface TimelineProps {
timeRange?: { start: Date; end: Date }; timeRange?: { start: Date; end: Date };
disableDateSelection?: boolean; disableDateSelection?: boolean;
schemeName?: string; schemeName?: string;
schemeRunId?: string;
schemeType?: string; schemeType?: string;
} }
@@ -80,6 +81,7 @@ const Timeline: React.FC<TimelineProps> = ({
timeRange, timeRange,
disableDateSelection = false, disableDateSelection = false,
schemeName = "", schemeName = "",
schemeRunId = "",
schemeType = "burst_analysis", schemeType = "burst_analysis",
}) => { }) => {
const data = useData(); const data = useData();
@@ -207,6 +209,7 @@ const Timeline: React.FC<TimelineProps> = ({
sourceType, sourceType,
target, target,
schemeName, schemeName,
schemeRunId,
schemeType, schemeType,
signal, signal,
}: { }: {
@@ -216,6 +219,7 @@ const Timeline: React.FC<TimelineProps> = ({
sourceType: "scheme" | "realtime"; sourceType: "scheme" | "realtime";
target: "primary" | "compare"; target: "primary" | "compare";
schemeName?: string; schemeName?: string;
schemeRunId?: string;
schemeType?: string; schemeType?: string;
signal?: AbortSignal; signal?: AbortSignal;
}) => { }) => {
@@ -232,16 +236,16 @@ const Timeline: React.FC<TimelineProps> = ({
junctionProperties, junctionProperties,
sourceType, sourceType,
"node", "node",
schemeName || "", schemeRunId || schemeName || "",
schemeType || "" schemeType || ""
); );
if (nodeCacheRef.current.has(nodeCacheKey)) { if (nodeCacheRef.current.has(nodeCacheKey)) {
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!; nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
} else { } else {
nodePromise = nodePromise =
sourceType === "scheme" && schemeName sourceType === "scheme" && schemeRunId
? apiFetch( ? apiFetch(
`${config.BACKEND_URL}/api/v1/timeseries/schemes/records?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`, `${config.BACKEND_URL}/api/v1/timeseries/analysis/runs/${encodeURIComponent(schemeRunId)}/values?result_time=${encodeURIComponent(query_time)}&element_type=node&field=${encodeURIComponent(junctionProperties)}`,
{ signal }, { signal },
) )
: apiFetch( : apiFetch(
@@ -261,16 +265,16 @@ const Timeline: React.FC<TimelineProps> = ({
normalizedPipeProperties, normalizedPipeProperties,
sourceType, sourceType,
"link", "link",
schemeName || "", schemeRunId || schemeName || "",
schemeType || "" schemeType || ""
); );
if (linkCacheRef.current.has(linkCacheKey)) { if (linkCacheRef.current.has(linkCacheKey)) {
linkRecords = linkCacheRef.current.get(linkCacheKey)!; linkRecords = linkCacheRef.current.get(linkCacheKey)!;
} else { } else {
linkPromise = linkPromise =
sourceType === "scheme" && schemeName sourceType === "scheme" && schemeRunId
? apiFetch( ? apiFetch(
`${config.BACKEND_URL}/api/v1/timeseries/schemes/records?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${normalizedPipeProperties}`, `${config.BACKEND_URL}/api/v1/timeseries/analysis/runs/${encodeURIComponent(schemeRunId)}/values?result_time=${encodeURIComponent(query_time)}&element_type=link&field=${encodeURIComponent(normalizedPipeProperties)}`,
{ signal }, { signal },
) )
: apiFetch( : apiFetch(
@@ -288,14 +292,22 @@ const Timeline: React.FC<TimelineProps> = ({
if (!nodeResponse.ok) { if (!nodeResponse.ok) {
throw new Error(`Node fetch failed: ${nodeResponse.status}`); throw new Error(`Node fetch failed: ${nodeResponse.status}`);
} }
nodeRecords = await nodeResponse.json(); const payload = await nodeResponse.json();
nodeRecords = sourceType === "scheme"
? {
results: Object.entries(payload ?? {}).map(([ID, value]) => ({
ID,
value,
})),
}
: payload;
nodeCacheRef.current.set( nodeCacheRef.current.set(
buildCacheKey( buildCacheKey(
query_time, query_time,
junctionProperties, junctionProperties,
sourceType, sourceType,
"node", "node",
schemeName || "", schemeRunId || schemeName || "",
schemeType || "" schemeType || ""
), ),
nodeRecords || [] nodeRecords || []
@@ -307,14 +319,22 @@ const Timeline: React.FC<TimelineProps> = ({
if (!linkResponse.ok) { if (!linkResponse.ok) {
throw new Error(`Link fetch failed: ${linkResponse.status}`); throw new Error(`Link fetch failed: ${linkResponse.status}`);
} }
linkRecords = await linkResponse.json(); const payload = await linkResponse.json();
linkRecords = sourceType === "scheme"
? {
results: Object.entries(payload ?? {}).map(([ID, value]) => ({
ID,
value,
})),
}
: payload;
linkCacheRef.current.set( linkCacheRef.current.set(
buildCacheKey( buildCacheKey(
query_time, query_time,
normalizedPipeProperties, normalizedPipeProperties,
sourceType, sourceType,
"link", "link",
schemeName || "", schemeRunId || schemeName || "",
schemeType || "" schemeType || ""
), ),
linkRecords || [] linkRecords || []
@@ -336,6 +356,7 @@ const Timeline: React.FC<TimelineProps> = ({
junctionProperties: string, junctionProperties: string,
pipeProperties: string, pipeProperties: string,
schemeName: string, schemeName: string,
schemeRunId: string,
schemeType: string schemeType: string
) => { ) => {
const revision = frameRequestRevisionRef.current + 1; const revision = frameRequestRevisionRef.current + 1;
@@ -344,7 +365,7 @@ const Timeline: React.FC<TimelineProps> = ({
const abortController = new AbortController(); const abortController = new AbortController();
frameAbortControllerRef.current = abortController; frameAbortControllerRef.current = abortController;
const primarySourceType = const primarySourceType =
disableDateSelection && schemeName ? "scheme" : "realtime"; disableDateSelection && schemeRunId ? "scheme" : "realtime";
const tasks = [ const tasks = [
fetchDataBySource({ fetchDataBySource({
queryTime, queryTime,
@@ -353,12 +374,13 @@ const Timeline: React.FC<TimelineProps> = ({
sourceType: primarySourceType, sourceType: primarySourceType,
target: "primary", target: "primary",
schemeName, schemeName,
schemeRunId,
schemeType, schemeType,
signal: abortController.signal, signal: abortController.signal,
}), }),
]; ];
if (isCompareMode && disableDateSelection && schemeName) { if (isCompareMode && disableDateSelection && schemeRunId) {
tasks.push( tasks.push(
fetchDataBySource({ fetchDataBySource({
queryTime, queryTime,
@@ -600,6 +622,7 @@ const Timeline: React.FC<TimelineProps> = ({
junctionText, junctionText,
pipeText, pipeText,
schemeName, schemeName,
schemeRunId,
schemeType, schemeType,
); );
} }
@@ -612,6 +635,7 @@ const Timeline: React.FC<TimelineProps> = ({
timelineCurrentTime, timelineCurrentTime,
selectedDate, selectedDate,
schemeName, schemeName,
schemeRunId,
schemeType, schemeType,
]); ]);
@@ -687,6 +711,7 @@ const Timeline: React.FC<TimelineProps> = ({
junctionText, junctionText,
pipeText, pipeText,
schemeName, schemeName,
schemeRunId,
schemeType, schemeType,
); );
}; };
+48 -35
View File
@@ -37,6 +37,7 @@ import {
import { config, NETWORK_NAME } from "@/config/config"; import { config, NETWORK_NAME } from "@/config/config";
import { useProject } from "@/contexts/ProjectContext"; import { useProject } from "@/contexts/ProjectContext";
import { apiFetch } from "@/lib/apiFetch"; import { apiFetch } from "@/lib/apiFetch";
import { getAnalysisScheme } from "@/lib/analysisRuns";
import { permissionCodes } from "@/lib/permissions"; import { permissionCodes } from "@/lib/permissions";
import { useAccessStore } from "@/store/accessStore"; import { useAccessStore } from "@/store/accessStore";
@@ -65,8 +66,7 @@ type ActiveSchemeDetail = {
valve_opening?: Record<string, number> | null; valve_opening?: Record<string, number> | null;
}; };
const isValveLayer = (layerId: string | undefined) => const isValveLayer = (layerId: string | undefined) => layerId === "valves";
layerId === "geo_valves_mat" || layerId === "geo_valves";
const Toolbar: React.FC<ToolbarProps> = ({ const Toolbar: React.FC<ToolbarProps> = ({
hiddenButtons, hiddenButtons,
@@ -93,11 +93,12 @@ const Toolbar: React.FC<ToolbarProps> = ({
const currentTime = data?.currentTime; const currentTime = data?.currentTime;
const selectedDate = data?.selectedDate; const selectedDate = data?.selectedDate;
const schemeName = data?.schemeName; const schemeName = data?.schemeName;
const schemeRunId = data?.schemeRunId;
const networkName = project?.networkName || NETWORK_NAME; const networkName = project?.networkName || NETWORK_NAME;
const isCompareMode = data?.isCompareMode ?? false; const isCompareMode = data?.isCompareMode ?? false;
const toggleCompareMode = data?.toggleCompareMode; const toggleCompareMode = data?.toggleCompareMode;
const canToggleCompare = Boolean( const canToggleCompare = Boolean(
enableCompare && (isCompareMode || (queryType === "scheme" && schemeName)), enableCompare && (isCompareMode || (queryType === "scheme" && schemeRunId)),
); );
useEffect(() => { useEffect(() => {
@@ -406,7 +407,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
if ( if (
queryType !== "scheme" || queryType !== "scheme" ||
schemeType !== "flushing_analysis" || schemeType !== "flushing_analysis" ||
!schemeName || !schemeRunId ||
!selectedValveId !selectedValveId
) { ) {
setActiveSchemeDetail(null); setActiveSchemeDetail(null);
@@ -418,15 +419,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
const querySchemeDetail = async () => { const querySchemeDetail = async () => {
try { try {
const params = new URLSearchParams(); const payload = await getAnalysisScheme(schemeRunId);
if (schemeType) params.set("scheme_type", schemeType);
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/schemes/${encodeURIComponent(schemeName)}?${params.toString()}`,
);
if (!response.ok) {
throw new Error(`getschemedetail failed: ${response.status}`);
}
const payload = await response.json();
if (!cancelled) { if (!cancelled) {
setActiveSchemeDetail(payload?.scheme_detail ?? null); setActiveSchemeDetail(payload?.scheme_detail ?? null);
} }
@@ -446,7 +439,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [open, queryType, schemeName, schemeType, selectedValveId]); }, [open, queryType, schemeRunId, schemeType, selectedValveId]);
useEffect(() => { useEffect(() => {
if (!selectedValveId || isSimulationDataActive) { if (!selectedValveId || isSimulationDataActive) {
@@ -710,39 +703,59 @@ const Toolbar: React.FC<ToolbarProps> = ({
dateObj.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0); dateObj.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
// 转为 UTC ISO 字符串 // 转为 UTC ISO 字符串
const querytime = dateObj.toISOString(); // 例如 "2025-09-16T16:30:00.000Z" const querytime = dateObj.toISOString(); // 例如 "2025-09-16T16:30:00.000Z"
let response: Response;
if (queryType === "scheme") { if (queryType === "scheme") {
if (!schemeRunId) {
throw new Error("Analysis run ID is missing");
}
const fields = type === "node"
? ["actual_demand", "total_head", "pressure", "quality"]
: [
"flow",
"friction",
"headloss",
"quality",
"reaction",
"setting",
"status",
"velocity",
];
const values = await Promise.all(
fields.map(async (field) => {
const params = new URLSearchParams({ const params = new URLSearchParams({
scheme_type: schemeType ?? "", result_time: querytime,
scheme_name: schemeName ?? "", element_type: type,
id: String(id), field,
type,
query_time: querytime,
}); });
response = await apiFetch( const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/timeseries/schemes/simulation-results?${params.toString()}`, `${config.BACKEND_URL}/api/v1/timeseries/analysis/runs/${encodeURIComponent(schemeRunId)}/values?${params.toString()}`,
); );
} else { if (!response.ok) {
throw new Error(`Analysis value fetch failed: ${response.status}`);
}
const payload = await response.json();
return [field, payload?.[String(id)]] as const;
}),
);
if (!cancelled) {
setComputedProperties(
Object.fromEntries(values.filter(([, value]) => value !== undefined)),
);
}
return;
}
const params = new URLSearchParams({ const params = new URLSearchParams({
id: String(id), id: String(id),
type, type,
query_time: querytime, query_time: querytime,
}); });
response = await apiFetch( const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/timeseries/realtime/simulation-results?${params.toString()}`, `${config.BACKEND_URL}/api/v1/timeseries/realtime/simulation-results?${params.toString()}`,
); );
} if (!response.ok) throw new Error("API request failed");
if (!response.ok) {
throw new Error("API request failed");
}
const data = await response.json(); const data = await response.json();
if (cancelled) return; if (cancelled) return;
if (!data.result || data.result.length === 0) { setComputedProperties(data.result?.[0] || {});
setComputedProperties({});
} else {
setComputedProperties(data.result[0] || {});
// console.log("查询到的计算属性:", data.result[0]);
}
} catch (error) { } catch (error) {
console.error("Error querying computed properties:", error); console.error("Error querying computed properties:", error);
if (!cancelled) { if (!cancelled) {
@@ -759,7 +772,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [highlightFeatures, currentTime, open, selectedDate, queryType, schemeName, schemeType, showPropertyPanel]); }, [highlightFeatures, currentTime, open, selectedDate, queryType, schemeName, schemeRunId, showPropertyPanel]);
const displayedComputedProperties = useMemo(() => { const displayedComputedProperties = useMemo(() => {
if (!isSimulationDataActive || !selectedValveId) { if (!isSimulationDataActive || !selectedValveId) {
@@ -22,7 +22,7 @@ const createValveFeature = () => {
minor_loss: 0, minor_loss: 0,
}; };
return { return {
getId: () => "geo_valves.V1", getId: () => "valves.V1",
getProperties: () => properties, getProperties: () => properties,
} as unknown as Feature; } as unknown as Feature;
}; };
@@ -161,7 +161,7 @@ describe("getSimulationElementType", () => {
it("keeps point-rendered pumps on the same hydraulic-link path", () => { it("keeps point-rendered pumps on the same hydraulic-link path", () => {
const pump = { const pump = {
getId: () => "geo_pumps.P1", getId: () => "pumps.P1",
getProperties: () => ({ getProperties: () => ({
id: "P1", id: "P1",
geometry: { getType: () => "Point" }, geometry: { getType: () => "Point" },
@@ -174,7 +174,7 @@ describe("getSimulationElementType", () => {
describe("buildFeatureProperties simulation values by hydraulic type", () => { describe("buildFeatureProperties simulation values by hydraulic type", () => {
it("shows link simulation results for a point-rendered pump", () => { it("shows link simulation results for a point-rendered pump", () => {
const pump = createFeature("geo_pumps", "P1", { const pump = createFeature("pumps", "P1", {
node1: "J1", node1: "J1",
node2: "J2", node2: "J2",
geometry: { getType: () => "Point" }, geometry: { getType: () => "Point" },
@@ -196,8 +196,8 @@ describe("buildFeatureProperties simulation values by hydraulic type", () => {
}); });
it.each([ it.each([
["geo_tanks", "T1", "水池"], ["tanks", "T1", "水池"],
["geo_reservoirs", "R1", "水库"], ["reservoirs", "R1", "水库"],
])("shows node simulation results for %s", (layer, id, type) => { ])("shows node simulation results for %s", (layer, id, type) => {
const feature = createFeature(layer, id, { const feature = createFeature(layer, id, {
geometry: { getType: () => "Point" }, geometry: { getType: () => "Point" },
@@ -236,13 +236,13 @@ export const buildFeatureProperties = (
}); });
}; };
if (layer === "geo_pipes_mat" || layer === "geo_pipes") { if (layer === "pipes") {
const result: ToolbarPropertyPanelData = { const result: ToolbarPropertyPanelData = {
id: properties.id, id: properties.id,
type: "管道", type: "管道",
properties: [ properties: [
{ label: "起始节点ID", value: properties.node1 }, { label: "起始节点ID", value: properties.start_node_id },
{ label: "终点节点ID", value: properties.node2 }, { label: "终点节点ID", value: properties.end_node_id },
{ label: "长度", value: properties.length?.toFixed?.(1), unit: "m" }, { label: "长度", value: properties.length?.toFixed?.(1), unit: "m" },
{ {
label: "管径", label: "管径",
@@ -260,7 +260,7 @@ export const buildFeatureProperties = (
return result; return result;
} }
if (layer === "geo_junctions_mat" || layer === "geo_junctions") { if (layer === "junctions") {
const result: ToolbarPropertyPanelData = { const result: ToolbarPropertyPanelData = {
id: properties.id, id: properties.id,
type: "节点", type: "节点",
@@ -271,27 +271,15 @@ export const buildFeatureProperties = (
unit: "m", unit: "m",
}, },
{ {
type: "table",
label: "基本需水量", label: "基本需水量",
columns: ["demand", "pattern"], value: Number.isFinite(Number(properties.base_demand))
rows: Array.from({ length: 5 }, (_, i) => i + 1) ? toM3h(Number(properties.base_demand), "lps").toFixed(3)
.map((idx) => { : properties.base_demand,
let demand = properties?.[`demand${idx}`]; unit: "m³/h",
const pattern = properties?.[`pattern${idx}`]; },
if ( {
demand !== undefined && label: "需水配置",
demand !== null && value: properties.demands,
demand !== ""
) {
demand = toM3h(Number(demand), "lps");
return [
typeof demand === "number" ? demand.toFixed(3) : demand,
pattern ?? "-",
];
}
return null;
})
.filter(Boolean) as (string | number)[][],
}, },
], ],
}; };
@@ -301,7 +289,7 @@ export const buildFeatureProperties = (
return result; return result;
} }
if (layer === "geo_tanks_mat" || layer === "geo_tanks") { if (layer === "tanks") {
const result: ToolbarPropertyPanelData = { const result: ToolbarPropertyPanelData = {
id: properties.id, id: properties.id,
type: "水池", type: "水池",
@@ -313,17 +301,17 @@ export const buildFeatureProperties = (
}, },
{ {
label: "初始水位", label: "初始水位",
value: properties.init_level?.toFixed?.(1), value: properties.initial_level?.toFixed?.(1),
unit: "m", unit: "m",
}, },
{ {
label: "最低水位", label: "最低水位",
value: properties.min_level?.toFixed?.(1), value: properties.minimum_level?.toFixed?.(1),
unit: "m", unit: "m",
}, },
{ {
label: "最高水位", label: "最高水位",
value: properties.max_level?.toFixed?.(1), value: properties.maximum_level?.toFixed?.(1),
unit: "m", unit: "m",
}, },
{ {
@@ -333,7 +321,7 @@ export const buildFeatureProperties = (
}, },
{ {
label: "最小容积", label: "最小容积",
value: properties.min_vol?.toFixed?.(1), value: properties.minimum_volume?.toFixed?.(1),
unit: "m³", unit: "m³",
}, },
{ {
@@ -346,7 +334,7 @@ export const buildFeatureProperties = (
return result; return result;
} }
if (layer === "geo_reservoirs_mat" || layer === "geo_reservoirs") { if (layer === "reservoirs") {
const result: ToolbarPropertyPanelData = { const result: ToolbarPropertyPanelData = {
id: properties.id, id: properties.id,
type: "水库", type: "水库",
@@ -356,37 +344,37 @@ export const buildFeatureProperties = (
value: properties.head?.toFixed?.(1), value: properties.head?.toFixed?.(1),
unit: "m", unit: "m",
}, },
{ label: "模式", value: properties.pattern_id },
], ],
}; };
appendNodeComputedProperties(result); appendNodeComputedProperties(result);
return result; return result;
} }
if (layer === "geo_pumps_mat" || layer === "geo_pumps") { if (layer === "pumps") {
const result: ToolbarPropertyPanelData = { const result: ToolbarPropertyPanelData = {
id: properties.id, id: properties.id,
type: "水泵", type: "水泵",
properties: [ properties: [
{ label: "起始节点 ID", value: properties.node1 }, { label: "起始节点 ID", value: properties.start_node_id },
{ label: "终点节点 ID", value: properties.node2 }, { label: "终点节点 ID", value: properties.end_node_id },
{ {
label: "功率", label: "功率",
value: properties.power?.toFixed?.(1), value: properties.power?.toFixed?.(1),
unit: "kW", unit: "kW",
}, },
{ {
label: "扬程", label: "扬程曲线",
value: properties.head?.toFixed?.(1), value: properties.head_curve_id,
unit: "m",
}, },
{ {
label: "转速", label: "转速",
value: properties.speed?.toFixed?.(1), value: properties.speed?.toFixed?.(1),
unit: "rpm", unit: "",
}, },
{ {
label: "模式", label: "模式",
value: properties.pattern, value: properties.pattern_id,
}, },
], ],
}; };
@@ -394,8 +382,8 @@ export const buildFeatureProperties = (
return result; return result;
} }
if (layer === "geo_valves_mat" || layer === "geo_valves") { if (layer === "valves") {
const valveType = valveSetting?.vType ?? properties.v_type; const valveType = valveSetting?.vType ?? properties.valve_type;
const hasSimulationValues = const hasSimulationValues =
Object.hasOwn(computedProperties, "status") || Object.hasOwn(computedProperties, "status") ||
Object.hasOwn(computedProperties, "setting"); Object.hasOwn(computedProperties, "setting");
@@ -409,8 +397,8 @@ export const buildFeatureProperties = (
id: properties.id, id: properties.id,
type: "阀门", type: "阀门",
properties: [ properties: [
{ label: "起始节点 ID", value: properties.node1 }, { label: "起始节点 ID", value: properties.start_node_id },
{ label: "终点节点 ID", value: properties.node2 }, { label: "终点节点 ID", value: properties.end_node_id },
{ {
label: "直径", label: "直径",
value: properties.diameter?.toFixed?.(1), value: properties.diameter?.toFixed?.(1),
@@ -519,7 +507,7 @@ export const buildFeatureProperties = (
} }
}; };
if (layer === "geo_scada_mat" || layer === "geo_scada") { if (layer === "scada_devices") {
return { return {
id: properties.id, id: properties.id,
type: "SCADA设备", type: "SCADA设备",
@@ -527,11 +515,13 @@ export const buildFeatureProperties = (
{ {
label: "类型", label: "类型",
value: value:
properties.type === "pipe_flow" ? "流量传感器" : "压力传感器", properties.device_type === "pipe_flow"
? "流量传感器"
: "压力传感器",
}, },
{ {
label: "关联节点 ID", label: "关联节点 ID",
value: properties.associated_element_id, value: properties.node_id ?? properties.link_id,
}, },
{ {
label: "传输模式", label: "传输模式",
@@ -81,6 +81,8 @@ interface DataContextType {
selectedDate?: Date; // 选择的日期 selectedDate?: Date; // 选择的日期
schemeName?: string; // 当前方案名称 schemeName?: string; // 当前方案名称
setSchemeName?: React.Dispatch<React.SetStateAction<string>>; setSchemeName?: React.Dispatch<React.SetStateAction<string>>;
schemeRunId?: string; // 当前分析运行 ID
setSchemeRunId?: React.Dispatch<React.SetStateAction<string>>;
setSelectedDate?: React.Dispatch<React.SetStateAction<Date>>; setSelectedDate?: React.Dispatch<React.SetStateAction<Date>>;
currentJunctionCalData?: any[]; // 当前计算结果 currentJunctionCalData?: any[]; // 当前计算结果
setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>; setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
@@ -226,6 +228,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
// const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17")); // const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 默认今天 const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 默认今天
const [schemeName, setSchemeName] = useState<string>(""); // 当前方案名称 const [schemeName, setSchemeName] = useState<string>(""); // 当前方案名称
const [schemeRunId, setSchemeRunId] = useState<string>("");
// 记录 id、对应属性的计算值 // 记录 id、对应属性的计算值
const [currentJunctionCalData, setCurrentJunctionCalData] = useState<any[]>( const [currentJunctionCalData, setCurrentJunctionCalData] = useState<any[]>(
[], [],
@@ -791,6 +794,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
); );
setSelectedDate(new Date()); setSelectedDate(new Date());
setSchemeName(""); setSchemeName("");
setSchemeRunId("");
setCurrentJunctionCalData([]); setCurrentJunctionCalData([]);
setCurrentPipeCalData([]); setCurrentPipeCalData([]);
setCompareJunctionCalData([]); setCompareJunctionCalData([]);
@@ -1118,6 +1122,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
setSelectedDate, setSelectedDate,
schemeName, schemeName,
setSchemeName, setSchemeName,
schemeRunId,
setSchemeRunId,
currentJunctionCalData, currentJunctionCalData,
setCurrentJunctionCalData, setCurrentJunctionCalData,
currentPipeCalData, currentPipeCalData,
@@ -63,6 +63,35 @@ import {
} from "./operationalLayers"; } from "./operationalLayers";
describe("operational map resources", () => { describe("operational map resources", () => {
it("requests the published tjwater_next layer names", () => {
const sources = createOperationalMapSources({
mapUrl: "https://maps.example.test/geoserver",
workspace: "tjwater_next",
});
expect((sources.junctions as any).options.url).toContain(
"tjwater_next:junctions@WebMercatorQuad@pbf",
);
expect((sources.pipes as any).options.url).toContain(
"tjwater_next:pipes@WebMercatorQuad@pbf",
);
expect((sources.valves as any).options.url).toContain(
"tjwater_next:valves@WebMercatorQuad@pbf",
);
expect((sources.reservoirs as any).options.url).toContain(
"typeName=tjwater_next:reservoirs",
);
expect((sources.pumps as any).options.url).toContain(
"typeName=tjwater_next:pumps",
);
expect((sources.tanks as any).options.url).toContain(
"typeName=tjwater_next:tanks",
);
expect((sources.scada as any).options.url).toContain(
"typeName=tjwater_next:scada_devices",
);
});
it("shares sources while keeping per-map layer instances independent", () => { it("shares sources while keeping per-map layer instances independent", () => {
const options = { const options = {
mapUrl: "https://maps.example.test/geoserver", mapUrl: "https://maps.example.test/geoserver",
+11 -36
View File
@@ -1,12 +1,9 @@
import { config } from "@/config/config"; import { config } from "@/config/config";
import { along, lineString, length, toMercator } from "@turf/turf";
import type { FeatureLike } from "ol/Feature"; import type { FeatureLike } from "ol/Feature";
import MVT from "ol/format/MVT"; import MVT from "ol/format/MVT";
import { Point } from "ol/geom";
import type BaseLayer from "ol/layer/Base"; import type BaseLayer from "ol/layer/Base";
import VectorLayer from "ol/layer/Vector"; import VectorLayer from "ol/layer/Vector";
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
import { toLonLat } from "ol/proj";
import GeoJSON from "ol/format/GeoJSON"; import GeoJSON from "ol/format/GeoJSON";
import VectorSource from "ol/source/Vector"; import VectorSource from "ol/source/Vector";
import VectorTileSource from "ol/source/VectorTile"; import VectorTileSource from "ol/source/VectorTile";
@@ -49,34 +46,12 @@ const createIconStyle = (src: string, scale = 0.1) =>
const scadaStyle = (feature: FeatureLike) => const scadaStyle = (feature: FeatureLike) =>
createIconStyle( createIconStyle(
feature.get("type") === "pipe_flow" feature.get("device_type") === "pipe_flow"
? "/icons/scada_flow.svg" ? "/icons/scada_flow.svg"
: "/icons/scada_pressure.svg", : "/icons/scada_pressure.svg",
); );
const pumpStyle = (feature: FeatureLike) => { const pumpStyle = () => createIconStyle("/icons/pump.svg", 0.12);
const geometry = feature.getGeometry();
if (!geometry || geometry.getType() !== "LineString") return [];
const coordinates = (geometry as any)
.getCoordinates()
.map((coordinate: number[]) => toLonLat(coordinate));
if (coordinates.length < 2) return [];
const featureLine = lineString(coordinates);
const midpoint = along(featureLine, length(featureLine) / 2).geometry
.coordinates;
return [
new Style({
geometry: new Point(toMercator(midpoint)),
image: new Icon({
src: "/icons/pump.svg",
scale: 0.12,
anchor: [0.5, 0.5],
}),
}),
];
};
const pointProperties = [ const pointProperties = [
{ name: "高程", value: "elevation" }, { name: "高程", value: "elevation" },
@@ -110,34 +85,34 @@ export const createOperationalMapSources = ({
return { return {
junctions: new VectorTileSource({ junctions: new VectorTileSource({
url: vectorTileUrl("geo_junctions"), url: vectorTileUrl("junctions"),
format: new MVT(), format: new MVT(),
projection: "EPSG:3857", projection: "EPSG:3857",
}), }),
pipes: new VectorTileSource({ pipes: new VectorTileSource({
url: vectorTileUrl("geo_pipes"), url: vectorTileUrl("pipes"),
format: new MVT(), format: new MVT(),
projection: "EPSG:3857", projection: "EPSG:3857",
}), }),
valves: new VectorTileSource({ valves: new VectorTileSource({
url: vectorTileUrl("geo_valves"), url: vectorTileUrl("valves"),
format: new MVT(), format: new MVT(),
projection: "EPSG:3857", projection: "EPSG:3857",
}), }),
reservoirs: new VectorSource({ reservoirs: new VectorSource({
url: vectorUrl("geo_reservoirs"), url: vectorUrl("reservoirs"),
format: new GeoJSON(), format: new GeoJSON(),
}), }),
pumps: new VectorSource({ pumps: new VectorSource({
url: vectorUrl("geo_pumps"), url: vectorUrl("pumps"),
format: new GeoJSON(), format: new GeoJSON(),
}), }),
tanks: new VectorSource({ tanks: new VectorSource({
url: vectorUrl("geo_tanks"), url: vectorUrl("tanks"),
format: new GeoJSON(), format: new GeoJSON(),
}), }),
scada: new VectorSource({ scada: new VectorSource({
url: vectorUrl("geo_scada"), url: vectorUrl("scada_devices"),
format: new GeoJSON(), format: new GeoJSON(),
}), }),
}; };
@@ -187,7 +162,7 @@ export const createOperationalMapResources = ({
properties: { properties: {
name: "阀门", name: "阀门",
value: "valves", value: "valves",
type: "linestring", type: "point",
properties: [], properties: [],
}, },
}), }),
@@ -213,7 +188,7 @@ export const createOperationalMapResources = ({
properties: { properties: {
name: "水泵", name: "水泵",
value: "pumps", value: "pumps",
type: "linestring", type: "point",
properties: [], properties: [],
}, },
}), }),
+3 -3
View File
@@ -27,14 +27,14 @@ const parseMapExtent = (value: RuntimeConfig["MAP_EXTENT"]): number[] => {
return value.split(",").map(Number); return value.split(",").map(Number);
} }
return [13508849, 3608036, 13555781, 3633813]; return [13508801.93, 3608163.35, 13555650.64, 3633685.14];
}; };
export const config = { export const config = {
BACKEND_URL: runtimeConfig.BACKEND_URL || "http://127.0.0.1:8000", BACKEND_URL: runtimeConfig.BACKEND_URL || "http://127.0.0.1:8000",
AGENT_URL: runtimeConfig.AGENT_URL || "http://127.0.0.1:8788", AGENT_URL: runtimeConfig.AGENT_URL || "http://127.0.0.1:8788",
MAP_URL: runtimeConfig.MAP_URL || "http://127.0.0.1:8080/geoserver", MAP_URL: runtimeConfig.MAP_URL || "http://127.0.0.1:8080/geoserver",
MAP_WORKSPACE: runtimeConfig.MAP_WORKSPACE || "tjwater", MAP_WORKSPACE: runtimeConfig.MAP_WORKSPACE || "tjwater_next",
MAP_EXTENT: parseMapExtent(runtimeConfig.MAP_EXTENT), MAP_EXTENT: parseMapExtent(runtimeConfig.MAP_EXTENT),
MAP_DEFAULT_STYLE: { MAP_DEFAULT_STYLE: {
"stroke-width": 3, "stroke-width": 3,
@@ -61,7 +61,7 @@ export const config = {
"scada", "scada",
], ],
}; };
export let NETWORK_NAME = runtimeConfig.NETWORK_NAME || "tjwater"; export let NETWORK_NAME = runtimeConfig.NETWORK_NAME || "tjwater_next";
export const setNetworkName = (name: string) => { export const setNetworkName = (name: string) => {
NETWORK_NAME = name; NETWORK_NAME = name;
+960 -10008
View File
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
import { api } from "@/lib/api";
import {
getAnalysisResults,
getAnalysisScheme,
listAnalysisSchemes,
} from "./analysisRuns";
jest.mock("@/lib/api", () => ({
api: { get: jest.fn() },
}));
const get = api.get as jest.Mock;
describe("analysis runs API adapter", () => {
beforeEach(() => get.mockReset());
it("filters runs and exposes the new run identity to scheme screens", async () => {
get.mockResolvedValue({
data: {
success: true,
count: 2,
data: [
{
run_id: "run-1",
name: "burst-a",
run_type: "burst_analysis",
created_by: "alice",
created_at: "2026-08-25T02:00:00Z",
started_at: "2026-08-25T01:00:00Z",
status: "completed",
parameters: { burst_ID: ["P-1"] },
},
{
run_id: "run-2",
name: "flush-a",
run_type: "flushing_analysis",
created_by: "bob",
created_at: "2026-08-24T02:00:00Z",
started_at: "2026-08-24T01:00:00Z",
status: "completed",
parameters: {},
},
],
},
});
const result = await listAnalysisSchemes({
runType: "burst_analysis",
queryDate: "2026-08-25",
});
expect(get).toHaveBeenCalledWith("/api/v1/analysis/runs");
expect(result).toEqual([
expect.objectContaining({
id: "run-1",
scheme_id: "run-1",
scheme_name: "burst-a",
schemeDetail: { burst_ID: ["P-1"] },
}),
]);
});
it("loads a run and its structured results by run id", async () => {
get
.mockResolvedValueOnce({
data: {
run_id: "run-1",
name: "burst-a",
run_type: "burst_analysis",
created_by: "alice",
created_at: "2026-08-25T02:00:00Z",
started_at: "2026-08-25T01:00:00Z",
status: "completed",
parameters: {},
},
})
.mockResolvedValueOnce({
data: [
{
result_id: "result-1",
run_id: "run-1",
result_type: "summary",
payload: { ok: true },
created_at: "2026-08-25T03:00:00Z",
},
],
});
expect((await getAnalysisScheme("run-1")).scheme_id).toBe("run-1");
expect(await getAnalysisResults("run-1", "summary")).toHaveLength(1);
expect(get).toHaveBeenLastCalledWith(
"/api/v1/analysis/runs/run-1/results",
{ params: { result_type: "summary" } },
);
});
});
+105
View File
@@ -0,0 +1,105 @@
import { api } from "@/lib/api";
export type AnalysisRun = {
run_id: string;
name: string;
run_type: string;
created_by: string;
created_at: string;
started_at: string;
status: string;
parameters: Record<string, unknown>;
};
export type AnalysisResult = {
result_id: string;
run_id: string;
result_type: string;
node_id?: string | null;
link_id?: string | null;
payload: Record<string, unknown>;
created_at: string;
};
type AnalysisRunListResponse = {
success: boolean;
data: AnalysisRun[];
count: number;
};
export type AnalysisSchemeRecord = AnalysisRun & {
id: string;
scheme_id: string;
scheme_name: string;
scheme_type: string;
username: string;
create_time: string;
scheme_start_time: string;
scheme_detail: Record<string, unknown>;
schemeName: string;
type: string;
startTime: string;
schemeDetail: Record<string, unknown>;
};
const normalizeParameters = (value: unknown): Record<string, unknown> =>
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
export const toAnalysisSchemeRecord = (run: AnalysisRun): AnalysisSchemeRecord => {
const parameters = normalizeParameters(run.parameters);
return {
...run,
parameters,
id: run.run_id,
scheme_id: run.run_id,
scheme_name: run.name,
scheme_type: run.run_type,
username: run.created_by,
create_time: run.created_at,
scheme_start_time: run.started_at,
scheme_detail: parameters,
schemeName: run.name,
type: run.run_type,
startTime: run.started_at,
schemeDetail: parameters,
};
};
export const listAnalysisSchemes = async ({
runType,
queryDate,
}: {
runType?: string;
queryDate?: string;
} = {}): Promise<AnalysisSchemeRecord[]> => {
const response = await api.get<AnalysisRunListResponse>(
"/api/v1/analysis/runs",
);
const runs = Array.isArray(response.data.data) ? response.data.data : [];
return runs
.filter((run) => !runType || run.run_type === runType)
.filter((run) => !queryDate || run.created_at.slice(0, 10) === queryDate)
.map(toAnalysisSchemeRecord);
};
export const getAnalysisScheme = async (
runId: string,
): Promise<AnalysisSchemeRecord> => {
const response = await api.get<AnalysisRun>(
`/api/v1/analysis/runs/${encodeURIComponent(runId)}`,
);
return toAnalysisSchemeRecord(response.data);
};
export const getAnalysisResults = async (
runId: string,
resultType?: string,
): Promise<AnalysisResult[]> => {
const response = await api.get<AnalysisResult[]>(
`/api/v1/analysis/runs/${encodeURIComponent(runId)}/results`,
{ params: resultType ? { result_type: resultType } : undefined },
);
return Array.isArray(response.data) ? response.data : [];
};
+1 -2
View File
@@ -29,8 +29,7 @@ const FIELD_LABELS: Record<string, string> = {
scada_burst_start: "爆管开始时间", scada_burst_start: "爆管开始时间",
scada_burst_end: "爆管结束时间", scada_burst_end: "爆管结束时间",
use_scada_flow: "使用流量监测数据", use_scada_flow: "使用流量监测数据",
simulation_scheme_name: "模拟方案名称", simulation_run_id: "模拟运行 ID",
simulation_scheme_type: "模拟方案类型",
source: "污染源节点", source: "污染源节点",
concentration: "污染物浓度", concentration: "污染物浓度",
pattern: "污染物注入模式", pattern: "污染物注入模式",
+1 -4
View File
@@ -45,7 +45,7 @@ interface MapClickEvent {
const getGeoserverConfig = () => ({ const getGeoserverConfig = () => ({
url: config.MAP_URL, url: config.MAP_URL,
workspace: config.MAP_WORKSPACE, workspace: config.MAP_WORKSPACE,
layers: ["geo_pipes_mat", "geo_junctions_mat", "geo_valves"], layers: ["pipes", "junctions", "valves"],
wfsVersion: "1.0.0", wfsVersion: "1.0.0",
outputFormat: "application/json", outputFormat: "application/json",
}); });
@@ -489,9 +489,6 @@ const handleMapClickSelectFeatures = async (
// 如果要素来自 VectorTileSource,需要通过 WFS 查询完整信息 // 如果要素来自 VectorTileSource,需要通过 WFS 查询完整信息
const queryId = firstFeature.getProperties().id; const queryId = firstFeature.getProperties().id;
const layerName = firstFeature.getProperties().layer; const layerName = firstFeature.getProperties().layer;
if (layerName === "geo_pipes" || layerName === "geo_junctions") {
layerName.concat("_mat");
}
if (!queryId) { if (!queryId) {
return null; return null;
} }