Files
TJWaterServer/src/components/olmap/NetworkPartitionOptimization/ZonePropsPanel.tsx

419 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useCallback, useState, useRef } from "react";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import Style from "ol/style/Style";
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 { useProject } from "@/contexts/ProjectContext";
interface PropertyItem {
key: string;
value: string | number | boolean;
label?: string;
}
interface ZonePropsPanelProps {
title?: string;
isVisible?: boolean;
onClose?: () => void;
}
const ZonePropsPanel: React.FC<ZonePropsPanelProps> = ({
title = "分区属性信息",
isVisible = true,
onClose,
}) => {
const map = useMap();
const project = useProject();
const workspace = project?.workspace;
const [props, setProps] = React.useState<
PropertyItem[] | Record<string, any>
>({});
const [highlightedFeature, setHighlightedFeature] = useState<any>(null);
const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
const handleMapClickSelectFeatures = useCallback(
(pixel: number[]) => {
if (!map || !highlightLayerRef.current) return;
let clickedFeature: any = null;
map.forEachFeatureAtPixel(pixel, (feature) => {
if (!clickedFeature) {
clickedFeature = feature;
}
});
if (clickedFeature) {
const layer = clickedFeature?.getId()?.toString().split(".")[0];
if (layer !== "network_zone") {
return;
}
setHighlightedFeature(clickedFeature);
setProps(clickedFeature.getProperties());
// 更新高亮图层
const source = highlightLayerRef.current.getSource();
source?.clear();
source?.addFeature(clickedFeature);
} else {
setHighlightedFeature(null);
setProps({});
// 清空高亮图层
const source = highlightLayerRef.current.getSource();
source?.clear();
}
},
[map]
);
// 将 properties 转换为统一格式
const formatProperties = (
props: PropertyItem[] | Record<string, any>
): PropertyItem[] => {
if (Array.isArray(props)) {
return props.filter((item) => !shouldHideProperty(item.key));
}
return Object.entries(props)
.filter(([key]) => !shouldHideProperty(key))
.map(([key, value]) => ({
key,
value,
label: getChineseLabel(key),
}));
};
// 判断是否应该隐藏某个属性
const shouldHideProperty = (key: string): boolean => {
const hiddenKeys = [
"id",
"geometry",
"Note1",
"Note3",
"Note4",
"Note5",
"Note6",
"Note7",
"Note8",
"Note9",
"Note10",
];
return hiddenKeys.includes(key);
};
useEffect(() => {
if (!map) {
return;
}
const workspaceValue = workspace || config.MAP_WORKSPACE;
const networkZoneLayer = new VectorLayer({
source: new VectorSource({
url: `${config.MAP_URL}/${workspaceValue}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${workspaceValue}:network_zone&outputFormat=application/json`,
format: new GeoJson(),
}),
style: new Style({
fill: new Fill({
color: "rgba(255, 255, 255, 0)",
}),
stroke: new Stroke({
color: "#e01414ff",
width: 5,
}),
}),
properties: {
name: "管网分区",
value: "network_zone",
},
});
map.addLayer(networkZoneLayer);
// 创建高亮图层
const highlightLayer = new VectorLayer({
source: new VectorSource(),
style: new Style({
fill: new Fill({
color: "rgba(255, 255, 0, 0.3)",
}),
stroke: new Stroke({
color: "#ff0000",
width: 3,
}),
}),
properties: {
name: "高亮分区",
value: "highlight_zone",
},
});
map.addLayer(highlightLayer);
highlightLayerRef.current = highlightLayer;
const clickListener = (evt: any) => {
handleMapClickSelectFeatures(evt.pixel);
};
map.on("click", clickListener);
return () => {
map.removeLayer(networkZoneLayer);
map.removeLayer(highlightLayer);
map.un("click", clickListener);
};
}, [map, handleMapClickSelectFeatures, workspace]);
// 获取中文标签
const getChineseLabel = (key: string): string => {
const labelMap: Record<string, string> = {
Id: "ID",
Area: "面积",
Complete: "完成度",
Consumptio: "消耗",
Descriptio: "描述",
FlowError: "流量误差",
Level: "级别",
ModelFlow: "模型流量",
NRW: "无收益水量",
NRWPercent: "无收益水量百分比",
Name: "名称",
Note1: "备注1",
Note2: "备注2",
Note3: "备注3",
Note4: "备注4",
Note5: "备注5",
Note6: "备注6",
Note7: "备注7",
Note8: "备注8",
Note9: "备注9",
Note10: "备注10",
ParentZone: "父区域",
PipeLength: "管道长度",
Population: "人口",
ScadaFlow: "SCADA流量",
Tag: "标签",
TotalFlowE: "总流量误差",
TotalModel: "总模型",
TotalScada: "总SCADA",
WaterConsu: "水消耗",
WaterSuppl: "水供应",
};
return labelMap[key] || key;
};
// 优先使用从store中获取的props如果没有则使用传入的properties
const dataToShow = props;
const formattedProperties = formatProperties(dataToShow);
// 定义属性的显示顺序
const propertyOrder = [
"Id",
"Name",
"PipeLength",
"ModelFlow",
"Population",
"Level",
"Note2",
"Area",
"Descriptio",
"ParentZone",
"Tag",
"Complete",
"Consumptio",
"FlowError",
"NRW",
"NRWPercent",
"ScadaFlow",
"TotalFlowE",
"TotalModel",
"TotalScada",
"WaterConsu",
"WaterSuppl",
];
// 根据自定义顺序对属性进行排序
const sortedProperties = [...formattedProperties].sort((a, b) => {
const aIndex = propertyOrder.indexOf(a.key);
const bIndex = propertyOrder.indexOf(b.key);
// 如果属性不在排序列表中,则将其放在末尾
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
return aIndex - bIndex;
});
// 格式化值显示
const formatValue = (value: any, key: string): string => {
if (value === null || value === undefined) {
return "-";
}
if (typeof value === "boolean") {
return value ? "是" : "否";
}
if (typeof value === "string" && value.trim() === "") {
return "-";
}
// 对于特定的数值字段,添加单位
if (typeof value === "number") {
switch (key) {
case "Area":
return `${value.toLocaleString()}`;
case "PipeLength":
return `${value.toLocaleString()} m`;
case "Population":
return `${value.toLocaleString()}`;
case "ModelFlow":
return `${value.toLocaleString()} L/天`;
case "ScadaFlow":
case "TotalModel":
case "TotalScada":
case "WaterConsu":
case "WaterSuppl":
return `${value.toLocaleString()} L/s`;
case "NRWPercent":
return value !== null ? `${value}%` : "-";
default:
return value.toLocaleString();
}
}
return String(value);
};
if (!isVisible) {
return null;
}
const isImportantKeys = ["Name", "Id", "ModelFlow", "Area", "PipeLength"];
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 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">{title}</h3>
</div>
{onClose && (
<button
onClick={onClose}
className="text-white hover:bg-white hover:bg-opacity-20 rounded-full p-1 transition-all duration-200"
aria-label="关闭"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
{/* 内容区域 */}
<div className="flex-1 overflow-y-auto px-4 py-3">
{sortedProperties.length === 0 ? (
<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">
{sortedProperties.map((item, index) => {
const isImportant = isImportantKeys.includes(item.key);
return (
<div
key={item.key || 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"
}`}
>
{item.label || item.key}
</span>
<span
className={`text-sm font-semibold text-right flex-1 ${
isImportant ? "text-blue-900" : "text-gray-800"
}`}
>
{formatValue(item.value, item.key)}
</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>
{sortedProperties.length}
</span>
{highlightedFeature && (
<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 ZonePropsPanel;