新增水质分析功能模块

This commit is contained in:
JIANG
2026-01-30 15:22:53 +08:00
parent c28325e997
commit d584268acd
12 changed files with 346 additions and 238 deletions

View File

@@ -31,24 +31,36 @@ 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 { 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 { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types";
interface SchemeQueryProps {
schemes?: ContaminantSchemeRecord[];
onSchemesChange?: (schemes: ContaminantSchemeRecord[]) => void;
onViewResults?: () => void;
network?: string;
}
const SCHEME_TYPE = "contaminant_simulation";
const SCHEME_TYPE = "contaminant_analysis";
const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemes: externalSchemes,
onSchemesChange,
onViewResults,
network = NETWORK_NAME,
}) => {
const [queryAll, setQueryAll] = useState<boolean>(true);
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [showTimeline, setShowTimeline] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [timeRange, setTimeRange] = useState<
@@ -64,8 +76,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const { open } = useNotification();
const map = useMap();
const data = useData();
const { schemeName, setSchemeName, setJunctionText, setPipeText } =
data || {};
const { schemeName, setSchemeName } = data || {};
const schemes =
externalSchemes !== undefined ? externalSchemes : internalSchemes;
@@ -79,6 +90,83 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}
}, [map]);
// 初始化高亮图层
useEffect(() => {
if (!map) return;
const themeColor = "rgba(3, 168, 107"; // #03a86b
const sourceStyle = [
// 外层扩散光圈
new Style({
image: new Circle({
radius: 12,
fill: new Fill({
color: `${themeColor}, 0.2)`,
}),
}),
}),
// 中层扩散背景
new Style({
image: new Circle({
radius: 8,
stroke: new Stroke({
color: `${themeColor}, 0.5)`,
width: 2,
}),
fill: new Fill({
color: `${themeColor}, 0.3)`,
}),
}),
}),
// 上层图标
new Style({
image: new Icon({
src: "/icons/contaminant_source.svg",
scale: 0.2,
anchor: [0.5, 1],
}),
}),
];
const highlightLayer = new VectorLayer({
source: new VectorSource(),
style: sourceStyle,
maxZoom: 24,
minZoom: 12,
properties: {
name: "污染源高亮",
value: "contaminant_source_highlight",
},
});
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 formatTime = (timeStr: string) => {
const time = moment(timeStr);
return time.format("MM-DD");
@@ -93,16 +181,18 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true);
try {
const response = await axios.get(
`${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`
`${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`,
);
let filteredResults = response.data;
if (!queryAll) {
const formattedDate = queryDate!.format("YYYY-MM-DD");
filteredResults = response.data.filter((item: ContaminantSchemaItem) => {
const itemDate = moment(item.create_time).format("YYYY-MM-DD");
return itemDate === formattedDate;
});
filteredResults = response.data.filter(
(item: ContaminantSchemaItem) => {
const itemDate = moment(item.create_time).format("YYYY-MM-DD");
return itemDate === formattedDate;
},
);
}
setSchemes(
@@ -114,7 +204,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
create_time: item.create_time,
startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail,
}))
})),
);
if (filteredResults.length === 0) {
@@ -138,19 +228,29 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}
};
const handleLocateSource = (sourceId?: string) => {
if (!sourceId) return;
queryFeaturesByIds([sourceId], "geo_junctions_mat").then((features) => {
if (features.length > 0) {
const extent = features[0].getGeometry()?.getExtent();
if (extent) {
map?.getView().fit(extent, {
maxZoom: 18,
duration: 1000,
});
const handleLocateSource = (sourceIds: string[]) => {
if (sourceIds.length > 0) {
queryFeaturesByIds(sourceIds, "geo_junctions_mat").then((features) => {
if (features.length > 0) {
// 设置高亮要素
setHighlightFeatures(features);
// 将 OpenLayers Feature 转换为 GeoJSON Feature
const geojsonFormat = new GeoJSON();
const geojsonFeatures = features.map((feature) =>
geojsonFormat.writeFeatureObject(feature),
);
const extent = bbox(featureCollection(geojsonFeatures as any));
if (extent) {
map?.getView().fit(extent, {
maxZoom: 18,
duration: 1000,
});
}
}
}
});
});
}
};
const handleViewDetails = (id: number) => {
@@ -158,17 +258,21 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
if (!scheme) return;
setShowTimeline(true);
const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined;
const schemeDate = scheme.startTime
? new Date(scheme.startTime)
: undefined;
if (scheme.startTime && scheme.schemeDetail?.duration) {
const start = new Date(scheme.startTime);
const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000);
const end = new Date(
start.getTime() + scheme.schemeDetail.duration * 1000,
);
setSelectedDate(schemeDate);
setTimeRange({ start, end });
}
setSchemeName?.(scheme.schemeName);
setJunctionText?.("quality");
setPipeText?.("quality");
handleLocateSource(scheme.schemeDetail?.source);
if (scheme.schemeDetail?.source) {
handleLocateSource([scheme.schemeDetail.source]);
}
};
return (
@@ -183,7 +287,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemeName={schemeName}
schemeType={SCHEME_TYPE}
/>,
mapContainer
mapContainer,
)}
<Box className="flex flex-col h-full">
<Box className="mb-2 p-2 bg-gray-50 rounded">
@@ -291,7 +395,11 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
{scheme.schemeName}
</Typography>
<Chip
label={scheme.type}
label={
scheme.type === "contaminant_analysis"
? "水质模拟"
: scheme.type
}
size="small"
className="h-5"
color="primary"
@@ -302,18 +410,21 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption"
className="text-gray-500 block"
>
ID: {scheme.id} · : {formatTime(scheme.create_time)}
ID: {scheme.id} · :{" "}
{formatTime(scheme.create_time)}
</Typography>
</Box>
<Box className="flex gap-1 ml-2">
<Tooltip
title={expandedId === scheme.id ? "收起详情" : "查看详情"}
title={
expandedId === scheme.id ? "收起详情" : "查看详情"
}
>
<IconButton
size="small"
onClick={() =>
setExpandedId(
expandedId === scheme.id ? null : scheme.id
expandedId === scheme.id ? null : scheme.id,
)
}
color="primary"
@@ -322,16 +433,19 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
<InfoIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="定位污染源">
{/* <Tooltip title="定位污染源">
<IconButton
size="small"
onClick={() => handleLocateSource(scheme.schemeDetail?.source)}
onClick={() =>
scheme.schemeDetail?.source &&
handleLocateSource([scheme.schemeDetail.source])
}
color="primary"
className="p-1"
>
<LocationIcon fontSize="small" />
</IconButton>
</Tooltip>
</Tooltip> */}
</Box>
</Box>
@@ -355,7 +469,9 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
className="font-medium text-blue-600 hover:text-blue-800 underline cursor-pointer"
onClick={(e) => {
e.preventDefault();
handleLocateSource(scheme.schemeDetail?.source);
handleLocateSource([
scheme.schemeDetail!.source!,
]);
}}
>
{scheme.schemeDetail.source}
@@ -381,7 +497,8 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption"
className="font-medium text-gray-900"
>
{scheme.schemeDetail?.concentration ?? "N/A"} mg/L
{scheme.schemeDetail?.concentration ?? "N/A"}{" "}
mg/L
</Typography>
</Box>
<Box className="flex items-center gap-2">
@@ -443,7 +560,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
className="font-medium text-gray-900"
>
{moment(scheme.create_time).format(
"YYYY-MM-DD HH:mm"
"YYYY-MM-DD HH:mm",
)}
</Typography>
</Box>
@@ -459,7 +576,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
className="font-medium text-gray-900"
>
{moment(scheme.startTime).format(
"YYYY-MM-DD HH:mm"
"YYYY-MM-DD HH:mm",
)}
</Typography>
</Box>
@@ -473,7 +590,10 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
fullWidth
size="small"
className="border-blue-600 text-blue-600 hover:bg-blue-50"
onClick={() => handleLocateSource(scheme.schemeDetail?.source)}
onClick={() =>
scheme.schemeDetail?.source &&
handleLocateSource([scheme.schemeDetail.source])
}
sx={{
textTransform: "none",
fontWeight: 500,