"use client"; import React, { useEffect, useMemo, useState } from "react"; import ReactDOM from "react-dom"; import { Box, Button, Typography, Checkbox, FormControlLabel, IconButton, Card, CardContent, Chip, Tooltip, Collapse, Link, } from "@mui/material"; import { Info as InfoIcon, LocationOn as LocationIcon, } from "@mui/icons-material"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; import dayjs, { Dayjs } from "dayjs"; import { api } from "@/lib/api"; import moment from "moment"; import { config, NETWORK_NAME } from "@config/config"; import { useNotification } from "@refinedev/core"; import { useData, useMap } from "@components/olmap/core/MapComponent"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { useControllableObjectState } from "@components/olmap/core/useControllableState"; 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, { FeatureLike } from "ol/Feature"; import { bbox, featureCollection } from "@turf/turf"; import Timeline from "@components/olmap/core/Controls/Timeline"; import { SchemeRecord, SchemaItem } from "./types"; import { FLOW_DISPLAY_UNIT } from "@utils/units"; import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState"; interface SchemeQueryProps { schemes?: SchemeRecord[]; onSchemesChange?: (schemes: SchemeRecord[]) => void; network?: string; state?: FlushingSchemeQueryState; onStateChange?: (state: FlushingSchemeQueryState) => void; } const SCHEME_TYPE = "flushing_analysis"; export interface FlushingSchemeQueryState { queryAll: boolean; queryDate: Dayjs | null; expandedId: number | null; showTimeline: boolean; selectedDate: Date | undefined; timeRange: { start: Date; end: Date } | undefined; hasQueried: boolean; } export const createFlushingSchemeQueryState = (): FlushingSchemeQueryState => ({ queryAll: true, queryDate: dayjs(new Date()), expandedId: null, showTimeline: false, selectedDate: undefined, timeRange: undefined, hasQueried: false, }); const SchemeQuery: React.FC = ({ schemes: externalSchemes, onSchemesChange, network = NETWORK_NAME, state, onStateChange, }) => { const [queryState, setQueryState, setQueryField] = useControllableObjectState( state, onStateChange, createFlushingSchemeQueryState(), ); const { queryAll, queryDate, expandedId, showTimeline, selectedDate, timeRange, hasQueried, } = queryState; const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); const [highlightLayer, setHighlightLayer] = useState | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); const [mapContainer, setMapContainer] = useState(null); const creatorName = useSchemeCreatorName(); const { open } = useNotification(); const map = useMap(); const data = useData(); const { schemeName, setSchemeName } = data || {}; const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const setSchemes = onSchemesChange || setInternalSchemes; const sortedSchemes = useMemo( () => schemes .slice() .sort( (a, b) => moment(b.create_time).valueOf() - moment(a.create_time).valueOf(), ), [schemes], ); useEffect(() => { if (!map) return; const target = map.getTargetElement(); if (target) { setMapContainer(target); } }, [map]); // Initialize highlight layer useEffect(() => { if (!map) return; const themeColor = "rgba(0, 0, 255"; // Blue for drainage const valveColor = "rgba(255, 165, 0"; // Orange for valves const sourceStyle = function (feature: FeatureLike) { const type = (feature as any).get("type"); if (type === "valve") { return [ new Style({ image: new Circle({ radius: 8, fill: new Fill({ color: `${valveColor}, 0.8)` }), stroke: new Stroke({ color: "white", width: 2 }), }), }) ]; } else { // Default drainage return [ 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 Circle({ radius: 4, fill: new Fill({ color: `${themeColor}, 1)` }), stroke: new Stroke({ color: "white", width: 1 }), }) }), ]; } }; const layer = new VectorLayer({ source: new VectorSource(), style: sourceStyle, zIndex: 1000, properties: { name: "FlushingQueryResultHighlight", queryable: false, }, }); map.addLayer(layer); setHighlightLayer(layer); return () => { map.removeLayer(layer); }; }, [map]); // Update highlight features 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 handleLocateDrainageNode = (nodeId: string) => { if (!nodeId) return; queryFeaturesByIds([nodeId], "geo_junctions_mat").then((features) => { if (features.length > 0) { // Add type property to distinguish styling features.forEach(f => f.set("type", "drainage")); setHighlightFeatures(features); zoomToFeatures(features); } else { open?.({ type: "error", message: "未找到该节点要素", }); } }); }; const handleLocateValves = (valveIds: string[]) => { if (!valveIds || valveIds.length === 0) return; queryFeaturesByIds(valveIds, "geo_valves").then((features) => { if (features.length > 0) { features.forEach(f => f.set("type", "valve")); setHighlightFeatures(features); zoomToFeatures(features); } else { open?.({ type: "error", message: "未找到阀门要素", }); } }); }; const zoomToFeatures = (features: 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, padding: [50, 50, 50, 50], }); } }; const formatTime = (timeStr: string) => { return moment(timeStr).format("MM-DD HH:mm"); }; const handleQuery = async () => { if (!queryAll && !queryDate) return; setLoading(true); try { const params: Record = { scheme_type: SCHEME_TYPE, }; if (!queryAll && queryDate) { params.query_date = queryDate.startOf("day").toISOString(); } const response = await api.get(`${config.BACKEND_URL}/api/v1/schemes`, { params, }); let filteredResults = response.data; // Filter by type filteredResults = filteredResults.filter((item: SchemaItem) => item.scheme_type === SCHEME_TYPE); if (!queryAll && queryDate) { const formattedDate = queryDate.format("YYYY-MM-DD"); filteredResults = filteredResults.filter((item: SchemaItem) => { const itemDate = moment(item.create_time).format("YYYY-MM-DD"); return itemDate === formattedDate; }); } const nextSchemes = filteredResults.map((item: SchemaItem) => ({ id: item.scheme_id, schemeName: item.scheme_name, type: item.scheme_type, username: item.username, create_time: item.create_time, startTime: item.scheme_start_time, schemeDetail: item.scheme_detail, })); setSchemes(nextSchemes); setQueryField("hasQueried", true); if (filteredResults.length === 0) { open?.({ type: "success", message: "未找到相关方案", description: "请尝试更改查询条件", }); } else { open?.({ type: "success", message: "查询成功", description: `共找到 ${filteredResults.length} 条方案记录`, }); } } catch (error) { console.error("查询请求失败:", error); open?.({ type: "error", message: "查询失败", description: "获取方案列表失败,请稍后重试", }); } finally { setLoading(false); } }; const handleViewResults = (scheme: SchemeRecord) => { setQueryState((previous) => ({ ...previous, showTimeline: true })); const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined; if (scheme.startTime && scheme.schemeDetail?.duration) { const start = new Date(scheme.startTime); const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000); setQueryState((previous) => ({ ...previous, showTimeline: true, selectedDate: schemeDate, timeRange: { start, end }, })); } setSchemeName?.(scheme.schemeName); // Locate drainage node by default if available if (scheme.schemeDetail?.drainage_node_ID) { handleLocateDrainageNode(scheme.schemeDetail.drainage_node_ID); } }; return ( <> {showTimeline && mapContainer && ReactDOM.createPortal( , mapContainer, )} {/* Query Controls */} { setQueryField("queryAll", e.target.checked); setQueryField("hasQueried", false); }} size="small" /> } label={查询全部} className="m-0" /> { if (value && dayjs.isDayjs(value)) { setQueryField("queryDate", value); setQueryField("hasQueried", false); } }} format="YYYY-MM-DD" disabled={queryAll} slotProps={{ textField: { size: "small", sx: { width: 200 }, }, }} /> {/* Results List */} {sortedSchemes.length === 0 ? ( ) : ( 共 {sortedSchemes.length} 条记录 {sortedSchemes.map((scheme) => ( {scheme.schemeName} 用户: {creatorName(scheme.username)} · 时间:{" "} {formatTime(scheme.create_time)} scheme.schemeDetail?.drainage_node_ID && handleLocateDrainageNode(scheme.schemeDetail.drainage_node_ID) } color="primary" className="p-1" > setQueryField( "expandedId", expandedId === scheme.id ? null : scheme.id, ) } color="primary" className="p-1" > {/* 排水节点 */} 排水节点: {scheme.schemeDetail?.drainage_node_ID ? ( { e.preventDefault(); handleLocateDrainageNode(scheme.schemeDetail!.drainage_node_ID); }} > {scheme.schemeDetail.drainage_node_ID} ) : ( N/A )} {/* 冲洗流量 */} 冲洗流量: {scheme.schemeDetail?.flushing_flow ?? "-"} {FLOW_DISPLAY_UNIT} {/* 持续时长 */} 持续时长: {scheme.schemeDetail?.duration ?? "-"} 秒 {/* 用户 */} 用户: {creatorName(scheme.username)} {/* 创建时间 */} 创建时间: {formatTime(scheme.create_time)} {/* 开始时间 */} 模拟开始: {formatTime(scheme.startTime)} {/* 阀门列表 */} 参与阀门及开度: {scheme.schemeDetail?.valve_opening && Object.entries(scheme.schemeDetail.valve_opening).length > 0 ? ( Object.entries(scheme.schemeDetail.valve_opening).map(([id, k]) => ( handleLocateValves([id])} className="text-xs h-6 bg-gray-50 cursor-pointer hover:bg-orange-50 hover:border-orange-200" /> )) ) : ( )} ))} )} ); }; export default SchemeQuery;