前端项目结构调整
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
Map as MapIcon,
|
||||
} from "@mui/icons-material";
|
||||
import dayjs from "dayjs";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import Feature from "ol/Feature";
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ 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 { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import { Style, Stroke, Icon } from "ol/style";
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
LocationOn as LocationIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
+2
-2
@@ -32,7 +32,7 @@ import { config, NETWORK_NAME } from "@config/config";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import { useData, useMap } from "@app/OlMap/MapComponent";
|
||||
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
} from "@turf/turf";
|
||||
import { Point } from "ol/geom";
|
||||
import { toLonLat } from "ol/proj";
|
||||
import Timeline from "@app/OlMap/Controls/Timeline";
|
||||
import Timeline from "@components/olmap/core/Controls/Timeline";
|
||||
import { SchemaItem, SchemeRecord } from "./types";
|
||||
|
||||
interface SchemeQueryProps {
|
||||
+1
-1
@@ -35,7 +35,7 @@ import {
|
||||
queryFeaturesByIds,
|
||||
handleMapClickSelectFeatures,
|
||||
} from "@/utils/mapQueryService";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
@@ -19,7 +19,7 @@ import dayjs, { Dayjs } from "dayjs";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { api } from "@/lib/api";
|
||||
import { config, NETWORK_NAME } from "@/config/config";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style";
|
||||
|
||||
@@ -30,14 +30,14 @@ 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 { useData, useMap } from "@components/olmap/core/MapComponent";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import { Style, Icon, Circle, Fill, Stroke } from "ol/style";
|
||||
import Feature from "ol/Feature";
|
||||
import { bbox, featureCollection } from "@turf/turf";
|
||||
import Timeline from "@app/OlMap/Controls/Timeline";
|
||||
import Timeline from "@components/olmap/core/Controls/Timeline";
|
||||
import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types";
|
||||
|
||||
interface SchemeQueryProps {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import ContaminantAnalysisParameters from "./AnalysisParameters";
|
||||
import ContaminantSchemeQuery from "./SchemeQuery";
|
||||
import { useData } from "@app/OlMap/MapComponent";
|
||||
import { useData } from "@components/olmap/core/MapComponent";
|
||||
|
||||
interface WaterQualityPanelProps {
|
||||
open?: boolean;
|
||||
|
||||
@@ -21,8 +21,8 @@ import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
import VectorTileSource from "ol/source/VectorTile";
|
||||
import { VectorTile } from "ol";
|
||||
import { FlatStyleLike } from "ol/style/flat";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import StyleLegend from "@app/OlMap/Controls/StyleLegend";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import StyleLegend from "@components/olmap/core/Controls/StyleLegend";
|
||||
import AnalysisParameters from "./AnalysisParameters";
|
||||
import SchemeQuery from "./SchemeQuery";
|
||||
import RecognitionResults from "./RecognitionResults";
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style";
|
||||
|
||||
@@ -30,7 +30,7 @@ import { api } from "@/lib/api";
|
||||
import moment from "moment";
|
||||
import { config, NETWORK_NAME } from "@config/config";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { useData, useMap } from "@app/OlMap/MapComponent";
|
||||
import { useData, useMap } from "@components/olmap/core/MapComponent";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
@@ -38,7 +38,7 @@ import VectorSource from "ol/source/Vector";
|
||||
import { Style, Icon, Circle, Fill, Stroke } from "ol/style";
|
||||
import Feature, { FeatureLike } from "ol/Feature";
|
||||
import { bbox, featureCollection } from "@turf/turf";
|
||||
import Timeline from "@app/OlMap/Controls/Timeline";
|
||||
import Timeline from "@components/olmap/core/Controls/Timeline";
|
||||
import { SchemeRecord, SchemaItem } from "./types";
|
||||
import { FLOW_DISPLAY_UNIT } from "@utils/units";
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ import dayjs from "dayjs";
|
||||
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
|
||||
import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb";
|
||||
import { FiSkipBack, FiSkipForward } from "react-icons/fi";
|
||||
import { useData } from "../../../app/OlMap/MapComponent";
|
||||
import { useData } from "@components/olmap/core/MapComponent";
|
||||
import { config, NETWORK_NAME } from "@/config/config";
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import { useMap } from "../../../app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { useHealthRisk } from "./HealthRiskContext";
|
||||
import {
|
||||
PredictionResult,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { api } from "@/lib/api";
|
||||
import moment from "moment";
|
||||
import { config, NETWORK_NAME } from "@config/config";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
|
||||
@@ -6,7 +6,7 @@ import Fill from "ol/style/Fill";
|
||||
import { Stroke } from "ol/style";
|
||||
import GeoJson from "ol/format/GeoJSON";
|
||||
import config from "@config/config";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { useProject } from "@/contexts/ProjectContext";
|
||||
|
||||
interface PropertyItem {
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ import { api } from "@/lib/api";
|
||||
import { useGetIdentity } from "@refinedev/core";
|
||||
import config from "@/config/config";
|
||||
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { useProject } from "@/contexts/ProjectContext";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||
@@ -0,0 +1,251 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useMap } from "../MapComponent";
|
||||
import TileLayer from "ol/layer/Tile.js";
|
||||
import XYZ from "ol/source/XYZ.js";
|
||||
import mapboxOutdoors from "@assets/map/layers/mapbox-outdoors.png";
|
||||
import mapboxLight from "@assets/map/layers/mapbox-light.png";
|
||||
import mapboxSatellite from "@assets/map/layers/mapbox-satellite.png";
|
||||
import mapboxSatelliteStreet from "@assets/map/layers/mapbox-satellite-streets.png";
|
||||
import mapboxStreets from "@assets/map/layers/mapbox-streets.png";
|
||||
import clsx from "clsx";
|
||||
import Group from "ol/layer/Group";
|
||||
import { MAPBOX_TOKEN } from "@config/config";
|
||||
import { TIANDITU_TOKEN } from "@config/config";
|
||||
const INITIAL_LAYER = "mapbox-light";
|
||||
|
||||
const streetsLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://api.mapbox.com/styles/v1/mapbox/streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
||||
tileSize: 512,
|
||||
maxZoom: 20,
|
||||
projection: "EPSG:3857",
|
||||
attributions:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
const lightMapLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
||||
tileSize: 512,
|
||||
maxZoom: 20,
|
||||
projection: "EPSG:3857",
|
||||
attributions:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
const satelliteLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
||||
tileSize: 512,
|
||||
maxZoom: 20,
|
||||
projection: "EPSG:3857",
|
||||
attributions:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
const satelliteStreetsLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
||||
tileSize: 512,
|
||||
maxZoom: 20,
|
||||
projection: "EPSG:3857",
|
||||
attributions:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
|
||||
const tiandituVectorLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
|
||||
projection: "EPSG:3857",
|
||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
const tiandituVectorAnnotationLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
|
||||
projection: "EPSG:3857",
|
||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
const tiandituImageLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
|
||||
projection: "EPSG:3857",
|
||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
const tiandituImageAnnotationLayer = new TileLayer({
|
||||
source: new XYZ({
|
||||
url: `https://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
|
||||
projection: "EPSG:3857",
|
||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
const tiandituVectorLayerGroup = new Group({
|
||||
layers: [tiandituVectorLayer, tiandituVectorAnnotationLayer],
|
||||
});
|
||||
const tiandituImageLayerGroup = new Group({
|
||||
layers: [tiandituImageLayer, tiandituImageAnnotationLayer],
|
||||
});
|
||||
const baseLayers = [
|
||||
{
|
||||
id: "mapbox-light",
|
||||
name: "默认地图",
|
||||
layer: lightMapLayer,
|
||||
// layer: tiandituVectorLayerGroup,
|
||||
img: mapboxLight.src,
|
||||
},
|
||||
{
|
||||
id: "mapbox-satellite",
|
||||
name: "卫星地图",
|
||||
layer: satelliteLayer,
|
||||
// layer: tiandituImageLayerGroup,
|
||||
img: mapboxSatellite.src,
|
||||
},
|
||||
{
|
||||
id: "mapbox-satellite-streets",
|
||||
name: "卫星街道地图",
|
||||
layer: satelliteStreetsLayer,
|
||||
img: mapboxSatelliteStreet.src,
|
||||
},
|
||||
{
|
||||
id: "mapbox-streets",
|
||||
name: "街道地图",
|
||||
layer: streetsLayer,
|
||||
img: mapboxStreets.src,
|
||||
},
|
||||
];
|
||||
|
||||
const BaseLayers: React.FC = () => {
|
||||
const map = useMap();
|
||||
// 切换底图选项展开,控制显示和卸载
|
||||
const [isShow, setShow] = useState(false);
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
// 快速切换底图
|
||||
const [activeId, setActiveId] = useState(INITIAL_LAYER);
|
||||
|
||||
// 初始化默认底图
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
// 添加所有底图至地图并根据 activeId 控制可见性
|
||||
baseLayers.forEach((layerInfo) => {
|
||||
const layers = map.getLayers().getArray();
|
||||
if (!layers.includes(layerInfo.layer)) {
|
||||
map.getLayers().insertAt(0, layerInfo.layer);
|
||||
}
|
||||
layerInfo.layer.setVisible(layerInfo.id === activeId);
|
||||
});
|
||||
}, [map]);
|
||||
|
||||
const changeMapLayers = (id: string) => {
|
||||
if (map) {
|
||||
// 根据 id 设置每个图层的可见性
|
||||
baseLayers.forEach(({ id: lid, layer }) => {
|
||||
layer.setVisible(lid === id);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickSwitch = () => {
|
||||
const nextId =
|
||||
activeId === baseLayers[0].id ? baseLayers[1].id : baseLayers[0].id;
|
||||
setActiveId(nextId);
|
||||
handleMapLayers(nextId);
|
||||
};
|
||||
|
||||
const handleMapLayers = (id: string) => {
|
||||
setActiveId(id);
|
||||
changeMapLayers(id);
|
||||
};
|
||||
|
||||
// 记录定时器,避免多次触发
|
||||
const hideTimer = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleEnter = () => {
|
||||
if (hideTimer.current) {
|
||||
clearTimeout(hideTimer.current);
|
||||
hideTimer.current = null;
|
||||
}
|
||||
setShow(true);
|
||||
setExpanded(true);
|
||||
};
|
||||
|
||||
const handleLeave = () => {
|
||||
setShow(false);
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setExpanded(false);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute right-17 bottom-11 z-1300">
|
||||
<div
|
||||
className="w-20 h-20 bg-white rounded-xl drop-shadow-xl shadow-black"
|
||||
onMouseEnter={handleEnter}
|
||||
onMouseLeave={handleLeave}
|
||||
>
|
||||
<div className="w-20 h-20 p-1">
|
||||
<button onClick={() => handleQuickSwitch()}>
|
||||
<img
|
||||
width={240}
|
||||
height={100}
|
||||
src={
|
||||
activeId === baseLayers[0].id
|
||||
? baseLayers[1].img
|
||||
: baseLayers[0].img
|
||||
}
|
||||
alt={
|
||||
activeId === baseLayers[0].id
|
||||
? baseLayers[1].name
|
||||
: baseLayers[0].name
|
||||
}
|
||||
className="object-cover object-left w-18 h-18 rounded-xl"
|
||||
/>
|
||||
<div className=" absolute left-1 bottom-1 flex w-18 h-auto items-center justify-center rounded-b-xl text-xs text-white bg-black opacity-80">
|
||||
<span>
|
||||
{activeId === baseLayers[0].id
|
||||
? baseLayers[1].name
|
||||
: baseLayers[0].name}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute flex right-24 bottom-0 w-90 h-25 bg-white rounded-xl drop-shadow-xl shadow-black transition-all duration-300",
|
||||
isShow ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
onMouseEnter={handleEnter}
|
||||
onMouseLeave={handleLeave}
|
||||
>
|
||||
{baseLayers.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
className="flex flex-auto flex-col justify-center items-center text-gray-500 text-xs"
|
||||
onClick={() => handleMapLayers(item.id)}
|
||||
>
|
||||
<img
|
||||
width={240}
|
||||
height={100}
|
||||
src={item.img}
|
||||
alt={item.name}
|
||||
className={clsx(
|
||||
"object-cover object-left w-16 h-16 rounded-md border-2 border-white hover:ring-2 ring-blue-300",
|
||||
{
|
||||
"ring-1 ring-blue-300": activeId === item.id,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span className="pt-1">{item.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseLayers;
|
||||
@@ -0,0 +1,388 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import ToolbarButton from "@components/olmap/common/ToolbarButton";
|
||||
|
||||
// 导入Material-UI图标
|
||||
import BackHandOutlinedIcon from "@mui/icons-material/BackHandOutlined";
|
||||
import BorderColorOutlinedIcon from "@mui/icons-material/BorderColorOutlined";
|
||||
import MoreHorizOutlinedIcon from "@mui/icons-material/MoreHorizOutlined";
|
||||
import TimelineIcon from "@mui/icons-material/Timeline";
|
||||
import CircleOutlinedIcon from "@mui/icons-material/CircleOutlined";
|
||||
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
|
||||
import GestureIcon from "@mui/icons-material/Gesture";
|
||||
import UndoIcon from "@mui/icons-material/Undo";
|
||||
import RedoIcon from "@mui/icons-material/Redo";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import SaveIcon from "@mui/icons-material/Save";
|
||||
|
||||
// 导入OpenLayers绘图相关模块
|
||||
import Draw, {
|
||||
DrawEvent,
|
||||
createBox,
|
||||
GeometryFunction,
|
||||
} from "ol/interaction/Draw";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import { Style, Stroke, Fill, Circle } from "ol/style";
|
||||
import { Geometry } from "ol/geom";
|
||||
import Feature from "ol/Feature";
|
||||
import { Type as GeometryType } from "ol/geom/Geometry";
|
||||
import { useMap } from "../MapComponent";
|
||||
|
||||
const DrawPanel: React.FC = () => {
|
||||
const map = useMap();
|
||||
const [activeTool, setActiveTool] = useState<string>("pan");
|
||||
const [drawLayer, setDrawLayer] = useState<VectorLayer<VectorSource> | null>(
|
||||
null
|
||||
);
|
||||
const [drawnFeatures, setDrawnFeatures] = useState<Feature<Geometry>[]>([]);
|
||||
const [historyStack, setHistoryStack] = useState<Feature<Geometry>[][]>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState<number>(-1);
|
||||
|
||||
const drawInteractionRef = useRef<Draw | null>(null);
|
||||
|
||||
// 创建并添加绘图图层
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
const drawSource = new VectorSource();
|
||||
const drawVectorLayer = new VectorLayer({
|
||||
source: drawSource,
|
||||
style: new Style({
|
||||
stroke: new Stroke({
|
||||
color: `rgba(255, 152, 0, 0.9)`,
|
||||
width: 2,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: `rgba(255, 152, 0, 0.3)`,
|
||||
}),
|
||||
image: new Circle({
|
||||
radius: 7,
|
||||
stroke: new Stroke({
|
||||
color: `rgba(255, 152, 0, 0.9)`,
|
||||
width: 2,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: `rgba(255, 152, 0, 0.3)`,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
properties: {
|
||||
name: "绘制图层", // 设置图层名称
|
||||
value: "drawLayer",
|
||||
type: "multigeometry",
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer(drawVectorLayer);
|
||||
setDrawLayer(drawVectorLayer);
|
||||
|
||||
return () => {
|
||||
if (drawInteractionRef.current && map) {
|
||||
map.removeInteraction(drawInteractionRef.current);
|
||||
drawInteractionRef.current = null;
|
||||
}
|
||||
map.removeLayer(drawVectorLayer);
|
||||
};
|
||||
}, [map, drawInteractionRef]);
|
||||
|
||||
// 保存到历史记录
|
||||
const saveToHistory = useCallback(
|
||||
(features: Feature<Geometry>[]) => {
|
||||
setHistoryStack((prevStack) => {
|
||||
const newHistory = prevStack.slice(0, historyIndex + 1);
|
||||
newHistory.push([...features]);
|
||||
setHistoryIndex(newHistory.length - 1);
|
||||
return newHistory;
|
||||
});
|
||||
},
|
||||
[historyIndex]
|
||||
);
|
||||
|
||||
// 添加绘图交互
|
||||
const addDrawInteraction = (
|
||||
type: GeometryType,
|
||||
geometryFunction?: GeometryFunction
|
||||
) => {
|
||||
if (!drawLayer) return;
|
||||
if (!map) return;
|
||||
|
||||
// 清除现有的绘图交互
|
||||
if (drawInteractionRef.current && map) {
|
||||
map.removeInteraction(drawInteractionRef.current);
|
||||
}
|
||||
|
||||
const source = drawLayer.getSource();
|
||||
if (!source) return;
|
||||
|
||||
const drawOptions: {
|
||||
source: VectorSource;
|
||||
type: GeometryType;
|
||||
style: Style;
|
||||
geometryFunction?: GeometryFunction;
|
||||
} = {
|
||||
source: source,
|
||||
type: type,
|
||||
style: new Style({
|
||||
stroke: new Stroke({
|
||||
color: `rgba(255, 152, 0, 0.9)`,
|
||||
width: 2,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: `rgba(255, 152, 0, 0.3)`,
|
||||
}),
|
||||
image: new Circle({
|
||||
radius: 7,
|
||||
stroke: new Stroke({
|
||||
color: `rgba(255, 152, 0, 0.9)`,
|
||||
width: 2,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: `rgba(255, 152, 0, 0.3)`,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
// 如果有几何函数,添加它
|
||||
if (geometryFunction) {
|
||||
drawOptions.geometryFunction = geometryFunction;
|
||||
}
|
||||
|
||||
const draw = new Draw(drawOptions);
|
||||
|
||||
// 绘图完成事件
|
||||
draw.on("drawend", (event: DrawEvent) => {
|
||||
const feature = event.feature;
|
||||
const currentFeatures = [...drawnFeatures, feature];
|
||||
setDrawnFeatures(currentFeatures);
|
||||
saveToHistory(currentFeatures);
|
||||
});
|
||||
|
||||
map.addInteraction(draw);
|
||||
drawInteractionRef.current = draw;
|
||||
};
|
||||
|
||||
// 处理工具点击
|
||||
const handleToolClick = (tool: string) => {
|
||||
// 如果点击的是当前激活的工具,则取消激活
|
||||
// console.log("当前激活的工具:", activeTool);
|
||||
// console.log("点击的工具:", tool);
|
||||
if (activeTool === tool) {
|
||||
setActiveTool("");
|
||||
if (drawInteractionRef.current && map) {
|
||||
map.removeInteraction(drawInteractionRef.current);
|
||||
drawInteractionRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool !== "undo" &&
|
||||
tool !== "redo" &&
|
||||
tool !== "delete" &&
|
||||
tool !== "save"
|
||||
) {
|
||||
setActiveTool(tool);
|
||||
}
|
||||
// 根据工具类型处理不同的交互
|
||||
switch (tool) {
|
||||
case "pan":
|
||||
// 平移地图,移除所有绘图交互
|
||||
if (drawInteractionRef.current && map) {
|
||||
map.removeInteraction(drawInteractionRef.current);
|
||||
drawInteractionRef.current = null;
|
||||
}
|
||||
break;
|
||||
case "select":
|
||||
// 选定要素,移除所有绘图交互
|
||||
if (drawInteractionRef.current && map) {
|
||||
map.removeInteraction(drawInteractionRef.current);
|
||||
drawInteractionRef.current = null;
|
||||
}
|
||||
break;
|
||||
case "edit":
|
||||
// 编辑要素,移除所有绘图交互
|
||||
if (drawInteractionRef.current && map) {
|
||||
map.removeInteraction(drawInteractionRef.current);
|
||||
drawInteractionRef.current = null;
|
||||
}
|
||||
break;
|
||||
case "point":
|
||||
addDrawInteraction("Point");
|
||||
break;
|
||||
case "line":
|
||||
addDrawInteraction("LineString");
|
||||
break;
|
||||
case "circle":
|
||||
addDrawInteraction("Circle");
|
||||
break;
|
||||
case "box":
|
||||
// 使用矩形绘制函数
|
||||
addDrawInteraction("Circle", createBox());
|
||||
break;
|
||||
case "polygon":
|
||||
addDrawInteraction("Polygon");
|
||||
break;
|
||||
case "undo":
|
||||
handleUndo();
|
||||
break;
|
||||
case "redo":
|
||||
handleRedo();
|
||||
break;
|
||||
case "delete":
|
||||
handleDelete();
|
||||
break;
|
||||
case "save":
|
||||
handleSave();
|
||||
break;
|
||||
default:
|
||||
if (drawInteractionRef.current && map) {
|
||||
map.removeInteraction(drawInteractionRef.current);
|
||||
drawInteractionRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 撤销功能
|
||||
const handleUndo = () => {
|
||||
if (historyIndex > 0) {
|
||||
const newIndex = historyIndex - 1;
|
||||
const previousFeatures = historyStack[newIndex];
|
||||
updateDrawLayer(previousFeatures);
|
||||
setDrawnFeatures(previousFeatures);
|
||||
setHistoryIndex(newIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// 重做功能
|
||||
const handleRedo = () => {
|
||||
if (historyIndex < historyStack.length - 1) {
|
||||
const newIndex = historyIndex + 1;
|
||||
const nextFeatures = historyStack[newIndex];
|
||||
updateDrawLayer(nextFeatures);
|
||||
setDrawnFeatures(nextFeatures);
|
||||
setHistoryIndex(newIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除所有绘制的要素
|
||||
const handleDelete = () => {
|
||||
if (!drawLayer) return;
|
||||
|
||||
const source = drawLayer.getSource();
|
||||
if (source) {
|
||||
source.clear();
|
||||
const emptyFeatures: Feature<Geometry>[] = [];
|
||||
setDrawnFeatures(emptyFeatures);
|
||||
saveToHistory(emptyFeatures);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存绘制的要素
|
||||
const handleSave = () => {};
|
||||
|
||||
// 更新绘图图层
|
||||
const updateDrawLayer = (features: Feature<Geometry>[]) => {
|
||||
if (!drawLayer) return;
|
||||
|
||||
const source = drawLayer.getSource();
|
||||
if (source) {
|
||||
source.clear();
|
||||
source.addFeatures(features);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化历史记录
|
||||
useEffect(() => {
|
||||
// 初始化空的历史记录
|
||||
if (historyStack.length === 0) {
|
||||
saveToHistory([]);
|
||||
}
|
||||
}, [historyStack.length, saveToHistory]);
|
||||
|
||||
// 判断按钮是否应该禁用
|
||||
const isUndoDisabled = historyIndex <= 0;
|
||||
const isRedoDisabled = historyIndex >= historyStack.length - 1;
|
||||
const isDeleteDisabled = drawnFeatures.length === 0;
|
||||
const isSaveDisabled = drawnFeatures.length === 0;
|
||||
|
||||
return (
|
||||
<div className="absolute top-20 left-4 bg-white p-1 rounded-xl shadow-lg flex flex-col opacity-85 hover:opacity-100 transition-opacity z-1300">
|
||||
<div className="flex">
|
||||
<ToolbarButton
|
||||
icon={<BackHandOutlinedIcon />}
|
||||
name="平移地图"
|
||||
isActive={activeTool === "pan"}
|
||||
onClick={() => handleToolClick("pan")}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<BorderColorOutlinedIcon />}
|
||||
name="矢量编辑"
|
||||
isActive={activeTool === "edit"}
|
||||
onClick={() => handleToolClick("edit")}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<MoreHorizOutlinedIcon />}
|
||||
name="绘制点"
|
||||
isActive={activeTool === "point"}
|
||||
onClick={() => handleToolClick("point")}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<TimelineIcon />}
|
||||
name="绘制线"
|
||||
isActive={activeTool === "line"}
|
||||
onClick={() => handleToolClick("line")}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<CircleOutlinedIcon />}
|
||||
name="绘制圆"
|
||||
isActive={activeTool === "circle"}
|
||||
onClick={() => handleToolClick("circle")}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<CheckBoxOutlineBlankIcon />}
|
||||
name="绘制框"
|
||||
isActive={activeTool === "box"}
|
||||
onClick={() => handleToolClick("box")}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<GestureIcon />}
|
||||
name="绘制多边形"
|
||||
isActive={activeTool === "polygon"}
|
||||
onClick={() => handleToolClick("polygon")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex mt-1 border-t-1 pt-1">
|
||||
<ToolbarButton
|
||||
icon={<UndoIcon />}
|
||||
name="撤销"
|
||||
isActive={false}
|
||||
onClick={() => handleToolClick("undo")}
|
||||
disabled={isUndoDisabled}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<RedoIcon />}
|
||||
name="重做"
|
||||
isActive={false}
|
||||
onClick={() => handleToolClick("redo")}
|
||||
disabled={isRedoDisabled}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<DeleteIcon />}
|
||||
name="删除"
|
||||
isActive={false}
|
||||
onClick={() => handleToolClick("delete")}
|
||||
disabled={isDeleteDisabled}
|
||||
/>
|
||||
{/* <ToolbarButton
|
||||
icon={<SaveIcon />}
|
||||
name="保存"
|
||||
isActive={false}
|
||||
onClick={() => handleToolClick("save")}
|
||||
disabled={isSaveDisabled}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawPanel;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useData, useMap } from "../MapComponent";
|
||||
import { Checkbox, FormControlLabel } from "@mui/material";
|
||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorTileLayer from "ol/layer/VectorTile";
|
||||
import { DeckLayer } from "@utils/layers";
|
||||
|
||||
// 定义统一的图层项接口
|
||||
interface LayerItem {
|
||||
id: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
type: "ol" | "deck";
|
||||
layerRef: any; // OpenLayers Layer 实例或 deck.gl layer 对象
|
||||
}
|
||||
|
||||
const LayerControl: React.FC = () => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
if (!data) return;
|
||||
const {
|
||||
deckLayer,
|
||||
isContourLayerAvailable,
|
||||
isWaterflowLayerAvailable,
|
||||
setShowWaterflowLayer,
|
||||
setShowContourLayer,
|
||||
} = data;
|
||||
const [layerItems, setLayerItems] = useState<LayerItem[]>([]);
|
||||
|
||||
const layerOrder = [
|
||||
"junctions",
|
||||
"reservoirs",
|
||||
"tanks",
|
||||
"pipes",
|
||||
"pumps",
|
||||
"valves",
|
||||
"scada",
|
||||
"waterflowLayer",
|
||||
"junctionContourLayer",
|
||||
];
|
||||
|
||||
// 更新图层列表
|
||||
const updateLayers = useCallback(() => {
|
||||
if (!map || !data) return;
|
||||
|
||||
const items: LayerItem[] = [];
|
||||
|
||||
// 1. 获取 OpenLayers 图层
|
||||
const mapLayers = map.getLayers().getArray();
|
||||
mapLayers.forEach((layer) => {
|
||||
// 筛选特定类型的 OpenLayers 图层
|
||||
if (
|
||||
layer instanceof WebGLVectorTileLayer ||
|
||||
layer instanceof VectorTileLayer ||
|
||||
layer instanceof VectorLayer
|
||||
) {
|
||||
const value = layer.get("value");
|
||||
const name = layer.get("name");
|
||||
// 只有设置了 value (作为 ID) 的图层才会被纳入控制
|
||||
if (value) {
|
||||
items.push({
|
||||
id: value,
|
||||
name: name || value,
|
||||
visible: layer.getVisible(),
|
||||
type: "ol",
|
||||
layerRef: layer,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 获取 DeckLayer 中的子图层
|
||||
if (deckLayer && deckLayer instanceof DeckLayer) {
|
||||
const deckLayers = deckLayer.getDeckLayers();
|
||||
deckLayers.forEach((layer: any) => {
|
||||
if (layer && layer.id) {
|
||||
// 仅处理 junctionContourLayer 和 waterflowLayer
|
||||
if (
|
||||
layer.id !== "junctionContourLayer" &&
|
||||
layer.id !== "waterflowLayer"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 检查可用性
|
||||
if (
|
||||
(layer.id === "junctionContourLayer" && !isContourLayerAvailable) ||
|
||||
(layer.id === "waterflowLayer" && !isWaterflowLayerAvailable)
|
||||
) {
|
||||
return; // 跳过不可用图层
|
||||
}
|
||||
const visible =
|
||||
deckLayer.getDeckLayerVisible(layer.id) ??
|
||||
layer.props?.visible ??
|
||||
true;
|
||||
items.push({
|
||||
id: layer.props.id,
|
||||
name: layer.props.name, // 使用 name 属性作为显示名称
|
||||
visible: visible,
|
||||
type: "deck",
|
||||
layerRef: layer,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤并排序
|
||||
const sortedItems = items
|
||||
.filter((item) => layerOrder.includes(item.id))
|
||||
.sort((a, b) => {
|
||||
const indexA = layerOrder.indexOf(a.id);
|
||||
const indexB = layerOrder.indexOf(b.id);
|
||||
return indexA - indexB;
|
||||
});
|
||||
|
||||
setLayerItems(sortedItems);
|
||||
}, [map, deckLayer, isWaterflowLayerAvailable, isContourLayerAvailable]);
|
||||
|
||||
useEffect(() => {
|
||||
updateLayers();
|
||||
|
||||
if (map) {
|
||||
const layerCollection = map.getLayers();
|
||||
layerCollection.on("change:length", updateLayers);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (map) {
|
||||
map.getLayers().un("change:length", updateLayers);
|
||||
}
|
||||
};
|
||||
}, [map, updateLayers]);
|
||||
|
||||
const handleVisibilityChange = (item: LayerItem, checked: boolean) => {
|
||||
if (item.type === "ol") {
|
||||
item.layerRef.setVisible(checked);
|
||||
} else if (item.type === "deck" && deckLayer) {
|
||||
if (item.id === "junctionContourLayer") {
|
||||
setShowContourLayer && setShowContourLayer(checked);
|
||||
}
|
||||
if (item.id === "waterflowLayer") {
|
||||
setShowWaterflowLayer && setShowWaterflowLayer(checked);
|
||||
}
|
||||
}
|
||||
|
||||
setLayerItems((prev) =>
|
||||
prev.map((i) => (i.id === item.id ? { ...i, visible: checked } : i)),
|
||||
);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute left-4 bottom-4 bg-white rounded-md drop-shadow-lg z-1300 opacity-85 hover:opacity-100 transition-opacity max-w-xs">
|
||||
<div className="ml-3 grid grid-cols-3">
|
||||
{layerItems.map((item) => (
|
||||
<FormControlLabel
|
||||
key={item.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={item.visible}
|
||||
onChange={(e) => handleVisibilityChange(item, e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={item.name}
|
||||
sx={{
|
||||
fontSize: "0.7rem",
|
||||
"& .MuiFormControlLabel-label": { fontSize: "0.7rem" },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayerControl;
|
||||
@@ -0,0 +1,234 @@
|
||||
import React from "react";
|
||||
|
||||
interface BaseProperty {
|
||||
label: string;
|
||||
value: string | number;
|
||||
unit?: string;
|
||||
formatter?: (value: string | number) => string;
|
||||
}
|
||||
|
||||
// 新增:表格型属性(用于二级数据)
|
||||
interface TableProperty {
|
||||
type: "table";
|
||||
label: string;
|
||||
columns: string[]; // 表头
|
||||
rows: (string | number)[][]; // 每行的数据
|
||||
}
|
||||
|
||||
type PropertyItem = BaseProperty | TableProperty;
|
||||
|
||||
interface PropertyPanelProps {
|
||||
id?: string;
|
||||
type?: string;
|
||||
properties?: PropertyItem[];
|
||||
}
|
||||
|
||||
const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
id,
|
||||
type = "未知类型",
|
||||
properties = [],
|
||||
}) => {
|
||||
const formatValue = (property: BaseProperty) => {
|
||||
if (property.formatter) {
|
||||
return property.formatter(property.value);
|
||||
}
|
||||
if (property.unit) {
|
||||
return `${property.value} ${property.unit}`;
|
||||
}
|
||||
return property.value;
|
||||
};
|
||||
|
||||
const isImportantKeys = ["ID", "类型", "Name", "面积", "长度"];
|
||||
|
||||
// 统计属性数量(表格型按行数计入)
|
||||
const totalProps = id
|
||||
? 2 +
|
||||
properties.reduce((sum, p) => {
|
||||
if ("type" in p && p.type === "table") return sum + p.rows.length;
|
||||
return sum + 1;
|
||||
}, 0)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-96 max-h-[850px] flex flex-col backdrop-blur-sm z-1300 opacity-95 hover:opacity-100 transition-all duration-300 ">
|
||||
{/* 头部 */}
|
||||
<div className="flex justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 className="text-lg font-semibold">属性面板</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-3">
|
||||
{!id ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-gray-400">
|
||||
<svg
|
||||
className="w-16 h-16 mb-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm">暂无属性信息</p>
|
||||
<p className="text-xs mt-1">请选择一个要素以查看其属性</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{/* ID 属性 */}
|
||||
<div className="group rounded-lg p-3 transition-all duration-200 bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500">
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<span className="font-medium text-xs uppercase tracking-wide text-blue-700">
|
||||
ID
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-right flex-1 text-blue-900">
|
||||
{id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 类型属性 */}
|
||||
<div className="group rounded-lg p-3 transition-all duration-200 bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500">
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<span className="font-medium text-xs uppercase tracking-wide text-blue-700">
|
||||
类型
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-right flex-1 text-blue-900">
|
||||
{type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 其他属性(包含二级表格) */}
|
||||
{properties.map((property, index) => {
|
||||
// 二级表格
|
||||
if ("type" in property && property.type === "table") {
|
||||
return (
|
||||
<div
|
||||
key={`table-${index}`}
|
||||
className="group rounded-lg p-3 transition-all duration-200 bg-gray-50 hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<span className="font-medium text-xs uppercase tracking-wide text-gray-600">
|
||||
{property.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-4 mt-2 border border-gray-300 rounded-md overflow-hidden shadow-sm">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-200 text-gray-700">
|
||||
<tr>
|
||||
{property.columns.map((col, ci) => (
|
||||
<th
|
||||
key={ci}
|
||||
className="px-3 py-2 text-left font-semibold"
|
||||
>
|
||||
{col}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-300">
|
||||
{property.rows.map((row, ri) => (
|
||||
<tr key={ri} className="bg-white hover:bg-gray-50">
|
||||
{row.map((cell, cci) => (
|
||||
<td
|
||||
key={cci}
|
||||
className="px-3 py-2 text-gray-800"
|
||||
>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 普通属性
|
||||
const base = property as BaseProperty;
|
||||
const isImportant = isImportantKeys.includes(base.label);
|
||||
return (
|
||||
<div
|
||||
key={`prop-${index}`}
|
||||
className={`group rounded-lg p-3 transition-all duration-200 ${
|
||||
isImportant
|
||||
? "bg-blue-50 hover:bg-blue-100 border-l-4 border-blue-500"
|
||||
: "bg-gray-50 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<span
|
||||
className={`font-medium text-xs uppercase tracking-wide ${
|
||||
isImportant ? "text-blue-700" : "text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{base.label}
|
||||
</span>
|
||||
<span
|
||||
className={`text-sm font-semibold text-right flex-1 ${
|
||||
isImportant ? "text-blue-900" : "text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{formatValue(base)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部统计区域 */}
|
||||
<div className="px-5 py-3 bg-gray-50 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-600 flex items-center gap-1">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
共 {totalProps} 个属性
|
||||
</span>
|
||||
{id && (
|
||||
<span className="text-green-600 flex items-center gap-1 font-medium">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
已选中
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PropertyPanel;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useMap } from "../MapComponent";
|
||||
import { ScaleLine } from "ol/control";
|
||||
|
||||
const Scale: React.FC = () => {
|
||||
const map = useMap();
|
||||
const [zoomLevel, setZoomLevel] = useState(0);
|
||||
const [coordinates, setCoordinates] = useState<[number, number]>([0, 0]);
|
||||
const scaleLineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const updateZoomLevel = () => {
|
||||
const zoom = map.getView().getZoom();
|
||||
setZoomLevel(zoom ?? 0); // 如果 zoom 是 undefined,则使用默认值 0
|
||||
};
|
||||
|
||||
const updateCoordinates = (event: any) => {
|
||||
const coords = event.coordinate;
|
||||
const transformedCoords = coords.map((c: number) =>
|
||||
parseFloat(c.toFixed(4))
|
||||
);
|
||||
setCoordinates(transformedCoords);
|
||||
};
|
||||
|
||||
map.on("moveend", updateZoomLevel);
|
||||
map.on("pointermove", updateCoordinates);
|
||||
|
||||
// Initialize values
|
||||
updateZoomLevel();
|
||||
|
||||
// ScaleLine control
|
||||
const scaleControl = new ScaleLine({
|
||||
target: scaleLineRef.current || undefined,
|
||||
units: "metric",
|
||||
bar: false,
|
||||
steps: 4,
|
||||
text: true,
|
||||
minWidth: 64,
|
||||
});
|
||||
map.addControl(scaleControl);
|
||||
|
||||
return () => {
|
||||
map.un("moveend", updateZoomLevel);
|
||||
map.un("pointermove", updateCoordinates);
|
||||
map.removeControl(scaleControl);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
.custom-scale-line .ol-scale-line {
|
||||
position: static;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.custom-scale-line .ol-scale-line-inner {
|
||||
border: 1px solid #475569;
|
||||
border-top: none;
|
||||
color: #334155;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="absolute bottom-0 right-0 flex items-center gap-2 px-3 py-1.5 bg-white/90 hover:bg-white rounded-tl-xl shadow-lg backdrop-blur-sm text-xs font-medium text-slate-700 z-1300 transition-all duration-300 pointer-events-auto">
|
||||
<div
|
||||
ref={scaleLineRef}
|
||||
className="custom-scale-line flex items-center justify-center min-w-[60px]"
|
||||
/>
|
||||
<div className="h-3 w-px bg-slate-300 mx-1" />
|
||||
<div className="min-w-[60px] text-center">
|
||||
缩放: {zoomLevel.toFixed(1)}
|
||||
</div>
|
||||
<div className="h-3 w-px bg-slate-300 mx-1" />
|
||||
<div className="tabular-nums min-w-[140px] text-center">
|
||||
坐标: {coordinates[0]}, {coordinates[1]}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scale;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
interface LegendStyleConfig {
|
||||
layerName: string;
|
||||
layerId: string;
|
||||
property: string;
|
||||
colors: string[];
|
||||
type: string; // 图例类型
|
||||
dimensions: number[]; // 尺寸大小
|
||||
breaks: number[]; // 分段值
|
||||
labels?: string[]; // 可选标签(用于离散分类)
|
||||
columns?: number;
|
||||
itemsPerColumn?: number;
|
||||
}
|
||||
// 图例组件
|
||||
// 该组件用于显示图层样式的图例,包含属性名称、颜色、尺寸和分段值等信息
|
||||
// 通过传入的配置对象动态生成图例内容,适用于不同的样式配置
|
||||
// 使用时需要确保传入的 colors、dimensions 和 breaks 数组长度一致
|
||||
|
||||
const StyleLegend: React.FC<LegendStyleConfig> = ({
|
||||
layerName,
|
||||
layerId,
|
||||
property,
|
||||
colors,
|
||||
type, // 图例类型
|
||||
dimensions,
|
||||
breaks,
|
||||
labels,
|
||||
columns = 1,
|
||||
itemsPerColumn,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
key={layerId}
|
||||
className="bg-white p-3 rounded-xl max-w-xs opacity-95 transition-opacity duration-300 hover:opacity-100"
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{layerName} - {property}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns:
|
||||
itemsPerColumn && itemsPerColumn > 0
|
||||
? undefined
|
||||
: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))`,
|
||||
gridTemplateRows:
|
||||
itemsPerColumn && itemsPerColumn > 0
|
||||
? `repeat(${itemsPerColumn}, minmax(0, auto))`
|
||||
: undefined,
|
||||
gridAutoFlow:
|
||||
itemsPerColumn && itemsPerColumn > 0 ? "column" : undefined,
|
||||
columnGap: 1.5,
|
||||
rowGap: 0.5,
|
||||
}}
|
||||
>
|
||||
{[...Array(breaks.length)].map((_, index) => {
|
||||
const color = colors[index]; // 默认颜色为黑色
|
||||
const dimension = dimensions[index]; // 默认尺寸为16
|
||||
|
||||
// // 处理第一个区间(小于 breaks[0])
|
||||
// if (index === 0) {
|
||||
// return (
|
||||
// <Box key={index} className="flex items-center gap-2 mb-1">
|
||||
// <Box
|
||||
// sx={
|
||||
// type === "point"
|
||||
// ? {
|
||||
// width: dimension,
|
||||
// height: dimension,
|
||||
// borderRadius: "50%",
|
||||
// backgroundColor: color,
|
||||
// }
|
||||
// : {
|
||||
// width: 16,
|
||||
// height: dimension,
|
||||
// backgroundColor: color,
|
||||
// border: `1px solid ${color}`,
|
||||
// }
|
||||
// }
|
||||
// />
|
||||
// <Typography variant="caption" className="text-xs">
|
||||
// {"<"} {breaks[0]?.toFixed(1)}
|
||||
// </Typography>
|
||||
// </Box>
|
||||
// );
|
||||
// }
|
||||
|
||||
// 处理中间区间(breaks[index] - breaks[index + 1])
|
||||
if (index + 1 < breaks.length) {
|
||||
const prevValue = breaks[index];
|
||||
const currentValue = breaks[index + 1];
|
||||
return (
|
||||
<Box key={index} className="flex items-center gap-2">
|
||||
<Box
|
||||
sx={
|
||||
type === "point"
|
||||
? {
|
||||
width: dimension,
|
||||
height: dimension,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
}
|
||||
: {
|
||||
width: 16,
|
||||
height: dimension,
|
||||
backgroundColor: color,
|
||||
border: `1px solid ${color}`,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Typography variant="caption" className="text-xs">
|
||||
{labels?.[index] ??
|
||||
`${prevValue?.toFixed(1)} - ${currentValue?.toFixed(1)}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StyleLegend;
|
||||
export type { LegendStyleConfig };
|
||||
@@ -0,0 +1,849 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Slider,
|
||||
Typography,
|
||||
Paper,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
IconButton,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from "@mui/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 from "dayjs";
|
||||
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
|
||||
import { TbRewindBackward15, TbRewindForward15 } from "react-icons/tb";
|
||||
import { FiSkipBack, FiSkipForward } from "react-icons/fi";
|
||||
import { useData } from "../MapComponent";
|
||||
import { config, NETWORK_NAME } from "@/config/config";
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import { useMap } from "../MapComponent";
|
||||
|
||||
interface TimelineProps {
|
||||
schemeDate?: Date;
|
||||
timeRange?: { start: Date; end: Date };
|
||||
disableDateSelection?: boolean;
|
||||
schemeName?: string;
|
||||
schemeType?: string;
|
||||
}
|
||||
|
||||
const Timeline: React.FC<TimelineProps> = ({
|
||||
schemeDate,
|
||||
timeRange,
|
||||
disableDateSelection = false,
|
||||
schemeName = "",
|
||||
schemeType = "burst_Analysis",
|
||||
}) => {
|
||||
const data = useData();
|
||||
if (!data) {
|
||||
return <div>Loading...</div>; // 或其他占位符
|
||||
}
|
||||
const {
|
||||
currentTime,
|
||||
setCurrentTime,
|
||||
selectedDate,
|
||||
setSelectedDate,
|
||||
setCurrentJunctionCalData,
|
||||
setCurrentPipeCalData,
|
||||
junctionText,
|
||||
pipeText,
|
||||
} = data;
|
||||
if (
|
||||
setCurrentTime === undefined ||
|
||||
currentTime === undefined ||
|
||||
selectedDate === undefined ||
|
||||
setSelectedDate === undefined
|
||||
) {
|
||||
return <div>Loading...</div>; // 或其他占位符
|
||||
}
|
||||
const { open } = useNotification();
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
|
||||
const [calculatedInterval, setCalculatedInterval] = useState<number>(15); // 分钟
|
||||
const [isCalculating, setIsCalculating] = useState<boolean>(false);
|
||||
|
||||
// 计算时间轴范围
|
||||
const minTime = timeRange
|
||||
? timeRange.start.getHours() * 60 + timeRange.start.getMinutes()
|
||||
: 0;
|
||||
const maxTime = timeRange
|
||||
? timeRange.end.getHours() * 60 + timeRange.end.getMinutes()
|
||||
: 1440;
|
||||
useEffect(() => {
|
||||
if (schemeDate) {
|
||||
setSelectedDate(schemeDate);
|
||||
}
|
||||
}, [schemeDate]);
|
||||
// 新增:用于 Draggable 的 nodeRef
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
// 添加缓存引用
|
||||
const nodeCacheRef = useRef<Map<string, any[]>>(new Map());
|
||||
const linkCacheRef = useRef<Map<string, any[]>>(new Map());
|
||||
// 添加防抖引用
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const fetchFrameData = async (
|
||||
queryTime: Date,
|
||||
junctionProperties: string,
|
||||
pipeProperties: string,
|
||||
schemeName: string,
|
||||
schemeType: string,
|
||||
) => {
|
||||
const query_time = queryTime.toISOString();
|
||||
let nodeRecords: any = { results: [] };
|
||||
let linkRecords: any = { results: [] };
|
||||
const requests: Promise<Response>[] = [];
|
||||
let nodePromise: Promise<any> | null = null;
|
||||
let linkPromise: Promise<any> | null = null;
|
||||
// 检查node缓存
|
||||
if (junctionProperties !== "" && junctionProperties !== "elevation") {
|
||||
const nodeCacheKey = `${query_time}_${junctionProperties}_${schemeName}_${schemeType}`;
|
||||
if (nodeCacheRef.current.has(nodeCacheKey)) {
|
||||
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
|
||||
} else {
|
||||
disableDateSelection && schemeName
|
||||
? (nodePromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${schemeName}`
|
||||
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`,
|
||||
))
|
||||
: (nodePromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
|
||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=node&property=${junctionProperties}`,
|
||||
));
|
||||
requests.push(nodePromise);
|
||||
}
|
||||
}
|
||||
// 处理特殊属性名称
|
||||
if (pipeProperties === "unit_headloss") pipeProperties = "headloss";
|
||||
|
||||
// 检查link缓存
|
||||
if (pipeProperties !== "" && pipeProperties !== "diameter") {
|
||||
const linkCacheKey = `${query_time}_${pipeProperties}_${schemeName}_${schemeType}`;
|
||||
if (linkCacheRef.current.has(linkCacheKey)) {
|
||||
linkRecords = linkCacheRef.current.get(linkCacheKey)!;
|
||||
} else {
|
||||
disableDateSelection && schemeName
|
||||
? (linkPromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${schemeName}`
|
||||
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${pipeProperties}`,
|
||||
))
|
||||
: (linkPromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
|
||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=link&property=${pipeProperties}`,
|
||||
));
|
||||
requests.push(linkPromise);
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有有效请求
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
if (nodePromise) {
|
||||
const nodeResponse = responses.shift()!;
|
||||
if (!nodeResponse.ok)
|
||||
throw new Error(`Node fetch failed: ${nodeResponse.status}`);
|
||||
nodeRecords = await nodeResponse.json();
|
||||
// 缓存数据(修复键以包含 schemeName)
|
||||
nodeCacheRef.current.set(
|
||||
`${query_time}_${junctionProperties}_${schemeName}_${schemeType}`,
|
||||
nodeRecords || [],
|
||||
);
|
||||
}
|
||||
if (linkPromise) {
|
||||
const linkResponse = responses.shift()!;
|
||||
if (!linkResponse.ok)
|
||||
throw new Error(`Link fetch failed: ${linkResponse.status}`);
|
||||
linkRecords = await linkResponse.json();
|
||||
// 缓存数据(修复键以包含 schemeName)
|
||||
linkCacheRef.current.set(
|
||||
`${query_time}_${pipeProperties}_${schemeName}_${schemeType}`,
|
||||
linkRecords || [],
|
||||
);
|
||||
}
|
||||
// 更新状态
|
||||
updateDataStates(nodeRecords.results || [], linkRecords.results || []);
|
||||
};
|
||||
|
||||
// 提取更新状态的逻辑
|
||||
const updateDataStates = (nodeResults: any[], linkResults: any[]) => {
|
||||
if (setCurrentJunctionCalData) {
|
||||
setCurrentJunctionCalData(nodeResults);
|
||||
} else {
|
||||
console.log("setCurrentJunctionCalData is undefined");
|
||||
}
|
||||
if (setCurrentPipeCalData) {
|
||||
setCurrentPipeCalData(linkResults);
|
||||
} else {
|
||||
console.log("setCurrentPipeCalData is undefined");
|
||||
}
|
||||
};
|
||||
|
||||
// 时间刻度数组 (每5分钟一个刻度)
|
||||
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
||||
value: i * 5,
|
||||
label: i % 24 === 0 ? formatTime(i * 5) : "",
|
||||
}));
|
||||
|
||||
// 格式化时间显示
|
||||
function formatTime(minutes: number): string {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours.toString().padStart(2, "0")}:${mins
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function currentTimeToDate(selectedDate: Date, minutes: number): Date {
|
||||
const date = new Date(selectedDate);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
date.setHours(hours, mins, 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
// 播放时间间隔选项
|
||||
const intervalOptions = [
|
||||
{ value: 5000, label: "5秒" },
|
||||
{ value: 10000, label: "10秒" },
|
||||
{ value: 15000, label: "15秒" },
|
||||
{ value: 20000, label: "20秒" },
|
||||
];
|
||||
// 强制计算时间段选项
|
||||
const calculatedIntervalOptions = [
|
||||
{ value: 1440, label: "1 天" },
|
||||
{ value: 60, label: "1 小时" },
|
||||
{ value: 30, label: "30 分钟" },
|
||||
{ value: 15, label: "15 分钟" },
|
||||
{ value: 5, label: "5 分钟" },
|
||||
];
|
||||
|
||||
// 处理时间轴滑动
|
||||
const handleSliderChange = useCallback(
|
||||
(event: Event, newValue: number | number[]) => {
|
||||
const value = Array.isArray(newValue) ? newValue[0] : newValue;
|
||||
// 如果有时间范围限制,只允许在范围内拖动
|
||||
if (timeRange && (value < minTime || value > maxTime)) {
|
||||
return;
|
||||
}
|
||||
// 防抖设置currentTime,避免频繁触发数据获取
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setCurrentTime(value);
|
||||
}, 500); // 500ms 防抖延迟
|
||||
},
|
||||
[timeRange, minTime, maxTime],
|
||||
);
|
||||
|
||||
// 播放控制
|
||||
const handlePlay = useCallback(() => {
|
||||
if (!isPlaying) {
|
||||
// if (junctionText === "" && pipeText === "") {
|
||||
// open?.({
|
||||
// type: "error",
|
||||
// message: "请至少设定并应用一个图层的样式。",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
setIsPlaying(true);
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentTime((prev) => {
|
||||
let next = prev + 15;
|
||||
if (timeRange) {
|
||||
if (next > maxTime) next = minTime;
|
||||
} else {
|
||||
if (next >= 1440) next = 0;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, playInterval);
|
||||
}
|
||||
}, [isPlaying, playInterval]);
|
||||
|
||||
const handlePause = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
// 设置为当前时间
|
||||
const currentTime = new Date();
|
||||
const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
setCurrentTime(minutes); // 组件卸载时重置时间
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 步进控制
|
||||
const handleDayStepBackward = useCallback(() => {
|
||||
setSelectedDate((prev) => {
|
||||
const newDate = new Date(prev);
|
||||
newDate.setDate(newDate.getDate() - 1);
|
||||
return newDate;
|
||||
});
|
||||
}, []);
|
||||
const handleDayStepForward = useCallback(() => {
|
||||
setSelectedDate((prev) => {
|
||||
const newDate = new Date(prev);
|
||||
newDate.setDate(newDate.getDate() + 1);
|
||||
return newDate;
|
||||
});
|
||||
}, []);
|
||||
const handleStepBackward = useCallback(() => {
|
||||
setCurrentTime((prev) => {
|
||||
let next = prev - 15;
|
||||
if (timeRange) {
|
||||
if (next < minTime) next = maxTime;
|
||||
} else {
|
||||
if (next < 0) next += 1440;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [timeRange, minTime, maxTime]);
|
||||
|
||||
const handleStepForward = useCallback(() => {
|
||||
setCurrentTime((prev) => {
|
||||
let next = prev + 15;
|
||||
if (timeRange) {
|
||||
if (next > maxTime) next = minTime;
|
||||
} else {
|
||||
if (next >= 1440) next = 0;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [timeRange, minTime, maxTime]);
|
||||
|
||||
// 日期选择处理
|
||||
const handleDateChange = useCallback((newDate: Date | null) => {
|
||||
if (newDate) {
|
||||
setSelectedDate(newDate);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 播放间隔改变处理
|
||||
const handleIntervalChange = useCallback(
|
||||
(event: any) => {
|
||||
const newInterval = event.target.value;
|
||||
setPlayInterval(newInterval);
|
||||
|
||||
// 如果正在播放,重新启动定时器
|
||||
if (isPlaying && intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentTime((prev) => {
|
||||
let next = prev + 15;
|
||||
if (timeRange) {
|
||||
if (next > maxTime) next = minTime;
|
||||
} else {
|
||||
if (next >= 1440) next = 0;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, newInterval);
|
||||
}
|
||||
},
|
||||
[isPlaying],
|
||||
);
|
||||
// 计算时间段改变处理
|
||||
const handleCalculatedIntervalChange = useCallback((event: any) => {
|
||||
const newInterval = event.target.value;
|
||||
setCalculatedInterval(newInterval);
|
||||
}, []);
|
||||
|
||||
// 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据
|
||||
useEffect(() => {
|
||||
// 首次加载时,如果 selectedDate 或 currentTime 未定义,则跳过执行,避免报错
|
||||
if (selectedDate && currentTime !== undefined && currentTime !== -1) {
|
||||
// 检查至少一个属性有值
|
||||
// const junctionProperties = junctionText;
|
||||
// const pipeProperties = pipeText;
|
||||
// if (junctionProperties === "" && pipeProperties === "") {
|
||||
// open?.({
|
||||
// type: "error",
|
||||
// message: "请至少设定并应用一个图层的样式。",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
fetchFrameData(
|
||||
currentTimeToDate(selectedDate, currentTime),
|
||||
junctionText,
|
||||
pipeText,
|
||||
schemeName,
|
||||
schemeType,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
junctionText,
|
||||
pipeText,
|
||||
currentTime,
|
||||
selectedDate,
|
||||
schemeName,
|
||||
schemeType,
|
||||
]);
|
||||
|
||||
// 组件卸载时清理定时器和防抖
|
||||
useEffect(() => {
|
||||
// 设置为当前时间
|
||||
const currentTime = new Date();
|
||||
const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
// 找到最近的前15分钟刻度
|
||||
const roundedMinutes = Math.floor(minutes / 15) * 15;
|
||||
setCurrentTime(roundedMinutes); // 组件卸载时重置时间
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 当 timeRange 改变时,设置 currentTime 到 minTime
|
||||
useEffect(() => {
|
||||
if (timeRange) {
|
||||
setCurrentTime(minTime);
|
||||
}
|
||||
}, [timeRange, minTime]);
|
||||
// 获取地图实例
|
||||
const map = useMap();
|
||||
// 这里防止地图缩放时,瓦片重新加载引起的属性更新出错
|
||||
useEffect(() => {
|
||||
// 监听地图缩放事件,缩放时停止播放
|
||||
if (map) {
|
||||
const onZoom = () => {
|
||||
handlePause();
|
||||
};
|
||||
map.getView().on("change:resolution", onZoom);
|
||||
|
||||
// 清理事件监听
|
||||
return () => {
|
||||
map.getView().un("change:resolution", onZoom);
|
||||
};
|
||||
}
|
||||
}, [map, handlePause]);
|
||||
// 清除当天当前时间点后的缓存并重新获取数据
|
||||
const clearCacheAndRefetch = (date: Date, timeInMinutes: number) => {
|
||||
const dateStr = date.toISOString().split("T")[0];
|
||||
|
||||
const clearCache = (
|
||||
cacheRef: ReturnType<typeof useRef<Map<string, any[]>>>,
|
||||
) => {
|
||||
if (!cacheRef.current) return;
|
||||
const cacheKeys = Array.from(cacheRef.current.keys());
|
||||
cacheKeys.forEach((key) => {
|
||||
const keyParts = key.split("_");
|
||||
const cacheDate = keyParts[0].split("T")[0];
|
||||
const cacheTimeStr = keyParts[0].split("T")[1];
|
||||
|
||||
if (cacheDate === dateStr && cacheTimeStr) {
|
||||
const [hours, minutes] = cacheTimeStr.split(":");
|
||||
const cacheTimeInMinutes =
|
||||
(parseInt(hours) + 8) * 60 + parseInt(minutes);
|
||||
|
||||
if (cacheTimeInMinutes >= timeInMinutes && cacheRef.current) {
|
||||
cacheRef.current.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
clearCache(nodeCacheRef);
|
||||
clearCache(linkCacheRef);
|
||||
// 重新获取当前时刻的新数据
|
||||
fetchFrameData(
|
||||
currentTimeToDate(selectedDate, currentTime),
|
||||
junctionText,
|
||||
pipeText,
|
||||
schemeName,
|
||||
schemeType,
|
||||
);
|
||||
};
|
||||
|
||||
const handleForceCalculate = async () => {
|
||||
if (!NETWORK_NAME) {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "管网名称缺失,无法进行强制计算。",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 提前提取日期和时间值,避免异步操作期间被时间轴拖动改变
|
||||
const calculationDate = selectedDate;
|
||||
const calculationTime = currentTime;
|
||||
const calculationDateStr = calculationDate.toISOString().split("T")[0];
|
||||
|
||||
setIsCalculating(true);
|
||||
// 显示处理中的通知
|
||||
open?.({
|
||||
type: "progress",
|
||||
message: "正在强制计算,请稍候...",
|
||||
undoableTimeout: 3,
|
||||
});
|
||||
try {
|
||||
const body = {
|
||||
name: NETWORK_NAME,
|
||||
simulation_date: calculationDateStr, // YYYY-MM-DD
|
||||
start_time: `${formatTime(calculationTime)}:00`, // HH:MM:00
|
||||
duration: calculatedInterval,
|
||||
};
|
||||
|
||||
const response = await apiFetch(
|
||||
`${config.BACKEND_URL}/api/v1/runsimulationmanuallybydate/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
open?.({
|
||||
type: "success",
|
||||
message: "重新计算成功",
|
||||
});
|
||||
// 清空当天当前时刻及之后的缓存并重新获取数据
|
||||
clearCacheAndRefetch(calculationDate, calculationTime);
|
||||
} else {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "重新计算失败",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Recalculation failed:", error);
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "重新计算时发生错误",
|
||||
});
|
||||
} finally {
|
||||
setIsCalculating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable nodeRef={draggableRef} handle=".drag-handle">
|
||||
<div
|
||||
ref={draggableRef}
|
||||
className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 w-[920px] opacity-90 hover:opacity-100 transition-opacity duration-300"
|
||||
>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
p: 2,
|
||||
pt: 1, // 减小顶部内边距,为拖拽柄留出空间
|
||||
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
{/* 拖拽柄区域 */}
|
||||
<Box
|
||||
className="drag-handle"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "16px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
cursor: "move",
|
||||
mb: 1,
|
||||
borderRadius: "4px 4px 0 0",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "40px",
|
||||
height: "4px",
|
||||
backgroundColor: "grey.400",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{/* 控制按钮栏 */}
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }}
|
||||
>
|
||||
<Tooltip title="后退一天">
|
||||
<span>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleDayStepBackward}
|
||||
size="small"
|
||||
disabled={disableDateSelection}
|
||||
>
|
||||
<FiSkipBack />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/* 日期选择器 */}
|
||||
<DatePicker
|
||||
label="模拟数据日期选择"
|
||||
value={dayjs(selectedDate)}
|
||||
onChange={(value) =>
|
||||
value && handleDateChange(value.toDate())
|
||||
}
|
||||
enableAccessibleFieldDOMStructure={false}
|
||||
format="YYYY-MM-DD"
|
||||
sx={{ width: 180 }}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
},
|
||||
}}
|
||||
maxDate={dayjs(new Date())} // 禁止选取未来的日期
|
||||
disabled={disableDateSelection}
|
||||
/>
|
||||
<Tooltip title="前进一天">
|
||||
<span>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleDayStepForward}
|
||||
size="small"
|
||||
disabled={
|
||||
disableDateSelection ||
|
||||
selectedDate.toDateString() ===
|
||||
new Date().toDateString()
|
||||
}
|
||||
>
|
||||
<FiSkipForward />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/* 播放控制按钮 */}
|
||||
<Box sx={{ display: "flex", gap: 1 }} className="ml-4">
|
||||
{/* 播放间隔选择 */}
|
||||
<FormControl size="small" sx={{ minWidth: 100 }}>
|
||||
<InputLabel>播放间隔</InputLabel>
|
||||
<Select
|
||||
value={playInterval}
|
||||
label="播放间隔"
|
||||
onChange={handleIntervalChange}
|
||||
>
|
||||
{intervalOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Tooltip title="后退一步">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleStepBackward}
|
||||
size="small"
|
||||
>
|
||||
<TbRewindBackward15 />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={isPlaying ? "暂停" : "播放"}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={isPlaying ? handlePause : handlePlay}
|
||||
size="small"
|
||||
>
|
||||
{isPlaying ? <Pause /> : <PlayArrow />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="前进一步">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleStepForward}
|
||||
size="small"
|
||||
>
|
||||
<TbRewindForward15 />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="停止">
|
||||
<IconButton
|
||||
color="secondary"
|
||||
onClick={handleStop}
|
||||
size="small"
|
||||
>
|
||||
<Stop />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }} className="ml-4">
|
||||
{/* 强制计算时间段 */}
|
||||
<FormControl size="small" sx={{ minWidth: 100 }}>
|
||||
<InputLabel>计算时间段</InputLabel>
|
||||
<Select
|
||||
value={calculatedInterval}
|
||||
label="强制计算时间段"
|
||||
onChange={handleCalculatedIntervalChange}
|
||||
>
|
||||
{calculatedIntervalOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 功能按钮 */}
|
||||
<Tooltip title="强制计算">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
onClick={handleForceCalculate}
|
||||
disabled={isCalculating}
|
||||
>
|
||||
强制计算
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* 当前时间显示 */}
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
ml: "auto",
|
||||
fontWeight: "bold",
|
||||
color: "primary.main",
|
||||
}}
|
||||
>
|
||||
{formatTime(currentTime)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Box ref={timelineRef} sx={{ px: 2, position: "relative" }}>
|
||||
<Slider
|
||||
value={currentTime}
|
||||
min={0}
|
||||
max={1440} // 24:00 = 1440分钟
|
||||
step={15} // 每15分钟一个步进
|
||||
marks={timeMarks.filter((_, index) => index % 12 === 0)} // 每小时显示一个标记
|
||||
onChange={handleSliderChange}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={formatTime}
|
||||
sx={{
|
||||
zIndex: 10,
|
||||
height: 8,
|
||||
"& .MuiSlider-track": {
|
||||
backgroundColor: "primary.main",
|
||||
height: 6,
|
||||
display: timeRange ? "none" : "block",
|
||||
},
|
||||
"& .MuiSlider-rail": {
|
||||
backgroundColor: "grey.300",
|
||||
height: 6,
|
||||
},
|
||||
"& .MuiSlider-thumb": {
|
||||
height: 20,
|
||||
width: 20,
|
||||
backgroundColor: "primary.main",
|
||||
border: "2px solid #fff",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
||||
"&:hover": {
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
||||
},
|
||||
},
|
||||
"& .MuiSlider-mark": {
|
||||
backgroundColor: "grey.400",
|
||||
height: 4,
|
||||
width: 2,
|
||||
},
|
||||
"& .MuiSlider-markActive": {
|
||||
backgroundColor: "primary.main",
|
||||
},
|
||||
"& .MuiSlider-markLabel": {
|
||||
fontSize: "0.75rem",
|
||||
color: "grey.600",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/* 禁用区域遮罩 */}
|
||||
{timeRange && (
|
||||
<>
|
||||
{/* 左侧禁用区域 */}
|
||||
{minTime > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: "14px",
|
||||
top: "30%",
|
||||
transform: "translateY(-50%)",
|
||||
width: `${(minTime / 1440) * 856 + 2}px`,
|
||||
height: "20px",
|
||||
backgroundColor: "rgba(189, 189, 189, 0.4)",
|
||||
pointerEvents: "none",
|
||||
backdropFilter: "blur(1px)",
|
||||
borderRadius: "2.5px",
|
||||
rounded: "true",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* 右侧禁用区域 */}
|
||||
{maxTime < 1440 && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: `${16 + (maxTime / 1440) * 856}px`,
|
||||
top: "30%",
|
||||
transform: "translateY(-50%)",
|
||||
width: `${((1440 - maxTime) / 1440) * 856}px`,
|
||||
height: "20px",
|
||||
backgroundColor: "rgba(189, 189, 189, 0.4)",
|
||||
pointerEvents: "none",
|
||||
backdropFilter: "blur(1px)",
|
||||
borderRadius: "2.5px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
@@ -0,0 +1,869 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useData, useMap } from "../MapComponent";
|
||||
import ToolbarButton from "@/components/olmap/common/ToolbarButton";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined";
|
||||
import QueryStatsOutlinedIcon from "@mui/icons-material/QueryStatsOutlined";
|
||||
import PropertyPanel from "./PropertyPanel"; // 引入属性面板组件
|
||||
import DrawPanel from "./DrawPanel"; // 引入绘图面板组件
|
||||
import HistoryDataPanel from "./HistoryDataPanel"; // 引入绘图面板组件
|
||||
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import { Style, Stroke, Fill, Circle } from "ol/style";
|
||||
import Feature from "ol/Feature";
|
||||
import StyleEditorPanel from "./StyleEditorPanel";
|
||||
import { LayerStyleState } from "./StyleEditorPanel";
|
||||
import StyleLegend from "./StyleLegend"; // 引入图例组件
|
||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
|
||||
import { config } from "@/config/config";
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
|
||||
|
||||
// 添加接口定义隐藏按钮的props
|
||||
interface ToolbarProps {
|
||||
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
||||
queryType?: string; // 可选的查询类型参数
|
||||
schemeType?: string; // 可选的方案类型参数
|
||||
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
||||
}
|
||||
const Toolbar: React.FC<ToolbarProps> = ({
|
||||
hiddenButtons,
|
||||
queryType,
|
||||
schemeType,
|
||||
HistoryPanel,
|
||||
}) => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
const { open } = useNotification();
|
||||
if (!data) return null;
|
||||
const { currentTime, selectedDate, schemeName } = data;
|
||||
const [activeTools, setActiveTools] = useState<string[]>([]);
|
||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
||||
const [showPropertyPanel, setShowPropertyPanel] = useState<boolean>(false);
|
||||
const [showDrawPanel, setShowDrawPanel] = useState<boolean>(false);
|
||||
const [showStyleEditor, setShowStyleEditor] = useState<boolean>(false);
|
||||
const [showHistoryPanel, setShowHistoryPanel] = useState<boolean>(false);
|
||||
const [highlightLayer, setHighlightLayer] =
|
||||
useState<VectorLayer<VectorSource> | null>(null);
|
||||
|
||||
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
|
||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
|
||||
{
|
||||
isActive: false, // 默认不激活,不显示图例
|
||||
layerId: "junctions",
|
||||
layerName: "节点",
|
||||
styleConfig: {
|
||||
property: "pressure",
|
||||
classificationMethod: "custom_breaks",
|
||||
customBreaks: [16, 18, 20, 22, 24, 26],
|
||||
customColors: [
|
||||
"rgba(255, 0, 0, 1)",
|
||||
"rgba(255, 127, 0, 1)",
|
||||
"rgba(255, 215, 0, 1)",
|
||||
"rgba(199, 224, 0, 1)",
|
||||
"rgba(76, 175, 80, 1)",
|
||||
"rgba(0, 158, 115, 1)",
|
||||
],
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "rainbow",
|
||||
singlePaletteIndex: 0,
|
||||
gradientPaletteIndex: 0,
|
||||
rainbowPaletteIndex: 0,
|
||||
showLabels: false,
|
||||
showId: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "junctions",
|
||||
layerName: "节点",
|
||||
property: "压力", // 暂时为空,等计算后更新
|
||||
colors: [],
|
||||
type: "point",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
isActive: false, // 默认不激活,不显示图例
|
||||
layerId: "pipes",
|
||||
layerName: "管道",
|
||||
styleConfig: {
|
||||
property: "flow",
|
||||
classificationMethod: "pretty_breaks",
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "gradient",
|
||||
singlePaletteIndex: 0,
|
||||
gradientPaletteIndex: 0,
|
||||
rainbowPaletteIndex: 0,
|
||||
showLabels: false,
|
||||
showId: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "pipes",
|
||||
layerName: "管道",
|
||||
property: "流量", // 暂时为空,等计算后更新
|
||||
colors: [],
|
||||
type: "linestring",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 计算激活的图例配置
|
||||
const activeLegendConfigs = layerStyleStates
|
||||
.filter((state) => state.isActive && state.legendConfig.property)
|
||||
.map((state) => ({
|
||||
...state.legendConfig,
|
||||
layerName: state.layerName,
|
||||
layerId: state.layerId,
|
||||
}));
|
||||
// 创建高亮图层
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const highLightSource = new VectorSource();
|
||||
const highLightLayer = new VectorLayer({
|
||||
source: highLightSource,
|
||||
style: new Style({
|
||||
stroke: new Stroke({
|
||||
color: `rgba(255, 0, 0, 1)`,
|
||||
width: 5,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: `rgba(255, 0, 0, 0.2)`,
|
||||
}),
|
||||
image: new Circle({
|
||||
radius: 7,
|
||||
stroke: new Stroke({
|
||||
color: `rgba(255, 0, 0, 1)`,
|
||||
width: 3,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: `rgba(255, 0, 0, 0.2)`,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
properties: {
|
||||
name: "属性查询高亮图层", // 设置图层名称
|
||||
value: "info_highlight_layer",
|
||||
type: "multigeometry",
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer(highLightLayer);
|
||||
setHighlightLayer(highLightLayer);
|
||||
|
||||
return () => {
|
||||
map.removeLayer(highLightLayer);
|
||||
};
|
||||
}, [map]);
|
||||
// 高亮要素的函数
|
||||
useEffect(() => {
|
||||
if (!highlightLayer) {
|
||||
return;
|
||||
}
|
||||
const source = highlightLayer.getSource();
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
// 清除之前的高亮
|
||||
source.clear();
|
||||
// 添加新的高亮要素
|
||||
highlightFeatures.forEach((feature) => {
|
||||
if (feature instanceof Feature) {
|
||||
source.addFeature(feature);
|
||||
}
|
||||
});
|
||||
}, [highlightFeatures, highlightLayer]);
|
||||
// 地图点击选择要素事件处理函数
|
||||
const handleMapClickSelectFeatures = useCallback(
|
||||
async (event: { coordinate: number[] }) => {
|
||||
if (!map) return;
|
||||
const feature = await mapClickSelectFeatures(event, map); // 调用导入的函数
|
||||
|
||||
if (!feature || !(feature instanceof Feature)) {
|
||||
// 如果没有点击到要素,且当前是 info 模式,则清除高亮
|
||||
if (activeTools.includes("info")) {
|
||||
setHighlightFeatures([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTools.includes("history")) {
|
||||
// 历史查询模式:支持同类型多选
|
||||
const featureId = feature.getProperties().id;
|
||||
const layerId = feature.getId()?.toString().split(".")[0] || "";
|
||||
console.log("点击选择要素", feature, "图层:", layerId);
|
||||
// 简单的类型检查函数
|
||||
const getBaseType = (lid: string) => {
|
||||
if (lid.includes("pipe")) return "pipe";
|
||||
if (lid.includes("junction")) return "junction";
|
||||
if (lid.includes("tank")) return "tank";
|
||||
if (lid.includes("reservoir")) return "reservoir";
|
||||
if (lid.includes("pump")) return "pump";
|
||||
if (lid.includes("valve")) return "valve";
|
||||
return lid;
|
||||
};
|
||||
|
||||
// 检查是否与已选要素类型一致
|
||||
if (highlightFeatures.length > 0) {
|
||||
const firstLayerId =
|
||||
highlightFeatures[0].getId()?.toString().split(".")[0] || "";
|
||||
|
||||
if (getBaseType(layerId) !== getBaseType(firstLayerId)) {
|
||||
// 如果点击的是已选中的要素(为了取消选中),则不报错
|
||||
const isAlreadySelected = highlightFeatures.some(
|
||||
(f) => f.getProperties().id === featureId,
|
||||
);
|
||||
if (!isAlreadySelected) {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "请选择相同类型的要素进行多选查询。",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHighlightFeatures((prev) => {
|
||||
const existingIndex = prev.findIndex(
|
||||
(f) => f.getProperties().id === featureId,
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// 如果已存在,移除
|
||||
return prev.filter((_, i) => i !== existingIndex);
|
||||
} else {
|
||||
// 如果不存在,添加
|
||||
return [...prev, feature];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 其他模式(如 info):单选
|
||||
setHighlightFeatures([feature]);
|
||||
}
|
||||
},
|
||||
[map, activeTools, highlightFeatures, open],
|
||||
);
|
||||
// 添加矢量属性查询事件监听器
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
// 监听 info 或 history 工具激活时添加
|
||||
if (activeTools.includes("info") || activeTools.includes("history")) {
|
||||
map.on("click", handleMapClickSelectFeatures);
|
||||
|
||||
return () => {
|
||||
map.un("click", handleMapClickSelectFeatures);
|
||||
};
|
||||
}
|
||||
}, [activeTools, map, handleMapClickSelectFeatures]);
|
||||
|
||||
// 处理工具栏按钮点击事件
|
||||
const handleToolClick = (tool: string) => {
|
||||
// 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭
|
||||
if (tool === "style") {
|
||||
if (activeTools.includes("style")) {
|
||||
// 如果样式工具已激活,点击时关闭
|
||||
setShowStyleEditor(false);
|
||||
setActiveTools((prev) => prev.filter((t) => t !== "style"));
|
||||
} else {
|
||||
// 激活样式工具,打开样式面板
|
||||
setActiveTools((prev) => [...prev, "style"]);
|
||||
setShowStyleEditor(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他工具的处理逻辑
|
||||
if (activeTools.includes(tool)) {
|
||||
// 如果当前工具已激活,再次点击时取消激活并关闭面板
|
||||
deactivateTool(tool);
|
||||
setActiveTools((prev) => prev.filter((t) => t !== tool));
|
||||
} else {
|
||||
// 如果当前工具未激活,先关闭所有其他工具,然后激活当前工具
|
||||
// 关闭所有面板(但保持样式编辑器状态)
|
||||
closeAllPanelsExceptStyle();
|
||||
|
||||
// 取消激活所有非样式工具
|
||||
setActiveTools((prev) => {
|
||||
const styleActive = prev.includes("style");
|
||||
return styleActive ? ["style", tool] : [tool];
|
||||
});
|
||||
|
||||
// 激活当前工具并打开对应面板
|
||||
activateTool(tool);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消激活指定工具并关闭对应面板
|
||||
const deactivateTool = (tool: string) => {
|
||||
switch (tool) {
|
||||
case "info":
|
||||
setShowPropertyPanel(false);
|
||||
setHighlightFeatures([]);
|
||||
break;
|
||||
case "draw":
|
||||
setShowDrawPanel(false);
|
||||
break;
|
||||
case "history":
|
||||
setShowHistoryPanel(false);
|
||||
setHighlightFeatures([]);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 激活指定工具并打开对应面板
|
||||
const activateTool = (tool: string) => {
|
||||
switch (tool) {
|
||||
case "info":
|
||||
setShowPropertyPanel(true);
|
||||
break;
|
||||
case "draw":
|
||||
setShowDrawPanel(true);
|
||||
break;
|
||||
case "history":
|
||||
setShowHistoryPanel(true);
|
||||
// 激活历史查询后:HistoryDataPanel 自行负责根据传入的 props 拉取数据。
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭所有面板(除了样式编辑器)
|
||||
const closeAllPanelsExceptStyle = () => {
|
||||
setShowPropertyPanel(false);
|
||||
setHighlightFeatures([]);
|
||||
setShowDrawPanel(false);
|
||||
setShowHistoryPanel(false);
|
||||
// 样式编辑器保持其当前状态,不自动关闭
|
||||
};
|
||||
const [computedProperties, setComputedProperties] = useState<
|
||||
Record<string, any>
|
||||
>({});
|
||||
// 添加 useEffect 来查询计算属性
|
||||
useEffect(() => {
|
||||
if (highlightFeatures.length === 0 || !selectedDate || !showPropertyPanel) {
|
||||
setComputedProperties({});
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightFeature = highlightFeatures[0];
|
||||
const id = highlightFeature.getProperties().id;
|
||||
if (!id) {
|
||||
setComputedProperties({});
|
||||
return;
|
||||
}
|
||||
|
||||
const queryComputedProperties = async () => {
|
||||
try {
|
||||
const properties = highlightFeature?.getProperties?.() || {};
|
||||
const type =
|
||||
properties.geometry?.getType?.() === "LineString" ? "link" : "node";
|
||||
// selectedDate 格式化为 YYYY-MM-DD
|
||||
let dateObj: Date;
|
||||
if (selectedDate instanceof Date) {
|
||||
dateObj = new Date(selectedDate);
|
||||
} else {
|
||||
dateObj = new Date(selectedDate);
|
||||
}
|
||||
const minutes = Number(currentTime) || 0;
|
||||
dateObj.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
|
||||
// 转为 UTC ISO 字符串
|
||||
const querytime = dateObj.toISOString(); // 例如 "2025-09-16T16:30:00.000Z"
|
||||
let response;
|
||||
if (queryType === "scheme") {
|
||||
response = await apiFetch(
|
||||
// `${config.BACKEND_URL}/queryschemesimulationrecordsbyidtime/?scheme_name=${schemeName}&id=${id}&querytime=${querytime}&type=${type}`
|
||||
`${config.BACKEND_URL}/api/v1/scheme/query/by-id-time?scheme_type=${schemeType}&scheme_name=${schemeName}&id=${id}&type=${type}&query_time=${querytime}`,
|
||||
);
|
||||
} else {
|
||||
response = await apiFetch(
|
||||
// `${config.BACKEND_URL}/querysimulationrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}`
|
||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-id-time?id=${id}&type=${type}&query_time=${querytime}`,
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error("API request failed");
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data.result || data.result.length === 0) {
|
||||
setComputedProperties({});
|
||||
} else {
|
||||
setComputedProperties(data.result[0] || {});
|
||||
console.log("查询到的计算属性:", data.result[0]);
|
||||
console.log(computedProperties);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error querying computed properties:", error);
|
||||
setComputedProperties({});
|
||||
}
|
||||
};
|
||||
// 仅当 currentTime 有效时查询
|
||||
if (currentTime !== -1 && queryType) queryComputedProperties();
|
||||
}, [highlightFeatures, currentTime, selectedDate, queryType, schemeName, schemeType]);
|
||||
|
||||
// 从要素属性中提取属性面板需要的数据
|
||||
const getFeatureProperties = useCallback(() => {
|
||||
if (highlightFeatures.length === 0) return {};
|
||||
const highlightFeature = highlightFeatures[0];
|
||||
const layer = highlightFeature?.getId()?.toString().split(".")[0];
|
||||
const properties = highlightFeature.getProperties();
|
||||
// 计算属性字段,增加 key 字段
|
||||
const pipeComputedFields = [
|
||||
{ key: "flow", label: "流量", unit: `${FLOW_DISPLAY_UNIT}` },
|
||||
{ key: "friction", label: "摩阻", unit: "" },
|
||||
{ key: "headloss", label: "水头损失", unit: "m" },
|
||||
{ key: "unit_headloss", label: "单位水头损失", unit: "m/km" },
|
||||
{ key: "quality", label: "水质", unit: "mg/L" },
|
||||
{ key: "reaction", label: "反应", unit: "1/d" },
|
||||
{ key: "setting", label: "设置", unit: "" },
|
||||
{ key: "status", label: "状态", unit: "" },
|
||||
{ key: "velocity", label: "流速", unit: "m/s" },
|
||||
];
|
||||
const nodeComputedFields = [
|
||||
{ key: "actual_demand", label: "实际需水量", unit: `${FLOW_DISPLAY_UNIT}` },
|
||||
{ key: "total_head", label: "水头", unit: "m" },
|
||||
{ key: "pressure", label: "压力", unit: "m" },
|
||||
{ key: "quality", label: "水质", unit: "mg/L" },
|
||||
];
|
||||
|
||||
if (layer === "geo_pipes_mat" || layer === "geo_pipes") {
|
||||
let result = {
|
||||
id: properties.id,
|
||||
type: "管道",
|
||||
properties: [
|
||||
{ label: "起始节点ID", value: properties.node1 },
|
||||
{ label: "终点节点ID", value: properties.node2 },
|
||||
{ label: "长度", value: properties.length?.toFixed?.(1), unit: "m" },
|
||||
{
|
||||
label: "管径",
|
||||
value: properties.diameter?.toFixed?.(1),
|
||||
unit: "mm",
|
||||
},
|
||||
{ label: "粗糙度", value: properties.roughness },
|
||||
{ label: "局部损失", value: properties.minor_loss },
|
||||
{ label: "初始状态", value: "开" },
|
||||
],
|
||||
};
|
||||
// 追加计算属性
|
||||
if (computedProperties) {
|
||||
pipeComputedFields.forEach(({ key, label, unit }) => {
|
||||
let value = computedProperties[key];
|
||||
|
||||
if (key === "flow" && value !== undefined) {
|
||||
value = toM3h(value, "lps");
|
||||
}
|
||||
|
||||
// 如果是单位水头损失且后端未返回,则通过水头损失/长度计算 (单位 m/km)
|
||||
if (
|
||||
key === "unit_headloss" &&
|
||||
value === undefined &&
|
||||
computedProperties.headloss !== undefined &&
|
||||
properties.length
|
||||
) {
|
||||
value = (computedProperties.headloss / properties.length) * 1000;
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
result.properties.push({
|
||||
label,
|
||||
value: typeof value === "number" ? value.toFixed(3) : value,
|
||||
unit,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (layer === "geo_junctions_mat" || layer === "geo_junctions") {
|
||||
let result = {
|
||||
id: properties.id,
|
||||
type: "节点",
|
||||
properties: [
|
||||
{
|
||||
label: "高程",
|
||||
value: properties.elevation?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
// 将 demand1~demand5 与 pattern1~pattern5 作为二级表格展示
|
||||
{
|
||||
type: "table",
|
||||
label: "基本需水量",
|
||||
columns: ["demand", "pattern"],
|
||||
rows: Array.from({ length: 5 }, (_, i) => i + 1)
|
||||
.map((idx) => {
|
||||
let d = properties?.[`demand${idx}`];
|
||||
const p = properties?.[`pattern${idx}`];
|
||||
// 仅当 demand 有效时展示该行
|
||||
if (d !== undefined && d !== null && d !== "") {
|
||||
d = toM3h(Number(d), "lps");
|
||||
return [typeof d === "number" ? d.toFixed(3) : d, p ?? "-"];
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as (string | number)[][],
|
||||
} as any,
|
||||
],
|
||||
};
|
||||
// 追加计算属性
|
||||
if (computedProperties) {
|
||||
nodeComputedFields.forEach(({ key, label, unit }) => {
|
||||
if (computedProperties[key] !== undefined) {
|
||||
let value = computedProperties[key];
|
||||
if (key === "actual_demand") {
|
||||
value = toM3h(value, "lps");
|
||||
}
|
||||
result.properties.push({
|
||||
label,
|
||||
value:
|
||||
value?.toFixed?.(3) || value,
|
||||
unit,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (layer === "geo_tanks_mat" || layer === "geo_tanks") {
|
||||
return {
|
||||
id: properties.id,
|
||||
type: "水池",
|
||||
properties: [
|
||||
{
|
||||
label: "高程",
|
||||
value: properties.elevation?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
{
|
||||
label: "初始水位",
|
||||
value: properties.init_level?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
{
|
||||
label: "最低水位",
|
||||
value: properties.min_level?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
{
|
||||
label: "最高水位",
|
||||
value: properties.max_level?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
{
|
||||
label: "直径",
|
||||
value: properties.diameter?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
{
|
||||
label: "最小容积",
|
||||
value: properties.min_vol?.toFixed?.(1),
|
||||
unit: "m³",
|
||||
},
|
||||
// {
|
||||
// label: "容积曲线",
|
||||
// value: properties.vol_curve,
|
||||
// },
|
||||
{
|
||||
label: "溢出",
|
||||
value: properties.overflow ? "是" : "否",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (layer === "geo_reservoirs_mat" || layer === "geo_reservoirs") {
|
||||
return {
|
||||
id: properties.id,
|
||||
type: "水库",
|
||||
properties: [
|
||||
{
|
||||
label: "水头",
|
||||
value: properties.head?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
// {
|
||||
// label: "模式",
|
||||
// value: properties.pattern,
|
||||
// },
|
||||
],
|
||||
};
|
||||
}
|
||||
if (layer === "geo_pumps_mat" || layer === "geo_pumps") {
|
||||
return {
|
||||
id: properties.id,
|
||||
type: "水泵",
|
||||
properties: [
|
||||
{ label: "起始节点 ID", value: properties.node1 },
|
||||
{ label: "终点节点 ID", value: properties.node2 },
|
||||
{
|
||||
label: "功率",
|
||||
value: properties.power?.toFixed?.(1),
|
||||
unit: "kW",
|
||||
},
|
||||
{
|
||||
label: "扬程",
|
||||
value: properties.head?.toFixed?.(1),
|
||||
unit: "m",
|
||||
},
|
||||
{
|
||||
label: "转速",
|
||||
value: properties.speed?.toFixed?.(1),
|
||||
unit: "rpm",
|
||||
},
|
||||
{
|
||||
label: "模式",
|
||||
value: properties.pattern,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (layer === "geo_valves_mat" || layer === "geo_valves") {
|
||||
return {
|
||||
id: properties.id,
|
||||
type: "阀门",
|
||||
properties: [
|
||||
{ label: "起始节点 ID", value: properties.node1 },
|
||||
{ label: "终点节点 ID", value: properties.node2 },
|
||||
{
|
||||
label: "直径",
|
||||
value: properties.diameter?.toFixed?.(1),
|
||||
unit: "mm",
|
||||
},
|
||||
{
|
||||
label: "阀门类型",
|
||||
value: properties.v_type,
|
||||
},
|
||||
// {
|
||||
// label: "设置",
|
||||
// value: properties.setting?.toFixed?.(2),
|
||||
// },
|
||||
{
|
||||
label: "局部损失",
|
||||
value: properties.minor_loss?.toFixed?.(2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
// 传输频率文字对应
|
||||
const getTransmissionFrequency = (transmission_frequency: string) => {
|
||||
// 传输频率文本:00:01:00,00:05:00,00:10:00,00:30:00,01:00:00,转换为分钟数
|
||||
const parts = transmission_frequency.split(":");
|
||||
if (parts.length !== 3) return transmission_frequency;
|
||||
const hours = parseInt(parts[0], 10);
|
||||
const minutes = parseInt(parts[1], 10);
|
||||
const seconds = parseInt(parts[2], 10);
|
||||
const totalMinutes = hours * 60 + minutes + (seconds >= 30 ? 1 : 0);
|
||||
return totalMinutes;
|
||||
};
|
||||
// 可靠度文字映射
|
||||
const getReliability = (reliability: number) => {
|
||||
switch (reliability) {
|
||||
case 1:
|
||||
return "高";
|
||||
case 2:
|
||||
return "中";
|
||||
case 3:
|
||||
return "低";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
if (layer === "geo_scada_mat" || layer === "geo_scada") {
|
||||
let result = {
|
||||
id: properties.id,
|
||||
type: "SCADA设备",
|
||||
properties: [
|
||||
{
|
||||
label: "类型",
|
||||
value:
|
||||
properties.type === "pipe_flow" ? "流量传感器" : "压力传感器",
|
||||
},
|
||||
{
|
||||
label: "关联节点 ID",
|
||||
value: properties.associated_element_id,
|
||||
},
|
||||
{
|
||||
label: "传输模式",
|
||||
value:
|
||||
properties.transmission_mode === "non_realtime"
|
||||
? "定时传输"
|
||||
: "实时传输",
|
||||
},
|
||||
{
|
||||
label: "传输频率",
|
||||
value: getTransmissionFrequency(properties.transmission_frequency),
|
||||
unit: "分钟",
|
||||
},
|
||||
{
|
||||
label: "可靠性",
|
||||
value: getReliability(properties.reliability),
|
||||
},
|
||||
],
|
||||
};
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
}, [highlightFeatures, computedProperties]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-4 left-4 bg-white p-1 rounded-xl shadow-lg flex opacity-85 hover:opacity-100 transition-opacity">
|
||||
{!hiddenButtons?.includes("info") && (
|
||||
<ToolbarButton
|
||||
icon={<InfoOutlinedIcon />}
|
||||
name="查看属性"
|
||||
isActive={activeTools.includes("info")}
|
||||
onClick={() => handleToolClick("info")}
|
||||
/>
|
||||
)}
|
||||
{!hiddenButtons?.includes("history") && (
|
||||
<ToolbarButton
|
||||
icon={<QueryStatsOutlinedIcon />}
|
||||
name="查询历史数据"
|
||||
isActive={activeTools.includes("history")}
|
||||
onClick={() => handleToolClick("history")}
|
||||
/>
|
||||
)}
|
||||
{!hiddenButtons?.includes("draw") && (
|
||||
<ToolbarButton
|
||||
icon={<EditOutlinedIcon />}
|
||||
name="标记绘制"
|
||||
isActive={activeTools.includes("draw")}
|
||||
onClick={() => handleToolClick("draw")}
|
||||
/>
|
||||
)}
|
||||
{!hiddenButtons?.includes("style") && (
|
||||
<ToolbarButton
|
||||
icon={<PaletteOutlinedIcon />}
|
||||
name="图层样式"
|
||||
isActive={activeTools.includes("style")}
|
||||
onClick={() => handleToolClick("style")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
|
||||
{showDrawPanel && map && <DrawPanel />}
|
||||
<div style={{ display: showStyleEditor ? "block" : "none" }}>
|
||||
<StyleEditorPanel
|
||||
layerStyleStates={layerStyleStates}
|
||||
setLayerStyleStates={setLayerStyleStates}
|
||||
/>
|
||||
</div>
|
||||
{showHistoryPanel &&
|
||||
(HistoryPanel ? (
|
||||
<HistoryPanel
|
||||
featureInfos={(() => {
|
||||
if (highlightFeatures.length === 0 || !showHistoryPanel)
|
||||
return [];
|
||||
|
||||
return highlightFeatures
|
||||
.map((feature) => {
|
||||
const properties = feature.getProperties();
|
||||
const id = properties.id;
|
||||
if (!id) return null;
|
||||
|
||||
// 从图层名称推断类型
|
||||
const layerId =
|
||||
feature.getId()?.toString().split(".")[0] || "";
|
||||
let type = "unknown";
|
||||
|
||||
if (layerId.includes("pipe")) {
|
||||
type = "pipe";
|
||||
} else if (layerId.includes("junction")) {
|
||||
type = "junction";
|
||||
} else if (layerId.includes("tank")) {
|
||||
type = "tank";
|
||||
} else if (layerId.includes("reservoir")) {
|
||||
type = "reservoir";
|
||||
} else if (layerId.includes("pump")) {
|
||||
type = "pump";
|
||||
} else if (layerId.includes("valve")) {
|
||||
type = "valve";
|
||||
}
|
||||
// 仅处理 type 为 pipe 或 junction 的情况
|
||||
if (type !== "pipe" && type !== "junction") {
|
||||
return null;
|
||||
}
|
||||
return [id, type];
|
||||
})
|
||||
.filter(Boolean) as [string, string][];
|
||||
})()}
|
||||
scheme_type="burst_Analysis"
|
||||
scheme_name={schemeName}
|
||||
type={queryType as "realtime" | "scheme" | "none"}
|
||||
/>
|
||||
) : (
|
||||
<HistoryDataPanel
|
||||
featureInfos={(() => {
|
||||
if (highlightFeatures.length === 0 || !showHistoryPanel)
|
||||
return [];
|
||||
|
||||
return highlightFeatures
|
||||
.map((feature) => {
|
||||
const properties = feature.getProperties();
|
||||
const id = properties.id;
|
||||
if (!id) return null;
|
||||
|
||||
// 从图层名称推断类型
|
||||
const layerId =
|
||||
feature.getId()?.toString().split(".")[0] || "";
|
||||
let type = "unknown";
|
||||
|
||||
if (layerId.includes("pipe")) {
|
||||
type = "pipe";
|
||||
} else if (layerId.includes("junction")) {
|
||||
type = "junction";
|
||||
} else if (layerId.includes("tank")) {
|
||||
type = "tank";
|
||||
} else if (layerId.includes("reservoir")) {
|
||||
type = "reservoir";
|
||||
} else if (layerId.includes("pump")) {
|
||||
type = "pump";
|
||||
} else if (layerId.includes("valve")) {
|
||||
type = "valve";
|
||||
}
|
||||
// 仅处理 type 为 pipe 或 junction 的情况
|
||||
if (type !== "pipe" && type !== "junction") {
|
||||
return null;
|
||||
}
|
||||
return [id, type];
|
||||
})
|
||||
.filter(Boolean) as [string, string][];
|
||||
})()}
|
||||
scheme_type="burst_Analysis"
|
||||
scheme_name={schemeName}
|
||||
type={queryType as "realtime" | "scheme" | "none"}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 图例显示 */}
|
||||
{activeLegendConfigs.length > 0 && (
|
||||
<div className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
||||
<div className="flex flex-row gap-3">
|
||||
{activeLegendConfigs.map((config, index) => (
|
||||
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import { useMap } from "../MapComponent";
|
||||
import AddRoundedIcon from "@mui/icons-material/AddRounded";
|
||||
import RemoveRoundedIcon from "@mui/icons-material/RemoveRounded";
|
||||
import FitScreenIcon from "@mui/icons-material/FitScreen";
|
||||
import { config } from "@config/config";
|
||||
|
||||
const Zoom: React.FC = () => {
|
||||
const map = useMap();
|
||||
|
||||
// 放大函数
|
||||
const handleZoomIn = () => {
|
||||
if (!map) return;
|
||||
const view = map.getView();
|
||||
view.animate({ zoom: (view.getZoom() ?? 0) + 1, duration: 200 });
|
||||
};
|
||||
|
||||
// 缩小函数
|
||||
const handleZoomOut = () => {
|
||||
if (!map) return;
|
||||
const view = map.getView();
|
||||
view.animate({ zoom: (view.getZoom() ?? 0) - 1, duration: 200 });
|
||||
};
|
||||
|
||||
// 缩放到全局 Extent
|
||||
const handleFitScreen = () => {
|
||||
if (!map) return;
|
||||
const view = map.getView();
|
||||
view.fit(config.MAP_EXTENT, { duration: 500 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute right-4 bottom-11 z-1300">
|
||||
<div className="w-8 h-26 flex flex-col gap-2 items-center">
|
||||
<div className="w-8 h-8 bg-gray-50 flex items-center justify-center rounded-xl drop-shadow-xl shadow-black">
|
||||
<button
|
||||
className="w-6 h-6 flex items-center justify-center rounded-xl hover:bg-white transition-all duration-100"
|
||||
onClick={handleFitScreen}
|
||||
>
|
||||
<FitScreenIcon fontSize="small" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-8 h-16 flex flex-col items-center justify-center bg-gray-50 rounded-xl drop-shadow-xl shadow-black">
|
||||
<button
|
||||
className="w-6 h-6 flex items-center justify-center rounded-xl hover:bg-white transition-all duration-100"
|
||||
onClick={handleZoomIn}
|
||||
aria-label="放大"
|
||||
>
|
||||
<AddRoundedIcon />
|
||||
</button>
|
||||
<div className="m-0.5 border-t-1 border-gray-300 w-6"></div>
|
||||
<button
|
||||
className="w-6 h-6 flex items-center justify-center rounded-xl hover:bg-white transition-all duration-100"
|
||||
onClick={handleZoomOut}
|
||||
aria-label="缩小"
|
||||
>
|
||||
<RemoveRoundedIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Zoom;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import Zoom from "./Controls/Zoom";
|
||||
import BaseLayers from "./Controls/BaseLayers";
|
||||
import ScaleLine from "./Controls/ScaleLine";
|
||||
import LayerControl from "./Controls/LayerControl";
|
||||
interface MapToolsProps {}
|
||||
|
||||
const MapTools: React.FC<MapToolsProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<Zoom />
|
||||
<ScaleLine />
|
||||
<BaseLayers />
|
||||
<LayerControl />
|
||||
{/* 继续添加其他自定义控件 */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MapTools;
|
||||
Reference in New Issue
Block a user