diff --git a/public/icons/burst_pipe_icon.svg b/public/icons/burst_pipe_icon.svg new file mode 100644 index 0000000..524c549 --- /dev/null +++ b/public/icons/burst_pipe_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/(main)/burst-pipe-analysis/page.tsx b/src/app/(main)/burst-pipe-analysis/page.tsx index ebc6433..bdaef6e 100644 --- a/src/app/(main)/burst-pipe-analysis/page.tsx +++ b/src/app/(main)/burst-pipe-analysis/page.tsx @@ -8,7 +8,7 @@ export default function Home() { return (
- {/* */} +
diff --git a/src/app/(main)/network-simulation/page.tsx b/src/app/(main)/network-simulation/page.tsx index f65129e..b44fcca 100644 --- a/src/app/(main)/network-simulation/page.tsx +++ b/src/app/(main)/network-simulation/page.tsx @@ -78,9 +78,7 @@ export default function Home() {
-
- -
+
{ }; return ( -
+
{layers.map((layer, index) => ( = ({ const isImportantKeys = ["ID", "类型", "Name", "面积", "长度"]; return ( -
+
{/* 头部 */}
diff --git a/src/app/OlMap/Controls/StyleEditorPanel.tsx b/src/app/OlMap/Controls/StyleEditorPanel.tsx index 9bce045..f7e5346 100644 --- a/src/app/OlMap/Controls/StyleEditorPanel.tsx +++ b/src/app/OlMap/Controls/StyleEditorPanel.tsx @@ -56,10 +56,13 @@ interface LayerStyleState { legendConfig: LegendStyleConfig; isActive: boolean; } -// 持久化存储 -const STORAGE_KEYS = { - layerStyleStates: "styleEditor_layerStyleStates", -}; + +// StyleEditorPanel 组件 Props 接口 +interface StyleEditorPanelProps { + layerStyleStates: LayerStyleState[]; + setLayerStyleStates: React.Dispatch>; +} + // 预设颜色方案 const SINGLE_COLOR_PALETTES = [ { @@ -108,7 +111,10 @@ const CLASSIFICATION_METHODS = [ // { name: "自然间断", value: "jenks_optimized" }, ]; -const StyleEditorPanel: React.FC = () => { +const StyleEditorPanel: React.FC = ({ + layerStyleStates, + setLayerStyleStates, +}) => { const map = useMap(); const data = useData(); if (!data) { @@ -123,7 +129,6 @@ const StyleEditorPanel: React.FC = () => { setShowPipeText, setJunctionText, setPipeText, - updateLegendConfigs, } = data; const { open, close } = useNotification(); @@ -155,20 +160,7 @@ const StyleEditorPanel: React.FC = () => { opacity: 0.9, adjustWidthByProperty: true, }); - // 样式状态管理 - 存储多个图层的样式状态 - const [layerStyleStates, setLayerStyleStates] = useState( - () => { - const saved = sessionStorage.getItem(STORAGE_KEYS.layerStyleStates); - return saved ? JSON.parse(saved) : []; - } - ); - // 保存layerStyleStates到sessionStorage - useEffect(() => { - sessionStorage.setItem( - STORAGE_KEYS.layerStyleStates, - JSON.stringify(layerStyleStates) - ); - }, [layerStyleStates]); + // 颜色方案选择 const [singlePaletteIndex, setSinglePaletteIndex] = useState(0); const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0); @@ -735,20 +727,6 @@ const StyleEditorPanel: React.FC = () => { } }, [styleConfig.colorType]); - // 获取所有激活的图例配置 - useEffect(() => { - if (!updateLegendConfigs) return; - updateLegendConfigs( - layerStyleStates - .filter((state) => state.isActive && state.legendConfig.property) - .map((state) => ({ - ...state.legendConfig, - layerName: state.layerName, - layerId: state.layerId, - })) - ); - }, [layerStyleStates]); - const getColorSetting = () => { if (styleConfig.colorType === "single") { return ( diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index 135b39b..27982f7 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -28,7 +28,20 @@ import { useData } from "../MapComponent"; import { config } from "@/config/config"; import { useMap } from "../MapComponent"; const backendUrl = config.backendUrl; -const Timeline: React.FC = () => { + +interface TimelineProps { + schemeDate?: Date; + timeRange?: { start: Date; end: Date }; + disableDateSelection?: boolean; + schemeName?: string; +} + +const Timeline: React.FC = ({ + schemeDate, + timeRange, + disableDateSelection = false, + schemeName = "", +}) => { const data = useData(); if (!data) { return
Loading...
; // 或其他占位符 @@ -55,8 +68,20 @@ const Timeline: React.FC = () => { const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(5000); // 毫秒 - const [calculatedInterval, setCalculatedInterval] = useState(1440); // 分钟 + const [calculatedInterval, setCalculatedInterval] = useState(15); // 分钟 + // 计算时间轴范围 + 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]); const intervalRef = useRef(null); const timelineRef = useRef(null); // 添加缓存引用 @@ -82,9 +107,13 @@ const Timeline: React.FC = () => { if (nodeCacheRef.current.has(nodeCacheKey)) { nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!; } else { - nodePromise = fetch( - `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` - ); + disableDateSelection && schemeName + ? (nodePromise = fetch( + `${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${schemeName}` + )) + : (nodePromise = fetch( + `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` + )); requests.push(nodePromise); } } @@ -95,9 +124,13 @@ const Timeline: React.FC = () => { if (linkCacheRef.current.has(linkCacheKey)) { linkRecords = linkCacheRef.current.get(linkCacheKey)!; } else { - linkPromise = fetch( - `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` - ); + disableDateSelection && schemeName + ? (linkPromise = fetch( + `${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${schemeName}` + )) + : (linkPromise = fetch( + `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` + )); requests.push(linkPromise); } } @@ -192,6 +225,10 @@ const Timeline: React.FC = () => { 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); @@ -200,7 +237,7 @@ const Timeline: React.FC = () => { setCurrentTime(value); }, 300); // 300ms 防抖延迟 }, - [] + [timeRange, minTime, maxTime] ); // 播放控制 @@ -217,7 +254,12 @@ const Timeline: React.FC = () => { intervalRef.current = setInterval(() => { setCurrentTime((prev) => { - const next = prev >= 1440 ? 0 : prev + 15; // 到达24:00后回到00:00 + let next = prev + 15; + if (timeRange) { + if (next > maxTime) next = minTime; + } else { + if (next >= 1440) next = 0; + } return next; }); }, playInterval); @@ -261,17 +303,27 @@ const Timeline: React.FC = () => { }, []); const handleStepBackward = useCallback(() => { setCurrentTime((prev) => { - const next = prev <= 0 ? 1440 : prev - 15; + 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) => { - const next = prev >= 1440 ? 0 : prev + 15; + 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) => { @@ -291,7 +343,12 @@ const Timeline: React.FC = () => { clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setCurrentTime((prev) => { - const next = prev >= 1440 ? 0 : prev + 15; + let next = prev + 15; + if (timeRange) { + if (next > maxTime) next = minTime; + } else { + if (next >= 1440) next = 0; + } return next; }); }, newInterval); @@ -333,8 +390,8 @@ const Timeline: React.FC = () => { const currentTime = new Date(); const minutes = currentTime.getHours() * 60 + currentTime.getMinutes(); // 找到最近的前15分钟刻度 - const roundedMinutes = Math.floor(minutes / 15) * 15; - setCurrentTime(roundedMinutes); // 组件卸载时重置时间 + // const roundedMinutes = Math.floor(minutes / 15) * 15; + setCurrentTime(minutes); // 组件卸载时重置时间 return () => { if (intervalRef.current) { @@ -345,6 +402,13 @@ const Timeline: React.FC = () => { } }; }, []); + + // 当 timeRange 改变时,设置 currentTime 到 minTime + useEffect(() => { + if (timeRange) { + setCurrentTime(minTime); + } + }, [timeRange, minTime]); // 获取地图实例 const map = useMap(); // 这里防止地图缩放时,瓦片重新加载引起的属性更新出错 @@ -364,211 +428,260 @@ const Timeline: React.FC = () => { }, [map, handlePause]); return ( - - - - {/* 控制按钮栏 */} - - - - - - - {/* 日期选择器 */} - - handleDateChange( - newValue && "toDate" in newValue - ? newValue.toDate() - : (newValue as Date | null) - ) - } - enableAccessibleFieldDOMStructure={false} - format="yyyy-MM-dd" - sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }} - maxDate={new Date()} // 禁止选取未来的日期 - /> - - - - - - {/* 播放控制按钮 */} - - {/* 播放间隔选择 */} - - 播放间隔 - - - - - - - - - - - - {isPlaying ? : } - - - - - - - - - - - - - - - - - {/* 强制计算时间段 */} - - 计算时间段 - - - - {/* 功能按钮 */} - - - - - - {/* 当前时间显示 */} - + + + + {/* 控制按钮栏 */} + - {formatTime(currentTime)} - - + + + + + + {/* 日期选择器 */} + + handleDateChange( + newValue && "toDate" in newValue + ? newValue.toDate() + : (newValue as Date | null) + ) + } + enableAccessibleFieldDOMStructure={false} + format="yyyy-MM-dd" + sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }} + maxDate={new Date()} // 禁止选取未来的日期 + disabled={disableDateSelection} + /> + + + + + + {/* 播放控制按钮 */} + + {/* 播放间隔选择 */} + + 播放间隔 + + - {/* 时间轴滑块 */} - - index % 12 === 0)} // 每小时显示一个标记 - onChange={handleSliderChange} - valueLabelDisplay="auto" - valueLabelFormat={formatTime} - sx={{ - height: 8, - "& .MuiSlider-track": { - backgroundColor: "primary.main", - height: 6, - }, - "& .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)", + + + + + + + + + {isPlaying ? : } + + + + + + + + + + + + + + + + + {/* 强制计算时间段 */} + + 计算时间段 + + + + {/* 功能按钮 */} + + + + + + {/* 当前时间显示 */} + + {formatTime(currentTime)} + + + + + index % 12 === 0)} // 每小时显示一个标记 + onChange={handleSliderChange} + valueLabelDisplay="auto" + valueLabelFormat={formatTime} + sx={{ + zIndex: 10, + height: 8, + "& .MuiSlider-track": { + backgroundColor: "primary.main", + height: 6, }, - }, - "& .MuiSlider-mark": { - backgroundColor: "grey.400", - height: 4, - width: 2, - }, - "& .MuiSlider-markActive": { - backgroundColor: "primary.main", - }, - "& .MuiSlider-markLabel": { - fontSize: "0.75rem", - color: "grey.600", - }, - }} - /> + "& .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 && ( + + )} + {/* 右侧禁用区域 */} + {maxTime < 1440 && ( + + )} + + )} + - - - + + +
); }; diff --git a/src/app/OlMap/Controls/Toolbar.tsx b/src/app/OlMap/Controls/Toolbar.tsx index 81e7d54..d45b2b1 100644 --- a/src/app/OlMap/Controls/Toolbar.tsx +++ b/src/app/OlMap/Controls/Toolbar.tsx @@ -13,12 +13,49 @@ import { Style, Stroke, Fill, Circle } from "ol/style"; import { FeatureLike } from "ol/Feature"; import Feature from "ol/Feature"; import StyleEditorPanel from "./StyleEditorPanel"; +import StyleLegend from "./StyleLegend"; // 引入图例组件 import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import { config } from "@/config/config"; const backendUrl = config.backendUrl; +// 图层样式状态接口 +interface StyleConfig { + property: string; + classificationMethod: string; + segments: number; + minSize: number; + maxSize: number; + minStrokeWidth: number; + maxStrokeWidth: number; + fixedStrokeWidth: number; + colorType: string; + startColor: string; + endColor: string; + showLabels: boolean; + opacity: number; + adjustWidthByProperty: boolean; +} + +interface LegendStyleConfig { + layerId: string; + layerName: string; + property: string; + colors: string[]; + type: string; + dimensions: number[]; + breaks: number[]; +} + +interface LayerStyleState { + layerId: string; + layerName: string; + styleConfig: StyleConfig; + legendConfig: LegendStyleConfig; + isActive: boolean; +} + // 添加接口定义隐藏按钮的props interface ToolbarProps { hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style'] @@ -38,6 +75,79 @@ const Toolbar: React.FC = ({ hiddenButtons }) => { const [highlightLayer, setHighlightLayer] = useState | null>(null); + // 样式状态管理 - 在 Toolbar 中管理,带有默认样式 + const [layerStyleStates, setLayerStyleStates] = useState([ + { + isActive: false, // 默认不激活,不显示图例 + layerId: "junctions", + layerName: "节点图层", + styleConfig: { + property: "pressure", + classificationMethod: "pretty_breaks", + segments: 6, + minSize: 4, + maxSize: 12, + minStrokeWidth: 2, + maxStrokeWidth: 8, + fixedStrokeWidth: 3, + colorType: "gradient", + startColor: "rgba(51, 153, 204, 0.9)", + endColor: "rgba(204, 51, 51, 0.9)", + showLabels: 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", + startColor: "rgba(51, 153, 204, 0.9)", + endColor: "rgba(204, 51, 51, 0.9)", + showLabels: 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; @@ -73,7 +183,9 @@ const Toolbar: React.FC = ({ hiddenButtons }) => { map.removeLayer(highLightLayer); }; }, [map]); - + useEffect(() => { + console.log(layerStyleStates); + }, [layerStyleStates]); // 高亮要素的函数 useEffect(() => { if (!highlightLayer) { @@ -348,7 +460,23 @@ const Toolbar: React.FC = ({ hiddenButtons }) => {
{showPropertyPanel && } {showDrawPanel && map && } - {showStyleEditor && } + {showStyleEditor && ( + + )} + + {/* 图例显示 */} + {activeLegendConfigs.length > 0 && ( +
+
+ {activeLegendConfigs.map((config, index) => ( + + ))} +
+
+ )} ); }; diff --git a/src/app/OlMap/Controls/Zoom.tsx b/src/app/OlMap/Controls/Zoom.tsx index 4d0bec5..14b89b7 100644 --- a/src/app/OlMap/Controls/Zoom.tsx +++ b/src/app/OlMap/Controls/Zoom.tsx @@ -30,7 +30,7 @@ const Zoom: React.FC = () => { }; return ( -
+
diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index 7f7f491..a10d40f 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -1,6 +1,8 @@ "use client"; import React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom"; // 添加这行 + import { Box, Button, @@ -35,9 +37,12 @@ import * as turf from "@turf/turf"; import { GeoJSON } from "ol/format"; import VectorLayer from "ol/layer/Vector"; import VectorSource from "ol/source/Vector"; -import { Stroke, Style } from "ol/style"; -import Feature from "ol/Feature"; -import { set } from "ol/transform"; +import { Stroke, Style, Icon } from "ol/style"; +import Feature, { FeatureLike } from "ol/Feature"; +import { along, lineString, length, toMercator } from "@turf/turf"; +import { Point } from "ol/geom"; +import { toLonLat } from "ol/proj"; +import Timeline from "@app/OlMap/Controls/Timeline"; interface SchemeDetail { burst_ID: string[]; @@ -72,7 +77,6 @@ interface SchemaItem { interface SchemeQueryProps { schemes?: SchemeRecord[]; onSchemesChange?: (schemes: SchemeRecord[]) => void; - onViewDetails?: (id: number) => void; onLocate?: (id: number) => void; network?: string; } @@ -80,20 +84,29 @@ interface SchemeQueryProps { const SchemeQuery: React.FC = ({ schemes: externalSchemes, onSchemesChange, - onViewDetails, onLocate, network = NETWORK_NAME, }) => { const [queryAll, setQueryAll] = useState(true); const [queryDate, setQueryDate] = useState(dayjs(new Date())); - const [internalSchemes, setInternalSchemes] = useState([]); - const [loading, setLoading] = useState(false); - const [expandedId, setExpandedId] = useState(null); - const { open } = useNotification(); - const [highlightLayer, setHighlightLayer] = useState | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); + + // 时间轴相关状态 + const [showTimeline, setShowTimeline] = useState(false); + const [selectedDate, setSelectedDate] = useState(undefined); + const [timeRange, setTimeRange] = useState< + { start: Date; end: Date } | undefined + >(); + const [internalSchemes, setInternalSchemes] = useState([]); + const [schemeName, setSchemeName] = useState(""); + const [loading, setLoading] = useState(false); + const [expandedId, setExpandedId] = useState(null); + const [mapContainer, setMapContainer] = useState(null); // 地图容器元素 + + const { open } = useNotification(); + const map = useMap(); // 使用外部提供的 schemes 或内部状态 @@ -181,38 +194,95 @@ const SchemeQuery: React.FC = ({ }); } }; + + // 内部的方案查询函数 + const handleViewDetails = (id: number) => { + setShowTimeline(true); + // 计算时间范围 + const scheme = schemes.find((s) => s.id === id); + const burstPipeIds = scheme?.schemeDetail?.burst_ID || []; + const schemeDate = scheme?.startTime + ? new Date(scheme.startTime) + : undefined; + if (scheme?.startTime && scheme.schemeDetail?.modify_total_duration) { + const start = new Date(scheme.startTime); + const end = new Date( + start.getTime() + scheme.schemeDetail.modify_total_duration * 1000 + ); + setSelectedDate(schemeDate); + setTimeRange({ start, end }); + setSchemeName(scheme.schemeName); + handleLocatePipes(burstPipeIds); + } + }; + // 初始化管道图层和高亮图层 useEffect(() => { if (!map) return; - - // 创建高亮图层 - 爆管管段标识样式 - const highlightLayer = new VectorLayer({ - source: new VectorSource(), - style: [ - // 外层发光效果(底层) + // 获取地图的目标容器 + const target = map.getTargetElement(); + if (target) { + setMapContainer(target); + } + const burstPipeStyle = function (feature: FeatureLike) { + const styles = []; + // 线条样式(底层发光,主线条,内层高亮线) + styles.push( new Style({ stroke: new Stroke({ color: "rgba(255, 0, 0, 0.3)", width: 12, }), }), - // 主线条 - 使用虚线表示爆管 new Style({ stroke: new Stroke({ - color: "#ff0000", + color: "rgba(255, 0, 0, 1)", width: 6, - lineDash: [15, 10], // 虚线样式,表示管道损坏/爆管 - }), - }), - // 内层高亮线 - new Style({ - stroke: new Stroke({ - color: "#ff6666", - width: 3, lineDash: [15, 10], }), }), - ], + new Style({ + stroke: new Stroke({ + color: "rgba(255, 102, 102, 1)", + width: 3, + lineDash: [15, 10], + }), + }) + ); + const geometry = feature.getGeometry(); + const lineCoords = + geometry?.getType() === "LineString" + ? (geometry as any).getCoordinates() + : null; + if (geometry) { + const lineCoordsWGS84 = lineCoords.map((coord: []) => { + const [lon, lat] = toLonLat(coord); + return [lon, lat]; + }); + // 计算中点 + const lineStringFeature = lineString(lineCoordsWGS84); + const lineLength = length(lineStringFeature); + const midPoint = along(lineStringFeature, lineLength / 2).geometry + .coordinates; + // 在中点添加 icon 样式 + const midPointMercator = toMercator(midPoint); + styles.push( + new Style({ + geometry: new Point(midPointMercator), + image: new Icon({ + src: "/icons/burst_pipe_icon.svg", + scale: 0.2, + anchor: [0.5, 1], + }), + }) + ); + } + return styles; + }; + // 创建高亮图层 - 爆管管段标识样式 + const highlightLayer = new VectorLayer({ + source: new VectorSource(), + style: burstPipeStyle, properties: { name: "爆管管段高亮", value: "burst_pipe_highlight", @@ -247,348 +317,366 @@ const SchemeQuery: React.FC = ({ }, [highlightFeatures]); return ( - - {/* 查询条件 - 单行布局 */} - - - - setQueryAll(e.target.checked)} - size="small" - /> - } - label={查询全部} - className="m-0" - /> - - - value && dayjs.isDayjs(value) && setQueryDate(value) + <> + {/* 将时间轴渲染到地图容器中 */} + {showTimeline && + mapContainer && + ReactDOM.createPortal( + , + mapContainer // 渲染到地图容器中,而不是 body + )} + + {/* 查询条件 - 单行布局 */} + + + + setQueryAll(e.target.checked)} + size="small" + /> } - format="YYYY-MM-DD" - disabled={queryAll} - slotProps={{ - textField: { - size: "small", - sx: { width: 200 }, - }, - }} + label={查询全部} + className="m-0" /> - - - - - - - {/* 结果列表 */} - - {schemes.length === 0 ? ( - - - - + value && dayjs.isDayjs(value) && setQueryDate(value) + } + format="YYYY-MM-DD" + disabled={queryAll} + slotProps={{ + textField: { + size: "small", + sx: { width: 200 }, + }, + }} /> - - + - 总共 0 条 - - No data - + - ) : ( - - - 共 {schemes.length} 条记录 - - {schemes.map((scheme) => ( - - - {/* 主要信息行 */} - - - - - {scheme.schemeName} - - - - - ID: {scheme.id} · 日期: {formatTime(scheme.create_time)} - - - {/* 操作按钮 */} - - - - setExpandedId( - expandedId === scheme.id ? null : scheme.id - ) - } - color="primary" - className="p-1" - > - - - - - onLocate?.(scheme.id)} - color="primary" - className="p-1" - > - - - - - + - {/* 可折叠的详细信息 */} - - - {/* 信息网格布局 */} - - {/* 爆管详情列 */} - - - - - 管段ID: - - - {scheme.schemeDetail?.burst_ID?.length ? ( - scheme.schemeDetail.burst_ID.map( - (pipeId, index) => ( - { - e.preventDefault(); - handleLocatePipes?.([pipeId]); - }} - > - {pipeId} - + {/* 结果列表 */} + + {schemes.length === 0 ? ( + + + + + + + + 总共 0 条 + + No data + + + ) : ( + + + 共 {schemes.length} 条记录 + + {schemes.map((scheme) => ( + + + {/* 主要信息行 */} + + + + + {scheme.schemeName} + + + + + ID: {scheme.id} · 日期:{" "} + {formatTime(scheme.create_time)} + + + {/* 操作按钮 */} + + + + setExpandedId( + expandedId === scheme.id ? null : scheme.id + ) + } + color="primary" + className="p-1" + > + + + + + onLocate?.(scheme.id)} + color="primary" + className="p-1" + > + + + + + + + {/* 可折叠的详细信息 */} + + + {/* 信息网格布局 */} + + {/* 爆管详情列 */} + + + + + 管段ID: + + + {scheme.schemeDetail?.burst_ID?.length ? ( + scheme.schemeDetail.burst_ID.map( + (pipeId, index) => ( + { + e.preventDefault(); + handleLocatePipes?.([pipeId]); + }} + > + {pipeId} + + ) ) - ) - ) : ( - - N/A - - )} + ) : ( + + N/A + + )} + + + + + 管径: + + + 560 mm + + + + + 爆管面积: + + + {scheme.schemeDetail?.burst_size?.[0] || + "N/A"}{" "} + cm² + + + + + 持续时间: + + + {scheme.schemeDetail?.modify_total_duration || + "N/A"}{" "} + 秒 + - - - 管径: - - - 560 mm - - - - - 爆管面积: - - - {scheme.schemeDetail?.burst_size?.[0] || "N/A"}{" "} - cm² - - - - - 持续时间: - - - {scheme.schemeDetail?.modify_total_duration || - "N/A"}{" "} - 秒 - + + + {/* 方案信息列 */} + + + + + 用户: + + + {scheme.user} + + + + + 创建时间: + + + {moment(scheme.create_time).format( + "YYYY-MM-DD HH:mm" + )} + + + + + 开始时间: + + + {moment(scheme.startTime).format( + "YYYY-MM-DD HH:mm" + )} + + - {/* 方案信息列 */} - - - - - 用户: - - - {scheme.user} - - - - - 创建时间: - - - {moment(scheme.create_time).format( - "YYYY-MM-DD HH:mm" - )} - - - - - 开始时间: - - - {moment(scheme.startTime).format( - "YYYY-MM-DD HH:mm" - )} - - - - - - - {/* 操作按钮区域 */} - - {scheme.schemeDetail?.burst_ID?.length ? ( + {/* 操作按钮区域 */} + + {scheme.schemeDetail?.burst_ID?.length ? ( + + ) : null} - ) : null} - + - - - - - ))} - - )} + + + + ))} + + )} + - + ); }; diff --git a/src/components/olmap/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysisPanel.tsx index 952dffd..5321b05 100644 --- a/src/components/olmap/BurstPipeAnalysisPanel.tsx +++ b/src/components/olmap/BurstPipeAnalysisPanel.tsx @@ -12,7 +12,6 @@ import { import AnalysisParameters from "./BurstPipeAnalysis/AnalysisParameters"; import SchemeQuery from "./BurstPipeAnalysis/SchemeQuery"; import LocationResults from "./BurstPipeAnalysis/LocationResults"; - interface SchemeDetail { burst_ID: string[]; burst_size: number[]; @@ -81,7 +80,6 @@ const BurstPipeAnalysisPanel: React.FC = ({ const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { setCurrentTab(newValue); }; - const drawerWidth = 520; return ( @@ -89,7 +87,7 @@ const BurstPipeAnalysisPanel: React.FC = ({ {/* 收起时的触发按钮 */} {!isOpen && ( @@ -205,10 +203,6 @@ const BurstPipeAnalysisPanel: React.FC = ({ { - console.log("查看详情:", id); - // TODO: 显示方案详情 - }} onLocate={(id) => { console.log("定位方案:", id); // TODO: 在地图上定位 diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index 81a96c0..73ff8c5 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -356,7 +356,7 @@ const SCADADataPanel: React.FC = ({ {/* Header */}