diff --git a/docker/influxdb/docker-compose.yml b/docker/influxdb/docker-compose.yml deleted file mode 100644 index 1000fe1..0000000 --- a/docker/influxdb/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - influxdb: - image: influxdb:2.7 - container_name: influxdb - environment: - DOCKER_INFLUXDB_INIT_MODE: setup - DOCKER_INFLUXDB_INIT_USERNAME: tjwater - DOCKER_INFLUXDB_INIT_PASSWORD: Tjwater@123456 - DOCKER_INFLUXDB_INIT_ORG: TJWATERORG - DOCKER_INFLUXDB_INIT_BUCKET: TJWATERBUCKET - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: kMPX2V5HsbzPpUT2B9HPBu1sTG1Emf-lPlT2UjxYnGAuocpXq_f_0lK4HHs-TbbKyjsZpICkMsyXG_V2D7P7yQ== - ports: - - "8086:8086" - volumes: - - C:\Users\admin\Documents\docker\influxdb\data:/var/lib/influxdb2 diff --git a/docker/keycloak/docker-compose.yml b/docker/keycloak/docker-compose.yml deleted file mode 100644 index 87baf6f..0000000 --- a/docker/keycloak/docker-compose.yml +++ /dev/null @@ -1,47 +0,0 @@ -services: - postgres: - image: postgis/postgis:14-3.5 - container_name: keycloakDB - environment: - POSTGRES_DB: keycloak - POSTGRES_USER: keycloak - POSTGRES_PASSWORD: keycloak - ports: - - "5434:5432" - volumes: - - C:\Users\admin\Documents\docker\keycloakDB\data:/var/lib/postgresql/data - networks: - - keycloak - - keycloak: - image: keycloak/keycloak:latest - container_name: keycloak - environment: - KC_HOSTNAME: localhost - KC_HOSTNAME_STRICT_BACKCHANNEL: "true" - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - KC_HEALTH_ENABLED: "true" - KC_LOG_LEVEL: info - KC_DB: postgres - KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak - KC_DB_USERNAME: keycloak - KC_DB_PASSWORD: keycloak - volumes: - - C:\Users\admin\Documents\docker\keycloak\themes:/opt/keycloak/themes - - C:\Users\admin\Documents\docker\keycloak\import:/opt/keycloak/data/import - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:8080/health/ready" ] - interval: 15s - timeout: 2s - retries: 15 - command: [ "start-dev", "--import-realm" ] - ports: - - "8088:8080" - depends_on: - - postgres - networks: - - keycloak -networks: - keycloak: - driver: bridge diff --git a/docker/mapservice/docker-compose.yml b/docker/mapservice/docker-compose.yml deleted file mode 100644 index 04d49b2..0000000 --- a/docker/mapservice/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -services: - postgis: - image: postgis/postgis:14-3.5 - container_name: postgis - environment: - POSTGRES_DB: TJWater - POSTGRES_USER: tjwater - POSTGRES_PASSWORD: Tjwater@123456 - ports: - - "5432:5432" - volumes: - - C:\Users\admin\Documents\docker\PostgreSQL\data:/var/lib/postgresql/data - networks: - - MapService - - geoserver: - image: docker.osgeo.org/geoserver:2.27.1 - container_name: geoserver - ports: - - "8080:8080" - depends_on: - - postgis - environment: - - GEOSERVER_ADMIN_USER=admin - - GEOSERVER_ADMIN_PASSWORD=geoserver - - INSTALL_EXTENSIONS=true - - STABLE_EXTENSIONS=css,vectortiles - - CORS_ENABLED=true - - CORS_ALLOWED_ORIGINS=* - volumes: - - C:\Users\admin\Documents\docker\GeoServer\data:/opt/geoserver_data - networks: - - MapService - -networks: - MapService: - driver: bridge \ No newline at end of file diff --git a/docker/redis/docker-compose.yml b/docker/redis/docker-compose.yml deleted file mode 100644 index bd16c2b..0000000 --- a/docker/redis/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - redis: - image: redis:8.2 - container_name: redis - # environment: - # REDIS_PASSWORD: Tjwater@123456 - ports: - - "6379:6379" - volumes: - - C:\Users\admin\Documents\docker\redis\data:/data \ No newline at end of file diff --git a/docker/timescaledb/docker-compose.yml b/docker/timescaledb/docker-compose.yml deleted file mode 100644 index e089e2a..0000000 --- a/docker/timescaledb/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -services: - timescaledb: - image: timescale/timescaledb:latest-pg15 - container_name: timescaledb - environment: - POSTGRES_DB: TJWater - POSTGRES_USER: tjwater - POSTGRES_PASSWORD: Tjwater@123456 - ports: - - "5435:5432" - volumes: - - C:\Users\admin\Documents\docker\serverdb\Timescaledb\data:/var/lib/postgresql/data - - grafana: - image: grafana/grafana:latest - container_name: grafana - ports: - - "3035:3000" - depends_on: - - timescaledb - volumes: - - C:\Users\admin\Documents\docker\serverdb\Grafana\data:/var/lib/grafana - environment: - - GF_SECURITY_ADMIN_USER=tjwater # 设置管理员用户名 - - GF_SECURITY_ADMIN_PASSWORD=Tjwater@123456 # 设置管理员密码 diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index 2df14bb..0e97b37 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -36,6 +36,7 @@ interface TimelineProps { timeRange?: { start: Date; end: Date }; disableDateSelection?: boolean; schemeName?: string; + schemeType?: string; } const Timeline: React.FC = ({ @@ -43,6 +44,7 @@ const Timeline: React.FC = ({ timeRange, disableDateSelection = false, schemeName = "", + schemeType = "burst_Analysis", }) => { const data = useData(); if (!data) { @@ -100,7 +102,8 @@ const Timeline: React.FC = ({ queryTime: Date, junctionProperties: string, pipeProperties: string, - schemeName: string + schemeName: string, + schemeType: string ) => { const query_time = queryTime.toISOString(); let nodeRecords: any = { results: [] }; @@ -110,14 +113,14 @@ const Timeline: React.FC = ({ let linkPromise: Promise | null = null; // 检查node缓存 if (junctionProperties !== "" && junctionProperties !== "elevation") { - const nodeCacheKey = `${query_time}_${junctionProperties}_${schemeName}`; + const nodeCacheKey = `${query_time}_${junctionProperties}_${schemeName}_${schemeType}`; if (nodeCacheRef.current.has(nodeCacheKey)) { nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!; } else { disableDateSelection && schemeName ? (nodePromise = fetch( // `${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${schemeName}` - `${backendUrl}/timescaledb/scheme/query/by-scheme-time-property?scheme_type=burst_Analysis&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}` + `${backendUrl}/timescaledb/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}` )) : (nodePromise = fetch( // `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` @@ -131,14 +134,14 @@ const Timeline: React.FC = ({ // 检查link缓存 if (pipeProperties !== "" && pipeProperties !== "diameter") { - const linkCacheKey = `${query_time}_${pipeProperties}_${schemeName}`; + const linkCacheKey = `${query_time}_${pipeProperties}_${schemeName}_${schemeType}`; if (linkCacheRef.current.has(linkCacheKey)) { linkRecords = linkCacheRef.current.get(linkCacheKey)!; } else { disableDateSelection && schemeName ? (linkPromise = fetch( // `${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${schemeName}` - `${backendUrl}/timescaledb/scheme/query/by-scheme-time-property?scheme_type=burst_Analysis&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${pipeProperties}` + `${backendUrl}/timescaledb/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${pipeProperties}` )) : (linkPromise = fetch( // `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` @@ -158,7 +161,7 @@ const Timeline: React.FC = ({ nodeRecords = await nodeResponse.json(); // 缓存数据(修复键以包含 schemeName) nodeCacheRef.current.set( - `${query_time}_${junctionProperties}_${schemeName}`, + `${query_time}_${junctionProperties}_${schemeName}_${schemeType}`, nodeRecords || [] ); } @@ -169,7 +172,7 @@ const Timeline: React.FC = ({ linkRecords = await linkResponse.json(); // 缓存数据(修复键以包含 schemeName) linkCacheRef.current.set( - `${query_time}_${pipeProperties}_${schemeName}`, + `${query_time}_${pipeProperties}_${schemeName}_${schemeType}`, linkRecords || [] ); } @@ -389,10 +392,11 @@ const Timeline: React.FC = ({ currentTimeToDate(selectedDate, currentTime), junctionText, pipeText, - schemeName + schemeName, + schemeType ); } - }, [junctionText, pipeText, currentTime, selectedDate]); + }, [junctionText, pipeText, currentTime, selectedDate, schemeName, schemeType]); // 组件卸载时清理定时器和防抖 useEffect(() => { @@ -469,7 +473,8 @@ const Timeline: React.FC = ({ currentTimeToDate(selectedDate, currentTime), junctionText, pipeText, - schemeName + schemeName, + schemeType ); }; diff --git a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx index 22257ab..3773d9e 100644 --- a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx +++ b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Box, Drawer, @@ -20,9 +20,13 @@ import { import AnalysisParameters from "./AnalysisParameters"; import SchemeQuery from "./SchemeQuery"; import LocationResults, { LocationResult } from "./LocationResults"; +import ContaminantAnalysisParameters from "../ContaminantSimulation/AnalysisParameters"; +import ContaminantSchemeQuery from "../ContaminantSimulation/SchemeQuery"; +import ContaminantResultsPanel from "../ContaminantSimulation/ResultsPanel"; import axios from "axios"; import { config } from "@config/config"; import { useNotification } from "@refinedev/core"; +import { useData } from "@app/OlMap/MapComponent"; interface SchemeDetail { burst_ID: string[]; burst_size: number[]; @@ -68,12 +72,20 @@ interface BurstPipeAnalysisPanelProps { onToggle?: () => void; } +type PanelMode = "burst" | "contaminant"; + const BurstPipeAnalysisPanel: React.FC = ({ open: controlledOpen, onToggle, }) => { const [internalOpen, setInternalOpen] = useState(true); const [currentTab, setCurrentTab] = useState(0); + const [panelMode, setPanelMode] = useState("burst"); + const previousMapText = useRef<{ junction?: string; pipe?: string } | null>( + null + ); + + const data = useData(); // 持久化方案查询结果 const [schemes, setSchemes] = useState([]); @@ -96,6 +108,24 @@ const BurstPipeAnalysisPanel: React.FC = ({ setCurrentTab(newValue); }; + useEffect(() => { + if (!data) return; + if (panelMode === "contaminant") { + if (!previousMapText.current) { + previousMapText.current = { + junction: data.junctionText, + pipe: data.pipeText, + }; + } + data.setJunctionText?.("quality"); + data.setPipeText?.("quality"); + } else if (panelMode === "burst" && previousMapText.current) { + data.setJunctionText?.(previousMapText.current.junction || "pressure"); + data.setPipeText?.(previousMapText.current.pipe || "flow"); + previousMapText.current = null; + } + }, [panelMode, data]); + const handleLocateScheme = async (scheme: SchemeRecord) => { try { const response = await axios.get( @@ -114,6 +144,8 @@ const BurstPipeAnalysisPanel: React.FC = ({ }; const drawerWidth = 520; + const isBurstMode = panelMode === "burst"; + const panelTitle = isBurstMode ? "爆管分析" : "水质模拟"; return ( <> @@ -131,7 +163,7 @@ const BurstPipeAnalysisPanel: React.FC = ({ className="text-gray-700 font-semibold my-1 text-xs" style={{ writingMode: "vertical-rl" }} > - 爆管分析 + {panelTitle} @@ -175,7 +207,7 @@ const BurstPipeAnalysisPanel: React.FC = ({ - 爆管分析 + {panelTitle} @@ -190,6 +222,31 @@ const BurstPipeAnalysisPanel: React.FC = ({ {/* Tabs 导航 */} + + setPanelMode(value)} + variant="fullWidth" + sx={{ + minHeight: 46, + "& .MuiTab-root": { + minHeight: 46, + textTransform: "none", + fontSize: "0.8rem", + fontWeight: 600, + }, + "& .Mui-selected": { + color: "#257DD4", + }, + "& .MuiTabs-indicator": { + backgroundColor: "#257DD4", + }, + }} + > + + + + = ({ } iconPosition="start" - label="定位结果" + label={isBurstMode ? "定位结果" : "模拟结果"} /> {/* Tab 内容 */} - + {isBurstMode ? ( + + ) : ( + + )} - + {isBurstMode ? ( + + ) : ( + + )} - + {isBurstMode ? ( + + ) : ( + + )} diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index 51c4cb5..886e0ee 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -336,6 +336,7 @@ const SchemeQuery: React.FC = ({ timeRange={timeRange} disableDateSelection={!!timeRange} schemeName={schemeName} + schemeType="burst_Analysis" />, mapContainer // 渲染到地图容器中,而不是 body )} diff --git a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx new file mode 100644 index 0000000..86996f4 --- /dev/null +++ b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx @@ -0,0 +1,367 @@ +"use client"; + +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Box, + Button, + IconButton, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { Close as CloseIcon } from "@mui/icons-material"; +import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import "dayjs/locale/zh-cn"; +import dayjs, { Dayjs } from "dayjs"; +import { useNotification } from "@refinedev/core"; +import axios from "axios"; +import { config, NETWORK_NAME } from "@/config/config"; +import { useMap } from "@app/OlMap/MapComponent"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style"; +import Feature from "ol/Feature"; +import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; + +const AnalysisParameters: React.FC = () => { + const map = useMap(); + const { open } = useNotification(); + + const network = NETWORK_NAME; + const [startTime, setStartTime] = useState(dayjs(new Date())); + const [sourceNode, setSourceNode] = useState(""); + const [concentration, setConcentration] = useState(1); + const [duration, setDuration] = useState(900); + const [pattern, setPattern] = useState(""); + const [isSelecting, setIsSelecting] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const [highlightLayer, setHighlightLayer] = + useState | null>(null); + const [highlightFeature, setHighlightFeature] = useState( + null + ); + + const isFormValid = useMemo(() => { + return ( + Boolean(network) && + Boolean(startTime) && + Boolean(sourceNode) && + concentration > 0 && + duration > 0 + ); + }, [network, startTime, sourceNode, concentration, duration]); + + useEffect(() => { + if (!map) return; + + const sourceStyle = new Style({ + image: new CircleStyle({ + radius: 10, + fill: new Fill({ color: "rgba(37, 125, 212, 0.35)" }), + stroke: new Stroke({ color: "rgba(37, 125, 212, 1)", width: 3 }), + }), + }); + + const layer = new VectorLayer({ + source: new VectorSource(), + style: sourceStyle, + properties: { + name: "污染源节点", + value: "contaminant_source_highlight", + }, + }); + + map.addLayer(layer); + setHighlightLayer(layer); + + return () => { + map.removeLayer(layer); + map.un("click", handleMapClickSelectFeatures); + }; + }, [map]); + + useEffect(() => { + if (!highlightLayer) return; + const source = highlightLayer.getSource(); + if (!source) return; + source.clear(); + if (highlightFeature) { + source.addFeature(highlightFeature); + } + }, [highlightFeature, highlightLayer]); + + const handleMapClickSelectFeatures = useCallback( + async (event: { coordinate: number[] }) => { + if (!map) return; + const feature = await mapClickSelectFeatures(event, map); + if (!feature) return; + + const layerId = feature.getId()?.toString().split(".")[0] || ""; + const isJunction = layerId.includes("junction"); + if (!isJunction) { + open?.({ + type: "error", + message: "请选择节点类型要素作为污染源。", + }); + return; + } + + const id = feature.getProperties().id; + if (!id) return; + setSourceNode(id); + setHighlightFeature(feature); + setIsSelecting(false); + map.un("click", handleMapClickSelectFeatures); + }, + [map, open] + ); + + const handleStartSelection = () => { + if (!map) return; + setIsSelecting(true); + map.on("click", handleMapClickSelectFeatures); + }; + + const handleEndSelection = () => { + if (!map) return; + setIsSelecting(false); + map.un("click", handleMapClickSelectFeatures); + }; + + const handleClearSource = () => { + setSourceNode(""); + setHighlightFeature(null); + }; + + const handleAnalyze = async () => { + if (!startTime) return; + setSubmitting(true); + open?.({ + key: "contaminant-analysis", + type: "progress", + message: "方案提交分析中", + undoableTimeout: 3, + }); + + try { + const params = { + network, + start_time: startTime.toISOString(), + source: sourceNode, + concentration, + duration, + pattern: pattern || undefined, + }; + + await axios.get(`${config.BACKEND_URL}/contaminant_simulation/`, { + params, + }); + + open?.({ + key: "contaminant-analysis", + type: "success", + message: "方案分析成功", + description: "水质模拟完成,请在方案查询中查看结果。", + }); + } catch (error) { + console.error("水质模拟请求失败:", error); + open?.({ + key: "contaminant-analysis", + type: "error", + message: "提交分析失败", + description: + error instanceof Error ? error.message : "请检查网络连接或稍后重试", + }); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + 选择污染源节点 + + {!isSelecting ? ( + + ) : ( + + )} + + + {isSelecting && ( + + 💡 点击地图上的节点作为污染源 + + )} + + + {sourceNode ? ( + + + {sourceNode} + + + 污染源节点 + + + + + + ) : ( + + 暂未选择污染源节点 + + )} + + + + + + 选择开始时间 + + + + value && dayjs.isDayjs(value) && setStartTime(value) + } + format="YYYY-MM-DD HH:mm" + slotProps={{ + textField: { + size: "small", + fullWidth: true, + }, + }} + localeText={ + pickerZhCN.components.MuiLocalizationProvider.defaultProps + .localeText + } + /> + + + + + + 管网名称 + + + + + + + 污染源浓度 (mg/L) + + setConcentration(parseFloat(e.target.value) || 0)} + placeholder="输入浓度" + /> + + + + + 持续时长 (秒) + + setDuration(parseInt(e.target.value, 10) || 0)} + placeholder="输入持续时长" + /> + + + + + 时间模式 + + setPattern(e.target.value)} + placeholder="可选,输入 pattern 名称" + /> + + + + + + + ); +}; + +export default AnalysisParameters; diff --git a/src/components/olmap/ContaminantSimulation/ResultsPanel.tsx b/src/components/olmap/ContaminantSimulation/ResultsPanel.tsx new file mode 100644 index 0000000..92aae8e --- /dev/null +++ b/src/components/olmap/ContaminantSimulation/ResultsPanel.tsx @@ -0,0 +1,30 @@ +"use client"; + +import React from "react"; +import { Box, Typography } from "@mui/material"; + +interface ResultsPanelProps { + schemeName?: string; +} + +const ResultsPanel: React.FC = ({ schemeName }) => { + return ( + + + + 水质模拟结果 + + + 请在下方时间轴查看各时刻的水质分布。 + + {schemeName && ( + + 当前方案:{schemeName} + + )} + + + ); +}; + +export default ResultsPanel; diff --git a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx new file mode 100644 index 0000000..429e74c --- /dev/null +++ b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx @@ -0,0 +1,511 @@ +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; +import ReactDOM from "react-dom"; +import { + Box, + Button, + Card, + CardContent, + Checkbox, + Chip, + Collapse, + FormControlLabel, + IconButton, + Link, + Tooltip, + Typography, +} from "@mui/material"; +import { + Info as InfoIcon, + LocationOn as LocationIcon, +} from "@mui/icons-material"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import "dayjs/locale/zh-cn"; +import dayjs, { Dayjs } from "dayjs"; +import axios from "axios"; +import moment from "moment"; +import { useNotification } from "@refinedev/core"; +import { config, NETWORK_NAME } from "@config/config"; +import { queryFeaturesByIds } from "@/utils/mapQueryService"; +import { useData, useMap } from "@app/OlMap/MapComponent"; +import Timeline from "@app/OlMap/Controls/Timeline"; +import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types"; + +interface SchemeQueryProps { + schemes?: ContaminantSchemeRecord[]; + onSchemesChange?: (schemes: ContaminantSchemeRecord[]) => void; + network?: string; +} + +const SCHEME_TYPE = "contaminant_simulation"; + +const SchemeQuery: React.FC = ({ + schemes: externalSchemes, + onSchemesChange, + network = NETWORK_NAME, +}) => { + const [queryAll, setQueryAll] = useState(true); + const [queryDate, setQueryDate] = useState(dayjs(new Date())); + const [showTimeline, setShowTimeline] = useState(false); + const [selectedDate, setSelectedDate] = useState(undefined); + const [timeRange, setTimeRange] = useState< + { start: Date; end: Date } | undefined + >(); + const [internalSchemes, setInternalSchemes] = useState< + ContaminantSchemeRecord[] + >([]); + const [loading, setLoading] = useState(false); + const [expandedId, setExpandedId] = useState(null); + const [mapContainer, setMapContainer] = useState(null); + + const { open } = useNotification(); + const map = useMap(); + const data = useData(); + const { schemeName, setSchemeName, setJunctionText, setPipeText } = + data || {}; + + const schemes = + externalSchemes !== undefined ? externalSchemes : internalSchemes; + const setSchemes = onSchemesChange || setInternalSchemes; + + useEffect(() => { + if (!map) return; + const target = map.getTargetElement(); + if (target) { + setMapContainer(target); + } + }, [map]); + + const formatTime = (timeStr: string) => { + const time = moment(timeStr); + return time.format("MM-DD"); + }; + + const filteredSchemes = useMemo(() => { + return schemes.filter((scheme) => scheme.type === SCHEME_TYPE); + }, [schemes]); + + const handleQuery = async () => { + if (!queryAll && !queryDate) return; + setLoading(true); + try { + const response = await axios.get( + `${config.BACKEND_URL}/getallschemes/?network=${network}` + ); + 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; + }); + } + + setSchemes( + filteredResults.map((item: ContaminantSchemaItem) => ({ + id: item.scheme_id, + schemeName: item.scheme_name, + type: item.scheme_type, + user: item.username, + create_time: item.create_time, + startTime: item.scheme_start_time, + schemeDetail: item.scheme_detail, + })) + ); + + if (filteredResults.length === 0) { + open?.({ + type: "error", + message: "查询结果", + description: queryAll + ? "没有找到任何方案" + : `${queryDate!.format("YYYY-MM-DD")} 没有找到相关方案`, + }); + } + } catch (error) { + console.error("查询请求失败:", error); + open?.({ + type: "error", + message: "查询失败", + description: "获取方案列表失败,请稍后重试", + }); + } finally { + setLoading(false); + } + }; + + const handleLocateSource = (sourceId?: string) => { + if (!sourceId) return; + queryFeaturesByIds([sourceId], "geo_junctions_mat").then((features) => { + if (features.length > 0) { + const extent = features[0].getGeometry()?.getExtent(); + if (extent) { + map?.getView().fit(extent, { + maxZoom: 18, + duration: 1000, + }); + } + } + }); + }; + + const handleViewDetails = (id: number) => { + const scheme = filteredSchemes.find((s) => s.id === id); + if (!scheme) return; + + setShowTimeline(true); + const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined; + if (scheme.startTime && scheme.schemeDetail?.duration) { + const start = new Date(scheme.startTime); + const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000); + setSelectedDate(schemeDate); + setTimeRange({ start, end }); + } + setSchemeName?.(scheme.schemeName); + setJunctionText?.("quality"); + setPipeText?.("quality"); + handleLocateSource(scheme.schemeDetail?.source); + }; + + return ( + <> + {showTimeline && + mapContainer && + ReactDOM.createPortal( + , + mapContainer + )} + + + + + setQueryAll(e.target.checked)} + size="small" + /> + } + label={查询全部} + className="m-0" + /> + + + value && dayjs.isDayjs(value) && setQueryDate(value) + } + format="YYYY-MM-DD" + disabled={queryAll} + slotProps={{ + textField: { + size: "small", + sx: { width: 200 }, + }, + }} + /> + + + + + + + + {filteredSchemes.length === 0 ? ( + + + + + + + + 总共 0 条 + + No data + + + ) : ( + + + 共 {filteredSchemes.length} 条记录 + + {filteredSchemes.map((scheme) => ( + + + + + + + {scheme.schemeName} + + + + + ID: {scheme.id} · 日期: {formatTime(scheme.create_time)} + + + + + + setExpandedId( + expandedId === scheme.id ? null : scheme.id + ) + } + color="primary" + className="p-1" + > + + + + + handleLocateSource(scheme.schemeDetail?.source)} + color="primary" + className="p-1" + > + + + + + + + + + + + + + + 污染源: + + + {scheme.schemeDetail?.source ? ( + { + e.preventDefault(); + handleLocateSource(scheme.schemeDetail?.source); + }} + > + {scheme.schemeDetail.source} + + ) : ( + + N/A + + )} + + + + + 浓度: + + + {scheme.schemeDetail?.concentration ?? "N/A"} mg/L + + + + + 持续时间: + + + {scheme.schemeDetail?.duration ?? "N/A"} 秒 + + + + + 模式: + + + {scheme.schemeDetail?.pattern || "无"} + + + + + + + + + + 用户: + + + {scheme.user} + + + + + 创建时间: + + + {moment(scheme.create_time).format( + "YYYY-MM-DD HH:mm" + )} + + + + + 开始时间: + + + {moment(scheme.startTime).format( + "YYYY-MM-DD HH:mm" + )} + + + + + + + + + + + + + + + ))} + + )} + + + + ); +}; + +export default SchemeQuery; diff --git a/src/components/olmap/ContaminantSimulation/types.ts b/src/components/olmap/ContaminantSimulation/types.ts new file mode 100644 index 0000000..407c08d --- /dev/null +++ b/src/components/olmap/ContaminantSimulation/types.ts @@ -0,0 +1,27 @@ +export interface ContaminantSchemeDetail { + source: string; + concentration: number; + duration: number; + pattern?: string | null; + start_time?: string; +} + +export interface ContaminantSchemeRecord { + id: number; + schemeName: string; + type: string; + user: string; + create_time: string; + startTime: string; + 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; +}